优化虎牙任务操作台逻辑
This commit is contained in:
@@ -12,3 +12,11 @@ body {
|
||||
html[data-theme='dark'] body {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Dayjs } from 'dayjs';
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import {
|
||||
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';
|
||||
import {
|
||||
huyaApi,
|
||||
@@ -205,12 +205,6 @@ function goodsCategoryLabel(item: HuyaGoodsItem): string {
|
||||
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 {
|
||||
if (!value) return '';
|
||||
return `¥${(value / 100).toFixed(2)}`;
|
||||
@@ -241,6 +235,7 @@ function isPaymentFinished(task: HuyaTaskItem | null | undefined): boolean {
|
||||
export default function HuyaTasksPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [form] = Form.useForm<HuyaConfig>();
|
||||
const [accountPool, setAccountPool] = useState<HuyaAccountItem[]>([]);
|
||||
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
||||
const [tasks, setTasks] = useState<HuyaTaskItem[]>([]);
|
||||
const [goods, setGoods] = useState<HuyaGoodsItem[]>([]);
|
||||
@@ -252,6 +247,10 @@ export default function HuyaTasksPage() {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [tagFilter, setTagFilter] = useState('');
|
||||
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 [selectedExchangeGoodsId, setSelectedExchangeGoodsId] = useState('');
|
||||
const [exchangeAt, setExchangeAt] = useState<Dayjs | null>(null);
|
||||
@@ -265,6 +264,7 @@ export default function HuyaTasksPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [savingConfig, setSavingConfig] = useState(false);
|
||||
const [taskRecordsVisible, setTaskRecordsVisible] = useState(false);
|
||||
const [batchId, setBatchId] = useState<string | null>(null);
|
||||
const [qrTask, setQrTask] = 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 autoOpenPayReady = useRef(false);
|
||||
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 { can } = usePermissions();
|
||||
|
||||
@@ -284,6 +286,25 @@ export default function HuyaTasksPage() {
|
||||
localStorage.setItem('huya_task_concurrency', String(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[]) => {
|
||||
if (autoOpenQrReady.current) return;
|
||||
items.filter(hasMiniQrcode).forEach((task) => autoOpenedQrTaskIds.current.add(task.id));
|
||||
@@ -319,7 +340,17 @@ export default function HuyaTasksPage() {
|
||||
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') {
|
||||
rememberExistingQrcodes(taskResult.value);
|
||||
rememberExistingPaymentQrcodes(taskResult.value);
|
||||
@@ -452,6 +483,27 @@ export default function HuyaTasksPage() {
|
||||
});
|
||||
}, [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 map = new Map<number, HuyaTaskItem>();
|
||||
tasks.forEach((task) => {
|
||||
@@ -546,11 +598,6 @@ export default function HuyaTasksPage() {
|
||||
}
|
||||
}, [goodsCategories, selectedGoodsCategory]);
|
||||
|
||||
const filteredGoods = useMemo(() => {
|
||||
if (!selectedGoodsCategory) return sortedGoods;
|
||||
return sortedGoods.filter((item) => goodsCategoryKey(item) === selectedGoodsCategory);
|
||||
}, [sortedGoods, selectedGoodsCategory]);
|
||||
|
||||
const goodsOptions = useMemo(() => {
|
||||
return sortedGoods.map((item) => ({
|
||||
value: item.product_id,
|
||||
@@ -637,6 +684,47 @@ export default function HuyaTasksPage() {
|
||||
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) => {
|
||||
setSelectedIds([account.id]);
|
||||
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'] = [
|
||||
{ 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'] = [
|
||||
{
|
||||
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) => (
|
||||
<Table
|
||||
columns={taskColumns}
|
||||
@@ -1142,13 +1181,15 @@ export default function HuyaTasksPage() {
|
||||
const accountContextMenuPosition = accountContextMenu
|
||||
? contextMenuPosition(accountContextMenu.x, accountContextMenu.y)
|
||||
: null;
|
||||
const accountTableBodyHeight = Math.max(320, accountTableAreaHeight - 42);
|
||||
|
||||
return (
|
||||
<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 }}>
|
||||
<h2 style={{ margin: 0 }}>虎牙任务操作台</h2>
|
||||
<Space wrap>
|
||||
<Tag>账号 {accounts.length}</Tag>
|
||||
<Tag>操作台 {accounts.length}</Tag>
|
||||
<Tag>账号库 {accountPool.length}</Tag>
|
||||
<Tag color="blue">已选 {selectedIds.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>
|
||||
@@ -1216,20 +1257,28 @@ export default function HuyaTasksPage() {
|
||||
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
|
||||
size="small"
|
||||
title="账号表格"
|
||||
extra={(
|
||||
<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>
|
||||
<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>
|
||||
</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' }}>
|
||||
<Input.Search
|
||||
@@ -1250,78 +1299,76 @@ export default function HuyaTasksPage() {
|
||||
/>
|
||||
<Text type="secondary">{filteredAccounts.length} / {accounts.length}</Text>
|
||||
</div>
|
||||
<Table
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedIds,
|
||||
onChange: (keys) => setSelectedIds(keys.map((key) => Number(key))),
|
||||
}}
|
||||
columns={accountColumns}
|
||||
dataSource={filteredAccounts}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{ pageSize: 12, showSizeChanger: true, showTotal: (total) => `共 ${total} 条` }}
|
||||
scroll={{ x: 1520 }}
|
||||
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 ref={accountTableAreaRef} style={{ flex: 1, minHeight: 360, overflow: 'hidden' }}>
|
||||
<Table
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedIds,
|
||||
onChange: (keys) => setSelectedIds(keys.map((key) => Number(key))),
|
||||
}}
|
||||
columns={accountColumns}
|
||||
dataSource={filteredAccounts}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
className="huya-task-account-table"
|
||||
pagination={false}
|
||||
scroll={filteredAccounts.length > 0 ? { x: 1520, y: accountTableBodyHeight } : undefined}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div
|
||||
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>
|
||||
|
||||
<Row gutter={12} style={{ marginTop: 12 }}>
|
||||
<Col xs={24} xl={12}>
|
||||
<Card size="small" title={<Space><ShoppingOutlined />兑换商品</Space>}>
|
||||
<Table
|
||||
columns={goodsColumns}
|
||||
dataSource={filteredGoods}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 260 }}
|
||||
locale={{ emptyText: '暂无兑换商品,请先刷新商品列表' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedExchangeGoodsId(record.product_id),
|
||||
})}
|
||||
rowClassName={(record) => record.product_id === selectedExchangeGoodsId ? 'ant-table-row-selected' : ''}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<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
|
||||
size="small"
|
||||
title={<Space><FieldTimeOutlined />任务记录</Space>}
|
||||
extra={(
|
||||
<Space size={10} wrap>
|
||||
<Text type="secondary">共 {tasks.length}</Text>
|
||||
<Text type="secondary">已计划 {plannedCount}</Text>
|
||||
<Text style={{ color: token.colorSuccess }}>成功 {successCount}</Text>
|
||||
<Text style={{ color: token.colorError }}>失败 {failedCount}</Text>
|
||||
<Button size="small" onClick={() => setTaskRecordsVisible((value) => !value)}>
|
||||
{taskRecordsVisible ? '收起' : '展开'}
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
style={{ marginTop: 12 }}
|
||||
styles={{ body: { padding: taskRecordsVisible ? 12 : 0 } }}
|
||||
>
|
||||
{taskRecordsVisible ? renderTaskTable(5) : null}
|
||||
</Card>
|
||||
<RealtimeLogPanel
|
||||
logs={logs}
|
||||
connected={wsConnected}
|
||||
title="虎牙实时日志"
|
||||
emptyText="暂无虎牙任务日志"
|
||||
height={120}
|
||||
collapsible
|
||||
defaultVisible={false}
|
||||
spinWhenEmpty
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
@@ -1529,6 +1576,88 @@ export default function HuyaTasksPage() {
|
||||
</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
|
||||
title="兑换记录"
|
||||
open={!!exchangeRecordsTask}
|
||||
|
||||
Reference in New Issue
Block a user