auto-sync: 2026-05-19 13:44:20
This commit is contained in:
@@ -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<string>('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<string, { total: number; done: number; activeStage: string | null }> = {};
|
||||
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<string, number> = { 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<string, number> = { 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<string, number> = { 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<string, number> = { 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 <EmptyState hasProject={false} />;
|
||||
if (v2tasksLoading && tasks.length === 0) return <LoadingSkeleton />;
|
||||
|
||||
Reference in New Issue
Block a user