diff --git a/src/frontend/src/components/EdictBoard.tsx b/src/frontend/src/components/EdictBoard.tsx index 2c4965a..c0fa196 100644 --- a/src/frontend/src/components/EdictBoard.tsx +++ b/src/frontend/src/components/EdictBoard.tsx @@ -286,58 +286,103 @@ export default function EdictBoard() { const v2tasksLoading = useStore(s => s.v2tasksLoading); const selectedProjectId = useStore(s => s.selectedProjectId); const loadV2Tasks = useStore(s => s.loadV2Tasks); + const toast = useStore(s => s.toast); + const loadV2TaskDetail = useStore(s => s.loadV2TaskDetail); const [statusFilter, setStatusFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); + const [archiveFilter, setArchiveFilter] = useState<'active' | 'archived' | 'all'>('active'); // 初始化加载 + 轮询 useEffect(() => { if (selectedProjectId) loadV2Tasks(); }, [selectedProjectId]); + // ── 卡片动作处理 ── + const handleCardAction = async (taskId: string, action: string) => { + const pid = selectedProjectId; + if (!pid) return; + try { + if (action === 'archive') { + const res = await fetch(`/api/projects/${pid}/tasks/${taskId}`, { + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ archived: 1 }), + }); + if (res.ok) { toast(`📦 已归档`); loadV2Tasks(); } + else toast('归档失败', 'err'); + } else if (action === 'approve') { + const res = await fetch(`/api/projects/${pid}/tasks/${taskId}/status`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'done' }), + }); + if (res.ok) { toast(`✅ 已通过`); loadV2Tasks(); } + else toast('操作失败', 'err'); + } else { + const res = await fetch(`/api/projects/${pid}/tasks/${taskId}/status`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: action }), + }); + if (res.ok) { toast(`✅ 状态已更新`); loadV2Tasks(); } + else toast('操作失败', 'err'); + } + } catch { toast('网络错误', 'err'); } + }; + const tasks = v2tasks as V2Task[]; + // 第 1 行筛选器:全部/活跃/归档 + const archiveFiltered = tasks.filter(t => { + if (archiveFilter === 'active') return !t.archived; + if (archiveFilter === 'archived') return !!t.archived; + return true; + }); + const filters = [ { key: 'all', label: '全部', icon: '📋' }, { key: 'pending', label: '待认领', icon: '📋' }, { key: 'claimed', label: '已认领', icon: '👤' }, { key: 'working', label: '执行中', icon: '⚔️' }, { key: 'review', label: '审查中', icon: '🔍' }, + { key: 'paused', label: '已暂停', icon: '⏸' }, + { key: 'escalated', label: '已升级', icon: '⚠️' }, + { key: 'waiting_human', label: '等人工', icon: '🔔' }, { key: 'done', label: '已完成', icon: '✅' }, { key: 'failed', label: '失败', icon: '❌' }, { key: 'blocked', label: '阻塞', icon: '🚧' }, + { key: 'cancelled', label: '已取消', icon: '🚫' }, ]; - // 构建子 Task 索引:parent_id → { total, done, activeStage } + // 使用 archiveFiltered 代替 tasks 来构建子 Task 索引和筛选 const subtaskIndex: Record = {}; - tasks.forEach(t => { + archiveFiltered.forEach(t => { if (t.parent_task) { if (!subtaskIndex[t.parent_task]) subtaskIndex[t.parent_task] = { total: 0, done: 0, activeStage: null }; subtaskIndex[t.parent_task].total++; if (t.status === 'done') subtaskIndex[t.parent_task].done++; - if (['working', 'claimed', 'review'].includes(t.status) && !subtaskIndex[t.parent_task].activeStage) { + if (['working', 'claimed', 'review', 'paused', 'escalated', 'waiting_human'].includes(t.status) && !subtaskIndex[t.parent_task].activeStage) { subtaskIndex[t.parent_task].activeStage = t.stage || t.status; } } }); - let filtered = tasks.filter(t => !t.parent_task); // 只显示顶层 Task + let filtered = archiveFiltered.filter(t => !t.parent_task); // 只显示顶层 Task if (statusFilter !== 'all') filtered = filtered.filter(t => t.status === statusFilter); if (searchQuery.trim()) { const q = searchQuery.trim().toLowerCase(); filtered = filtered.filter(t => (t.title || '').toLowerCase().includes(q) || (t.id || '').toLowerCase().includes(q)); } - const order: Record = { working: 0, review: 1, claimed: 2, blocked: 3, pending: 4, failed: 5, done: 6, cancelled: 7 }; - filtered.sort((a, b) => (order[a.status] ?? 8) - (order[b.status] ?? 8)); + const order: Record = { escalated: 0, waiting_human: 1, working: 2, review: 3, claimed: 4, paused: 5, blocked: 6, pending: 7, failed: 8, done: 9, cancelled: 10 }; + filtered.sort((a, b) => (order[a.status] ?? 11) - (order[b.status] ?? 11)); - const counts: Record = { all: tasks.filter(t => !t.parent_task).length }; - tasks.filter(t => !t.parent_task).forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); + const topLevelTasks = archiveFiltered.filter(t => !t.parent_task); + const counts: Record = { all: topLevelTasks.length }; + topLevelTasks.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); - const topLevelTasks = tasks.filter(t => !t.parent_task); - const activeCount = topLevelTasks.filter(t => ['working', 'claimed', 'review'].includes(t.status)).length; + const activeCount = topLevelTasks.filter(t => ['working', 'claimed', 'review', 'paused', 'escalated', 'waiting_human'].includes(t.status)).length; const doneCount = topLevelTasks.filter(t => t.status === 'done').length; const failedCount = topLevelTasks.filter(t => ['failed', 'blocked'].includes(t.status)).length; const reviewCount = topLevelTasks.filter(t => t.status === 'review').length; + const archivedCount = topLevelTasks.filter(t => t.archived).length; if (!selectedProjectId) return ; if (v2tasksLoading && tasks.length === 0) return ;