feat: 与我相关重构 — workspace-engine 聚合引擎
架构:新建 lib/workspace-engine.ts - aggregateWorkItems(): 从所有 store 统一聚合"与我相关"数据 - 每个 WorkItem 包含:type, title, status, completed, productName, projectName, versionName, versionId - 新增模块只需在引擎中添加聚合逻辑,工作台自动展示 - 解决了之前各模块增加内容但工作台无法关联的问题 功能改动: 1. 每个任务显示完整上下文:产品 / 项目 / 版本 2. 版本名可点击跳转到版本详情 3. 所有任务(含已完成)统一展示,已完成半透明+绿色勾 4. 左侧增加"显示已完成"开关 5. 待办/已完成计数统计 6. 统一卡片样式:类型标签 + 状态 + 编号 + 标题 + 优先级 7. Bug 额外显示严重程度标签 8. 列表默认排序:未完成在前,已完成在后 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,109 +2,113 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp, Code2, ClipboardCheck, Bug as BugIcon } from 'lucide-react';
|
import { Search, FileText, Palette, ClipboardList, Code2, ClipboardCheck, Bug as BugIcon, ExternalLink, CheckCircle2 } from 'lucide-react';
|
||||||
import { useProductStore } from '@/stores/useProductStore';
|
import { useProductStore } from '@/stores/useProductStore';
|
||||||
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
||||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||||
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
||||||
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
|
||||||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||||
import { useBugStore } from '@/stores/useBugStore';
|
import { useBugStore } from '@/stores/useBugStore';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
import { flattenVersions } from '@/lib/derive';
|
import { flattenVersions } from '@/lib/derive';
|
||||||
import { calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan';
|
import { aggregateWorkItems, WORK_ITEM_TYPE_LABEL } from '@/lib/workspace-engine';
|
||||||
import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours } from '@/lib/dev-task';
|
import type { WorkItem, WorkItemType } from '@/lib/workspace-engine';
|
||||||
|
import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR } from '@/lib/dev-task';
|
||||||
import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case';
|
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 { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
|
||||||
import type { PlanTask, VersionPlan } from '@/lib/version-plan';
|
|
||||||
import type { DevTask } from '@/lib/dev-task';
|
|
||||||
|
|
||||||
type TabKey = 'all' | 'research' | 'product' | 'ui' | 'devTask' | 'testCase' | 'bug';
|
type TabKey = 'all' | 'plan_research' | 'plan_product' | 'plan_ui' | 'devTask' | 'testCase' | 'bug';
|
||||||
|
|
||||||
const TABS: { key: TabKey; label: string; icon: any }[] = [
|
const TABS: { key: TabKey; label: string; icon: any }[] = [
|
||||||
{ key: 'all', label: '全部待办', icon: ClipboardList },
|
{ key: 'all', label: '全部', icon: ClipboardList },
|
||||||
{ key: 'research', label: '调研', icon: Search },
|
{ key: 'plan_research', label: '调研', icon: Search },
|
||||||
{ key: 'product', label: '产品方案', icon: FileText },
|
{ key: 'plan_product', label: '产品方案', icon: FileText },
|
||||||
{ key: 'ui', label: 'UI设计', icon: Palette },
|
{ key: 'plan_ui', label: 'UI设计', icon: Palette },
|
||||||
{ key: 'devTask', label: '开发任务', icon: Code2 },
|
{ key: 'devTask', label: '开发任务', icon: Code2 },
|
||||||
{ key: 'testCase', label: '测试用例', icon: ClipboardCheck },
|
{ key: 'testCase', label: '测试用例', icon: ClipboardCheck },
|
||||||
{ key: 'bug', label: 'Bug', icon: BugIcon },
|
{ key: 'bug', label: 'Bug', icon: BugIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PLAN_STATUS_STYLE: Record<string, string> = {
|
||||||
|
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<string, string> = { pending: '未开始', in_progress: '进行中', completed: '已完成' };
|
||||||
|
|
||||||
export default function WorkspacePage() {
|
export default function WorkspacePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { overview, fetchOverview } = useProductStore();
|
const { overview, fetchOverview } = useProductStore();
|
||||||
const { plans, fetchPlans, updatePlan, completePlan } = useVersionPlanStore();
|
const { plans, fetchPlans } = useVersionPlanStore();
|
||||||
const { requirements, fetchRequirements } = useRequirementStore();
|
const { requirements, fetchRequirements } = useRequirementStore();
|
||||||
const { tasks: devTasks, fetchTasks } = useDevTaskStore();
|
const { tasks: devTasks, fetchTasks } = useDevTaskStore();
|
||||||
const { categories, fetchCategories } = useTaskCategoryStore();
|
|
||||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
const { testCases, fetchTestCases } = useTestCaseStore();
|
||||||
const { bugs, fetchBugs } = useBugStore();
|
const { bugs, fetchBugs } = useBugStore();
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>('all');
|
const [activeTab, setActiveTab] = useState<TabKey>('all');
|
||||||
const [completingPlan, setCompletingPlan] = useState<VersionPlan | null>(null);
|
const [showCompleted, setShowCompleted] = useState(true);
|
||||||
|
|
||||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||||
useEffect(() => { fetchTasks(); }, [fetchTasks]);
|
useEffect(() => { fetchTasks(); }, [fetchTasks]);
|
||||||
useEffect(() => { fetchCategories(); }, [fetchCategories]);
|
|
||||||
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
|
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
|
||||||
useEffect(() => { fetchBugs(); }, [fetchBugs]);
|
useEffect(() => { fetchBugs(); }, [fetchBugs]);
|
||||||
|
|
||||||
const userName = user?.name ?? '';
|
const userName = user?.name ?? '';
|
||||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||||
|
|
||||||
// 我负责的所有未完成计划
|
const versionMap = useMemo(() => {
|
||||||
const myPlans = useMemo(() =>
|
const map = new Map<string, { id: string; name: string; productName: string; projectName: string }>();
|
||||||
plans.filter((p) => p.owner === userName && p.status !== 'completed'),
|
allVersions.forEach((v) => map.set(v.id, { id: v.id, name: v.name, productName: v.productName, projectName: v.projectName }));
|
||||||
[plans, userName]
|
return map;
|
||||||
|
}, [allVersions]);
|
||||||
|
|
||||||
|
const requirementVersionMap = useMemo(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
requirements.forEach((r) => { if (r.versionId) map.set(r.id, r.versionId); });
|
||||||
|
return map;
|
||||||
|
}, [requirements]);
|
||||||
|
|
||||||
|
const workItems = useMemo(() =>
|
||||||
|
aggregateWorkItems(userName, plans, devTasks, testCases, bugs, versionMap, requirementVersionMap),
|
||||||
|
[userName, plans, devTasks, testCases, bugs, versionMap, requirementVersionMap]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 我负责的所有未完成开发任务
|
const filteredItems = useMemo(() => {
|
||||||
const myDevTasks = useMemo(() =>
|
let items = activeTab === 'all' ? workItems : workItems.filter((i) => i.type === activeTab);
|
||||||
devTasks.filter((t) => t.assigneeId === userName && t.status !== 'submitted'),
|
if (!showCompleted) items = items.filter((i) => !i.completed);
|
||||||
[devTasks, userName]
|
return items.sort((a, b) => {
|
||||||
);
|
if (a.completed !== b.completed) return a.completed ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}, [workItems, activeTab, showCompleted]);
|
||||||
|
|
||||||
// 我负责的待执行/执行中/失败测试用例
|
const counts = useMemo(() => ({
|
||||||
const myTestCases = useMemo(() =>
|
all: workItems.length,
|
||||||
testCases.filter((c) => c.assigneeId === userName && (c.status === 'pending' || c.status === 'running' || c.status === 'failed')),
|
plan_research: workItems.filter((i) => i.type === 'plan_research').length,
|
||||||
[testCases, userName]
|
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]);
|
||||||
|
|
||||||
// 我负责的未关闭 Bug
|
const pendingCount = workItems.filter((i) => !i.completed).length;
|
||||||
const myBugs = useMemo(() =>
|
const completedCount = workItems.filter((i) => i.completed).length;
|
||||||
bugs.filter((b) => b.assigneeId === userName && b.status !== 'closed' && b.status !== 'rejected'),
|
|
||||||
[bugs, userName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const counts = {
|
|
||||||
all: myPlans.length + myDevTasks.length + myTestCases.length + myBugs.length,
|
|
||||||
research: myPlans.filter((p) => p.type === 'research').length,
|
|
||||||
product: myPlans.filter((p) => p.type === 'product').length,
|
|
||||||
ui: myPlans.filter((p) => p.type === 'ui').length,
|
|
||||||
devTask: myDevTasks.length,
|
|
||||||
testCase: myTestCases.length,
|
|
||||||
bug: myBugs.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
const filtered = activeTab === 'all' ? myPlans : (activeTab === 'devTask' || activeTab === 'testCase' || activeTab === 'bug') ? [] : myPlans.filter((p) => p.type === activeTab);
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
|
|
||||||
const toggleTask = (plan: VersionPlan, task: PlanTask) => {
|
|
||||||
const next: PlanTask['status'] = task.status === 'pending' ? 'in_progress' : task.status === 'in_progress' ? 'completed' : 'pending';
|
|
||||||
const updatedTasks = (plan.tasks || []).map((t) => t.id === task.id ? { ...t, status: next } : t);
|
|
||||||
updatePlan(plan.id, { tasks: updatedTasks });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* 左侧:分组 */}
|
|
||||||
<div className="w-60 shrink-0 border-r border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
|
<div className="w-60 shrink-0 border-r border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
|
||||||
<div className="flex h-14 items-center px-5 border-b border-[var(--line)]">
|
<div className="flex h-14 items-center px-5 border-b border-[var(--line)]">
|
||||||
<h1 className="text-[15px] font-semibold text-[var(--ink)]">与我相关</h1>
|
<h1 className="text-[15px] font-semibold text-[var(--ink)]">与我相关</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-4 py-3 border-b border-[var(--line)]">
|
||||||
|
<div className="flex items-center gap-3 text-[11px]">
|
||||||
|
<span className="text-[var(--ink-muted)]">待办 <span className="font-medium text-[var(--ink)]">{pendingCount}</span></span>
|
||||||
|
<span className="text-[var(--ink-muted)]">已完成 <span className="font-medium text-emerald-600">{completedCount}</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<nav className="flex-1 p-3 space-y-1">
|
<nav className="flex-1 p-3 space-y-1">
|
||||||
{TABS.map((tab) => {
|
{TABS.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
@@ -114,331 +118,127 @@ export default function WorkspacePage() {
|
|||||||
<button
|
<button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
onClick={() => setActiveTab(tab.key)}
|
onClick={() => setActiveTab(tab.key)}
|
||||||
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-[13px] transition-colors ${
|
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-[13px] transition-colors ${active ? 'bg-[var(--accent-soft)] text-[var(--accent)] font-medium' : 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]'}`}
|
||||||
active
|
|
||||||
? 'bg-[var(--accent-soft)] text-[var(--accent)] font-medium'
|
|
||||||
: 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Icon className="h-3.5 w-3.5" />
|
<Icon className="h-3.5 w-3.5" />
|
||||||
<span className="flex-1 text-left">{tab.label}</span>
|
<span className="flex-1 text-left">{tab.label}</span>
|
||||||
<span className={`text-[11px] tabular-nums px-1.5 py-0.5 rounded ${active ? 'bg-[var(--accent)] text-white' : 'bg-[var(--bg-subtle)] text-[var(--ink-muted)]'}`}>
|
<span className={`text-[11px] tabular-nums px-1.5 py-0.5 rounded ${active ? 'bg-[var(--accent)] text-white' : 'bg-[var(--bg-subtle)] text-[var(--ink-muted)]'}`}>{count}</span>
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
<div className="p-3 border-t border-[var(--line)]">
|
||||||
|
<label className="flex items-center gap-2 text-[12px] text-[var(--ink-soft)] cursor-pointer">
|
||||||
|
<input type="checkbox" checked={showCompleted} onChange={(e) => setShowCompleted(e.target.checked)} className="h-3.5 w-3.5 rounded" />
|
||||||
|
显示已完成
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:待办列表 */}
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<header className="flex h-14 shrink-0 items-center border-b border-[var(--line)] bg-[var(--bg-card)] px-5">
|
<header className="flex h-14 shrink-0 items-center border-b border-[var(--line)] bg-[var(--bg-card)] px-5">
|
||||||
<h2 className="text-[14px] font-semibold text-[var(--ink)]">
|
<h2 className="text-[14px] font-semibold text-[var(--ink)]">{TABS.find((t) => t.key === activeTab)?.label}</h2>
|
||||||
{TABS.find((t) => t.key === activeTab)?.label}
|
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{filteredItems.length} 项</span>
|
||||||
</h2>
|
|
||||||
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{activeTab === 'devTask' ? myDevTasks.length : activeTab === 'testCase' ? myTestCases.length : activeTab === 'bug' ? myBugs.length : filtered.length} 项</span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-5 bg-[var(--bg)] space-y-3">
|
<div className="flex-1 overflow-y-auto p-5 bg-[var(--bg)]">
|
||||||
{activeTab === 'devTask' ? (
|
{filteredItems.length === 0 ? (
|
||||||
myDevTasks.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
||||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无开发任务</p>
|
<p className="text-[13px] text-[var(--ink-muted)]">暂无相关任务</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
myDevTasks.map((task) => {
|
<div className="space-y-2">
|
||||||
const cat = categories.find((c) => c.id === task.categoryId);
|
{filteredItems.map((item) => (
|
||||||
const req = requirements.find((r) => r.id === task.requirementId);
|
<WorkItemCard key={item.id} item={item} onNavigate={() => item.versionId && router.push(`/versions/${item.versionId}`)} />
|
||||||
return (
|
|
||||||
<div key={task.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${DEV_TASK_STATUS_COLOR[task.status]}`}>
|
|
||||||
{DEV_TASK_STATUS_LABEL[task.status]}
|
|
||||||
</span>
|
|
||||||
{task.isBlocked && <span className="text-[10px] text-red-500 bg-red-50 px-1.5 py-0.5 rounded">阻塞</span>}
|
|
||||||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{task.taskNo}</span>
|
|
||||||
<span className="text-[14px] font-medium text-[var(--ink)]">{task.title}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] text-[var(--ink-muted)]">{task.priority}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4 text-[11px] text-[var(--ink-muted)]">
|
|
||||||
{req && <span>{req.code} {req.title}</span>}
|
|
||||||
{cat && <span className="px-1.5 py-0.5 rounded text-[10px]" style={{ backgroundColor: cat.color ? `${cat.color}15` : undefined, color: cat.color }}>{cat.name}</span>}
|
|
||||||
<span>预计 {formatHours(task.estimateHours)}</span>
|
|
||||||
{task.actualHours > 0 && <span>已投入 {task.actualHours}h</span>}
|
|
||||||
{task.dueDate && <span>截止 {task.dueDate}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
) : activeTab === 'testCase' ? (
|
|
||||||
myTestCases.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
|
||||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无测试用例</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
myTestCases.map((tc) => (
|
|
||||||
<div key={tc.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${TEST_CASE_STATUS_COLOR[tc.status]}`}>
|
|
||||||
{TEST_CASE_STATUS_LABEL[tc.status]}
|
|
||||||
</span>
|
|
||||||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{tc.caseNo}</span>
|
|
||||||
<span className="text-[14px] font-medium text-[var(--ink)]">{tc.title}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] text-[var(--ink-muted)]">{tc.priority}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
) : activeTab === 'bug' ? (
|
|
||||||
myBugs.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
|
||||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无 Bug</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
myBugs.map((bug) => (
|
|
||||||
<div key={bug.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${BUG_STATUS_COLOR[bug.status]}`}>
|
|
||||||
{BUG_STATUS_LABEL[bug.status]}
|
|
||||||
</span>
|
|
||||||
<span className={`text-[10px] px-1.5 py-0.5 rounded ${BUG_SEVERITY_COLOR[bug.severity]}`}>{BUG_SEVERITY_LABEL[bug.severity]}</span>
|
|
||||||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{bug.bugNo}</span>
|
|
||||||
<span className="text-[14px] font-medium text-[var(--ink)]">{bug.title}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] text-[var(--ink-muted)]">{bug.priority}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
) : filtered.length === 0 ? (
|
|
||||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
|
||||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无待办事项</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filtered.map((plan) => {
|
|
||||||
const version = allVersions.find((v) => v.id === plan.versionId);
|
|
||||||
const isActive = plan.startTime <= today;
|
|
||||||
const typeLabel = plan.type === 'research' ? '调研' : plan.type === 'product' ? '产品方案' : 'UI设计';
|
|
||||||
const linkedReqs = (plan.linkedRequirementIds || []).map((id) => requirements.find((r) => r.id === id)).filter(Boolean) as { id: string; code: string; title: string }[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlanCard
|
|
||||||
key={plan.id}
|
|
||||||
plan={plan}
|
|
||||||
versionName={version?.name}
|
|
||||||
versionInfo={version ? `${version.productName} / ${version.projectName}` : '-'}
|
|
||||||
versionId={version?.id}
|
|
||||||
typeLabel={typeLabel}
|
|
||||||
isActive={isActive}
|
|
||||||
linkedReqs={linkedReqs}
|
|
||||||
onToggleTask={(task) => toggleTask(plan, task)}
|
|
||||||
onToggleReq={(reqId) => {
|
|
||||||
const current = plan.completedRequirementIds || [];
|
|
||||||
const next = current.includes(reqId) ? current.filter((id) => id !== reqId) : [...current, reqId];
|
|
||||||
updatePlan(plan.id, { completedRequirementIds: next });
|
|
||||||
}}
|
|
||||||
onAddTask={(title) => {
|
|
||||||
const newTask: PlanTask = { id: `task-${Date.now()}`, title, status: 'pending' };
|
|
||||||
updatePlan(plan.id, { tasks: [...(plan.tasks || []), newTask] });
|
|
||||||
}}
|
|
||||||
onComplete={() => setCompletingPlan(plan)}
|
|
||||||
onJumpVersion={() => version && router.push(`/versions/${version.id}`)}
|
|
||||||
onUpdate={(data) => updatePlan(plan.id, data)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{completingPlan && (
|
|
||||||
<CompleteModal
|
|
||||||
onClose={() => setCompletingPlan(null)}
|
|
||||||
onSubmit={(result) => {
|
|
||||||
completePlan(completingPlan.id, result);
|
|
||||||
setCompletingPlan(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PlanCard({ plan, versionName, versionInfo, versionId, typeLabel, isActive, linkedReqs, onToggleTask, onToggleReq, onAddTask, onComplete, onJumpVersion, onUpdate }: {
|
|
||||||
plan: VersionPlan;
|
|
||||||
versionName?: string;
|
|
||||||
versionInfo: string;
|
|
||||||
versionId?: string;
|
|
||||||
typeLabel: string;
|
|
||||||
isActive: boolean;
|
|
||||||
linkedReqs: { id: string; code: string; title: string }[];
|
|
||||||
onToggleTask: (task: PlanTask) => void;
|
|
||||||
onToggleReq: (reqId: string) => void;
|
|
||||||
onAddTask: (title: string) => void;
|
|
||||||
onComplete: () => void;
|
|
||||||
onJumpVersion: () => void;
|
|
||||||
onUpdate: (data: Partial<VersionPlan>) => void;
|
|
||||||
}) {
|
|
||||||
const [newTaskTitle, setNewTaskTitle] = useState('');
|
|
||||||
const isResearch = plan.type === 'research';
|
|
||||||
const progress = isResearch
|
|
||||||
? calcPlanProgress(plan.tasks)
|
|
||||||
: calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds);
|
|
||||||
const allReqsDone = !isResearch && plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0 && progress === 100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${isActive ? 'bg-blue-50 text-blue-600' : 'bg-zinc-100 text-zinc-500'}`}>
|
|
||||||
{isActive ? '进行中' : '未开始'}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-[var(--ink-muted)] px-1.5 py-0.5 rounded bg-[var(--bg-subtle)]">{typeLabel}</span>
|
|
||||||
<span className="text-[14px] font-medium text-[var(--ink)]">{plan.title}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{(allReqsDone || (isResearch && plan.tasks && plan.tasks.length > 0)) && (
|
|
||||||
<button onClick={onComplete} className="h-7 px-3 rounded-md text-[11px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]">
|
|
||||||
提交完成
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-[11px] text-[var(--ink-muted)] mb-3">
|
|
||||||
<span>{versionInfo} / </span>
|
|
||||||
<button onClick={onJumpVersion} className="text-[var(--accent)] hover:underline">{versionName || '版本'}</button>
|
|
||||||
<span>{plan.startTime.slice(0, 10)} → {plan.endTime.slice(0, 10)}</span>
|
|
||||||
{progress > 0 && <span className="font-medium text-[var(--ink-soft)]">{progress}%</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 进度条 */}
|
|
||||||
{((isResearch && plan.tasks && plan.tasks.length > 0) || (!isResearch && plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0)) && (
|
|
||||||
<div className="mb-3 h-1.5 rounded-full bg-[var(--bg-subtle)] overflow-hidden">
|
|
||||||
<div className="h-full rounded-full bg-[var(--accent)] transition-all" style={{ width: `${progress}%` }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 调研:任务清单 */}
|
|
||||||
{isResearch && (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{(plan.tasks || []).map((task) => (
|
|
||||||
<div key={task.id} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-[var(--bg-subtle)]">
|
|
||||||
<button
|
|
||||||
onClick={() => onToggleTask(task)}
|
|
||||||
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${
|
|
||||||
task.status === 'completed' ? 'bg-[var(--accent)] border-[var(--accent)]' :
|
|
||||||
task.status === 'in_progress' ? 'border-blue-400 bg-blue-50' :
|
|
||||||
'border-[var(--line)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{task.status === 'completed' && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
|
|
||||||
{task.status === 'in_progress' && <div className="h-1.5 w-1.5 rounded-full bg-blue-500" />}
|
|
||||||
</button>
|
|
||||||
<span className={`flex-1 text-[12px] ${task.status === 'completed' ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`}>
|
|
||||||
{task.title}
|
|
||||||
</span>
|
|
||||||
<span className={`text-[10px] ${task.status === 'completed' ? 'text-green-600' : task.status === 'in_progress' ? 'text-blue-600' : 'text-[var(--ink-muted)]'}`}>
|
|
||||||
{task.status === 'completed' ? '已完成' : task.status === 'in_progress' ? '进行中' : '未开始'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
<input
|
|
||||||
value={newTaskTitle}
|
|
||||||
onChange={(e) => setNewTaskTitle(e.target.value)}
|
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' && newTaskTitle.trim()) { onAddTask(newTaskTitle.trim()); setNewTaskTitle(''); } }}
|
|
||||||
placeholder="添加任务,回车确认"
|
|
||||||
className="flex-1 h-7 rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkItemCard({ item, onNavigate }: { item: WorkItem; onNavigate: () => void }) {
|
||||||
|
const statusBadge = getStatusBadge(item);
|
||||||
|
|
||||||
{/* 产品方案/UI:关联需求清单 */}
|
|
||||||
{!isResearch && linkedReqs.length > 0 && (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{linkedReqs.map((req) => {
|
|
||||||
const isDone = (plan.completedRequirementIds || []).includes(req.id);
|
|
||||||
return (
|
return (
|
||||||
<div key={req.id} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-[var(--bg-subtle)]">
|
<div className={`rounded-xl border border-[var(--line)] bg-[var(--bg-card)] px-4 py-3 transition-colors hover:border-[var(--accent)] ${item.completed ? 'opacity-60' : ''}`}>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={() => onToggleReq(req.id)}
|
{/* 完成标记 */}
|
||||||
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${isDone ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
|
{item.completed && <CheckCircle2 className="h-4 w-4 text-emerald-500 shrink-0" />}
|
||||||
>
|
|
||||||
{isDone && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
|
{/* 类型标签 */}
|
||||||
|
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-[var(--bg-subtle)] text-[var(--ink-muted)] shrink-0">
|
||||||
|
{WORK_ITEM_TYPE_LABEL[item.type]}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* 状态 */}
|
||||||
|
{statusBadge}
|
||||||
|
|
||||||
|
{/* 编号 */}
|
||||||
|
{item.extra?.taskNo && <span className="text-[11px] font-mono text-[var(--ink-muted)] shrink-0">{item.extra.taskNo}</span>}
|
||||||
|
{item.extra?.caseNo && <span className="text-[11px] font-mono text-[var(--ink-muted)] shrink-0">{item.extra.caseNo}</span>}
|
||||||
|
{item.extra?.bugNo && <span className="text-[11px] font-mono text-[var(--ink-muted)] shrink-0">{item.extra.bugNo}</span>}
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<span className="text-[13px] font-medium text-[var(--ink)] flex-1 truncate">{item.title}</span>
|
||||||
|
|
||||||
|
{/* 优先级 */}
|
||||||
|
{item.priority && <span className="text-[10px] text-[var(--ink-muted)] shrink-0">{item.priority}</span>}
|
||||||
|
|
||||||
|
{/* Bug 严重程度 */}
|
||||||
|
{item.extra?.severity && (
|
||||||
|
<span className={`text-[10px] px-1.5 py-0.5 rounded shrink-0 ${BUG_SEVERITY_COLOR[item.extra.severity as keyof typeof BUG_SEVERITY_COLOR] || ''}`}>
|
||||||
|
{BUG_SEVERITY_LABEL[item.extra.severity as keyof typeof BUG_SEVERITY_LABEL] || ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 跳转 */}
|
||||||
|
<button onClick={onNavigate} className="h-6 w-6 flex items-center justify-center rounded text-[var(--ink-muted)] hover:text-[var(--accent)] hover:bg-[var(--bg-subtle)] shrink-0" title="跳转到版本详情">
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{req.code}</span>
|
|
||||||
<span className={`flex-1 text-[12px] ${isDone ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`}>{req.title}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
{/* 上下文:产品 / 项目 / 版本 */}
|
||||||
{allReqsDone && (
|
<div className="flex items-center gap-2 mt-1.5 ml-0 text-[11px] text-[var(--ink-muted)]">
|
||||||
<div className="mt-2 rounded-lg bg-green-50 border border-green-200 px-3 py-2 text-[12px] text-green-700">
|
<span>{item.productName}</span>
|
||||||
所有需求已完成,请提交原型成果
|
<span className="text-[var(--line)]">/</span>
|
||||||
|
<span>{item.projectName}</span>
|
||||||
|
<span className="text-[var(--line)]">/</span>
|
||||||
|
<button onClick={onNavigate} className="text-[var(--accent)] hover:underline">{item.versionName}</button>
|
||||||
|
{item.extra?.dueDate && <span className="ml-2">截止 {item.extra.dueDate}</span>}
|
||||||
|
{item.extra?.startTime && <span className="ml-2">{item.extra.startTime.slice(0, 16).replace('T', ' ')} → {item.extra.endTime?.slice(0, 16).replace('T', ' ')}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isResearch && linkedReqs.length === 0 && (
|
|
||||||
<div className="text-[12px] text-[var(--ink-muted)] py-2">
|
|
||||||
未关联需求,<button onClick={onJumpVersion} className="text-[var(--accent)] hover:underline">前往版本详情</button>关联
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompleteModal({ onClose, onSubmit }: {
|
function getStatusBadge(item: WorkItem) {
|
||||||
onClose: () => void;
|
if (item.type === 'devTask') {
|
||||||
onSubmit: (result: { resultType: 'link' | 'file'; resultUrl?: string; resultFileName?: string; resultFileData?: string }) => void;
|
|
||||||
}) {
|
|
||||||
const [resultType, setResultType] = useState<'link' | 'file'>('link');
|
|
||||||
const [url, setUrl] = useState('');
|
|
||||||
const [fileName, setFileName] = useState('');
|
|
||||||
const [fileData, setFileData] = useState('');
|
|
||||||
|
|
||||||
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
setFileName(file.name);
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => setFileData(reader.result as string);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSubmit = resultType === 'link' ? url.trim().length > 0 : fileData.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
|
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full shrink-0 ${DEV_TASK_STATUS_COLOR[item.status as keyof typeof DEV_TASK_STATUS_COLOR] || 'bg-zinc-100 text-zinc-600'}`}>
|
||||||
<div className="w-full max-w-sm rounded-2xl bg-[var(--bg-card)] border border-[var(--line)] p-5 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
|
{DEV_TASK_STATUS_LABEL[item.status as keyof typeof DEV_TASK_STATUS_LABEL] || item.status}
|
||||||
<h3 className="text-[13px] font-semibold text-[var(--ink)] mb-4">提交成果</h3>
|
</span>
|
||||||
<div className="space-y-3">
|
);
|
||||||
<div className="flex gap-2">
|
}
|
||||||
<button type="button" onClick={() => setResultType('link')} className={`h-8 px-3 rounded-lg text-[12px] font-medium border transition-colors ${resultType === 'link' ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] text-[var(--ink-soft)]'}`}><Link2 className="h-3 w-3 inline mr-1" />链接</button>
|
if (item.type === 'testCase') {
|
||||||
<button type="button" onClick={() => setResultType('file')} className={`h-8 px-3 rounded-lg text-[12px] font-medium border transition-colors ${resultType === 'file' ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] text-[var(--ink-soft)]'}`}><FileUp className="h-3 w-3 inline mr-1" />文件</button>
|
return (
|
||||||
</div>
|
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full shrink-0 ${TEST_CASE_STATUS_COLOR[item.status as keyof typeof TEST_CASE_STATUS_COLOR] || 'bg-zinc-100 text-zinc-600'}`}>
|
||||||
{resultType === 'link' ? (
|
{TEST_CASE_STATUS_LABEL[item.status as keyof typeof TEST_CASE_STATUS_LABEL] || item.status}
|
||||||
<input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://..." className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
</span>
|
||||||
) : (
|
);
|
||||||
<div>
|
}
|
||||||
<input type="file" onChange={handleFile} className="text-[12px] text-[var(--ink-soft)]" />
|
if (item.type === 'bug') {
|
||||||
{fileName && <p className="text-[11px] text-[var(--ink-muted)] mt-1">已选:{fileName}</p>}
|
return (
|
||||||
</div>
|
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full shrink-0 ${BUG_STATUS_COLOR[item.status as keyof typeof BUG_STATUS_COLOR] || 'bg-zinc-100 text-zinc-600'}`}>
|
||||||
)}
|
{BUG_STATUS_LABEL[item.status as keyof typeof BUG_STATUS_LABEL] || item.status}
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
</span>
|
||||||
<button onClick={onClose} className="h-8 px-3 rounded-lg text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]">取消</button>
|
);
|
||||||
<button onClick={() => onSubmit({ resultType, resultUrl: resultType === 'link' ? url.trim() : fileData, resultFileName: fileName || undefined, resultFileData: resultType === 'file' ? fileData : undefined })} disabled={!canSubmit} className="h-8 px-4 rounded-lg text-[12px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)] disabled:opacity-50">确认</button>
|
}
|
||||||
</div>
|
return (
|
||||||
</div>
|
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full shrink-0 ${PLAN_STATUS_STYLE[item.status] || 'bg-zinc-100 text-zinc-600'}`}>
|
||||||
</div>
|
{PLAN_STATUS_LABEL[item.status] || item.status}
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
127
apps/web/lib/workspace-engine.ts
Normal file
127
apps/web/lib/workspace-engine.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import type { VersionPlan } from './version-plan';
|
||||||
|
import type { DevTask } from './dev-task';
|
||||||
|
import type { TestCase } from './test-case';
|
||||||
|
import type { Bug } from './bug';
|
||||||
|
|
||||||
|
export type WorkItemType = 'plan_research' | 'plan_product' | 'plan_ui' | 'devTask' | 'testCase' | 'bug';
|
||||||
|
|
||||||
|
export interface WorkItem {
|
||||||
|
id: string;
|
||||||
|
type: WorkItemType;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
completed: boolean;
|
||||||
|
productName: string;
|
||||||
|
projectName: string;
|
||||||
|
versionName: string;
|
||||||
|
versionId: string;
|
||||||
|
priority?: string;
|
||||||
|
extra?: Record<string, any>;
|
||||||
|
raw: VersionPlan | DevTask | TestCase | Bug;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionContext {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
productName: string;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aggregateWorkItems(
|
||||||
|
userName: string,
|
||||||
|
plans: VersionPlan[],
|
||||||
|
devTasks: DevTask[],
|
||||||
|
testCases: TestCase[],
|
||||||
|
bugs: Bug[],
|
||||||
|
versionMap: Map<string, VersionContext>,
|
||||||
|
requirementVersionMap: Map<string, string>,
|
||||||
|
): WorkItem[] {
|
||||||
|
const items: WorkItem[] = [];
|
||||||
|
|
||||||
|
// Plans: owner === userName
|
||||||
|
plans.filter((p) => p.owner === userName).forEach((p) => {
|
||||||
|
const ver = versionMap.get(p.versionId);
|
||||||
|
items.push({
|
||||||
|
id: p.id,
|
||||||
|
type: p.type === 'research' ? 'plan_research' : p.type === 'product' ? 'plan_product' : 'plan_ui',
|
||||||
|
title: p.title,
|
||||||
|
status: p.status,
|
||||||
|
completed: p.status === 'completed',
|
||||||
|
productName: ver?.productName ?? '-',
|
||||||
|
projectName: ver?.projectName ?? '-',
|
||||||
|
versionName: ver?.name ?? '-',
|
||||||
|
versionId: p.versionId,
|
||||||
|
extra: { startTime: p.startTime, endTime: p.endTime },
|
||||||
|
raw: p,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// DevTasks: assigneeId === userName
|
||||||
|
devTasks.filter((t) => t.assigneeId === userName).forEach((t) => {
|
||||||
|
const versionId = requirementVersionMap.get(t.requirementId) ?? '';
|
||||||
|
const ver = versionMap.get(versionId);
|
||||||
|
items.push({
|
||||||
|
id: t.id,
|
||||||
|
type: 'devTask',
|
||||||
|
title: t.title,
|
||||||
|
status: t.status,
|
||||||
|
completed: t.status === 'submitted',
|
||||||
|
productName: ver?.productName ?? '-',
|
||||||
|
projectName: ver?.projectName ?? '-',
|
||||||
|
versionName: ver?.name ?? '-',
|
||||||
|
versionId,
|
||||||
|
priority: t.priority,
|
||||||
|
extra: { taskNo: t.taskNo, dueDate: t.dueDate },
|
||||||
|
raw: t,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TestCases: assigneeId === userName
|
||||||
|
testCases.filter((c) => c.assigneeId === userName).forEach((c) => {
|
||||||
|
const ver = versionMap.get(c.versionId);
|
||||||
|
items.push({
|
||||||
|
id: c.id,
|
||||||
|
type: 'testCase',
|
||||||
|
title: c.title,
|
||||||
|
status: c.status,
|
||||||
|
completed: c.status === 'passed',
|
||||||
|
productName: ver?.productName ?? '-',
|
||||||
|
projectName: ver?.projectName ?? '-',
|
||||||
|
versionName: ver?.name ?? '-',
|
||||||
|
versionId: c.versionId,
|
||||||
|
priority: c.priority,
|
||||||
|
extra: { caseNo: c.caseNo },
|
||||||
|
raw: c,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bugs: assigneeId === userName
|
||||||
|
bugs.filter((b) => b.assigneeId === userName).forEach((b) => {
|
||||||
|
const ver = versionMap.get(b.versionId);
|
||||||
|
items.push({
|
||||||
|
id: b.id,
|
||||||
|
type: 'bug',
|
||||||
|
title: b.title,
|
||||||
|
status: b.status,
|
||||||
|
completed: b.status === 'closed' || b.status === 'rejected',
|
||||||
|
productName: ver?.productName ?? '-',
|
||||||
|
projectName: ver?.projectName ?? '-',
|
||||||
|
versionName: ver?.name ?? '-',
|
||||||
|
versionId: b.versionId,
|
||||||
|
priority: b.priority,
|
||||||
|
extra: { bugNo: b.bugNo, severity: b.severity },
|
||||||
|
raw: b,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORK_ITEM_TYPE_LABEL: Record<WorkItemType, string> = {
|
||||||
|
plan_research: '调研',
|
||||||
|
plan_product: '产品方案',
|
||||||
|
plan_ui: 'UI设计',
|
||||||
|
devTask: '开发任务',
|
||||||
|
testCase: '测试用例',
|
||||||
|
bug: 'Bug',
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user