diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index 751ec50..b6b0eaa 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -406,6 +406,14 @@ modules: note: "N1-N5 全部完成(2026-07-02):六组导航 + 卡片分层 + 洞察页 + Gitea/仓库归位" position: [3750, 300] + - id: manage-multi-view + name: 管理页多维表视图 + area: frontend + status: planned + progress: 0 + note: "项目(默认A+可选B)& 产品组(默认F+可选G)四视图,多对一关系,用户可新建" + position: [4000, 0] + edges: # 前端依赖 - from: workspace-mgmt @@ -608,6 +616,14 @@ edges: to: source-library type: dependency + # 管理页多维表视图 + - from: project-mgmt + to: manage-multi-view + type: dependency + - from: tags-groups + to: manage-multi-view + type: dependency + # 云产品扩展 - from: cloud-products to: cloud-docker-apps diff --git a/.blueprint/modules/manage-multi-view.md b/.blueprint/modules/manage-multi-view.md new file mode 100644 index 0000000..f86e49f --- /dev/null +++ b/.blueprint/modules/manage-multi-view.md @@ -0,0 +1,58 @@ +--- +id: manage-multi-view +name: 管理页多维表视图 +status: planned +progress: 0 +--- + +## 概述 + +为"管理 → 项目"和"管理 → 产品组"两个实体分别提供多维表视图。 +数据关系:多个项目对应一个产品组(多对一)。 + +视图设计原则: +- 默认视图:系统预置,开箱即用,同时作为用户理解数据关系的教材 +- 新建视图:用户在默认视图基础上选择预定义模板,按需构建个性化视角 +- 关系表达分工:层级关系由导航路径承载;量的关系由视图的关联字段/分组列承载 + +## 视图配置 + +### 项目侧 + +| 类型 | 视图名 | 分组方式 | 核心字段 | +|------|--------|---------|---------| +| 默认 | 全览表格(A) | 无分组,按名称排序 | 项目名、产品组、状态、负责人 | +| 可新建 | 按产品组分组(B) | 分组=产品组,含分组计数 | 项目名、状态、负责人 | + +### 产品组侧 + +| 类型 | 视图名 | 核心字段 | +|------|--------|---------| +| 默认 | 产品组概览(F) | 组名、项目数量(汇总)、负责人 | +| 可新建 | 产品组健康度(G) | 组名、进行中数量、已完成数量、搁置数量 | + +## 任务卡 + +### 📋 项目默认视图:全览表格(A) +- complexity: M +- status: todo +- files: src/components/manage/ProjectListView.tsx +- acceptance: [UI] 打开"管理→项目",默认渲染全览表格,展示"项目名、产品组、状态、负责人"四列,无分组,按名称排序,行数与数据库一致 + +### 📋 产品组默认视图:产品组概览(F) +- complexity: M +- status: todo +- files: src/components/manage/ProductGroupView.tsx +- acceptance: [UI] 打开"管理→产品组",默认渲染概览表格,展示"组名、项目数量(汇总)、负责人"三列,项目数量值与实际关联记录数一致 + +### 📋 用户新建视图:项目侧(B) +- complexity: M +- status: todo +- files: src/components/manage/ProjectListView.tsx +- acceptance: [UI] 点击"新建视图",可选"按产品组分组"模板,创建后表格按产品组分组展示,分组头显示该组项目数量 + +### 📋 用户新建视图:产品组侧(G) +- complexity: M +- status: todo +- files: src/components/manage/ProductGroupView.tsx +- acceptance: [UI] 点击"新建视图",可选"产品组健康度"模板,创建后展示"组名、进行中、已完成、搁置"四列数量汇总 diff --git a/src/components/manage/ManagePage.tsx b/src/components/manage/ManagePage.tsx index 7cb17d2..9e89f09 100644 --- a/src/components/manage/ManagePage.tsx +++ b/src/components/manage/ManagePage.tsx @@ -64,11 +64,22 @@ function ProjectsTab({ spaceId }: { spaceId?: string }) { 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; @@ -79,11 +90,29 @@ function ProjectsTab({ spaceId }: { spaceId?: string }) { showToast("已删除"); }; + const PROJECT_VIEWS = [ + { key: "all", label: "全览表格" }, + { key: "by-group", label: "按产品组分组" }, + ] as const; + return ( <>

共 {projects.length} 个项目

-
+
+
+ {PROJECT_VIEWS.map(({ key, label }) => ( + + ))} +
-
- - - - {["名称 / 描述", "类型", "状态", "优先级", "技术栈", "本地路径", "操作"].map((h) => ( - - ))} - - - - {projects.map((p) => ( - - - - - - - - + {/* 全览表格视图 */} + {view === "all" && ( +
+
- {h} -
-
{p.name}
- {p.description && ( -
{p.description}
- )} -
- {p.type && ( - - {p.type} - - )} - -
- {(p.tech_stack ?? "").split(",").filter(Boolean).map((t) => ( - {t.trim()} - ))} -
-
-
- {p.wsl_path && ( -
- WSL - {p.wsl_path} -
- )} - {p.win_path && ( -
- WIN - {p.win_path} -
- )} -
-
-
- - -
-
+ + + {["名称 / 描述", "产品组", "状态", "优先级", "技术栈", "本地路径", "操作"].map((h) => ( + + ))} - ))} - -
+ {h} +
- {projects.length === 0 && ( -
暂无项目
- )} -
+ + + {projects.map((p) => { + const groupNames = getProjectGroupNames(p.id); + return ( + + +
{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"> @@ -204,6 +346,7 @@ function ProjectsTab({ spaceId }: { spaceId?: string }) { 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); @@ -250,130 +393,233 @@ function GroupsTab({ spaceId }: { spaceId?: string }) { 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 }) => ( + + ))} +
+ +
-
- {groups.map((g, idx) => { - const members = maps.filter((m) => m.group_id === g.id); - return ( -
-
-
- {/* 排序按钮 */} -
- - + {/* 产品组概览视图 */} + {groupView === "overview" && ( +
+ + + + {["产品组名称", "项目数量", "描述", "操作"].map((h) => ( + + ))} + + + + {groups.map((g) => { + const count = maps.filter((m) => m.group_id === g.id).length; + return ( + + + + + + + ); + })} + +
+ {h} +
+
{g.name}
+
{g.id}
+
+ + {count} + + + {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} + )}
- {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 && ( + 暂无成员,点击「+ 成员」添加 )}
-
- - - - -
-
- {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"} - - )} - {/* MCP 注入状态 */} - {hasPath ? ( - - - MCP - - ) : ( - - 无路径 - - )} - {m.role && {m.role}} - -
- ) : null; - })} - {members.length === 0 && ( - 暂无成员,点击「+ 成员」添加 - )} -
-
- ); - })} - {groups.length === 0 && ( -
暂无产品组
- )} -
+ ); + })} + {groups.length === 0 && ( +
暂无产品组
+ )} +
+ )} - {/* Group modal */} {groupModal !== undefined && ( )} - - {/* Add member modal */} {memberTarget && ( )} - {delGroup && ( setDelGroup(null)} size="sm">