import { useState, useEffect, useCallback } from 'react' import { Table, Button, Tag, Progress, Result, Empty, Tabs, InputNumber, Card, Skeleton, message } from 'antd' import { PlayCircleOutlined, PauseCircleOutlined, DeleteOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined, } from '@ant-design/icons' const MAX_CONCURRENT = 3 export default function BatchManager() { const [tasks, setTasks] = useState([]) const [maxConcurrent, setMaxConcurrent] = useState(MAX_CONCURRENT) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchTasks = useCallback(async () => { setLoading(true) try { const res = await fetch('/api/analysis/tasks') if (!res.ok) throw new Error('获取任务列表失败') const data = await res.json() setTasks(data.tasks || []) setError(null) } catch (err) { setError(err.message) } finally { setLoading(false) } }, []) useEffect(() => { fetchTasks() const interval = setInterval(fetchTasks, 5000) return () => clearInterval(interval) }, [fetchTasks]) const handleCancel = async (taskId) => { try { const res = await fetch(`/api/analysis/cancel/${taskId}`, { method: 'DELETE' }) if (!res.ok) throw new Error('取消失败') message.success('任务已取消') fetchTasks() } catch (err) { message.error(err.message) } } const handleRetry = async (taskId) => { const task = tasks.find(t => t.task_id === taskId) if (!task) return try { const res = await fetch('/api/analysis/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticker: task.ticker }), }) if (!res.ok) throw new Error('重试失败') message.success('任务已重新提交') fetchTasks() } catch (err) { message.error(err.message) } } const getStatusIcon = (status) => { switch (status) { case 'completed': return case 'running': return case 'failed': return default: return } } const getDecisionBadge = (decision) => { if (!decision) return null const colorMap = { BUY: 'var(--color-buy)', SELL: 'var(--color-sell)', HOLD: 'var(--color-hold)', } return ( {decision} ) } const getStatusTag = (task) => { const statusMap = { pending: { text: '等待', color: 'var(--color-hold)' }, running: { text: '分析中', color: 'var(--color-running)' }, completed: { text: '完成', color: 'var(--color-buy)' }, failed: { text: '失败', color: 'var(--color-sell)' }, } const s = statusMap[task.status] return ( {s.text} ) } const columns = [ { title: '状态', key: 'status', width: 100, render: (_, record) => (
{getStatusIcon(record.status)} {getStatusTag(record)}
), }, { title: '股票', key: 'stock', render: (_, record) => (
{record.ticker}
), }, { title: '进度', dataIndex: 'progress', key: 'progress', width: 150, render: (val, record) => record.status === 'running' || record.status === 'pending' ? ( ) : ( {val}% ), }, { title: '决策', dataIndex: 'decision', key: 'decision', width: 80, render: (decision) => getDecisionBadge(decision), }, { title: '任务ID', dataIndex: 'task_id', key: 'task_id', width: 200, render: (text) => ( {text} ), }, { title: '错误', dataIndex: 'error', key: 'error', render: (error) => error ? ( {error} ) : null, }, { title: '操作', key: 'action', width: 150, render: (_, record) => (
{record.status === 'running' && ( )} {record.status === 'failed' && ( )}
), }, ] const pendingCount = tasks.filter((t) => t.status === 'pending').length const runningCount = tasks.filter((t) => t.status === 'running').length const completedCount = tasks.filter((t) => t.status === 'completed').length const failedCount = tasks.filter((t) => t.status === 'failed').length return (
{/* Stats */}
{pendingCount}
等待中
{runningCount}
分析中
{completedCount}
已完成
{failedCount}
失败
{/* Settings */}
最大并发数: setMaxConcurrent(val)} style={{ width: 80 }} /> 同时运行的分析任务数量
{/* Tasks Table */}
{loading ? ( ) : error ? ( { fetchTasks() }} aria-label="重试" > 重试 } /> ) : tasks.length === 0 ? ( } /> ) : ( )} ) }