Files
ftb-project-management/apps/web/components/dev-task/DevTaskDetailDrawer.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

132 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 } from 'react';
import { X, AlertTriangle, Link2 } from 'lucide-react';
import { StatusBadge } from './StatusBadge';
import { CategoryChip } from './CategoryChip';
import { WorklogPanel } from './WorklogPanel';
import { useDevTaskStore } from '@/stores/useDevTaskStore';
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
import { ALLOWED_TRANSITIONS, DEV_TASK_STATUS_LABEL, formatHours } from '@/lib/dev-task';
import type { DevTask, DevTaskStatus } from '@/lib/dev-task';
interface Props {
task: DevTask;
allTasks: DevTask[];
onClose: () => void;
}
export function DevTaskDetailDrawer({ task, allTasks, onClose }: Props) {
const { changeStatus, setBlocked } = useDevTaskStore();
const { categories } = useTaskCategoryStore();
const category = categories.find((c) => c.id === task.categoryId);
const [blockReason, setBlockReason] = useState(task.blockReason || '');
const [showBlockInput, setShowBlockInput] = useState(false);
const nextStatuses = ALLOWED_TRANSITIONS[task.status];
const predecessors = (task.predecessorIds || []).map((id) => allTasks.find((t) => t.id === id)).filter(Boolean) as DevTask[];
const handleTransition = (to: DevTaskStatus) => {
const result = changeStatus(task.id, to);
if (!result.ok) alert(result.message);
};
const handleBlock = () => {
if (!blockReason.trim()) return;
setBlocked(task.id, true, blockReason.trim());
setShowBlockInput(false);
};
const handleUnblock = () => {
setBlocked(task.id, false);
setBlockReason('');
};
return (
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
<div className="w-full max-w-md h-full bg-[var(--bg-card)] border-l border-[var(--line)] shadow-[var(--shadow-lg)] flex flex-col" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between h-14 px-5 border-b border-[var(--line)] shrink-0">
<div className="flex items-center gap-2">
<span className="text-[12px] font-mono text-[var(--ink-muted)]">{task.taskNo}</span>
<StatusBadge status={task.status} />
{task.isBlocked && <span className="text-[10px] text-red-500 bg-red-50 px-1.5 py-0.5 rounded"></span>}
</div>
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--bg-subtle)]"><X className="h-4 w-4" /></button>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto p-5 space-y-5">
<h2 className="text-[15px] font-semibold text-[var(--ink)]">{task.title}</h2>
{task.description && <p className="text-[12px] text-[var(--ink-soft)]">{task.description}</p>}
{/* 状态流转 */}
{nextStatuses.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-[11px] text-[var(--ink-muted)]"></span>
{nextStatuses.map((s) => (
<button key={s} onClick={() => handleTransition(s)} className="h-7 px-3 rounded-md text-[11px] font-medium border border-[var(--line)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors">
{DEV_TASK_STATUS_LABEL[s]}
</button>
))}
</div>
)}
{/* 阻塞管理 */}
<div className="rounded-lg border border-[var(--line)] p-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-[12px] font-medium text-[var(--ink)]"></span>
{task.isBlocked ? (
<button onClick={handleUnblock} className="text-[11px] text-emerald-600 hover:underline"></button>
) : (
<button onClick={() => setShowBlockInput(true)} className="text-[11px] text-red-500 hover:underline"></button>
)}
</div>
{task.isBlocked && (
<div className="flex items-start gap-1.5 text-[12px] text-red-600 bg-red-50 rounded px-2 py-1.5">
<AlertTriangle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span>{task.blockReason}</span>
</div>
)}
{showBlockInput && !task.isBlocked && (
<div className="flex gap-2">
<input value={blockReason} onChange={(e) => setBlockReason(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleBlock(); }} placeholder="阻塞原因" className="flex-1 h-8 rounded-md border border-[var(--line)] px-2 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
<button onClick={handleBlock} className="h-8 px-3 rounded-md text-[11px] font-medium bg-red-500 text-white"></button>
</div>
)}
</div>
{/* 基本信息 */}
<div className="grid grid-cols-2 gap-3 text-[12px]">
<div><span className="text-[var(--ink-muted)]"></span><CategoryChip category={category} /></div>
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{task.assigneeId}</span></div>
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{task.priority}</span></div>
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{formatHours(task.estimateHours)}</span></div>
{task.dueDate && <div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{task.dueDate}</span></div>}
{task.completedAt && <div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{task.completedAt}</span></div>}
</div>
{/* 前置任务 */}
{predecessors.length > 0 && (
<div className="space-y-1.5">
<span className="text-[12px] font-medium text-[var(--ink)]"></span>
{predecessors.map((p) => (
<div key={p.id} className="flex items-center gap-2 text-[12px] px-2 py-1 rounded bg-[var(--bg-subtle)]">
<Link2 className="h-3 w-3 text-[var(--ink-muted)]" />
<span className="font-mono text-[var(--ink-muted)]">{p.taskNo}</span>
<span className="text-[var(--ink)]">{p.title}</span>
<StatusBadge status={p.status} />
</div>
))}
</div>
)}
{/* 工时记录 */}
<WorklogPanel taskId={task.id} />
</div>
</div>
</div>
);
}