auto-sync: 2026-05-19 13:44:20

This commit is contained in:
cfdaily
2026-05-19 13:44:20 +08:00
parent 9872eb97d2
commit 77ccec13c3
+55 -10
View File
@@ -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 />;