初步增加 web 界面

This commit is contained in:
yml2213
2026-06-22 13:11:15 +08:00
parent 4c924375aa
commit 347edb8103
66 changed files with 6816 additions and 21 deletions
+208
View File
@@ -0,0 +1,208 @@
import { useEffect, useState, useRef } from 'react';
import {
Table, Button, Select, message, Tag, Space, Card, Row, Col, Statistic, Spin,
} from 'antd';
import { PlayCircleOutlined, StopOutlined } from '@ant-design/icons';
import { accountApi, loginApi } from '../api/modules';
import { getUser, hasPerm } from '../store/auth';
const STATUS_COLORS: Record<string, string> = {
pending: 'default',
running: 'processing',
success: 'success',
failed: 'error',
error: 'error',
};
const STATUS_LABELS: Record<string, string> = {
pending: '等待中',
running: '登录中',
success: '成功',
failed: '失败',
error: '异常',
};
export default function LoginTasksPage() {
const [accounts, setAccounts] = useState<any[]>([]);
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [tasks, setTasks] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [batchId, setBatchId] = useState<string | null>(null);
const [logs, setLogs] = useState<{ level: string; message: string }[]>([]);
const wsRef = useRef<WebSocket | null>(null);
const user = getUser();
const canBatch = hasPerm(user, 'login:batch');
const loadAccounts = async () => {
try {
const data = await accountApi.list();
setAccounts(data);
} catch (e: any) {
message.error(e.message);
}
};
const loadTasks = async () => {
try {
const data = await loginApi.listTasks(batchId || undefined);
setTasks(data);
} catch {}
};
useEffect(() => {
loadAccounts();
}, []);
useEffect(() => {
const timer = setInterval(loadTasks, 3000);
return () => clearInterval(timer);
}, [batchId]);
const handleBatchLogin = async () => {
if (selectedIds.length === 0) {
message.warning('请选择账号');
return;
}
setLoading(true);
setLogs([]);
try {
const result = await loginApi.createBatch(selectedIds);
setBatchId(result.batch_id);
message.success(`已创建登录任务,共 ${result.count} 个账号`);
// 连接 WebSocket
const wsUrl = `ws://${window.location.hostname}:8000/api/login/ws/login/${result.batch_id}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.level === 'heartbeat') return;
setLogs((prev) => [...prev, msg]);
};
ws.onclose = () => {
wsRef.current = null;
};
} catch (e: any) {
message.error(e.message);
} finally {
setLoading(false);
}
};
const handleStop = async () => {
if (batchId) {
try {
await loginApi.stop(batchId);
message.success('已发送停止信号');
} catch (e: any) {
message.error(e.message);
}
}
};
const successCount = tasks.filter((t) => t.status === 'success').length;
const failedCount = tasks.filter((t) => ['failed', 'error'].includes(t.status)).length;
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '账号', dataIndex: 'account_username' },
{
title: '状态',
dataIndex: 'status',
render: (status: string) => <Tag color={STATUS_COLORS[status]}>{STATUS_LABELS[status] || status}</Tag>,
},
{ title: '消息', dataIndex: 'message', ellipsis: true },
{ title: '时间', dataIndex: 'created_at', width: 180 },
];
return (
<div>
<h2></h2>
{canBatch && (
<Card size="small" style={{ marginBottom: 16 }}>
<Space>
<Select
mode="multiple"
style={{ minWidth: 400 }}
placeholder="选择要登录的账号"
value={selectedIds}
onChange={setSelectedIds}
options={accounts.map((a) => ({ value: a.id, label: a.username }))}
maxTagCount="responsive"
/>
<Button
type="primary"
icon={<PlayCircleOutlined />}
loading={loading}
onClick={handleBatchLogin}
>
</Button>
{batchId && (
<Button danger icon={<StopOutlined />} onClick={handleStop}>
</Button>
)}
</Space>
</Card>
)}
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="任务总数" value={tasks.length} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="成功" value={successCount} valueStyle={{ color: '#3f8600' }} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="失败" value={failedCount} valueStyle={{ color: '#cf1322' }} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="当前批次" value={batchId || '-'} /></Card>
</Col>
</Row>
<Row gutter={16}>
<Col span={14}>
<Card title="任务列表" size="small">
<Table
columns={columns}
dataSource={tasks}
rowKey="id"
size="small"
pagination={{ pageSize: 15 }}
/>
</Card>
</Col>
<Col span={10}>
<Card
title="实时日志"
size="small"
bodyStyle={{ maxHeight: 500, overflow: 'auto', fontFamily: 'monospace', fontSize: 12 }}
>
{logs.length === 0 ? (
<Spin spinning={!!batchId} size="small" />
) : (
logs.map((log, i) => (
<div
key={i}
style={{
color:
log.level === 'error' ? '#ff0000' :
log.level === 'success' ? '#008000' :
log.level === 'warning' ? '#FF8C00' :
'#333',
}}
>
{log.message}
</div>
))
)}
</Card>
</Col>
</Row>
</div>
);
}