diff --git a/src/frontend/src/components/EdictBoard.tsx b/src/frontend/src/components/EdictBoard.tsx index f7ccf51..f61c04a 100644 --- a/src/frontend/src/components/EdictBoard.tsx +++ b/src/frontend/src/components/EdictBoard.tsx @@ -1,48 +1,11 @@ /** * EdictBoard v2.0 — 任务看板 - * 数据模型:v2.6 黑板(扁平任务,无 DAG) - * Mock 数据用于 UI 预览 - * - * 司马懿评审修复: - * - P0: priority 改为 number(1-10),前端映射 label - * - P1: 状态管线补 review/blocked(6步),VALID_TRANSITIONS 对齐后端 8 状态 - * - P2: V2Task 补 depends_on, parent_task 字段 + * 从 store 读取真实 V2Task 数据,无 mock */ -import { useState } from 'react'; -import { useStore } from '../store'; +import { useState, useEffect } from 'react'; +import { useStore, type V2Task } from '../store'; -// ── v2.0 Task 类型(对齐后端 tasks 表)── -export interface V2Task { - id: string; - title: string; - description: string; - status: 'pending' | 'claimed' | 'working' | 'review' | 'done' | 'failed' | 'blocked' | 'cancelled'; - assignee: string | null; - assigned_by: string | null; - depends_on: string | null; // P2: 依赖任务 - parent_task: string | null; // P2: 父任务 - priority: number; // P0: 1-10 整数 - task_type: string; - created_at: string; - updated_at: string; - claimed_at: string | null; - completed_at: string | null; - started_at: string | null; - deadline: string | null; - retry_count: number; - max_retries: number; - risk_level: 'low' | 'standard' | 'high' | 'critical'; - escalated: number; // 0/1 整数,与后端一致 - // 关联数据(API 聚合或 mock) - comments_count: number; - outputs_count: number; - review_status: 'none' | 'pending' | 'approved' | 'rejected' | 'rebuttal'; - latest_event: string | null; - project_id: string; -} - -// ── 状态管线(6 步,对齐后端 VALID_STATUSES)── -// blocked 不在线性管道上,用独立标记 +// ── 状态管线 ── const PIPELINE_STEPS = [ { key: 'pending', label: '待认领', icon: '📋' }, { key: 'claimed', label: '已认领', icon: '👤' }, @@ -51,16 +14,11 @@ const PIPELINE_STEPS = [ { key: 'done', label: '已完成', icon: '✅' }, ] as const; -// 状态在线性管线中的位置索引(用于判断 done/active/pending) const PIPELINE_ORDER: Record = { pending: 0, claimed: 1, working: 2, review: 3, done: 4, - // 以下不在线性管道上 - failed: 2, // 标记在 working 位置 - blocked: 2, // 标记在 working 位置 - cancelled: -1, + failed: 2, blocked: 2, cancelled: -1, }; -// ── 状态元信息(8 状态全覆盖)── const STATUS_META: Record = { pending: { color: '#7a9aff', bg: '#0a1028', label: '待认领' }, claimed: { color: '#a07aff', bg: '#110a28', label: '已认领' }, @@ -72,33 +30,17 @@ const STATUS_META: Record cancelled: { color: '#6b7280', bg: '#141824', label: '已取消' }, }; -// P0: priority 整数映射 const PRIORITY_META: Record = { - 1: { color: '#6b7280', label: '低' }, - 2: { color: '#6b7280', label: '低' }, - 3: { color: '#3b82f6', label: '中' }, - 4: { color: '#3b82f6', label: '中' }, - 5: { color: '#f59e0b', label: '高' }, - 6: { color: '#f59e0b', label: '高' }, - 7: { color: '#ff5270', label: '紧急' }, - 8: { color: '#ff5270', label: '紧急' }, - 9: { color: '#ff5270', label: '紧急' }, - 10: { color: '#ff5270', label: '紧急' }, + 1: { color: '#6b7280', label: '低' }, 2: { color: '#6b7280', label: '低' }, + 3: { color: '#3b82f6', label: '中' }, 4: { color: '#3b82f6', label: '中' }, + 5: { color: '#f59e0b', label: '高' }, 6: { color: '#f59e0b', label: '高' }, + 7: { color: '#ff5270', label: '紧急' }, 8: { color: '#ff5270', label: '紧急' }, + 9: { color: '#ff5270', label: '紧急' }, 10: { 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: '⚔️' }, + low: { color: '#2ecc8a', label: '低风险' }, standard: { color: '#6b7280', label: '标准' }, + high: { color: '#f59e0b', label: '高风险' }, critical: { color: '#ff5270', label: '极高风险' }, }; const AGENT_EMOJI: Record = { @@ -106,81 +48,10 @@ const AGENT_EMOJI: Record = { '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', - depends_on: null, parent_task: null, - priority: 7, 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', - deadline: '2026-05-17T18:00:00', retry_count: 0, max_retries: 3, risk_level: 'standard', escalated: 0, - 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', - depends_on: null, parent_task: null, - priority: 3, 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', - deadline: null, retry_count: 0, max_retries: 2, risk_level: 'low', escalated: 0, - 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', - depends_on: null, parent_task: null, - priority: 9, 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, - deadline: '2026-05-18T18:00:00', retry_count: 0, max_retries: 2, risk_level: 'critical', escalated: 0, - 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', - depends_on: 'task-006', parent_task: null, - priority: 5, 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, - deadline: null, retry_count: 0, max_retries: 2, risk_level: 'standard', escalated: 0, - 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', - depends_on: null, parent_task: 'task-001', - priority: 7, 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', - deadline: null, retry_count: 2, max_retries: 3, risk_level: 'high', escalated: 1, - comments_count: 8, outputs_count: 0, - review_status: 'rebuttal', latest_event: '网格搜索内存溢出,第2次重试失败', project_id: 'demo', - }, - { - id: 'task-006', title: '滑点模型实现', description: '实现基于成交量加权的滑点模型,支持市价单和限价单', - status: 'review', assignee: 'guanyu-dev', assigned_by: 'pangtong-fujunshi', - depends_on: null, parent_task: null, - priority: 6, task_type: 'coding', created_at: '2026-05-17T07:30:00', updated_at: '2026-05-17T11:30:00', - claimed_at: '2026-05-17T07:45:00', completed_at: null, started_at: '2026-05-17T07:46:00', - deadline: '2026-05-17T20:00:00', retry_count: 0, max_retries: 2, risk_level: 'standard', escalated: 0, - comments_count: 1, outputs_count: 1, - review_status: 'pending', latest_event: '产出已提交,等待司马懿审查', project_id: 'demo', - }, - { - id: 'task-007', title: '回测数据下载', description: '从 NAS 下载沪深300历史日线数据(2020-2025)', - status: 'blocked', assignee: 'zhaoyun-data', assigned_by: 'pangtong-fujunshi', - depends_on: 'task-003', parent_task: null, - priority: 4, task_type: 'data', created_at: '2026-05-17T06:30:00', updated_at: '2026-05-17T10:30:00', - claimed_at: '2026-05-17T06:35:00', completed_at: null, started_at: '2026-05-17T06:36:00', - deadline: null, retry_count: 0, max_retries: 2, risk_level: 'low', escalated: 0, - comments_count: 1, outputs_count: 0, - review_status: 'none', latest_event: '等待 vnpy 网关升级完成后才能下载', project_id: 'demo', - }, -]; +function getPriorityLabel(p: number) { + return PRIORITY_META[p] || { color: '#6b7280', label: `P${p}` }; +} -// ── 工具函数 ── function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60000); @@ -200,13 +71,8 @@ function formatDeadline(iso: string): string { return `${Math.floor(hrs / 24)}天后`; } -function getPriorityLabel(p: number): { color: string; label: string } { - return PRIORITY_META[p] || { color: '#6b7280', label: `P${p}` }; -} - // ── 状态管线组件 ── function StatusPipeline({ status }: { status: string }) { - const isLinear = ['pending', 'claimed', 'working', 'review', 'done'].includes(status); const isFailed = status === 'failed'; const isBlocked = status === 'blocked'; const isCancelled = status === 'cancelled'; @@ -215,16 +81,12 @@ function StatusPipeline({ status }: { status: string }) { return (
{PIPELINE_STEPS.map((stage, i) => { - const isDone = isLinear && i < curIdx; - const isActive = (isLinear && i === curIdx) || (isFailed && i === 2) || (isBlocked && i === 2); + const isDone = !isFailed && !isBlocked && !isCancelled && i < curIdx; + const isActive = (curIdx === i) || (isFailed && i === 2) || (isBlocked && i === 2); const isFailedNode = isFailed && i === 2; const isBlockedNode = isBlocked && i === 2; - let bg = 'transparent'; - let borderColor = 'transparent'; - let textColor = '#6b7280'; - let icon = stage.icon; - + let bg = 'transparent', borderColor = 'transparent', textColor = '#6b7280', icon = stage.icon; if (isDone) { bg = '#0a2018'; textColor = '#2ecc8a'; icon = '✓'; } if (isActive && !isFailedNode && !isBlockedNode) { bg = '#0a1530'; borderColor = '#6a9eff'; textColor = '#6a9eff'; } if (isFailedNode) { bg = '#200a10'; borderColor = '#ff5270'; textColor = '#ff5270'; icon = '✗'; } @@ -255,88 +117,51 @@ function StatusPipeline({ status }: { status: string }) { // ── 任务卡片 ── function TaskCard({ task, onOpen }: { task: V2Task; onOpen: () => void }) { - const sm = STATUS_META[task.status]; + const sm = STATUS_META[task.status] || STATUS_META.pending; const pm = getPriorityLabel(task.priority); - const rm = RISK_META[task.risk_level]; - const rvm = REVIEW_META[task.review_status]; + const rm = RISK_META[task.risk_level || 'standard'] || RISK_META.standard; const agentEmoji = task.assignee ? (AGENT_EMOJI[task.assignee] || '🤖') : ''; return ( -
{ 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 = ''; }} > - {/* 状态管线 */} - - {/* ID + 标题 */}
{task.id}
{task.title}
- {/* 标签行 */}
{sm.label} - {task.assignee && ( - - {agentEmoji} {task.assignee} - - )} - - {pm.label}优先级 - - {task.escalated ? 1 : 0 ? ( - - ⚠️ 已升级 - - ) : null} - {task.depends_on && ( - - 🔗 依赖 {task.depends_on} - - )} + {task.assignee && {agentEmoji} {task.assignee}} + {pm.label}优先级 + {task.escalated ? ⚠️ 已升级 : null} + {task.depends_on && 🔗 依赖}
- {/* 元信息行 */}
⏱ {timeAgo(task.updated_at)} - {task.outputs_count > 0 && 📦 {task.outputs_count}产出} - {task.comments_count > 0 && 💬 {task.comments_count}评论} - {rvm.icon && {rvm.icon}} + {(task.outputs_count || 0) > 0 && 📦 {task.outputs_count}产出} + {(task.comments_count || 0) > 0 && 💬 {task.comments_count}评论}
- {/* 最新事件 */} - {task.latest_event && ( -
- 💬 {task.latest_event} + {task.latest_event_detail && ( +
+ 💬 {task.latest_event_detail}
)} - {/* 底部 */}
{rm.label} {task.retry_count > 0 && 🔄 x{task.retry_count}}
- {task.deadline && ( - - 📅 {formatDeadline(task.deadline)} - - )} - 详情 → + {task.deadline && 📅 {formatDeadline(task.deadline)}} + 详情 →
@@ -345,10 +170,18 @@ function TaskCard({ task, onOpen }: { task: V2Task; onOpen: () => void }) { // ── 主组件 ── export default function EdictBoard() { + const v2tasks = useStore(s => s.v2tasks); + const loading = useStore(s => s.v2tasksLoading); const setModalTaskId = useStore(s => s.setModalTaskId); + const loadV2Tasks = useStore(s => s.loadV2Tasks); + const selectedProjectId = useStore(s => s.selectedProjectId); + const [statusFilter, setStatusFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); + // 初始加载 + useEffect(() => { loadV2Tasks(); }, [selectedProjectId]); + const filters = [ { key: 'all', label: '全部', icon: '📋' }, { key: 'pending', label: '待认领', icon: '📋' }, @@ -360,45 +193,39 @@ export default function EdictBoard() { { key: 'blocked', label: '阻塞', icon: '🚧' }, ]; - let tasks = MOCK_TASKS; + let tasks = v2tasks; if (statusFilter !== 'all') tasks = tasks.filter(t => t.status === statusFilter); if (searchQuery.trim()) { const q = searchQuery.trim().toLowerCase(); - tasks = tasks.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)); } - // 排序:working > review > claimed > blocked > pending > failed > done const order: Record = { working: 0, review: 1, claimed: 2, blocked: 3, pending: 4, failed: 5, done: 6, cancelled: 7 }; - tasks.sort((a, b) => (order[a.status] ?? 8) - (order[b.status] ?? 8)); + tasks = [...tasks].sort((a, b) => (order[a.status] ?? 8) - (order[b.status] ?? 8)); - const counts: Record = { all: MOCK_TASKS.length }; - MOCK_TASKS.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); + const counts: Record = { all: v2tasks.length }; + v2tasks.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); - // 统计 - const activeCount = MOCK_TASKS.filter(t => ['working', 'claimed', 'review'].includes(t.status)).length; - const doneCount = MOCK_TASKS.filter(t => t.status === 'done').length; - const failedCount = MOCK_TASKS.filter(t => ['failed', 'blocked'].includes(t.status)).length; + const activeCount = v2tasks.filter(t => ['working', 'claimed', 'review'].includes(t.status)).length; + const doneCount = v2tasks.filter(t => t.status === 'done').length; + const failedCount = v2tasks.filter(t => ['failed', 'blocked'].includes(t.status)).length; + const reviewCount = v2tasks.filter(t => t.status === 'review').length; return (
{/* 顶部统计 */}
-
-
活跃任务
-
{activeCount}
-
-
-
已完成
-
{doneCount}
-
-
-
失败/阻塞
-
{failedCount}
-
-
-
审查中
-
{MOCK_TASKS.filter(t => t.status === 'review').length}
-
+ {[ + { label: '活跃任务', val: activeCount, color: '#6a9eff' }, + { label: '已完成', val: doneCount, color: '#2ecc8a' }, + { label: '失败/阻塞', val: failedCount, color: '#ff5270' }, + { label: '审查中', val: reviewCount, color: '#818cf8' }, + ].map(s => ( +
+
{s.label}
+
{s.val}
+
+ ))}
{/* 筛选 + 搜索 */} @@ -414,36 +241,22 @@ export default function EdictBoard() { }}> {f.icon}{f.label} {(counts[f.key] || 0) > 0 && ( - {counts[f.key]} + {counts[f.key]} )} ))}
- 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', - }} - /> + 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' }} /> 🔍
{/* 卡片网格 */}
- {tasks.map(t => ( - setModalTaskId(t.id)} /> - ))} - {tasks.length === 0 && ( -
- 暂无匹配任务 -
- )} + {loading && tasks.length === 0 &&
加载中...
} + {!loading && tasks.length === 0 &&
暂无任务
} + {tasks.map(t => setModalTaskId(t.id)} />)}
);