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,
}));
const badgeTypeOptions = BADGE_TYPE_OPTIONS.filter((item) =>
['ACHIEVEMENT', 'ACTIVITY'].includes(String(item.value)),
['VIP', 'ACHIEVEMENT', 'ACTIVITY'].includes(String(item.value)),
).map((item) => ({
label: item.name,
value: item.value as any,

View File

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

View File

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