Files
ftb-project-management/apps/web/components/dev-task/DevTaskTab.tsx
Script Generator a560634091 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>
2026-06-11 18:02:07 +08:00

105 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useMemo, useEffect } from 'react';
import { Plus, Code2 } from 'lucide-react';
import { useDevTaskStore } from '@/stores/useDevTaskStore';
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { DevTaskRow } from './DevTaskRow';
import { DevTaskCreateModal } from './DevTaskCreateModal';
import { DevTaskDetailDrawer } from './DevTaskDetailDrawer';
import { calcGroupProgress } from '@/lib/dev-task';
import type { DevTask } from '@/lib/dev-task';
interface Props {
versionId: string;
requirementIds: string[];
}
export function DevTaskTab({ versionId, requirementIds }: Props) {
const { tasks, fetchTasks } = useDevTaskStore();
const { categories, fetchCategories } = useTaskCategoryStore();
const { fetchWorklogs } = useTaskWorklogStore();
const { requirements } = useRequirementStore();
useEffect(() => { fetchTasks(); }, [fetchTasks]);
useEffect(() => { fetchCategories(); }, [fetchCategories]);
useEffect(() => { fetchWorklogs(); }, [fetchWorklogs]);
const versionTasks = useMemo(
() => tasks.filter((t) => requirementIds.includes(t.requirementId)),
[tasks, requirementIds],
);
const progress = calcGroupProgress(versionTasks);
const totalEstimate = versionTasks.reduce((s, t) => s + t.estimateHours, 0);
const totalActual = versionTasks.reduce((s, t) => s + t.actualHours, 0);
const blockedCount = versionTasks.filter((t) => t.isBlocked).length;
const [showCreate, setShowCreate] = useState(false);
const [selectedTask, setSelectedTask] = useState<DevTask | null>(null);
const groupedByReq = useMemo(() => {
const map = new Map<string, DevTask[]>();
for (const t of versionTasks) {
const list = map.get(t.requirementId) || [];
list.push(t);
map.set(t.requirementId, list);
}
return map;
}, [versionTasks]);
return (
<div className="space-y-4">
{/* 统计æ <C3A6> */}
<div className="flex items-center gap-4 px-4 py-3 rounded-lg bg-[var(--bg-subtle)] border border-[var(--line)]">
<div className="flex items-center gap-2">
<Code2 className="h-4 w-4 text-[var(--accent)]" />
<span className="text-[13px] font-medium text-[var(--ink)]">å¼å<EFBFBD>è¿åº¦ {progress}%</span>
</div>
<div className="h-2 flex-1 rounded-full bg-[var(--bg)] overflow-hidden">
<div className="h-full rounded-full bg-[var(--accent)] transition-all" style={{ width: `${progress}%` }} />
</div>
<span className="text-[11px] text-[var(--ink-muted)] tabular-nums">
{versionTasks.length} 任务 · æŠå¥ {totalActual}h / é¢è®¡ {totalEstimate}h
</span>
{blockedCount > 0 && (
<span className="text-[11px] text-red-500 bg-red-50 px-1.5 py-0.5 rounded">{blockedCount} 阻塞</span>
)}
<button onClick={() => setShowCreate(true)} className="ml-auto flex items-center gap-1 h-7 px-3 rounded-md text-[11px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]">
<Plus className="h-3 w-3" />æ°å»ºä»»åŠ¡
</button>
</div>
{/* 按需求分组列表 */}
{versionTasks.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)]">æšæ å¼å<EFBFBD>任务</p>
<button onClick={() => setShowCreate(true)} className="mt-3 text-[12px] text-[var(--accent)] hover:underline">åˆå»ºç¬¬ä¸ä¸ªä»»åŠ¡</button>
</div>
) : (
Array.from(groupedByReq.entries()).map(([reqId, reqTasks]) => {
const req = requirements.find((r) => r.id === reqId);
const reqProgress = calcGroupProgress(reqTasks);
return (
<div key={reqId} className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2 bg-[var(--bg-subtle)] border-b border-[var(--line)]">
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{req?.code}</span>
<span className="text-[12px] font-medium text-[var(--ink)] flex-1 truncate">{req?.title}</span>
<span className="text-[11px] text-[var(--ink-muted)]">{reqProgress}%</span>
</div>
{reqTasks.map((t) => (
<DevTaskRow key={t.id} task={t} category={categories.find((c) => c.id === t.categoryId)} onClick={() => setSelectedTask(t)} />
))}
</div>
);
})
)}
{showCreate && <DevTaskCreateModal versionId={versionId} requirementIds={requirementIds} onClose={() => setShowCreate(false)} />}
{selectedTask && <DevTaskDetailDrawer task={selectedTask} allTasks={versionTasks} onClose={() => setSelectedTask(null)} />}
</div>
);
}