All checks were successful
CI / test (push) Successful in 3m58s
任务队列页对齐评估列表: - 分页:始终显示(pageSize 10 + showSizeChanger + showTotal),此前仅在 >10 条时显示导致 t480(8 条)无分页器 - 列宽重新分配:评估列 230→300(长名称不截断)、原因列固定 240(ellipsis)、 入队/完成时间 170、队列状态 100、优先级 80 - 字体格式对齐评估列表:去掉 size="middle" 改用默认(与评估列表一致) tsc 0 错误 vitest 16 passed
150 lines
5.8 KiB
TypeScript
150 lines
5.8 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import {
|
||
Button, Empty, Space, Table, Tag, Tooltip, message,
|
||
} from 'antd'
|
||
import type { ColumnsType } from 'antd/es/table'
|
||
import { ReloadOutlined } from '@ant-design/icons'
|
||
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
|
||
import { usePolling } from '../../hooks/usePolling'
|
||
import { colors, statusColors } from '../../tokens'
|
||
import { formatDateTime } from '../../utils/date'
|
||
import { EVAL_STATUS } from './status'
|
||
|
||
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
|
||
pending: { label: '待认领', color: 'orange' },
|
||
assigned: { label: '执行中', color: 'blue' },
|
||
completed: { label: '已完成', color: 'green' },
|
||
failed: { label: '失败', color: 'red' },
|
||
}
|
||
|
||
type FilterKey = 'all' | 'unresolved' | TaskQueueStatus
|
||
|
||
/**
|
||
* 任务队列监控(方案③可视化,与评估列表页统一 stat-chip + 单行表格)。
|
||
*
|
||
* 平台每 60s 扫描 executing 评估入队 + 触发 OpenClaw worker 执行。本组件
|
||
* 展示任务明细与状态分布,5s 轮询;统计条点击筛选。
|
||
*/
|
||
export default function TaskQueueMonitor() {
|
||
const [tasks, setTasks] = useState<TaskQueueItem[] | null>(null)
|
||
const [stats, setStats] = useState<{
|
||
pending: number; assigned: number; completed: number; failed: number; unresolved: number
|
||
} | null>(null)
|
||
const [filter, setFilter] = useState<FilterKey>('all')
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
const loadData = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await intelligentEvalsApi.listTasks(
|
||
filter === 'all' || filter === 'unresolved' ? undefined : { status: filter },
|
||
)
|
||
setTasks(res.data.tasks)
|
||
setStats(res.data.stats)
|
||
} catch {
|
||
message.error('加载任务队列失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// Initial fetch + reload on filter change (usePolling owns the 5s interval).
|
||
useEffect(() => {
|
||
void loadData()
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [filter])
|
||
|
||
usePolling(() => { void loadData() }, 5000, true)
|
||
|
||
const chipMeta: { key: FilterKey; label: string; color?: string; get: () => number }[] = [
|
||
{ key: 'all', label: '全部任务', get: () => (stats ? Object.values(stats).reduce((a, b) => a + b, 0) : (tasks?.length ?? 0)) },
|
||
{ key: 'unresolved', label: '待处理', color: colors.warning, get: () => stats?.unresolved ?? 0 },
|
||
{ key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 },
|
||
{ key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 },
|
||
{ key: 'completed', label: '已完成', color: statusColors.completed, get: () => stats?.completed ?? 0 },
|
||
{ key: 'failed', label: '失败', color: statusColors.failed, get: () => stats?.failed ?? 0 },
|
||
]
|
||
|
||
const columns: ColumnsType<TaskQueueItem> = [
|
||
{
|
||
title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 170,
|
||
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
|
||
},
|
||
{
|
||
title: '评估', key: 'eval', width: 300, ellipsis: true,
|
||
render: (_, t) => (
|
||
<Space size={6}>
|
||
<span style={{ fontWeight: 500 }}>{t.eval_name ?? t.eval_id.slice(0, 8)}</span>
|
||
{t.eval_status && EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS] && (
|
||
<Tag color={EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].color} style={{ margin: 0, fontSize: 11 }}>
|
||
{EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].label}
|
||
</Tag>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '队列状态', dataIndex: 'status', key: 'status', width: 100,
|
||
render: (s: TaskQueueStatus) => {
|
||
const meta = STATUS_META[s]
|
||
return <Tag color={meta.color}>{meta.label}</Tag>
|
||
},
|
||
},
|
||
{
|
||
title: '优先级', dataIndex: 'priority', key: 'priority', width: 80,
|
||
render: (p: number) => <Tag>{p}</Tag>,
|
||
},
|
||
{
|
||
title: '原因', dataIndex: 'reason', key: 'reason', width: 240, ellipsis: true,
|
||
render: (r: string) => <Tooltip title={r}><span>{r}</span></Tooltip>,
|
||
},
|
||
{
|
||
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 170,
|
||
render: (v: string | null, t) => {
|
||
if (t.status === 'failed' && t.error) {
|
||
return <Tooltip title={t.error}><span style={{ color: statusColors.failed }}>失败</span></Tooltip>
|
||
}
|
||
return v ? formatDateTime(v) : <span style={{ color: colors.textMuted }}>—</span>
|
||
},
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||
<div className="stat-bar">
|
||
{chipMeta.map((c) => (
|
||
<div
|
||
key={c.key}
|
||
className={`stat-chip ${filter === c.key ? 'active' : ''}`}
|
||
onClick={() => setFilter(c.key)}
|
||
>
|
||
<div className="n" style={{ color: c.color ?? colors.text }}>{c.get()}</div>
|
||
<div className="l">{c.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{ background: colors.bgContainer, borderRadius: 8, padding: 16, flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
|
||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} />
|
||
</div>
|
||
<div style={{ height: 'calc(100% - 44px)', overflowY: 'auto' }}>
|
||
<Table
|
||
rowKey="id"
|
||
loading={loading}
|
||
dataSource={tasks ?? []}
|
||
columns={columns}
|
||
pagination={{
|
||
pageSize: 10,
|
||
showSizeChanger: true,
|
||
pageSizeOptions: [10, 20, 50],
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
}}
|
||
locale={{ emptyText: <Empty description="暂无任务" /> }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|