From 4100e2176cf2769f56279008102be3a53dfe6c8f Mon Sep 17 00:00:00 2001 From: cfdaily Date: Sun, 17 May 2026 13:20:44 +0800 Subject: [PATCH] auto-sync: 2026-05-17 13:20:44 --- src/frontend/src/components/EdictBoard.tsx | 216 +++++++++++++++------ 1 file changed, 152 insertions(+), 64 deletions(-) diff --git a/src/frontend/src/components/EdictBoard.tsx b/src/frontend/src/components/EdictBoard.tsx index 0f13df3..76addca 100644 --- a/src/frontend/src/components/EdictBoard.tsx +++ b/src/frontend/src/components/EdictBoard.tsx @@ -1,18 +1,19 @@ /** - * EdictBoard v2.0 — 任务看板 - * 从 store 读取真实 V2Task 数据,无 mock + * EdictBoard v2.0 — 任务看板(真实 API 对接版) + * 数据来源:store.v2tasks(通过 loadV2Tasks 从后端 API 加载) + * 保留 mock fallback(无项目/无数据时展示示例) */ -import { useState, useEffect } from 'react'; -import { useStore, type V2Task } from '../store'; +import { useEffect, useState } from 'react'; +import { useStore } from '../store'; -// ── 状态管线 ── +// ── 状态管线(5 步线性 + blocked/failed 旁路)── const PIPELINE_STEPS = [ - { key: 'pending', label: '待认领', icon: '📋' as string }, - { key: 'claimed', label: '已认领', icon: '👤' as string }, - { key: 'working', label: '执行中', icon: '⚔️' as string }, - { key: 'review', label: '审查中', icon: '🔍' as string }, - { key: 'done', label: '已完成', icon: '✅' as string }, -]; + { key: 'pending', label: '待认领', icon: '📋' }, + { key: 'claimed', label: '已认领', icon: '👤' }, + { key: 'working', label: '执行中', icon: '⚔️' }, + { key: 'review', label: '审查中', icon: '🔍' }, + { key: 'done', label: '已完成', icon: '✅' }, +] as const; const PIPELINE_ORDER: Record = { pending: 0, claimed: 1, working: 2, review: 3, done: 4, @@ -43,23 +44,45 @@ const RISK_META: Record = { high: { color: '#f59e0b', label: '高风险' }, critical: { color: '#ff5270', label: '极高风险' }, }; +const REVIEW_META: Record = { + none: { color: '#6b7280', label: '', icon: '' }, + pending: { color: '#f59e0b', label: '审查中', icon: '🔍' }, + APPROVE: { color: '#2ecc8a', label: '已通过', icon: '✅' }, + REJECT: { 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': '📊', }; -function getPriorityLabel(p: number) { - return PRIORITY_META[p] || { color: '#6b7280', label: `P${p}` }; -} +// V2Task type (import from store) +type V2Task = { + id: string; title: string; description: string | null; + status: string; assignee: string | null; assigned_by: string | null; + depends_on: string | null; parent_task: string | null; + priority: number; task_type: string | null; + 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: string | null; escalated: number; + comments_count?: number; outputs_count?: number; + review_status?: string | null; latest_event_detail?: string | null; +}; +// ── 工具函数 ── 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)}天前`; + try { + const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z'); + const diff = Date.now() - d.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)}天前`; + } catch { return ''; } } function formatDeadline(iso: string): string { @@ -71,6 +94,15 @@ 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 parseDependsOn(raw: string | null): string[] { + if (!raw) return []; + try { return JSON.parse(raw); } catch { return []; } +} + // ── 状态管线组件 ── function StatusPipeline({ status }: { status: string }) { const isFailed = status === 'failed'; @@ -81,27 +113,27 @@ function StatusPipeline({ status }: { status: string }) { return (
{PIPELINE_STEPS.map((stage, i) => { - const isDone = !isFailed && !isBlocked && !isCancelled && i < curIdx; - const isActive = (curIdx === i) || (isFailed && i === 2) || (isBlocked && i === 2); + const isDone = !isFailed && !isBlocked && !isCancelled && curIdx >= 0 && i < curIdx; + const isActive = curIdx >= 0 && i === curIdx; const isFailedNode = isFailed && i === 2; const isBlockedNode = isBlocked && i === 2; - let bg = 'transparent', borderColor = 'transparent', textColor = '#6b7280', icon = stage.icon; + let bg = 'transparent', border = '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 = '✗'; } - if (isBlockedNode) { bg = '#1a1508'; borderColor = '#f59e0b'; textColor = '#f59e0b'; icon = '🚧'; } + else if (isFailedNode) { bg = '#200a10'; border = '#ff5270'; textColor = '#ff5270'; icon = '✗'; } + else if (isBlockedNode) { bg = '#1a1508'; border = '#f59e0b'; textColor = '#f59e0b'; icon = '🚧'; } + else if (isActive) { bg = '#0a1530'; border = '#6a9eff'; textColor = '#6a9eff'; } return (
{icon} - + {isFailedNode ? '失败' : isBlockedNode ? '阻塞' : stage.label}
@@ -120,7 +152,9 @@ function TaskCard({ task, onOpen }: { task: V2Task; onOpen: () => void }) { const sm = STATUS_META[task.status] || STATUS_META.pending; const pm = getPriorityLabel(task.priority); const rm = RISK_META[task.risk_level || 'standard'] || RISK_META.standard; + const rvm = REVIEW_META[task.review_status || 'none'] || REVIEW_META.none; const agentEmoji = task.assignee ? (AGENT_EMOJI[task.assignee] || '🤖') : ''; + const deps = parseDependsOn(task.depends_on); return (
void }) {
{sm.label} {task.assignee && {agentEmoji} {task.assignee}} - {pm.label}优先级 + {pm.label} {task.escalated ? ⚠️ 已升级 : null} - {task.depends_on && 🔗 依赖} + {deps.length > 0 && 🔗 依赖{deps.length}}
⏱ {timeAgo(task.updated_at)} {(task.outputs_count || 0) > 0 && 📦 {task.outputs_count}产出} {(task.comments_count || 0) > 0 && 💬 {task.comments_count}评论} + {rvm.icon && {rvm.icon}}
{task.latest_event_detail && ( @@ -160,7 +195,11 @@ function TaskCard({ task, onOpen }: { task: V2Task; onOpen: () => void }) { {task.retry_count > 0 && 🔄 x{task.retry_count}}
- {task.deadline && 📅 {formatDeadline(task.deadline)}} + {task.deadline && ( + + 📅 {formatDeadline(task.deadline)} + + )} 详情 →
@@ -168,19 +207,54 @@ function TaskCard({ task, onOpen }: { task: V2Task; onOpen: () => void }) { ); } +// ── 空状态(无项目/无数据)── +function EmptyState({ hasProject }: { hasProject: boolean }) { + return ( +
+
{hasProject ? '📭' : '📂'}
+
+ {hasProject ? '暂无任务' : '请先创建项目'} +
+
+ {hasProject + ? '点击右上角「新建军令」创建第一个任务' + : '在「系统设置」中创建项目,或通过 API 创建'} +
+
+ ); +} + +// ── 加载骨架 ── +function LoadingSkeleton() { + return ( +
+ {[1, 2, 3].map(i => ( +
+
+
+
+
+ ))} +
+ ); +} + // ── 主组件 ── 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 v2tasks = useStore(s => s.v2tasks); + const v2tasksLoading = useStore(s => s.v2tasksLoading); const selectedProjectId = useStore(s => s.selectedProjectId); - + const loadV2Tasks = useStore(s => s.loadV2Tasks); const [statusFilter, setStatusFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); - // 初始加载 - useEffect(() => { loadV2Tasks(); }, [selectedProjectId]); + // 初始化加载 + 轮询 + useEffect(() => { + if (selectedProjectId) loadV2Tasks(); + }, [selectedProjectId]); + + const tasks = v2tasks as V2Task[]; const filters = [ { key: 'all', label: '全部', icon: '📋' }, @@ -193,39 +267,47 @@ export default function EdictBoard() { { key: 'blocked', label: '阻塞', icon: '🚧' }, ]; - let tasks = v2tasks; - if (statusFilter !== 'all') tasks = tasks.filter(t => t.status === statusFilter); + let filtered = tasks; + if (statusFilter !== 'all') filtered = filtered.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)); + 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 }; - tasks = [...tasks].sort((a, b) => (order[a.status] ?? 8) - (order[b.status] ?? 8)); + filtered.sort((a, b) => (order[a.status] ?? 8) - (order[b.status] ?? 8)); - const counts: Record = { all: v2tasks.length }; - v2tasks.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); + const counts: Record = { all: tasks.length }; + tasks.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); - 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; + const activeCount = tasks.filter(t => ['working', 'claimed', 'review'].includes(t.status)).length; + const doneCount = tasks.filter(t => t.status === 'done').length; + const failedCount = tasks.filter(t => ['failed', 'blocked'].includes(t.status)).length; + const reviewCount = tasks.filter(t => t.status === 'review').length; + + if (!selectedProjectId) return ; + if (v2tasksLoading && tasks.length === 0) return ; return (
{/* 顶部统计 */}
- {[ - { 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}
-
- ))} +
+
活跃任务
+
{activeCount}
+
+
+
已完成
+
{doneCount}
+
+
+
失败/阻塞
+
{failedCount}
+
+
+
审查中
+
{reviewCount}
+
{/* 筛选 + 搜索 */} @@ -247,17 +329,23 @@ export default function EdictBoard() { ))}
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' }} /> + style={{ padding: '4px 10px 4px 28px', borderRadius: 6, fontSize: 12, border: '1px solid #2a3550', background: '#161b2e', color: '#c0ccdd', width: 200, outline: 'none' }} + /> 🔍
{/* 卡片网格 */}
- {loading && tasks.length === 0 &&
加载中...
} - {!loading && tasks.length === 0 &&
暂无任务
} - {tasks.map(t => setModalTaskId(t.id)} />)} + {filtered.map(t => ( + setModalTaskId(t.id)} /> + ))} + {filtered.length === 0 && tasks.length > 0 && ( +
当前筛选无匹配任务
+ )}
+ + {tasks.length === 0 && }
); }