优化虎牙任务操作台逻辑
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user