@@ -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 < string , string > = {
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 < string > ( '' ) ;
const [ quantity , setQuantity ] = useState ( 1 ) ;
const [ allowTopup , setAllowTopup ] = useState ( true ) ;
const [ recentGoodsIds , setRecentGoodsIds ] = useState < string [ ] > ( ( ) = > loadRecentGoodsIds ( ) ) ;
const [ supply , setSupply ] = useState < DouyuPoolSupply | null > ( null ) ;
const [ result , setResult ] = useState < DouyuPoolExchangeResult | null > ( null ) ;
const [ leases , setLeases ] = useState < DouyuPoolLease [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ previewing , setPreviewing ] = useState ( false ) ;
// 活跃批次与任务流水监控
const [ activeBatchId , setActiveBatchId ] = useState < string | null > ( null ) ;
const [ batchTasks , setBatchTasks ] = useState < DouyuTaskItem [ ] > ( [ ] ) ;
const [ batchLoading , setBatchLoading ] = useState ( false ) ;
const [ stoppingBatch , setStoppingBatch ] = useState ( false ) ;
// 微信支付弹窗状态
const [ payModalVisible , setPayModalVisible ] = useState ( false ) ;
const [ selectedPayTaskId , setSelectedPayTaskId ] = useState < number | null > ( null ) ;
const payQrCanvasWrapRef = useRef < HTMLDivElement | null > ( null ) ;
const autoOpenedPayTasks = useRef < Set < number > > ( 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 < string , unknown > | 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 < string > ( ) ;
// 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 < DouyuPoolAccount > = [
{ 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 ) = > (
< Space direction = "vertical" size = { 0 } >
< Text strong > { val || ` # ${ row . account_id } ` } < / Text >
{ row . role_change_available_at && (
< Text type = "secondary" style = { { fontSize : 11 } } >
换 绑 : { row . role_change_available_at . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) }
< / Text >
) }
< / Space >
) ,
} ,
{
title : '梯队' , dataIndex : 'tier' , width : 90 ,
render : ( tier : string ) = > (
@@ -120,37 +414,118 @@ export default function DouyuPoolPage() {
< / Tag >
) ,
} ,
{ title : '积分' , dataIndex : 'points' , width : 90 } ,
{ title : '当前 积分' , dataIndex : 'points' , width : 95 } ,
{
title : '需充值' , width : 160 ,
render : ( _ , row ) = > ( row . topup_yuan > 0
? < Text type = "warning" > { row . topup_yuan } 元 ( { row . recharge_orders . join ( ' + ' ) } ) < / Text >
: < Text type = "secondary" > 无 需 < / Text > ) ,
title : '需充值金额 ' , width : 150 ,
render : ( _ , row ) = > {
if ( row . topup_yuan <= 0 ) return < Text type = "secondary" > 无 需 充 值 < / Text > ;
const isSplit = row . recharge_orders && row . recharge_orders . length > 1 ;
return (
< Space direction = "vertical" size = { 0 } >
< Text type = "warning" strong > ¥ { row . topup_yuan } 元 < / Text >
{ isSplit && (
< Text type = "secondary" style = { { fontSize : 11 } } >
( 拆 为 { row . recharge_orders . join ( ' + ' ) } )
< / Text >
) }
< / Space >
) ;
} ,
} ,
{
title : '可换绑时间' , dataIndex : 'role_change_available_at ', width : 180 ,
render : ( value : string | null ) = > ( value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 19 ) : '—' ) ,
title : '当前执行状态 ' , width : 200 ,
render : ( _ , row ) = > (
< Badge
status = { row . stepTone }
text = { < Text style = { { fontSize : 13 } } > { row . currentStepText } < / Text > }
/ >
) ,
} ,
{
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 (
< Space size = { 8 } >
{ canPay && (
< Button
type = "primary"
size = "small"
icon = { < CreditCardOutlined / > }
onClick = { ( ) = > {
setSelectedPayTaskId ( row . payTask ! . id ) ;
setPayModalVisible ( true ) ;
} }
>
扫 码 支 付
< / Button >
) }
{ isExchanged && (
< ExchangeResultImage
task = { row . exchangeTask ! }
onClick = { ( task , url ) = > setExchangePreview ( { task , url } ) }
/ >
) }
< / Space >
) ;
} ,
} ,
] ;
const leaseColumns : ColumnsType < DouyuPoolLease > = [
{ 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 ) = > (
< Button
type = "link"
size = "small"
style = { { padding : 0 } }
onClick = { ( ) = > {
setActiveBatchId ( val ) ;
message . info ( ` 已切换查看批次 ${ val } ` ) ;
} }
>
{ val || '—' }
< / Button >
) ,
} ,
{
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 ) = > (
< Button size = "small" danger onClick = { ( ) = > void releaseLease ( row . id ) } > 释 放 < / Button >
< Space size = { 8 } >
< Button
size = "small"
icon = { < EyeOutlined / > }
onClick = { ( ) = > {
setActiveBatchId ( row . batch_id ) ;
message . info ( ` 已切换查看批次 ${ row . batch_id } ` ) ;
} }
>
查 看 进 度
< / Button >
< Button size = "small" danger onClick = { ( ) = > void releaseLease ( row . id ) } > 释 放 < / Button >
< / Space >
) ,
} ,
] ;
// 支付二维码 URL
const payUrl = taskQrUrl ( activePayTask ) ;
const payAccountName = activePayTask
? ( activePayTask . account_nickname || activePayTask . account_username || activePayTask . account_uid || ` # ${ activePayTask . account_id } ` )
: '' ;
return (
< Space direction = "vertical" size = { 16 } style = { { width : '100%' } } >
{ /* 顶部发起卡片 */ }
< Card
title = "发起兑换(系统自动分配账号)"
extra = { (
@@ -159,21 +534,61 @@ export default function DouyuPoolPage() {
< / Button >
) }
>
{ /* 常用 / 历史快捷标签 */ }
{ quickGoods . length > 0 && (
< div style = { { marginBottom : 16 } } >
< Space wrap size = { [ 8 , 8 ] } align = "center" >
< Text type = "secondary" style = { { fontSize : 13 , marginRight : 2 } } >
< HistoryOutlined style = { { marginRight : 4 } } / >
常 用 / 历 史 快 捷 :
< / Text >
{ quickGoods . map ( ( item ) = > {
const isSelected = commodityId === item . commodity_id ;
const isHot = item . name . includes ( '兔兔白日梦' ) || item . name . includes ( '狂战萌兔' ) ;
return (
< Tag.CheckableTag
key = { item . commodity_id }
checked = { isSelected }
onChange = { ( ) = > 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 && < FireOutlined style = { { color : '#ff4d4f' , marginRight : 4 } } / > }
{ item . name }
< Text
type = { isSelected ? undefined : 'secondary' }
style = { { fontSize : 12 , marginLeft : 4 , color : isSelected ? '#fff' : undefined } }
>
( { item . score ? ? 0 } 分 )
< / Text >
< / Tag.CheckableTag >
) ;
} ) }
< / Space >
< / div >
) }
< Row gutter = { [ 16 , 16 ] } align = "bottom" >
< Col xs = { 24 } md = { 10 } >
< Text type = "secondary" > 皮 肤 < / Text >
< Text type = "secondary" style = { { display : 'block' , marginBottom : 4 } } > 选 择 要 兑 换 的 皮 肤 < / Text >
< Select
showSearch
optionFilterProp = "label"
style = { { width : '100%' } }
placeholder = "选择要兑换的 皮肤"
placeholder = "选择皮肤"
value = { commodityId || undefined }
onChange = { setCommodityId }
onChange = { handleSelectGood }
options = { goodsOptions }
/ >
< / Col >
< Col xs = { 12 } md = { 4 } >
< Text type = "secondary" > 需 要 几 个 账 号 < / Text >
< Text type = "secondary" style = { { display : 'block' , marginBottom : 4 } } > 需 要 几 个 账 号 < / Text >
< InputNumber
min = { 1 }
max = { 50 }
@@ -183,14 +598,14 @@ export default function DouyuPoolPage() {
/ >
< / Col >
< Col xs = { 12 } md = { 5 } >
< Text type = "secondary" > 积 分 不 足 时 < / Text >
< Text type = "secondary" style = { { display : 'block' , marginBottom : 4 } } > 积 分 不 足 时 策 略 < / Text >
< Select
style = { { width : '100%' } }
value = { allowTopup ? 'topup' : 'skip' }
onChange = { ( value ) = > setAllowTopup ( value === 'topup' ) }
options = { [
{ value : 'topup' , label : '自动充值积分 ' } ,
{ value : 'skip' , label : '只 用已有积分' } ,
{ value : 'topup' , label : '自动充值鱼翅补足 ' } ,
{ value : 'skip' , label : '仅 用已有积分 (不充值) ' } ,
] }
/ >
< / Col >
@@ -201,6 +616,7 @@ export default function DouyuPoolPage() {
icon = { < ThunderboltOutlined / > }
loading = { loading }
onClick = { ( ) = > void submit ( ) }
size = "middle"
>
提 交 并 自 动 分 配
< / Button >
@@ -208,65 +624,132 @@ export default function DouyuPoolPage() {
< / Row >
{ supply && (
< Row gutter = { 16 } style = { { marginTop : 16 } } >
< Row gutter = { 16 } style = { { marginTop : 20 } } >
< Col xs = { 12 } md = { 6 } >
< Statistic title = "可直接兑(免充值)" value = { supply . zero_topup } suffix = "个" / >
< / Col >
< Col xs = { 12 } md = { 6 } >
< Statistic title = "充值后可兑" value = { supply . after_topup } suffix = "个" / >
< Statistic
title = "免充值可用账号"
value = { supply . zero_topup }
suffix = "个"
valueStyle = { { color : token.colorSuccess } }
/ >
< / Col >
< Col xs = { 12 } md = { 6 } >
< Statistic
title = "全部兑完预计花费 "
title = "需充值可用账号 "
value = { supply . after_topup }
suffix = "个"
valueStyle = { { color : token.colorWarning } }
/ >
< / Col >
< Col xs = { 12 } md = { 6 } >
< Statistic
title = { (
< span >
池 内 全 量 补 足 预 算
< Tooltip title = "将池内所有待充账号全部补足所需总花费(非单次分配费用)" >
< Text type = "secondary" style = { { fontSize : 12 , marginLeft : 4 } } > ℹ ️ < / Text >
< / Tooltip >
< / span >
) }
value = { supply . cost . est_yuan }
suffix = "元"
loading = { previewing }
/ >
< / Col >
< Col xs = { 12 } md = { 6 } >
< Statistic title = "当前租用中 " value = { supply . leased } suffix = "个" / >
< Statistic title = "我的在租账号 " value = { leases . length } suffix = "个" / >
< / Col >
< / Row >
) }
{ supply ? . manual_payment_required && (
< Alert
style = { { marginTop : 12 } }
type = "warning "
style = { { marginTop : 16 } }
type = "info "
showIcon
message = { ` 当前鱼翅渠道为「 ${ supply . recharge_channel } 」,需要人工扫码支付 ` }
description = "系统会生成支付码并等待 10 分钟;超时后转入人工处理,需要你到「我的租约」里继续或放弃 。"
message = "当前充值渠道为「微信扫码支付」"
description = "提交后若分配到需充值账号,系统将在此处自动弹出支付二维码。扫码支付后系统将自动完成赠令与皮肤兑换全流程 。"
/ >
) }
< / Card >
{ result && (
< Card title = { ` 本次分配结果(批次 ${ result . batch_id || '—' } ) ` } >
< Descriptions size = "small" column = { 4 } style = { { marginBottom : 12 } } >
< Descriptions.Item label = "需求" > { result . requested } < / Descriptions.Item >
< Descriptions.Item label = "实配" > { result . allocated } < / Descriptions.Item >
< Descriptions.Item label = "缺口" >
< Text type = { result . shortfall > 0 ? 'danger' : undefined } > { result . shortfall } < / Text >
< / Descriptions.Item >
< Descriptions.Item label = "充值渠道" > { result . recharge_channel || '—' } < / Descriptions.Item >
< / Descriptions >
{ result . reasons . length > 0 && (
{ /* 活跃批次执行与进度面板 */ }
{ activeBatchId && (
< Card
title = { (
< Space size = { 12 } >
< span > 本 次 分 配 与 任 务 流 水 ( 批 次 { activeBatchId } ) < / span >
{ batchLoading ? (
< Tag icon = { < SyncOutlined spin / > } color = "processing" > 实 时 同 步 中 < / Tag >
) : (
< Tag color = "default" > 已 同 步 < / Tag >
) }
< / Space >
) }
extra = { (
< Space size = { 8 } >
{ pendingPayTasks . some ( ( t ) = > hasPaymentQrcode ( t ) && t . status === 'running' ) && (
< Button
type = "primary"
danger
icon = { < CreditCardOutlined / > }
onClick = { ( ) = > setPayModalVisible ( true ) }
>
打 开 支 付 码
< / Button >
) }
< Button
icon = { < StopOutlined / > }
loading = { stoppingBatch }
onClick = { ( ) = > void handleStopBatch ( ) }
>
终 止 批 次
< / Button >
< Button danger onClick = { ( ) = > void handleReleaseBatch ( ) } >
释 放 本 批 租 约
< / Button >
< / Space >
) }
>
{ result && (
< Descriptions size = "small" column = { { xs : 1 , sm : 2 , md : 4 } } style = { { marginBottom : 16 } } >
< Descriptions.Item label = "需求数量" > { result . requested } 个 < / Descriptions.Item >
< Descriptions.Item label = "实配数量" >
< Text strong type = { result . allocated > 0 ? 'success' : 'danger' } > { result . allocated } 个 < / Text >
< / Descriptions.Item >
< Descriptions.Item label = "缺口" >
< Text type = { result . shortfall > 0 ? 'danger' : undefined } > { result . shortfall } 个 < / Text >
< / Descriptions.Item >
< Descriptions.Item label = "充值渠道" > { result . recharge_channel || 'wechat_qr' } < / Descriptions.Item >
< / Descriptions >
) }
{ result ? . reasons && result . reasons . length > 0 && (
< Paragraph type = "warning" style = { { marginBottom : 12 } } >
{ result . reasons . map ( ( reason ) = > < div key = { reason } > · { reason } < / div > ) }
< / Paragraph >
) }
< Table
rowKey = "account_id"
size = "small"
pagination = { false }
columns = { accountColumns }
dataSource = { result . accounts }
dataSource = { accountProgressList }
locale = { { emptyText : '本次没有分配账号' } }
/ >
< / Card >
) }
< Card title = "我的租约(用完自动收回,也可手动释放)" >
{ /* 我的租约列表 */ }
< Card
title = "我的租约(用完会自动收回,也可随时手动释放)"
extra = { (
< Button icon = { < ReloadOutlined / > } size = "small" onClick = { ( ) = > void loadLeases ( ) } >
刷 新 租 约
< / Button >
) }
>
< Table
rowKey = "id"
size = "small"
@@ -277,6 +760,176 @@ export default function DouyuPoolPage() {
style = { { background : token.colorBgContainer } }
/ >
< / Card >
{ /* 微信支付二维码弹窗 */ }
< Modal
title = { (
< Space >
< CreditCardOutlined / >
< span > 微 信 扫 码 充 值 < / span >
< / Space >
) }
open = { payModalVisible }
onCancel = { ( ) = > setPayModalVisible ( false ) }
footer = { null }
centered
width = { 440 }
>
{ pendingPayTasks . length > 1 && (
< Tabs
size = "small"
activeKey = { String ( activePayTask ? . id || '' ) }
onChange = { ( key ) = > 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 ? (
< div style = { { textAlign : 'center' , padding : '8px 0' } } >
{ payAccountName && (
< Text type = "secondary" style = { { display : 'block' , marginBottom : 6 , fontSize : 13 } } >
充 值 账 号 : < Text strong > { payAccountName } < / Text >
< / Text >
) }
{ /* 突出展示充值金额 */ }
< div
style = { {
background : '#fffbe6' ,
border : '1px solid #ffe58f' ,
borderRadius : 8 ,
padding : '8px 16px' ,
display : 'inline-flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
gap : 8 ,
marginBottom : 12 ,
} }
>
< Text type = "secondary" style = { { fontSize : 13 } } > 需 充 值 金 额 : < / Text >
< Text strong style = { { fontSize : 24 , color : '#d46b08' } } >
¥ { activePayAmountInfo . amount != null ? ` ${ activePayAmountInfo . amount } 元 ` : '—' }
< / Text >
{ activePayAmountInfo . orders . length > 1 && (
< Text type = "secondary" style = { { fontSize : 12 , marginLeft : 4 } } >
( 拆 为 { activePayAmountInfo . orders . join ( ' + ' ) } )
< / Text >
) }
< / div >
{ paymentArrived ( activePayTask ) ? (
< Space direction = "vertical" align = "center" size = { 16 } style = { { width : '100%' , padding : '20px 0' } } >
< CheckCircleOutlined style = { { fontSize : 48 , color : token.colorSuccess } } / >
< Alert
type = "success"
showIcon
message = "微信支付已到账"
description = "系统正在自动进行赠送精英令与兑换,请稍候..."
/ >
< Button type = "primary" onClick = { ( ) = > setPayModalVisible ( false ) } > 完 成 并 关 闭 < / Button >
< / Space >
) : [ 'failed' , 'error' , 'stopped' , 'manual_required' ] . includes ( activePayTask . status ) ? (
< Space direction = "vertical" align = "center" size = { 16 } style = { { width : '100%' , padding : '20px 0' } } >
< CloseCircleOutlined style = { { fontSize : 48 , color : token.colorError } } / >
< Alert
type = "error"
showIcon
message = { activePayTask . status === 'stopped' ? '任务已停止' : '支付码任务未完成' }
description = { activePayTask . message || '请重试或在后台查看' }
/ >
< Button onClick = { ( ) = > setPayModalVisible ( false ) } > 关 闭 < / Button >
< / Space >
) : (
< Space direction = "vertical" align = "center" size = { 14 } style = { { width : '100%' } } >
{ payUrl ? (
< div ref = { payQrCanvasWrapRef } style = { { background : '#fff' , padding : 12 , borderRadius : 8 , display : 'inline-block' } } >
< QRCode value = { payUrl } size = { 220 } bordered = { false } / >
< / div >
) : (
< div style = { { padding : '40px 0' } } >
< SyncOutlined spin style = { { fontSize : 28 , color : token.colorPrimary , marginBottom : 12 } } / >
< div > < Text type = "secondary" > 正 在 向 斗 鱼 申 请 支 付 二 维 码 . . . < / Text > < / div >
< / div >
) }
{ payUrl && (
< >
< QrActions wrapRef = { payQrCanvasWrapRef } url = { payUrl } disabled = { ! payUrl } / >
< Alert
type = "warning"
showIcon
icon = { < ClockCircleOutlined / > }
message = { activePayTask . message || '请使用微信扫码支付' }
description = "支付码有效时间 10 分钟。扫码支付成功后,系统将自动检测到账并推进兑换。"
style = { { textAlign : 'left' , width : '100%' , marginTop : 8 } }
/ >
< / >
) }
< / Space >
) }
< / div >
) : (
< div style = { { textAlign : 'center' , padding : '30px 0' } } >
< Text type = "secondary" > 当 前 没 有 待 支 付 的 充 值 任 务 < / Text >
< / div >
) }
< / Modal >
{ /* 兑换结果大图弹窗 */ }
< Modal
title = "兑换成功凭证"
open = { ! ! exchangePreview }
onCancel = { ( ) = > setExchangePreview ( null ) }
footer = { (
< Space size = { 8 } >
< Button
type = "primary"
icon = { < CopyOutlined / > }
loading = { copyingPreview }
onClick = { async ( ) = > {
if ( ! exchangePreview ) return ;
setCopyingPreview ( true ) ;
try {
const res = await fetch ( exchangePreview . url ) ;
const blob = await res . blob ( ) ;
await copyImageToClipboard ( blob ) ;
message . success ( '兑换凭证图片已复制到剪贴板' ) ;
} catch ( e : unknown ) {
message . error ( getErrorMessage ( e ) ) ;
} finally {
setCopyingPreview ( false ) ;
}
} }
>
复 制 凭 证 图 片
< / Button >
< Button onClick = { ( ) = > setExchangePreview ( null ) } > 关 闭 < / Button >
< / Space >
) }
centered
width = { 380 }
>
{ exchangePreview && (
< div style = { { textAlign : 'center' , padding : '12px 0' } } >
< img
src = { exchangePreview . url }
alt = "兑换凭证"
style = { { width : '100%' , maxHeight : 480 , objectFit : 'contain' , borderRadius : 8 , border : '1px solid #f0f0f0' } }
/ >
< div style = { { marginTop : 12 } } >
< Text type = "secondary" > { resultText ( exchangePreview . task . result , 'commodity_id' ) } < / Text >
< / div >
< / div >
) }
< / Modal >
< / Space >
) ;
}