实现虎牙刷新商品列表

This commit is contained in:
yml2213
2026-07-04 22:21:35 +08:00
parent e855d7e449
commit fa0d49ff8c
6 changed files with 432 additions and 18 deletions
+121 -15
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Button, Card, Col, Form, Input, InputNumber, message, Modal, Row, Select, Space, Table, Tag, Tooltip, Typography, theme,
Button, Card, Col, Form, Input, InputNumber, message, Modal, Row, Select, Space, Table, Tabs, Tag, Tooltip, Typography, theme,
} from 'antd';
import type { TableProps } from 'antd';
import {
@@ -87,6 +87,34 @@ function resultProfileNick(result: Record<string, unknown> | null | undefined):
return typeof nick === 'string' ? nick : '';
}
function goodsRawString(item: HuyaGoodsItem, key: string): string {
const value = item.raw?.[key];
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
return '';
}
function goodsRawNumber(item: HuyaGoodsItem, key: string): number {
const value = item.raw?.[key];
if (typeof value === 'number') return value;
if (typeof value === 'string' && value.trim()) return Number(value) || 0;
return 0;
}
function goodsCategoryKey(item: HuyaGoodsItem): string {
return goodsRawString(item, 'category_id') || goodsRawString(item, 'category_name') || 'uncategorized';
}
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 hasMiniQrcode(task: HuyaTaskItem): boolean {
return task.task_type === 'get_bind_qr' && Boolean(resultText(task.result, 'mini_qrcode_image'));
}
@@ -101,6 +129,7 @@ export default function HuyaTasksPage() {
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [selectedTaskType, setSelectedTaskType] = useState('query_points');
const [selectedGoodsId, setSelectedGoodsId] = useState<string>('');
const [selectedGoodsCategory, setSelectedGoodsCategory] = useState('');
const [rechargeCount, setRechargeCount] = useState(1);
const [concurrency, setConcurrency] = useState(() => {
const v = localStorage.getItem('huya_task_concurrency');
@@ -200,16 +229,58 @@ export default function HuyaTasksPage() {
return accounts.map((account) => ({ value: account.id, label: accountLabel(account) }));
}, [accounts]);
const sortedGoods = useMemo(() => {
return [...goods].sort((a, b) => (
goodsRawNumber(a, 'category_sort') - goodsRawNumber(b, 'category_sort')
|| goodsRawNumber(a, 'raw_order') - goodsRawNumber(b, 'raw_order')
|| a.id - b.id
));
}, [goods]);
const goodsOptions = useMemo(() => {
return goods.map((item) => ({
return sortedGoods.map((item) => ({
value: item.product_id,
label: `${item.name || item.product_id}${item.price ? ` / ${item.price}积分` : ''}`,
}));
}, [goods]);
}, [sortedGoods]);
const selectedGoods = useMemo(() => {
return goods.find((item) => item.product_id === selectedGoodsId) || null;
}, [goods, selectedGoodsId]);
return sortedGoods.find((item) => item.product_id === selectedGoodsId) || null;
}, [sortedGoods, selectedGoodsId]);
const goodsCategories = useMemo(() => {
const map = new Map<string, { key: string; label: string; sort: number; count: number }>();
sortedGoods.forEach((item) => {
const key = goodsCategoryKey(item);
const current = map.get(key);
if (current) {
current.count += 1;
return;
}
map.set(key, {
key,
label: goodsCategoryLabel(item),
sort: goodsRawNumber(item, 'category_sort'),
count: 1,
});
});
return Array.from(map.values()).sort((a, b) => a.sort - b.sort || a.label.localeCompare(b.label));
}, [sortedGoods]);
useEffect(() => {
if (goodsCategories.length === 0) {
if (selectedGoodsCategory) setSelectedGoodsCategory('');
return;
}
if (!goodsCategories.some((item) => item.key === selectedGoodsCategory)) {
setSelectedGoodsCategory(goodsCategories[0].key);
}
}, [goodsCategories, selectedGoodsCategory]);
const filteredGoods = useMemo(() => {
if (!selectedGoodsCategory) return sortedGoods;
return sortedGoods.filter((item) => goodsCategoryKey(item) === selectedGoodsCategory);
}, [sortedGoods, selectedGoodsCategory]);
const createPayload = (taskType: string) => {
if (taskType !== 'recharge_points') return {};
@@ -238,13 +309,22 @@ export default function HuyaTasksPage() {
concurrency,
payload: createPayload(taskType),
});
const finishTask = () => {
setBatchId(null);
setStarting(false);
if (taskType === 'refresh_goods') {
void loadAll();
} else {
void loadTasks();
}
};
setBatchId(result.batch_id);
message.success(`已创建 ${taskTypes[taskType] || taskType},共 ${result.count} 个账号`);
await loadTasks();
connectLogs(`/api/huya/ws/${result.batch_id}`, {
onClose: () => { setBatchId(null); setStarting(false); loadTasks(); },
onResult: () => { setBatchId(null); setStarting(false); loadTasks(); },
onError: () => { setBatchId(null); setStarting(false); loadTasks(); },
onClose: finishTask,
onResult: finishTask,
onError: finishTask,
});
} catch (e: unknown) {
message.error(getErrorMessage(e));
@@ -315,6 +395,11 @@ export default function HuyaTasksPage() {
if (value?.is_bound === false) return <Tag></Tag>;
return <Text type="secondary">-</Text>;
}
if (record.task_type === 'refresh_goods') {
const goodsCount = value?.goods_count;
if (typeof goodsCount === 'number') return <Tag color="green"> {goodsCount} </Tag>;
return <Text type="secondary">-</Text>;
}
const availableScore = value?.available_score;
if (typeof availableScore === 'number') {
@@ -365,6 +450,14 @@ export default function HuyaTasksPage() {
const goodsColumns: TableProps<HuyaGoodsItem>['columns'] = [
{ title: '商品ID', dataIndex: 'product_id', width: 120, ellipsis: true },
{ title: '名称', dataIndex: 'name', ellipsis: true },
{
title: '分类',
width: 110,
render: (_: unknown, record) => {
const label = goodsCategoryLabel(record);
return label ? <Tag>{label}</Tag> : <Text type="secondary">-</Text>;
},
},
{
title: '价格',
dataIndex: 'price',
@@ -376,7 +469,7 @@ export default function HuyaTasksPage() {
title: '库存',
dataIndex: 'remain_text',
width: 100,
render: (value: string) => value || <Text type="secondary">-</Text>,
render: (value: string) => formatRemainText(value) || <Text type="secondary">-</Text>,
},
{
title: '更新时间',
@@ -398,7 +491,7 @@ export default function HuyaTasksPage() {
</Space>
</div>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', paddingRight: 2 }}>
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', paddingRight: 2 }}>
<Row gutter={12}>
<Col xs={24} xl={10}>
<Card
@@ -468,14 +561,27 @@ export default function HuyaTasksPage() {
style={{ width: 120 }}
/>
</Space>
{goodsCategories.length > 0 && (
<Tabs
size="small"
activeKey={selectedGoodsCategory}
onChange={setSelectedGoodsCategory}
items={
goodsCategories.map((item) => ({
key: item.key,
label: `${item.label} (${item.count})`,
}))
}
/>
)}
<Table
columns={goodsColumns}
dataSource={goods}
dataSource={filteredGoods}
rowKey="id"
size="small"
pagination={false}
scroll={{ x: 620, y: 220 }}
locale={{ emptyText: '暂无商品快照,后续接入刷新商品列表后写入' }}
scroll={{ x: 760, y: 220 }}
locale={{ emptyText: '暂无商品快照,请先刷新商品列表' }}
/>
</Card>
</Col>
@@ -534,7 +640,7 @@ export default function HuyaTasksPage() {
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
{QUICK_ACTIONS.map((item) => (
<Tooltip key={item.key} title={item.key === 'refresh_goods' ? '当前阶段创建计划任务,真实拉取逻辑后续接入' : undefined}>
<Tooltip key={item.key} title={item.key === 'refresh_goods' ? '使用一个选中的 CK 刷新当前 SID 商品快照' : undefined}>
<Button
icon={item.icon}
size="small"
@@ -547,7 +653,7 @@ export default function HuyaTasksPage() {
))}
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
</Text>
</div>
</Card>