import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Card, Col, DatePicker, Dropdown, Form, Input, InputNumber, Modal, QRCode, Radio, Row, Select, Space, Table, Tag, Tooltip, Typography, theme, } from 'antd'; import { message } from '../utils/antdMessage'; import type { MenuProps, TableProps } from 'antd'; import type { Dayjs } from 'dayjs'; import type { MouseEvent as ReactMouseEvent } from 'react'; import { AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined, ImportOutlined, LinkOutlined, MoreOutlined, PlayCircleOutlined, QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined, } from '@ant-design/icons'; import { huyaApi, type HuyaAccountItem, type HuyaConfig, type HuyaGoodsItem, type HuyaRechargeGoodsItem, type HuyaTaskItem, } from '../api/modules'; import RealtimeLogPanel from '../components/RealtimeLogPanel'; import { usePermissions } from '../hooks/usePermissions'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; const { Text } = Typography; const FALLBACK_TASK_TYPES: Record = { get_bind_qr: '获取绑定二维码', confirm_bind: '确认绑定', query_points: '一键查询积分', query_game_name: '一键查询游戏名', query_exchange_records: '一键查询兑换记录', refresh_goods: '刷新商品列表', refresh_recharge_goods: '刷新充值商品列表', exchange_goods: '兑换商品', create_recharge_order: '生成支付二维码', }; const QUICK_ACTIONS = [ { key: 'get_bind_qr', icon: }, { key: 'confirm_bind', icon: }, { key: 'query_points', icon: }, { key: 'query_game_name', icon: }, { key: 'query_exchange_records', icon: }, { key: 'refresh_goods', icon: }, { key: 'refresh_recharge_goods', icon: }, { key: 'exchange_goods', icon: }, { key: 'create_recharge_order', icon: }, ]; const BATCH_COMPATIBLE_TASK_TYPES = new Set(['confirm_bind', 'query_game_name']); function canRunDuringBatch(taskType: string): boolean { return BATCH_COMPATIBLE_TASK_TYPES.has(taskType); } const STATUS_COLORS: Record = { planned: 'default', pending: 'default', running: 'processing', success: 'success', failed: 'error', error: 'error', stopped: 'warning', }; const STATUS_LABELS: Record = { planned: '已计划', pending: '等待中', running: '执行中', success: '成功', failed: '失败', error: '异常', stopped: '已停止', }; const ACCOUNT_STATUS_LABELS: Record = { imported: '已导入', updated: '已更新', password_imported: '待登录', login_success: '登录成功', password_changed: '已改密', login_failed: '登录失败', active: '正常', invalid: '失效', points_queried: '已查积分', game_queried: '已查角色', game_not_bound: '未绑定', bind_qr_generated: '待扫码', bind_confirmed: '已绑定', goods_exchanged: '已兑换', recharge_order_created: '待支付', }; const ACCOUNT_STATUS_COLORS: Record = { imported: 'blue', updated: 'cyan', password_imported: 'warning', login_success: 'success', password_changed: 'success', login_failed: 'error', active: 'success', invalid: 'error', points_queried: 'success', game_queried: 'success', game_not_bound: 'default', bind_qr_generated: 'processing', bind_confirmed: 'success', goods_exchanged: 'success', recharge_order_created: 'processing', }; const HUYA_WORKBENCH_ACCOUNT_IDS_KEY = 'huya_task_workbench_account_ids'; function readWorkbenchAccountIds(): number[] { try { const raw = localStorage.getItem(HUYA_WORKBENCH_ACCOUNT_IDS_KEY); if (!raw) return []; const values = JSON.parse(raw); if (!Array.isArray(values)) return []; return Array.from(new Set(values.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0))); } catch { return []; } } function saveWorkbenchAccountIds(ids: number[]) { const normalized = Array.from(new Set(ids.filter((id) => Number.isInteger(id) && id > 0))); if (normalized.length === 0) { localStorage.removeItem(HUYA_WORKBENCH_ACCOUNT_IDS_KEY); return; } localStorage.setItem(HUYA_WORKBENCH_ACCOUNT_IDS_KEY, JSON.stringify(normalized)); } function accountLabel(account: HuyaAccountItem): string { const name = account.nickname || account.username || account.uid || `#${account.id}`; const tag = account.tag ? ` [${account.tag}]` : ''; const phone = account.game_phone ? ` / ${account.game_phone}` : ''; return `${name}${tag}${phone}`; } function accountDisplayName(account: HuyaAccountItem): string { return account.nickname || account.username || account.uid || `#${account.id}`; } function resultText(result: Record | null | undefined, key: string): string { const value = result?.[key]; return typeof value === 'string' ? value : ''; } function resultObject(result: Record | null | undefined, key: string): Record | null { const value = result?.[key]; if (!value || typeof value !== 'object' || Array.isArray(value)) return null; return value as Record; } function resultProfileNick(result: Record | null | undefined): string { const profile = result?.profile; if (!profile || typeof profile !== 'object' || Array.isArray(profile)) return ''; const nick = (profile as Record).nick; return typeof nick === 'string' ? nick : ''; } function taskPayloadValue(task: HuyaTaskItem | null | undefined, key: string): string { const payload = resultObject(task?.result, 'payload'); const value = payload?.[key]; if (typeof value === 'string') return value; if (typeof value === 'number') return String(value); return ''; } function taskProductText(task: HuyaTaskItem | null | undefined): string { const result = task?.result; return ( resultText(result, 'product_name') || taskPayloadValue(task, 'product_name') || resultText(result, 'product_id') || taskPayloadValue(task, 'product_id') || resultText(result, 'spu_id') || taskPayloadValue(task, 'spu_id') ); } function contextMenuPosition(x: number, y: number) { if (typeof window === 'undefined') return { left: x, top: y }; return { left: Math.max(8, Math.min(x, window.innerWidth - 220)), top: Math.max(8, Math.min(y, window.innerHeight - 390)), }; } function goodsRawString(item: HuyaGoodsItem, key: string): string { const value = item.raw?.[key]; if (typeof value === 'string') return value; if (typeof value === 'number') return String(value); return ''; } function goodsRawNumber(item: HuyaGoodsItem, key: string): number { const value = item.raw?.[key]; if (typeof value === 'number') return value; if (typeof value === 'string' && value.trim()) return Number(value) || 0; return 0; } function goodsCategoryKey(item: HuyaGoodsItem): string { return goodsRawString(item, 'category_id') || goodsRawString(item, 'category_name') || 'uncategorized'; } function goodsCategoryLabel(item: HuyaGoodsItem): string { return goodsRawString(item, 'category_name') || goodsRawString(item, 'category_id') || '未分类'; } function formatPriceText(value: number | null | undefined): string { if (!value) return ''; return `¥${(value / 100).toFixed(2)}`; } function hasMiniQrcode(task: HuyaTaskItem): boolean { if (task.task_type !== 'get_bind_qr') return false; if (resultText(task.result, 'mini_qrcode_image')) return true; return task.result?.has_mini_qrcode === true; } function mergeTaskImageCache( items: HuyaTaskItem[], imageCache: Map, ): HuyaTaskItem[] { return items.map((task) => { const image = resultText(task.result, 'mini_qrcode_image'); if (image) { imageCache.set(task.id, image); return task; } const cached = imageCache.get(task.id); if (!cached || !task.result) return task; return { ...task, result: { ...task.result, mini_qrcode_image: cached, has_mini_qrcode: true, }, }; }); } function bindReadyForConfirm(task: HuyaTaskItem | null | undefined): boolean { return task?.task_type === 'get_bind_qr' && task.result?.bind_ready_for_confirm === true; } function hasPaymentQrcode(task: HuyaTaskItem): boolean { return task.task_type === 'create_recharge_order' && Boolean(resultText(task.result, 'pay_url')); } function paymentStatus(task: HuyaTaskItem | null | undefined): string { const status = task?.result?.payment_status; return typeof status === 'string' ? status : ''; } function paymentStatusLabel(task: HuyaTaskItem | null | undefined): string { const label = task?.result?.payment_status_label; return typeof label === 'string' ? label : ''; } function isPaymentFinished(task: HuyaTaskItem | null | undefined): boolean { return task?.task_type === 'create_recharge_order' && paymentStatus(task) === 'paid'; } export default function HuyaTasksPage() { const { token } = theme.useToken(); const [form] = Form.useForm(); const [accountPool, setAccountPool] = useState([]); const [accounts, setAccounts] = useState([]); const [tasks, setTasks] = useState([]); const [goods, setGoods] = useState([]); const [rechargeGoods, setRechargeGoods] = useState([]); const [taskTypes, setTaskTypes] = useState>(FALLBACK_TASK_TYPES); const [tags, setTags] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [selectedTaskType, setSelectedTaskType] = useState('query_points'); const [searchText, setSearchText] = useState(''); const [tagFilter, setTagFilter] = useState(''); const [accountContextMenu, setAccountContextMenu] = useState<{ account: HuyaAccountItem; accountIds: number[]; x: number; y: number } | null>(null); const [importOpen, setImportOpen] = useState(false); const [importSearchText, setImportSearchText] = useState(''); const [importTagFilter, setImportTagFilter] = useState(''); const [importSelectedIds, setImportSelectedIds] = useState([]); const [selectedGoodsCategory, setSelectedGoodsCategory] = useState(''); const [selectedExchangeGoodsId, setSelectedExchangeGoodsId] = useState(''); const [exchangeAt, setExchangeAt] = useState(null); const [selectedRechargeGoodsId, setSelectedRechargeGoodsId] = useState(''); const [rechargeCount, setRechargeCount] = useState(1); const [rechargePayChannel, setRechargePayChannel] = useState('Weixin'); const [concurrency, setConcurrency] = useState(() => { const v = localStorage.getItem('huya_task_concurrency'); return v ? Math.max(1, Math.min(10, Number(v) || 3)) : 3; }); const [loading, setLoading] = useState(false); const [starting, setStarting] = useState(false); const [stopping, setStopping] = useState(false); const [savingConfig, setSavingConfig] = useState(false); const [taskRecordsVisible, setTaskRecordsVisible] = useState(false); const [batchId, setBatchId] = useState(null); const [qrTask, setQrTask] = useState(null); const [payTask, setPayTask] = useState(null); const [exchangeRecordsTask, setExchangeRecordsTask] = useState(null); const autoOpenedQrTaskIds = useRef>(new Set()); const autoOpenQrReady = useRef(false); const autoOpenedPayTaskIds = useRef>(new Set()); const autoOpenPayReady = useRef(false); const qrImageCacheRef = useRef>(new Map()); const notifiedPaidTaskIds = useRef>(new Set()); const accountTableAreaRef = useRef(null); const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(460); const tasksLoadingRef = useRef(false); const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs(); const { can } = usePermissions(); const canTask = can('huya:task'); const canConfig = can('huya:config'); useEffect(() => { localStorage.setItem('huya_task_concurrency', String(concurrency)); }, [concurrency]); useEffect(() => { const node = accountTableAreaRef.current; if (!node) return; const updateHeight = () => { setAccountTableAreaHeight(Math.max(360, Math.floor(node.getBoundingClientRect().height))); }; updateHeight(); if (typeof ResizeObserver === 'undefined') { window.addEventListener('resize', updateHeight); return () => window.removeEventListener('resize', updateHeight); } const observer = new ResizeObserver(updateHeight); observer.observe(node); return () => observer.disconnect(); }, []); const rememberExistingQrcodes = useCallback((items: HuyaTaskItem[]) => { if (autoOpenQrReady.current) return; items.filter(hasMiniQrcode).forEach((task) => autoOpenedQrTaskIds.current.add(task.id)); autoOpenQrReady.current = true; }, []); const rememberExistingPaymentQrcodes = useCallback((items: HuyaTaskItem[]) => { if (autoOpenPayReady.current) return; items.filter(hasPaymentQrcode).forEach((task) => autoOpenedPayTaskIds.current.add(task.id)); autoOpenPayReady.current = true; }, []); const openQrTask = useCallback((task: HuyaTaskItem) => { autoOpenedQrTaskIds.current.add(task.id); const cachedImage = qrImageCacheRef.current.get(task.id); const currentImage = resultText(task.result, 'mini_qrcode_image'); if (currentImage) { qrImageCacheRef.current.set(task.id, currentImage); setQrTask(task); return; } if (cachedImage) { setQrTask({ ...task, result: { ...(task.result || {}), mini_qrcode_image: cachedImage, has_mini_qrcode: true, }, }); return; } setQrTask(task); // 列表接口默认不带 base64,打开弹窗时再拉详情。 void huyaApi.getTask(task.id).then((detail) => { const image = resultText(detail.result, 'mini_qrcode_image'); if (image) qrImageCacheRef.current.set(detail.id, image); setQrTask((current) => (current && current.id === detail.id ? detail : current)); setTasks((prev) => prev.map((item) => (item.id === detail.id ? detail : item))); }).catch(() => { // 详情失败时仍展示已有状态,不打断操作。 }); }, []); const openPayTask = useCallback((task: HuyaTaskItem) => { autoOpenedPayTaskIds.current.add(task.id); setPayTask(task); }, []); const loadAll = useCallback(async () => { setLoading(true); try { const [accountResult, taskResult, goodsResult, rechargeGoodsResult, configResult, taskTypeResult, tagResult] = await Promise.allSettled([ huyaApi.listAccounts({ include_cookie: false }), huyaApi.listTasks(), huyaApi.listGoods(), huyaApi.listRechargeGoods(), canConfig ? huyaApi.getConfig() : Promise.resolve(null), huyaApi.taskTypes(), huyaApi.listTags(), ]); if (accountResult.status === 'fulfilled') { const nextPool = accountResult.value; const nextPoolById = new Map(nextPool.map((account) => [account.id, account])); setAccountPool(nextPool); setAccounts((prev) => { const persistedIds = readWorkbenchAccountIds(); const sourceIds = prev.length > 0 ? prev.map((account) => account.id) : persistedIds; const nextAccounts = sourceIds .map((id) => nextPoolById.get(id)) .filter((account): account is HuyaAccountItem => Boolean(account)); saveWorkbenchAccountIds(nextAccounts.map((account) => account.id)); return nextAccounts; }); setSelectedIds((prev) => prev.filter((id) => nextPoolById.has(id))); } if (taskResult.status === 'fulfilled') { const nextTasks = mergeTaskImageCache(taskResult.value, qrImageCacheRef.current); rememberExistingQrcodes(nextTasks); rememberExistingPaymentQrcodes(nextTasks); setTasks(nextTasks); if (!nextTasks.some((task) => ['pending', 'running'].includes(task.status))) { setBatchId(null); } } if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value); if (rechargeGoodsResult.status === 'fulfilled') setRechargeGoods(rechargeGoodsResult.value); if (configResult.status === 'fulfilled' && configResult.value) form.setFieldsValue(configResult.value); if (taskTypeResult.status === 'fulfilled') setTaskTypes({ ...FALLBACK_TASK_TYPES, ...taskTypeResult.value }); if (tagResult.status === 'fulfilled') setTags(tagResult.value); const failedLabels = [ accountResult.status === 'rejected' ? `CK 列表: ${getErrorMessage(accountResult.reason)}` : '', taskResult.status === 'rejected' ? `任务记录: ${getErrorMessage(taskResult.reason)}` : '', goodsResult.status === 'rejected' ? `兑换商品: ${getErrorMessage(goodsResult.reason)}` : '', rechargeGoodsResult.status === 'rejected' ? `充值商品: ${getErrorMessage(rechargeGoodsResult.reason)}` : '', configResult.status === 'rejected' ? `虎牙配置: ${getErrorMessage(configResult.reason)}` : '', taskTypeResult.status === 'rejected' ? `任务类型: ${getErrorMessage(taskTypeResult.reason)}` : '', tagResult.status === 'rejected' ? `标签: ${getErrorMessage(tagResult.reason)}` : '', ].filter(Boolean); if (failedLabels.length > 0) { message.warning(`部分数据加载失败:${failedLabels.join(';')}`); } } finally { setLoading(false); } }, [canConfig, form, rememberExistingPaymentQrcodes, rememberExistingQrcodes]); const loadTasks = useCallback(async () => { if (tasksLoadingRef.current) return; tasksLoadingRef.current = true; try { const data = await huyaApi.listTasks(); const nextTasks = mergeTaskImageCache(data, qrImageCacheRef.current); rememberExistingQrcodes(nextTasks); rememberExistingPaymentQrcodes(nextTasks); setTasks(nextTasks); if (!nextTasks.some((task) => ['pending', 'running'].includes(task.status))) { setBatchId(null); } } catch { // 轮询失败不打扰操作,下一轮继续刷新。 } finally { tasksLoadingRef.current = false; } }, [rememberExistingPaymentQrcodes, rememberExistingQrcodes]); useEffect(() => { loadAll(); }, [loadAll]); // 有活跃任务时 3 秒轮询;空闲时 15 秒轻量刷新,避免 Network 面板一直刷 tasks。 const hasActiveTasks = useMemo( () => tasks.some((task) => ['pending', 'running', 'planned'].includes(task.status)), [tasks], ); useEffect(() => { const intervalMs = hasActiveTasks || wsConnected ? 3000 : 15000; const timer = setInterval(loadTasks, intervalMs); return () => clearInterval(timer); }, [hasActiveTasks, loadTasks, wsConnected]); useEffect(() => { if (!autoOpenQrReady.current || qrTask) return; const nextQrTask = tasks .filter((task) => hasMiniQrcode(task) && !autoOpenedQrTaskIds.current.has(task.id)) .sort((a, b) => b.id - a.id)[0]; if (nextQrTask) openQrTask(nextQrTask); }, [openQrTask, qrTask, tasks]); useEffect(() => { if (!autoOpenPayReady.current || payTask) return; const nextPayTask = tasks .filter((task) => hasPaymentQrcode(task) && !isPaymentFinished(task) && !autoOpenedPayTaskIds.current.has(task.id)) .sort((a, b) => b.id - a.id)[0]; if (nextPayTask) openPayTask(nextPayTask); }, [openPayTask, payTask, tasks]); useEffect(() => { if (!payTask) return; const latest = tasks.find((task) => task.id === payTask.id); if (!latest) return; if (isPaymentFinished(latest)) { if (!notifiedPaidTaskIds.current.has(latest.id)) { notifiedPaidTaskIds.current.add(latest.id); message.success('虎牙支付成功'); } setPayTask(null); return; } if (latest !== payTask) setPayTask(latest); }, [payTask, tasks]); useEffect(() => { if (!qrTask) return; const latest = tasks.find((task) => task.id === qrTask.id); if (latest && latest !== qrTask) setQrTask(latest); }, [qrTask, tasks]); useEffect(() => { if (!accountContextMenu) return; const close = () => setAccountContextMenu(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') close(); }; window.addEventListener('click', close); window.addEventListener('scroll', close, true); window.addEventListener('resize', close); window.addEventListener('keydown', closeOnEscape); return () => { window.removeEventListener('click', close); window.removeEventListener('scroll', close, true); window.removeEventListener('resize', close); window.removeEventListener('keydown', closeOnEscape); }; }, [accountContextMenu]); const tagColorMap = useMemo(() => { const map: Record = {}; tags.forEach((tag, index) => { map[tag] = ['blue', 'green', 'cyan', 'geekblue', 'purple', 'orange', 'magenta', 'volcano'][index % 8]; }); return map; }, [tags]); const filteredAccounts = useMemo(() => { const keyword = searchText.trim().toLowerCase(); return accounts.filter((account) => { if (tagFilter && account.tag !== tagFilter) return false; if (!keyword) return true; return ( account.uid.toLowerCase().includes(keyword) || account.yyuid.toLowerCase().includes(keyword) || account.username.toLowerCase().includes(keyword) || account.nickname.toLowerCase().includes(keyword) || account.tag.toLowerCase().includes(keyword) || account.game_name.toLowerCase().includes(keyword) || account.game_channel.toLowerCase().includes(keyword) || account.game_phone.toLowerCase().includes(keyword) || account.cookie_preview.toLowerCase().includes(keyword) ); }); }, [accounts, searchText, tagFilter]); const importFilteredAccounts = useMemo(() => { const importedIds = new Set(accounts.map((account) => account.id)); const keyword = importSearchText.trim().toLowerCase(); return accountPool.filter((account) => { if (importedIds.has(account.id)) return false; if (importTagFilter && account.tag !== importTagFilter) return false; if (!keyword) return true; return ( account.uid.toLowerCase().includes(keyword) || account.yyuid.toLowerCase().includes(keyword) || account.username.toLowerCase().includes(keyword) || account.nickname.toLowerCase().includes(keyword) || account.tag.toLowerCase().includes(keyword) || account.game_name.toLowerCase().includes(keyword) || account.game_channel.toLowerCase().includes(keyword) || account.game_phone.toLowerCase().includes(keyword) || account.cookie_preview.toLowerCase().includes(keyword) ); }); }, [accountPool, accounts, importSearchText, importTagFilter]); const latestTaskByAccount = useMemo(() => { const map = new Map(); tasks.forEach((task) => { const current = map.get(task.account_id); if (!current || task.id > current.id) map.set(task.account_id, task); }); return map; }, [tasks]); const latestQueryGameTaskByAccount = useMemo(() => { const map = new Map(); tasks.forEach((task) => { if (task.task_type !== 'query_game_name') return; const current = map.get(task.account_id); if (!current || task.id > current.id) map.set(task.account_id, task); }); return map; }, [tasks]); const latestGoodsTaskByAccount = useMemo(() => { const map = new Map(); tasks.forEach((task) => { if (!['exchange_goods', 'create_recharge_order'].includes(task.task_type)) return; const current = map.get(task.account_id); if (!current || task.id > current.id) map.set(task.account_id, task); }); return map; }, [tasks]); const selectedAccounts = useMemo(() => { const selected = new Set(selectedIds); return accounts.filter((account) => selected.has(account.id)); }, [accounts, selectedIds]); const selectedAccountText = selectedAccounts.length === 1 ? accountLabel(selectedAccounts[0]) : `已选 ${selectedIds.length} 个账号`; const runningTaskBatchId = useMemo(() => { // 支付监听会长期 running;绑定二维码自动轮询角色时也算活跃。 // 仅当 get_bind_qr 已结束轮询但仍残留 running 时,不锁 UI。 const task = tasks.find((item) => { if (!['pending', 'running'].includes(item.status)) return false; if (item.task_type === 'create_recharge_order') { const status = paymentStatus(item); return !status || !['paid', 'timeout', 'stopped'].includes(status); } if (item.task_type === 'get_bind_qr') { // 正在自动轮询扫码/角色 if (item.status === 'running' && item.result?.bind_polling === true) return true; // 有二维码但未标记轮询:可能是历史僵尸,不锁 UI if (hasMiniQrcode(item) && item.result?.bind_polling !== true) return false; } return true; }); return task?.batch_id || null; }, [tasks]); const activeBatchId = runningTaskBatchId || (wsConnected ? batchId : null); const batchBusy = Boolean(activeBatchId); const isTaskActionDisabled = useCallback((taskType: string, accountIds = selectedIds) => ( !canTask || accountIds.length === 0 || ((batchBusy || wsConnected) && !canRunDuringBatch(taskType)) ), [batchBusy, canTask, selectedIds, wsConnected]); const sortedGoods = useMemo(() => { return [...goods].sort((a, b) => ( goodsRawNumber(a, 'category_sort') - goodsRawNumber(b, 'category_sort') || goodsRawNumber(a, 'raw_order') - goodsRawNumber(b, 'raw_order') || a.id - b.id )); }, [goods]); const sortedRechargeGoods = useMemo(() => { return [...rechargeGoods].sort((a, b) => { const aOrder = goodsRawNumber({ raw: a.raw } as HuyaGoodsItem, 'raw_order'); const bOrder = goodsRawNumber({ raw: b.raw } as HuyaGoodsItem, 'raw_order'); return aOrder - bOrder || a.id - b.id; }); }, [rechargeGoods]); const rechargeGoodsOptions = useMemo(() => { return sortedRechargeGoods.map((item) => ({ value: item.spu_id, label: `${item.name || item.spu_id}${item.price ? ` / ${formatPriceText(item.price)}` : ''}`, })); }, [sortedRechargeGoods]); const selectedRechargeGoods = useMemo(() => { return sortedRechargeGoods.find((item) => item.spu_id === selectedRechargeGoodsId) || null; }, [sortedRechargeGoods, selectedRechargeGoodsId]); useEffect(() => { if (sortedRechargeGoods.length === 0) { if (selectedRechargeGoodsId) setSelectedRechargeGoodsId(''); return; } if (!sortedRechargeGoods.some((item) => item.spu_id === selectedRechargeGoodsId)) { setSelectedRechargeGoodsId(sortedRechargeGoods[0].spu_id); } }, [selectedRechargeGoodsId, sortedRechargeGoods]); const goodsCategories = useMemo(() => { const map = new Map(); sortedGoods.forEach((item) => { const key = goodsCategoryKey(item); const current = map.get(key); if (current) { current.count += 1; return; } map.set(key, { key, label: goodsCategoryLabel(item), sort: goodsRawNumber(item, 'category_sort'), count: 1, }); }); return Array.from(map.values()).sort((a, b) => a.sort - b.sort || a.label.localeCompare(b.label)); }, [sortedGoods]); useEffect(() => { if (goodsCategories.length === 0) { if (selectedGoodsCategory) setSelectedGoodsCategory(''); return; } if (!goodsCategories.some((item) => item.key === selectedGoodsCategory)) { setSelectedGoodsCategory(goodsCategories[0].key); } }, [goodsCategories, selectedGoodsCategory]); const goodsOptions = useMemo(() => { return sortedGoods.map((item) => ({ value: item.product_id, label: `${item.name || item.product_id}${item.price ? ` / ${item.price}积分` : ''}`, })); }, [sortedGoods]); const selectedExchangeGoods = useMemo(() => { return sortedGoods.find((item) => item.product_id === selectedExchangeGoodsId) || null; }, [selectedExchangeGoodsId, sortedGoods]); useEffect(() => { if (sortedGoods.length === 0) { if (selectedExchangeGoodsId) setSelectedExchangeGoodsId(''); return; } if (!sortedGoods.some((item) => item.product_id === selectedExchangeGoodsId)) { setSelectedExchangeGoodsId(sortedGoods[0].product_id); } }, [selectedExchangeGoodsId, sortedGoods]); const createPayload = (taskType: string) => { if (taskType === 'exchange_goods') { return { product_id: selectedExchangeGoods?.product_id || selectedExchangeGoodsId, product_name: selectedExchangeGoods?.name || '', scheduled_at: exchangeAt ? exchangeAt.toISOString() : '', }; } if (taskType !== 'create_recharge_order') return {}; return { spu_id: selectedRechargeGoods?.spu_id || selectedRechargeGoodsId, sku_id: selectedRechargeGoods?.sku_id || '', product_name: selectedRechargeGoods?.name || '', count: rechargeCount, pay_channel: rechargePayChannel, }; }; const startTask = async (taskType = selectedTaskType, accountIds = selectedIds) => { if (accountIds.length === 0) { message.warning('请先选择虎牙 CK'); return; } if ((activeBatchId || wsConnected) && !canRunDuringBatch(taskType)) { message.warning('当前已有虎牙批次在运行,请先停止或等待结束'); return; } if (taskType === 'create_recharge_order' && !selectedRechargeGoodsId) { message.warning('请先选择充值商品'); return; } if (taskType === 'exchange_goods' && !selectedExchangeGoodsId) { message.warning('请先选择兑换商品'); return; } setStarting(true); try { const result = await huyaApi.createTasks({ account_ids: accountIds, task_type: taskType, concurrency, payload: createPayload(taskType), }); setBatchId(result.batch_id); message.success(`已创建 ${taskTypes[taskType] || taskType},共 ${result.count} 个账号`); await loadTasks(); connectLogs(`/api/huya/ws/${result.batch_id}`, { onClose: () => { setStarting(false); setStopping(false); void loadAll(); }, onResult: () => { setBatchId(null); setStarting(false); setStopping(false); void loadAll(); }, onError: () => { setStarting(false); setStopping(false); void loadAll(); }, }); } catch (e: unknown) { message.error(getErrorMessage(e)); setStarting(false); } }; const handleStopBatch = async () => { if (!activeBatchId) { // 没有可识别活跃批次时,尝试清理历史残留 running。 const stale = tasks.find((item) => ['pending', 'running'].includes(item.status)); if (!stale?.batch_id) { message.warning('当前没有可停止的虎牙批次'); return; } setStopping(true); try { const result = await huyaApi.stopBatch(stale.batch_id); message.success(result.message); setBatchId(null); setStarting(false); void loadAll(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStopping(false); } return; } setStopping(true); try { const result = await huyaApi.stopBatch(activeBatchId); message.success(result.message); setStarting(false); void loadTasks(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStopping(false); } }; const accountActionItems: MenuProps['items'] = QUICK_ACTIONS.map((item) => ({ key: item.key, icon: item.icon, label: taskTypes[item.key] || item.key, })); const importAccountsToWorkbench = (ids: number[]) => { const accountById = new Map(accountPool.map((account) => [account.id, account])); const uniqueIds = Array.from(new Set(ids)).filter((id) => accountById.has(id)); if (uniqueIds.length === 0) { message.warning('请先选择要导入的账号'); return; } setAccounts((prev) => { const prevIds = new Set(prev.map((account) => account.id)); const nextAccounts = [...prev]; uniqueIds.forEach((id) => { if (!prevIds.has(id)) { const account = accountById.get(id); if (account) nextAccounts.push(account); } }); saveWorkbenchAccountIds(nextAccounts.map((account) => account.id)); return nextAccounts; }); setSelectedIds(uniqueIds); setImportSelectedIds([]); setImportOpen(false); message.success(`已导入 ${uniqueIds.length} 个账号到操作台`); }; const removeSelectedAccounts = () => { if (selectedIds.length === 0) { message.warning('请先选择要移出的账号'); return; } const selected = new Set(selectedIds); setAccounts((prev) => { const nextAccounts = prev.filter((account) => !selected.has(account.id)); saveWorkbenchAccountIds(nextAccounts.map((account) => account.id)); return nextAccounts; }); setSelectedIds([]); setAccountContextMenu(null); }; const clearWorkbenchAccounts = () => { saveWorkbenchAccountIds([]); setAccounts([]); setSelectedIds([]); setAccountContextMenu(null); }; const runSingleAccountAction = (taskType: string, account: HuyaAccountItem) => { setSelectedIds([account.id]); setAccountContextMenu(null); void startTask(taskType, [account.id]); }; const runContextAccountAction = (taskType: string) => { if (!accountContextMenu) return; setSelectedIds(accountContextMenu.accountIds); setAccountContextMenu(null); void startTask(taskType, accountContextMenu.accountIds); }; const saveConfig = async () => { setSavingConfig(true); try { const values = await form.validateFields(); const result = await huyaApi.updateConfig(values); form.setFieldsValue(result); message.success('虎牙配置已保存'); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setSavingConfig(false); } }; const successCount = tasks.filter((task) => task.status === 'success').length; const plannedCount = tasks.filter((task) => task.status === 'planned').length; const failedCount = tasks.filter((task) => ['failed', 'error'].includes(task.status)).length; const qrResult = qrTask?.result || null; const qrQueryTask = qrTask ? latestQueryGameTaskByAccount.get(qrTask.account_id) || null : null; const qrQueryResult = qrTask && qrQueryTask && qrQueryTask.id > qrTask.id ? qrQueryTask.result : null; const qrQueryRunning = Boolean(qrQueryTask && qrTask && qrQueryTask.id > qrTask.id && qrQueryTask.status === 'running'); const qrQueryFinished = Boolean(qrQueryTask && qrTask && qrQueryTask.id > qrTask.id && qrQueryTask.status !== 'running'); const qrImage = resultText(qrResult, 'mini_qrcode_image'); const qrBindPhase = resultText(qrResult, 'bind_phase'); const qrAutoPolling = qrTask?.status === 'running' && qrResult?.bind_polling === true; const qrBindReady = bindReadyForConfirm(qrTask) || Boolean(resultText(qrQueryResult, 'role_name')); const qrRoleSourceResult = resultText(qrQueryResult, 'role_name') ? qrQueryResult : qrResult; const qrWaitingRole = qrAutoPolling || qrQueryRunning; const qrGameTitle = resultText(qrRoleSourceResult, 'game_title'); const qrRoleName = resultText(qrRoleSourceResult, 'role_name'); const qrGameRole = resultObject(qrRoleSourceResult, 'game_role'); const qrRoleArea = typeof qrGameRole?.area_name === 'string' ? qrGameRole.area_name : ''; const qrRolePlat = typeof qrGameRole?.plat_name === 'string' ? qrGameRole.plat_name : ''; const qrRoleLine = qrBindReady ? [qrRolePlat, qrRoleArea, qrRoleName].filter(Boolean).join(' - ') : ''; const qrStatusText = qrBindReady ? '已识别角色,待确认' : qrAutoPolling ? ( qrBindPhase === 'qrcode_completed' ? '等待角色同步' : qrBindPhase === 'qrcode_scanned' ? '已扫码' : qrBindPhase === 'qrcode_expired' ? '二维码已失效' : '等待扫码绑定' ) : qrQueryRunning ? '查询角色中' : qrQueryFinished ? '未检测到角色' : qrBindPhase === 'role_timeout' ? '未检测到角色' : qrBindPhase === 'qrcode_completed' ? '等待角色同步' : qrBindPhase === 'qrcode_scanned' ? '已扫码' : qrBindPhase === 'qrcode_expired' ? '二维码已失效' : '等待绑定'; const qrAccountName = qrTask ? resultProfileNick(qrResult) || qrTask.account_nickname || qrTask.account_uid || `#${qrTask.account_id}` : ''; const payResult = payTask?.result || null; const payUrl = resultText(payResult, 'pay_url'); const payProductName = resultText(payResult, 'product_name'); const payAmountText = resultText(payResult, 'amount_text'); const payChannelLabel = resultText(payResult, 'pay_channel_label'); const payStatus = paymentStatus(payTask); const payStatusText = paymentStatusLabel(payTask); const payOrderId = payResult?.order_id; const payAccountName = payTask ? payTask.account_nickname || payTask.account_uid || `#${payTask.account_id}` : ''; const exchangeRecordsResult = exchangeRecordsTask?.result || null; const exchangeRecordsRaw = exchangeRecordsResult?.records; const exchangeRecords = Array.isArray(exchangeRecordsRaw) ? exchangeRecordsRaw.filter((item) => item && typeof item === 'object') as Record[] : []; const exchangeRecordsAccountName = exchangeRecordsTask ? exchangeRecordsTask.account_nickname || exchangeRecordsTask.account_uid || `#${exchangeRecordsTask.account_id}` : ''; const confirmQrBind = () => { if (!qrTask || !qrBindReady) return; const accountId = qrTask.account_id; setQrTask(null); void startTask('confirm_bind', [accountId]); }; const queryQrRole = () => { if (!qrTask) return; const accountId = qrTask.account_id; void startTask('query_game_name', [accountId]); }; const renderTaskResult = (value: Record | null, record: HuyaTaskItem) => { if (hasMiniQrcode(record) || resultText(value, 'mini_qrcode_image')) { if (bindReadyForConfirm(record) || Boolean(resultText(value, 'role_name'))) { const roleName = resultText(value, 'role_name'); return ( 待确认 {roleName ? {roleName} : null} ); } if (record.status === 'running' && value?.bind_polling === true) { const phase = resultText(value, 'bind_phase'); const phaseText = phase === 'qrcode_scanned' ? '已扫码' : phase === 'qrcode_completed' ? '等待角色' : '等待扫码'; return ( {phaseText} ); } return ( ); } if (record.task_type === 'get_bind_qr') { if (value?.can_change_bind === false) { const changeAt = resultText(value, 'change_available_at'); return ( 不可更换 {changeAt ? {changeAt} : null} ); } return -; } if (record.task_type === 'confirm_bind') { if (value?.bind_confirmed === true) { const gameRole = resultObject(value, 'game_role'); const roleName = typeof gameRole?.role_name === 'string' ? gameRole.role_name : ''; return ( 已绑定 {roleName ? {roleName} : null} ); } return -; } if (record.task_type === 'query_game_name') { const roleName = resultText(value, 'role_name'); if (roleName) return {roleName}; if (value?.is_bound === false) return 未绑定; return -; } if (record.task_type === 'query_exchange_records') { const recordCount = value?.record_count; if (typeof recordCount === 'number' && recordCount > 0) { return ( ); } if (typeof recordCount === 'number') return 记录 {recordCount} 条; return -; } if (record.task_type === 'refresh_goods') { const goodsCount = value?.goods_count; if (typeof goodsCount === 'number') return 商品 {goodsCount} 个; return -; } if (record.task_type === 'exchange_goods') { const productName = resultText(value, 'product_name'); const orderId = resultText(value, 'order_id'); if (record.status !== 'success') { return productName ? {productName} : -; } return ( 已兑换 {productName ? {productName} : null} {orderId ? {orderId} : null} ); } if (record.task_type === 'refresh_recharge_goods') { const goodsCount = value?.goods_count; const failedCount = value?.failed_count; if (typeof goodsCount === 'number') { return ( 充值商品 {goodsCount} 个 {typeof failedCount === 'number' && failedCount > 0 ? 失败 {failedCount} : null} ); } return -; } if (record.task_type === 'create_recharge_order') { if (resultText(value, 'pay_url')) { if (isPaymentFinished(record)) return 已支付; const status = paymentStatus(record); return ( {status === 'timeout' ? 待支付 : 等待支付} ); } return -; } const availableScore = value?.available_score; if (typeof availableScore === 'number') { return 可用积分 {availableScore}; } return value ? {JSON.stringify(value)} : -; }; const handleAccountRowContextMenu = (record: HuyaAccountItem, event: ReactMouseEvent) => { event.preventDefault(); event.stopPropagation(); const accountIds = selectedIds.includes(record.id) && selectedIds.length > 0 ? selectedIds : [record.id]; setSelectedIds(accountIds); setAccountContextMenu({ account: record, accountIds, x: event.clientX, y: event.clientY, }); }; const accountColumns: TableProps['columns'] = [ { title: '#', width: 56, align: 'center', render: (_: unknown, record) => record.id, }, { title: '虎牙 CK', width: 250, render: (_: unknown, record) => ( {accountDisplayName(record)} {record.tag ? {record.tag} : null} UID {record.uid || record.yyuid || '-'} {record.cookie_preview || '-'} ), }, { title: '手机', dataIndex: 'game_phone', width: 128, ellipsis: true, render: (value: string) => value || -, }, { title: '游戏名', dataIndex: 'game_name', width: 170, ellipsis: true, render: (value: string, record) => value ? ( {value} {record.game_channel ? {record.game_channel} : null} ) : 未查, }, { title: '积分', dataIndex: 'points', width: 92, align: 'center', render: (points: number | null) => points ?? 未查, sorter: (a, b) => (a.points ?? -1) - (b.points ?? -1), }, { title: '兑换商品', width: 190, ellipsis: true, render: (_: unknown, record) => { const task = latestGoodsTaskByAccount.get(record.id); const product = taskProductText(task); if (!task || !product) return -; return ( {product} {taskTypes[task.task_type] || task.task_type} ); }, }, { title: '数据状态', width: 150, render: (_: unknown, record) => { const latest = latestTaskByAccount.get(record.id); return ( {ACCOUNT_STATUS_LABELS[record.status] || record.status || '-'} {latest ? ( {STATUS_LABELS[latest.status] || latest.status} ) : null} ); }, }, { title: '最近结果', width: 260, ellipsis: true, render: (_: unknown, record) => { const latest = latestTaskByAccount.get(record.id); if (!latest) return 暂无任务; return ( {taskTypes[latest.task_type] || latest.task_type}:{latest.message || '-'} {renderTaskResult(latest.result, latest)} ); }, }, { title: '更新时间', dataIndex: 'updated_at', width: 154, render: (value: string | null) => value ? formatTime(value) : -, }, { title: '操作', width: 76, fixed: 'right', align: 'center', render: (_: unknown, record) => { return ( { runSingleAccountAction(String(key), record); }, }} trigger={['click']} >