import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Card, Col, Input, InputNumber, message, Popconfirm, Row, Space, Statistic, Switch, Table, Tag, Typography, } from 'antd'; import type { TableProps } from 'antd'; import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'; import { accountCheckApi, type AccountCheckBatch, type AccountCheckItem } from '../api/modules'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; const { TextArea } = Input; const { Text, Title } = Typography; const FORM_STORAGE_KEY = 'douyu_account_check_form'; const BATCH_STORAGE_KEY = 'douyu_account_check_batch_id'; const STATUS_LABELS: Record = { pending: '等待', running: '检测中', account_cancelled: '账号已注销', password_wrong: '账号密码错误', account_unverified: '账号未认证', account_verified: '账号已认证', account_auth_unknown: '认证状态未知', error: '检测失败', stopped: '已停止', }; const STATUS_COLORS: Record = { pending: 'default', running: 'processing', account_cancelled: 'error', password_wrong: 'error', account_unverified: 'warning', account_verified: 'success', account_auth_unknown: 'warning', error: 'error', stopped: 'warning', }; const FINISHED_BATCH_STATUS = new Set(['finished', 'stopped', 'error']); const RUNNING_BATCH_STATUS = new Set(['pending', 'running']); function readStoredText() { try { return localStorage.getItem(FORM_STORAGE_KEY) || ''; } catch { return ''; } } function readStoredBatchId() { try { return localStorage.getItem(BATCH_STORAGE_KEY) || ''; } catch { return ''; } } function readStoredNumber(key: string, fallback: number) { try { const raw = localStorage.getItem(key); if (raw === null) return fallback; const value = Number(raw); return Number.isFinite(value) ? value : fallback; } catch { return fallback; } } function downloadBlob(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); } function fileTimestamp() { const d = new Date(); const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; } export default function AccountCheckPage() { const [text, setText] = useState(readStoredText); const [concurrency, setConcurrency] = useState(() => readStoredNumber('account_check_concurrency', 3)); const [maxTotalTime, setMaxTotalTime] = useState(() => readStoredNumber('account_check_max_total_time', 300)); const [maxLoginRetries, setMaxLoginRetries] = useState(() => readStoredNumber('account_check_max_login_retries', 0)); const [useProxy, setUseProxy] = useState(() => localStorage.getItem('account_check_use_proxy') === 'true'); const [batch, setBatch] = useState(null); const [starting, setStarting] = useState(false); const [refreshing, setRefreshing] = useState(false); const [stopping, setStopping] = useState(false); const [downloading, setDownloading] = useState(false); const restoredBatchRef = useRef(false); const batchId = batch?.batch_id || ''; const isRunning = !!batch && RUNNING_BATCH_STATUS.has(batch.status); const canDownload = !!batch && FINISHED_BATCH_STATUS.has(batch.status); const loadBatchById = useCallback(async (id: string) => { if (!id) return; setRefreshing(true); try { const data = await accountCheckApi.getBatch(id); setBatch(data); localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id); } catch (e: unknown) { const err = getErrorMessage(e); if (err.includes('批次不存在') || err.includes('服务已重启')) { localStorage.removeItem(BATCH_STORAGE_KEY); setBatch(null); message.warning('上次账号检测批次已不存在,已清除恢复记录'); } else { message.error(err); } } finally { setRefreshing(false); } }, []); const refreshBatch = useCallback(async () => { if (!batchId) return; await loadBatchById(batchId); }, [batchId, loadBatchById]); useEffect(() => { localStorage.setItem(FORM_STORAGE_KEY, text); }, [text]); useEffect(() => { localStorage.setItem('account_check_concurrency', String(concurrency)); }, [concurrency]); useEffect(() => { localStorage.setItem('account_check_max_total_time', String(maxTotalTime)); }, [maxTotalTime]); useEffect(() => { localStorage.setItem('account_check_max_login_retries', String(maxLoginRetries)); }, [maxLoginRetries]); useEffect(() => { localStorage.setItem('account_check_use_proxy', String(useProxy)); }, [useProxy]); useEffect(() => { if (restoredBatchRef.current) return; restoredBatchRef.current = true; const storedBatchId = readStoredBatchId(); if (storedBatchId) { loadBatchById(storedBatchId); } }, [loadBatchById]); useEffect(() => { if (!isRunning || !batchId) return undefined; const timer = window.setInterval(() => { refreshBatch(); }, 2000); return () => window.clearInterval(timer); }, [batchId, isRunning, refreshBatch]); const statusCounts = batch?.status_counts || {}; const categoryStats = useMemo(() => [ { title: '已注销', value: statusCounts.account_cancelled || 0 }, { title: '密码错误', value: statusCounts.password_wrong || 0 }, { title: '未认证', value: statusCounts.account_unverified || 0 }, { title: '已认证', value: statusCounts.account_verified || 0 }, ], [statusCounts]); const handleStart = async () => { if (!text.trim()) { message.warning('请先导入账号'); return; } setStarting(true); try { const data = await accountCheckApi.start({ text, concurrency, max_total_time: maxTotalTime, max_login_retries: maxLoginRetries, use_proxy: useProxy, }); setBatch(data); localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id); message.success(`账号检测批次已启动,共 ${data.total} 个账号`); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStarting(false); } }; const handleStop = async () => { if (!batchId) return; setStopping(true); try { const result = await accountCheckApi.stop(batchId); message.success(result.message); refreshBatch(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStopping(false); } }; const handleDownload = async () => { if (!batchId) return; setDownloading(true); try { const blob = await accountCheckApi.download(batchId); downloadBlob(blob instanceof Blob ? blob : new Blob([blob]), `account-check-${fileTimestamp()}.zip`); message.success('已下载压缩包'); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setDownloading(false); } }; const columns: TableProps['columns'] = [ { title: '行', dataIndex: 'line', width: 70 }, { title: '账号', dataIndex: 'username', width: 180, ellipsis: true }, { title: '邮箱', dataIndex: 'email', width: 220, ellipsis: true }, { title: '状态', dataIndex: 'status', width: 130, render: (status: string) => ( {STATUS_LABELS[status] || status} ), }, { title: '消息', dataIndex: 'message', ellipsis: true }, { title: '完成时间', dataIndex: 'finished_at', width: 180, render: (value: string | null) => formatTime(value), }, ]; return ( 账号检测