重构 DouyuTasksPage 布局:删除任务记录区,操作工具栏移至账号表格上方,账号表格全宽
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
|||||||
} from '../api/modules';
|
} from '../api/modules';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
|
||||||
import { formatTime } from '../utils/time';
|
|
||||||
import { getErrorMessage } from '../utils/error';
|
import { getErrorMessage } from '../utils/error';
|
||||||
import { message } from '../utils/antdMessage';
|
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_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
|
||||||
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
|
||||||
planned: 'default', pending: 'default', running: 'processing',
|
|
||||||
success: 'success', failed: 'error', error: 'error', stopped: 'warning',
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
|
||||||
planned: '已计划', pending: '等待中', running: '执行中',
|
|
||||||
success: '成功', failed: '失败', error: '异常', stopped: '已停止',
|
|
||||||
};
|
|
||||||
|
|
||||||
function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
|
function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
|
||||||
const value = result?.[key];
|
const value = result?.[key];
|
||||||
return typeof value === 'string' ? value : '';
|
return typeof value === 'string' ? value : '';
|
||||||
@@ -112,8 +101,17 @@ function resultFlag(result: Record<string, unknown> | null | undefined, key: str
|
|||||||
return text === '1' || text === 'true' || text === 'yes';
|
return text === '1' || text === 'true' || text === 'yes';
|
||||||
}
|
}
|
||||||
|
|
||||||
function taskPayUrl(task: DouyuTaskItem | null | undefined): string {
|
// 提取任务结果中的二维码 URL:绑定/电竞绑定用 url,支付类用 pay_url
|
||||||
return resultText(task?.result, 'pay_url') || resultText(task?.result, '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 {
|
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 [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
|
||||||
const [selectedGoodsId, setSelectedGoodsId] = useState('');
|
const [selectedGoodsId, setSelectedGoodsId] = useState('');
|
||||||
const [accountSearch, setAccountSearch] = useState('');
|
const [accountSearch, setAccountSearch] = useState('');
|
||||||
const taskRecordAreaRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const [taskRecordAreaHeight, setTaskRecordAreaHeight] = useState(360);
|
|
||||||
const accountTableAreaRef = useRef<HTMLDivElement | null>(null);
|
const accountTableAreaRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(360);
|
const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(360);
|
||||||
|
const [accountPageSize, setAccountPageSize] = useState<number>(() => {
|
||||||
|
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 [importOpen, setImportOpen] = useState(false);
|
||||||
const [importPool, setImportPool] = useState<DouyuTaskAccountItem[]>([]);
|
const [importPool, setImportPool] = useState<DouyuTaskAccountItem[]>([]);
|
||||||
@@ -408,29 +408,18 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
|
|
||||||
useEffect(() => { loadData(); }, [loadData]);
|
useEffect(() => { loadData(); }, [loadData]);
|
||||||
|
|
||||||
// 监听任务记录/账号表格区域高度变化,动态计算 scroll.y 实现表体内部滚动
|
// 监听账号表格区域高度变化,动态计算 scroll.y 实现表体内部滚动
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateNode = (
|
const node = accountTableAreaRef.current;
|
||||||
node: HTMLDivElement | null,
|
if (!node) return;
|
||||||
setter: (v: number) => void,
|
const update = () => setAccountTableAreaHeight(Math.max(200, Math.floor(node.getBoundingClientRect().height)));
|
||||||
) => {
|
update();
|
||||||
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();
|
|
||||||
if (typeof ResizeObserver === 'undefined') {
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
window.addEventListener('resize', updateHeights);
|
window.addEventListener('resize', update);
|
||||||
return () => window.removeEventListener('resize', updateHeights);
|
return () => window.removeEventListener('resize', update);
|
||||||
}
|
}
|
||||||
const observer = new ResizeObserver(updateHeights);
|
const observer = new ResizeObserver(update);
|
||||||
if (taskNode) observer.observe(taskNode);
|
observer.observe(node);
|
||||||
if (accountNode) observer.observe(accountNode);
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -974,157 +963,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '最近结果', width: 200,
|
title: '二维码', width: 76, align: 'center',
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const task = latestTaskByAccount.get(record.id);
|
const task = latestTaskByAccount.get(record.id);
|
||||||
if (!task) return <Text type="secondary">无</Text>;
|
const url = taskQrUrl(task);
|
||||||
const typeLabel = taskTypes[task.task_type] || task.task_type;
|
if (!url) return null;
|
||||||
// 是否有可打开的二维码/弹窗
|
|
||||||
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 openTask = () => {
|
const openTask = () => {
|
||||||
|
if (!task) return;
|
||||||
if (hasBindQrcode(task)) openQrTask(task);
|
if (hasBindQrcode(task)) openQrTask(task);
|
||||||
else if (hasPaymentQrcode(task)) openPayTask(task);
|
else if (hasPaymentQrcode(task)) openPayTask(task);
|
||||||
else openEsportsBindTask(task);
|
else openEsportsBindTask(task);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
<div onClick={openTask} style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0 }} title="点击查看大图">
|
||||||
<Space size={4} wrap>
|
<QRCode value={url} size={56} />
|
||||||
<Tag color={STATUS_COLORS[task.status] || 'default'} style={{ margin: 0 }}>{typeLabel}</Tag>
|
</div>
|
||||||
{canOpen && (
|
|
||||||
<Button
|
|
||||||
size="small" type="link" icon={<QrcodeOutlined />}
|
|
||||||
onClick={openTask}
|
|
||||||
style={{ padding: 0, height: 20, fontSize: 12 }}
|
|
||||||
>
|
|
||||||
查看
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
<Text
|
|
||||||
type="secondary" style={{ fontSize: 12 }}
|
|
||||||
ellipsis={{ tooltip: task.message || '' }}
|
|
||||||
>
|
|
||||||
{task.message || STATUS_LABELS[task.status] || task.status}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 任务记录表
|
|
||||||
const taskColumns: TableProps<DouyuTaskItem>['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 (
|
|
||||||
<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');
|
|
||||||
const ready = bindReadyForConfirm(r);
|
|
||||||
const polling = r.status === 'running' && r.result?.bind_polling === true;
|
|
||||||
const bindUrl = resultText(r.result, 'url');
|
|
||||||
return (
|
|
||||||
<Space wrap size={4}>
|
|
||||||
<Text>{r.message || '-'}</Text>
|
|
||||||
{ready ? <Tag color="gold">{roleName || '待确认'}</Tag> : null}
|
|
||||||
{!ready && (polling || phase) ? (
|
|
||||||
<Tag color="processing">{bindPhaseText(phase, polling)}</Tag>
|
|
||||||
) : null}
|
|
||||||
{bindUrl && (
|
|
||||||
<Button size="small" icon={<QrcodeOutlined />} onClick={() => openQrTask(r)}>
|
|
||||||
查看
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 (
|
|
||||||
<Space wrap size={4}>
|
|
||||||
<Text>{r.message || '-'}</Text>
|
|
||||||
{roleName ? <Tag color="blue">{roleName}</Tag> : null}
|
|
||||||
{(areaName || platName) ? (
|
|
||||||
<Text type="secondary">{[areaName, platName].filter(Boolean).join(' / ')}</Text>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (r.task_type === 'confirm_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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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}>
|
|
||||||
<Text>{r.message || '-'}</Text>
|
|
||||||
{url && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<QrcodeOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
if (hasPaymentQrcode(r)) openPayTask(r);
|
|
||||||
else if (hasBindQrcode(r)) openQrTask(r);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
查看
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ title: '任务', dataIndex: 'task_type', width: 170, render: (v) => taskTypes[v] || v },
|
|
||||||
{
|
|
||||||
title: '状态', dataIndex: 'status', width: 80,
|
|
||||||
render: (v) => <Tag color={STATUS_COLORS[v]}>{STATUS_LABELS[v] || v}</Tag>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '时间', dataIndex: 'finished_at', width: 170,
|
|
||||||
render: (_, r) => formatTime(r.finished_at || r.created_at),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filteredAccounts = useMemo(() => {
|
const filteredAccounts = useMemo(() => {
|
||||||
if (!accountSearch.trim()) return accounts;
|
if (!accountSearch.trim()) return accounts;
|
||||||
const kw = accountSearch.toLowerCase();
|
const kw = accountSearch.toLowerCase();
|
||||||
@@ -1268,258 +1126,231 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{/* 主布局 */}
|
{/* 操作工具栏 */}
|
||||||
<div style={{
|
<Card
|
||||||
flex: 1, minHeight: 0,
|
size="small"
|
||||||
display: 'grid',
|
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
|
||||||
gridTemplateColumns: 'minmax(0, 2fr) minmax(0, 1fr)',
|
extra={
|
||||||
gap: 12, overflow: 'hidden',
|
<Space>
|
||||||
}}>
|
<Tag color={selectedIds.length ? 'blue' : 'default'}>{selectedText}</Tag>
|
||||||
{/* 左侧账号 */}
|
<Space.Compact>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
|
<Button size="small" disabled>并发</Button>
|
||||||
<Card
|
<InputNumber
|
||||||
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, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }}
|
|
||||||
>
|
|
||||||
<Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap>
|
|
||||||
<Space>
|
|
||||||
<Input
|
|
||||||
size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
|
|
||||||
value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)}
|
|
||||||
style={{ width: 200 }} allowClear
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>
|
|
||||||
{selectedIds.length > 0 && (
|
|
||||||
<Button size="small" danger onClick={removeSelected}>移出选中</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
<div ref={accountTableAreaRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Table
|
|
||||||
rowKey="id"
|
|
||||||
size="small"
|
size="small"
|
||||||
className="douyu-task-record-table"
|
min={1}
|
||||||
loading={loading}
|
max={10}
|
||||||
rowSelection={{
|
value={concurrency}
|
||||||
selectedRowKeys: selectedIds,
|
onChange={(v) => setConcurrency(v || 1)}
|
||||||
onChange: (keys) => setSelectedIds(keys.map(Number)),
|
style={{ width: 70 }}
|
||||||
}}
|
|
||||||
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: '右键打开账号动作',
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
style={{ flexShrink: 0, marginBottom: 12 }}
|
||||||
|
styles={{ body: { padding: 8 } }}
|
||||||
|
>
|
||||||
|
{isEsportsHandbook ? (
|
||||||
|
<div style={compactOperationGridStyle}>
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<div style={sectionTitleStyle}>
|
||||||
|
<CreditCardOutlined />
|
||||||
|
<span>开通手册</span>
|
||||||
|
</div>
|
||||||
|
{renderActionButton('create_esports_qr', 'primary')}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div style={sectionStyle}>
|
||||||
</div>
|
<div style={sectionTitleStyle}>
|
||||||
|
<ShoppingOutlined />
|
||||||
{/* 右侧:任务记录(弹性高度) + 操作区(自适应) */}
|
<span>皮肤兑换</span>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
|
</div>
|
||||||
<Card
|
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||||
size="small" title="任务记录"
|
<Select
|
||||||
extra={
|
size="small"
|
||||||
<Space size={12}>
|
value={selectedGoodsId || undefined}
|
||||||
<Tag color="success">成功 {visibleTasks.filter((t) => t.status === 'success').length}</Tag>
|
onChange={setSelectedGoodsId}
|
||||||
<Tag color="error">失败 {visibleTasks.filter((t) => ['failed', 'error'].includes(t.status)).length}</Tag>
|
options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
|
||||||
|
placeholder="选择电竞皮肤"
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
<div style={actionGridStyle}>
|
||||||
|
{renderActionButton('refresh_esports_goods')}
|
||||||
|
{renderActionButton('exchange_esports_goods', 'primary')}
|
||||||
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
|
||||||
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' } }}
|
|
||||||
>
|
|
||||||
<div ref={taskRecordAreaRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Table
|
|
||||||
rowKey="id" size="small" columns={taskColumns} dataSource={visibleTasks}
|
|
||||||
className="douyu-task-record-table"
|
|
||||||
loading={loading}
|
|
||||||
pagination={{
|
|
||||||
pageSize: 15,
|
|
||||||
showSizeChanger: false,
|
|
||||||
size: 'small',
|
|
||||||
showLessItems: true,
|
|
||||||
showTotal: (total) => `共 ${total} 条`,
|
|
||||||
}}
|
|
||||||
tableLayout="fixed"
|
|
||||||
scroll={{ x: 850, y: Math.max(120, taskRecordAreaHeight - 80) }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div style={sectionStyle}>
|
||||||
|
<div style={sectionTitleStyle}>
|
||||||
<Card
|
<CreditCardOutlined />
|
||||||
size="small"
|
<span>充值与送礼</span>
|
||||||
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
|
</div>
|
||||||
extra={<Tag color={selectedIds.length ? 'blue' : 'default'}>{selectedText}</Tag>}
|
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||||
style={{ flexShrink: 0, display: 'flex', flexDirection: 'column' }}
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
styles={{ body: { padding: 8, maxHeight: 260, overflow: 'auto' } }}
|
<InputNumber
|
||||||
>
|
size="small"
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
min={1}
|
||||||
<Space.Compact style={{ width: '100%' }}>
|
value={goldAmount}
|
||||||
<Button size="small" disabled>并发</Button>
|
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>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
size="small"
|
size="small"
|
||||||
min={1}
|
min={1}
|
||||||
max={10}
|
value={giftCount}
|
||||||
value={concurrency}
|
onChange={(v) => setGiftCount(v || 1)}
|
||||||
onChange={(v) => setConcurrency(v || 1)}
|
addonAfter="赠送数量"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
<div style={actionGridStyle}>
|
||||||
|
{renderActionButton('donate_esports_chicken_gift', 'primary')}
|
||||||
{isEsportsHandbook ? (
|
{renderActionButton('donate_esports_firework_gift', 'primary')}
|
||||||
<div style={compactOperationGridStyle}>
|
|
||||||
<div style={sectionStyle}>
|
|
||||||
<div style={sectionTitleStyle}>
|
|
||||||
<CreditCardOutlined />
|
|
||||||
<span>开通手册</span>
|
|
||||||
</div>
|
|
||||||
{renderActionButton('create_esports_qr', 'primary')}
|
|
||||||
</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_esports_goods')}
|
|
||||||
{renderActionButton('exchange_esports_goods', 'primary')}
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
<div style={sectionStyle}>
|
|
||||||
<div style={sectionTitleStyle}>
|
|
||||||
<CreditCardOutlined />
|
|
||||||
<span>充值与送礼</span>
|
|
||||||
</div>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
|
||||||
<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>
|
|
||||||
<InputNumber
|
|
||||||
size="small"
|
|
||||||
min={1}
|
|
||||||
value={giftCount}
|
|
||||||
onChange={(v) => setGiftCount(v || 1)}
|
|
||||||
addonAfter="赠送数量"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
<div style={actionGridStyle}>
|
|
||||||
{renderActionButton('donate_esports_chicken_gift', 'primary')}
|
|
||||||
{renderActionButton('donate_esports_firework_gift', 'primary')}
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</Space>
|
||||||
<div style={compactOperationGridStyle}>
|
</div>
|
||||||
<div style={sectionStyle}>
|
</div>
|
||||||
<div style={sectionTitleStyle}>
|
) : (
|
||||||
<ShoppingOutlined />
|
<div style={compactOperationGridStyle}>
|
||||||
<span>兑换</span>
|
<div style={sectionStyle}>
|
||||||
</div>
|
<div style={sectionTitleStyle}>
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
<ShoppingOutlined />
|
||||||
<Select
|
<span>兑换</span>
|
||||||
size="small"
|
</div>
|
||||||
value={selectedGoodsId || undefined}
|
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||||
onChange={setSelectedGoodsId}
|
<Select
|
||||||
options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
|
size="small"
|
||||||
placeholder="选择兑换商品"
|
value={selectedGoodsId || undefined}
|
||||||
showSearch
|
onChange={setSelectedGoodsId}
|
||||||
optionFilterProp="label"
|
options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
|
||||||
style={{ width: '100%' }}
|
placeholder="选择兑换商品"
|
||||||
/>
|
showSearch
|
||||||
<div style={actionGridStyle}>
|
optionFilterProp="label"
|
||||||
{renderActionButton('refresh_goods')}
|
style={{ width: '100%' }}
|
||||||
{renderActionButton('exchange_goods', 'primary')}
|
/>
|
||||||
</div>
|
<div style={actionGridStyle}>
|
||||||
</Space>
|
{renderActionButton('refresh_goods')}
|
||||||
</div>
|
{renderActionButton('exchange_goods', 'primary')}
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Space>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
</div>
|
||||||
勾选账号后可批量执行;账号行右键可执行绑定与查询动作。
|
|
||||||
</Text>
|
<div style={sectionStyle}>
|
||||||
</Space>
|
<div style={sectionTitleStyle}>
|
||||||
</Card>
|
<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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
勾选账号后可批量执行;账号行右键可执行绑定与查询动作。
|
||||||
|
</Text>
|
||||||
|
</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' } }}
|
||||||
|
>
|
||||||
|
<Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap>
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
|
||||||
|
value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)}
|
||||||
|
style={{ width: 200 }} allowClear
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>
|
||||||
|
{selectedIds.length > 0 && (
|
||||||
|
<Button size="small" danger onClick={removeSelected}>移出选中</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
<div ref={accountTableAreaRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
className="douyu-task-record-table"
|
||||||
|
loading={loading}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys: selectedIds,
|
||||||
|
onChange: (keys) => 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: '右键打开账号动作',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
{/* 右键菜单 */}
|
{/* 右键菜单 */}
|
||||||
{contextMenu && (
|
{contextMenu && (
|
||||||
|
|||||||
Reference in New Issue
Block a user