diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index d9854e8..ed52ed8 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Search, FileText, Palette, ClipboardList, Code2, ClipboardCheck, Bug as BugIcon, ExternalLink, CheckCircle2 } from 'lucide-react'; +import { Search, FileText, Palette, ClipboardList, Code2, ClipboardCheck, Bug as BugIcon, ExternalLink, CheckCircle2, ChevronRight, ChevronDown, FolderOpen, Layers } from 'lucide-react'; import { useProductStore } from '@/stores/useProductStore'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; @@ -47,6 +47,7 @@ export default function WorkspacePage() { const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('all'); const [showCompleted, setShowCompleted] = useState(true); + const [selectedVersionId, setSelectedVersionId] = useState(null); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchPlans(); }, [fetchPlans]); @@ -75,70 +76,121 @@ export default function WorkspacePage() { [userName, plans, devTasks, testCases, bugs, versionMap, requirementVersionMap] ); - const filteredItems = useMemo(() => { - let items = activeTab === 'all' ? workItems : workItems.filter((i) => i.type === activeTab); - if (!showCompleted) items = items.filter((i) => !i.completed); - return items.sort((a, b) => { - if (a.completed !== b.completed) return a.completed ? 1 : -1; - return 0; + // 构建树:只显示跟自己有关的产品/项目/版本 + const tree = useMemo(() => { + const myVersionIds = new Set(workItems.map((i) => i.versionId).filter(Boolean)); + const productMap = new Map }>(); + + allVersions.forEach((v) => { + if (!myVersionIds.has(v.id)) return; + if (!productMap.has(v.productName)) productMap.set(v.productName, { name: v.productName, projects: new Map() }); + const prod = productMap.get(v.productName)!; + if (!prod.projects.has(v.projectName)) prod.projects.set(v.projectName, { name: v.projectName, versions: [] }); + const proj = prod.projects.get(v.projectName)!; + const pending = workItems.filter((i) => i.versionId === v.id && !i.completed).length; + if (!proj.versions.find((ver) => ver.id === v.id)) { + proj.versions.push({ id: v.id, name: v.name, pendingCount: pending }); + } }); - }, [workItems, activeTab, showCompleted]); - const counts = useMemo(() => ({ - all: workItems.length, - plan_research: workItems.filter((i) => i.type === 'plan_research').length, - plan_product: workItems.filter((i) => i.type === 'plan_product').length, - plan_ui: workItems.filter((i) => i.type === 'plan_ui').length, - devTask: workItems.filter((i) => i.type === 'devTask').length, - testCase: workItems.filter((i) => i.type === 'testCase').length, - bug: workItems.filter((i) => i.type === 'bug').length, - }), [workItems]); + return productMap; + }, [allVersions, workItems]); - const pendingCount = workItems.filter((i) => !i.completed).length; - const completedCount = workItems.filter((i) => i.completed).length; + const filteredItems = useMemo(() => { + let items = workItems; + if (selectedVersionId) items = items.filter((i) => i.versionId === selectedVersionId); + if (activeTab !== 'all') items = items.filter((i) => i.type === activeTab); + if (!showCompleted) items = items.filter((i) => !i.completed); + return items.sort((a, b) => (a.completed !== b.completed ? (a.completed ? 1 : -1) : 0)); + }, [workItems, activeTab, showCompleted, selectedVersionId]); + + // 待办数量按 tab 分(受 version filter 影响) + const pendingByTab = useMemo(() => { + const base = selectedVersionId ? workItems.filter((i) => i.versionId === selectedVersionId) : workItems; + return { + all: base.filter((i) => !i.completed).length, + plan_research: base.filter((i) => i.type === 'plan_research' && !i.completed).length, + plan_product: base.filter((i) => i.type === 'plan_product' && !i.completed).length, + plan_ui: base.filter((i) => i.type === 'plan_ui' && !i.completed).length, + devTask: base.filter((i) => i.type === 'devTask' && !i.completed).length, + testCase: base.filter((i) => i.type === 'testCase' && !i.completed).length, + bug: base.filter((i) => i.type === 'bug' && !i.completed).length, + }; + }, [workItems, selectedVersionId]); + + const pendingCount = pendingByTab.all; + const completedCount = (selectedVersionId ? workItems.filter((i) => i.versionId === selectedVersionId) : workItems).filter((i) => i.completed).length; return (
-
-
-

与我相关

+ {/* 左侧第一列:产品/项目/版本树 */} +
+
+

与我相关

-
+
+ + {Array.from(tree.entries()).map(([prodName, prod]) => ( + + ))} +
+
+ + {/* 左侧第二列:环节分类 */} +
+
待办 {pendingCount} - 已完成 {completedCount} + 完成 {completedCount}
-
+ {/* 右侧:任务列表 */}

{TABS.find((t) => t.key === activeTab)?.label}

{filteredItems.length} 项 + {selectedVersionId && ( + + {versionMap.get(selectedVersionId)?.name} + + )}
@@ -159,49 +211,82 @@ export default function WorkspacePage() { ); } +function ProductNode({ name, prod, selectedVersionId, onSelect }: { + name: string; + prod: { name: string; projects: Map }; + selectedVersionId: string | null; + onSelect: (id: string | null) => void; +}) { + const [expanded, setExpanded] = useState(true); + return ( +
+ + {expanded && Array.from(prod.projects.entries()).map(([projName, proj]) => ( + + ))} +
+ ); +} + +function ProjectNode({ name, versions, selectedVersionId, onSelect }: { + name: string; + versions: { id: string; name: string; pendingCount: number }[]; + selectedVersionId: string | null; + onSelect: (id: string | null) => void; +}) { + const [expanded, setExpanded] = useState(true); + return ( +
+ + {expanded && versions.map((v) => ( + + ))} +
+ ); +} + function WorkItemCard({ item, onNavigate }: { item: WorkItem; onNavigate: () => void }) { const statusBadge = getStatusBadge(item); return (
- {/* 完成标记 */} {item.completed && } - - {/* 类型标签 */} {WORK_ITEM_TYPE_LABEL[item.type]} - - {/* 状态 */} {statusBadge} - - {/* 编号 */} {item.extra?.taskNo && {item.extra.taskNo}} {item.extra?.caseNo && {item.extra.caseNo}} {item.extra?.bugNo && {item.extra.bugNo}} - - {/* 标题 */} {item.title} - - {/* 优先级 */} {item.priority && {item.priority}} - - {/* Bug 严重程度 */} {item.extra?.severity && ( {BUG_SEVERITY_LABEL[item.extra.severity as keyof typeof BUG_SEVERITY_LABEL] || ''} )} - - {/* 跳转 */}
- - {/* 上下文:产品 / 项目 / 版本 */} -
+
{item.productName} / {item.projectName}