import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Badge, Button, Card, Col, Descriptions, InputNumber, Modal, QRCode, Row, Select, Space, Statistic, Table, Tabs, Tag, Tooltip, Typography, theme, } from 'antd'; import { CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, CopyOutlined, CreditCardOutlined, EyeOutlined, FireOutlined, HistoryOutlined, ReloadOutlined, StopOutlined, SyncOutlined, ThunderboltOutlined, } from '@ant-design/icons'; import { message } from '../utils/antdMessage'; import { getErrorMessage } from '../utils/error'; import { douyuApi, douyuPoolApi } from '../api/modules'; import type { DouyuPoolExchangeResult, DouyuPoolLease, DouyuPoolSupply, DouyuTaskItem, } from '../api/modules'; import type { ColumnsType } from 'antd/es/table'; import ExchangeResultImage from '../components/ExchangeResultImage'; import QrActions from '../components/QrActions'; import { copyImageToClipboard } from '../utils/exchangeImage'; import { hasPaymentQrcode, paymentArrived, resultText, taskQrUrl, } from './douyu-tasks/config'; const { Text, Paragraph } = Typography; const DOUYU_POOL_RECENT_GOODS_KEY = 'douyu_pool_recent_goods_ids'; function loadRecentGoodsIds(): string[] { try { const raw = localStorage.getItem(DOUYU_POOL_RECENT_GOODS_KEY); return raw ? (JSON.parse(raw) as string[]) : []; } catch { return []; } } function saveRecentGoodsId(id: string) { if (!id) return; try { const existing = loadRecentGoodsIds().filter((x) => x !== id); const updated = [id, ...existing].slice(0, 8); localStorage.setItem(DOUYU_POOL_RECENT_GOODS_KEY, JSON.stringify(updated)); } catch { // 忽略存储异常 } } /** 任务状态友好标签 */ const TASK_TYPE_NAMES: Record = { query_elite_status: '检查精英宝典状态', query_gold_balance: '查询鱼翅余额', query_points: '查询精英积分', create_gold_qr: '鱼翅充值', donate_elite_gift: '赠送精英令', exchange_goods: '兑换精英宝典皮肤', exchange_esports_goods: '兑换电竞手册皮肤', }; /** * 账号分配台(自动化工作台)。 * * 客服操作闭环:选皮肤 → 填数量 → 提交分配 → * 若需充值自动弹出微信支付码 → 扫码到账后自动赠送并兑换 → 产出兑换凭证。 */ export default function DouyuPoolPage() { const { token } = theme.useToken(); const [goods, setGoods] = useState<{ commodity_id: string; name: string; score: number | null }[]>([]); const [commodityId, setCommodityId] = useState(''); const [quantity, setQuantity] = useState(1); const [allowTopup, setAllowTopup] = useState(true); const [recentGoodsIds, setRecentGoodsIds] = useState(() => loadRecentGoodsIds()); const [supply, setSupply] = useState(null); const [result, setResult] = useState(null); const [leases, setLeases] = useState([]); const [loading, setLoading] = useState(false); const [previewing, setPreviewing] = useState(false); // 活跃批次与任务流水监控 const [activeBatchId, setActiveBatchId] = useState(null); const [batchTasks, setBatchTasks] = useState([]); const [batchLoading, setBatchLoading] = useState(false); const [stoppingBatch, setStoppingBatch] = useState(false); // 微信支付弹窗状态 const [payModalVisible, setPayModalVisible] = useState(false); const [selectedPayTaskId, setSelectedPayTaskId] = useState(null); const payQrCanvasWrapRef = useRef(null); const autoOpenedPayTasks = useRef>(new Set()); // 兑换结果大图弹窗 const [exchangePreview, setExchangePreview] = useState<{ task: DouyuTaskItem; url: string } | null>(null); const [copyingPreview, setCopyingPreview] = useState(false); // 倒计时刷新 const [, setTick] = useState(0); useEffect(() => { const timer = window.setInterval(() => setTick((t) => t + 1), 1000); return () => window.clearInterval(timer); }, []); const loadGoods = useCallback(async () => { try { const rows = await douyuPoolApi.goods(); const usable = rows.filter((item) => (item.score ?? 0) > 0); setGoods(usable); if (usable.length > 0 && !commodityId) setCommodityId(usable[0].commodity_id); } catch (e: unknown) { message.error(getErrorMessage(e)); } }, [commodityId]); const loadLeases = useCallback(async () => { try { const list = await douyuPoolApi.leases(true); setLeases(list); } catch (e: unknown) { message.error(getErrorMessage(e)); } }, []); useEffect(() => { void loadGoods(); }, [loadGoods]); useEffect(() => { void loadLeases(); }, [loadLeases]); const loadSupply = useCallback(async () => { if (!commodityId) return; setPreviewing(true); try { setSupply(await douyuPoolApi.supply('elite', commodityId)); } catch (e: unknown) { setSupply(null); message.error(getErrorMessage(e)); } finally { setPreviewing(false); } }, [commodityId]); useEffect(() => { void loadSupply(); }, [loadSupply]); // 轮询活跃批次任务 const pollBatchTasks = useCallback(async (batchId: string) => { try { const res = await douyuApi.listTasks('elite', batchId); setBatchTasks(res); return res; } catch { return []; } }, []); useEffect(() => { if (!activeBatchId) { setBatchTasks([]); return; } let timer: number | null = null; let alive = true; const run = async () => { setBatchLoading(true); const tasks = await pollBatchTasks(activeBatchId); if (!alive) return; setBatchLoading(false); // 检查是否有仍在运行的任务,若有则持续 2 秒轮询一次 const hasActive = tasks.some((t) => ['planned', 'pending', 'running'].includes(t.status)); if (hasActive && alive) { timer = window.setTimeout(() => { void run(); }, 2000); } }; void run(); return () => { alive = false; if (timer) window.clearTimeout(timer); }; }, [activeBatchId, pollBatchTasks]); // 提取待支付任务列表(create_gold_qr 且有 pay_url) const pendingPayTasks = useMemo(() => { return batchTasks.filter( (task) => ['create_gold_qr', 'create_elite_qr', 'create_esports_qr'].includes(task.task_type) && (hasPaymentQrcode(task) || paymentArrived(task) || ['running', 'pending', 'success'].includes(task.status)), ); }, [batchTasks]); // 待支付任务变动时,若有新二维码生成,且用户未手动处理过,自动弹出支付窗口 useEffect(() => { const freshPayTask = pendingPayTasks.find( (t) => t.status === 'running' && hasPaymentQrcode(t) && !autoOpenedPayTasks.current.has(t.id), ); if (freshPayTask) { autoOpenedPayTasks.current.add(freshPayTask.id); setSelectedPayTaskId(freshPayTask.id); setPayModalVisible(true); } }, [pendingPayTasks]); // 当前在支付弹窗中选中的任务 const activePayTask = useMemo(() => { if (!selectedPayTaskId && pendingPayTasks.length > 0) { return pendingPayTasks[0]; } return batchTasks.find((t) => t.id === selectedPayTaskId) || pendingPayTasks[0] || null; }, [batchTasks, pendingPayTasks, selectedPayTaskId]); // 当前待支付任务对应的金额 const activePayAmountInfo = useMemo(() => { if (!activePayTask) return { amount: null, orders: [] }; const matchedAccount = result?.accounts?.find((a) => a.account_id === activePayTask.account_id); const amountFromMsg = activePayTask.message?.match(/(\d+)\s*元/)?.[1]; const amountFromPayload = (activePayTask.result?.payload as Record | undefined)?.amount; const amountFromResult = activePayTask.result?.amount || activePayTask.result?.gold_amount; const amount = matchedAccount?.topup_yuan ?? (amountFromResult != null ? Number(amountFromResult) : null) ?? (amountFromPayload != null ? Number(amountFromPayload) : null) ?? (amountFromMsg ? Number(amountFromMsg) : null); return { amount, orders: matchedAccount?.recharge_orders || [], }; }, [activePayTask, result?.accounts]); const handleSelectGood = useCallback((id: string) => { setCommodityId(id); saveRecentGoodsId(id); setRecentGoodsIds(loadRecentGoodsIds()); }, []); // 快捷常用商品列表(最近使用历史 + 热门高价值套装) const quickGoods = useMemo(() => { if (!goods.length) return []; const map = new Map(goods.map((g) => [g.commodity_id, g])); const list: typeof goods = []; const seen = new Set(); // 1. 最近使用过的历史商品 for (const id of recentGoodsIds) { const item = map.get(id); if (item && !seen.has(id)) { list.push(item); seen.add(id); } } // 2. 热门高价值套装(如兔兔白日梦、狂战萌兔) for (const g of goods) { if ( (g.name.includes('兔兔白日梦') || g.name.includes('狂战萌兔')) && !seen.has(g.commodity_id) ) { list.push(g); seen.add(g.commodity_id); } } // 3. 兜底补足 for (const g of goods) { if (list.length >= 6) break; if (!seen.has(g.commodity_id)) { list.push(g); seen.add(g.commodity_id); } } return list.slice(0, 6); }, [goods, recentGoodsIds]); const submit = async () => { if (!commodityId) { message.warning('请先选择皮肤'); return; } saveRecentGoodsId(commodityId); setRecentGoodsIds(loadRecentGoodsIds()); setLoading(true); try { const data = await douyuPoolApi.exchange({ scope: 'elite', commodity_id: commodityId, quantity, allow_topup: allowTopup, allow_fallback: true, }); setResult(data); if (data.batch_id) { setActiveBatchId(data.batch_id); } if (data.shortfall > 0) { message.warning(`实配 ${data.allocated} / 需求 ${data.requested},缺 ${data.shortfall}`); } else { message.success(`已分配 ${data.allocated} 个账号,正在执行补给与兑换(批次 ${data.batch_id})`); } await Promise.all([loadSupply(), loadLeases()]); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setLoading(false); } }; const handleStopBatch = async () => { if (!activeBatchId) return; setStoppingBatch(true); try { await douyuPoolApi.stopBatch(activeBatchId); message.success('已发送停止指令'); await pollBatchTasks(activeBatchId); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setStoppingBatch(false); } }; const handleReleaseBatch = async () => { if (!activeBatchId) return; try { const res = await douyuPoolApi.releaseBatch(activeBatchId); message.success(`已释放该批次 ${res.released} 个账号租约`); await Promise.all([loadSupply(), loadLeases()]); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const releaseLease = async (leaseId: number) => { try { await douyuPoolApi.releaseLease(leaseId); message.success('已释放租约'); await Promise.all([loadSupply(), loadLeases()]); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const goodsOptions = useMemo( () => goods.map((item) => ({ value: item.commodity_id, label: `${item.name}(${item.score ?? 0} 分)`, })), [goods], ); // 账号维度聚合任务执行进度 const accountProgressList = useMemo(() => { if (!result?.accounts) return []; return result.accounts.map((acc) => { const accTasks = batchTasks.filter((t) => t.account_id === acc.account_id); const payTask = accTasks.find((t) => ['create_gold_qr', 'create_elite_qr'].includes(t.task_type)); const exchangeTask = accTasks.find((t) => ['exchange_goods', 'exchange_esports_goods'].includes(t.task_type)); const donateTask = accTasks.find((t) => t.task_type === 'donate_elite_gift'); const runningTask = accTasks.find((t) => ['running', 'pending'].includes(t.status)); const failedTask = accTasks.find((t) => ['failed', 'error', 'manual_required'].includes(t.status)); let currentStepText = '等待启动'; let stepTone: 'default' | 'processing' | 'success' | 'warning' | 'error' = 'default'; if (exchangeTask?.status === 'success') { currentStepText = '✅ 兑换成功'; stepTone = 'success'; } else if (failedTask) { currentStepText = `❌ ${failedTask.message || '执行失败'}`; stepTone = 'error'; } else if (exchangeTask?.status === 'running') { currentStepText = '🔄 正在兑换皮肤...'; stepTone = 'processing'; } else if (donateTask?.status === 'running') { currentStepText = '🎁 正在赠送精英令...'; stepTone = 'processing'; } else if (payTask?.status === 'running') { currentStepText = hasPaymentQrcode(payTask) ? '💳 等待扫码支付' : '💳 生成支付码中...'; stepTone = 'warning'; } else if (runningTask) { currentStepText = `⚡ ${TASK_TYPE_NAMES[runningTask.task_type] || runningTask.task_type}...`; stepTone = 'processing'; } return { ...acc, tasks: accTasks, payTask, exchangeTask, currentStepText, stepTone, }; }); }, [result?.accounts, batchTasks]); const accountColumns: ColumnsType<(typeof accountProgressList)[number]> = [ { title: '账号ID', dataIndex: 'account_id', width: 85 }, { title: '账号', dataIndex: 'username', ellipsis: true, render: (val: string, row) => ( {val || `#${row.account_id}`} {row.role_change_available_at && ( 换绑: {row.role_change_available_at.replace('T', ' ').slice(0, 16)} )} ), }, { title: '梯队', dataIndex: 'tier', width: 90, render: (tier: string) => ( {tier === 'A' ? '免充值' : '需充值'} ), }, { title: '当前积分', dataIndex: 'points', width: 95 }, { title: '需充值金额', width: 150, render: (_, row) => { if (row.topup_yuan <= 0) return 无需充值; const isSplit = row.recharge_orders && row.recharge_orders.length > 1; return ( ¥ {row.topup_yuan} 元 {isSplit && ( (拆为 {row.recharge_orders.join(' + ')}) )} ); }, }, { title: '当前执行状态', width: 200, render: (_, row) => ( {row.currentStepText}} /> ), }, { title: '操作 / 凭证', width: 160, render: (_, row) => { const canPay = row.payTask && hasPaymentQrcode(row.payTask) && row.payTask.status === 'running'; const isExchanged = row.exchangeTask && row.exchangeTask.status === 'success'; return ( {canPay && ( )} {isExchanged && ( setExchangePreview({ task, url })} /> )} ); }, }, ]; const leaseColumns: ColumnsType = [ { title: '账号', dataIndex: 'username', ellipsis: true }, { title: '商品ID', dataIndex: 'commodity_id', width: 220 }, { title: '批次号', dataIndex: 'batch_id', width: 140, render: (val: string) => ( ), }, { title: '到期时间', dataIndex: 'expires_at', width: 170, render: (value: string | null) => (value ? value.replace('T', ' ').slice(0, 19) : '—'), }, { title: '操作', width: 160, render: (_, row) => ( ), }, ]; // 支付二维码 URL const payUrl = taskQrUrl(activePayTask); const payAccountName = activePayTask ? (activePayTask.account_nickname || activePayTask.account_username || activePayTask.account_uid || `#${activePayTask.account_id}`) : ''; return ( {/* 顶部发起卡片 */} } onClick={() => { void loadGoods(); void loadSupply(); void loadLeases(); }}> 刷新 )} > {/* 常用 / 历史快捷标签 */} {quickGoods.length > 0 && (
常用 / 历史快捷: {quickGoods.map((item) => { const isSelected = commodityId === item.commodity_id; const isHot = item.name.includes('兔兔白日梦') || item.name.includes('狂战萌兔'); return ( handleSelectGood(item.commodity_id)} style={{ padding: '3px 10px', fontSize: 13, borderRadius: 16, border: isSelected ? undefined : `1px solid ${token.colorBorder}`, cursor: 'pointer', transition: 'all 0.2s', }} > {isHot && } {item.name} ({item.score ?? 0}分) ); })}
)} 选择要兑换的皮肤 setAllowTopup(value === 'topup')} options={[ { value: 'topup', label: '自动充值鱼翅补足' }, { value: 'skip', label: '仅用已有积分 (不充值)' }, ]} /> {supply && ( 池内全量补足预算 ℹ️ )} value={supply.cost.est_yuan} suffix="元" loading={previewing} /> )} {supply?.manual_payment_required && ( )}
{/* 活跃批次执行与进度面板 */} {activeBatchId && ( 本次分配与任务流水(批次 {activeBatchId}) {batchLoading ? ( } color="processing">实时同步中 ) : ( 已同步 )}
)} extra={( {pendingPayTasks.some((t) => hasPaymentQrcode(t) && t.status === 'running') && ( )} )} > {result && ( {result.requested} 个 0 ? 'success' : 'danger'}>{result.allocated} 个 0 ? 'danger' : undefined}>{result.shortfall} 个 {result.recharge_channel || 'wechat_qr'} )} {result?.reasons && result.reasons.length > 0 && ( {result.reasons.map((reason) =>
· {reason}
)}
)} )} {/* 我的租约列表 */} } size="small" onClick={() => void loadLeases()}> 刷新租约 )} >
{/* 微信支付二维码弹窗 */} 微信扫码充值 )} open={payModalVisible} onCancel={() => setPayModalVisible(false)} footer={null} centered width={440} > {pendingPayTasks.length > 1 && ( setSelectedPayTaskId(Number(key))} items={pendingPayTasks.map((t, idx) => { const acc = result?.accounts?.find((a) => a.account_id === t.account_id); const yuan = acc?.topup_yuan; return { key: String(t.id), label: `账号 #${idx + 1} (${t.account_username || t.account_id})${yuan ? ` · ¥${yuan}元` : ''}`, }; })} style={{ marginBottom: 12 }} /> )} {activePayTask ? (
{payAccountName && ( 充值账号:{payAccountName} )} {/* 突出展示充值金额 */}
需充值金额: ¥ {activePayAmountInfo.amount != null ? `${activePayAmountInfo.amount} 元` : '—'} {activePayAmountInfo.orders.length > 1 && ( (拆为 {activePayAmountInfo.orders.join(' + ')}) )}
{paymentArrived(activePayTask) ? ( ) : ['failed', 'error', 'stopped', 'manual_required'].includes(activePayTask.status) ? ( ) : ( {payUrl ? (
) : (
正在向斗鱼申请支付二维码...
)} {payUrl && ( <> } message={activePayTask.message || '请使用微信扫码支付'} description="支付码有效时间 10 分钟。扫码支付成功后,系统将自动检测到账并推进兑换。" style={{ textAlign: 'left', width: '100%', marginTop: 8 }} /> )}
)}
) : (
当前没有待支付的充值任务
)}
{/* 兑换结果大图弹窗 */} setExchangePreview(null)} footer={( )} centered width={380} > {exchangePreview && (
兑换凭证
{resultText(exchangePreview.task.result, 'commodity_id')}
)}
); }