优化虎牙任务操作台逻辑

This commit is contained in:
yml2213
2026-07-06 15:38:04 +08:00
parent f9a321512f
commit 1f16193690
2 changed files with 314 additions and 177 deletions
+8
View File
@@ -12,3 +12,11 @@ body {
html[data-theme='dark'] body { html[data-theme='dark'] body {
color-scheme: dark; color-scheme: dark;
} }
.huya-task-account-table .ant-table-placeholder > .ant-table-cell {
border-bottom: 0;
}
.huya-task-account-table .ant-table-placeholder:hover > .ant-table-cell {
background: transparent;
}
+306 -177
View File
@@ -7,7 +7,7 @@ import type { Dayjs } from 'dayjs';
import type { MouseEvent as ReactMouseEvent } from 'react'; import type { MouseEvent as ReactMouseEvent } from 'react';
import { import {
AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined, AppstoreOutlined, CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined,
LinkOutlined, MoreOutlined, PlayCircleOutlined, QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, ImportOutlined, LinkOutlined, MoreOutlined, PlayCircleOutlined, QrcodeOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
huyaApi, huyaApi,
@@ -205,12 +205,6 @@ function goodsCategoryLabel(item: HuyaGoodsItem): string {
return goodsRawString(item, 'category_name') || goodsRawString(item, 'category_id') || '未分类'; return goodsRawString(item, 'category_name') || goodsRawString(item, 'category_id') || '未分类';
} }
function formatRemainText(value: string): string {
const text = String(value || '').trim();
if (!text || text.endsWith('%')) return text;
return /^-?\d+(\.\d+)?$/.test(text) ? `${text}%` : text;
}
function formatPriceText(value: number | null | undefined): string { function formatPriceText(value: number | null | undefined): string {
if (!value) return ''; if (!value) return '';
return `¥${(value / 100).toFixed(2)}`; return `¥${(value / 100).toFixed(2)}`;
@@ -241,6 +235,7 @@ function isPaymentFinished(task: HuyaTaskItem | null | undefined): boolean {
export default function HuyaTasksPage() { export default function HuyaTasksPage() {
const { token } = theme.useToken(); const { token } = theme.useToken();
const [form] = Form.useForm<HuyaConfig>(); const [form] = Form.useForm<HuyaConfig>();
const [accountPool, setAccountPool] = useState<HuyaAccountItem[]>([]);
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]); const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
const [tasks, setTasks] = useState<HuyaTaskItem[]>([]); const [tasks, setTasks] = useState<HuyaTaskItem[]>([]);
const [goods, setGoods] = useState<HuyaGoodsItem[]>([]); const [goods, setGoods] = useState<HuyaGoodsItem[]>([]);
@@ -252,6 +247,10 @@ export default function HuyaTasksPage() {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tagFilter, setTagFilter] = useState(''); const [tagFilter, setTagFilter] = useState('');
const [accountContextMenu, setAccountContextMenu] = useState<{ account: HuyaAccountItem; accountIds: number[]; x: number; y: number } | null>(null); const [accountContextMenu, setAccountContextMenu] = useState<{ account: HuyaAccountItem; accountIds: number[]; x: number; y: number } | null>(null);
const [importOpen, setImportOpen] = useState(false);
const [importSearchText, setImportSearchText] = useState('');
const [importTagFilter, setImportTagFilter] = useState('');
const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
const [selectedGoodsCategory, setSelectedGoodsCategory] = useState(''); const [selectedGoodsCategory, setSelectedGoodsCategory] = useState('');
const [selectedExchangeGoodsId, setSelectedExchangeGoodsId] = useState(''); const [selectedExchangeGoodsId, setSelectedExchangeGoodsId] = useState('');
const [exchangeAt, setExchangeAt] = useState<Dayjs | null>(null); const [exchangeAt, setExchangeAt] = useState<Dayjs | null>(null);
@@ -265,6 +264,7 @@ export default function HuyaTasksPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
const [savingConfig, setSavingConfig] = useState(false); const [savingConfig, setSavingConfig] = useState(false);
const [taskRecordsVisible, setTaskRecordsVisible] = useState(false);
const [batchId, setBatchId] = useState<string | null>(null); const [batchId, setBatchId] = useState<string | null>(null);
const [qrTask, setQrTask] = useState<HuyaTaskItem | null>(null); const [qrTask, setQrTask] = useState<HuyaTaskItem | null>(null);
const [payTask, setPayTask] = useState<HuyaTaskItem | null>(null); const [payTask, setPayTask] = useState<HuyaTaskItem | null>(null);
@@ -274,6 +274,8 @@ export default function HuyaTasksPage() {
const autoOpenedPayTaskIds = useRef<Set<number>>(new Set()); const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
const autoOpenPayReady = useRef(false); const autoOpenPayReady = useRef(false);
const notifiedPaidTaskIds = useRef<Set<number>>(new Set()); const notifiedPaidTaskIds = useRef<Set<number>>(new Set());
const accountTableAreaRef = useRef<HTMLDivElement | null>(null);
const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(460);
const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs(); const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs();
const { can } = usePermissions(); const { can } = usePermissions();
@@ -284,6 +286,25 @@ export default function HuyaTasksPage() {
localStorage.setItem('huya_task_concurrency', String(concurrency)); localStorage.setItem('huya_task_concurrency', String(concurrency));
}, [concurrency]); }, [concurrency]);
useEffect(() => {
const node = accountTableAreaRef.current;
if (!node) return;
const updateHeight = () => {
setAccountTableAreaHeight(Math.max(360, Math.floor(node.getBoundingClientRect().height)));
};
updateHeight();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', updateHeight);
return () => window.removeEventListener('resize', updateHeight);
}
const observer = new ResizeObserver(updateHeight);
observer.observe(node);
return () => observer.disconnect();
}, []);
const rememberExistingQrcodes = useCallback((items: HuyaTaskItem[]) => { const rememberExistingQrcodes = useCallback((items: HuyaTaskItem[]) => {
if (autoOpenQrReady.current) return; if (autoOpenQrReady.current) return;
items.filter(hasMiniQrcode).forEach((task) => autoOpenedQrTaskIds.current.add(task.id)); items.filter(hasMiniQrcode).forEach((task) => autoOpenedQrTaskIds.current.add(task.id));
@@ -319,7 +340,17 @@ export default function HuyaTasksPage() {
huyaApi.listTags(), huyaApi.listTags(),
]); ]);
if (accountResult.status === 'fulfilled') setAccounts(accountResult.value); if (accountResult.status === 'fulfilled') {
const nextPool = accountResult.value;
const nextPoolById = new Map(nextPool.map((account) => [account.id, account]));
setAccountPool(nextPool);
setAccounts((prev) => (
prev
.map((account) => nextPoolById.get(account.id))
.filter((account): account is HuyaAccountItem => Boolean(account))
));
setSelectedIds((prev) => prev.filter((id) => nextPoolById.has(id)));
}
if (taskResult.status === 'fulfilled') { if (taskResult.status === 'fulfilled') {
rememberExistingQrcodes(taskResult.value); rememberExistingQrcodes(taskResult.value);
rememberExistingPaymentQrcodes(taskResult.value); rememberExistingPaymentQrcodes(taskResult.value);
@@ -452,6 +483,27 @@ export default function HuyaTasksPage() {
}); });
}, [accounts, searchText, tagFilter]); }, [accounts, searchText, tagFilter]);
const importFilteredAccounts = useMemo(() => {
const importedIds = new Set(accounts.map((account) => account.id));
const keyword = importSearchText.trim().toLowerCase();
return accountPool.filter((account) => {
if (importedIds.has(account.id)) return false;
if (importTagFilter && account.tag !== importTagFilter) return false;
if (!keyword) return true;
return (
account.uid.toLowerCase().includes(keyword)
|| account.yyuid.toLowerCase().includes(keyword)
|| account.username.toLowerCase().includes(keyword)
|| account.nickname.toLowerCase().includes(keyword)
|| account.tag.toLowerCase().includes(keyword)
|| account.game_name.toLowerCase().includes(keyword)
|| account.game_channel.toLowerCase().includes(keyword)
|| account.game_phone.toLowerCase().includes(keyword)
|| account.cookie_preview.toLowerCase().includes(keyword)
);
});
}, [accountPool, accounts, importSearchText, importTagFilter]);
const latestTaskByAccount = useMemo(() => { const latestTaskByAccount = useMemo(() => {
const map = new Map<number, HuyaTaskItem>(); const map = new Map<number, HuyaTaskItem>();
tasks.forEach((task) => { tasks.forEach((task) => {
@@ -546,11 +598,6 @@ export default function HuyaTasksPage() {
} }
}, [goodsCategories, selectedGoodsCategory]); }, [goodsCategories, selectedGoodsCategory]);
const filteredGoods = useMemo(() => {
if (!selectedGoodsCategory) return sortedGoods;
return sortedGoods.filter((item) => goodsCategoryKey(item) === selectedGoodsCategory);
}, [sortedGoods, selectedGoodsCategory]);
const goodsOptions = useMemo(() => { const goodsOptions = useMemo(() => {
return sortedGoods.map((item) => ({ return sortedGoods.map((item) => ({
value: item.product_id, value: item.product_id,
@@ -637,6 +684,47 @@ export default function HuyaTasksPage() {
label: taskTypes[item.key] || item.key, label: taskTypes[item.key] || item.key,
})); }));
const importAccountsToWorkbench = (ids: number[]) => {
const accountById = new Map(accountPool.map((account) => [account.id, account]));
const uniqueIds = Array.from(new Set(ids)).filter((id) => accountById.has(id));
if (uniqueIds.length === 0) {
message.warning('请先选择要导入的账号');
return;
}
setAccounts((prev) => {
const prevIds = new Set(prev.map((account) => account.id));
const nextAccounts = [...prev];
uniqueIds.forEach((id) => {
if (!prevIds.has(id)) {
const account = accountById.get(id);
if (account) nextAccounts.push(account);
}
});
return nextAccounts;
});
setSelectedIds(uniqueIds);
setImportSelectedIds([]);
setImportOpen(false);
message.success(`已导入 ${uniqueIds.length} 个账号到操作台`);
};
const removeSelectedAccounts = () => {
if (selectedIds.length === 0) {
message.warning('请先选择要移出的账号');
return;
}
const selected = new Set(selectedIds);
setAccounts((prev) => prev.filter((account) => !selected.has(account.id)));
setSelectedIds([]);
setAccountContextMenu(null);
};
const clearWorkbenchAccounts = () => {
setAccounts([]);
setSelectedIds([]);
setAccountContextMenu(null);
};
const runSingleAccountAction = (taskType: string, account: HuyaAccountItem) => { const runSingleAccountAction = (taskType: string, account: HuyaAccountItem) => {
setSelectedIds([account.id]); setSelectedIds([account.id]);
setAccountContextMenu(null); setAccountContextMenu(null);
@@ -955,6 +1043,48 @@ export default function HuyaTasksPage() {
}, },
]; ];
const importAccountColumns: TableProps<HuyaAccountItem>['columns'] = [
{ title: 'ID', dataIndex: 'id', width: 76, align: 'center' },
{
title: '虎牙 CK',
width: 230,
render: (_: unknown, record) => (
<Space direction="vertical" size={1}>
<Space size={6} wrap>
<Text strong>{accountDisplayName(record)}</Text>
{record.tag ? <Tag color={tagColorMap[record.tag]}>{record.tag}</Tag> : null}
</Space>
<Text type="secondary" style={{ fontSize: 12 }}>UID {record.uid || record.yyuid || '-'}</Text>
</Space>
),
},
{
title: '手机',
dataIndex: 'game_phone',
width: 130,
ellipsis: true,
render: (value: string) => value || <Text type="secondary">-</Text>,
},
{
title: '游戏名',
dataIndex: 'game_name',
ellipsis: true,
render: (value: string, record) => value ? (
<Space direction="vertical" size={0}>
<Text>{value}</Text>
{record.game_channel ? <Text type="secondary" style={{ fontSize: 12 }}>{record.game_channel}</Text> : null}
</Space>
) : <Text type="secondary"></Text>,
},
{
title: '积分',
dataIndex: 'points',
width: 86,
align: 'center',
render: (points: number | null) => points ?? <Text type="secondary"></Text>,
},
];
const taskColumns: TableProps<HuyaTaskItem>['columns'] = [ const taskColumns: TableProps<HuyaTaskItem>['columns'] = [
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' }, { title: 'ID', dataIndex: 'id', width: 70, align: 'center' },
{ {
@@ -993,88 +1123,6 @@ export default function HuyaTasksPage() {
}, },
]; ];
const goodsColumns: TableProps<HuyaGoodsItem>['columns'] = [
{ title: '商品ID', dataIndex: 'product_id', width: 88, ellipsis: true },
{
title: '名称',
dataIndex: 'name',
width: 230,
render: (value: string) => (
<Text
title={value}
style={{
display: 'block',
lineHeight: '20px',
whiteSpace: 'normal',
wordBreak: 'break-all',
}}
>
{value || '-'}
</Text>
),
},
{
title: '分类',
width: 96,
render: (_: unknown, record) => {
const label = goodsCategoryLabel(record);
return label ? <Tag>{label}</Tag> : <Text type="secondary">-</Text>;
},
},
{
title: '价格',
dataIndex: 'price',
width: 78,
align: 'center',
render: (value: number | null) => value ?? <Text type="secondary">-</Text>,
},
{
title: '库存',
dataIndex: 'remain_text',
width: 72,
render: (value: string) => formatRemainText(value) || <Text type="secondary">-</Text>,
},
{
title: '更新时间',
dataIndex: 'updated_at',
width: 154,
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
},
];
const rechargeGoodsColumns: TableProps<HuyaRechargeGoodsItem>['columns'] = [
{ title: 'SPU', dataIndex: 'spu_id', width: 120, ellipsis: true },
{ title: 'SKU', dataIndex: 'sku_id', width: 100, ellipsis: true },
{ title: '名称', dataIndex: 'name', ellipsis: true },
{
title: '单价',
dataIndex: 'price',
width: 90,
align: 'center',
render: (value: number | null) => formatPriceText(value) || <Text type="secondary">-</Text>,
},
{
title: '库存',
dataIndex: 'stock',
width: 90,
align: 'center',
render: (value: number | null) => value ?? <Text type="secondary">-</Text>,
},
{
title: '来源',
dataIndex: 'task_name',
width: 140,
ellipsis: true,
render: (value: string) => value || <Text type="secondary"></Text>,
},
{
title: '更新时间',
dataIndex: 'updated_at',
width: 160,
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
},
];
const exchangeRecordColumns: TableProps<Record<string, unknown>>['columns'] = [ const exchangeRecordColumns: TableProps<Record<string, unknown>>['columns'] = [
{ {
title: '序号', title: '序号',
@@ -1118,15 +1166,6 @@ export default function HuyaTasksPage() {
}, },
]; ];
const taskSummary = (
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 8, color: token.colorTextSecondary, flexWrap: 'wrap' }}>
<span> <b>{tasks.length}</b> </span>
<span> <b>{plannedCount}</b></span>
<span> <b style={{ color: token.colorSuccess }}>{successCount}</b></span>
<span> <b style={{ color: token.colorError }}>{failedCount}</b></span>
</div>
);
const renderTaskTable = (pageSize = 12) => ( const renderTaskTable = (pageSize = 12) => (
<Table <Table
columns={taskColumns} columns={taskColumns}
@@ -1142,13 +1181,15 @@ export default function HuyaTasksPage() {
const accountContextMenuPosition = accountContextMenu const accountContextMenuPosition = accountContextMenu
? contextMenuPosition(accountContextMenu.x, accountContextMenu.y) ? contextMenuPosition(accountContextMenu.x, accountContextMenu.y)
: null; : null;
const accountTableBodyHeight = Math.max(320, accountTableAreaHeight - 42);
return ( return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ flexShrink: 0, marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}> <div style={{ flexShrink: 0, marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
<h2 style={{ margin: 0 }}></h2> <h2 style={{ margin: 0 }}></h2>
<Space wrap> <Space wrap>
<Tag> {accounts.length}</Tag> <Tag> {accounts.length}</Tag>
<Tag> {accountPool.length}</Tag>
<Tag color="blue"> {selectedIds.length}</Tag> <Tag color="blue"> {selectedIds.length}</Tag>
<Tag color="green"> {accounts.filter((item) => item.points !== null && item.points !== undefined).length}</Tag> <Tag color="green"> {accounts.filter((item) => item.points !== null && item.points !== undefined).length}</Tag>
<Tag color="cyan"> {accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length}</Tag> <Tag color="cyan"> {accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length}</Tag>
@@ -1216,20 +1257,28 @@ export default function HuyaTasksPage() {
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<div style={{ minWidth: 0, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', paddingRight: 2 }}> <div style={{ minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', paddingRight: 2 }}>
<Card <Card
size="small" size="small"
title="账号表格" title="账号表格"
extra={( extra={(
<Space size={6} wrap> <Space size={6} wrap>
<Button size="small" type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
</Button>
<Button size="small" onClick={() => setSelectedIds(filteredAccounts.map((item) => item.id))}> <Button size="small" onClick={() => setSelectedIds(filteredAccounts.map((item) => item.id))}>
</Button> </Button>
<Button size="small" onClick={() => setSelectedIds([])}> <Button size="small" disabled={selectedIds.length === 0} onClick={removeSelectedAccounts}>
</Button>
<Button size="small" danger disabled={accounts.length === 0} onClick={clearWorkbenchAccounts}>
</Button> </Button>
</Space> </Space>
)} )}
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}
styles={{ body: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } }}
> >
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10, alignItems: 'center' }}>
<Input.Search <Input.Search
@@ -1250,78 +1299,76 @@ export default function HuyaTasksPage() {
/> />
<Text type="secondary">{filteredAccounts.length} / {accounts.length}</Text> <Text type="secondary">{filteredAccounts.length} / {accounts.length}</Text>
</div> </div>
<Table <div ref={accountTableAreaRef} style={{ flex: 1, minHeight: 360, overflow: 'hidden' }}>
rowSelection={{ <Table
selectedRowKeys: selectedIds, rowSelection={{
onChange: (keys) => setSelectedIds(keys.map((key) => Number(key))), selectedRowKeys: selectedIds,
}} onChange: (keys) => setSelectedIds(keys.map((key) => Number(key))),
columns={accountColumns} }}
dataSource={filteredAccounts} columns={accountColumns}
rowKey="id" dataSource={filteredAccounts}
loading={loading} rowKey="id"
size="small" loading={loading}
pagination={{ pageSize: 12, showSizeChanger: true, showTotal: (total) => `${total}` }} size="small"
scroll={{ x: 1520 }} className="huya-task-account-table"
rowClassName={(record) => selectedIds.includes(record.id) ? 'ant-table-row-selected' : ''} pagination={false}
onRow={(record) => ({ scroll={filteredAccounts.length > 0 ? { x: 1520, y: accountTableBodyHeight } : undefined}
onClick: (event) => handleAccountRowClick(record, event), locale={{
onContextMenu: (event) => handleAccountRowContextMenu(record, event), emptyText: (
style: { cursor: 'pointer' }, <div
title: '点击选中,右键打开账号动作', style={{
})} minHeight: accountTableBodyHeight,
/> display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: token.colorTextTertiary,
}}
>
</div>
),
}}
rowClassName={(record) => selectedIds.includes(record.id) ? 'ant-table-row-selected' : ''}
onRow={(record) => ({
onClick: (event) => handleAccountRowClick(record, event),
onContextMenu: (event) => handleAccountRowContextMenu(record, event),
style: { cursor: 'pointer' },
title: '点击选中,右键打开账号动作',
})}
/>
</div>
</Card> </Card>
<Row gutter={12} style={{ marginTop: 12 }}> <Card
<Col xs={24} xl={12}> size="small"
<Card size="small" title={<Space><ShoppingOutlined /></Space>}> title={<Space><FieldTimeOutlined /></Space>}
<Table extra={(
columns={goodsColumns} <Space size={10} wrap>
dataSource={filteredGoods} <Text type="secondary"> {tasks.length}</Text>
rowKey="id" <Text type="secondary"> {plannedCount}</Text>
size="small" <Text style={{ color: token.colorSuccess }}> {successCount}</Text>
pagination={false} <Text style={{ color: token.colorError }}> {failedCount}</Text>
scroll={{ y: 260 }} <Button size="small" onClick={() => setTaskRecordsVisible((value) => !value)}>
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }} {taskRecordsVisible ? '收起' : '展开'}
onRow={(record) => ({ </Button>
onClick: () => setSelectedExchangeGoodsId(record.product_id), </Space>
})} )}
rowClassName={(record) => record.product_id === selectedExchangeGoodsId ? 'ant-table-row-selected' : ''} style={{ marginTop: 12 }}
/> styles={{ body: { padding: taskRecordsVisible ? 12 : 0 } }}
</Card> >
</Col> {taskRecordsVisible ? renderTaskTable(5) : null}
<Col xs={24} xl={12}>
<Card size="small" title={<Space><CreditCardOutlined /></Space>}>
<Table
columns={rechargeGoodsColumns}
dataSource={sortedRechargeGoods}
rowKey="id"
size="small"
pagination={false}
scroll={{ y: 260 }}
locale={{ emptyText: '暂无充值商品,请先刷新充值商品列表' }}
onRow={(record) => ({
onClick: () => setSelectedRechargeGoodsId(record.spu_id),
})}
rowClassName={(record) => record.spu_id === selectedRechargeGoodsId ? 'ant-table-row-selected' : ''}
/>
</Card>
</Col>
</Row>
<Card size="small" title={<Space><FieldTimeOutlined /></Space>} style={{ marginTop: 12 }}>
{taskSummary}
{renderTaskTable(8)}
<RealtimeLogPanel
logs={logs}
connected={wsConnected}
title="虎牙实时日志"
emptyText="暂无虎牙任务日志"
collapsible
spinWhenEmpty
style={{ marginTop: 8 }}
/>
</Card> </Card>
<RealtimeLogPanel
logs={logs}
connected={wsConnected}
title="虎牙实时日志"
emptyText="暂无虎牙任务日志"
height={120}
collapsible
defaultVisible={false}
spinWhenEmpty
style={{ marginTop: 8 }}
/>
</div> </div>
<div style={{ minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }}> <div style={{ minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }}>
@@ -1529,6 +1576,88 @@ export default function HuyaTasksPage() {
</div> </div>
</div> </div>
<Modal
title="导入账号到操作台"
open={importOpen}
onCancel={() => {
setImportOpen(false);
setImportSelectedIds([]);
}}
footer={[
<Button key="cancel" onClick={() => {
setImportOpen(false);
setImportSelectedIds([]);
}}>
</Button>,
<Button
key="all"
disabled={importFilteredAccounts.length === 0}
onClick={() => importAccountsToWorkbench(importFilteredAccounts.map((account) => account.id))}
>
</Button>,
<Button
key="ok"
type="primary"
disabled={importSelectedIds.length === 0}
onClick={() => importAccountsToWorkbench(importSelectedIds)}
>
</Button>,
]}
width={860}
>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<Input.Search
allowClear
placeholder="搜索 UID、昵称、标签、游戏名、手机号、CK"
value={importSearchText}
onChange={(event) => setImportSearchText(event.target.value)}
style={{ width: 330 }}
prefix={<SearchOutlined />}
/>
<Select
allowClear
placeholder="按标签筛选"
value={importTagFilter || undefined}
onChange={(value) => setImportTagFilter(value || '')}
options={tags.map((tag) => ({ value: tag, label: tag }))}
style={{ width: 150 }}
/>
<Text type="secondary">
{importFilteredAccounts.length} / {accountPool.length}
</Text>
</div>
<Table
rowSelection={{
selectedRowKeys: importSelectedIds,
onChange: (keys) => setImportSelectedIds(keys.map((key) => Number(key))),
}}
columns={importAccountColumns}
dataSource={importFilteredAccounts}
rowKey="id"
size="small"
pagination={{ pageSize: 8, showSizeChanger: false, showTotal: (total) => `${total}` }}
scroll={{ x: 720, y: 360 }}
locale={{ emptyText: '没有可导入账号' }}
rowClassName={(record) => importSelectedIds.includes(record.id) ? 'ant-table-row-selected' : ''}
onRow={(record) => ({
onClick: (event) => {
if (isInteractiveTarget(event.target)) return;
setImportSelectedIds((prev) => (
prev.includes(record.id)
? prev.filter((id) => id !== record.id)
: [...prev, record.id]
));
},
style: { cursor: 'pointer' },
})}
/>
</Space>
</Modal>
<Modal <Modal
title="兑换记录" title="兑换记录"
open={!!exchangeRecordsTask} open={!!exchangeRecordsTask}