import { useState, useEffect, useCallback, useMemo } from 'react' import { Table, Button, Progress, Result, Card, message, Popconfirm, Tooltip } from 'antd' import { DeleteOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons' import ContractCues from '../components/ContractCues' import DecisionBadge from '../components/DecisionBadge' import { StatusIcon, StatusTag } from '../components/StatusIcon' import { getDecision, getErrorMessage } from '../utils/contractView' export default function BatchManager() { const [tasks, setTasks] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchTasks = useCallback(async (showLoading = true) => { if (showLoading) 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 { if (showLoading) setLoading(false) } }, []) useEffect(() => { fetchTasks(true) const interval = setInterval(() => fetchTasks(false), 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(false) } 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(false) } catch (err) { message.error(err.message) } } const handleCopyTaskId = (taskId) => { navigator.clipboard.writeText(taskId).then(() => { message.success('已复制任务ID') }).catch(() => { message.error('复制失败') }) } const columns = useMemo(() => [ { title: '状态', key: 'status', width: 110, render: (_, record) => ( <> ), }, { title: '股票', dataIndex: 'ticker', key: 'ticker', render: (text) => ( {text} ), }, { title: '进度', dataIndex: 'progress', key: 'progress', width: 140, render: (val, record) => record.status === 'running' || record.status === 'pending' ? ( ) : ( {val || 0}% ), }, { title: '决策', key: 'decision', width: 180, render: (_, record) => (
), }, { title: '任务ID', dataIndex: 'task_id', key: 'task_id', width: 220, render: (text) => ( {text.slice(0, 18)}... ), }, { title: '错误', key: 'error', width: 180, ellipsis: { showTitle: false }, render: (_, record) => { const error = getErrorMessage(record) return error ? ( {error} ) : null }, }, { title: '操作', key: 'action', width: 120, render: (_, record) => (
{record.status === 'running' && ( handleCancel(record.task_id)} okText="确认" cancelText="取消" > )} {record.status === 'failed' && ( )}
), }, ], [tasks]) // eslint-disable-line react-hooks/exhaustive-deps const stats = useMemo(() => ({ pending: tasks.filter(t => t.status === 'pending').length, running: tasks.filter(t => t.status === 'running').length, completed: tasks.filter(t => t.status === 'completed' || t.status === 'degraded_success').length, failed: tasks.filter(t => t.status === 'failed').length, }), [tasks]) return (
{/* Compact stat strip */}
{[ { label: '等待中', key: 'pending', color: 'var(--text-muted)', border: 'var(--text-muted)' }, { label: '分析中', key: 'running', color: 'var(--running)', border: 'var(--running)' }, { label: '已完成', key: 'completed', color: 'var(--buy)', border: 'var(--buy)' }, { label: '失败', key: 'failed', color: 'var(--sell)', border: 'var(--sell)' }, ].map(({ label, key, color, border }) => (
{stats[key]}
{label}
))}
{/* Tasks Table */}
{loading && tasks.length === 0 ? (
加载中...
) : error && tasks.length === 0 ? ( fetchTasks(true)}> 重试 } /> ) : tasks.length === 0 ? (
暂无批量任务
在股票筛选页面提交分析任务
) : ( )} ) }