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; loading.value = true;
try { try {
switch (type) { switch (type) {
case 'BADGE': case 'BADGE': {
case 'ROOM_BADGE': { const result = await listSysOriginTypeList(props.sysOrigin, 'BADGE');
const badgeType = type === 'BADGE' ? 'ACTIVITY' : 'ROOM_ACHIEVEMENT'; store.list = (result || []).map((item) => normalizeSourceItem(item, type));
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'],
}),
);
break; break;
} }
case 'EMOJI': { case 'EMOJI': {
@ -287,6 +278,21 @@ async function loadSource(type: string) {
store.list = (result || []).map((item) => normalizeSourceItem(item, type)); store.list = (result || []).map((item) => normalizeSourceItem(item, type));
break; 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: { default: {
const result = await listSysOriginTypeList(props.sysOrigin, type); const result = await listSysOriginTypeList(props.sysOrigin, type);
store.list = (result || []).map((item) => normalizeSourceItem(item, type)); store.list = (result || []).map((item) => normalizeSourceItem(item, type));

View File

@ -24,6 +24,7 @@ import {
} from '#/api/legacy/props'; } from '#/api/legacy/props';
import { import {
BADGE_TYPE_OPTIONS,
NOBLE_VIP_OPTIONS, NOBLE_VIP_OPTIONS,
PROPS_RESOURCE_CONFIG_TYPES, PROPS_RESOURCE_CONFIG_TYPES,
} from '../shared'; } from '../shared';
@ -43,19 +44,26 @@ const saving = ref(false);
const coverUploading = ref(false); const coverUploading = ref(false);
const sourceUploading = ref(false); const sourceUploading = ref(false);
const expandUploading = ref(false); const expandUploading = ref(false);
const notSelectUploading = ref(false);
const coverInputRef = ref<HTMLInputElement>(); const coverInputRef = ref<HTMLInputElement>();
const sourceInputRef = ref<HTMLInputElement>(); const sourceInputRef = ref<HTMLInputElement>();
const expandInputRef = ref<HTMLInputElement>(); const expandInputRef = ref<HTMLInputElement>();
const notSelectInputRef = ref<HTMLInputElement>();
const form = reactive<Record<string, any>>({ const form = reactive<Record<string, any>>({
adminFree: false, adminFree: false,
amount: '', amount: '',
badgeKey: '',
badgeLevel: '',
badgeType: 'ACTIVITY',
code: '', code: '',
cover: '', cover: '',
expand: '', expand: '',
id: '', id: '',
milestone: '',
name: '', name: '',
notSelectUrl: '',
sourceUrl: '', sourceUrl: '',
sysOrigin: '', sysOrigin: '',
type: '', type: '',
@ -76,6 +84,9 @@ const isSourceRequired = computed(
); );
const showExpandUpload = computed(() => form.type === 'CHAT_BUBBLE'); const showExpandUpload = computed(() => form.type === 'CHAT_BUBBLE');
const sourceUploadLabel = computed(() => { const sourceUploadLabel = computed(() => {
if (form.type === 'BADGE') {
return '动效资源(选填)';
}
if (showExpandUpload.value) { if (showExpandUpload.value) {
return 'iOS资源'; return 'iOS资源';
} }
@ -90,15 +101,65 @@ const nobleVipNameOptions = NOBLE_VIP_OPTIONS.map((item) => ({
label: item.name, label: item.name,
value: item.value as any, 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() { function resetForm() {
form.adminFree = false; form.adminFree = false;
form.amount = ''; form.amount = '';
form.badgeKey = '';
form.badgeLevel = '';
form.badgeType = 'ACTIVITY';
form.code = ''; form.code = '';
form.cover = ''; form.cover = '';
form.expand = ''; form.expand = '';
form.id = ''; form.id = '';
form.milestone = '';
form.name = ''; form.name = '';
form.notSelectUrl = '';
form.sourceUrl = ''; form.sourceUrl = '';
form.sysOrigin = props.sysOriginOptions[0]?.value ?? 'LIKEI'; form.sysOrigin = props.sysOriginOptions[0]?.value ?? 'LIKEI';
form.type = PROPS_RESOURCE_CONFIG_TYPES[0]?.value ?? ''; form.type = PROPS_RESOURCE_CONFIG_TYPES[0]?.value ?? '';
@ -125,6 +186,11 @@ watch(
form.sourceUrl = record.sourceUrl ?? ''; form.sourceUrl = record.sourceUrl ?? '';
form.sysOrigin = record.sysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI'; form.sysOrigin = record.sysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI';
form.type = record.type ?? ''; form.type = record.type ?? '';
if (form.type === 'BADGE') {
applyBadgeExpand(record.expand);
} else {
applyBadgeExpand('');
}
}, },
{ immediate: true }, { immediate: true },
); );
@ -143,7 +209,10 @@ watch(
form.sourceUrl = ''; form.sourceUrl = '';
form.expand = ''; form.expand = '';
} }
if (value !== 'CHAT_BUBBLE') { if (value === 'BADGE' && !form.badgeType) {
form.badgeType = 'ACTIVITY';
}
if (!['BADGE', 'CHAT_BUBBLE'].includes(value)) {
form.expand = ''; form.expand = '';
} }
}, },
@ -161,6 +230,10 @@ function openExpandPicker() {
expandInputRef.value?.click(); expandInputRef.value?.click();
} }
function openNotSelectPicker() {
notSelectInputRef.value?.click();
}
function handleCoverInput(event: Event) { function handleCoverInput(event: Event) {
void uploadFile(event, 'cover', coverUploading, true); void uploadFile(event, 'cover', coverUploading, true);
} }
@ -173,9 +246,13 @@ function handleExpandInput(event: Event) {
void uploadFile(event, 'expand', expandUploading, true); void uploadFile(event, 'expand', expandUploading, true);
} }
function handleNotSelectInput(event: Event) {
void uploadFile(event, 'notSelectUrl', notSelectUploading, true);
}
async function uploadFile( async function uploadFile(
event: Event, event: Event,
targetKey: 'cover' | 'expand' | 'sourceUrl', targetKey: 'cover' | 'expand' | 'notSelectUrl' | 'sourceUrl',
loadingRef: typeof coverUploading, loadingRef: typeof coverUploading,
imageBucket = false, imageBucket = false,
) { ) {
@ -207,6 +284,26 @@ function validateForm() {
message.warning('请选择类型'); message.warning('请选择类型');
return false; 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()) { if (!String(form.cover || '').trim()) {
message.warning('请上传封面'); message.warning('请上传封面');
return false; return false;
@ -241,8 +338,16 @@ async function handleSubmit() {
saving.value = true; saving.value = true;
try { try {
const payload = { const payload = {
...form, adminFree: form.adminFree,
amount: isAmountRequired.value ? Number(form.amount) : 0, 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)); await (isUpdate.value ? updatePropsSource(payload) : addPropsSource(payload));
message.success('保存成功'); message.success('保存成功');
@ -278,6 +383,24 @@ async function handleSubmit() {
:options="propsTypeOptions" :options="propsTypeOptions"
/> />
</FormItem> </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="封面"> <FormItem label="封面">
<input <input
ref="coverInputRef" ref="coverInputRef"
@ -296,6 +419,24 @@ async function handleSubmit() {
</Button> </Button>
</div> </div>
</FormItem> </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"> <FormItem v-if="showSourceUpload" :label="sourceUploadLabel">
<input <input
ref="sourceInputRef" ref="sourceInputRef"

View File

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

View File

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