优化虎牙精英宝典工作台与Cookie生成

This commit is contained in:
yml2213
2026-09-02 12:48:41 +08:00
parent 0d21a3fa09
commit 2a1b1e642c
2 changed files with 79 additions and 19 deletions
+9 -9
View File
@@ -75,13 +75,13 @@ def _yamid_new_generate32(rng=None) -> str:
_generate_bits(c, 0, 31), _generate_bits(c, 0, 31),
_generate_bits(c, 32, 47), _generate_bits(c, 32, 47),
_generate_bits(c, 48, 59) + "1", _generate_bits(c, 48, 59) + "1",
_generate_bits(r.randrange(4096), 0, 7), _generate_bits(r.randrange(4095), 0, 7),
_generate_bits(r.randrange(4096), 0, 7), _generate_bits(r.randrange(4095), 0, 7),
_generate_bits(r.randrange(8192), 0, 7) _generate_bits(r.randrange(8191), 0, 7)
+ _generate_bits(r.randrange(8192), 8, 15) + _generate_bits(r.randrange(8191), 8, 15)
+ _generate_bits(r.randrange(8192), 0, 7) + _generate_bits(r.randrange(8191), 0, 7)
+ _generate_bits(r.randrange(8192), 8, 15) + _generate_bits(r.randrange(8191), 8, 15)
+ _generate_bits(r.randrange(8192), 0, 15), + _generate_bits(r.randrange(8191), 0, 15),
)) ))
@@ -131,7 +131,7 @@ def fill_web_cookie_fields(
pairs[key] = value.strip() pairs[key] = value.strip()
def add(key: str, value: str): def add(key: str, value: str):
if value and key not in pairs: if value and (key not in pairs or not pairs[key]):
pairs[key] = value pairs[key] = value
uid = int(uid or 0) uid = int(uid or 0)
@@ -209,4 +209,4 @@ __all__ = [
"cookie_fields", "cookie_fields",
"fill_web_cookie_fields", "fill_web_cookie_fields",
"missing_web_cookie_fields", "missing_web_cookie_fields",
] ]
+70 -10
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { MouseEvent } from 'react';
import { import {
Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography, Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Tooltip, Typography,
} from 'antd'; } from 'antd';
@@ -13,6 +14,7 @@ import { usePermissions } from '../hooks/usePermissions';
import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
import { getErrorMessage } from '../utils/error'; import { getErrorMessage } from '../utils/error';
import { message } from '../utils/antdMessage'; import { message } from '../utils/antdMessage';
import { formatTime } from '../utils/time';
const { Text } = Typography; const { Text } = Typography;
const SCOPE = 'elite' as const; const SCOPE = 'elite' as const;
@@ -35,6 +37,16 @@ const TASK_STATUS_LABELS: Record<string, string> = {
failed: '失败', error: '异常', stopped: '已停止', timeout: '支付超时', failed: '失败', error: '异常', stopped: '已停止', timeout: '支付超时',
}; };
const HUYA_QUICK_ACTIONS = [
{ key: 'get_bind_qr', icon: <QrcodeOutlined /> },
{ key: 'query_game_name', icon: <SearchOutlined /> },
{ key: 'confirm_bind', icon: <CheckCircleOutlined /> },
{ key: 'query_points', icon: <SearchOutlined /> },
{ key: 'query_act_tasks', icon: <SearchOutlined /> },
{ key: 'refresh_goods', icon: <ReloadOutlined /> },
{ key: 'query_exchange_records', icon: <SearchOutlined /> },
];
function savedInterval(): number { function savedInterval(): number {
const value = Number(localStorage.getItem('huya_elite_refresh_interval')); const value = Number(localStorage.getItem('huya_elite_refresh_interval'));
return Number.isInteger(value) && value >= 5 && value <= 60 ? value : 15; return Number.isInteger(value) && value >= 5 && value <= 60 ? value : 15;
@@ -49,6 +61,11 @@ function latestTask(tasks: HuyaTaskItem[], accountId: number): HuyaTaskItem | un
return tasks.filter((task) => task.account_id === accountId).sort((a, b) => b.id - a.id)[0]; return tasks.filter((task) => task.account_id === accountId).sort((a, b) => b.id - a.id)[0];
} }
function taskQrUrl(task: HuyaTaskItem | undefined): string {
if (!task) return '';
return resultText(task, 'bind_redirect_url') || resultText(task, 'pay_url') || resultText(task, 'url');
}
export default function HuyaElitePage() { export default function HuyaElitePage() {
const { can } = usePermissions(); const { can } = usePermissions();
const canConfig = can('huya:config'); const canConfig = can('huya:config');
@@ -78,6 +95,7 @@ export default function HuyaElitePage() {
const [configOpen, setConfigOpen] = useState(false); const [configOpen, setConfigOpen] = useState(false);
const [configLoading, setConfigLoading] = useState(false); const [configLoading, setConfigLoading] = useState(false);
const [configSaving, setConfigSaving] = useState(false); const [configSaving, setConfigSaving] = useState(false);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; accountIds: number[] } | null>(null);
const tasksLoading = useRef(false); const tasksLoading = useRef(false);
const logs = useWebSocketLogs(); const logs = useWebSocketLogs();
@@ -115,6 +133,21 @@ export default function HuyaElitePage() {
const timer = window.setInterval(() => { void loadTasks(); }, (active ? 3 : refreshInterval) * 1000); const timer = window.setInterval(() => { void loadTasks(); }, (active ? 3 : refreshInterval) * 1000);
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, [loadTasks, refreshInterval, tasks]); }, [loadTasks, refreshInterval, tasks]);
useEffect(() => {
if (!contextMenu) return undefined;
const close = () => setContextMenu(null);
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') close();
};
window.addEventListener('click', close);
window.addEventListener('resize', close);
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('click', close);
window.removeEventListener('resize', close);
window.removeEventListener('keydown', onKeyDown);
};
}, [contextMenu]);
const saveAccounts = async (ids: number[]) => { const saveAccounts = async (ids: number[]) => {
const saved = await huyaApi.updateWorkbenchAccounts(ids, SCOPE); const saved = await huyaApi.updateWorkbenchAccounts(ids, SCOPE);
@@ -124,8 +157,8 @@ export default function HuyaElitePage() {
setSelectedIds((prev) => prev.filter((id) => saved.account_ids.includes(id))); setSelectedIds((prev) => prev.filter((id) => saved.account_ids.includes(id)));
}; };
const startTask = async (taskType: string) => { const startTask = async (taskType: string, accountIds: number[] = selectedIds) => {
if (!selectedIds.length) { message.warning('请先勾选账号'); return; } if (!accountIds.length) { message.warning('请先勾选账号'); return; }
if (taskType === 'exchange_goods' && !selectedGoodsId) { message.warning('请先选择兑换商品'); return; } if (taskType === 'exchange_goods' && !selectedGoodsId) { message.warning('请先选择兑换商品'); return; }
if (taskType === 'create_recharge_order' && !selectedRechargeSpu) { message.warning('请先选择宝典商品'); return; } if (taskType === 'create_recharge_order' && !selectedRechargeSpu) { message.warning('请先选择宝典商品'); return; }
setStarting(true); setStarting(true);
@@ -137,7 +170,7 @@ export default function HuyaElitePage() {
const selected = rechargeGoods.find((item) => item.spu_id === selectedRechargeSpu); const selected = rechargeGoods.find((item) => item.spu_id === selectedRechargeSpu);
return { spu_id: selectedRechargeSpu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典' }; return { spu_id: selectedRechargeSpu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典' };
})() : {}; })() : {};
const created = await huyaApi.createTasks({ account_ids: selectedIds, task_type: taskType, handbook_scope: SCOPE, concurrency, payload }); const created = await huyaApi.createTasks({ account_ids: accountIds, task_type: taskType, handbook_scope: SCOPE, concurrency, payload });
setActiveBatches((prev) => [...new Set([...prev, created.batch_id])]); setActiveBatches((prev) => [...new Set([...prev, created.batch_id])]);
logs.connectBatch(created.batch_id, `/api/huya/ws/${created.batch_id}`, { logs.connectBatch(created.batch_id, `/api/huya/ws/${created.batch_id}`, {
clear: false, clear: false,
@@ -177,6 +210,23 @@ export default function HuyaElitePage() {
catch (error) { message.error(getErrorMessage(error)); } catch (error) { message.error(getErrorMessage(error)); }
}; };
const handleRowContextMenu = (row: HuyaAccountItem, event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const accountIds = selectedIds.includes(row.id) && selectedIds.length > 1
? selectedIds
: [row.id];
setSelectedIds(accountIds);
setContextMenu({ x: event.clientX, y: event.clientY, accountIds });
};
const runContextAction = (taskType: string) => {
if (!contextMenu) return;
const ids = contextMenu.accountIds;
setContextMenu(null);
void startTask(taskType, ids);
};
const openQrTask = async (task: HuyaTaskItem) => { const openQrTask = async (task: HuyaTaskItem) => {
setQrTask(task); setQrTask(task);
try { setQrTask(await huyaApi.getTask(task.id)); } catch { /* 使用列表结果 */ } try { setQrTask(await huyaApi.getTask(task.id)); } catch { /* 使用列表结果 */ }
@@ -192,10 +242,11 @@ export default function HuyaElitePage() {
const accountColumns: TableProps<HuyaAccountItem>['columns'] = [ const accountColumns: TableProps<HuyaAccountItem>['columns'] = [
{ title: '#', width: 45, render: (_value, _row, index) => <Text type="secondary">{index + 1}</Text> }, { title: '#', width: 45, render: (_value, _row, index) => <Text type="secondary">{index + 1}</Text> },
{ title: '账号', width: 190, render: (_value, row) => <Space direction="vertical" size={0}><Text strong ellipsis>{row.nickname || row.username || row.uid}</Text><Text type="secondary" style={{ fontSize: 12 }}>UID {row.uid || '-'}</Text></Space> }, { title: '账号', width: 180, render: (_value, row) => <Space direction="vertical" size={0} style={{ width: '100%' }}><Text strong ellipsis title={row.nickname || row.username || row.uid}>{row.nickname || row.username || row.uid}</Text><Text type="secondary" style={{ fontSize: 12 }} ellipsis title={row.username}>UID {row.uid || '-'}</Text></Space> },
{ title: '游戏名', width: 180, render: (_value, row) => row.game_name || row.game_channel || <Text type="secondary"></Text> }, { title: '游戏名', width: 160, render: (_value, row) => <Space direction="vertical" size={0} style={{ width: '100%' }}><Text ellipsis title={row.game_name || row.game_channel}>{row.game_name || <Text type="secondary"></Text>}</Text>{row.game_channel && <Text type="secondary" style={{ fontSize: 12 }} ellipsis>{row.game_channel}</Text>}</Space> },
{ title: '积分', dataIndex: 'points', width: 80, render: (value: number | null) => value ?? <Text type="secondary">-</Text> }, { title: '积分', dataIndex: 'points', width: 75, align: 'right', render: (value: number | null) => value ?? <Text type="secondary">-</Text> },
{ title: '最近操作', ellipsis: true, render: (_value, row) => { const task = latestTask(tasks, row.id); return task ? <Space direction="vertical" size={0}><Text>{TASK_LABELS[task.task_type] || task.task_type}</Text><Text type="secondary" ellipsis>{task.message || '-'}</Text></Space> : <Text type="secondary"></Text>; } }, { title: '最近操作', width: 250, render: (_value, row) => { const task = latestTask(tasks, row.id); return task ? <Space direction="vertical" size={1} style={{ width: '100%' }}><Space size={4}><Tag color={TASK_COLORS[task.status] || 'default'}>{TASK_STATUS_LABELS[task.status] || task.status}</Tag><Text strong ellipsis>{TASK_LABELS[task.task_type] || task.task_type}</Text></Space><Text type="secondary" ellipsis title={task.message}>{task.message || '-'}</Text><Text type="secondary" style={{ fontSize: 12 }}>{formatTime(task.finished_at || task.created_at)}</Text></Space> : <Text type="secondary"></Text>; } },
{ title: '二维码', width: 80, align: 'center', render: (_value, row) => { const task = latestTask(tasks, row.id); const url = taskQrUrl(task); const image = resultText(task, 'mini_qrcode_image'); if (!url && !image) return <Text type="secondary">-</Text>; return <div onClick={(event) => { event.stopPropagation(); if (task) void openQrTask(task); }} style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0 }} title="点击查看二维码">{image ? <img src={`data:image/png;base64,${image}`} alt="二维码" style={{ width: 52, height: 52, objectFit: 'contain' }} /> : <QRCode value={url} size={52} bordered={false} />}</div>; } },
{ title: '状态', width: 90, render: (_value, row) => { const task = latestTask(tasks, row.id); const status = task?.status || row.status || ''; return <Tag color={TASK_COLORS[status] || 'default'}>{TASK_STATUS_LABELS[status] || status || '待操作'}</Tag>; } }, { title: '状态', width: 90, render: (_value, row) => { const task = latestTask(tasks, row.id); const status = task?.status || row.status || ''; return <Tag color={TASK_COLORS[status] || 'default'}>{TASK_STATUS_LABELS[status] || status || '待操作'}</Tag>; } },
]; ];
@@ -211,14 +262,23 @@ export default function HuyaElitePage() {
</Space> </Space>
); );
const accountsBody = <><Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap><Input size="small" allowClear prefix={<SearchOutlined />} placeholder="搜索账号/昵称/游戏名" value={search} onChange={(event) => setSearch(event.target.value)} style={{ width: 240 }} /><Space><Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}></Button>{selectedIds.length > 0 && <Button size="small" danger onClick={() => void saveAccounts(accounts.map((item) => item.id).filter((id) => !selectedIds.includes(id)))}></Button>}</Space></Space><div className="huya-account-table"><Table rowKey="id" size="small" loading={loading} rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)) }} columns={accountColumns} dataSource={filteredAccounts} pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `${total}` }} scroll={{ x: 760, y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (task && ['get_bind_qr', 'create_recharge_order'].includes(task.task_type) && task.result) void openQrTask(task); } })} /></div></>; const accountsBody = <><Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between' }} wrap><Input size="small" allowClear prefix={<SearchOutlined />} placeholder="搜索账号/昵称/游戏名" value={search} onChange={(event) => setSearch(event.target.value)} style={{ width: 240 }} /><Space><Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}></Button>{selectedIds.length > 0 && <Button size="small" danger onClick={() => void saveAccounts(accounts.map((item) => item.id).filter((id) => !selectedIds.includes(id)))}></Button>}</Space></Space><div className="huya-account-table"><Table rowKey="id" size="small" loading={loading} tableLayout="fixed" rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)) }} columns={accountColumns} dataSource={filteredAccounts} locale={{ emptyText: '工作台暂无账号,点击右上角「导入账号」添加' }} pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (total) => `${total}` }} scroll={{ x: 1040, y: 'calc(100vh - 300px)' }} onRow={(row) => ({ onClick: () => { const task = latestTask(tasks, row.id); if (task && ['get_bind_qr', 'create_recharge_order'].includes(task.task_type) && task.result) void openQrTask(task); }, onContextMenu: (event) => handleRowContextMenu(row, event), style: { cursor: 'context-menu' }, title: '右键打开账号动作' })} /></div></>;
const configField = (key: Exclude<keyof HuyaConfig, 'updated_at'>, label: string) => configDraft && <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}><Text>{label}</Text><Input value={configDraft[key] || ''} onChange={(event) => setConfigDraft({ ...configDraft, [key]: event.target.value })} style={{ width: 300 }} /></Space>; const configField = (key: Exclude<keyof HuyaConfig, 'updated_at'>, label: string) => configDraft && <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}><Text>{label}</Text><Input value={configDraft[key] || ''} onChange={(event) => setConfigDraft({ ...configDraft, [key]: event.target.value })} style={{ width: 300 }} /></Space>;
const contextMenuStyle = contextMenu ? {
position: 'fixed' as const,
zIndex: 1000,
left: Math.max(8, Math.min(contextMenu.x, window.innerWidth - 228)),
top: Math.max(8, Math.min(contextMenu.y, window.innerHeight - 330)),
} : undefined;
return <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}><style>{`.huya-operation-section{border:1px solid rgba(128,128,128,.22);border-radius:6px;padding:8px}.huya-operation-title{display:flex;gap:6px;align-items:center;font-weight:600;margin-bottom:8px}.huya-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.huya-account-table{flex:1;min-height:0;overflow:hidden}.huya-account-table .ant-table-wrapper,.huya-account-table .ant-spin-nested-loading,.huya-account-table .ant-spin-container{height:100%}`}</style> const tableStyle = `.huya-operation-section{border:1px solid rgba(128,128,128,.22);border-radius:6px;padding:8px}.huya-operation-title{display:flex;gap:6px;align-items:center;font-weight:600;margin-bottom:8px}.huya-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.huya-account-table{flex:1;min-height:0;overflow:hidden}.huya-account-table .ant-table-wrapper,.huya-account-table .ant-spin-nested-loading,.huya-account-table .ant-spin-container{height:100%}.huya-account-table .ant-table{height:100%;display:flex;flex-direction:column}.huya-account-table .ant-table-container{flex:1;min-height:0;display:flex;flex-direction:column}.huya-account-table .ant-table-header{flex-shrink:0}.huya-account-table .ant-table-body{flex:1;min-height:0;overflow:auto !important}.huya-account-table .ant-table-thead > tr > th,.huya-account-table .ant-table-tbody > tr > td{padding:4px 8px;line-height:20px}.huya-account-table .ant-typography{font-size:12px}.huya-account-table .ant-tag{font-size:11px;line-height:18px;margin-inline-end:0;padding-inline:5px}.huya-account-table .ant-pagination{flex-shrink:0;margin:0;padding:6px 4px 2px;border-top:1px solid rgba(128,128,128,.22)}`;
return <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}><style>{tableStyle}</style>
<Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}><div><h2 style={{ margin: 0 }}></h2><Text type="secondary"></Text></div><Space><Tooltip title={layoutMode === 'split' ? '切换为上下布局' : '切换为左右布局'}><Button icon={<ColumnWidthOutlined />} onClick={() => { const next = layoutMode === 'split' ? 'stack' : 'split'; setLayoutMode(next); localStorage.setItem(LAYOUT_KEY, next); }} /></Tooltip><Button icon={<ReloadOutlined />} onClick={() => void load()} loading={loading}></Button>{canConfig && <Button icon={<SettingOutlined />} onClick={() => void openSettings()}></Button>}{!!activeBatches.length && <Button danger icon={<StopOutlined />} onClick={() => void stop()}></Button>}</Space></Space> <Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}><div><h2 style={{ margin: 0 }}></h2><Text type="secondary"></Text></div><Space><Tooltip title={layoutMode === 'split' ? '切换为上下布局' : '切换为左右布局'}><Button icon={<ColumnWidthOutlined />} onClick={() => { const next = layoutMode === 'split' ? 'stack' : 'split'; setLayoutMode(next); localStorage.setItem(LAYOUT_KEY, next); }} /></Tooltip><Button icon={<ReloadOutlined />} onClick={() => void load()} loading={loading}></Button>{canConfig && <Button icon={<SettingOutlined />} onClick={() => void openSettings()}></Button>}{!!activeBatches.length && <Button danger icon={<StopOutlined />} onClick={() => void stop()}></Button>}</Space></Space>
{layoutMode === 'split' ? <div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}><Card size="small" title="账号" extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>} style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}</Card><Card size="small" title="精英宝典操作" extra={<Space size={4}><Text type="secondary"></Text><InputNumber size="small" min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 58 }} /></Space>} style={{ width: 360, flexShrink: 0, minHeight: 0, overflow: 'hidden' }} styles={{ body: { padding: 8, overflowY: 'auto', height: 'calc(100% - 38px)' } }}>{operationBody}</Card></div> : <><Card size="small" title="精英宝典操作" extra={<Space size={4}><Text type="secondary"></Text><InputNumber size="small" min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 58 }} /></Space>} style={{ flexShrink: 0, marginBottom: 12 }} styles={{ body: { padding: 8 } }}>{operationBody}</Card><Card size="small" title="账号" extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}</Card></>} {layoutMode === 'split' ? <div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}><Card size="small" title="账号" extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>} style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}</Card><Card size="small" title="精英宝典操作" extra={<Space size={4}><Text type="secondary"></Text><InputNumber size="small" min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 58 }} /></Space>} style={{ width: 360, flexShrink: 0, minHeight: 0, overflow: 'hidden' }} styles={{ body: { padding: 8, overflowY: 'auto', height: 'calc(100% - 38px)' } }}>{operationBody}</Card></div> : <><Card size="small" title="精英宝典操作" extra={<Space size={4}><Text type="secondary"></Text><InputNumber size="small" min={1} max={10} value={concurrency} onChange={(value) => setConcurrency(value || 1)} style={{ width: 58 }} /></Space>} style={{ flexShrink: 0, marginBottom: 12 }} styles={{ body: { padding: 8 } }}>{operationBody}</Card><Card size="small" title="账号" extra={<Tag color="blue"> {selectedIds.length}/{accounts.length}</Tag>} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}>{accountsBody}</Card></>}
<Text type="secondary" style={{ fontSize: 12, marginTop: 6 }}> {exchangeCount} </Text> <Text type="secondary" style={{ fontSize: 12, marginTop: 6 }}> {exchangeCount} </Text>
{contextMenu && <div style={contextMenuStyle} onClick={(event) => event.stopPropagation()}><Card size="small" title={contextMenu.accountIds.length > 1 ? '批量动作' : '账号动作'} styles={{ body: { padding: 4 } }} style={{ width: 220, boxShadow: '0 4px 12px rgba(0,0,0,0.14)' }}><Space direction="vertical" style={{ width: '100%' }}>{HUYA_QUICK_ACTIONS.map((item) => <Button key={item.key} block size="small" type="text" icon={item.icon} onClick={() => runContextAction(item.key)} disabled={starting} style={{ justifyContent: 'flex-start' }}>{TASK_LABELS[item.key] || item.key}</Button>)}</Space></Card></div>}
<Modal open={qrTask !== null} title={qrTask?.task_type === 'create_recharge_order' ? '开通支付二维码' : '绑定二维码'} footer={null} onCancel={() => setQrTask(null)} centered>{qrTask && <div style={{ textAlign: 'center' }}>{resultText(qrTask, 'mini_qrcode_image') ? <img src={`data:image/png;base64,${resultText(qrTask, 'mini_qrcode_image')}`} alt="绑定二维码" style={{ width: 240, height: 240 }} /> : <QRCode value={resultText(qrTask, 'pay_url') || resultText(qrTask, 'bind_redirect_url') || 'https://zt.huya.com/b02faae1/pc/index.html'} size={240} />}<p><Tag color={TASK_COLORS[qrTask.status] || 'default'}>{qrTask.message || qrTask.status}</Tag></p>{qrTask.task_type === 'create_recharge_order' && <Text type="secondary"></Text>}</div>}</Modal> <Modal open={qrTask !== null} title={qrTask?.task_type === 'create_recharge_order' ? '开通支付二维码' : '绑定二维码'} footer={null} onCancel={() => setQrTask(null)} centered>{qrTask && <div style={{ textAlign: 'center' }}>{resultText(qrTask, 'mini_qrcode_image') ? <img src={`data:image/png;base64,${resultText(qrTask, 'mini_qrcode_image')}`} alt="绑定二维码" style={{ width: 240, height: 240 }} /> : <QRCode value={resultText(qrTask, 'pay_url') || resultText(qrTask, 'bind_redirect_url') || 'https://zt.huya.com/b02faae1/pc/index.html'} size={240} />}<p><Tag color={TASK_COLORS[qrTask.status] || 'default'}>{qrTask.message || qrTask.status}</Tag></p>{qrTask.task_type === 'create_recharge_order' && <Text type="secondary"></Text>}</div>}</Modal>
<Modal open={importOpen} title="导入精英宝典账号" onCancel={() => setImportOpen(false)} onOk={() => { void saveAccounts([...new Set([...accounts.map((item) => item.id), ...importSelected])]).then(() => { setImportSelected([]); setImportOpen(false); }).catch((error) => message.error(getErrorMessage(error))); }} okText="导入" cancelText="取消"><Input.Search allowClear placeholder="搜索账号" value={importSearch} onChange={(event) => setImportSearch(event.target.value)} style={{ marginBottom: 8 }} /><Select allowClear placeholder="标签" value={importTag || undefined} onChange={(value) => setImportTag(value || '')} options={importTags.map((tag) => ({ value: tag, label: tag }))} style={{ width: '100%', marginBottom: 8 }} /><Table rowKey="id" size="small" dataSource={pool.filter((item) => !accounts.some((current) => current.id === item.id) && (!importSearch || [item.uid, item.nickname, item.username].some((value) => String(value || '').includes(importSearch))) && (!importTag || item.tag === importTag))} columns={[{ title: '账号', render: (_value, row) => row.nickname || row.username || row.uid }, { title: '标签', dataIndex: 'tag' }]} pagination={{ pageSize: 8 }} rowSelection={{ selectedRowKeys: importSelected, onChange: (keys) => setImportSelected(keys.map(Number)) }} /></Modal> <Modal open={importOpen} title="导入精英宝典账号" onCancel={() => setImportOpen(false)} onOk={() => { void saveAccounts([...new Set([...accounts.map((item) => item.id), ...importSelected])]).then(() => { setImportSelected([]); setImportOpen(false); }).catch((error) => message.error(getErrorMessage(error))); }} okText="导入" cancelText="取消"><Input.Search allowClear placeholder="搜索账号" value={importSearch} onChange={(event) => setImportSearch(event.target.value)} style={{ marginBottom: 8 }} /><Select allowClear placeholder="标签" value={importTag || undefined} onChange={(value) => setImportTag(value || '')} options={importTags.map((tag) => ({ value: tag, label: tag }))} style={{ width: '100%', marginBottom: 8 }} /><Table rowKey="id" size="small" dataSource={pool.filter((item) => !accounts.some((current) => current.id === item.id) && (!importSearch || [item.uid, item.nickname, item.username].some((value) => String(value || '').includes(importSearch))) && (!importTag || item.tag === importTag))} columns={[{ title: '账号', render: (_value, row) => row.nickname || row.username || row.uid }, { title: '标签', dataIndex: 'tag' }]} pagination={{ pageSize: 8 }} rowSelection={{ selectedRowKeys: importSelected, onChange: (keys) => setImportSelected(keys.map(Number)) }} /></Modal>
<Modal open={configOpen} title="精英宝典设置" onCancel={() => setConfigOpen(false)} onOk={() => void saveConfig()} confirmLoading={configSaving} okText="保存" cancelText="取消" width={500}>{configLoading ? <div style={{ padding: 24, textAlign: 'center' }}>...</div> : configDraft && <Space direction="vertical" size={10} style={{ width: '100%' }}>{configField('sid', '活动 SID')}{configField('bind_act_id', '绑定活动 ID')}{configField('outer_act_id', '外部活动 ID')}{configField('room_pid', '直播间 PID')}{configField('pay_channel', '支付渠道')}<Space style={{ width: '100%', justifyContent: 'space-between' }}><Text></Text><InputNumber min={5} max={60} value={refreshInterval} onChange={(value) => { const next = Math.min(60, Math.max(5, value || 15)); setRefreshInterval(next); localStorage.setItem('huya_elite_refresh_interval', String(next)); }} style={{ width: 300 }} /></Space></Space>}</Modal> <Modal open={configOpen} title="精英宝典设置" onCancel={() => setConfigOpen(false)} onOk={() => void saveConfig()} confirmLoading={configSaving} okText="保存" cancelText="取消" width={500}>{configLoading ? <div style={{ padding: 24, textAlign: 'center' }}>...</div> : configDraft && <Space direction="vertical" size={10} style={{ width: '100%' }}>{configField('sid', '活动 SID')}{configField('bind_act_id', '绑定活动 ID')}{configField('outer_act_id', '外部活动 ID')}{configField('room_pid', '直播间 PID')}{configField('pay_channel', '支付渠道')}<Space style={{ width: '100%', justifyContent: 'space-between' }}><Text></Text><InputNumber min={5} max={60} value={refreshInterval} onChange={(value) => { const next = Math.min(60, Math.max(5, value || 15)); setRefreshInterval(next); localStorage.setItem('huya_elite_refresh_interval', String(next)); }} style={{ width: 300 }} /></Space></Space>}</Modal>