feat: support badge resources in admin

This commit is contained in:
zhx 2026-05-15 20:41:24 +08:00
parent 9ce6b450c1
commit 6e79730445
4 changed files with 254 additions and 99 deletions

View File

@ -245,18 +245,9 @@ async function loadSource(type: string) {
loading.value = true;
try {
switch (type) {
case 'BADGE':
case 'ROOM_BADGE': {
const badgeType = type === 'BADGE' ? 'ACTIVITY' : 'ROOM_ACHIEVEMENT';
const result = await listBadgePictureBySysOrigin(props.sysOrigin, badgeType);
store.list = (result || []).map((item) =>
normalizeSourceItem(item, type, {
coverKeys: ['selectUrl', 'cover', 'coverUrl'],
idKeys: ['badgeConfigId', 'id', 'badgeId'],
nameKeys: ['badgeName', 'name'],
sourceUrlKeys: ['animationUrl', 'sourceUrl', 'resourceUrl'],
}),
);
case 'BADGE': {
const result = await listSysOriginTypeList(props.sysOrigin, 'BADGE');
store.list = (result || []).map((item) => normalizeSourceItem(item, type));
break;
}
case 'EMOJI': {
@ -287,6 +278,21 @@ async function loadSource(type: string) {
store.list = (result || []).map((item) => normalizeSourceItem(item, type));
break;
}
case 'ROOM_BADGE': {
const result = await listBadgePictureBySysOrigin(
props.sysOrigin,
'ROOM_ACHIEVEMENT',
);
store.list = (result || []).map((item) =>
normalizeSourceItem(item, type, {
coverKeys: ['selectUrl', 'cover', 'coverUrl'],
idKeys: ['badgeConfigId', 'id', 'badgeId'],
nameKeys: ['badgeName', 'name'],
sourceUrlKeys: ['animationUrl', 'sourceUrl', 'resourceUrl'],
}),
);
break;
}
default: {
const result = await listSysOriginTypeList(props.sysOrigin, type);
store.list = (result || []).map((item) => normalizeSourceItem(item, type));

View File

@ -24,6 +24,7 @@ import {
} from '#/api/legacy/props';
import {
BADGE_TYPE_OPTIONS,
NOBLE_VIP_OPTIONS,
PROPS_RESOURCE_CONFIG_TYPES,
} from '../shared';
@ -43,19 +44,26 @@ const saving = ref(false);
const coverUploading = ref(false);
const sourceUploading = ref(false);
const expandUploading = ref(false);
const notSelectUploading = ref(false);
const coverInputRef = ref<HTMLInputElement>();
const sourceInputRef = ref<HTMLInputElement>();
const expandInputRef = ref<HTMLInputElement>();
const notSelectInputRef = ref<HTMLInputElement>();
const form = reactive<Record<string, any>>({
adminFree: false,
amount: '',
badgeKey: '',
badgeLevel: '',
badgeType: 'ACTIVITY',
code: '',
cover: '',
expand: '',
id: '',
milestone: '',
name: '',
notSelectUrl: '',
sourceUrl: '',
sysOrigin: '',
type: '',
@ -76,6 +84,9 @@ const isSourceRequired = computed(
);
const showExpandUpload = computed(() => form.type === 'CHAT_BUBBLE');
const sourceUploadLabel = computed(() => {
if (form.type === 'BADGE') {
return '动效资源(选填)';
}
if (showExpandUpload.value) {
return 'iOS资源';
}
@ -90,15 +101,65 @@ const nobleVipNameOptions = NOBLE_VIP_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
const badgeTypeOptions = BADGE_TYPE_OPTIONS.filter((item) =>
['ACHIEVEMENT', 'ACTIVITY'].includes(String(item.value)),
).map((item) => ({
label: item.name,
value: item.value as any,
}));
function parseBadgeExpand(value: unknown) {
if (!value || typeof value !== 'string') {
return {};
}
try {
return JSON.parse(value) as Record<string, any>;
} catch {
return {};
}
}
function applyBadgeExpand(value: unknown) {
const expand = parseBadgeExpand(value);
form.badgeKey = expand.badgeKey ?? '';
form.badgeLevel = expand.badgeLevel ?? '';
form.badgeType = expand.badgeType || 'ACTIVITY';
form.milestone = expand.milestone ?? '';
form.notSelectUrl = expand.notSelectUrl ?? '';
}
function buildBadgeExpand() {
const expand: Record<string, any> = {
badgeType: form.badgeType || 'ACTIVITY',
};
if (String(form.badgeKey || '').trim()) {
expand.badgeKey = String(form.badgeKey).trim();
}
if (String(form.badgeLevel || '').trim()) {
expand.badgeLevel = Number(form.badgeLevel);
}
if (String(form.milestone || '').trim()) {
expand.milestone = Number(form.milestone);
}
if (String(form.notSelectUrl || '').trim()) {
expand.notSelectUrl = String(form.notSelectUrl).trim();
}
return JSON.stringify(expand);
}
function resetForm() {
form.adminFree = false;
form.amount = '';
form.badgeKey = '';
form.badgeLevel = '';
form.badgeType = 'ACTIVITY';
form.code = '';
form.cover = '';
form.expand = '';
form.id = '';
form.milestone = '';
form.name = '';
form.notSelectUrl = '';
form.sourceUrl = '';
form.sysOrigin = props.sysOriginOptions[0]?.value ?? 'LIKEI';
form.type = PROPS_RESOURCE_CONFIG_TYPES[0]?.value ?? '';
@ -125,6 +186,11 @@ watch(
form.sourceUrl = record.sourceUrl ?? '';
form.sysOrigin = record.sysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI';
form.type = record.type ?? '';
if (form.type === 'BADGE') {
applyBadgeExpand(record.expand);
} else {
applyBadgeExpand('');
}
},
{ immediate: true },
);
@ -143,7 +209,10 @@ watch(
form.sourceUrl = '';
form.expand = '';
}
if (value !== 'CHAT_BUBBLE') {
if (value === 'BADGE' && !form.badgeType) {
form.badgeType = 'ACTIVITY';
}
if (!['BADGE', 'CHAT_BUBBLE'].includes(value)) {
form.expand = '';
}
},
@ -161,6 +230,10 @@ function openExpandPicker() {
expandInputRef.value?.click();
}
function openNotSelectPicker() {
notSelectInputRef.value?.click();
}
function handleCoverInput(event: Event) {
void uploadFile(event, 'cover', coverUploading, true);
}
@ -173,9 +246,13 @@ function handleExpandInput(event: Event) {
void uploadFile(event, 'expand', expandUploading, true);
}
function handleNotSelectInput(event: Event) {
void uploadFile(event, 'notSelectUrl', notSelectUploading, true);
}
async function uploadFile(
event: Event,
targetKey: 'cover' | 'expand' | 'sourceUrl',
targetKey: 'cover' | 'expand' | 'notSelectUrl' | 'sourceUrl',
loadingRef: typeof coverUploading,
imageBucket = false,
) {
@ -207,6 +284,26 @@ function validateForm() {
message.warning('请选择类型');
return false;
}
if (form.type === 'BADGE' && !form.badgeType) {
message.warning('请选择徽章类型');
return false;
}
if (
form.type === 'BADGE' &&
String(form.badgeLevel || '').trim() &&
Number.isNaN(Number(form.badgeLevel))
) {
message.warning('请输入正确的徽章等级');
return false;
}
if (
form.type === 'BADGE' &&
String(form.milestone || '').trim() &&
Number.isNaN(Number(form.milestone))
) {
message.warning('请输入正确的成就里程碑');
return false;
}
if (!String(form.cover || '').trim()) {
message.warning('请上传封面');
return false;
@ -241,8 +338,16 @@ async function handleSubmit() {
saving.value = true;
try {
const payload = {
...form,
adminFree: form.adminFree,
amount: isAmountRequired.value ? Number(form.amount) : 0,
code: form.code,
cover: form.cover,
expand: form.type === 'BADGE' ? buildBadgeExpand() : form.expand,
id: form.id,
name: form.name,
sourceUrl: form.sourceUrl,
sysOrigin: form.sysOrigin,
type: form.type,
};
await (isUpdate.value ? updatePropsSource(payload) : addPropsSource(payload));
message.success('保存成功');
@ -278,6 +383,24 @@ async function handleSubmit() {
:options="propsTypeOptions"
/>
</FormItem>
<template v-if="form.type === 'BADGE'">
<FormItem label="徽章类型">
<Select
v-model:value="form.badgeType"
:options="badgeTypeOptions"
option-label-prop="label"
/>
</FormItem>
<FormItem label="徽章等级">
<Input v-model:value="form.badgeLevel" placeholder="无等级可填 0" />
</FormItem>
<FormItem label="徽章 Key">
<Input v-model:value="form.badgeKey" placeholder="不填则使用编码" />
</FormItem>
<FormItem label="成就里程碑">
<Input v-model:value="form.milestone" placeholder="活动徽章可不填" />
</FormItem>
</template>
<FormItem label="封面">
<input
ref="coverInputRef"
@ -296,6 +419,24 @@ async function handleSubmit() {
</Button>
</div>
</FormItem>
<FormItem v-if="form.type === 'BADGE'" label="未选中图(选填)">
<input
ref="notSelectInputRef"
accept="image/*"
class="hidden-input"
type="file"
@change="handleNotSelectInput"
/>
<div class="upload-field">
<div v-if="form.notSelectUrl" class="upload-preview">
<Image class="upload-preview__image" :src="form.notSelectUrl" />
</div>
<div v-else class="upload-empty">未选中图可不上传</div>
<Button :loading="notSelectUploading" @click="openNotSelectPicker">
{{ form.notSelectUrl ? '重新上传' : '上传未选中图' }}
</Button>
</div>
</FormItem>
<FormItem v-if="showSourceUpload" :label="sourceUploadLabel">
<input
ref="sourceInputRef"

View File

@ -1,6 +1,18 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import {
Button,
Drawer,
Form,
FormItem,
Input,
message,
Select,
Space,
Switch,
} from 'antdv-next';
import { listEmojiGroupsBySysOrigin } from '#/api/legacy/app-system';
import { listBadgePictureBySysOrigin } from '#/api/legacy/badge';
import { listGiftBySysOrigin } from '#/api/legacy/gift';
@ -9,24 +21,9 @@ import {
listSysOriginTypeList,
saveOrUpdatePropsActivityRewardGroup,
} from '#/api/legacy/props';
import {
Button,
Drawer,
Form,
FormItem,
Input,
Select,
Space,
Switch,
message,
} from 'antdv-next';
import { getDisplaySysOriginText } from '#/views/system/shared';
import RewardIcon from './reward-icon.vue';
import {
PROPS_SOURCE_GROUP_ADD_TYPES,
PROPS_SOURCE_GROUP_TYPE_MAP,
formatRewardText,
getPropsTypeName,
getRewardBadgeType,
@ -34,7 +31,10 @@ import {
isCurrencyRewardType,
isGameCouponType,
isPropsCouponType,
PROPS_SOURCE_GROUP_ADD_TYPES,
PROPS_SOURCE_GROUP_TYPE_MAP,
} from '../shared';
import RewardIcon from './reward-icon.vue';
const props = defineProps<{
open: boolean;
@ -104,7 +104,8 @@ const selectedDraftResource = ref<DraftSourceOption | null>(null);
const title = computed(() => {
const sysOrigin = getDisplaySysOriginText(props.sysOrigin || form.sysOrigin);
return sysOrigin ? `${form.id ? '修改' : '添加'}${sysOrigin}` : form.id ? '修改' : '添加';
const action = form.id ? '修改' : '添加';
return sysOrigin ? `${action}${sysOrigin}` : action;
});
const rewardItems = computed(() => form.rewardConfigList);
@ -194,7 +195,7 @@ watch(
function needsRemoteOptions(type: string) {
return !(
['SPECIAL_ID', 'GOLD', 'DIAMOND', 'GAME_COUPON', 'PROP_COUPON'].includes(type)
['DIAMOND', 'GAME_COUPON', 'GOLD', 'PROP_COUPON', 'SPECIAL_ID'].includes(type)
);
}
@ -209,49 +210,70 @@ async function loadOptions(type: string) {
store.loading = true;
draftLoading.value = true;
try {
if (type === 'GIFT') {
const list = await listGiftBySysOrigin(form.sysOrigin);
store.list = list.map((item) => ({
cover: item.giftPhoto || '',
id: item.id ?? '',
name: item.giftName || '',
sourceUrl: item.giftSourceUrl || '',
}));
} else if (type === 'EMOJI') {
const list = await listEmojiGroupsBySysOrigin(form.sysOrigin);
store.list = list.map((item) => ({
cover: item.cover || '',
id: item.id ?? '',
name: item.groupName || '',
sourceUrl: '',
}));
} else if (isBadgeRewardType(type)) {
const badgeType = getRewardBadgeType(type);
const list = await listBadgePictureBySysOrigin(form.sysOrigin, badgeType);
store.list = list.map((item) => ({
badgeName: item.badgeName || '',
cover: item.selectUrl || '',
id: item.badgeConfigId ?? '',
name: item.badgeName || '',
sourceUrl: item.animationUrl || '',
}));
} else if (type === 'NOBLE_VIP') {
const list = await listNotFamilyBySysOriginType(form.sysOrigin, type);
store.list = list.map((item) => ({
cover: item.cover || '',
id: item.id ?? '',
name: item.name || '',
sourceUrl: item.sourceUrl || '',
}));
} else {
const list = await listSysOriginTypeList(form.sysOrigin, type);
store.list = list.map((item) => ({
cover: item.cover || '',
id: item.id ?? '',
name: item.name || '',
remark: item.remark || '',
sourceUrl: item.sourceUrl || '',
}));
switch (type) {
case 'BADGE': {
const list = await listSysOriginTypeList(form.sysOrigin, 'BADGE');
store.list = list.map((item) => ({
badgeName: item.name || '',
cover: item.cover || '',
id: item.id ?? '',
name: item.name || '',
sourceUrl: item.sourceUrl || '',
}));
break;
}
case 'EMOJI': {
const list = await listEmojiGroupsBySysOrigin(form.sysOrigin);
store.list = list.map((item) => ({
cover: item.cover || '',
id: item.id ?? '',
name: item.groupName || '',
sourceUrl: '',
}));
break;
}
case 'GIFT': {
const list = await listGiftBySysOrigin(form.sysOrigin);
store.list = list.map((item) => ({
cover: item.giftPhoto || '',
id: item.id ?? '',
name: item.giftName || '',
sourceUrl: item.giftSourceUrl || '',
}));
break;
}
case 'NOBLE_VIP': {
const list = await listNotFamilyBySysOriginType(form.sysOrigin, type);
store.list = list.map((item) => ({
cover: item.cover || '',
id: item.id ?? '',
name: item.name || '',
sourceUrl: item.sourceUrl || '',
}));
break;
}
default: {
if (isBadgeRewardType(type)) {
const badgeType = getRewardBadgeType(type);
const list = await listBadgePictureBySysOrigin(form.sysOrigin, badgeType);
store.list = list.map((item) => ({
badgeName: item.badgeName || '',
cover: item.selectUrl || '',
id: item.badgeConfigId ?? '',
name: item.badgeName || '',
sourceUrl: item.animationUrl || '',
}));
break;
}
const list = await listSysOriginTypeList(form.sysOrigin, type);
store.list = list.map((item) => ({
cover: item.cover || '',
id: item.id ?? '',
name: item.name || '',
remark: item.remark || '',
sourceUrl: item.sourceUrl || '',
}));
}
}
store.loaded = true;
} finally {
@ -266,7 +288,7 @@ async function handleDraftTypeChange(value: string) {
await loadOptions(value);
}
function handleDraftContentChange(value: string | number) {
function handleDraftContentChange(value: number | string) {
if (!draft.type || !needsRemoteOptions(draft.type)) {
return;
}

View File

@ -1,13 +1,6 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { listBadgePictureBySysOrigin } from '#/api/legacy/badge';
import { listGiftBySysOrigin } from '#/api/legacy/gift';
import {
listNotFamilyBySysOriginType,
pagePropsSource,
} from '#/api/legacy/props';
import {
Button,
Drawer,
@ -18,7 +11,11 @@ import {
Spin,
} from 'antdv-next';
defineOptions({ name: 'ResidentActivityResourceSelectDrawer' });
import { listGiftBySysOrigin } from '#/api/legacy/gift';
import {
listNotFamilyBySysOriginType,
pagePropsSource,
} from '#/api/legacy/props';
type ResourceItem = {
amount?: number | string;
@ -29,6 +26,8 @@ type ResourceItem = {
type: string;
};
defineOptions({ name: 'ResidentActivityResourceSelectDrawer' });
const props = withDefaults(
defineProps<{
defaultType?: string;
@ -62,7 +61,7 @@ const typeOptions = [
{ label: '徽章', value: 'BADGE' },
].map((item) => ({ ...item, value: item.value as any }));
const listOnlyTypes = new Set(['BADGE', 'GIFT', 'NOBLE_VIP']);
const listOnlyTypes = new Set(['GIFT', 'NOBLE_VIP']);
const loading = ref(false);
const list = ref<ResourceItem[]>([]);
@ -181,19 +180,6 @@ async function loadListType(type: string) {
applyLocalPage();
return;
}
if (type === 'BADGE') {
const result = await listBadgePictureBySysOrigin(props.sysOrigin, 'ACTIVITY');
localRows.value = (result || []).map((item) =>
normalizeResource(item, type, {
coverKeys: ['selectUrl', 'cover'],
idKeys: ['badgeConfigId', 'id'],
nameKeys: ['badgeName', 'name'],
sourceUrlKeys: ['animationUrl', 'sourceUrl'],
}),
);
applyLocalPage();
return;
}
if (type === 'NOBLE_VIP') {
const result = await listNotFamilyBySysOriginType(props.sysOrigin, type);
localRows.value = (result || []).map((item) =>
@ -371,7 +357,7 @@ watch(
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
@show-size-change="handlePageChange"
/>
</div>
</Drawer>