@@ -24,7 +24,9 @@ import { message } from '../utils/antdMessage';
const { Text } = Typography ;
const QUICK_ACTIONS = [
type HandbookKind = 'elite' | 'esports' ;
const ELITE_QUICK_ACTIONS = [
{ key : 'get_bind_qr' , icon : < QrcodeOutlined / > } ,
{ key : 'confirm_bind' , icon : < CheckCircleOutlined / > } ,
{ key : 'create_elite_qr' , icon : < CreditCardOutlined / > } ,
@@ -38,8 +40,37 @@ const QUICK_ACTIONS = [
{ key : 'donate_elite_gift' , icon : < GiftOutlined / > } ,
] ;
const BIND_ACTION_KEYS = [ 'get_bind_qr' , 'confirm_bind' , 'query_game_name' , 'query_change_bind_time' ] ;
const QUERY_ACTION_KEYS = [ 'query_points' , 'query_limited_goods' , 'query_gold_balance' ] ;
const ESPORTS_QUICK_ACTIONS = [
{ key : 'prepare_esports_bind' , icon : < QrcodeOutlined / > } ,
{ key : 'query_esports_game_name' , icon : < SearchOutlined / > } ,
{ key : 'create_esports_qr' , icon : < CreditCardOutlined / > } ,
] ;
const ELITE_BIND_ACTION_KEYS = [ 'get_bind_qr' , 'confirm_bind' , 'query_game_name' , 'query_change_bind_time' ] ;
const ELITE_QUERY_ACTION_KEYS = [ 'query_points' , 'query_limited_goods' , 'query_gold_balance' ] ;
const ELITE_TASK_TYPES = new Set ( [
'get_bind_qr' ,
'confirm_bind' ,
'create_elite_qr' ,
'create_gold_qr' ,
'donate_elite_gift' ,
'query_points' ,
'exchange_goods' ,
'query_game_name' ,
'query_change_bind_time' ,
'query_limited_goods' ,
'query_gold_balance' ,
'refresh_goods' ,
'query_exchange_records' ,
'prefetch_csrf_token' ,
] ) ;
const ESPORTS_TASK_TYPES = new Set ( [
'prepare_esports_bind' ,
'get_esports_bind_qr' ,
'query_esports_game_name' ,
'confirm_esports_bind' ,
'create_esports_qr' ,
] ) ;
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount' ;
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count' ;
@@ -58,6 +89,14 @@ const BIND_STATUS_LABELS: Record<string, string> = {
game_queried : '已识别角色' ,
game_not_bound : '未绑定' ,
bind_confirmed : '已绑定' ,
esports_bind_qr_generated : '电竞待绑定' ,
esports_role_queried : '已查电竞角色' ,
esports_bind_ready : '电竞待确认' ,
esports_role_switching : '电竞换角色' ,
esports_bind_confirming : '电竞绑定中' ,
esports_bound : '电竞已绑定' ,
esports_qr_created : '电竞待开通' ,
esports_opened : '电竞已开通' ,
change_time_queried : '已查换绑' ,
} ;
@@ -93,17 +132,25 @@ function bindReadyForConfirm(task: DouyuTaskItem | null | undefined): boolean {
function hasBindQrcode ( task : DouyuTaskItem | null | undefined ) : boolean {
if ( ! task || task . task_type !== 'get_bind_qr' ) return false ;
// 有二维码,或已有待确认角色(无需再扫码)都应可展示绑定面板
return Boolean ( resultText ( task . result , 'url' ) ) || bindReadyForConfirm ( task ) ;
return Boolean ( resultText ( task . result , 'url' ) )
|| bindReadyForConfirm ( task )
|| resultFlag ( task . result , 'esports_bound' ) ;
}
function hasPaymentQrcode ( task : DouyuTaskItem | null | undefined ) : boolean {
return Boolean ( task && [ 'create_elite_qr' , 'create_gold_qr' ] . includes ( task . task_type ) && resultText ( task . result , 'pay_url' ) ) ;
return Boolean (
task
&& [ 'create_elite_qr' , 'create_esports_qr' , 'create_gold_qr' ] . includes ( task . task_type )
&& resultText ( task . result , 'pay_url' ) ,
) ;
}
function paymentArrived ( task : DouyuTaskItem | null | undefined ) : boolean {
if ( ! task || ! [ 'create_elite_qr' , 'create_gold_qr' ] . includes ( task . task_type ) ) return false ;
if ( ! task || ! [ 'create_elite_qr' , 'create_esports_qr' , 'create_gold_qr' ] . includes ( task . task_type ) ) return false ;
if ( task . status !== 'success' ) return false ;
return task . result ? . gold_recharged === true || task . result ? . elite_opened === true ;
return task . result ? . gold_recharged === true
|| task . result ? . elite_opened === true
|| task . result ? . esports_opened === true ;
}
function bindPhaseText ( phase : string , polling : boolean ) : string {
@@ -149,7 +196,14 @@ function savedPositiveInteger(key: string, fallback = 1): number {
return Number . isInteger ( value ) && value >= 1 ? value : fallback ;
}
export default function DouyuTasksPage() {
export default function DouyuTasksPage ( { handbook } : { handbook : HandbookKind } ) {
const isEsportsHandbook = handbook === 'esports' ;
const handbookTitle = isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台' ;
const handbookDescription = isEsportsHandbook
? '电竞手册角色绑定与开通任务'
: '精英宝典绑定、开通、积分与兑换任务' ;
const activeTaskTypes = isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES ;
const quickActions = isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS ;
const { can } = usePermissions ( ) ;
const [ accounts , setAccounts ] = useState < DouyuTaskAccountItem [ ] > ( [ ] ) ;
const [ selectedIds , setSelectedIds ] = useState < number [ ] > ( [ ] ) ;
@@ -174,10 +228,13 @@ export default function DouyuTasksPage() {
// 绑定/支付弹窗以 task 为唯一数据源
const [ qrTask , setQrTask ] = useState < DouyuTaskItem | null > ( null ) ;
const [ esportsBindTask , setEsportsBindTask ] = useState < DouyuTaskItem | null > ( null ) ;
const [ payTask , setPayTask ] = useState < DouyuTaskItem | null > ( null ) ;
const autoOpenedQrTaskIds = useRef < Set < number > > ( new Set ( ) ) ;
const autoOpenedEsportsBindTaskIds = useRef < Set < number > > ( new Set ( ) ) ;
const autoOpenedPayTaskIds = useRef < Set < number > > ( new Set ( ) ) ;
const autoOpenQrReady = useRef ( false ) ;
const autoOpenEsportsBindReady = useRef ( false ) ;
const autoOpenPayReady = useRef ( false ) ;
const logs = useWebSocketLogs ( ) ;
@@ -192,22 +249,41 @@ export default function DouyuTasksPage() {
return map ;
} , [ tasks ] ) ;
const latestEsportsStateTaskByAccount = useMemo ( ( ) = > {
const map = new Map < number , DouyuTaskItem > ( ) ;
for ( const task of tasks ) {
if ( ! [ 'prepare_esports_bind' , 'get_esports_bind_qr' , 'query_esports_game_name' , 'confirm_esports_bind' ]
. includes ( task . task_type ) || ! task . result ) continue ;
const previous = map . get ( task . account_id ) ;
if ( ! previous || task . id > previous . id ) map . set ( task . account_id , task ) ;
}
return map ;
} , [ tasks ] ) ;
const visibleTasks = useMemo (
( ) = > tasks . filter ( ( task ) = > activeTaskTypes . has ( task . task_type ) ) ,
[ activeTaskTypes , tasks ] ,
) ;
// 换绑时间:优先最近一次查询/拦截任务结果,回退账号落库字段
const latestChangeWaitByAccount = useMemo ( ( ) = > {
const map = new Map < number , { wait : number | null ; text : string ; taskId : number } > ( ) ;
for ( const task of tasks ) {
if ( ! [ 'query_change_bind_time' , 'get_bind_qr' ] . includes ( task . task_type ) ) continue ;
const taskTypes = isEsportsHandbook
? [ 'prepare_esports_bind' , 'get_esports_bind_qr' , 'query_esports_game_name' , 'confirm_esports_bind' ]
: [ 'query_change_bind_time' , 'get_bind_qr' ] ;
if ( ! taskTypes . includes ( task . task_type ) ) continue ;
if ( ! [ 'success' , 'failed' ] . includes ( task . status ) ) continue ;
const wait = resultNumber ( task . result , 'change_role_wait_time' ) ;
// get_bind_qr 只有冷却拦截时才有 wait;无 wait 时跳过,避免覆盖
if ( task . task_type === 'get_bind_qr' && wait == null ) continue ;
if ( ! isEsportsHandbook && task . task_type === 'get_bind_qr' && wait == null ) continue ;
const existing = map . get ( task . account_id ) ;
if ( existing && existing . taskId > task . id ) continue ;
const text = resultText ( task . result , 'change_role_wait_text' ) || formatWaitSeconds ( wait ) ;
map . set ( task . account_id , { wait , text , taskId : task.id } ) ;
}
return map ;
} , [ tasks ] ) ;
} , [ isEsportsHandbook , tasks ] ) ;
const loadData = useCallback ( async ( ) = > {
setLoading ( true ) ;
@@ -227,9 +303,13 @@ export default function DouyuTasksPage() {
if ( ! autoOpenQrReady . current ) {
for ( const task of taskResult . value ) {
if ( hasBindQrcode ( task ) ) autoOpenedQrTaskIds . current . add ( task . id ) ;
if ( task . task_type === 'prepare_esports_bind' && task . result ? . esports_bind_dialog === true ) {
autoOpenedEsportsBindTaskIds . current . add ( task . id ) ;
}
if ( hasPaymentQrcode ( task ) ) autoOpenedPayTaskIds . current . add ( task . id ) ;
}
autoOpenQrReady . current = true ;
autoOpenEsportsBindReady . current = true ;
autoOpenPayReady . current = true ;
}
}
@@ -264,8 +344,8 @@ export default function DouyuTasksPage() {
} , [ giftCount ] ) ;
const hasActiveTasks = useMemo (
( ) = > tasks . some ( ( task ) = > [ 'pending' , 'running' , 'planned' ] . includes ( task . status ) ) ,
[ tasks ] ,
( ) = > visibleTasks . some ( ( task ) = > [ 'pending' , 'running' , 'planned' ] . includes ( task . status ) ) ,
[ visibleTasks ] ,
) ;
// 有活跃任务或 WS 连接时 3 秒刷 tasks;空闲 15 秒轻量刷新
@@ -277,33 +357,55 @@ export default function DouyuTasksPage() {
const openQrTask = useCallback ( ( task : DouyuTaskItem ) = > {
autoOpenedQrTaskIds . current . add ( task . id ) ;
setEsportsBindTask ( null ) ;
setPayTask ( null ) ;
setQrTask ( task ) ;
} , [ ] ) ;
const openEsportsBindTask = useCallback ( ( task : DouyuTaskItem ) = > {
autoOpenedEsportsBindTaskIds . current . add ( task . id ) ;
setQrTask ( null ) ;
setPayTask ( null ) ;
setEsportsBindTask ( task ) ;
} , [ ] ) ;
const openPayTask = useCallback ( ( task : DouyuTaskItem ) = > {
autoOpenedPayTaskIds . current . add ( task . id ) ;
setQrTask ( null ) ;
setEsportsBindTask ( null ) ;
setPayTask ( task ) ;
} , [ ] ) ;
// 自动弹出最新绑定二维码(running 有 url 即可)
useEffect ( ( ) = > {
if ( ! autoOpenQrReady . current || qrTask ) return ;
const nextQrTask = tasks
const nextQrTask = visibleTasks
. filter ( ( task ) = > hasBindQrcode ( task ) && ! autoOpenedQrTaskIds . current . has ( task . id ) )
. sort ( ( a , b ) = > b . id - a . id ) [ 0 ] ;
if ( nextQrTask ) openQrTask ( nextQrTask ) ;
} , [ openQrTask , qrTask , tasks ] ) ;
} , [ openQrTask , qrTask , visibleTasks ] ) ;
// 电竞手册先查状态再打开独立绑定面板,不把切换角色二维码当作主流程。
useEffect ( ( ) = > {
if ( ! autoOpenEsportsBindReady . current || esportsBindTask ) return ;
const nextTask = visibleTasks
. filter ( ( task ) = > (
task . task_type === 'prepare_esports_bind'
&& task . result ? . esports_bind_dialog === true
&& ! autoOpenedEsportsBindTaskIds . current . has ( task . id )
) )
. sort ( ( a , b ) = > b . id - a . id ) [ 0 ] ;
if ( nextTask ) openEsportsBindTask ( nextTask ) ;
} , [ esportsBindTask , openEsportsBindTask , visibleTasks ] ) ;
// 自动弹出支付二维码
useEffect ( ( ) = > {
if ( ! autoOpenPayReady . current || payTask ) return ;
const nextPayTask = tasks
const nextPayTask = visibleTasks
. filter ( ( task ) = > hasPaymentQrcode ( task ) && ! paymentArrived ( task ) && ! autoOpenedPayTaskIds . current . has ( task . id ) )
. sort ( ( a , b ) = > b . id - a . id ) [ 0 ] ;
if ( nextPayTask ) openPayTask ( nextPayTask ) ;
} , [ openPayTask , payTask , tasks ] ) ;
} , [ openPayTask , payTask , visibleTasks ] ) ;
// 弹窗跟随最新 task 状态(后端 progress 写入后 UI 自动更新)
useEffect ( ( ) = > {
@@ -312,6 +414,12 @@ export default function DouyuTasksPage() {
if ( latest && latest !== qrTask ) setQrTask ( latest ) ;
} , [ qrTask , tasks ] ) ;
useEffect ( ( ) = > {
if ( ! esportsBindTask ) return ;
const latest = tasks . find ( ( task ) = > task . id === esportsBindTask . id ) ;
if ( latest && latest !== esportsBindTask ) setEsportsBindTask ( latest ) ;
} , [ esportsBindTask , tasks ] ) ;
useEffect ( ( ) = > {
if ( ! payTask ) return ;
const latest = tasks . find ( ( task ) = > task . id === payTask . id ) ;
@@ -325,14 +433,25 @@ export default function DouyuTasksPage() {
const batchBusy = Boolean ( runningBatchId || logs . connected || hasActiveTasks ) ;
const startTask = async ( taskType : string , accountIds? : number [ ] ) = > {
const startTask = async (
taskType : string ,
accountIds? : number [ ] ,
extraPayload : Record < string , unknown > = { } ,
) = > {
const ids = accountIds || selectedIds ;
if ( ids . length === 0 ) {
message . warning ( '请先选择账号' ) ;
return ;
}
// 绑定确认/查询允许在其他账号仍轮询时继续,避免卡在多账号批次里
const allowDuringBatch = taskType === 'confirm_bind' || taskType === 'query_game_name' ;
const allowDuringBatch = [
'confirm_bind' ,
'confirm_esports_bind' ,
'query_game_name' ,
'query_esports_game_name' ,
'prepare_esports_bind' ,
'get_esports_bind_qr' ,
] . includes ( taskType ) ;
if ( batchBusy && ! allowDuringBatch ) {
message . warning ( '当前已有批次在运行,请先停止或等待结束' ) ;
return ;
@@ -341,7 +460,7 @@ export default function DouyuTasksPage() {
message . warning ( '请先选择兑换商品' ) ;
return ;
}
const payload : Record < string , unknown > = { } ;
const payload : Record < string , unknown > = { . . . extraPayload } ;
if ( taskType === 'exchange_goods' ) payload . commodity_id = selectedGoodsId ;
if ( taskType === 'create_gold_qr' ) payload . amount = goldAmount ;
if ( taskType === 'donate_elite_gift' ) payload . gift_count = giftCount ;
@@ -460,12 +579,87 @@ export default function DouyuTasksPage() {
void startTask ( taskType , contextMenu . accountIds ) ;
} ;
// ---- 电竞手册绑定面板 ----
const esportsRelatedTasks = esportsBindTask
? tasks
. filter ( ( task ) = > (
task . account_id === esportsBindTask . account_id
&& task . id >= esportsBindTask . id
&& [ 'prepare_esports_bind' , 'get_esports_bind_qr' , 'query_esports_game_name' , 'confirm_esports_bind' ]
. includes ( task . task_type )
) )
. sort ( ( a , b ) = > b . id - a . id )
: [ ] ;
const esportsLatestStateTask = esportsRelatedTasks . find ( ( task ) = > task . result ) || esportsBindTask ;
const esportsLatestRoleTask = esportsRelatedTasks . find (
( task ) = > task . task_type === 'query_esports_game_name' ,
) || null ;
const esportsLatestQrTask = esportsRelatedTasks . find (
( task ) = > Boolean ( resultText ( task . result , 'url' ) ) ,
) || null ;
const esportsConfirmRunning = esportsRelatedTasks . some (
( task ) = > task . task_type === 'confirm_esports_bind' && [ 'planned' , 'pending' , 'running' ] . includes ( task . status ) ,
) ;
const esportsRoleQueryRunning = Boolean (
esportsLatestRoleTask && [ 'planned' , 'pending' , 'running' ] . includes ( esportsLatestRoleTask . status ) ,
) ;
const esportsResult = esportsLatestStateTask ? . result || esportsBindTask ? . result || null ;
const esportsBound = resultFlag ( esportsResult , 'esports_bound' ) || resultFlag ( esportsResult , 'bind_confirmed' ) ;
const esportsRoleName = resultText ( esportsResult , 'role_name' ) ;
const esportsAreaName = resultText ( esportsResult , 'area_name' ) ;
const esportsPlatName = resultText ( esportsResult , 'plat_name' ) ;
const esportsRoleLine = [ esportsPlatName , esportsAreaName ] . filter ( Boolean ) . join ( ' / ' ) ;
const esportsCanChangeRole = Boolean ( esportsBindTask && ! esportsConfirmRunning ) ;
const esportsChangeWait = resultNumber ( esportsResult , 'change_role_wait_time' ) ;
const esportsQrUrl = resultText ( esportsLatestQrTask ? . result , 'url' ) ;
const esportsAccountName = esportsBindTask
? (
esportsBindTask . account_nickname
|| esportsBindTask . account_username
|| esportsBindTask . account_uid
|| ` # ${ esportsBindTask . account_id } `
)
: '' ;
const esportsStatusText = esportsBound
? '电竞手册已绑定'
: esportsConfirmRunning
? '正在完成电竞手册绑定'
: esportsRoleQueryRunning
? '正在查询最新角色'
: esportsLatestStateTask ? . message || '加载电竞手册绑定状态' ;
const esportsCanConfirm = Boolean (
esportsBindTask
&& ! esportsBound
&& esportsRoleName
&& ! esportsConfirmRunning ,
) ;
const queryEsportsRole = ( ) = > {
if ( ! esportsBindTask || esportsRoleQueryRunning ) return ;
void startTask ( 'query_esports_game_name' , [ esportsBindTask . account_id ] ) ;
} ;
const switchEsportsRole = ( ) = > {
if ( ! esportsBindTask || ! esportsCanChangeRole ) return ;
void startTask ( 'get_esports_bind_qr' , [ esportsBindTask . account_id ] ) ;
} ;
const confirmEsportsBindFromDialog = ( ) = > {
if ( ! esportsBindTask || ! esportsCanConfirm ) {
message . warning ( '请先确认当前角色' ) ;
return ;
}
void startTask ( 'confirm_esports_bind' , [ esportsBindTask . account_id ] ) ;
} ;
// ---- QR panel derived state ----
const qrResult = qrTask ? . result || null ;
const qrUrl = resultText ( qrResult , 'url' ) ;
const qrBindPhase = resultText ( qrResult , 'bind_phase' ) ;
const qrAutoPolling = qrTask ? . status === 'running' && qrResult ? . bind_polling === true ;
const qrQueryTask = qrTask ? latestQueryGameTaskByAccount . get ( qrTask . account_id ) || null : 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 qrQueryRoleName = resultText ( qrQueryResult , 'role_name' ) ;
@@ -485,14 +679,29 @@ export default function DouyuTasksPage() {
const qrRoleName = resultText ( qrRoleSourceResult , 'role_name' ) ;
const qrAreaName = resultText ( qrRoleSourceResult , 'area_name' ) ;
const qrPlatName = resultText ( qrRoleSourceResult , 'plat_name' ) ;
const qrCurrentRoleName = resultText ( qrResult , 'current_role_name' )
|| ( resultFlag ( qrResult , 'is_bound_act' ) ? resultText ( qrResult , 'role_name' ) : ' ')
const qrCurrentRoleName = qrQueryRoleName
|| resultText ( qrResult , 'current_role_name ' )
|| (
resultFlag ( qrResult , 'is_bound_act' )
? resultText ( qrResult , 'role_name' )
: ''
)
|| ( qrQueryIsBoundAct ? qrQueryRoleName : '' ) ;
const qrCurrentAreaName = resultText ( qrResult , 'current_ area_name' )
|| ( resultFlag ( qrResult , 'is_bound_act' ) ? resultText ( qrResult , 'area_name' ) : ' ')
const qrCurrentAreaName = resultText ( qrQueryResult , 'area_name' )
|| resultText ( qrResult , 'current_area_name ' )
|| (
resultFlag ( qrResult , 'is_bound_act' )
? resultText ( qrResult , 'area_name' )
: ''
)
|| ( qrQueryIsBoundAct ? resultText ( qrQueryResult , 'area_name' ) : '' ) ;
const qrCurrentPlatName = resultText ( qrResult , 'current_ plat_name' )
|| ( resultFlag ( qrResult , 'is_bound_act' ) ? resultText ( qrResult , 'plat_name' ) : ' ')
const qrCurrentPlatName = resultText ( qrQueryResult , 'plat_name' )
|| resultText ( qrResult , 'current_plat_name ' )
|| (
resultFlag ( qrResult , 'is_bound_act' )
? resultText ( qrResult , 'plat_name' )
: ''
)
|| ( qrQueryIsBoundAct ? resultText ( qrQueryResult , 'plat_name' ) : '' ) ;
const qrRoleLine = qrBindReady
? [ qrPlatName , qrAreaName , qrRoleName ] . filter ( Boolean ) . join ( ' - ' )
@@ -542,13 +751,18 @@ export default function DouyuTasksPage() {
{
title : '游戏名' , dataIndex : 'game_name' , width : 130 ,
render : ( _ , record ) = > {
const queryResult = latestQueryGameTaskByAccount . get ( record . id ) ? . result || null ;
const roleName = resultText ( queryResult , 'role_name' ) || record . game_name ;
const queryResult = (
isEsportsHandbook
? latestEsportsStateTaskByAccount . get ( record . id )
: latestQueryGameTaskByAccount . get ( record . id )
) ? . result || null ;
const roleName = resultText ( queryResult , 'role_name' )
|| ( isEsportsHandbook ? record.esports_game_name : record.game_name ) ;
const channel = (
[ resultText ( queryResult , 'area_name' ) , resultText ( queryResult , 'plat_name' ) ]
. filter ( Boolean )
. join ( ' / ' )
|| record . game_channel
|| ( isEsportsHandbook ? record.esports_game_channel : record.game_channel )
) ;
return roleName ? (
< Space direction = "vertical" size = { 0 } >
@@ -559,18 +773,22 @@ export default function DouyuTasksPage() {
} ,
} ,
{
title : '积分' , dataIndex : 'points' , width : 70 ,
title : isEsportsHandbook ? '电竞 积分' : '积分' ,
dataIndex : isEsportsHandbook ? 'esports_points' : 'points' ,
width : 80 ,
render : ( v ) = > v ? ? < Text type = "secondary" > - < / Text > ,
} ,
{
. . . ( isEsportsHandbook ? [ ] : [ {
title : '鱼翅' , dataIndex : 'gold_balance' , width : 70 ,
render : ( v ) = > v ? ? < Text type = "secondary" > - < / Text > ,
} ,
render : ( v : number | null ) = > v ? ? < Text type = "secondary" > - < / Text > ,
} ] ) ,
{
title : '换绑时间' , dataIndex : 'change_role_wait_time' , width : 130 ,
render : ( _ , record ) = > {
const fromTask = latestChangeWaitByAccount . get ( record . id ) ;
const wait = fromTask ? . wait ? ? record . change_role_wait_time ;
const wait = fromTask ? . wait ? ? (
isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time
) ;
const text = fromTask ? . text || formatWaitSeconds ( wait ) ;
if ( wait == null && ! text ) {
return < Text type = "secondary" > 未 查 < / Text > ;
@@ -591,16 +809,21 @@ export default function DouyuTasksPage() {
return < Text type = "secondary" > { text || '-' } < / Text > ;
} ,
sorter : ( a , b ) = > {
const aw = latestChangeWaitByAccount . get ( a . id ) ? . wait ? ? a . change_role_wait_time ? ? - 1 ;
const bw = latestChangeWaitByAccount . get ( b . id ) ? . wait ? ? b . change_role_wait_time ? ? - 1 ;
const aw = latestChangeWaitByAccount . get ( a . id ) ? . wait
? ? ( isEsportsHandbook ? a.esports_change_role_wait_time : a.change_role_wait_time ) ? ? - 1 ;
const bw = latestChangeWaitByAccount . get ( b . id ) ? . wait
? ? ( isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time ) ? ? - 1 ;
return aw - bw ;
} ,
} ,
{
title : '状态' , dataIndex : 'bind_status' , width : 100 ,
render : ( v ) = > ( v
? < Tag > { BIND_STATUS_LABELS [ v ] || v } < / Tag >
: < Text type = "secondary" > - < / Text > ) ,
render : ( _ , record ) = > {
const value = isEsportsHandbook ? record.esports_bind_status : record.bind_status ;
return value
? < Tag > { BIND_STATUS_LABELS [ value ] || value } < / Tag >
: < Text type = "secondary" > - < / Text > ;
} ,
} ,
] ;
@@ -618,6 +841,22 @@ export default function DouyuTasksPage() {
{
title : '结果' , dataIndex : 'message' , ellipsis : true ,
render : ( _ , r ) = > {
if ( [ 'prepare_esports_bind' , 'get_esports_bind_qr' ] . includes ( r . task_type ) ) {
const roleName = resultText ( r . result , 'role_name' ) ;
const esportsBound = resultFlag ( r . result , 'esports_bound' ) || resultFlag ( r . result , 'bind_confirmed' ) ;
const bindUrl = resultText ( r . result , 'url' ) ;
return (
< Space wrap size = { 4 } >
< Text > { r . message || '-' } < / Text >
{ roleName ? < Tag color = { esportsBound ? 'green' : 'blue' } > { roleName } < / Tag > : null }
{ bindUrl || r . result ? . esports_bind_dialog === true ? (
< Button size = "small" icon = { < QrcodeOutlined / > } onClick = { ( ) = > openEsportsBindTask ( r ) } >
查 看
< / Button >
) : null }
< / Space >
) ;
}
if ( r . task_type === 'get_bind_qr' ) {
const roleName = resultText ( r . result , 'role_name' ) ;
const phase = resultText ( r . result , 'bind_phase' ) ;
@@ -639,7 +878,7 @@ export default function DouyuTasksPage() {
< / Space >
) ;
}
if ( r . task_type === 'query_game_name' ) {
if ( [ 'query_game_name' , 'query_esports_game_name' ] . includes ( r . task_type ) ) {
const roleName = resultText ( r . result , 'role_name' ) ;
const areaName = resultText ( r . result , 'area_name' ) ;
const platName = resultText ( r . result , 'plat_name' ) ;
@@ -663,6 +902,16 @@ export default function DouyuTasksPage() {
< / Space >
) ;
}
if ( r . task_type === 'confirm_esports_bind' ) {
const roleName = resultText ( r . result , 'role_name' ) ;
const confirmed = resultFlag ( r . result , 'bind_confirmed' ) ;
return (
< Space wrap size = { 4 } >
< Text > { r . message || '-' } < / Text >
{ roleName ? < Tag color = { confirmed ? 'green' : 'orange' } > { roleName } < / Tag > : null }
< / Space >
) ;
}
const url = taskPayUrl ( r ) ;
return (
< Space wrap size = { 4 } >
@@ -696,7 +945,8 @@ export default function DouyuTasksPage() {
( a . nickname || '' ) . toLowerCase ( ) . includes ( kw )
|| ( a . username || '' ) . toLowerCase ( ) . includes ( kw )
|| ( a . uid || '' ) . toLowerCase ( ) . includes ( kw )
|| ( a . game_name || '' ) . toLowerCase ( ) . includes ( kw ) ,
|| ( a . game_name || '' ) . toLowerCase ( ) . includes ( kw )
|| ( a . esports_game_name || '' ) . toLowerCase ( ) . includes ( kw ) ,
) ;
} , [ accounts , accountSearch ] ) ;
@@ -710,7 +960,7 @@ export default function DouyuTasksPage() {
? ( payTask . account_nickname || payTask . account_username || payTask . account_uid || ` # ${ payTask . account_id } ` )
: '' ;
const selectedText = selectedIds . length > 0 ? ` 已选 ${ selectedIds . length } 个账号 ` : '请先勾选账号' ;
const actionByKey = useMemo ( ( ) = > new Map ( QUICK_ACTIONS . map ( ( item ) = > [ item . key , item ] ) ) , [ ] ) ;
const actionByKey = useMemo ( ( ) = > new Map ( quickActions . map ( ( item ) = > [ item . key , item ] ) ) , [ quickActions ] ) ;
const actionDisabled = ( taskType : string ) = > {
if ( batchBusy || selectedIds . length === 0 ) return true ;
if ( taskType === 'exchange_goods' ) return ! selectedGoodsId ;
@@ -757,8 +1007,8 @@ export default function DouyuTasksPage() {
{ /* Header */ }
< Space style = { { justifyContent : 'space-between' , width : '100%' , marginBottom : 8 , flexShrink : 0 } } >
< div >
< h2 style = { { margin : 0 } } > 斗 鱼 任 务 操 作 台 < / h2 >
< Text type = "secondary" > 绑 定 、 宝 典 、 鱼 翅 、 积 分 、 兑 换 和 余 额 任 务 < / Text >
< h2 style = { { margin : 0 } } > { handbookTitle } < / h2 >
< Text type = "secondary" > { handbookDescription } < / Text >
< / div >
< Space >
< Button icon = { < ReloadOutlined / > } onClick = { loadData } loading = { loading } > 刷 新 < / Button >
@@ -822,15 +1072,15 @@ export default function DouyuTasksPage() {
size = "small" title = "任务记录"
extra = {
< Space size = { 12 } >
< Tag color = "success" > 成 功 { tasks . filter ( ( t ) = > t . status === 'success' ) . length } < / Tag >
< Tag color = "error" > 失 败 { tasks . filter ( ( t ) = > [ 'failed' , 'error' ] . includes ( t . status ) ) . length } < / Tag >
< Tag color = "success" > 成 功 { visibleTasks . filter ( ( t ) = > t . status === 'success' ) . length } < / Tag >
< Tag color = "error" > 失 败 { visibleTasks . filter ( ( t ) = > [ 'failed' , 'error' ] . includes ( t . status ) ) . length } < / Tag >
< / Space >
}
style = { { flex : '0 0 300px' , minHeight : 260 , display : 'flex' , flexDirection : 'column' , overflow : 'hidden' } }
styles = { { body : { flex : 1 , minHeight : 0 , padding : 8 , overflow : 'hidden' , display : 'flex' , flexDirection : 'column' } } }
>
< Table
rowKey = "id" size = "small" columns = { taskColumns } dataSource = { tasks }
rowKey = "id" size = "small" columns = { taskColumns } dataSource = { visibleTasks }
style = { { flex : 1 , minHeight : 0 } }
loading = { loading } pagination = { { pageSize : 4 , showSizeChanger : false , size : 'small' } }
scroll = { { x : 700 , y : 150 } }
@@ -842,7 +1092,7 @@ export default function DouyuTasksPage() {
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 12 , overflow : 'auto' } } >
< Card
size = "small"
title = "操作"
title = { isEsportsHandbook ? '电竞手册操作' : '精英宝典操作' }
extra = { < Tag color = { selectedIds . length ? 'blue' : 'default' } > { selectedText } < / Tag > }
styles = { { body : { padding : 8 } } }
>
@@ -859,92 +1109,116 @@ export default function DouyuTasksPage() {
/ >
< / Space.Compact >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< QrcodeOutlined / >
< span > 绑 定 与 角 色 < / span >
< / div >
< div style = { actionGridStyle } >
{ BIND_ACTION_KEYS . map ( ( key ) = > renderActionButton ( key ) ) }
< / div >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< SearchOutlined / >
< span > 查 询 < / span >
< / div >
< div style = { actionGridStyle } >
{ QUERY_ACTION_KEYS . map ( ( key ) = > renderActionButton ( key ) ) }
< / div >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< ShoppingOutlined / >
< span > 兑 换 < / span >
< / div >
< Space direction = "vertical" style = { { width : '100%' } } size = { 6 } >
< Select
size = "small"
value = { selectedGoodsId || undefined }
onChange = { setSelectedGoodsId }
options = { goods . map ( ( item ) = > ( { value : item.commodity_id , label : goodsLabel ( item ) } ) ) }
placeholder = "选择兑换商品"
showSearch
optionFilterProp = "label"
style = { { width : '100%' } }
/ >
< div style = { actionGridStyle } >
{ renderActionButton ( 'refresh_goods' ) }
{ renderActionButton ( 'exchange_goods' , 'primary' ) }
{ isEsportsHandbook ? (
< >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< QrcodeOutlined / >
< span > 角 色 绑 定 < / span >
< / div >
< div style = { actionGridStyle } >
{ renderActionButton ( 'prepare_esports_bind' , 'primary' ) }
{ renderActionButton ( 'query_esports_game_name' ) }
< / div >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< CreditCardOutlined / >
< span > 开 通 手 册 < / span >
< / div >
{ renderActionButton ( 'create_esports_qr' , 'primary' ) }
< / div >
< / >
) : (
< >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< QrcodeOutlined / >
< span > 绑 定 与 角 色 < / span >
< / div >
< div style = { actionGridStyle } >
{ ELITE_BIND_ACTION_KEYS . map ( ( key ) = > renderActionButton ( key ) ) }
< / div >
< / div >
< / Space >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< CreditCardOutlined / >
< span > 充 值 与 送 礼 < / span >
< / div >
< Space direction = "vertical" style = { { width : '100%' } } size = { 6 } >
{ renderActionButton ( 'create_elite_qr' ) }
< Space.Compact style = { { width : '100%' } } >
< InputNumber
size = "small"
min = { 1 }
value = { goldAmount }
onChange = { ( v ) = > setGoldAmount ( v || 1 ) }
style = { { width : '100%' } }
/ >
< Button
size = "small"
icon = { < CreditCardOutlined / > }
onClick = { ( ) = > startTask ( 'create_gold_qr' ) }
disabled = { actionDisabled ( 'create_gold_qr' ) }
>
充 值 鱼 翅
< / Button >
< / Space.Compact >
< Space.Compact style = { { width : '100%' } } >
< InputNumber
size = "small"
min = { 1 }
value = { giftCount }
onChange = { ( v ) = > setGiftCount ( v || 1 ) }
style = { { width : '100%' } }
/ >
< Button
size = "small"
icon = { < GiftOutlined / > }
onClick = { ( ) = > startTask ( 'donate_elite_gift' ) }
disabled = { actionDisabled ( 'donate_elite_gift' ) }
>
赠 送 精 英 令
< / Button >
< / Space.Compact >
< / Space >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< SearchOutlined / >
< span > 查 询 < / span >
< / div >
< div style = { actionGridStyle } >
{ ELITE_QUERY_ACTION_KEYS . map ( ( key ) = > renderActionButton ( key ) ) }
< / div >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< ShoppingOutlined / >
< span > 兑 换 < / span >
< / div >
< Space direction = "vertical" style = { { width : '100%' } } size = { 6 } >
< Select
size = "small"
value = { selectedGoodsId || undefined }
onChange = { setSelectedGoodsId }
options = { goods . map ( ( item ) = > ( { value : item.commodity_id , label : goodsLabel ( item ) } ) ) }
placeholder = "选择兑换商品"
showSearch
optionFilterProp = "label"
style = { { width : '100%' } }
/ >
< div style = { actionGridStyle } >
{ renderActionButton ( 'refresh_goods' ) }
{ renderActionButton ( 'exchange_goods' , 'primary' ) }
< / div >
< / Space >
< / div >
< div style = { sectionStyle } >
< div style = { sectionTitleStyle } >
< CreditCardOutlined / >
< span > 开 通 、 充 值 与 送 礼 < / span >
< / div >
< Space direction = "vertical" style = { { width : '100%' } } size = { 6 } >
{ renderActionButton ( 'create_elite_qr' , 'primary' ) }
< Space.Compact style = { { width : '100%' } } >
< InputNumber
size = "small"
min = { 1 }
value = { goldAmount }
onChange = { ( v ) = > setGoldAmount ( v || 1 ) }
style = { { width : '100%' } }
/ >
< Button
size = "small"
icon = { < CreditCardOutlined / > }
onClick = { ( ) = > startTask ( 'create_gold_qr' ) }
disabled = { actionDisabled ( 'create_gold_qr' ) }
>
充 值 鱼 翅
< / Button >
< / Space.Compact >
< Space.Compact style = { { width : '100%' } } >
< InputNumber
size = "small"
min = { 1 }
value = { giftCount }
onChange = { ( v ) = > setGiftCount ( v || 1 ) }
style = { { width : '100%' } }
/ >
< Button
size = "small"
icon = { < GiftOutlined / > }
onClick = { ( ) = > startTask ( 'donate_elite_gift' ) }
disabled = { actionDisabled ( 'donate_elite_gift' ) }
>
赠 送 精 英 令
< / Button >
< / Space.Compact >
< / Space >
< / div >
< / >
) }
< Text type = "secondary" style = { { fontSize : 12 } } >
勾 选 账 号 后 可 批 量 执 行 ; 右 键 账 号 行 可 快 速 执 行 单 账 号 动 作 。
< / Text >
@@ -965,7 +1239,7 @@ export default function DouyuTasksPage() {
style = { { width : 200 , boxShadow : '0 4px 12px rgba(0,0,0,0.12)' } }
>
< Space direction = "vertical" style = { { width : '100%' } } >
{ QUICK_ACTIONS . map ( ( item ) = > (
{ quickActions . map ( ( item ) = > (
< Button
key = { item . key } block size = "small" type = "text"
icon = { item . icon }
@@ -1011,8 +1285,16 @@ export default function DouyuTasksPage() {
columns = { [
{ title : '昵称' , dataIndex : 'nickname' , render : ( _ , r ) = > r . nickname || r . username } ,
{ title : 'UID' , dataIndex : 'uid' } ,
{ title : '游戏名' , dataIndex : 'game_name' , render : ( v ) = > v || '-' } ,
{ title : '积分' , dataIndex : 'points' , render : ( v ) = > v ? ? '-' } ,
{
title : '游戏名' ,
dataIndex : isEsportsHandbook ? 'esports_game_name' : 'game_name' ,
render : ( v ) = > v || '-' ,
} ,
{
title : isEsportsHandbook ? '电竞积分' : '积分' ,
dataIndex : isEsportsHandbook ? 'esports_points' : 'points' ,
render : ( v ) = > v ? ? '-' ,
} ,
] }
pagination = { { pageSize : 10 } }
locale = { { emptyText : '没有可导入的账号(操作台已包含所有账号)' } }
@@ -1021,7 +1303,7 @@ export default function DouyuTasksPage() {
{ /* Config modal */ }
< Modal
title = "斗鱼活动配置"
title = { isEsportsHandbook ? '电竞手册配置' : '精英宝典配置' }
open = { configOpen }
onCancel = { ( ) = > setConfigOpen ( false ) }
onOk = { ( ) = > configFormValues && saveConfig ( configFormValues ) }
@@ -1030,7 +1312,11 @@ export default function DouyuTasksPage() {
>
{ configFormValues && (
< Space direction = "vertical" style = { { width : '100%' } } size = { 8 } >
{ [
{ ( isEsportsHandbook ? [
{ label : '电竞手册 manualID' , key : 'esports_manual_id' } ,
{ label : '电竞手册活动' , key : 'esports_act_alias' } ,
{ label : '房间 ID' , key : 'room_id' } ,
] : [
{ label : 'manualID' , key : 'manual_id' } ,
{ label : 'RID' , key : 'rid' } ,
{ label : '绑定二维码活动' , key : 'bind_act_alias' } ,
@@ -1039,7 +1325,7 @@ export default function DouyuTasksPage() {
{ label : '房间 ID' , key : 'room_id' } ,
{ label : '精英令礼物 ID' , key : 'gift_id' } ,
{ label : '皮肤 ID' , key : 'skin_id' } ,
] . map ( ( { label , key } ) = > (
] ) . map ( ( { label , key } ) = > (
< Space key = { key } style = { { width : '100%' , justifyContent : 'space-between' } } >
< Text > { label } < / Text >
< Input
@@ -1049,10 +1335,12 @@ export default function DouyuTasksPage() {
/ >
< / Space >
) ) }
{ [
{ ( isEsportsHandbook ? [
{ label : '电竞手册金额(分)' , key : 'esports_amount' } ,
] : [
{ label : '宝典金额(分)' , key : 'elite_amount' } ,
{ label : '鱼翅支付方式' , key : 'gold_pay_type' } ,
] . map ( ( { label , key } ) = > (
] ) . map ( ( { label , key } ) = > (
< Space key = { key } style = { { width : '100%' , justifyContent : 'space-between' } } >
< Text > { label } < / Text >
< InputNumber
@@ -1066,7 +1354,87 @@ export default function DouyuTasksPage() {
) }
< / Modal >
{ /* Bind QR Modal — driven by qrTask */ }
{ /* 电竞手册绑定弹窗:活动绑定与角色选择是独立流程。 */ }
< Modal
title = {
< Space >
< QrcodeOutlined / >
< span > 绑 定 电 竞 手 册 角 色 < / span >
< / Space >
}
open = { ! ! esportsBindTask }
onCancel = { ( ) = > {
setEsportsBindTask ( null ) ;
} }
footer = { null }
centered
width = { 440 }
>
{ esportsBindTask && (
< Space direction = "vertical" style = { { width : '100%' } } size = { 14 } >
{ esportsAccountName ? (
< Text type = "secondary" > { esportsAccountName } < / Text >
) : null }
< Tag color = { esportsBound ? 'green' : esportsConfirmRunning ? 'processing' : 'blue' } >
{ esportsStatusText }
< / Tag >
{ esportsRoleName ? (
< div >
< Text type = "secondary" style = { { display : 'block' , marginBottom : 4 } } > 当 前 游 戏 角 色 < / Text >
< Text strong style = { { fontSize : 16 } } > { esportsRoleName } < / Text >
{ esportsRoleLine ? (
< Text type = "secondary" style = { { display : 'block' , marginTop : 4 } } > { esportsRoleLine } < / Text >
) : null }
< / div >
) : (
< Text type = "secondary" > 当 前 未 检 测 到 游 戏 角 色 , 请 先 切 换 角 色 。 < / Text >
) }
{ esportsChangeWait != null && esportsChangeWait > 0 && esportsRoleName ? (
< Text type = "secondary" > 换 绑 冷 却 剩 余 : { formatWaitSeconds ( esportsChangeWait ) } < / Text >
) : null }
{ esportsQrUrl ? (
< div style = { { textAlign : 'center' } } >
< QRCode value = { esportsQrUrl } size = { 220 } bordered = { false } / >
< Text type = "secondary" style = { { display : 'block' , marginTop : 8 } } >
请 在 腾 讯 页 面 选 择 角 色 , 完 成 后 查 询 最 新 角 色 。
< / Text >
< / div >
) : null }
< Space wrap >
< Button
icon = { < SearchOutlined / > }
onClick = { queryEsportsRole }
loading = { esportsRoleQueryRunning }
disabled = { esportsConfirmRunning }
>
查 询 最 新 角 色
< / Button >
< Button
icon = { < QrcodeOutlined / > }
onClick = { switchEsportsRole }
disabled = { ! esportsCanChangeRole || esportsConfirmRunning }
>
切 换 角 色
< / Button >
< Button
type = "primary"
icon = { < CheckCircleOutlined / > }
onClick = { confirmEsportsBindFromDialog }
disabled = { ! esportsCanConfirm }
loading = { esportsConfirmRunning }
>
完 成 绑 定
< / Button >
< / Space >
< / Space >
) }
< / Modal >
{ /* 精英宝典绑定二维码弹窗。 */ }
< Modal
title = {
< Space >