From d0419634ad3a9b13debf5ba7fcf0fc8523ef378c Mon Sep 17 00:00:00 2001 From: cfdaily Date: Sun, 17 May 2026 12:12:27 +0800 Subject: [PATCH] auto-sync: 2026-05-17 12:12:27 --- src/frontend/src/components/EdictBoard.tsx | 764 +++++++++------------ 1 file changed, 311 insertions(+), 453 deletions(-) diff --git a/src/frontend/src/components/EdictBoard.tsx b/src/frontend/src/components/EdictBoard.tsx index 2021a86..ed68bf3 100644 --- a/src/frontend/src/components/EdictBoard.tsx +++ b/src/frontend/src/components/EdictBoard.tsx @@ -1,522 +1,380 @@ +/** + * EdictBoard v2.0 — 任务看板 + * 数据模型:v2.6 黑板(扁平任务,无 DAG) + * Mock 数据用于 UI 预览 + */ import { useState } from 'react'; -import { useStore, isEdict, isArchived, getPipeStatus, stateLabel, deptColor, PIPE, STATE_LABEL } from '../store'; -import ArtifactList from './ArtifactList'; +import { useStore } from '../store'; -/** Format ISO timestamp to relative duration string */ -function formatDuration(iso: string): string { - try { - const then = new Date(iso).getTime(); - const now = Date.now(); - const diffMs = now - then; - if (diffMs < 0) return '刚刚'; - const mins = Math.floor(diffMs / 60000); - if (mins < 1) return '刚刚'; - if (mins < 60) return `${mins}分钟`; - const hrs = Math.floor(mins / 60); - const remainMins = mins % 60; - if (hrs < 24) return `${hrs}小时${remainMins ? remainMins + '分' : ''}`; - const days = Math.floor(hrs / 24); - return `${days}天${hrs % 24 ? (hrs % 24) + '小时' : ''}`; - } catch { return ''; } +// ── v2.0 Task 类型 ── +export interface V2Task { + id: string; + title: string; + description: string; + status: 'pending' | 'claimed' | 'working' | 'done' | 'failed'; + assignee: string | null; + assigned_by: string | null; + priority: 'low' | 'medium' | 'high' | 'critical'; + task_type: string; + created_at: string; + updated_at: string; + claimed_at: string | null; + completed_at: string | null; + started_at: string | null; + retry_count: number; + max_retries: number; + risk_level: 'low' | 'standard' | 'high' | 'critical'; + escalated: boolean; + deadline: string | null; + // 关联数据(mock) + comments_count: number; + outputs_count: number; + review_status: 'none' | 'pending' | 'approved' | 'rejected' | 'rebuttal'; + latest_event: string | null; + project_id: string; } -import { api, type Task } from '../api'; -// 排序权重 -const STATE_ORDER: Record = { - created: 0, planning: 1, challenging: 2, assigned: 3, - executing: 4, paused: 4.5, reviewing: 5, - completed: 8, failed: 7, cancelled: 9, escalated: 6, - // legacy - Doing: 4, Review: 5, Assigned: 3, Menxia: 1, Zhongshu: 2, - Taizi: 0, Inbox: 0, Blocked: 6, Next: 0, Done: 8, Cancelled: 9, +// ── 状态管线 ── +const PIPELINE = [ + { key: 'pending', label: '待认领', icon: '📋' }, + { key: 'claimed', label: '已认领', icon: '👤' }, + { key: 'working', label: '执行中', icon: '⚔️' }, + { key: 'done', label: '已完成', icon: '✅' }, +] as const; + +const STATUS_META: Record = { + pending: { color: '#7a9aff', bg: '#0a1028', label: '待认领' }, + claimed: { color: '#a07aff', bg: '#110a28', label: '已认领' }, + working: { color: '#6a9eff', bg: '#0a1530', label: '执行中' }, + done: { color: '#2ecc8a', bg: '#0a2018', label: '已完成' }, + failed: { color: '#ff5270', bg: '#200a10', label: '失败' }, }; -// 状态筛选定义 -const STATUS_FILTERS = [ - { key: 'all', label: '全部', icon: '📋' }, - { key: 'planning', label: '待拆解', icon: '🐦' }, - { key: 'challenging', label: '审核中', icon: '🦅' }, - { key: 'executing', label: '执行中', icon: '⚔️' }, - { key: 'completed', label: '已完成', icon: '🏆' }, - { key: 'failed', label: '失败', icon: '❌' }, - { key: 'paused', label: '暂停', icon: '⏸️' }, - { key: 'cancelled', label: '已取消', icon: '🚫' }, +const PRIORITY_META: Record = { + low: { color: '#6b7280', label: '低' }, + medium: { color: '#3b82f6', label: '中' }, + high: { color: '#f59e0b', label: '高' }, + critical: { color: '#ff5270', label: '紧急' }, +}; + +const RISK_META: Record = { + low: { color: '#2ecc8a', label: '低风险' }, + standard: { color: '#6b7280', label: '标准' }, + high: { color: '#f59e0b', label: '高风险' }, + critical: { color: '#ff5270', label: '极高风险' }, +}; + +const REVIEW_META: Record = { + none: { color: '#6b7280', label: '无审查', icon: '' }, + pending: { color: '#f59e0b', label: '审查中', icon: '🔍' }, + approved: { color: '#2ecc8a', label: '已通过', icon: '✅' }, + rejected: { color: '#ff5270', label: '已驳回', icon: '❌' }, + rebuttal: { color: '#818cf8', label: '反驳中', icon: '⚔️' }, +}; + +const AGENT_EMOJI: Record = { + 'pangtong-fujunshi': '🐦', 'simayi-challenger': '🦅', 'jiangwei-infra': '🔧', + 'guanyu-dev': '⚔️', 'zhangfei-dev': '💪', 'zhaoyun-data': '📊', +}; + +// ── Mock 数据 ── +const MOCK_TASKS: V2Task[] = [ + { + id: 'task-001', title: '动量因子策略回测', description: '基于成交量的日线动量策略回测,持仓周期3-10天波段交易', + status: 'working', assignee: 'zhangfei-dev', assigned_by: 'pangtong-fujunshi', + priority: 'high', task_type: 'backtest', created_at: '2026-05-17T08:30:00', updated_at: '2026-05-17T11:20:00', + claimed_at: '2026-05-17T08:35:00', completed_at: null, started_at: '2026-05-17T08:36:00', + retry_count: 0, max_retries: 3, risk_level: 'standard', escalated: false, + deadline: '2026-05-17T18:00:00', comments_count: 3, outputs_count: 1, + review_status: 'pending', latest_event: '回测脚本运行中,已完成70%数据回测', project_id: 'demo', + }, + { + id: 'task-002', title: '分钟线数据质量检查', description: '检查沪深300成分股1分钟线数据完整性(2024-01至今)', + status: 'done', assignee: 'zhaoyun-data', assigned_by: 'pangtong-fujunshi', + priority: 'medium', task_type: 'data_verify', created_at: '2026-05-17T07:00:00', updated_at: '2026-05-17T09:15:00', + claimed_at: '2026-05-17T07:05:00', completed_at: '2026-05-17T09:15:00', started_at: '2026-05-17T07:06:00', + retry_count: 0, max_retries: 2, risk_level: 'low', escalated: false, + deadline: null, comments_count: 5, outputs_count: 2, + review_status: 'approved', latest_event: '数据质量报告已通过审查', project_id: 'demo', + }, + { + id: 'task-003', title: 'vnpy网关接口升级', description: '将CTP网关从6.3.19升级到6.7.0,适配新版API', + status: 'claimed', assignee: 'jiangwei-infra', assigned_by: 'pangtong-fujunshi', + priority: 'critical', task_type: 'infra', created_at: '2026-05-17T06:00:00', updated_at: '2026-05-17T10:00:00', + claimed_at: '2026-05-17T10:00:00', completed_at: null, started_at: null, + retry_count: 0, max_retries: 2, risk_level: 'critical', escalated: false, + deadline: '2026-05-18T18:00:00', comments_count: 2, outputs_count: 0, + review_status: 'none', latest_event: '姜维已认领,准备开始', project_id: 'demo', + }, + { + id: 'task-004', title: '风控规则代码审查', description: '审查止损/仓位/滑点检查模块代码质量', + status: 'pending', assignee: null, assigned_by: 'pangtong-fujunshi', + priority: 'medium', task_type: 'code_review', created_at: '2026-05-17T09:00:00', updated_at: '2026-05-17T09:00:00', + claimed_at: null, completed_at: null, started_at: null, + retry_count: 0, max_retries: 2, risk_level: 'standard', escalated: false, + deadline: null, comments_count: 0, outputs_count: 0, + review_status: 'none', latest_event: null, project_id: 'demo', + }, + { + id: 'task-005', title: '策略参数优化实验', description: '使用网格搜索优化动量因子参数(lookback=5-30, threshold=0.01-0.05)', + status: 'failed', assignee: 'zhangfei-dev', assigned_by: 'pangtong-fujunshi', + priority: 'high', task_type: 'research', created_at: '2026-05-16T14:00:00', updated_at: '2026-05-17T03:00:00', + claimed_at: '2026-05-16T14:10:00', completed_at: null, started_at: '2026-05-16T14:12:00', + retry_count: 2, max_retries: 3, risk_level: 'high', escalated: true, + deadline: null, comments_count: 8, outputs_count: 0, + review_status: 'rebuttal', latest_event: '网格搜索内存溢出,第2次重试失败', project_id: 'demo', + }, + { + id: 'task-006', title: '滑点模型实现', description: '实现基于成交量加权的滑点模型,支持市价单和限价单', + status: 'working', assignee: 'guanyu-dev', assigned_by: 'pangtong-fujunshi', + priority: 'high', task_type: 'coding', created_at: '2026-05-17T07:30:00', updated_at: '2026-05-17T11:00:00', + claimed_at: '2026-05-17T07:45:00', completed_at: null, started_at: '2026-05-17T07:46:00', + retry_count: 0, max_retries: 2, risk_level: 'standard', escalated: false, + deadline: '2026-05-17T20:00:00', comments_count: 1, outputs_count: 0, + review_status: 'none', latest_event: '滑点基类已完成,正在实现成交量加权逻辑', project_id: 'demo', + }, ]; -// 状态到筛选key的映射 -function stateToFilterKey(state: string, task: Task): string { - const meta = task.sourceMeta || {}; - // moziplus 用 status + plan_status,edict 用 state + sourceMeta - const s = task.status || state; // 优先用 moziplus status - const planStatus = task.plan_status || (meta.plan_status as string) || ''; - if (s === 'planning' && planStatus === 'challenging') return 'challenging'; - if (s === 'planning') return 'planning'; - if (s === 'executing') return 'executing'; - if (s === 'reviewing') return 'executing'; // reviewing is a sub-state of executing - if (s === 'completed' || s === 'Done') return 'completed'; - if (s === 'failed') return 'failed'; - if (s === 'paused') return 'paused'; - if (s === 'cancelled') return 'cancelled'; - return s; +// ── 工具函数 ── +function timeAgo(iso: string): string { + const diff = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return '刚刚'; + if (mins < 60) return `${mins}分钟前`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}小时前`; + return `${Math.floor(hrs / 24)}天前`; } -function MiniPipe({ task }: { task: Task }) { - const stages = getPipeStatus(task); +function formatDeadline(iso: string): string { + const diff = new Date(iso).getTime() - Date.now(); + if (diff < 0) return '已逾期'; + const hrs = Math.floor(diff / 3600000); + if (hrs < 1) return `${Math.floor(diff / 60000)}分钟后`; + if (hrs < 24) return `${hrs}小时后`; + return `${Math.floor(hrs / 24)}天后`; +} + +// ── 状态管线组件 ── +function StatusPipeline({ status }: { status: string }) { + const currentIdx = PIPELINE.findIndex(p => p.key === status); + const failed = status === 'failed'; + return ( -
- {stages.map((s, i) => ( - -
-
{s.icon}
-
{s.dept}
-
- {i < stages.length - 1 &&
} -
- ))} +
+ {PIPELINE.map((stage, i) => { + const isDone = !failed && i < currentIdx; + const isActive = !failed && i === currentIdx; + const isFailed = failed && i === currentIdx; + return ( + +
+ {isDone ? '✓' : stage.icon} + {stage.label} +
+ {i < PIPELINE.length - 1 && ( + + )} +
+ ); + })}
); } -/** 获取任务最近事件的描述 */ -function getLatestEvent(task: Task): string | null { - // 优先取 flow_log 最后一条的 remark - if (task.flow_log && task.flow_log.length > 0) { - const last = task.flow_log[task.flow_log.length - 1]; - if (last.remark) return last.remark; - } - // 次选取 activity 最后一条的 text - if (task.activity && task.activity.length > 0) { - const last = task.activity[task.activity.length - 1]; - if (last.text) return last.text; - } - return null; -} - -function EdictCard({ task }: { task: Task }) { - const setModalTaskId = useStore((s) => s.setModalTaskId); - const toast = useStore((s) => s.toast); - const loadAll = useStore((s) => s.loadAll); - - const hb = task.heartbeat || { status: 'unknown', label: '⚪' }; - const stCls = 'st-' + (task.state || ''); - const deptCls = 'dt-' + (task.org || '').replace(/\s/g, ''); - const curStage = PIPE.find((_, i) => getPipeStatus(task)[i].status === 'active'); - const todos = task.todos || []; - const todoDone = todos.filter((x) => x.status === 'completed').length; - const todoTotal = todos.length; - const taskState = task.status || task.state || ''; - // 前端本地跟 ACTION_GUARDS 同样的映射表(不调 API,卡片列表性能要求) - const ACTION_GUARDS: Record = { - pause: ['planning', 'executing'], - resume: ['paused', 'cancelled'], - cancel: ['created', 'planning', 'executing', 'paused', 'escalated', 'failed'], - retry: ['failed'], - escalate: ['executing', 'failed'], - rollback: ['escalated'], - steer: ['executing'], - }; - const isAllowed = (action: string) => { - if (taskState === 'cancelling' || taskState === 'pausing') return false; // BUG-6: 中间态禁止操作 - const allowed = ACTION_GUARDS[action] || []; - return allowed.includes(taskState); - }; - const canStop = isAllowed('pause'); - const canResume = isAllowed('resume'); - const canCancel = isAllowed('cancel'); - const archived = isArchived(task); - const isBlocked = task.block && task.block !== '无' && task.block !== '-'; - const isCompleted = ['completed', 'Done'].includes(taskState); - const latestEvent = getLatestEvent(task); - - const handleAction = async (action: string, e: React.MouseEvent) => { - e.stopPropagation(); - if (action === 'stop' || action === 'cancel') { - const reason = prompt(action === 'stop' ? '请输入暂停原因:' : '请输入取消原因:'); - if (reason === null) return; - try { - const r = await api.taskAction(task.id, action, reason); - if (r.ok) { toast(r.message || '操作成功'); loadAll(); } - else toast(r.error || '操作失败', 'err'); - } catch { toast('服务器连接失败', 'err'); } - } else if (action === 'resume') { - try { - const r = await api.taskAction(task.id, 'resume', '恢复执行'); - if (r.ok) { toast(r.message || '已恢复'); loadAll(); } - else toast(r.error || '操作失败', 'err'); - } catch { toast('服务器连接失败', 'err'); } - } - }; - - const handleArchive = async (e: React.MouseEvent) => { - e.stopPropagation(); - try { - const r = await api.archiveTask(task.id, !task.archived); - if (r.ok) { toast(r.message || '操作成功'); loadAll(); } - else toast(r.error || '操作失败', 'err'); - } catch { toast('服务器连接失败', 'err'); } - }; - - // M3: 检测 waiting_human 节点 - const waitingNodes = (task.nodes || []).filter((n: any) => n.status === 'waiting_human'); - const hasWaitingHuman = waitingNodes.length > 0; +// ── 任务卡片 ── +function TaskCard({ task, onOpen }: { task: V2Task; onOpen: () => void }) { + const sm = STATUS_META[task.status]; + const pm = PRIORITY_META[task.priority]; + const rm = RISK_META[task.risk_level]; + const rvm = REVIEW_META[task.review_status]; + const agentEmoji = task.assignee ? (AGENT_EMOJI[task.assignee] || '🤖') : ''; return (
setModalTaskId(task.id)} - style={hasWaitingHuman ? { border: '2px solid #f59e0b', animation: 'pulse-border 2s ease-in-out infinite' } : undefined} + onClick={onOpen} + style={{ + background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 14, + padding: 18, cursor: 'pointer', transition: 'border-color .15s, transform .1s, box-shadow .15s', + }} + onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--acc)'; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 4px 20px rgba(106,158,255,.1)'; }} + onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = ''; }} > - -
{task.id}
-
{task.title || '(无标题)'}
-
- {stateLabel(task)} - {task.org && {task.org}} - {task.updated_at && ( - - ⏱ {formatDuration(task.updated_at)} + {/* 状态管线 */} + + + {/* ID + 标题 */} +
{task.id}
+
{task.title}
+ + {/* 标签行 */} +
+ {sm.label} + {task.assignee && ( + + {agentEmoji} {task.assignee} )} - {curStage && ( - - 当前: {curStage.dept} · {curStage.action} + + {pm.label}优先级 + + {task.escalated && ( + + ⚠️ 已升级 )}
- {task.now && task.now !== '-' && ( -
- {task.now.substring(0, 80)} -
- )} - {(task.review_round || 0) > 0 && ( -
- {Array.from({ length: task.review_round || 0 }, (_, i) => ( - - {i + 1} - - ))} - 第 {task.review_round} 轮磋商 -
- )} - {todoTotal > 0 && ( -
- 📋 {todoDone}/{todoTotal} -
-
-
- {todoDone === todoTotal ? '✅ 全部完成' : '🔄 进行中'} -
- )} -
- {hb.label} - {isBlocked && ( - - 🚫 {task.block} - - )} - {taskState === 'cancelling' && ( -
- ⏳ 正在取消(等待当前节点完成...) -
- )} - {taskState === 'pausing' && ( -
- ⏳ 正在暂停(等待当前节点完成...) -
- )} - {task.eta && task.eta !== '-' && ( - 📅 {task.eta} - )} + + {/* 元信息行 */} +
+ ⏱ {timeAgo(task.updated_at)} + {task.outputs_count > 0 && 📦 {task.outputs_count}产出} + {task.comments_count > 0 && 💬 {task.comments_count}评论} + {rvm.icon && {rvm.icon} {rvm.label}}
- {/* 最近事件 */} - {latestEvent && ( + + {/* 最新事件 */} + {task.latest_event && (
- 💬 {latestEvent.substring(0, 100)} -
- )} - {/* M3: waiting_human 通知 */} - {hasWaitingHuman && ( -
-
- 🛐 - 等待御批({waitingNodes.length}) -
- {waitingNodes.slice(0, 2).map((n: any) => ( -
- 🎯 {n.name || n.node_id}({n.agent_id}) -
- ))} - + 💬 {task.latest_event}
)} -
e.stopPropagation()}> - {canStop && ( - - )} - {canCancel && ( - - )} - {canResume && ( - - )} - {isCompleted && !task.archived && ( - - )} - {task.archived && ( - - )} + {/* 底部 */} +
+
+ {rm.label} + {task.retry_count > 0 && 🔄 x{task.retry_count}} +
+
+ {task.deadline && ( + + 📅 {formatDeadline(task.deadline)} + + )} + 详情 → +
- - {/* M3: 成果物摘要(已完成 + 执行中任务) */} - {(isCompleted || taskState === 'executing') && ( - - )}
); } +// ── 主组件 ── export default function EdictBoard() { - const liveStatus = useStore((s) => s.liveStatus); - const edictFilter = useStore((s) => s.edictFilter); - const setEdictFilter = useStore((s) => s.setEdictFilter); - const statusFilter = useStore((s) => s.statusFilter); - const setStatusFilter = useStore((s) => s.setStatusFilter); - const searchQuery = useStore((s) => s.searchQuery); - const setSearchQuery = useStore((s) => s.setSearchQuery); - const toast = useStore((s) => s.toast); - const loadAll = useStore((s) => s.loadAll); + const setModalTaskId = useStore(s => s.setModalTaskId); + const [statusFilter, setStatusFilter] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [batchLoading, setBatchLoading] = useState(false); + const filters = [ + { key: 'all', label: '全部', icon: '📋' }, + { key: 'pending', label: '待认领', icon: '📋' }, + { key: 'claimed', label: '已认领', icon: '👤' }, + { key: 'working', label: '执行中', icon: '⚔️' }, + { key: 'done', label: '已完成', icon: '✅' }, + { key: 'failed', label: '失败', icon: '❌' }, + ]; - const tasks = liveStatus?.tasks || []; - const allEdicts = tasks.filter(isEdict); - const activeEdicts = allEdicts.filter((t) => !isArchived(t)); - const archivedEdicts = allEdicts.filter((t) => isArchived(t)); - - // Step 1: archive filter - let edicts: Task[]; - if (edictFilter === 'active') edicts = activeEdicts; - else if (edictFilter === 'archived') edicts = archivedEdicts; - else edicts = allEdicts; - - // Step 2: status filter - if (statusFilter !== 'all') { - edicts = edicts.filter((t) => stateToFilterKey(t.state, t) === statusFilter); - } - - // Step 3: search filter + let tasks = MOCK_TASKS; + if (statusFilter !== 'all') tasks = tasks.filter(t => t.status === statusFilter); if (searchQuery.trim()) { const q = searchQuery.trim().toLowerCase(); - edicts = edicts.filter((t) => - (t.title || '').toLowerCase().includes(q) || - (t.id || '').toLowerCase().includes(q) - ); + tasks = tasks.filter(t => t.title.toLowerCase().includes(q) || t.id.toLowerCase().includes(q)); } - // Sort - edicts.sort((a, b) => (STATE_ORDER[a.state] ?? 9) - (STATE_ORDER[b.state] ?? 9)); + // 排序:working > claimed > pending > done > failed + const order: Record = { working: 0, claimed: 1, pending: 2, failed: 3, done: 4 }; + tasks.sort((a, b) => (order[a.status] ?? 5) - (order[b.status] ?? 5)); - const unArchivedDone = allEdicts.filter((t) => !t.archived && ['completed', 'Done', 'Cancelled', 'cancelled'].includes(t.status || t.state)); + const counts: Record = { all: MOCK_TASKS.length }; + MOCK_TASKS.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); - const handleArchiveAll = async () => { - if (!confirm('将所有已完成/已取消的任务移入归档?')) return; - try { - const r = await api.archiveAllDone(); - if (r.ok) { toast(`📦 ${r.count || 0} 道任务已归档`); loadAll(); } - else toast(r.error || '批量归档失败', 'err'); - } catch { toast('服务器连接失败', 'err'); } - }; - - const handleScan = async () => { - try { - const r = await api.schedulerScan(); - if (r.ok) toast(`🧭 引擎巡检完成:${r.count || 0} 个动作`); - else toast(r.error || '巡检失败', 'err'); - loadAll(); - } catch { toast('服务器连接失败', 'err'); } - }; - - // Count per status for badges - const statusCounts: Record = { all: 0 }; - for (const t of (edictFilter === 'active' ? activeEdicts : edictFilter === 'archived' ? archivedEdicts : allEdicts)) { - statusCounts.all++; - const key = stateToFilterKey(t.state, t); - statusCounts[key] = (statusCounts[key] || 0) + 1; - } + // 统计 + const activeCount = MOCK_TASKS.filter(t => ['working', 'claimed'].includes(t.status)).length; + const doneCount = MOCK_TASKS.filter(t => t.status === 'done').length; + const failedCount = MOCK_TASKS.filter(t => t.status === 'failed').length; return (
- {/* Archive Bar */} -
- 筛选: - {(['active', 'archived', 'all'] as const).map((f) => ( - - ))} - {unArchivedDone.length > 0 && ( - - )} - - 活跃 {activeEdicts.length} · 归档 {archivedEdicts.length} · 共 {allEdicts.length} - - + {/* 顶部统计 */} +
+
+
活跃任务
+
{activeCount}
+
+
+
已完成
+
{doneCount}
+
+
+
失败/升级
+
{failedCount}
+
+
+
审查中
+
{MOCK_TASKS.filter(t => t.review_status !== 'none').length}
+
- {/* Status Filter + Search Bar */} -
- {STATUS_FILTERS.map((f) => ( - ))}
- setSearchQuery(e.target.value)} + setSearchQuery(e.target.value)} style={{ padding: '4px 10px 4px 28px', borderRadius: 6, fontSize: 12, - border: '1px solid #2a3550', background: '#161b2e', color: '#c0ccdd', - width: 200, outline: 'none', + border: '1px solid #2a3550', background: '#161b2e', color: '#c0ccdd', width: 200, outline: 'none', }} /> - 🔍 + 🔍
- {/* Batch Actions Bar */} - {selectedIds.size > 0 && ( -
- - ☑ 已选 {selectedIds.size} 道 - - - - -
- )} - - {/* Grid */} -
- {edicts.length === 0 ? ( -
- 暂无任务
- - 通过编排台提交任务,庞统分析后拆解为执行计划 - + {/* 卡片网格 */} +
+ {tasks.map(t => ( + setModalTaskId(t.id)} /> + ))} + {tasks.length === 0 && ( +
+ 暂无匹配任务
- ) : ( - edicts.map((t) => ( -
- { - const next = new Set(selectedIds); - if (next.has(t.id)) next.delete(t.id); else next.add(t.id); - setSelectedIds(next); - }} - style={{ - position: 'absolute', top: 8, left: 8, zIndex: 2, - width: 16, height: 16, cursor: 'pointer', - accentColor: 'var(--acc)', - }} - /> - -
- )) )}