auto-sync: 2026-05-17 13:08:45

This commit is contained in:
cfdaily
2026-05-17 13:08:45 +08:00
parent f4aa62e84f
commit 7c03512a88
+69 -256
View File
@@ -1,48 +1,11 @@
/**
* EdictBoard v2.0 — 任务看板
* 数据模型:v2.6 黑板(扁平任务,无 DAG)
* Mock 数据用于 UI 预览
*
* 司马懿评审修复:
* - P0: priority 改为 number1-10),前端映射 label
* - P1: 状态管线补 review/blocked6步),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>
);