import { useEffect, useMemo, useRef, useState } from "react"; import { closeSession } from "./api"; import { driverById } from "./catalog"; import { askInput } from "./dialogs"; import { DriverIcon } from "./DriverIcon"; import { ObjectTree } from "./ObjectTree"; import { TreeChevron } from "./TreeChevron"; import { DEFAULT_CONN_GROUP, useWorkspaceStore, type SavedConnection, } from "./store"; function cx(...xs: Array) { return xs.filter(Boolean).join(" "); } type DropHint = { groupId: string; index: number } | null; export function ConnectionSidebar({ onSelect, onDelete, }: { onSelect: (id: string) => void; onDelete: (id: string) => void; }) { const connections = useWorkspaceStore((s) => s.connections); const groups = useWorkspaceStore((s) => s.connGroups); const expanded = useWorkspaceStore((s) => s.expandedConnGroups); const activeId = useWorkspaceStore((s) => s.activeConnectionId); const addConnGroup = useWorkspaceStore((s) => s.addConnGroup); const renameConnGroup = useWorkspaceStore((s) => s.renameConnGroup); const removeConnGroup = useWorkspaceStore((s) => s.removeConnGroup); const moveConnection = useWorkspaceStore((s) => s.moveConnection); const toggleConnGroup = useWorkspaceStore((s) => s.toggleConnGroup); const notify = useWorkspaceStore((s) => s.notify); const editConnection = useWorkspaceStore((s) => s.editConnection); const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; conn: SavedConnection } | null>(null); /** 断开连接:关会话 + 清树 + 清激活 */ const disconnect = (c: SavedConnection) => { const st = useWorkspaceStore.getState(); const sid = st.sessions[c.id]; if (sid) { void closeSession(sid).catch(() => {}); } st.setSession(c.id, null); if (st.activeConnectionId === c.id) { st.set("activeConnectionId", null); st.set("tree", []); st.set("connState", "down"); st.set("status", `已断开 ${c.name}`); } }; const [dragId, setDragId] = useState(null); const [dropHint, setDropHint] = useState(null); const dragIdRef = useRef(null); const byGroup = useMemo(() => { const map = new Map(); for (const g of groups) map.set(g.id, []); for (const c of connections) { const gid = c.groupId || DEFAULT_CONN_GROUP; if (!map.has(gid)) map.set(gid, []); map.get(gid)!.push(c); } for (const list of map.values()) { list.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)); } return map; }, [connections, groups]); const onAddGroup = () => { void (async () => { // C6:askInput 替代 window.prompt(WKWebView 下 prompt 返回 null) const name = await askInput({ title: "分组名称", defaultValue: "新分组" }); if (name == null) return; const id = addConnGroup(name); notify({ kind: "info", title: "分组", message: `已创建「${name.trim() || "新分组"}」`, mode: "toast" }); void id; })(); }; const onRenameGroup = (id: string, cur: string) => { if (id === DEFAULT_CONN_GROUP) return; void (async () => { const name = await askInput({ title: "重命名分组", defaultValue: cur }); if (name == null || !name.trim()) return; renameConnGroup(id, name); })(); }; const onRemoveGroup = (id: string, name: string) => { if (id === DEFAULT_CONN_GROUP) return; void (async () => { const ok = await useWorkspaceStore.getState().askConfirm({ title: "删除分组", message: `删除分组「${name}」?连接将移回默认。`, okLabel: "删除", danger: true, }); if (!ok) return; removeConnGroup(id); })(); }; const setDrop = (groupId: string, index: number) => { const id = dragIdRef.current; if (!id) return; setDropHint({ groupId, index }); }; const commitDrop = () => { const id = dragIdRef.current; const hint = dropHint; dragIdRef.current = null; setDragId(null); setDropHint(null); if (!id || !hint) return; moveConnection(id, hint.groupId, hint.index); }; return (
连接
{connections.length === 0 && (
暂无连接
)} {groups.map((g) => { const list = byGroup.get(g.id) ?? []; const open = expanded.includes(g.id); return (
{ if (!dragIdRef.current) return; e.preventDefault(); setDrop(g.id, list.length); }} onDrop={(e) => { e.preventDefault(); commitDrop(); }} >
toggleConnGroup(g.id)}> {open ? "▾" : "▸"} {g.name} {list.length} {g.id !== DEFAULT_CONN_GROUP && ( e.stopPropagation()}> )}
{open && (
{list.length === 0 && (
{ e.preventDefault(); setDrop(g.id, 0); }} > 拖到此处
)} {list.map((c, i) => (
{dropHint?.groupId === g.id && dropHint.index === i &&
}
{ dragIdRef.current = c.id; setDragId(c.id); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", c.id); }} onDragEnd={() => { dragIdRef.current = null; setDragId(null); setDropHint(null); }} onDragOver={(e) => { if (!dragIdRef.current || dragIdRef.current === c.id) return; e.preventDefault(); e.stopPropagation(); const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const before = e.clientY < rect.top + rect.height / 2; setDrop(g.id, before ? i : i + 1); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); commitDrop(); }} >
{activeId === c.id && } {dropHint?.groupId === g.id && dropHint.index === list.length && i === list.length - 1 && (
)}
))}
)}
); })}
{ctxMenu && (
{activeId === ctxMenu.conn.id && ( )}
)}
); }