feat: extend VIP resource config

This commit is contained in:
zhx 2026-05-16 02:21:43 +08:00
parent 17e81a9759
commit b8bbaec2ac
3 changed files with 56 additions and 9 deletions

View File

@ -102,7 +102,7 @@ const nobleVipNameOptions = NOBLE_VIP_OPTIONS.map((item) => ({
value: item.value as any, value: item.value as any,
})); }));
const badgeTypeOptions = BADGE_TYPE_OPTIONS.filter((item) => const badgeTypeOptions = BADGE_TYPE_OPTIONS.filter((item) =>
['ACHIEVEMENT', 'ACTIVITY'].includes(String(item.value)), ['VIP', 'ACHIEVEMENT', 'ACTIVITY'].includes(String(item.value)),
).map((item) => ({ ).map((item) => ({
label: item.name, label: item.name,
value: item.value as any, value: item.value as any,

View File

@ -21,6 +21,7 @@ export const PROPS_TYPES: OptionItem[] = [
export const PROPS_RESOURCE_CONFIG_TYPES: OptionItem[] = [ export const PROPS_RESOURCE_CONFIG_TYPES: OptionItem[] = [
...PROPS_TYPES, ...PROPS_TYPES,
{ value: 'VIP_EFFECT_IMAGE', name: 'VIP动效图' },
{ value: 'BADGE', name: '徽章' }, { value: 'BADGE', name: '徽章' },
]; ];
@ -64,6 +65,7 @@ export const NOBLE_VIP_OPTIONS: OptionItem[] = [
]; ];
export const BADGE_TYPE_OPTIONS: OptionItem[] = [ export const BADGE_TYPE_OPTIONS: OptionItem[] = [
{ value: 'VIP', name: '用户-VIP徽章' },
{ value: 'ACHIEVEMENT', name: '用户-成就徽章' }, { value: 'ACHIEVEMENT', name: '用户-成就徽章' },
{ value: 'ADMINISTRATOR', name: '用户-管理员' }, { value: 'ADMINISTRATOR', name: '用户-管理员' },
{ value: 'ACTIVITY', name: '用户-活动徽章' }, { value: 'ACTIVITY', name: '用户-活动徽章' },
@ -128,6 +130,7 @@ export const PROPS_SOURCE_GROUP_TYPE_MAP: Record<
CUSTOMIZE: { value: 'CUSTOMIZE', name: '自定义(只读)' }, CUSTOMIZE: { value: 'CUSTOMIZE', name: '自定义(只读)' },
FRAGMENTS: { value: 'FRAGMENTS', name: '碎片' }, FRAGMENTS: { value: 'FRAGMENTS', name: '碎片' },
DATA_CARD: { value: 'PROPS', name: '资料卡' }, DATA_CARD: { value: 'PROPS', name: '资料卡' },
VIP_EFFECT_IMAGE: { value: 'PROPS', name: 'VIP动效图' },
PROP_COUPON: { value: 'PROP_COUPON', name: '道具券' }, PROP_COUPON: { value: 'PROP_COUPON', name: '道具券' },
HONOR_ACTIVITY: { value: 'BADGE', name: '荣誉-活动' }, HONOR_ACTIVITY: { value: 'BADGE', name: '荣誉-活动' },
}; };
@ -290,6 +293,7 @@ export function isPropsSourceType(type?: null | string) {
'NOBLE_VIP', 'NOBLE_VIP',
'RIDE', 'RIDE',
'THEME', 'THEME',
'VIP_EFFECT_IMAGE',
].includes(String(type || '')); ].includes(String(type || ''));
} }

View File

@ -32,11 +32,14 @@ import {
defineOptions({ name: 'ResidentVipConfigPage' }); defineOptions({ name: 'ResidentVipConfigPage' });
const resourceFields = [ const resourceFields = [
{ key: 'badge', title: '徽章', type: 'BADGE' }, { key: 'longBadge', title: '长徽章素材', type: 'BADGE', badgeTypes: ['VIP'] },
{ key: 'shortBadge', title: '短徽章素材', type: 'BADGE', badgeTypes: ['ACTIVITY', 'ACHIEVEMENT'] },
{ key: 'avatarFrame', title: '头像框', type: 'AVATAR_FRAME' }, { key: 'avatarFrame', title: '头像框', type: 'AVATAR_FRAME' },
{ key: 'entryEffect', title: '进场特效', type: 'RIDE' }, { key: 'entryEffect', title: '进场特效', type: 'RIDE' },
{ key: 'chatBubble', title: '聊天气泡', type: 'CHAT_BUBBLE' }, { key: 'chatBubble', title: '聊天气泡', type: 'CHAT_BUBBLE' },
{ key: 'floatPicture', title: '飘窗', type: 'FLOAT_PICTURE' }, { key: 'floatPicture', title: '飘窗', type: 'FLOAT_PICTURE' },
{ key: 'backgroundCard', title: '背景卡素材', type: 'DATA_CARD' },
{ key: 'effectImage', title: '动效图', type: 'VIP_EFFECT_IMAGE' },
] as const; ] as const;
type ResourceField = (typeof resourceFields)[number]; type ResourceField = (typeof resourceFields)[number];
@ -163,16 +166,19 @@ function createResource() {
function createLevel(level: number) { function createLevel(level: number) {
return { return {
avatarFrame: createResource(), avatarFrame: createResource(),
badge: createResource(), backgroundCard: createResource(),
chatBubble: createResource(), chatBubble: createResource(),
displayName: `VIP ${level}`, displayName: `VIP ${level}`,
durationDays: 30, durationDays: 30,
enabled: false, enabled: false,
entryEffect: createResource(), entryEffect: createResource(),
effectImage: createResource(),
floatPicture: createResource(), floatPicture: createResource(),
level, level,
levelCode: `VIP${level}`, levelCode: `VIP${level}`,
longBadge: createResource(),
priceGold: 0, priceGold: 0,
shortBadge: createResource(),
}; };
} }
@ -282,12 +288,42 @@ async function loadResourceOptions(field: ResourceField) {
} }
resourceLoading[field.key] = true; resourceLoading[field.key] = true;
try { try {
resourceRows[field.key] = await listSysOriginTypeList(form.sysOrigin, field.type); resourceRows[field.key] = filterResourceRowsByField(
await listSysOriginTypeList(form.sysOrigin, field.type),
field,
);
} finally { } finally {
resourceLoading[field.key] = false; resourceLoading[field.key] = false;
} }
} }
function filterResourceRowsByField(rows: Array<Record<string, any>>, field: ResourceField) {
if (!('badgeTypes' in field) || !field.badgeTypes?.length) {
return rows;
}
const allowed = new Set(field.badgeTypes.map((item) => String(item).toUpperCase()));
return rows.filter((item) => allowed.has(resourceBadgeType(item)));
}
function resourceBadgeType(value: Record<string, any>) {
const expand = parseJsonObject(value.expand);
return String(expand.badgeType || 'ACTIVITY').toUpperCase();
}
function parseJsonObject(value: unknown) {
if (typeof value !== 'string' || !value.trim()) {
return {} as Record<string, any>;
}
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, any>)
: ({} as Record<string, any>);
} catch {
return {} as Record<string, any>;
}
}
async function loadResourceOptionsByKey(key: string) { async function loadResourceOptionsByKey(key: string) {
const field = findResourceField(key); const field = findResourceField(key);
if (field) { if (field) {
@ -329,17 +365,20 @@ function normalizeLevels(levels: Array<Record<string, any>> | undefined) {
} }
return Array.from({ length: 5 }, (_, index) => { return Array.from({ length: 5 }, (_, index) => {
const level = index + 1; const level = index + 1;
const item = levelMap.get(level) || createLevel(level); const item = (levelMap.get(level) || createLevel(level)) as Record<string, any>;
return { return {
...createLevel(level), ...createLevel(level),
displayName: String(item.displayName || `VIP ${level}`), displayName: String(item.displayName || `VIP ${level}`),
enabled: Boolean(item.enabled), enabled: Boolean(item.enabled),
priceGold: Number(item.priceGold || 0), priceGold: Number(item.priceGold || 0),
badge: normalizeResource(item.badge), longBadge: normalizeResource(item.longBadge || item['badge']),
shortBadge: normalizeResource(item.shortBadge),
avatarFrame: normalizeResource(item.avatarFrame), avatarFrame: normalizeResource(item.avatarFrame),
entryEffect: normalizeResource(item.entryEffect), entryEffect: normalizeResource(item.entryEffect),
chatBubble: normalizeResource(item.chatBubble), chatBubble: normalizeResource(item.chatBubble),
floatPicture: normalizeResource(item.floatPicture), floatPicture: normalizeResource(item.floatPicture),
backgroundCard: normalizeResource(item.backgroundCard),
effectImage: normalizeResource(item.effectImage),
}; };
}); });
} }
@ -357,16 +396,20 @@ function buildSavePayload() {
sysOrigin: form.sysOrigin, sysOrigin: form.sysOrigin,
levels: (form.levels || []).map((item: Record<string, any>) => ({ levels: (form.levels || []).map((item: Record<string, any>) => ({
avatarFrame: buildResourceSavePayload(item.avatarFrame), avatarFrame: buildResourceSavePayload(item.avatarFrame),
badge: buildResourceSavePayload(item.badge), badge: buildResourceSavePayload(item.longBadge),
backgroundCard: buildResourceSavePayload(item.backgroundCard),
chatBubble: buildResourceSavePayload(item.chatBubble), chatBubble: buildResourceSavePayload(item.chatBubble),
displayName: String(item.displayName || '').trim(), displayName: String(item.displayName || '').trim(),
durationDays: 30, durationDays: 30,
enabled: Boolean(item.enabled), enabled: Boolean(item.enabled),
entryEffect: buildResourceSavePayload(item.entryEffect), entryEffect: buildResourceSavePayload(item.entryEffect),
effectImage: buildResourceSavePayload(item.effectImage),
floatPicture: buildResourceSavePayload(item.floatPicture), floatPicture: buildResourceSavePayload(item.floatPicture),
level: Number(item.level || 0), level: Number(item.level || 0),
levelCode: `VIP${Number(item.level || 0)}`, levelCode: `VIP${Number(item.level || 0)}`,
longBadge: buildResourceSavePayload(item.longBadge),
priceGold: Number(item.priceGold || 0), priceGold: Number(item.priceGold || 0),
shortBadge: buildResourceSavePayload(item.shortBadge),
})), })),
}; };
} }
@ -551,7 +594,7 @@ watch(
:data-source="form.levels" :data-source="form.levels"
:pagination="false" :pagination="false"
row-key="level" row-key="level"
:scroll="{ x: 1700 }" :scroll="{ x: 2600 }"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'"> <template v-if="column.key === 'level'">
@ -600,7 +643,7 @@ watch(
<div>固定 5 个等级每次购买 30 同等级再次购买会续 30 </div> <div>固定 5 个等级每次购买 30 同等级再次购买会续 30 </div>
<div>资源从道具资源配置选择保存时会记录当前资源快照</div> <div>资源从道具资源配置选择保存时会记录当前资源快照</div>
<div>用户只能升级升级时支付目标等级和当前等级的差价到期后资源自动失效</div> <div>用户只能升级升级时支付目标等级和当前等级的差价到期后资源自动失效</div>
<div>升级成功后会把徽章头像框进场特效聊天气泡和飘窗替换成新等级资源</div> <div>升级成功后会把长徽章徽章头像框进场特效聊天气泡飘窗和背景卡替换成新等级资源动效图只用于 VIP 页面展示</div>
</div> </div>
<Space class="actions" wrap> <Space class="actions" wrap>