merge: 合并和平小店兑换功能

This commit is contained in:
yml2213
2026-08-08 00:03:16 +08:00
6 changed files with 350 additions and 10 deletions
+84 -6
View File
@@ -91,6 +91,7 @@ const PEACE_QUICK_ACTIONS = [
{ key: 'query_xpd_balance', icon: <SearchOutlined /> },
{ key: 'query_xpd_fragments', icon: <SearchOutlined /> },
{ key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
{ key: 'exchange_xpd_goods', icon: <ShoppingOutlined /> },
];
const ELITE_TASK_TYPES = new Set([
@@ -133,6 +134,7 @@ const PEACE_TASK_TYPES = new Set([
'refresh_xpd_goods',
'query_xpd_balance',
'query_xpd_fragments',
'exchange_xpd_goods',
]);
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
@@ -231,9 +233,34 @@ function goodsLabel(item: DouyuGoodsItem): string {
function xpdGoodsLabel(item: DouyuGoodsItem): string {
const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null };
const price = xpd.price != null ? ` / ${xpd.price}点券` : '';
const left = xpd.goods_left != null ? ` / 剩${xpd.goods_left}` : '';
return `${item.name || item.commodity_id}${price}${left}`;
const raw = item.raw?.raw as Record<string, unknown> | undefined;
const pointPrice = Number(xpd.price ?? raw?.iPrice);
const fragmentPrice = Number(raw?.iJb2Price);
const prices = [
Number.isFinite(pointPrice) && pointPrice > 0 ? `${pointPrice}点券` : '',
Number.isFinite(fragmentPrice) && fragmentPrice > 0 ? `${fragmentPrice}碎片` : '',
].filter(Boolean);
const stock = xpd.goods_left;
const stockText = stock != null && Number.isFinite(Number(stock))
? `库存${stock}`
: '库存未知';
return `${item.name || item.commodity_id}${prices.length ? ` / ${prices.join(' 或 ')}` : ' / 暂不可兑换'} / ${stockText}`;
}
function xpdGoodsPaymentOptions(item: DouyuGoodsItem | undefined): Array<{ value: '1' | '5'; label: string }> {
if (!item) return [];
const xpd = item as DouyuGoodsItem & { price?: number | null };
const raw = item.raw?.raw as Record<string, unknown> | undefined;
const pointPrice = Number(xpd.price ?? raw?.iPrice);
const fragmentPrice = Number(raw?.iJb2Price);
return [
Number.isFinite(pointPrice) && pointPrice > 0
? { value: '1' as const, label: `使用点券兑换(${pointPrice}点券)` }
: null,
Number.isFinite(fragmentPrice) && fragmentPrice > 0
? { value: '5' as const, label: `使用扭蛋碎片兑换(${fragmentPrice}碎片)` }
: null,
].filter((option): option is { value: '1' | '5'; label: string } => option !== null);
}
function formatWaitSeconds(seconds: number | null | undefined): string {
@@ -337,6 +364,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
const [selectedGoodsId, setSelectedGoodsId] = useState('');
const [xpdPayType, setXpdPayType] = useState<'1' | '5'>('1');
const [accountSearch, setAccountSearch] = useState('');
const [layoutMode, setLayoutMode] = useState<'stack' | 'split'>(() => {
const v = localStorage.getItem(DOUYU_LAYOUT_MODE_STORAGE_KEY);
@@ -847,6 +875,26 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
// 多批次并发:不再用全局 busy 标志阻塞 UI,刷新频率直接用各子条件判断
const selectedXpdGoods = useMemo(
() => goods.find((item) => item.commodity_id === selectedGoodsId),
[goods, selectedGoodsId],
);
const xpdPaymentOptions = useMemo(
() => xpdGoodsPaymentOptions(selectedXpdGoods),
[selectedXpdGoods],
);
const selectedXpdGoodsSoldOut = (() => {
const stock = (selectedXpdGoods as (DouyuGoodsItem & { goods_left?: number | null }) | undefined)?.goods_left;
return stock != null && Number.isFinite(Number(stock)) && Number(stock) <= 0;
})();
useEffect(() => {
if (xpdPaymentOptions.length === 0) return;
if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
setXpdPayType(xpdPaymentOptions[0].value);
}
}, [xpdPayType, xpdPaymentOptions]);
const startTask = async (
taskType: string,
accountIds?: number[],
@@ -858,12 +906,25 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return;
}
// 多批次并发:不再拦截,允许在已有批次运行时继续创建新批次
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) {
if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType) && !selectedGoodsId) {
message.warning('请先选择兑换商品');
return;
}
if (taskType === 'exchange_xpd_goods') {
if (selectedXpdGoodsSoldOut) {
message.warning('该商品库存不足,请刷新商品列表后重试');
return;
}
if (!xpdPaymentOptions.some((option) => option.value === xpdPayType)) {
message.warning('该商品不支持当前兑换方式');
return;
}
}
const payload: Record<string, unknown> = { ...extraPayload };
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) payload.commodity_id = selectedGoodsId;
if (['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods'].includes(taskType)) {
payload.commodity_id = selectedGoodsId;
}
if (taskType === 'exchange_xpd_goods') payload.pay_type = xpdPayType;
if (taskType === 'create_gold_qr') payload.amount = goldAmount;
if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) {
payload.gift_count = giftCount;
@@ -1484,6 +1545,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
const actionDisabled = (taskType: string) => {
if (selectedIds.length === 0) return true;
if (taskType === 'exchange_xpd_goods') {
return !selectedGoodsId
|| selectedXpdGoodsSoldOut
|| !xpdPaymentOptions.some((option) => option.value === xpdPayType);
}
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
// 选中账号中有正在执行的任务时禁用,避免同账号并发不同类型任务(并发写状态字段)
return selectedHasRunning;
@@ -1578,7 +1644,19 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
optionFilterProp="label"
style={{ width: '100%' }}
/>
{renderActionButton('refresh_xpd_goods')}
<Select
size="small"
value={xpdPaymentOptions.some((option) => option.value === xpdPayType) ? xpdPayType : undefined}
onChange={setXpdPayType}
options={xpdPaymentOptions}
placeholder={selectedGoodsId ? '该商品暂无可用兑换方式' : '先选择商品'}
disabled={!selectedGoodsId || xpdPaymentOptions.length === 0}
style={{ width: '100%' }}
/>
<div style={actionGridStyle}>
{renderActionButton('refresh_xpd_goods')}
{renderActionButton('exchange_xpd_goods', 'primary')}
</div>
</Space>
</div>
</div>
+3 -2
View File
@@ -1,6 +1,6 @@
import type { DouyuTaskItem } from '../api/types';
const EXCHANGE_TASK_TYPES = new Set(['exchange_goods', 'exchange_esports_goods']);
const EXCHANGE_TASK_TYPES = new Set(['exchange_goods', 'exchange_esports_goods', 'exchange_xpd_goods']);
const CARD_W = 320;
const CARD_H = 470;
const FONT_FAMILY = '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif';
@@ -28,9 +28,10 @@ function goodsOf(task: DouyuTaskItem): {
const goods = (task.result?.goods ?? null) as Record<string, unknown> | null;
const name = rawString(goods, 'commodityName')
|| rawString(goods, 'name')
|| rawString(goods, 'sGoodsName')
|| task.message.replace(/^兑换[^:]*[:]?\s*/, '').trim()
|| rawString(task.result, 'commodity_id');
const pic = rawString(goods, 'webPic') || rawString(goods, 'pic');
const pic = rawString(goods, 'webPic') || rawString(goods, 'pic') || rawString(goods, 'sGoodsPic');
const roleName = rawString(task.result, 'game_name');
const channel = rawString(task.result, 'game_channel');
const accountName = rawString(task.result, 'account_name')