import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { deleteProject, getGroups, getGroupMaps, getProjects, getSpaces, deleteGroup, addGroupMember, removeGroupMember, createGroup, updateGroup, reorderGroups, listTemplates, deleteTemplate, getGroupMentorReport, type ProductGroup, type Project, type ProjectTemplate, } from "../../lib/commands"; import { useUIStore } from "../../store/ui"; import { PriorityBadge, StatusBadge } from "../ui/Badge"; import { Dialog } from "../ui/Dialog"; import { TemplateEditor } from "./TemplateEditor"; import { NewProjectModal } from "./NewProjectModal"; import { renderTechStack } from "./techStackUtils"; import { TagsManager } from "./TagsManager"; import { RegistryTab } from "../dashboard/RepoRegistryModal"; export function ManagePage() { const [tab, setTab] = useState<"projects" | "groups" | "templates" | "tags" | "repos">("projects"); const { data: spaces = [] } = useQuery({ queryKey: ["spaces"], queryFn: getSpaces }); const currentSpaceId = spaces.find((s) => s.is_current)?.id; const TAB_LABELS = { projects: "项目", groups: "产品组", templates: "项目模板", tags: "标签", repos: "仓库" }; return (
{(["projects", "groups", "templates", "tags", "repos"] as const).map((t) => ( ))}
{tab === "projects" && } {tab === "groups" && } {tab === "templates" && } {tab === "tags" && } {tab === "repos" && (
)}
); } /* ── Projects Tab ──────────────────────────────────────────────────────────── */ function ProjectsTab({ spaceId }: { spaceId?: string }) { const openEdit = useUIStore((s) => s.openEdit); const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const [delTarget, setDelTarget] = useState(null); const [deleteLocal, setDeleteLocal] = useState(false); const [showNewProject, setShowNewProject] = useState(false); const [view, setView] = useState<"all" | "by-group">("all"); const { data: projects = [] } = useQuery({ queryKey: ["projects", spaceId], queryFn: () => getProjects(undefined, spaceId), }); const { data: groups = [] } = useQuery({ queryKey: ["groups", spaceId], queryFn: () => getGroups(spaceId), }); const { data: maps = [] } = useQuery({ queryKey: ["maps"], queryFn: getGroupMaps }); const getProjectGroupNames = (projectId: string): string[] => { const groupIds = maps.filter((m) => m.project_id === projectId).map((m) => m.group_id); return groupIds.map((id) => groups.find((g) => g.id === id)?.name ?? "").filter(Boolean); }; const confirmDelete = async () => { if (!delTarget) return; await deleteProject(delTarget, deleteLocal); setDelTarget(null); setDeleteLocal(false); qc.invalidateQueries({ queryKey: ["projects"] }); showToast("已删除"); }; const PROJECT_VIEWS = [ { key: "all", label: "全览表格" }, { key: "by-group", label: "按产品组分组" }, ] as const; return ( <>

共 {projects.length} 个项目

{PROJECT_VIEWS.map(({ key, label }) => ( ))}
{/* 全览表格视图 */} {view === "all" && (
{["名称 / 描述", "产品组", "状态", "优先级", "技术栈", "本地路径", "操作"].map((h) => ( ))} {projects.map((p) => { const groupNames = getProjectGroupNames(p.id); return ( ); })}
{h}
{p.name}
{p.description && (
{p.description}
)}
{groupNames.length > 0 ? groupNames.map((gName) => ( {gName} )) : ( )}
{(p.tech_stack ?? "").split(",").filter(Boolean).map((t) => ( {t.trim()} ))}
{p.wsl_path && (
WSL {p.wsl_path}
)} {p.win_path && (
WIN {p.win_path}
)}
{projects.length === 0 && (
暂无项目
)}
)} {/* 按产品组分组视图 */} {view === "by-group" && (
{groups.map((g) => { const groupProjects = maps .filter((m) => m.group_id === g.id) .map((m) => projects.find((p) => p.id === m.project_id)) .filter((p): p is Project => p !== undefined); return (
{g.name} {groupProjects.length} {g.description && ( {g.description} )}
{["名称 / 描述", "状态", "优先级", "操作"].map((h) => ( ))} {groupProjects.map((p) => ( ))} {groupProjects.length === 0 && ( )}
{h}
{p.name}
{p.description && (
{p.description}
)}
暂无项目
); })} {(() => { const ungrouped = projects.filter((p) => !maps.some((m) => m.project_id === p.id)); if (ungrouped.length === 0) return null; return (
未分组 {ungrouped.length}
{["名称 / 描述", "状态", "优先级", "操作"].map((h) => ( ))} {ungrouped.map((p) => ( ))}
{h}
{p.name}
{p.description &&
{p.description}
}
); })()} {groups.length === 0 && projects.length === 0 && (
暂无产品组,请先创建产品组并添加成员
)}
)} {delTarget && ( { setDelTarget(null); setDeleteLocal(false); }} size="sm">

删除后不可恢复,关联记录也会一并删除。

{delTarget}

)} {showNewProject && ( setShowNewProject(false)} /> )} ); } /* ── Groups Tab ────────────────────────────────────────────────────────────── */ function GroupsTab({ spaceId }: { spaceId?: string }) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const [groupView, setGroupView] = useState<"overview" | "health" | "detail">("overview"); const [groupModal, setGroupModal] = useState(undefined); const [memberTarget, setMemberTarget] = useState(null); const [delGroup, setDelGroup] = useState(null); const [inspecting, setInspecting] = useState>(new Set()); const handleInspect = async (groupId: string) => { setInspecting((prev) => new Set(prev).add(groupId)); try { const report = await getGroupMentorReport(groupId); await navigator.clipboard.writeText(report); showToast("已复制,可粘贴给 Claude 进行全组分析"); } catch (e) { showToast(`❌ 巡检失败: ${e}`); } finally { setInspecting((prev) => { const s = new Set(prev); s.delete(groupId); return s; }); } }; const { data: groups = [] } = useQuery({ queryKey: ["groups", spaceId], queryFn: () => getGroups(spaceId), }); const { data: maps = [] } = useQuery({ queryKey: ["maps"], queryFn: getGroupMaps }); const { data: projects = [] } = useQuery({ queryKey: ["projects", spaceId], queryFn: () => getProjects(undefined, spaceId), }); const confirmDel = async () => { if (!delGroup) return; await deleteGroup(delGroup); setDelGroup(null); qc.invalidateQueries({ queryKey: ["groups", spaceId] }); qc.invalidateQueries({ queryKey: ["maps"] }); showToast("已删除"); }; const move = async (index: number, dir: -1 | 1) => { const next = index + dir; if (next < 0 || next >= groups.length) return; const ids = groups.map((g) => g.id); [ids[index], ids[next]] = [ids[next], ids[index]]; await reorderGroups(ids); qc.invalidateQueries({ queryKey: ["groups", spaceId] }); }; const GROUP_VIEWS = [ { key: "overview", label: "产品组概览" }, { key: "health", label: "健康度" }, { key: "detail", label: "成员详情" }, ] as const; const renderGroupActions = (g: ProductGroup) => (
); return ( <>

共 {groups.length} 个产品组

{GROUP_VIEWS.map(({ key, label }) => ( ))}
{/* 产品组概览视图 */} {groupView === "overview" && (
{["产品组名称", "项目", "描述", "操作"].map((h) => ( ))} {groups.map((g) => { const groupProjectEntries = maps .filter((m) => m.group_id === g.id) .map((m) => { const p = projects.find((x) => x.id === m.project_id); return p ? { id: p.id, name: p.name } : null; }) .filter((e): e is { id: string; name: string } => e !== null); return ( ); })}
{h}
{g.name}
{g.id}
{groupProjectEntries.length > 0 ? groupProjectEntries.map(({ id, name }) => ( {name} )) : ( )}
{g.description ?? "—"}
{renderGroupActions(g)}
{groups.length === 0 && (
暂无产品组
)}
)} {/* 健康度视图 */} {groupView === "health" && (
{["产品组名称", "进行中", "维护中", "暂停", "已归档", "合计"].map((h) => ( ))} {groups.map((g) => { const groupProjectIds = maps.filter((m) => m.group_id === g.id).map((m) => m.project_id); const gps = groupProjectIds .map((id) => projects.find((p) => p.id === id)) .filter((p): p is Project => p !== undefined); const counts = { active: gps.filter((p) => p.status === "active").length, maintenance: gps.filter((p) => p.status === "maintenance").length, paused: gps.filter((p) => p.status === "paused").length, archived: gps.filter((p) => p.status === "archived").length, }; return ( ); })}
{h}
{g.name} {counts.active > 0 ? {counts.active} : } {counts.maintenance > 0 ? {counts.maintenance} : } {counts.paused > 0 ? {counts.paused} : } {counts.archived > 0 ? {counts.archived} : } {Object.values(counts).reduce((a, b) => a + b, 0)}
{groups.length === 0 && (
暂无产品组
)}
)} {/* 成员详情视图 */} {groupView === "detail" && (
{groups.map((g, idx) => { const members = maps.filter((m) => m.group_id === g.id); return (
{g.name} {g.id} {g.description && ( {g.description} )}
{renderGroupActions(g)}
{members.map((m) => { const p = projects.find((x) => x.id === m.project_id); const hasPath = !!(p?.win_path || p?.wsl_path); return p ? (
{p.name} {p.platform && ( {p.platform === "wsl" ? "WSL" : "Win"} )} {hasPath ? ( MCP ) : ( 无路径 )} {m.role && {m.role}}
) : null; })} {members.length === 0 && ( 暂无成员,点击「+ 成员」添加 )}
); })} {groups.length === 0 && (
暂无产品组
)}
)} {groupModal !== undefined && ( setGroupModal(undefined)} onSave={() => { setGroupModal(undefined); qc.invalidateQueries({ queryKey: ["groups", spaceId] }); showToast(groupModal ? "已更新" : "已创建"); }} /> )} {memberTarget && ( m.group_id === memberTarget.id).map((m) => m.project_id))} onClose={() => setMemberTarget(null)} onSave={() => { setMemberTarget(null); qc.invalidateQueries({ queryKey: ["maps"] }); showToast("成员已添加"); }} /> )} {delGroup && ( setDelGroup(null)} size="sm">

删除产品组不会删除项目,但会移除所有成员关系。

)} ); } function GroupFormModal({ group, spaceId, onClose, onSave }: { group: ProductGroup | null; spaceId?: string; onClose: () => void; onSave: () => void; }) { const showToast = useUIStore((s) => s.showToast); const [form, setForm] = useState({ id: group?.id ?? "", name: group?.name ?? "", description: group?.description ?? "" }); const save = async () => { if (!form.name.trim()) { showToast("❌ 名称不能为空"); return; } if (!group && !form.id.trim()) { showToast("❌ ID 不能为空"); return; } if (group) await updateGroup(group.id, { name: form.name, description: form.description || undefined }); else await createGroup({ id: form.id, name: form.name, description: form.description || undefined, space_id: spaceId }); onSave(); }; return (
{!group && (
setForm((f) => ({ ...f, id: e.target.value }))} />
)}
setForm((f) => ({ ...f, name: e.target.value }))} />
setForm((f) => ({ ...f, description: e.target.value }))} />
); } function AddMemberModal({ group, projects, existingIds, onClose, onSave }: { group: ProductGroup; projects: Project[]; existingIds: Set; onClose: () => void; onSave: () => void; }) { const showToast = useUIStore((s) => s.showToast); const [selected, setSelected] = useState(""); const [role, setRole] = useState(""); const available = projects.filter((p) => !existingIds.has(p.id)); const save = async () => { if (!selected) return; try { const warning = await addGroupMember(selected, group.id, role || undefined); if (warning) { showToast(`成员已添加,但 MCP 注入失败:${warning}`); } onSave(); } catch (e) { showToast(`❌ 添加失败: ${e}`); } }; return (
setRole(e.target.value)} placeholder="主服务 / 前端 / SDK…" />
); } /* ── Templates Tab ─────────────────────────────────────────────────────────── */ function TemplatesTab({ spaceId: _spaceId }: { spaceId?: string }) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const [editorTarget, setEditorTarget] = useState(undefined); const [delTarget, setDelTarget] = useState(null); const { data: templates = [] } = useQuery({ queryKey: ["templates"], queryFn: listTemplates, }); const handleDelete = async () => { if (!delTarget) return; try { await deleteTemplate(delTarget.id); qc.invalidateQueries({ queryKey: ["templates"] }); showToast("已删除"); } catch (e) { showToast(`❌ ${e}`); } finally { setDelTarget(null); } }; const PLATFORM_COLOR: Record = { both: "bg-gray-100 text-gray-500 border-gray-200", win: "bg-blue-50 text-blue-600 border-blue-200", wsl: "bg-green-50 text-green-600 border-green-200", }; const PLATFORM_LABEL: Record = { both: "通用", win: "Windows", wsl: "WSL" }; return ( <>

共 {templates.length} 个模板

{templates.map((t) => (
{t.name} {t.isBuiltin && ( 内置 )} {PLATFORM_LABEL[t.platform] ?? t.platform}
{t.description && (

{t.description}

)}
{!t.isBuiltin && ( )}
{t.techStack && renderTechStack(t.techStack)} {t.initCommands && ( $ {t.initCommands.split("\n")[0]} )}
))} {templates.length === 0 && (
暂无模板
)}
{editorTarget !== undefined && ( setEditorTarget(undefined)} /> )} {delTarget && ( setDelTarget(null)} size="sm">

删除「{delTarget.name}」后不可恢复。

)} ); }