auto-sync: 2026-05-17 13:08:45
This commit is contained in:
@@ -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<string, number> = {
|
||||
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<string, { color: string; bg: string; label: string }> = {
|
||||
pending: { color: '#7a9aff', bg: '#0a1028', label: '待认领' },
|
||||
claimed: { color: '#a07aff', bg: '#110a28', label: '已认领' },
|
||||
@@ -72,33 +30,17 @@ const STATUS_META: Record<string, { color: string; bg: string; label: string }>
|
||||
cancelled: { color: '#6b7280', bg: '#141824', label: '已取消' },
|
||||
};
|
||||
|
||||
// P0: priority 整数映射
|
||||
const PRIORITY_META: Record<number, { color: string; label: string }> = {
|
||||
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<string, { color: string; label: string }> = {
|
||||
low: { color: '#2ecc8a', label: '低风险' },
|
||||
standard: { color: '#6b7280', label: '标准' },
|
||||
high: { color: '#f59e0b', label: '高风险' },
|
||||
critical: { color: '#ff5270', label: '极高风险' },
|
||||
};
|
||||
|
||||
const REVIEW_META: Record<string, { color: string; label: string; icon: string }> = {
|
||||
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<string, string> = {
|
||||
@@ -106,81 +48,10 @@ const AGENT_EMOJI: Record<string, string> = {
|
||||
'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 (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
|
||||
{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 (
|
||||
<div
|
||||
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',
|
||||
}}
|
||||
<div 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 = ''; }}
|
||||
>
|
||||
{/* 状态管线 */}
|
||||
<StatusPipeline status={task.status} />
|
||||
|
||||
{/* ID + 标题 */}
|
||||
<div style={{ fontSize: 10, color: 'var(--acc)', fontWeight: 700, letterSpacing: '.04em', margin: '8px 0 4px' }}>{task.id}</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, lineHeight: 1.4, marginBottom: 10 }}>{task.title}</div>
|
||||
|
||||
{/* 标签行 */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: `1px solid ${sm.color}44`, color: sm.color, background: sm.bg }}>{sm.label}</span>
|
||||
{task.assignee && (
|
||||
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: '1px solid #6b728044', color: '#dde4f8', background: '#141824' }}>
|
||||
{agentEmoji} {task.assignee}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: `1px solid ${pm.color}44`, color: pm.color, background: '#141824' }}>
|
||||
{pm.label}优先级
|
||||
</span>
|
||||
{task.escalated ? 1 : 0 ? (
|
||||
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: '1px solid #ff527044', color: '#ff5270', background: '#200a10' }}>
|
||||
⚠️ 已升级
|
||||
</span>
|
||||
) : null}
|
||||
{task.depends_on && (
|
||||
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: '1px solid #f59e0b44', color: '#f59e0b', background: '#1a1508' }}>
|
||||
🔗 依赖 {task.depends_on}
|
||||
</span>
|
||||
)}
|
||||
{task.assignee && <span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: '1px solid #6b728044', color: '#dde4f8', background: '#141824' }}>{agentEmoji} {task.assignee}</span>}
|
||||
<span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: `1px solid ${pm.color}44`, color: pm.color, background: '#141824' }}>{pm.label}优先级</span>
|
||||
{task.escalated ? <span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: '1px solid #ff527044', color: '#ff5270', background: '#200a10' }}>⚠️ 已升级</span> : null}
|
||||
{task.depends_on && <span style={{ fontSize: 10, padding: '2px 7px', borderRadius: 4, border: '1px solid #f59e0b44', color: '#f59e0b', background: '#1a1508' }}>🔗 依赖</span>}
|
||||
</div>
|
||||
|
||||
{/* 元信息行 */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, fontSize: 11, color: 'var(--muted)', marginBottom: 8 }}>
|
||||
<span>⏱ {timeAgo(task.updated_at)}</span>
|
||||
{task.outputs_count > 0 && <span>📦 {task.outputs_count}产出</span>}
|
||||
{task.comments_count > 0 && <span>💬 {task.comments_count}评论</span>}
|
||||
{rvm.icon && <span style={{ color: rvm.color }}>{rvm.icon}</span>}
|
||||
{(task.outputs_count || 0) > 0 && <span>📦 {task.outputs_count}产出</span>}
|
||||
{(task.comments_count || 0) > 0 && <span>💬 {task.comments_count}评论</span>}
|
||||
</div>
|
||||
|
||||
{/* 最新事件 */}
|
||||
{task.latest_event && (
|
||||
<div style={{
|
||||
fontSize: 10, color: '#8899aa', lineHeight: 1.4,
|
||||
padding: '3px 6px', background: '#1a1f2e', borderRadius: 4,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 8,
|
||||
}}>
|
||||
💬 {task.latest_event}
|
||||
{task.latest_event_detail && (
|
||||
<div style={{ fontSize: 10, color: '#8899aa', lineHeight: 1.4, padding: '3px 6px', background: '#1a1f2e', borderRadius: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 8 }}>
|
||||
💬 {task.latest_event_detail}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid var(--line)', paddingTop: 8 }}>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 10, padding: '1px 5px', borderRadius: 3, background: `${rm.color}22`, color: rm.color }}>{rm.label}</span>
|
||||
{task.retry_count > 0 && <span style={{ fontSize: 10, color: '#f59e0b' }}>🔄 x{task.retry_count}</span>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{task.deadline && (
|
||||
<span style={{ fontSize: 10, color: new Date(task.deadline).getTime() - Date.now() < 3600000 ? '#ff5270' : 'var(--muted)' }}>
|
||||
📅 {formatDeadline(task.deadline)}
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
fontSize: 10, padding: '2px 8px', borderRadius: 4,
|
||||
background: 'var(--acc)11', color: 'var(--acc)', border: '1px solid var(--acc)33',
|
||||
cursor: 'pointer',
|
||||
}}>详情 →</span>
|
||||
{task.deadline && <span style={{ fontSize: 10, color: new Date(task.deadline).getTime() - Date.now() < 3600000 ? '#ff5270' : 'var(--muted)' }}>📅 {formatDeadline(task.deadline)}</span>}
|
||||
<span style={{ fontSize: 10, padding: '2px 8px', borderRadius: 4, background: 'var(--acc)11', color: 'var(--acc)', border: '1px solid var(--acc)33', cursor: 'pointer' }}>详情 →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<string>('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<string, number> = { 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<string, number> = { all: MOCK_TASKS.length };
|
||||
MOCK_TASKS.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; });
|
||||
const counts: Record<string, number> = { 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 (
|
||||
<div>
|
||||
{/* 顶部统计 */}
|
||||
<div style={{ display: 'flex', gap: 10, marginBottom: 14 }}>
|
||||
<div style={{ flex: 1, padding: '10px 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 10 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 2 }}>活跃任务</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#6a9eff' }}>{activeCount}</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, padding: '10px 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 10 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 2 }}>已完成</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#2ecc8a' }}>{doneCount}</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, padding: '10px 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 10 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 2 }}>失败/阻塞</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#ff5270' }}>{failedCount}</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, padding: '10px 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 10 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 2 }}>审查中</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: '#818cf8' }}>{MOCK_TASKS.filter(t => t.status === 'review').length}</div>
|
||||
</div>
|
||||
{[
|
||||
{ label: '活跃任务', val: activeCount, color: '#6a9eff' },
|
||||
{ label: '已完成', val: doneCount, color: '#2ecc8a' },
|
||||
{ label: '失败/阻塞', val: failedCount, color: '#ff5270' },
|
||||
{ label: '审查中', val: reviewCount, color: '#818cf8' },
|
||||
].map(s => (
|
||||
<div key={s.label} style={{ flex: 1, padding: '10px 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 10 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', marginBottom: 2 }}>{s.label}</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700, color: s.color }}>{s.val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 筛选 + 搜索 */}
|
||||
@@ -414,36 +241,22 @@ export default function EdictBoard() {
|
||||
}}>
|
||||
<span>{f.icon}</span><span>{f.label}</span>
|
||||
{(counts[f.key] || 0) > 0 && (
|
||||
<span style={{
|
||||
fontSize: 10, background: statusFilter === f.key ? 'var(--acc)' : '#2a3550',
|
||||
color: statusFilter === f.key ? '#000' : '#8899aa',
|
||||
borderRadius: 8, padding: '0 4px', minWidth: 16, textAlign: 'center',
|
||||
}}>{counts[f.key]}</span>
|
||||
<span style={{ fontSize: 10, background: statusFilter === f.key ? 'var(--acc)' : '#2a3550', color: statusFilter === f.key ? '#000' : '#8899aa', borderRadius: 8, padding: '0 4px', minWidth: 16, textAlign: 'center' }}>{counts[f.key]}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div style={{ marginLeft: 'auto', position: 'relative' }}>
|
||||
<input type="text" placeholder="搜索任务..." value={searchQuery}
|
||||
onChange={e => 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',
|
||||
}}
|
||||
/>
|
||||
<input type="text" placeholder="搜索任务..." value={searchQuery} onChange={e => 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' }} />
|
||||
<span style={{ position: 'absolute', left: 8, top: '50%', transform: 'translateY(-50%)', fontSize: 12, color: '#556' }}>🔍</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片网格 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 12, marginTop: 12 }}>
|
||||
{tasks.map(t => (
|
||||
<TaskCard key={t.id} task={t} onOpen={() => setModalTaskId(t.id)} />
|
||||
))}
|
||||
{tasks.length === 0 && (
|
||||
<div style={{ gridColumn: '1/-1', textAlign: 'center', padding: 40, color: 'var(--muted)' }}>
|
||||
暂无匹配任务
|
||||
</div>
|
||||
)}
|
||||
{loading && tasks.length === 0 && <div style={{ gridColumn: '1/-1', textAlign: 'center', padding: 40, color: 'var(--muted)' }}>加载中...</div>}
|
||||
{!loading && tasks.length === 0 && <div style={{ gridColumn: '1/-1', textAlign: 'center', padding: 40, color: 'var(--muted)' }}>暂无任务</div>}
|
||||
{tasks.map(t => <TaskCard key={t.id} task={t} onOpen={() => setModalTaskId(t.id)} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user