From ddb1d63417201fb676fff835fa118ab43dc0eb0e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 1 Aug 2026 13:00:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20DouyuTasksPage=20=E5=B8=83?= =?UTF-8?q?=E5=B1=80=EF=BC=9A=E5=88=A0=E9=99=A4=E4=BB=BB=E5=8A=A1=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=8C=BA=EF=BC=8C=E6=93=8D=E4=BD=9C=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A0=8F=E7=A7=BB=E8=87=B3=E8=B4=A6=E5=8F=B7=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E4=B8=8A=E6=96=B9=EF=BC=8C=E8=B4=A6=E5=8F=B7=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E5=85=A8=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/frontend/src/pages/DouyuTasksPage.tsx | 655 ++++++++-------------- 1 file changed, 243 insertions(+), 412 deletions(-) diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index fd64f42..3fc5cf3 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -17,7 +17,6 @@ import { } from '../api/modules'; import { usePermissions } from '../hooks/usePermissions'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; -import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; import { message } from '../utils/antdMessage'; @@ -89,16 +88,6 @@ const ESPORTS_TASK_TYPES = new Set([ const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount'; const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; -const STATUS_COLORS: Record = { - planned: 'default', pending: 'default', running: 'processing', - success: 'success', failed: 'error', error: 'error', stopped: 'warning', -}; - -const STATUS_LABELS: Record = { - planned: '已计划', pending: '等待中', running: '执行中', - success: '成功', failed: '失败', error: '异常', stopped: '已停止', -}; - function resultText(result: Record | null | undefined, key: string): string { const value = result?.[key]; return typeof value === 'string' ? value : ''; @@ -112,8 +101,17 @@ function resultFlag(result: Record | null | undefined, key: str return text === '1' || text === 'true' || text === 'yes'; } -function taskPayUrl(task: DouyuTaskItem | null | undefined): string { - return resultText(task?.result, 'pay_url') || resultText(task?.result, 'url'); +// 提取任务结果中的二维码 URL:绑定/电竞绑定用 url,支付类用 pay_url +function taskQrUrl(task: DouyuTaskItem | null | undefined): string { + if (!task) return ''; + if (task.task_type === 'get_bind_qr') return resultText(task.result, 'url'); + if (['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)) { + return resultText(task.result, 'pay_url'); + } + if (['prepare_esports_bind', 'get_esports_bind_qr'].includes(task.task_type)) { + return resultText(task.result, 'url'); + } + return ''; } function bindReadyForConfirm(task: DouyuTaskItem | null | undefined): boolean { @@ -256,10 +254,12 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY)); const [selectedGoodsId, setSelectedGoodsId] = useState(''); const [accountSearch, setAccountSearch] = useState(''); - const taskRecordAreaRef = useRef(null); - const [taskRecordAreaHeight, setTaskRecordAreaHeight] = useState(360); const accountTableAreaRef = useRef(null); const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(360); + const [accountPageSize, setAccountPageSize] = useState(() => { + const v = Number(localStorage.getItem('douyu_task_account_page_size')); + return [10, 20, 50, 100].includes(v) ? v : 20; + }); const [importOpen, setImportOpen] = useState(false); const [importPool, setImportPool] = useState([]); @@ -408,29 +408,18 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) useEffect(() => { loadData(); }, [loadData]); - // 监听任务记录/账号表格区域高度变化,动态计算 scroll.y 实现表体内部滚动 + // 监听账号表格区域高度变化,动态计算 scroll.y 实现表体内部滚动 useEffect(() => { - const updateNode = ( - node: HTMLDivElement | null, - setter: (v: number) => void, - ) => { - if (!node) return; - setter(Math.max(200, Math.floor(node.getBoundingClientRect().height))); - }; - const taskNode = taskRecordAreaRef.current; - const accountNode = accountTableAreaRef.current; - const updateHeights = () => { - updateNode(taskNode, setTaskRecordAreaHeight); - updateNode(accountNode, setAccountTableAreaHeight); - }; - updateHeights(); + const node = accountTableAreaRef.current; + if (!node) return; + const update = () => setAccountTableAreaHeight(Math.max(200, Math.floor(node.getBoundingClientRect().height))); + update(); if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', updateHeights); - return () => window.removeEventListener('resize', updateHeights); + window.addEventListener('resize', update); + return () => window.removeEventListener('resize', update); } - const observer = new ResizeObserver(updateHeights); - if (taskNode) observer.observe(taskNode); - if (accountNode) observer.observe(accountNode); + const observer = new ResizeObserver(update); + observer.observe(node); return () => observer.disconnect(); }, []); @@ -974,157 +963,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) }, }, { - title: '最近结果', width: 200, + title: '二维码', width: 76, align: 'center', render: (_, record) => { const task = latestTaskByAccount.get(record.id); - if (!task) return ; - const typeLabel = taskTypes[task.task_type] || task.task_type; - // 是否有可打开的二维码/弹窗 - const canOpen = hasBindQrcode(task) || hasPaymentQrcode(task) - || (['prepare_esports_bind', 'get_esports_bind_qr'].includes(task.task_type) - && (Boolean(resultText(task.result, 'url')) || task.result?.esports_bind_dialog === true)); + const url = taskQrUrl(task); + if (!url) return null; const openTask = () => { + if (!task) return; if (hasBindQrcode(task)) openQrTask(task); else if (hasPaymentQrcode(task)) openPayTask(task); else openEsportsBindTask(task); }; return ( - - - {typeLabel} - {canOpen && ( - - )} - - - {task.message || STATUS_LABELS[task.status] || task.status} - - +
+ +
); }, }, ]; - // 任务记录表 - const taskColumns: TableProps['columns'] = [ - { - title: '账号', dataIndex: 'account_username', width: 130, - render: (_, r) => r.account_nickname || r.account_username || r.account_uid, - }, - { - title: '结果', dataIndex: 'message', width: 300, 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 ( - - {r.message || '-'} - {roleName ? {roleName} : null} - {bindUrl || r.result?.esports_bind_dialog === true ? ( - - ) : null} - - ); - } - if (r.task_type === 'get_bind_qr') { - const roleName = resultText(r.result, 'role_name'); - const phase = resultText(r.result, 'bind_phase'); - const ready = bindReadyForConfirm(r); - const polling = r.status === 'running' && r.result?.bind_polling === true; - const bindUrl = resultText(r.result, 'url'); - return ( - - {r.message || '-'} - {ready ? {roleName || '待确认'} : null} - {!ready && (polling || phase) ? ( - {bindPhaseText(phase, polling)} - ) : null} - {bindUrl && ( - - )} - - ); - } - 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'); - return ( - - {r.message || '-'} - {roleName ? {roleName} : null} - {(areaName || platName) ? ( - {[areaName, platName].filter(Boolean).join(' / ')} - ) : null} - - ); - } - if (r.task_type === 'confirm_bind') { - const roleName = resultText(r.result, 'role_name'); - const confirmed = resultFlag(r.result, 'bind_confirmed'); - return ( - - {r.message || '-'} - {roleName ? {roleName} : null} - - ); - } - if (r.task_type === 'confirm_esports_bind') { - const roleName = resultText(r.result, 'role_name'); - const confirmed = resultFlag(r.result, 'bind_confirmed'); - return ( - - {r.message || '-'} - {roleName ? {roleName} : null} - - ); - } - const url = taskPayUrl(r); - return ( - - {r.message || '-'} - {url && ( - - )} - - ); - }, - }, - { title: '任务', dataIndex: 'task_type', width: 170, render: (v) => taskTypes[v] || v }, - { - title: '状态', dataIndex: 'status', width: 80, - render: (v) => {STATUS_LABELS[v] || v}, - }, - { - title: '时间', dataIndex: 'finished_at', width: 170, - render: (_, r) => formatTime(r.finished_at || r.created_at), - }, - ]; - const filteredAccounts = useMemo(() => { if (!accountSearch.trim()) return accounts; const kw = accountSearch.toLowerCase(); @@ -1268,258 +1126,231 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) - {/* 主布局 */} -
- {/* 左侧账号 */} -
- 已选 {selectedIds.length}/{accounts.length}} - style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} - styles={{ body: { flex: 1, minHeight: 0, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }} - > - - - } - value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)} - style={{ width: 200 }} allowClear - /> - - - - {selectedIds.length > 0 && ( - - )} - - -
- + {selectedText} + + + setSelectedIds(keys.map(Number)), - }} - columns={accountColumns} - dataSource={filteredAccounts} - pagination={{ pageSize: 15, showSizeChanger: false, size: 'small', showLessItems: true }} - tableLayout="fixed" - scroll={{ x: 960, y: Math.max(120, accountTableAreaHeight - 80) }} - onRow={(record) => ({ - onContextMenu: (e) => handleRowContextMenu(record, e), - style: { cursor: 'context-menu' }, - title: '右键打开账号动作', - })} + min={1} + max={10} + value={concurrency} + onChange={(v) => setConcurrency(v || 1)} + style={{ width: 70 }} /> + + + } + style={{ flexShrink: 0, marginBottom: 12 }} + styles={{ body: { padding: 8 } }} + > + {isEsportsHandbook ? ( +
+
+
+ + 开通手册 +
+ {renderActionButton('create_esports_qr', 'primary')}
- -
- - {/* 右侧:任务记录(弹性高度) + 操作区(自适应) */} -
- - 成功 {visibleTasks.filter((t) => t.status === 'success').length} - 失败 {visibleTasks.filter((t) => ['failed', 'error'].includes(t.status)).length} +
+
+ + 皮肤兑换 +
+ +
`共 ${total} 条`, - }} - tableLayout="fixed" - scroll={{ x: 850, y: Math.max(120, taskRecordAreaHeight - 80) }} - /> - - - {selectedText}} - style={{ flexShrink: 0, display: 'flex', flexDirection: 'column' }} - styles={{ body: { padding: 8, maxHeight: 260, overflow: 'auto' } }} - > - - - +
+
+ + 充值与送礼 +
+ + + setGoldAmount(v || 1)} + style={{ width: '100%' }} + /> + + setConcurrency(v || 1)} + value={giftCount} + onChange={(v) => setGiftCount(v || 1)} + addonAfter="赠送数量" style={{ width: '100%' }} /> - - - {isEsportsHandbook ? ( -
-
-
- - 开通手册 -
- {renderActionButton('create_esports_qr', 'primary')} -
-
-
- - 皮肤兑换 -
- - ({ value: item.commodity_id, label: goodsLabel(item) }))} - placeholder="选择兑换商品" - showSearch - optionFilterProp="label" - style={{ width: '100%' }} - /> -
- {renderActionButton('refresh_goods')} - {renderActionButton('exchange_goods', 'primary')} -
-
-
- -
-
- - 开通、充值与送礼 -
- - {renderActionButton('create_elite_qr', 'primary')} - - setGoldAmount(v || 1)} - style={{ width: '100%' }} - /> - - - - setGiftCount(v || 1)} - style={{ width: '100%' }} - /> - - - -
+ +
+
+ ) : ( +
+
+
+ + 兑换 +
+ + } + value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)} + style={{ width: 200 }} allowClear + /> + + + + {selectedIds.length > 0 && ( + + )} + + +
+
setSelectedIds(keys.map(Number)), + }} + columns={accountColumns} + dataSource={filteredAccounts} + pagination={{ + pageSize: accountPageSize, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50, 100], + size: 'small', + showLessItems: true, + showTotal: (total) => `共 ${total} 条`, + onChange: (_page, size) => { + if (size !== accountPageSize) { + setAccountPageSize(size); + localStorage.setItem('douyu_task_account_page_size', String(size)); + } + }, + }} + tableLayout="fixed" + scroll={{ x: 960, y: Math.max(120, accountTableAreaHeight - 80) }} + onRow={(record) => ({ + onContextMenu: (e) => handleRowContextMenu(record, e), + style: { cursor: 'context-menu' }, + title: '右键打开账号动作', + })} + /> - + {/* 右键菜单 */} {contextMenu && (