diff --git a/apps/src/views/resident-activity/voice-room-rocket.vue b/apps/src/views/resident-activity/voice-room-rocket.vue index f8be460..a04f794 100644 --- a/apps/src/views/resident-activity/voice-room-rocket.vue +++ b/apps/src/views/resident-activity/voice-room-rocket.vue @@ -29,6 +29,12 @@ import { saveVoiceRoomRocketLevelConfigs, saveVoiceRoomRocketRewardConfigs, } from '#/api/legacy/resident-activity'; +import { listBadgePictureBySysOrigin } from '#/api/legacy/badge'; +import { listGiftBySysOrigin } from '#/api/legacy/gift'; +import { + listNotFamilyBySysOriginType, + listSysOriginTypeList, +} from '#/api/legacy/props'; import { getAllowedSysOrigins } from '#/views/system/shared'; defineOptions({ name: 'ResidentVoiceRoomRocketPage' }); @@ -41,7 +47,6 @@ type LevelRow = { id?: number | string; level: number; needEnergy: number; - progressBarUrl: string; rocketAnimationUrl: string; rocketIconUrl: string; shakeThresholdPercent: string; @@ -64,6 +69,16 @@ type RewardRow = { weight: number; }; +type ResourceOption = { + amount?: number | string; + cover: string; + id: number | string; + name: string; + remark?: string; + sourceUrl: string; + type: string; +}; + const accessStore = useAccessStore(); const sysOriginOptions = computed(() => { const options = getAllowedSysOrigins(accessStore.accessCodes || []); @@ -80,6 +95,12 @@ const rewardLoading = ref(false); const configSaving = ref(false); const levelSaving = ref(false); const rewardSaving = ref(false); +const rocketResourceLoading = ref(false); +const rocketResourceRows = ref([]); +const rewardResourceLoading = reactive>({}); +const rewardResourceRows = reactive>({}); + +const ROCKET_RESOURCE_TYPE = 'CUSTOMIZE'; const configForm = reactive({ broadcastDurationSeconds: 7, @@ -113,6 +134,7 @@ const rewardTypeOptions = [ { label: 'VIP', value: 'VIP_CARD' }, { label: '礼物', value: 'GIFT' }, { label: '徽章', value: 'BADGE' }, + { label: '轮空', value: 'NONE' }, ]; const levelOptions = computed(() => @@ -122,6 +144,12 @@ const levelOptions = computed(() => })), ); +function rewardTypeOptionsForRow(row: Record) { + return row.rewardScene === 'IN_ROOM' + ? rewardTypeOptions + : rewardTypeOptions.filter((item) => item.value !== 'NONE'); +} + const statusMeta = computed(() => { if (!configForm.configured) { return { color: 'warning', text: '未配置' }; @@ -134,10 +162,14 @@ const statusMeta = computed(() => { const levelColumns = [ { dataIndex: 'level', key: 'level', title: '等级', width: 90 }, { dataIndex: 'needEnergy', key: 'needEnergy', title: '所需能量', width: 160 }, - { dataIndex: 'shakeThresholdPercent', key: 'shakeThresholdPercent', title: '晃动阈值(%)', width: 160 }, { dataIndex: 'rocketIconUrl', key: 'rocketIconUrl', title: '火箭图标', width: 260 }, - { dataIndex: 'rocketAnimationUrl', key: 'rocketAnimationUrl', title: '发射动画', width: 260 }, - { dataIndex: 'progressBarUrl', key: 'progressBarUrl', title: '进度条资源', width: 260 }, + { + dataIndex: 'rocketAnimationUrl', + key: 'rocketAnimationUrl', + title: '发射动画', + width: 260, + }, + { dataIndex: 'shakeThresholdPercent', key: 'shakeThresholdPercent', title: '晃动阈值(%)', width: 160 }, { dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 }, { dataIndex: 'sort', key: 'sort', title: '排序', width: 100 }, ]; @@ -145,9 +177,9 @@ const levelColumns = [ const rewardColumns = [ { dataIndex: 'rewardScene', key: 'rewardScene', title: '奖励场景', width: 150 }, { dataIndex: 'rewardType', key: 'rewardType', title: '奖励类型', width: 150 }, - { dataIndex: 'rewardItemId', key: 'rewardItemId', title: '资源ID', width: 170 }, + { dataIndex: 'rewardItemId', key: 'rewardItemId', title: '资源', width: 220 }, { dataIndex: 'rewardName', key: 'rewardName', title: '奖励名称', width: 180 }, - { dataIndex: 'rewardCover', key: 'rewardCover', title: '封面URL', width: 220 }, + { dataIndex: 'rewardCover', key: 'rewardCover', title: '封面', width: 220 }, { dataIndex: 'rewardAmount', key: 'rewardAmount', title: '数量/金币/天数', width: 150 }, { dataIndex: 'expireDays', key: 'expireDays', title: '有效天数', width: 130 }, { dataIndex: 'weight', key: 'weight', title: '权重', width: 110 }, @@ -161,7 +193,6 @@ function createDefaultLevel(level: number): LevelRow { enabled: true, level, needEnergy: level * 100, - progressBarUrl: '', rocketAnimationUrl: '', rocketIconUrl: '', shakeThresholdPercent: '95.0000', @@ -246,7 +277,6 @@ function normalizeLevelRows(rows: Array> | undefined) { enabled: row.enabled === undefined ? true : Boolean(row.enabled), id: row.id, needEnergy: toNumber(row.needEnergy, level * 100), - progressBarUrl: String(row.progressBarUrl || ''), rocketAnimationUrl: String(row.rocketAnimationUrl || ''), rocketIconUrl: String(row.rocketIconUrl || ''), shakeThresholdPercent: String(row.shakeThresholdPercent || '95.0000'), @@ -273,6 +303,183 @@ function normalizeRewardRows(rows: Array> | undefined) { })); } +function normalizePropsResource(item: Record, type: string): ResourceOption { + return { + amount: item.amount, + cover: String(item.cover || item.img || item.icon || ''), + id: item.id, + name: String(item.name || item.title || item.remark || item.id || ''), + remark: item.remark, + sourceUrl: String(item.sourceUrl || item.url || item.cover || ''), + type, + }; +} + +function normalizeGiftResource(item: Record): ResourceOption { + return { + amount: item.giftCandy, + cover: String(item.giftPhoto || ''), + id: item.id, + name: String(item.giftName || item.id || ''), + sourceUrl: String(item.giftSourceUrl || item.giftPhoto || ''), + type: 'GIFT', + }; +} + +function normalizeBadgeResource(item: Record): ResourceOption { + return { + cover: String(item.selectUrl || item.cover || ''), + id: item.badgeConfigId || item.id, + name: String(item.badgeName || item.name || item.id || ''), + sourceUrl: String(item.animationUrl || item.selectUrl || item.cover || ''), + type: 'BADGE', + }; +} + +function optionLabel(item: ResourceOption) { + return `${item.id} / ${item.name}`.trim(); +} + +function toResourceSelectOptions(rows: ResourceOption[]) { + return rows + .filter((item) => item.id !== undefined && item.id !== null && item.id !== '') + .map((item) => ({ + label: optionLabel(item), + value: String(item.id), + })); +} + +function toRocketResourceSelectOptions( + record: Record, + key: 'rocketAnimationUrl' | 'rocketIconUrl', +) { + const options = rocketResourceRows.value + .map((item) => { + const value = + key === 'rocketIconUrl' + ? String(item.cover || item.sourceUrl || '') + : String(item.sourceUrl || item.cover || ''); + return { + label: optionLabel(item), + value, + }; + }) + .filter((item) => item.value); + const current = String(record[key] || ''); + if (current && !options.some((item) => item.value === current)) { + options.unshift({ label: current, value: current }); + } + return options; +} + +async function loadRocketResources() { + if (!selectedSysOrigin.value || rocketResourceLoading.value) { + return; + } + rocketResourceLoading.value = true; + try { + const result = await listSysOriginTypeList( + selectedSysOrigin.value, + ROCKET_RESOURCE_TYPE, + ); + rocketResourceRows.value = (result || []).map((item) => + normalizePropsResource(item, ROCKET_RESOURCE_TYPE), + ); + } finally { + rocketResourceLoading.value = false; + } +} + +function rewardResourceType(rewardType: string) { + const value = String(rewardType || '').trim(); + const mapping: Record = { + AVATAR_FRAME: 'AVATAR_FRAME', + BADGE: 'BADGE', + CHAT_BUBBLE: 'CHAT_BUBBLE', + GIFT: 'GIFT', + MOUNT: 'RIDE', + VIP_CARD: 'NOBLE_VIP', + }; + return mapping[value] || ''; +} + +function isRewardResourceDisabled(row: Record) { + return row.rewardType === 'GOLD' || row.rewardType === 'NONE'; +} + +async function loadRewardResources(rewardType: string) { + const type = String(rewardType || '').trim(); + const sourceType = rewardResourceType(type); + if ( + !selectedSysOrigin.value || + !sourceType || + (rewardResourceRows[type] || []).length > 0 + ) { + return; + } + rewardResourceLoading[type] = true; + try { + if (type === 'GIFT') { + const result = await listGiftBySysOrigin(selectedSysOrigin.value); + rewardResourceRows[type] = (result || []).map(normalizeGiftResource); + return; + } + if (type === 'BADGE') { + const result = await listBadgePictureBySysOrigin( + selectedSysOrigin.value, + 'ACTIVITY', + ); + rewardResourceRows[type] = (result || []).map(normalizeBadgeResource); + return; + } + const loader = + type === 'VIP_CARD' ? listNotFamilyBySysOriginType : listSysOriginTypeList; + const result = await loader(selectedSysOrigin.value, sourceType); + rewardResourceRows[type] = (result || []).map((item) => + normalizePropsResource(item, sourceType), + ); + } finally { + rewardResourceLoading[type] = false; + } +} + +function rewardResourceOptions(row: Record) { + const rows = rewardResourceRows[row.rewardType] || []; + const options = toResourceSelectOptions(rows); + const current = String(row.rewardItemId || '').trim(); + if (current && !options.some((item) => item.value === current)) { + options.unshift({ + label: `${current} / ${row.rewardName || '已配置资源'}`, + value: current, + }); + } + return options; +} + +function handleRewardSceneChange(row: Record) { + if (row.rewardScene !== 'IN_ROOM' && row.rewardType === 'NONE') { + row.rewardType = 'GOLD'; + handleRewardTypeChange(row); + } +} + +function handleRewardResourceChange( + row: Record, + value?: null | number | string, +) { + const selected = + (rewardResourceRows[row.rewardType] || []).find( + (item) => String(item.id) === String(value), + ) || null; + if (!selected) { + row.rewardItemId = value || null; + return; + } + row.rewardItemId = selected.id; + row.rewardName = selected.name; + row.rewardCover = selected.cover || selected.sourceUrl || ''; +} + async function loadConfig() { if (!selectedSysOrigin.value) { return; @@ -316,7 +523,7 @@ async function loadRewards() { } async function loadAll() { - await Promise.all([loadConfig(), loadLevels(), loadRewards()]); + await Promise.all([loadConfig(), loadLevels(), loadRewards(), loadRocketResources()]); } async function saveConfig() { @@ -361,7 +568,6 @@ async function saveLevels() { id: row.id, level: row.level, needEnergy: toNumber(row.needEnergy, row.level * 100), - progressBarUrl: row.progressBarUrl, rocketAnimationUrl: row.rocketAnimationUrl, rocketIconUrl: row.rocketIconUrl, shakeThresholdPercent: row.shakeThresholdPercent, @@ -376,18 +582,19 @@ async function saveLevels() { } } -function addRewardRow(scene = 'TOP1') { +function addRewardRow(scene = 'TOP1', rewardType = 'GOLD') { + const isNone = rewardType === 'NONE'; rewardRows.value.push({ enabled: true, expireDays: null, level: rewardLevel.value, localKey: `new-${Date.now()}-${rewardRows.value.length}`, - rewardAmount: 1, + rewardAmount: isNone ? 0 : 1, rewardCover: '', rewardItemId: null, - rewardName: scene === 'TOP1' ? '金币' : '', + rewardName: isNone ? '轮空' : rewardType === 'GOLD' ? '金币' : '', rewardScene: scene, - rewardType: 'GOLD', + rewardType, sort: rewardRows.value.length + 1, weight: 1, }); @@ -398,13 +605,32 @@ function removeRewardRow(index: number) { } function handleRewardTypeChange(row: Record) { + if (row.rewardType === 'NONE') { + if (row.rewardScene !== 'IN_ROOM') { + row.rewardScene = 'IN_ROOM'; + } + row.rewardItemId = null; + row.rewardCover = ''; + row.expireDays = null; + row.rewardAmount = 0; + row.rewardName = '轮空'; + return; + } if (row.rewardType === 'GOLD') { row.rewardItemId = null; + row.rewardCover = ''; row.expireDays = null; if (!row.rewardName) { row.rewardName = '金币'; } + return; } + row.rewardItemId = null; + row.rewardCover = ''; + if (row.rewardName === '金币' || row.rewardName === '轮空') { + row.rewardName = ''; + } + void loadRewardResources(row.rewardType); } function validateRewards() { @@ -417,12 +643,20 @@ function validateRewards() { message.warning('请选择奖励类型'); return false; } + if (row.rewardType === 'NONE' && row.rewardScene !== 'IN_ROOM') { + message.warning('轮空只能配置在房用户奖励'); + return false; + } if (!String(row.rewardName || '').trim()) { message.warning('请输入奖励名称'); return false; } - if (row.rewardType !== 'GOLD' && !String(row.rewardItemId || '').trim()) { - message.warning('非金币奖励必须填写资源ID'); + if ( + row.rewardType !== 'GOLD' && + row.rewardType !== 'NONE' && + !String(row.rewardItemId || '').trim() + ) { + message.warning('非金币奖励必须选择资源'); return false; } } @@ -439,14 +673,20 @@ async function saveRewards() { level: rewardLevel.value, rewards: rewardRows.value.map((row) => ({ enabled: row.enabled, - expireDays: toOptionalNumber(row.expireDays), + expireDays: + row.rewardType === 'GOLD' || row.rewardType === 'NONE' + ? undefined + : toOptionalNumber(row.expireDays), id: row.id, level: rewardLevel.value, - rewardAmount: toNumber(row.rewardAmount, 1), - rewardCover: row.rewardCover, + rewardAmount: + row.rewardType === 'NONE' ? 0 : toNumber(row.rewardAmount, 1), + rewardCover: row.rewardType === 'NONE' ? '' : row.rewardCover, rewardItemId: - row.rewardType === 'GOLD' ? undefined : toOptionalString(row.rewardItemId), - rewardName: row.rewardName.trim(), + row.rewardType === 'GOLD' || row.rewardType === 'NONE' + ? undefined + : toOptionalString(row.rewardItemId), + rewardName: row.rewardType === 'NONE' ? '轮空' : row.rewardName.trim(), rewardScene: row.rewardScene, rewardType: row.rewardType, sort: toNumber(row.sort, 1), @@ -474,6 +714,10 @@ watch( watch( selectedSysOrigin, () => { + rocketResourceRows.value = []; + Object.keys(rewardResourceRows).forEach((key) => { + delete rewardResourceRows[key]; + }); void loadAll(); }, { immediate: true }, @@ -605,7 +849,7 @@ watch(rewardLevel, () => { :data-source="levelRows" :loading="levelLoading" :pagination="false" - :scroll="{ x: 1420 }" + :scroll="{ x: 1240 }" row-key="level" > + + - - - @@ -676,6 +934,7 @@ watch(rewardLevel, () => { + @@ -695,33 +954,53 @@ watch(rewardLevel, () => { v-model:value="record.rewardScene" :options="sceneOptions" class="table-select" + @change="() => handleRewardSceneChange(record)" />