feat(huya): 开通充值支持数量与记住上次选择,精英宝典单独一块
- 支持「其他开通商品」按份数下单(payload.count → createOrder 的 item_count),数量上限取商品 buy_limit - 商品选择/数量写入 localStorage,下次进来还是上次那套 - 精英宝典(限购主商品)从其他开通商品里拆出来单独一块,避免混在一个下拉里选错 - 测试:payload.count=3 进入 run_open_flow 且结果/文案带 x3
This commit is contained in:
@@ -22,6 +22,13 @@ const SCOPE = 'elite' as const;
|
||||
const ACT_ID = 25135;
|
||||
const DEFAULT_SID = '2203';
|
||||
const LAYOUT_KEY = 'huya_elite_layout_mode';
|
||||
// 开通/充值:记住上次选的商品与数量(浏览器本地,换台机器不影响)
|
||||
const RECHARGE_SPU_KEY = 'huya_elite_recharge_spu';
|
||||
const RECHARGE_COUNT_KEY = 'huya_elite_recharge_count';
|
||||
const ELITE_SPU_KEY = 'huya_elite_elite_spu';
|
||||
// 与后端 ELITE_HANDBOOK_SPU_IDS 对齐:精英宝典单独一块,不和其他开通商品混在一个下拉里
|
||||
const ELITE_HANDBOOK_SPU_IDS = ['hy-8328015', 'hy-5879340', 'hy-3845257', 'hy-2947756'];
|
||||
const ELITE_HANDBOOK_NAME_KEYWORD = '精英宝典';
|
||||
|
||||
const TASK_LABELS: Record<string, string> = {
|
||||
query_act_tasks: '读取宝典任务', get_bind_qr: '获取绑定二维码', query_game_name: '查询游戏角色',
|
||||
@@ -54,6 +61,37 @@ function savedInterval(): number {
|
||||
return Number.isInteger(value) && value >= 5 && value <= 60 ? value : 15;
|
||||
}
|
||||
|
||||
/** 上次选的开通商品(列表里已不存在时会自动回落到第一个)。 */
|
||||
function savedRechargeSpu(): string {
|
||||
return localStorage.getItem(RECHARGE_SPU_KEY) || '';
|
||||
}
|
||||
|
||||
/** 上次填的开通数量(后端 clamp 1~999)。 */
|
||||
function savedRechargeCount(): number {
|
||||
const value = Number(localStorage.getItem(RECHARGE_COUNT_KEY));
|
||||
return Number.isInteger(value) && value >= 1 && value <= 999 ? value : 1;
|
||||
}
|
||||
|
||||
/** 上次选的精英宝典商品(就一个时基本用不上)。 */
|
||||
function savedEliteSpu(): string {
|
||||
return localStorage.getItem(ELITE_SPU_KEY) || '';
|
||||
}
|
||||
|
||||
/** 精英宝典(含换过 SKU 的历史 spu)走单独入口,其余商品走「其他开通商品」。 */
|
||||
function isEliteHandbookGood(item: HuyaRechargeGoodsItem): boolean {
|
||||
return ELITE_HANDBOOK_SPU_IDS.includes(item.spu_id) || (item.name || '').includes(ELITE_HANDBOOK_NAME_KEYWORD);
|
||||
}
|
||||
|
||||
/** 开通商品顺序:跟虎牙页面一致的 raw_order,其次按 id。 */
|
||||
function byRechargeOrder(a: HuyaRechargeGoodsItem, b: HuyaRechargeGoodsItem): number {
|
||||
return Number(a.raw?.raw_order ?? 0) - Number(b.raw?.raw_order ?? 0) || a.id - b.id;
|
||||
}
|
||||
|
||||
/** 开通商品的下拉/展示文案:名称 / 单价 / spu。 */
|
||||
function rechargeGoodsLabel(item: HuyaRechargeGoodsItem): string {
|
||||
return `${item.name || item.spu_id} / ${item.price ? `${(item.price / 100).toFixed(2)}元` : '价格动态'} / ${item.spu_id}`;
|
||||
}
|
||||
|
||||
function resultText(task: HuyaTaskItem | undefined, key: string): string {
|
||||
const value = task?.result?.[key];
|
||||
return value == null ? '' : String(value);
|
||||
@@ -132,7 +170,11 @@ export default function HuyaElitePage() {
|
||||
const [goods, setGoods] = useState<HuyaGoodsItem[]>([]);
|
||||
const [selectedGoodsId, setSelectedGoodsId] = useState('');
|
||||
const [rechargeGoods, setRechargeGoods] = useState<HuyaRechargeGoodsItem[]>([]);
|
||||
const [selectedRechargeSpu, setSelectedRechargeSpu] = useState('');
|
||||
const [selectedRechargeSpu, setSelectedRechargeSpu] = useState(savedRechargeSpu);
|
||||
// 精英宝典单独一块:和自己的商品选择分开记
|
||||
const [selectedEliteSpu, setSelectedEliteSpu] = useState(savedEliteSpu);
|
||||
// 开通/充值数量:一个账号按这个份数下单(后端 payload.count,1~999),记住上次填的值
|
||||
const [rechargeCount, setRechargeCount] = useState(savedRechargeCount);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [starting, setStarting] = useState(false);
|
||||
@@ -219,18 +261,56 @@ export default function HuyaElitePage() {
|
||||
setSelectedIds((prev) => prev.filter((id) => saved.account_ids.includes(id)));
|
||||
};
|
||||
|
||||
const startTask = async (taskType: string, accountIds: number[] = selectedIds) => {
|
||||
// 开通商品分组:精英宝典(限购主商品,单独一块)与其他商品(币/徽章/令…,带数量那一块)
|
||||
const rechargeGoodsSorted = [...rechargeGoods].sort(byRechargeOrder);
|
||||
const eliteRechargeGoods = rechargeGoodsSorted.filter(isEliteHandbookGood);
|
||||
const otherRechargeGoods = rechargeGoodsSorted.filter((item) => !isEliteHandbookGood(item));
|
||||
const selectedEliteGoods = eliteRechargeGoods.find((item) => item.spu_id === selectedEliteSpu) || eliteRechargeGoods[0];
|
||||
const rechargeEliteLimit = Math.max(1, Math.min(999, selectedEliteGoods?.buy_limit || 999));
|
||||
|
||||
// 其他开通商品的数量:上限取商品的 buy_limit(限购商品只能 1 份),
|
||||
// 下单时作为后端 payload.count(1~999,走 createOrder 的 item_count)。
|
||||
const selectedRechargeGoods = otherRechargeGoods.find((item) => item.spu_id === selectedRechargeSpu);
|
||||
const rechargeBuyLimit = Math.max(1, Math.min(999, selectedRechargeGoods?.buy_limit || 999));
|
||||
const rechargeCountValue = Math.max(1, Math.min(rechargeCount, rechargeBuyLimit));
|
||||
const rechargeAmountText = selectedRechargeGoods?.price
|
||||
? `单价 ¥${(selectedRechargeGoods.price / 100).toFixed(2)} × ${rechargeCountValue}`
|
||||
: '';
|
||||
|
||||
// 换商品:按新商品的限购把数量压回合法范围,并把这次选择写进浏览器,下次进来还是它
|
||||
const selectRechargeGoods = (spu: string) => {
|
||||
const limit = Math.max(1, Math.min(999, otherRechargeGoods.find((item) => item.spu_id === spu)?.buy_limit || 999));
|
||||
const nextCount = Math.max(1, Math.min(rechargeCount, limit));
|
||||
setSelectedRechargeSpu(spu);
|
||||
setRechargeCount(nextCount);
|
||||
localStorage.setItem(RECHARGE_SPU_KEY, spu);
|
||||
localStorage.setItem(RECHARGE_COUNT_KEY, String(nextCount));
|
||||
};
|
||||
const selectEliteGoods = (spu: string) => {
|
||||
setSelectedEliteSpu(spu);
|
||||
localStorage.setItem(ELITE_SPU_KEY, spu);
|
||||
};
|
||||
const changeRechargeCount = (value: number | null) => {
|
||||
const next = Math.max(1, Math.min(rechargeBuyLimit, Number(value) || 1));
|
||||
setRechargeCount(next);
|
||||
localStorage.setItem(RECHARGE_COUNT_KEY, String(next));
|
||||
};
|
||||
|
||||
const startTask = async (taskType: string, accountIds: number[] = selectedIds, rechargeOverride?: { spu: string; count: number }) => {
|
||||
if (!accountIds.length) { message.warning('请先勾选账号'); return; }
|
||||
if (taskType === 'exchange_goods' && !selectedGoodsId) { message.warning('请先选择兑换商品'); return; }
|
||||
if (taskType === 'create_recharge_order' && !selectedRechargeSpu) { message.warning('请先选择宝典商品'); return; }
|
||||
if (taskType === 'create_recharge_order' && !rechargeOverride && !selectedRechargeSpu) { message.warning('请先选择开通商品'); return; }
|
||||
setStarting(true);
|
||||
try {
|
||||
const payload = taskType === 'exchange_goods'
|
||||
? { sid: Number(config?.sid || DEFAULT_SID), product_id: Number(selectedGoodsId), act_id: ACT_ID }
|
||||
: taskType === 'query_act_tasks' ? { act_id: ACT_ID }
|
||||
: taskType === 'create_recharge_order' ? (() => {
|
||||
const selected = rechargeGoods.find((item) => item.spu_id === selectedRechargeSpu);
|
||||
return { spu_id: selectedRechargeSpu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典' };
|
||||
// 精英宝典那块用 override 传自己的 spu(限购 1 份);其他商品用下拉选的 spu + 数量
|
||||
const spu = rechargeOverride?.spu || selectedRechargeSpu;
|
||||
const count = rechargeOverride?.count ?? rechargeCountValue;
|
||||
const selected = rechargeGoods.find((item) => item.spu_id === spu);
|
||||
return { spu_id: spu, sku_id: Number(selected?.sku_id || 0), product_name: selected?.name || '精英宝典', count };
|
||||
})() : {};
|
||||
// 支付码要「同时弹出多个二维码」:并发必须 >= 账号数,否则排在后面的码要等前面的码付完/超时才开始生成
|
||||
const batchConcurrency = taskType === 'create_recharge_order'
|
||||
@@ -309,14 +389,24 @@ export default function HuyaElitePage() {
|
||||
return accounts.filter((item) => !value || [item.uid, item.nickname, item.username, item.tag, item.game_name, item.game_phone].some((v) => String(v || '').toLowerCase().includes(value)));
|
||||
}, [accounts, search]);
|
||||
const filteredGoods = useMemo(() => goods.map((item) => ({ ...item, label: `${item.name} / ${item.price ?? '-'}分 / ${item.remain_text || '动态库存'}` })), [goods]);
|
||||
const filteredRechargeGoods = useMemo(() => rechargeGoods.map((item) => ({ ...item, label: `${item.name || item.spu_id} / ${item.price ? `${(item.price / 100).toFixed(2)}元` : '价格动态'} / ${item.spu_id}` })).sort((a, b) => Number(a.raw?.raw_order ?? 0) - Number(b.raw?.raw_order ?? 0) || a.id - b.id), [rechargeGoods]);
|
||||
// 默认选中列表里的第一个商品;已选商品从列表消失时同步回落到第一个
|
||||
// 精英宝典:通常只有一个商品;换过 SKU 或已下线时回落到当前的第一个(并记住回落结果)
|
||||
useEffect(() => {
|
||||
if (!filteredRechargeGoods.length) return;
|
||||
if (!filteredRechargeGoods.some((item) => item.spu_id === selectedRechargeSpu)) {
|
||||
setSelectedRechargeSpu(filteredRechargeGoods[0].spu_id);
|
||||
const list = [...rechargeGoods].sort(byRechargeOrder).filter(isEliteHandbookGood);
|
||||
if (!list.length) return;
|
||||
if (!list.some((item) => item.spu_id === selectedEliteSpu)) {
|
||||
setSelectedEliteSpu(list[0].spu_id);
|
||||
localStorage.setItem(ELITE_SPU_KEY, list[0].spu_id);
|
||||
}
|
||||
}, [filteredRechargeGoods, selectedRechargeSpu]);
|
||||
}, [rechargeGoods, selectedEliteSpu]);
|
||||
// 其他开通商品:已选商品从列表消失时回落到第一个(并记住回落结果)
|
||||
useEffect(() => {
|
||||
const list = [...rechargeGoods].sort(byRechargeOrder).filter((item) => !isEliteHandbookGood(item));
|
||||
if (!list.length) return;
|
||||
if (!list.some((item) => item.spu_id === selectedRechargeSpu)) {
|
||||
setSelectedRechargeSpu(list[0].spu_id);
|
||||
localStorage.setItem(RECHARGE_SPU_KEY, list[0].spu_id);
|
||||
}
|
||||
}, [rechargeGoods, selectedRechargeSpu]);
|
||||
const exchangeCount = tasks.filter((task) => task.task_type === 'exchange_goods').length;
|
||||
// 开通支付码:同一批次里每个账号一个码,未支付的排前面;已支付/已开通的只留一行状态
|
||||
const payTasks = useMemo(() => {
|
||||
@@ -367,8 +457,8 @@ export default function HuyaElitePage() {
|
||||
<Button size="small" type="primary" icon={<SearchOutlined />} onClick={() => void startTask('query_all')} disabled={starting || !selectedIds.length}>一键查询</Button><Button size="small" icon={<QrcodeOutlined />} onClick={() => void startTask('get_bind_qr')} disabled={starting || !selectedIds.length}>绑定二维码</Button><Button size="small" icon={<SearchOutlined />} onClick={() => void startTask('query_game_name')} disabled={starting || !selectedIds.length}>查询角色</Button><Button size="small" icon={<CheckCircleOutlined />} onClick={() => void startTask('confirm_bind')} disabled={starting || !selectedIds.length}>确认绑定</Button><Button size="small" icon={<SearchOutlined />} onClick={() => void startTask('query_points')} disabled={starting || !selectedIds.length}>刷新积分</Button><Button size="small" onClick={() => void startTask('query_act_tasks')} disabled={starting || !selectedIds.length}>宝典任务</Button><Button size="small" onClick={() => void startTask('query_exchange_records')} disabled={starting || !selectedIds.length}>兑换记录</Button>
|
||||
</div></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><ShoppingOutlined /><span>兑换</span></div><Select size="small" showSearch optionFilterProp="label" value={selectedGoodsId || undefined} onChange={setSelectedGoodsId} options={filteredGoods.map((item) => ({ value: item.product_id, label: item.label }))} placeholder="选择兑换商品" style={{ width: '100%' }} /><div className="huya-action-grid"><Button size="small" icon={<ReloadOutlined />} onClick={() => void startTask('refresh_goods')} disabled={starting || !selectedIds.length}>刷新商品</Button><Button size="small" type="primary" icon={<ShoppingOutlined />} onClick={() => void startTask('exchange_goods')} disabled={starting || !selectedIds.length || !selectedGoodsId}>兑换商品</Button></div></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><GiftOutlined /><span>开通宝典</span></div><Button block size="small" icon={<QrcodeOutlined />} onClick={() => void startTask('create_recharge_order')} disabled={starting || !selectedIds.length}>生成开通支付码</Button></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><ShoppingOutlined /><span>开通商品</span></div><Select size="small" showSearch optionFilterProp="label" value={selectedRechargeSpu || undefined} onChange={setSelectedRechargeSpu} options={filteredRechargeGoods.map((item) => ({ value: item.spu_id, label: item.label }))} placeholder="先刷新开通商品" style={{ width: '100%' }} /><Button block size="small" icon={<ReloadOutlined />} onClick={() => void startTask('refresh_recharge_goods')} disabled={starting || !selectedIds.length} style={{ marginTop: 6 }}>刷新开通商品</Button></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><GiftOutlined /><span>开通宝典</span></div>{eliteRechargeGoods.length > 1 ? <Select size="small" showSearch optionFilterProp="label" value={selectedEliteGoods?.spu_id} onChange={selectEliteGoods} options={eliteRechargeGoods.map((item) => ({ value: item.spu_id, label: rechargeGoodsLabel(item) }))} style={{ width: '100%' }} /> : <Text style={{ fontSize: 12 }} ellipsis>{selectedEliteGoods ? `${rechargeGoodsLabel(selectedEliteGoods)}${rechargeEliteLimit < 999 ? ` · 限购 ${rechargeEliteLimit} 份` : ''}` : '列表里还没有精英宝典,请先刷新开通商品'}</Text>}<Button block size="small" type="primary" icon={<QrcodeOutlined />} onClick={() => { if (selectedEliteGoods) void startTask('create_recharge_order', selectedIds, { spu: selectedEliteGoods.spu_id, count: 1 }); }} disabled={starting || !selectedIds.length || !selectedEliteGoods} style={{ marginTop: 6 }}>确认开通宝典并生成支付码</Button></div>
|
||||
<div className="huya-operation-section"><div className="huya-operation-title"><ShoppingOutlined /><span>其他开通商品</span></div><Select size="small" showSearch optionFilterProp="label" value={selectedRechargeSpu || undefined} onChange={selectRechargeGoods} options={otherRechargeGoods.map((item) => ({ value: item.spu_id, label: rechargeGoodsLabel(item) }))} placeholder="先刷新开通商品" style={{ width: '100%' }} /><div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}><Text type="secondary" style={{ fontSize: 12 }}>数量</Text><InputNumber size="small" min={1} max={rechargeBuyLimit} value={rechargeCountValue} onChange={changeRechargeCount} style={{ width: 80 }} /><Text type="secondary" style={{ fontSize: 12 }}>{rechargeAmountText}{rechargeBuyLimit < 999 ? ` · 限购 ${rechargeBuyLimit} 份` : ''}</Text></div><Button block size="small" type="primary" icon={<QrcodeOutlined />} onClick={() => void startTask('create_recharge_order')} disabled={starting || !selectedIds.length || !selectedRechargeSpu} style={{ marginTop: 6 }}>{rechargeCountValue > 1 ? `确认充值并生成支付码 ×${rechargeCountValue}` : '确认充值并生成支付码'}</Button><Button block size="small" icon={<ReloadOutlined />} onClick={() => void startTask('refresh_recharge_goods')} disabled={starting || !selectedIds.length} style={{ marginTop: 6 }}>刷新开通商品</Button></div>
|
||||
<RealtimeLogPanel logs={logs.logs} connected={logs.connected} title="实时日志" emptyText="暂无任务日志" height={150} collapsible defaultVisible={false} />
|
||||
</Space>
|
||||
);
|
||||
@@ -409,7 +499,7 @@ export default function HuyaElitePage() {
|
||||
<div style={{ background: '#fff', padding: 6, borderRadius: 6, lineHeight: 0, display: 'inline-block' }}>{resultText(task, 'mini_qrcode_image') ? <img src={`data:image/png;base64,${resultText(task, 'mini_qrcode_image')}`} alt="支付码" style={{ width: 150, height: 150, objectFit: 'contain' }} /> : <QRCode value={qrValue} size={150} type="svg" errorLevel="L" bordered={false} color="#000" bgColor="#fff" />}</div>
|
||||
<div><Text strong ellipsis>{payTaskLabel(task)}</Text></div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>UID {task.account_uid || '-'}</Text>
|
||||
<div><Text type="secondary" style={{ fontSize: 12 }}>{[resultText(task, 'product_name') || '精英宝典', resultText(task, 'amount_text')].filter(Boolean).join(' ')}</Text></div>
|
||||
<div><Text type="secondary" style={{ fontSize: 12 }}>{[resultText(task, 'product_name') || '精英宝典', Number(resultText(task, 'count')) > 1 ? `×${resultText(task, 'count')}` : '', resultText(task, 'amount_text')].filter(Boolean).join(' ')}</Text></div>
|
||||
<div style={{ marginTop: 4 }}><Tag color={TASK_COLORS[task.status] || 'default'}>{resultText(task, 'payment_status_label') || TASK_STATUS_LABELS[task.status] || task.status}</Tag></div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }} ellipsis title={task.message}>{task.message || '-'}</Text>
|
||||
{isLoopbackUrl(qrValue) && <div><Text type="warning" style={{ fontSize: 12 }}>二维码是本机地址(localhost),手机扫不开</Text></div>}
|
||||
|
||||
Reference in New Issue
Block a user