feat(dev-task): 开发任务模块 V1 完整实现
- 核心库:dev-task.ts(类型+状态机+进度计算)、task-worklog.ts(工时)、task-category.ts(字典)、work-item.ts(聚合契约) - Store:useDevTaskStore(CRUD+状态流转)、useTaskWorklogStore(工时记录)、useTaskCategoryStore(字典管理) - UI组件:DevTaskTab、DevTaskCreateModal、DevTaskDetailDrawer、DevTaskRow、WorklogPanel、StatusBadge、CategoryChip - 集成:版本详情页"开发任务"Tab、"与我相关"工作台开发任务分组、任务类型管理页 - 设计文档:spec + 实施计划 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,22 +2,27 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp } from 'lucide-react';
|
||||
import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp, Code2 } from 'lucide-react';
|
||||
import { useProductStore } from '@/stores/useProductStore';
|
||||
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
||||
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import { flattenVersions } from '@/lib/derive';
|
||||
import { calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan';
|
||||
import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours } from '@/lib/dev-task';
|
||||
import type { PlanTask, VersionPlan } from '@/lib/version-plan';
|
||||
import type { DevTask } from '@/lib/dev-task';
|
||||
|
||||
type TabKey = 'all' | 'research' | 'product' | 'ui';
|
||||
type TabKey = 'all' | 'research' | 'product' | 'ui' | 'devTask';
|
||||
|
||||
const TABS: { key: TabKey; label: string; icon: any }[] = [
|
||||
{ key: 'all', label: '全部待办', icon: ClipboardList },
|
||||
{ key: 'research', label: '调研', icon: Search },
|
||||
{ key: 'product', label: '产品方案', icon: FileText },
|
||||
{ key: 'ui', label: 'UI设计', icon: Palette },
|
||||
{ key: 'devTask', label: '开发任务', icon: Code2 },
|
||||
];
|
||||
|
||||
export default function WorkspacePage() {
|
||||
@@ -25,6 +30,8 @@ export default function WorkspacePage() {
|
||||
const { overview, fetchOverview } = useProductStore();
|
||||
const { plans, fetchPlans, updatePlan, completePlan } = useVersionPlanStore();
|
||||
const { requirements, fetchRequirements } = useRequirementStore();
|
||||
const { tasks: devTasks, fetchTasks } = useDevTaskStore();
|
||||
const { categories, fetchCategories } = useTaskCategoryStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('all');
|
||||
const [completingPlan, setCompletingPlan] = useState<VersionPlan | null>(null);
|
||||
@@ -32,6 +39,8 @@ export default function WorkspacePage() {
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||
useEffect(() => { fetchTasks(); }, [fetchTasks]);
|
||||
useEffect(() => { fetchCategories(); }, [fetchCategories]);
|
||||
|
||||
const userName = user?.name ?? '';
|
||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||
@@ -42,14 +51,21 @@ export default function WorkspacePage() {
|
||||
[plans, userName]
|
||||
);
|
||||
|
||||
// 我负责的所有未完成开发任务
|
||||
const myDevTasks = useMemo(() =>
|
||||
devTasks.filter((t) => t.assigneeId === userName && t.status !== 'done'),
|
||||
[devTasks, userName]
|
||||
);
|
||||
|
||||
const counts = {
|
||||
all: myPlans.length,
|
||||
all: myPlans.length + myDevTasks.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,
|
||||
};
|
||||
|
||||
const filtered = activeTab === 'all' ? myPlans : myPlans.filter((p) => p.type === activeTab);
|
||||
const filtered = activeTab === 'all' ? myPlans : activeTab === 'devTask' ? [] : myPlans.filter((p) => p.type === activeTab);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const toggleTask = (plan: VersionPlan, task: PlanTask) => {
|
||||
@@ -97,11 +113,44 @@ export default function WorkspacePage() {
|
||||
<h2 className="text-[14px] font-semibold text-[var(--ink)]">
|
||||
{TABS.find((t) => t.key === activeTab)?.label}
|
||||
</h2>
|
||||
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{filtered.length} 项</span>
|
||||
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{activeTab === 'devTask' ? myDevTasks.length : filtered.length} 项</span>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-5 bg-[var(--bg)] space-y-3">
|
||||
{filtered.length === 0 ? (
|
||||
{activeTab === 'devTask' ? (
|
||||
myDevTasks.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>
|
||||
) : (
|
||||
myDevTasks.map((task) => {
|
||||
const cat = categories.find((c) => c.id === task.categoryId);
|
||||
const req = requirements.find((r) => r.id === task.requirementId);
|
||||
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>
|
||||
);
|
||||
})
|
||||
)
|
||||
) : 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>
|
||||
|
||||
Reference in New Issue
Block a user