feat(huya): 增加协作单礼包字典与名称编辑

This commit is contained in:
yml2213
2026-11-07 16:54:01 +08:00
parent afcbff2207
commit efa8bf2a46
12 changed files with 1183 additions and 44 deletions
+88 -41
View File
@@ -27,6 +27,7 @@ import {
type HuyaCollabCkPreview,
type HuyaCollabFrequentGift,
type HuyaCollabGift,
type HuyaCollabGiftCatalog,
type HuyaCollabImageSlot,
type HuyaCollabMetric,
type HuyaCollabOrder,
@@ -40,6 +41,7 @@ import {
import HuyaCollabShopModal from '../components/HuyaCollabShopModal';
import HuyaCollabShopPicker from '../components/HuyaCollabShopPicker';
import HuyaCollabHomeImageUploader from '../components/HuyaCollabHomeImageUploader';
import HuyaCollabGiftModal from '../components/HuyaCollabGiftModal';
import { message } from '../utils/antdMessage';
import { getErrorMessage } from '../utils/error';
import { copyTextToClipboard } from '../utils/clipboard';
@@ -513,10 +515,12 @@ export default function HuyaCollabPage() {
const [summary, setSummary] = useState<HuyaCollabSummary | null>(null);
const [owners, setOwners] = useState<HuyaCollabOwner[]>([]);
const [shops, setShops] = useState<HuyaCollabShop[]>([]);
const [giftCatalog, setGiftCatalog] = useState<HuyaCollabGiftCatalog[]>([]);
const [giftNames, setGiftNames] = useState<string[]>([]);
// 「常用礼包」:台账里用得最多的几个(新建/补资料的礼包名快捷按钮,见 2026-10-18 需求)
const [frequentGifts, setFrequentGifts] = useState<HuyaCollabFrequentGift[]>([]);
const [shopModalOpen, setShopModalOpen] = useState(false);
const [giftModalOpen, setGiftModalOpen] = useState(false);
const [filters, setFilters] = useState<HuyaCollabOrderQuery>({
include_void: false,
archive_scope: 'active',
@@ -616,6 +620,7 @@ export default function HuyaCollabPage() {
nextSummary,
nextOwners,
nextShops,
nextGiftCatalog,
nextGiftNames,
nextFrequentGifts,
] = await Promise.all([
@@ -632,6 +637,7 @@ export default function HuyaCollabPage() {
}),
withMeta ? huyaApi.collabOrderOwners() : Promise.resolve(null),
withMeta ? huyaApi.listCollabShops() : Promise.resolve(null),
withMeta ? huyaApi.listCollabGiftCatalog() : Promise.resolve(null),
withMeta ? huyaApi.collabOrderGifts() : Promise.resolve(null),
withMeta ? huyaApi.collabFrequentGifts() : Promise.resolve(null),
]);
@@ -640,6 +646,7 @@ export default function HuyaCollabPage() {
setSummary(nextSummary);
if (nextOwners) setOwners(nextOwners);
if (nextShops) setShops(nextShops);
if (nextGiftCatalog) setGiftCatalog(nextGiftCatalog);
if (nextGiftNames) setGiftNames(nextGiftNames);
if (nextFrequentGifts) setFrequentGifts(nextFrequentGifts);
// 选中是「当前页」的概念:刷新后剔除已经不在这页上的行,避免点到别人的脏 id。
@@ -657,7 +664,7 @@ export default function HuyaCollabPage() {
// 暂停后连 WebSocket 也不建:既不自动重拉,也不做断线兜底轮询。
const realtimeSyncEnabled = realtimeEnabled && !realtimePaused;
const collabModalOpen = Boolean(
createOpen || supplementTarget || detail || statusAction || batchStatusOpen || rechargeOpen || archiveOpen || ckTarget || ckExportOpen || shopModalOpen,
createOpen || supplementTarget || detail || statusAction || batchStatusOpen || rechargeOpen || archiveOpen || ckTarget || ckExportOpen || shopModalOpen || giftModalOpen,
);
const toggleRealtimeSync = (nextEnabled: boolean) => {
@@ -761,12 +768,16 @@ export default function HuyaCollabPage() {
}
if (preview.ok && preview.has_role) {
message.success(`CK 有效:绑定角色 ${preview.bind_game_role}`);
// 「记住的礼包」如果不在当前列表里就清掉重选;列表为空时不动它(允许手填)
// 「记住的礼包」如果不在实时列表、也不在标准字典里就清掉重选。
const options = preview.gifts.length > 0 ? preview.gifts : giftsRef.current;
const available = new Set([
...options.map((gift) => gift.name),
...giftCatalog.filter((gift) => gift.is_active).map((gift) => gift.name),
]);
const picked = String(createForm.getFieldValue('gift_name') ?? '').trim();
if (picked && options.length > 0 && !options.some((gift) => gift.name === picked)) {
if (picked && available.size > 0 && !available.has(picked)) {
createForm.setFieldValue('gift_name', undefined);
setGiftHint(`上次选的「${picked}」不在当前的礼包列表里,已清空,请重新点选`);
setGiftHint(`上次选的「${picked}」不在实时礼包或标准字典里,已清空,请重新点选`);
} else {
setGiftHint('');
}
@@ -778,7 +789,7 @@ export default function HuyaCollabPage() {
if (seq === previewSeq.current) setPreviewing(false);
}
},
[emptyPreview, createForm],
[emptyPreview, createForm, giftCatalog],
);
/**
@@ -1825,33 +1836,49 @@ export default function HuyaCollabPage() {
: giftOptions.length > 0
? `礼包用本地缓存(${giftOptions.length} 个${giftCacheLabel ? ` · ${giftCacheLabel}` : ''}),要更新点「强制刷新礼包」`
: (ckPreview?.gifts_message || '');
const hasSelectableGifts = giftOptions.length > 0
|| giftCatalog.some((gift) => gift.is_active);
/**
* 礼包下拉选项:只允许点选缓存里的礼包,不接受手输。
* 当前值是历史/已下线礼包时补一个只读选项,保证界面能显示,但依然只能从下拉里选。
*/
/** 礼包下拉:实时活动礼包 + 管理员维护的标准字典,二者按名称去重。 */
const giftSelectOptions = (current?: string | null) => {
const value = String(current ?? '').trim();
const options = giftOptions.map((gift) => ({
value: gift.name,
label: [
gift.category_name ? `[${gift.category_name}]` : '',
gift.name,
gift.price ? `· ${gift.price} 积分` : '',
gift.remain_text ? `· 剩 ${gift.remain_text}` : '',
].filter(Boolean).join(' '),
}));
if (value && !giftOptions.some((gift) => gift.name === value)) {
const seen = new Set<string>();
const options = giftOptions
.filter((gift) => {
if (!gift.name || seen.has(gift.name)) return false;
seen.add(gift.name);
return true;
})
.map((gift) => ({
value: gift.name,
label: [
gift.category_name ? `[${gift.category_name}]` : '',
gift.name,
gift.price ? `· ${gift.price} 积分` : '',
gift.remain_text ? `· 剩 ${gift.remain_text}` : '',
].filter(Boolean).join(' '),
}));
for (const gift of giftCatalog) {
if (!gift.is_active || !gift.name || seen.has(gift.name)) continue;
seen.add(gift.name);
options.push({
value: gift.name,
label: `${gift.name} · ${gift.points} 积分 · 标准字典`,
});
}
if (value && !seen.has(value)) {
options.unshift({ value, label: `${value}(历史值)` });
}
return options;
};
/** 礼包名对应的积分单价;缓存里没有 / 没价格时返回 null(后端核账时还会按名称补一次)。 */
/** 礼包名对应的积分:实时价格优先,其次标准字典;都没有时返回 null。 */
const giftRequiredPoints = (name?: string | null): number | null => {
const value = String(name ?? '').trim();
if (!value) return null;
return giftOptions.find((gift) => gift.name === value)?.price ?? null;
return giftOptions.find((gift) => gift.name === value)?.price
?? giftCatalog.find((gift) => gift.name === value)?.points
?? null;
};
/**
@@ -1863,8 +1890,11 @@ export default function HuyaCollabPage() {
* 点一个拉不到的名字等于白点(见设计文档 §11.4)。缓存为空时整行不显示。
*/
const frequentGiftChoices = (): HuyaCollabFrequentGift[] => {
if (giftOptions.length === 0) return [];
const available = new Set(giftOptions.map((gift) => gift.name));
const available = new Set([
...giftOptions.map((gift) => gift.name),
...giftCatalog.filter((gift) => gift.is_active).map((gift) => gift.name),
]);
if (available.size === 0) return [];
return frequentGifts.filter((item) => available.has(item.name)).slice(0, 6);
};
@@ -2107,7 +2137,7 @@ export default function HuyaCollabPage() {
// 前缀单独一行、礼包名完整换行,别让单行省略号把名字吃掉
labelRender={() => renderGiftSelectedLabel(row.gift_name)}
styles={{ content: { whiteSpace: 'normal', textOverflow: 'clip', lineHeight: 1.3 } }}
notFoundContent={giftOptions.length === 0 ? '礼包缓存为空:先在「新建」里贴 CK 拉一次' : '没有匹配的礼包'}
notFoundContent={hasSelectableGifts ? '没有匹配的礼包' : '实时礼包为空,标准字典也还没有配置'}
onChange={(next: string) => void patchOrder(row, {
gift_name: next,
required_points: giftRequiredPoints(next),
@@ -2528,6 +2558,9 @@ export default function HuyaCollabPage() {
{canEdit && !isArchiveView && (
<Button icon={<ShopOutlined />} onClick={() => setShopModalOpen(true)}>店铺管理</Button>
)}
{canManage && !isArchiveView && (
<Button icon={<GiftOutlined />} onClick={() => setGiftModalOpen(true)}>礼包管理</Button>
)}
{canEdit && !isArchiveView && (
<Button
type="primary"
@@ -2776,16 +2809,16 @@ export default function HuyaCollabPage() {
</Col>
<Col span={12}>
<Form.Item name="gift_name" label="礼包名"
tooltip="只接受礼包缓存里的点选结果,可输入关键字搜索;不接受手工输入">
tooltip="从实时活动礼包或管理员维护的标准字典里点选,可输入关键字搜索">
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder="输入关键字搜索,点选礼包"
options={giftSelectOptions(detail.gift_name)}
notFoundContent={giftOptions.length === 0
? '礼包缓存为空:先在「新建」里贴 CK 拉一次'
: '没有匹配的礼包'}
notFoundContent={hasSelectableGifts
? '没有匹配的礼包'
: '实时礼包为空,标准字典也还没有配置'}
onChange={(value?: string) => {
detailForm.setFieldValue('required_points', giftRequiredPoints(value));
}}
@@ -3117,20 +3150,27 @@ export default function HuyaCollabPage() {
<Form.Item
name="gift_name"
label="礼包名(用户购买的物品,可稍后补)"
tooltip="默认空白,不会自动带上次选的礼包(2026-10-18)。只接受礼包缓存里的点选结果,可输入关键字搜索;不选也能先建单,之后点「补资料」再选。"
extra={giftHint || (giftsCachedAt && giftOptions.length > 0
? `礼包列表缓存于 ${giftCacheLabel}(${giftOptions.length} 个)`
: undefined)}
tooltip="默认空白,不会自动带上次选的礼包。可从实时活动礼包或管理员维护的标准字典里点选;不选也能先建单,之后点「补资料」再选。"
extra={giftHint || [
giftsCachedAt && giftOptions.length > 0
? `实时礼包缓存于 ${giftCacheLabel}(${giftOptions.length} 个)`
: '',
giftCatalog.length > 0
? `标准字典 ${giftCatalog.filter((gift) => gift.is_active).length} 个可用`
: '',
].filter(Boolean).join(';') || undefined}
validateStatus={giftHint ? 'warning' : undefined}
>
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder={giftOptions.length > 0 ? '输入关键字搜索,点选礼包' : '礼包缓存为空:先贴 CK 拉取'}
placeholder={hasSelectableGifts ? '输入关键字搜索,点选礼包' : '请先在上方「礼包管理」配置标准礼包'}
options={giftSelectOptions(createGiftValue)}
disabled={giftOptions.length === 0 && !createGiftValue}
notFoundContent={giftOptions.length === 0 ? '先在左侧贴一份有效 CK 拉取礼包列表' : '没有匹配的礼包'}
disabled={!hasSelectableGifts && !createGiftValue}
notFoundContent={hasSelectableGifts
? '没有匹配的礼包'
: '实时礼包为空,标准字典也还没有配置'}
onChange={(value?: string) => {
// 选了就清掉「上次那个礼包不在本次列表里」的提示,并带出积分价格
setGiftHint('');
@@ -3256,17 +3296,17 @@ export default function HuyaCollabPage() {
}),
)}
<Form.Item name="gift_name" label="礼包名"
tooltip="只接受礼包缓存里的点选结果,可输入关键字搜索;不接受手工输入">
tooltip="从实时活动礼包或管理员维护的标准字典里点选,可输入关键字搜索">
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder={giftOptions.length > 0 ? '输入关键字搜索,点选礼包' : '礼包缓存为空:先在「新建」里贴 CK 拉一次'}
placeholder={hasSelectableGifts ? '输入关键字搜索,点选礼包' : '请先让管理员配置标准礼包'}
options={giftSelectOptions(supplementGiftValue ?? supplementTarget.gift_name)}
disabled={giftOptions.length === 0 && !supplementGiftValue}
notFoundContent={giftOptions.length === 0
? '礼包缓存为空:先在「新建」里贴 CK 拉一次'
: '没有匹配的礼包'}
disabled={!hasSelectableGifts && !supplementGiftValue}
notFoundContent={hasSelectableGifts
? '没有匹配的礼包'
: '实时礼包为空,标准字典也还没有配置'}
onChange={(value?: string) => {
supplementForm.setFieldValue('required_points', giftRequiredPoints(value));
}}
@@ -3587,6 +3627,13 @@ export default function HuyaCollabPage() {
onClose={() => setShopModalOpen(false)}
onChanged={() => void load()}
/>
<HuyaCollabGiftModal
open={giftModalOpen}
gifts={giftCatalog}
canManage={canManage}
onClose={() => setGiftModalOpen(false)}
onChanged={() => void load()}
/>
</Space>
);
}