import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Switch, Table, Tag, Typography, } from 'antd'; import type { TableProps } from 'antd'; import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'; import { huyaApi, type HuyaAutoRegisterBatch, type HuyaAutoRegisterItem } from '../api/modules'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; const { TextArea } = Input; const { Text, Title } = Typography; type PasswordMode = 'random' | 'fixed'; interface StoredRegisterForm { text: string; tag: string; concurrency: number; waitSeconds: number; pollInterval: number; passwordMode: PasswordMode; passwordPrefix: string; fixedPassword: string; useProxy: boolean; } const FORM_STORAGE_KEY = 'huya_auto_register_form'; const BATCH_STORAGE_KEY = 'huya_auto_register_batch_id'; const DEFAULT_FORM: StoredRegisterForm = { text: '', tag: '', concurrency: 1, waitSeconds: 180, pollInterval: 5, passwordMode: 'random', passwordPrefix: 'hy', fixedPassword: '', useProxy: false, }; const STATUS_LABELS: Record = { pending: '等待', sending: '发码', waiting: '等待验证码', logging: '保存', changing: '改密', success: '成功', error: '失败', stopped: '已停止', }; const STATUS_COLORS: Record = { pending: 'default', sending: 'processing', waiting: 'processing', logging: 'processing', changing: 'processing', success: 'success', error: 'error', stopped: 'warning', }; const RUNNING_STATUS = new Set(['pending', 'running']); function safeNumber(value: unknown, fallback: number) { const n = Number(value); return Number.isFinite(n) ? n : fallback; } function readStoredForm(): StoredRegisterForm { try { const raw = localStorage.getItem(FORM_STORAGE_KEY); if (!raw) return DEFAULT_FORM; const parsed = JSON.parse(raw) as Partial; const passwordMode = parsed.passwordMode === 'fixed' ? 'fixed' : 'random'; return { ...DEFAULT_FORM, ...parsed, concurrency: safeNumber(parsed.concurrency, DEFAULT_FORM.concurrency), waitSeconds: safeNumber(parsed.waitSeconds, DEFAULT_FORM.waitSeconds), pollInterval: safeNumber(parsed.pollInterval, DEFAULT_FORM.pollInterval), passwordMode, useProxy: Boolean(parsed.useProxy), }; } catch { return DEFAULT_FORM; } } function readStoredBatchId() { try { return localStorage.getItem(BATCH_STORAGE_KEY) || ''; } catch { return ''; } } export default function HuyaRegisterPage() { const initialFormRef = useRef(null); if (initialFormRef.current === null) { initialFormRef.current = readStoredForm(); } const initialForm = initialFormRef.current; const [text, setText] = useState(initialForm.text); const [tag, setTag] = useState(initialForm.tag); const [concurrency, setConcurrency] = useState(initialForm.concurrency); const [waitSeconds, setWaitSeconds] = useState(initialForm.waitSeconds); const [pollInterval, setPollInterval] = useState(initialForm.pollInterval); const [passwordMode, setPasswordMode] = useState(initialForm.passwordMode); const [passwordPrefix, setPasswordPrefix] = useState(initialForm.passwordPrefix); const [fixedPassword, setFixedPassword] = useState(initialForm.fixedPassword); const [useProxy, setUseProxy] = useState(initialForm.useProxy); const [batch, setBatch] = useState(null); const [starting, setStarting] = useState(false); const [refreshing, setRefreshing] = useState(false); const [stopping, setStopping] = useState(false); const restoredBatchRef = useRef(false); const batchId = batch?.batch_id || ''; const isRunning = !!batch && RUNNING_STATUS.has(batch.status); const loadBatchById = useCallback(async (id: string) => { if (!id) return; setRefreshing(true); try { const data = await huyaApi.getAutoRegisterBatch(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, JSON.stringify({ text, tag, concurrency, waitSeconds, pollInterval, passwordMode, passwordPrefix, fixedPassword, useProxy, })); }, [text, tag, concurrency, waitSeconds, pollInterval, passwordMode, passwordPrefix, fixedPassword, useProxy]); useEffect(() => { if (restoredBatchRef.current) return; restoredBatchRef.current = true; const storedBatchId = readStoredBatchId(); if (storedBatchId) { loadBatchById(storedBatchId); } }, [loadBatchById]); useEffect(() => { if (batch?.batch_id) { localStorage.setItem(BATCH_STORAGE_KEY, batch.batch_id); } }, [batch?.batch_id]); useEffect(() => { if (!isRunning || !batchId) return undefined; const timer = window.setInterval(() => { refreshBatch(); }, 2000); return () => window.clearInterval(timer); }, [batchId, isRunning, refreshBatch]); const successRows = useMemo( () => (batch?.items || []).filter((item) => item.status === 'success' && item.password && (item.username || item.uid)), [batch], ); const handleStart = async () => { if (!text.trim()) { message.warning('请先粘贴手机号池'); return; } if (passwordMode === 'fixed' && !fixedPassword.trim()) { message.warning('请先填写固定密码'); return; } setStarting(true); try { const data = await huyaApi.startAutoRegister({ text, tag: tag.trim(), concurrency, wait_seconds: waitSeconds, poll_interval: pollInterval, password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : 'hy', fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : '', use_proxy: useProxy, }); setBatch(data); message.success('自动注册批次已启动'); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStarting(false); } }; const handleStop = async () => { if (!batchId) return; setStopping(true); try { const result = await huyaApi.stopAutoRegisterBatch(batchId); message.success(result.message); refreshBatch(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStopping(false); } }; const handleExportSuccess = () => { if (successRows.length === 0) { message.warning('当前批次没有可导出的成功账号'); return; } const body = successRows .map((item) => `${item.username || item.uid}----${item.password}----${item.phone}----${item.sms_url}`) .join('\n'); const blob = new Blob([body], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `huya-register-accounts-${batchId || 'success'}.txt`; link.click(); URL.revokeObjectURL(url); }; const columns: TableProps['columns'] = [ { title: '行', dataIndex: 'line', width: 64 }, { title: '手机号', dataIndex: 'phone', width: 150 }, { title: '虎牙号', width: 150, render: (_, item) => item.username || item.uid || '-', }, { title: '密码', dataIndex: 'password', width: 130, render: (value: string) => value || '-', }, { title: '平台', dataIndex: 'provider', width: 90 }, { title: '状态', dataIndex: 'status', width: 110, render: (status: string) => ( {STATUS_LABELS[status] || status} ), }, { title: '注册码', dataIndex: 'code', width: 100 }, { title: '改密码', dataIndex: 'change_code', width: 100 }, { title: '轮询', width: 90, render: (_, item) => `${item.attempts}/${item.change_attempts}`, }, { title: '账号', width: 160, render: (_, item) => item.uid || (item.account_id ? `#${item.account_id}` : '-'), }, { title: '消息', dataIndex: 'message', ellipsis: true, render: (value: string) => value || '-', }, { title: '完成时间', dataIndex: 'finished_at', width: 170, render: (value: string | null) => formatTime(value), }, ]; return ( 虎牙自动注册