'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; 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'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore'; import { useWorkActivityStore } from '@/stores/useWorkActivityStore'; import { flattenVersions } from '@/lib/derive'; import { aggregateWorkItems, getWorkspacePendingCountByVersion, WORK_ITEM_TYPE_LABEL } from '@/lib/workspace-engine'; import type { WorkItem, WorkItemType } from '@/lib/workspace-engine'; import { DailyReportPanel } from '@/components/workspace/DailyReportPanel'; import { PlanDetailDrawer } from '@/components/version/PlanDetailDrawer'; import { DevTaskDetailDrawer } from '@/components/dev-task/DevTaskDetailDrawer'; import { TestCaseDetailDrawer } from '@/components/test-case/TestCaseDetailDrawer'; import { BugDetailDrawer } from '@/components/bug/BugDetailDrawer'; import { BugCreateModal } from '@/components/bug/BugCreateModal'; import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, getEstimateHours, getActualHours } from '@/lib/dev-task'; import { formatDateTime, formatLocalDate } from '@/lib/format'; import { formatShortTime, formatWorkHours } from '@/lib/work-hours'; import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case'; import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; import { getWorkspaceDailyReport } from '@/lib/workspace-daily-report'; import { VERSION_STATUS_BG, VERSION_STATUS_LABEL, getVersionReadonlyNotice, isVersionReadonly, type VersionStatus } from '@/lib/version-status'; import { loadV22WorkspaceData, type V22WorkspaceData } from '@/lib/v22-api'; import { selectWorkspaceCollections } from '@/lib/workspace-v22-source'; type TabKey = 'all' | 'plan_research' | 'plan_product' | 'plan_ui' | 'devTask' | 'testCase' | 'bug'; type WorkspaceVersionContext = { id: string; name: string; productName: string; projectName: string; status: VersionStatus }; type TreeVersion = { id: string; name: string; status: VersionStatus; pendingCount: number }; type ProductTree = Map }>; const TABS: { key: TabKey; label: string; icon: any }[] = [ { key: 'all', label: '全部', icon: ClipboardList }, { key: 'plan_research', label: '调研', icon: Search }, { key: 'plan_product', label: '产品方案', icon: FileText }, { key: 'plan_ui', label: 'UI设计', icon: Palette }, { key: 'devTask', label: '开发任务', icon: Code2 }, { key: 'testCase', label: '测试用例', icon: ClipboardCheck }, { key: 'bug', label: 'Bug', icon: BugIcon }, ]; const PLAN_STATUS_STYLE: Record = { pending: 'bg-zinc-100 text-zinc-600', in_progress: 'bg-blue-50 text-blue-600', completed: 'bg-emerald-50 text-emerald-600', }; const PLAN_STATUS_LABEL: Record = { pending: '未开始', in_progress: '进行中', completed: '已完成' }; export default function WorkspacePage() { const router = useRouter(); const { overview, fetchOverview } = useProductStore(); const { plans, fetchPlans, loaded: plansLoaded } = useVersionPlanStore(); const { requirements, fetchRequirements, loaded: requirementsLoaded } = useRequirementStore(); const { tasks: devTasks, fetchTasks, loaded: devTasksLoaded } = useDevTaskStore(); const { testCases, fetchTestCases, loaded: testCasesLoaded } = useTestCaseStore(); const { bugs, fetchBugs, loaded: bugsLoaded } = useBugStore(); const { worklogs, fetchWorklogs } = useTaskWorklogStore(); const { activities, fetchActivities } = useWorkActivityStore(); const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('all'); const [showCompleted, setShowCompleted] = useState(true); const [selectedVersionId, setSelectedVersionId] = useState(null); const [drawerItem, setDrawerItem] = useState(null); const [bugFromTestCaseId, setBugFromTestCaseId] = useState(null); const [v22WorkspaceData, setV22WorkspaceData] = useState(null); const [v22WorkspaceLoaded, setV22WorkspaceLoaded] = useState(false); const [v22WorkspaceFailed, setV22WorkspaceFailed] = useState(false); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchWorklogs(); }, [fetchWorklogs]); useEffect(() => { fetchActivities(); }, [fetchActivities]); const userId = user?.id ?? ''; const userName = user?.name ?? ''; const workspaceUserKey = userId || userName; const workspaceUserRefs = useMemo(() => [userName, userId].filter(Boolean), [userId, userName]); useEffect(() => { if (!workspaceUserKey.trim()) { setV22WorkspaceData(null); setV22WorkspaceLoaded(false); setV22WorkspaceFailed(false); return; } let cancelled = false; setV22WorkspaceLoaded(false); setV22WorkspaceFailed(false); loadV22WorkspaceData(workspaceUserKey) .then((data) => { if (cancelled) return; setV22WorkspaceData(data); setV22WorkspaceLoaded(true); }) .catch(() => { if (cancelled) return; setV22WorkspaceData(null); setV22WorkspaceFailed(true); }); return () => { cancelled = true; }; }, [workspaceUserKey]); useEffect(() => { if (!workspaceUserKey.trim() || !v22WorkspaceFailed) return; void fetchPlans(); void fetchRequirements(); void fetchTasks(); void fetchTestCases(); void fetchBugs(); }, [ fetchBugs, fetchPlans, fetchRequirements, fetchTasks, fetchTestCases, v22WorkspaceFailed, workspaceUserKey, ]); const allVersions = useMemo(() => flattenVersions(overview), [overview]); const versionMap = useMemo(() => { const map = new Map(); allVersions.forEach((v) => map.set(v.id, { id: v.id, name: v.name, productName: v.productName, projectName: v.projectName, status: v.status })); return map; }, [allVersions]); const requirementVersionMap = useMemo(() => { const map = new Map(); requirements.forEach((r) => { if (r.versionId) map.set(r.id, r.versionId); }); return map; }, [requirements]); const workspaceCollections = useMemo( () => selectWorkspaceCollections({ v22Loaded: v22WorkspaceLoaded, v22Failed: v22WorkspaceFailed, v22Data: v22WorkspaceData, appData: { plans, devTasks, testCases, bugs }, }), [bugs, devTasks, plans, testCases, v22WorkspaceData, v22WorkspaceFailed, v22WorkspaceLoaded], ); const workItems = useMemo(() => aggregateWorkItems( workspaceUserRefs, workspaceCollections.versionPlans, workspaceCollections.devTasks, workspaceCollections.testCases, workspaceCollections.bugs, versionMap, requirementVersionMap, ), [workspaceUserRefs, workspaceCollections, versionMap, requirementVersionMap] ); const pendingCountByVersion = useMemo(() => getWorkspacePendingCountByVersion(workItems), [workItems]); // 构建树:只显示跟自己有关的产品/项目/版本 const tree = useMemo(() => { const myVersionIds = new Set(workItems.map((i) => i.versionId).filter(Boolean)); const productMap: ProductTree = 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 = pendingCountByVersion.get(v.id) ?? 0; if (!proj.versions.find((ver) => ver.id === v.id)) { proj.versions.push({ id: v.id, name: v.name, status: v.status, pendingCount: pending }); } }); return productMap; }, [allVersions, pendingCountByVersion, workItems]); 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]); const today = useMemo(() => formatLocalDate(), []); const dailyReport = useMemo(() => getWorkspaceDailyReport({ activities, worklogs, workItems, userId: userName || userId, date: today, }), [activities, userId, worklogs, workItems, userName, today] ); // 待办数量按 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; const selectedVersion = selectedVersionId ? versionMap.get(selectedVersionId) : undefined; const drawerVersionStatus = drawerItem ? versionMap.get(drawerItem.versionId)?.status : undefined; const drawerReadOnly = drawerVersionStatus ? isVersionReadonly(drawerVersionStatus) : false; const ensureWorkspaceDrawerStores = useCallback(async (item: WorkItem) => { const pendingLoads: Array> = []; if (!requirementsLoaded) pendingLoads.push(fetchRequirements()); if ((item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') && !plansLoaded) { pendingLoads.push(fetchPlans()); } if (item.type === 'devTask' && !devTasksLoaded) pendingLoads.push(fetchTasks()); if (item.type === 'testCase' && !testCasesLoaded) pendingLoads.push(fetchTestCases()); if (item.type === 'bug' && !bugsLoaded) pendingLoads.push(fetchBugs()); await Promise.all(pendingLoads); }, [ bugsLoaded, devTasksLoaded, fetchBugs, fetchPlans, fetchRequirements, fetchTasks, fetchTestCases, plansLoaded, requirementsLoaded, testCasesLoaded, ]); const openWorkItemDrawer = useCallback(async (item: WorkItem) => { await ensureWorkspaceDrawerStores(item); setDrawerItem(item); }, [ensureWorkspaceDrawerStores]); return (
{/* 左侧第一列:产品/项目/版本树 */}

与我相关

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

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

{filteredItems.length} 项 {selectedVersion && ( {selectedVersion.name} )}
{filteredItems.length === 0 ? (

暂无相关任务

) : (
{filteredItems.map((item) => ( item.versionId && router.push(`/versions/${item.versionId}`)} onClick={() => { void openWorkItemDrawer(item); }} /> ))}
)}
{/* Detail Drawers */} {drawerItem && (drawerItem.type === 'plan_research' || drawerItem.type === 'plan_product' || drawerItem.type === 'plan_ui') && ( setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} /> )} {drawerItem && drawerItem.type === 'devTask' && ( t.id)} readOnly={drawerReadOnly} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} /> )} {drawerItem && drawerItem.type === 'testCase' && ( setDrawerItem(null)} onCreateBug={(tcId) => { if (!drawerReadOnly) { setDrawerItem(null); setBugFromTestCaseId(tcId); } }} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} /> )} {drawerItem && drawerItem.type === 'bug' && ( setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} /> )} {bugFromTestCaseId && ( setBugFromTestCaseId(null)} /> )}
); } 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: TreeVersion[]; selectedVersionId: string | null; onSelect: (id: string | null) => void; }) { const [expanded, setExpanded] = useState(true); return (
{expanded && versions.map((v) => ( ))}
); } function VersionStatusTag({ status, readonlyOnly = false }: { status?: VersionStatus; readonlyOnly?: boolean }) { if (!status) return null; const readonlyNotice = getVersionReadonlyNotice(status); if (readonlyOnly && !readonlyNotice) return null; return ( {VERSION_STATUS_LABEL[status]} ); } function WorkItemCard({ item, versionStatus, onNavigate, onClick }: { item: WorkItem; versionStatus?: VersionStatus; onNavigate: () => void; onClick: () => void; }) { const statusBadge = getStatusBadge(item); const isDevTask = item.type === 'devTask'; const devTaskRaw = isDevTask ? (item.raw as any) : null; const devEstimate = devTaskRaw ? getEstimateHours(devTaskRaw) : 0; const devActual = devTaskRaw ? getActualHours(devTaskRaw) : 0; const devOverrun = devActual > devEstimate && devEstimate > 0; const devTimeRange = devTaskRaw ? ( devTaskRaw.actualStartAt ? `实际 ${formatShortTime(devTaskRaw.actualStartAt)} → ${devTaskRaw.actualEndAt ? formatShortTime(devTaskRaw.actualEndAt) : '进行中'}` : (devTaskRaw.expectedStartAt && devTaskRaw.expectedEndAt ? `预计 ${formatShortTime(devTaskRaw.expectedStartAt)} → ${formatShortTime(devTaskRaw.expectedEndAt)}` : '') ) : ''; 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}} {item.extra?.severity && ( {BUG_SEVERITY_LABEL[item.extra.severity as keyof typeof BUG_SEVERITY_LABEL] || ''} )}
{item.productName} / {item.projectName} / {isDevTask && devTimeRange && ( <> {devTimeRange} {devEstimate > 0 && ( 0 ? (devOverrun ? 'text-red-600' : (devActual < devEstimate ? 'text-emerald-600' : '')) : ''}`}> {devActual > 0 ? `${formatWorkHours(devActual)} / ${formatWorkHours(devEstimate)}` : `预 ${formatWorkHours(devEstimate)}`} )} )} {item.extra?.startTime && {formatDateTime(item.extra.startTime)} → {formatDateTime(item.extra.endTime)}}
); } function getStatusBadge(item: WorkItem) { if (item.type === 'devTask') { return ( {DEV_TASK_STATUS_LABEL[item.status as keyof typeof DEV_TASK_STATUS_LABEL] || item.status} ); } if (item.type === 'testCase') { return ( {TEST_CASE_STATUS_LABEL[item.status as keyof typeof TEST_CASE_STATUS_LABEL] || item.status} ); } if (item.type === 'bug') { return ( {BUG_STATUS_LABEL[item.status as keyof typeof BUG_STATUS_LABEL] || item.status} ); } return ( {PLAN_STATUS_LABEL[item.status] || item.status} ); }