From 603fc6eadc3ddccdc08b07cebef4c4a33147b797 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 7 Oct 2026 16:59:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(douyu-pool):=20=E5=AE=8C=E5=96=84=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E6=89=AB=E7=A0=81=E6=94=AF=E4=BB=98=E4=B8=8E=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E9=97=AD=E7=8E=AF=E5=8F=8A=E5=B8=B8=E7=94=A8=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端在扫码支付弹窗中增加显眼充值金额卡片与多账号 Tabs 支付标注 - 前端增加常用与历史快捷皮肤选择胶囊栏,支持本地缓存记忆与一键切换 - 后端补给链首步引入 query_elite_status 并支持 require_opened 前置熔断校验 - 优化供给与预估花费文案展示,增加兑换结果大图预览与凭证复制功能 - 验证方式:全量 784 个 pytest 用例全部通过,前端 tsc/build 校验通过 --- tests/test_douyu_pool_api.py | 2 +- tests/test_supply_chain.py | 5 +- web/backend/services/account_pool.py | 8 +- web/backend/services/douyu_runner_manual.py | 10 + web/frontend/src/pages/DouyuPoolPage.tsx | 769 ++++++++++++++++++-- 5 files changed, 732 insertions(+), 62 deletions(-) diff --git a/tests/test_douyu_pool_api.py b/tests/test_douyu_pool_api.py index 32a7902..3f72798 100644 --- a/tests/test_douyu_pool_api.py +++ b/tests/test_douyu_pool_api.py @@ -208,8 +208,8 @@ class TestExchange(_ApiBase): assert result.allocated == 1 tasks = self.session.query(DouyuTask).order_by(DouyuTask.id.asc()).all() assert [t.task_type for t in tasks] == [ + "query_elite_status", "query_gold_balance", - "query_points", "create_gold_qr", "donate_elite_gift", "exchange_goods", diff --git a/tests/test_supply_chain.py b/tests/test_supply_chain.py index 65629de..993b826 100644 --- a/tests/test_supply_chain.py +++ b/tests/test_supply_chain.py @@ -28,12 +28,13 @@ class TestBuildSupplyChain: assert chain[0].payload == {"commodity_id": "C1", "num": 1} def test_tier_b_refreshes_before_spending(self): - """链首必须先刷余额 —— 否则 `_skip_when` 读的是旧值,会多充钱。""" + """链首必须先查开通状态与余额 —— 否则未开通会误充值,旧值会多充钱。""" chain = pool.build_supply_chain(self._candidate(300), commodity_id="C1") assert [s.task_type for s in chain][:2] == [ + "query_elite_status", "query_gold_balance", - "query_points", ] + assert chain[0].payload == {"require_opened": True} def test_tier_b_topup_amount_is_exact_gap(self): # 缺口 980 分 → 98 个令 → 98 元 diff --git a/web/backend/services/account_pool.py b/web/backend/services/account_pool.py index 3662c4a..5088541 100644 --- a/web/backend/services/account_pool.py +++ b/web/backend/services/account_pool.py @@ -635,9 +635,15 @@ def build_supply_chain( ) ] + # 检查手册开通状态与最新积分 + status_step = ( + SupplyStep("query_elite_status", {"require_opened": True}) + if exchange_task_type == "exchange_goods" + else SupplyStep("query_points", {}) + ) steps: list[SupplyStep] = [ + status_step, SupplyStep("query_gold_balance", {}), - SupplyStep("query_points", {}), ] # 充值:余额够就跳过(闸门在执行时求值,读的是刚刷新过的账号字段) for amount in recharge_amounts_for(candidate.topup_gifts): diff --git a/web/backend/services/douyu_runner_manual.py b/web/backend/services/douyu_runner_manual.py index f277910..eb8ee42 100644 --- a/web/backend/services/douyu_runner_manual.py +++ b/web/backend/services/douyu_runner_manual.py @@ -117,6 +117,16 @@ class ManualMixin: state = "已开通" if result["elite_opened"] else "未开通" expire_text = self._format_expire_text(result["elite_expire_time"]) points = result["elite_manual_score"] + payload = self._task_payload(task) + if payload.get("require_opened") and not result["elite_opened"]: + self._mark_task( + db, + task, + "failed", + "精英宝典未开通,无法继续充值或兑换,请先开通手册", + result, + ) + return self._mark_task( db, task, diff --git a/web/frontend/src/pages/DouyuPoolPage.tsx b/web/frontend/src/pages/DouyuPoolPage.tsx index 9f83176..23db2be 100644 --- a/web/frontend/src/pages/DouyuPoolPage.tsx +++ b/web/frontend/src/pages/DouyuPoolPage.tsx @@ -1,24 +1,73 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - Alert, Button, Card, Col, Descriptions, InputNumber, Row, Select, Space, - Statistic, Table, Tag, Typography, theme, + Alert, Badge, Button, Card, Col, Descriptions, InputNumber, Modal, + QRCode, Row, Select, Space, Statistic, Table, Tabs, Tag, Tooltip, + Typography, theme, } from 'antd'; -import { ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons'; +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 { douyuPoolApi } from '../api/modules'; +import { douyuApi, douyuPoolApi } from '../api/modules'; import type { - DouyuPoolAccount, DouyuPoolExchangeResult, DouyuPoolLease, DouyuPoolSupply, + 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(); @@ -26,12 +75,36 @@ export default function DouyuPoolPage() { 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(); @@ -45,7 +118,8 @@ export default function DouyuPoolPage() { const loadLeases = useCallback(async () => { try { - setLeases(await douyuPoolApi.leases(true)); + const list = await douyuPoolApi.leases(true); + setLeases(list); } catch (e: unknown) { message.error(getErrorMessage(e)); } @@ -69,8 +143,143 @@ export default function DouyuPoolPage() { 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({ @@ -78,10 +287,13 @@ export default function DouyuPoolPage() { 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}`); + message.success(`已分配 ${data.allocated} 个账号,正在执行补给与兑换(批次 ${data.batch_id})`); } await Promise.all([loadSupply(), loadLeases()]); } catch (e: unknown) { @@ -91,10 +303,35 @@ export default function DouyuPoolPage() { } }; + 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('已释放'); + message.success('已释放租约'); await Promise.all([loadSupply(), loadLeases()]); } catch (e: unknown) { message.error(getErrorMessage(e)); @@ -109,9 +346,66 @@ export default function DouyuPoolPage() { [goods], ); - const accountColumns: ColumnsType = [ - { title: '账号ID', dataIndex: 'account_id', width: 90 }, - { title: '账号', dataIndex: 'username', ellipsis: true }, + // 账号维度聚合任务执行进度 + 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) => ( @@ -120,37 +414,118 @@ export default function DouyuPoolPage() { ), }, - { title: '积分', dataIndex: 'points', width: 90 }, + { title: '当前积分', dataIndex: 'points', width: 95 }, { - title: '需充值', width: 160, - render: (_, row) => (row.topup_yuan > 0 - ? {row.topup_yuan} 元({row.recharge_orders.join(' + ')}) - : 无需), + 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: '可换绑时间', dataIndex: 'role_change_available_at', width: 180, - render: (value: string | null) => (value ? value.replace('T', ' ').slice(0, 19) : '—'), + 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: '商品', dataIndex: 'commodity_id', width: 200 }, - { title: '批次', dataIndex: 'batch_id', width: 130 }, + { title: '商品ID', dataIndex: 'commodity_id', width: 220 }, { - title: '到期', dataIndex: 'expires_at', width: 170, + 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: 100, + 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 ( + {/* 顶部发起卡片 */} )} > + {/* 常用 / 历史快捷标签 */} + {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: '只用已有积分' }, + { value: 'topup', label: '自动充值鱼翅补足' }, + { value: 'skip', label: '仅用已有积分 (不充值)' }, ]} /> @@ -201,6 +616,7 @@ export default function DouyuPoolPage() { icon={} loading={loading} onClick={() => void submit()} + size="middle" > 提交并自动分配 @@ -208,65 +624,132 @@ export default function DouyuPoolPage() { {supply && ( - + - - - - + + + + + 池内全量补足预算 + + ℹ️ + + + )} value={supply.cost.est_yuan} suffix="元" loading={previewing} /> - + )} {supply?.manual_payment_required && ( )}
- {result && ( - - - {result.requested} - {result.allocated} - - 0 ? 'danger' : undefined}>{result.shortfall} - - {result.recharge_channel || '—'} - - {result.reasons.length > 0 && ( + {/* 活跃批次执行与进度面板 */} + {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')} +
+
+ )} +
); }