185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Table, Tag, Card, Row, Col, Statistic, Button, Tooltip } from 'antd'
|
|
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
|
import { useAuthStore } from '../../stores/auth'
|
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
|
import { getPhaseDisplay, getPhaseSummary } from '../../utils/phaseProgress'
|
|
|
|
interface Task {
|
|
id: number
|
|
collector?: string
|
|
datasource_name?: string
|
|
status: 'success' | 'failed' | 'running' | 'pending'
|
|
phase?: string | null
|
|
phase_progress?: number | null
|
|
phase_message?: string | null
|
|
phase_current?: number | null
|
|
phase_total?: number | null
|
|
phase_unit?: string | null
|
|
records_processed: number
|
|
total_records?: number | null
|
|
progress?: number | null
|
|
started_at: string
|
|
completed_at: string
|
|
duration_seconds: number
|
|
}
|
|
|
|
function Tasks() {
|
|
const { token } = useAuthStore()
|
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [stats, setStats] = useState({
|
|
total_today: 0,
|
|
success: 0,
|
|
failed: 0,
|
|
running: 0,
|
|
})
|
|
|
|
const fetchTasks = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch('/api/v1/tasks', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
const data = await res.json()
|
|
setTasks(data.data || [])
|
|
setStats(data.stats || stats)
|
|
} catch (error) {
|
|
console.error('Failed to fetch tasks:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchTasks()
|
|
}, [token])
|
|
|
|
const columns = [
|
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
|
{
|
|
title: '收集器',
|
|
dataIndex: 'collector',
|
|
key: 'collector',
|
|
render: (_: string, task: Task) => <Tag color="blue">{task.collector || task.datasource_name || '-'}</Tag>,
|
|
},
|
|
{
|
|
title: '阶段',
|
|
dataIndex: 'phase',
|
|
key: 'phase',
|
|
render: (_: string, task: Task) => {
|
|
const detail = task.phase ? getPhaseDisplay(task) : null
|
|
return detail ? (
|
|
<Tooltip title={detail}>
|
|
<Tag color={task.status === 'running' ? 'processing' : 'default'}>{getPhaseSummary(task)}</Tag>
|
|
</Tooltip>
|
|
) : '-'
|
|
},
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
render: (status: string) => {
|
|
const colors: Record<string, string> = {
|
|
success: 'success',
|
|
failed: 'error',
|
|
running: 'processing',
|
|
pending: 'default',
|
|
}
|
|
const icons: Record<string, JSX.Element> = {
|
|
success: <CheckCircleOutlined />,
|
|
failed: <CloseCircleOutlined />,
|
|
running: <SyncOutlined spin />,
|
|
pending: <SyncOutlined />,
|
|
}
|
|
return (
|
|
<Tag color={colors[status] || 'default'} icon={icons[status]}>
|
|
{status === 'success' ? '成功' : status === 'failed' ? '失败' : status === 'running' ? '运行中' : '等待中'}
|
|
</Tag>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
title: '处理记录',
|
|
dataIndex: 'records_processed',
|
|
key: 'records_processed',
|
|
render: (n: number) => n.toLocaleString(),
|
|
},
|
|
{
|
|
title: '耗时',
|
|
dataIndex: 'duration_seconds',
|
|
key: 'duration_seconds',
|
|
render: (s: number) => s ? `${s.toFixed(2)}s` : '-',
|
|
},
|
|
{
|
|
title: '开始时间',
|
|
dataIndex: 'started_at',
|
|
key: 'started_at',
|
|
render: (t: string) => formatDateTimeZhCN(t),
|
|
},
|
|
]
|
|
|
|
const statusCounts = tasks.reduce(
|
|
(acc, task) => {
|
|
acc[task.status]++
|
|
return acc
|
|
},
|
|
{ success: 0, failed: 0, running: 0, pending: 0 } as Record<string, number>
|
|
)
|
|
|
|
const successRate = tasks.length > 0 ? (statusCounts.success / tasks.length) * 100 : 0
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="page-shell tasks-page">
|
|
<Row gutter={[16, 16]}>
|
|
<Col xs={24} md={6}>
|
|
<Card>
|
|
<Statistic title="今日任务" value={tasks.length} prefix={<ReloadOutlined />} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="成功率"
|
|
value={successRate.toFixed(1)}
|
|
suffix="%"
|
|
valueStyle={{ color: successRate >= 90 ? '#52c41a' : '#faad14' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={6}>
|
|
<Card>
|
|
<Statistic title="成功" value={statusCounts.success} valueStyle={{ color: '#52c41a' }} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={6}>
|
|
<Card>
|
|
<Statistic title="失败" value={statusCounts.failed} valueStyle={{ color: '#ff4d4f' }} />
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Card
|
|
className="tasks-page__table-card"
|
|
title="任务历史"
|
|
extra={
|
|
<Button type="primary" icon={<ReloadOutlined />} onClick={fetchTasks}>
|
|
刷新
|
|
</Button>
|
|
}
|
|
>
|
|
<TableScrollRegion className="data-source-table-region">
|
|
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 'max-content', y: 480 }} tableLayout="fixed" />
|
|
</TableScrollRegion>
|
|
</Card>
|
|
</div>
|
|
</AppLayout>
|
|
)
|
|
}
|
|
|
|
export default Tasks
|