This commit is contained in:
zhx 2026-05-14 18:02:09 +08:00
parent 790b0f9f7a
commit 0a8bddbea0
5 changed files with 980 additions and 0 deletions

View File

@ -11,6 +11,7 @@ const VIP_BASE = '/go/resident-activity/vip';
const RECHARGE_REWARD_BASE = '/go/resident-activity/recharge-reward';
const TASK_CENTER_BASE = '/go/resident-activity/task-center';
const VOICE_ROOM_RED_PACKET_BASE = '/go/resident-activity/voice-room-red-packet';
const VOICE_ROOM_ROCKET_BASE = '/go/resident-activity/voice-room-rocket';
export async function getResidentInviteConfig(sysOrigin: string) {
return requestClient.get<Record<string, any>>('/resident-activity/invite/config', {
@ -172,6 +173,68 @@ export async function retryVoiceRoomRedPacketRefund(packetNo: string) {
);
}
export async function getVoiceRoomRocketConfig(sysOrigin: string) {
return requestClient.get<Record<string, any>>(`${VOICE_ROOM_ROCKET_BASE}/config`, {
params: { sysOrigin },
});
}
export async function saveVoiceRoomRocketConfig(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
`${VOICE_ROOM_ROCKET_BASE}/config/save`,
data,
);
}
export async function getVoiceRoomRocketLevelConfigs(sysOrigin: string) {
return requestClient.get<Array<Record<string, any>>>(
`${VOICE_ROOM_ROCKET_BASE}/level-configs`,
{ params: { sysOrigin } },
);
}
export async function saveVoiceRoomRocketLevelConfigs(data: Record<string, any>) {
return requestClient.post<Array<Record<string, any>>>(
`${VOICE_ROOM_ROCKET_BASE}/level-configs/save`,
data,
);
}
export async function getVoiceRoomRocketRewardConfigs(params: Record<string, any>) {
return requestClient.get<Array<Record<string, any>>>(
`${VOICE_ROOM_ROCKET_BASE}/reward-configs`,
{ params },
);
}
export async function saveVoiceRoomRocketRewardConfigs(data: Record<string, any>) {
return requestClient.post<Array<Record<string, any>>>(
`${VOICE_ROOM_ROCKET_BASE}/reward-configs/save`,
data,
);
}
export async function pageVoiceRoomRocketLaunchRecords(params: Record<string, any>) {
return requestClient.get<Record<string, any>>(
`${VOICE_ROOM_ROCKET_BASE}/launch-records`,
{ params },
);
}
export async function pageVoiceRoomRocketRewardRecords(params: Record<string, any>) {
return requestClient.get<Record<string, any>>(
`${VOICE_ROOM_ROCKET_BASE}/reward-records`,
{ params },
);
}
export async function retryVoiceRoomRocketRewardRecord(id: number | string) {
return requestClient.post<Record<string, any>>(
`${VOICE_ROOM_ROCKET_BASE}/reward-records/retry`,
{ id },
);
}
export async function pageSpecifiedGiftWeeklyRankConfigs(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
`${SPECIFIED_GIFT_WEEKLY_RANK_BASE}/config/page`,

View File

@ -41,6 +41,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/resident-activity/voice-room-red-packet.vue'),
meta: { title: '红包' },
},
{
name: 'ResidentVoiceRoomRocket',
path: 'voice-room-rocket',
component: () => import('#/views/resident-activity/voice-room-rocket.vue'),
meta: { title: '火箭' },
},
{
name: 'ResidentRegisterRewardConfig',
path: 'register-reward/config',

View File

@ -63,6 +63,7 @@ function buildIdentityTags() {
['货运代理', target.freightAgent],
['BD', target.bd],
['BD Leader', target.bdLeader],
['Yumi 经理', target.yumiManager || target.yumi_manager],
];
return pairs.filter(([, enabled]) => Boolean(enabled));
}

View File

@ -0,0 +1,846 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
Button,
Card,
Input,
InputNumber,
Select,
Space,
Spin,
Switch,
Table,
Tabs,
TabPane,
Tag,
TextArea,
message,
} from 'antdv-next';
import {
getVoiceRoomRocketConfig,
getVoiceRoomRocketLevelConfigs,
getVoiceRoomRocketRewardConfigs,
saveVoiceRoomRocketConfig,
saveVoiceRoomRocketLevelConfigs,
saveVoiceRoomRocketRewardConfigs,
} from '#/api/legacy/resident-activity';
import { getAllowedSysOrigins } from '#/views/system/shared';
defineOptions({ name: 'ResidentVoiceRoomRocketPage' });
const RIYADH_TIMEZONE = 'Asia/Riyadh';
const DEFAULT_MAX_LEVEL = 6;
type LevelRow = {
enabled: boolean;
id?: number | string;
level: number;
needEnergy: number;
progressBarUrl: string;
rocketAnimationUrl: string;
rocketIconUrl: string;
shakeThresholdPercent: string;
sort: number;
};
type RewardRow = {
enabled: boolean;
expireDays?: null | number;
id?: number | string;
localKey: string;
level: number;
rewardAmount: number;
rewardCover: string;
rewardItemId?: null | number | string;
rewardName: string;
rewardScene: string;
rewardType: string;
sort: number;
weight: number;
};
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const selectedSysOrigin = ref('');
const activeTab = ref('config');
const rewardLevel = ref(1);
const configLoading = ref(false);
const levelLoading = ref(false);
const rewardLoading = ref(false);
const configSaving = ref(false);
const levelSaving = ref(false);
const rewardSaving = ref(false);
const configForm = reactive({
broadcastDurationSeconds: 7,
configured: false,
enabled: false,
energyRuleJsonText: '{}',
id: '',
inRoomRewardDelaySeconds: 10,
maxLevel: DEFAULT_MAX_LEVEL,
rankingTopLimit: 100,
rewardRecordKeepDays: 35,
ruleTextText: '{}',
timezone: RIYADH_TIMEZONE,
updateTime: '',
});
const levelRows = ref<LevelRow[]>([]);
const rewardRows = ref<RewardRow[]>([]);
const sceneOptions = [
{ label: '贡献第一', value: 'TOP1' },
{ label: '点火人', value: 'IGNITE' },
{ label: '在房用户', value: 'IN_ROOM' },
];
const rewardTypeOptions = [
{ label: '金币', value: 'GOLD' },
{ label: '坐骑', value: 'MOUNT' },
{ label: '头像框', value: 'AVATAR_FRAME' },
{ label: '气泡', value: 'CHAT_BUBBLE' },
{ label: 'VIP', value: 'VIP_CARD' },
{ label: '礼物', value: 'GIFT' },
{ label: '徽章', value: 'BADGE' },
];
const levelOptions = computed(() =>
Array.from({ length: configForm.maxLevel || DEFAULT_MAX_LEVEL }, (_, index) => ({
label: `Lv.${index + 1}`,
value: index + 1,
})),
);
const statusMeta = computed(() => {
if (!configForm.configured) {
return { color: 'warning', text: '未配置' };
}
return configForm.enabled
? { color: 'success', text: '已启用' }
: { color: 'default', text: '已关闭' };
});
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: 'enabled', key: 'enabled', title: '启用', width: 90 },
{ dataIndex: 'sort', key: 'sort', title: '排序', width: 100 },
];
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: 'rewardName', key: 'rewardName', title: '奖励名称', width: 180 },
{ dataIndex: 'rewardCover', key: 'rewardCover', title: '封面URL', width: 220 },
{ dataIndex: 'rewardAmount', key: 'rewardAmount', title: '数量/金币/天数', width: 150 },
{ dataIndex: 'expireDays', key: 'expireDays', title: '有效天数', width: 130 },
{ dataIndex: 'weight', key: 'weight', title: '权重', width: 110 },
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 },
{ dataIndex: 'sort', key: 'sort', title: '排序', width: 100 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 100 },
];
function createDefaultLevel(level: number): LevelRow {
return {
enabled: true,
level,
needEnergy: level * 100,
progressBarUrl: '',
rocketAnimationUrl: '',
rocketIconUrl: '',
shakeThresholdPercent: '95.0000',
sort: level,
};
}
function stringifyJSON(value: unknown) {
if (value === null || value === undefined || value === '') {
return '{}';
}
if (typeof value === 'string') {
return value.trim() || '{}';
}
return JSON.stringify(value, null, 2);
}
function parseJSONText(value: string, label: string) {
const raw = String(value || '').trim();
if (!raw) {
return {};
}
try {
return JSON.parse(raw);
} catch {
message.warning(`${label} 必须是合法 JSON`);
return undefined;
}
}
function toNumber(value: unknown, fallback = 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function toOptionalNumber(value: unknown) {
if (value === null || value === undefined || value === '') {
return undefined;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
function toOptionalString(value: unknown) {
const text = String(value ?? '').trim();
return text || undefined;
}
function normalizeConfig(value: Record<string, any> | null | undefined) {
const next = value || {};
configForm.broadcastDurationSeconds = toNumber(
next.broadcastDurationSeconds,
7,
);
configForm.configured = Boolean(next.configured);
configForm.enabled = Boolean(next.enabled);
configForm.energyRuleJsonText = stringifyJSON(next.energyRuleJson);
configForm.id = String(next.id || '');
configForm.inRoomRewardDelaySeconds = toNumber(
next.inRoomRewardDelaySeconds,
10,
);
configForm.maxLevel = toNumber(next.maxLevel, DEFAULT_MAX_LEVEL);
configForm.rankingTopLimit = toNumber(next.rankingTopLimit, 100);
configForm.rewardRecordKeepDays = toNumber(next.rewardRecordKeepDays, 35);
configForm.ruleTextText = stringifyJSON(next.ruleText);
configForm.timezone = RIYADH_TIMEZONE;
configForm.updateTime = String(next.updateTime || '');
}
function normalizeLevelRows(rows: Array<Record<string, any>> | undefined) {
const source = Array.isArray(rows) ? rows : [];
const byLevel = new Map<number, Record<string, any>>();
source.forEach((item) => {
byLevel.set(toNumber(item.level), item);
});
levelRows.value = Array.from({ length: DEFAULT_MAX_LEVEL }, (_, index) => {
const level = index + 1;
const row = byLevel.get(level) || {};
return {
...createDefaultLevel(level),
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'),
sort: toNumber(row.sort, level),
};
});
}
function normalizeRewardRows(rows: Array<Record<string, any>> | undefined) {
rewardRows.value = (Array.isArray(rows) ? rows : []).map((row, index) => ({
enabled: row.enabled === undefined ? true : Boolean(row.enabled),
expireDays: toOptionalNumber(row.expireDays) ?? null,
id: row.id,
level: toNumber(row.level, rewardLevel.value),
localKey: `${row.id || 'new'}-${index}-${Date.now()}`,
rewardAmount: toNumber(row.rewardAmount, 1),
rewardCover: String(row.rewardCover || ''),
rewardItemId: row.rewardItemId ?? null,
rewardName: String(row.rewardName || ''),
rewardScene: String(row.rewardScene || 'TOP1'),
rewardType: String(row.rewardType || 'GOLD'),
sort: toNumber(row.sort, index + 1),
weight: toNumber(row.weight, 1),
}));
}
async function loadConfig() {
if (!selectedSysOrigin.value) {
return;
}
configLoading.value = true;
try {
const result = await getVoiceRoomRocketConfig(selectedSysOrigin.value);
normalizeConfig(result);
} finally {
configLoading.value = false;
}
}
async function loadLevels() {
if (!selectedSysOrigin.value) {
return;
}
levelLoading.value = true;
try {
const result = await getVoiceRoomRocketLevelConfigs(selectedSysOrigin.value);
normalizeLevelRows(result);
} finally {
levelLoading.value = false;
}
}
async function loadRewards() {
if (!selectedSysOrigin.value) {
return;
}
rewardLoading.value = true;
try {
const result = await getVoiceRoomRocketRewardConfigs({
level: rewardLevel.value,
sysOrigin: selectedSysOrigin.value,
});
normalizeRewardRows(result);
} finally {
rewardLoading.value = false;
}
}
async function loadAll() {
await Promise.all([loadConfig(), loadLevels(), loadRewards()]);
}
async function saveConfig() {
const energyRuleJson = parseJSONText(
configForm.energyRuleJsonText,
'能量规则',
);
if (energyRuleJson === undefined) {
return;
}
const ruleText = parseJSONText(configForm.ruleTextText, '规则说明');
if (ruleText === undefined) {
return;
}
configSaving.value = true;
try {
const result = await saveVoiceRoomRocketConfig({
broadcastDurationSeconds: configForm.broadcastDurationSeconds,
enabled: configForm.enabled,
energyRuleJson,
inRoomRewardDelaySeconds: configForm.inRoomRewardDelaySeconds,
maxLevel: DEFAULT_MAX_LEVEL,
rankingTopLimit: configForm.rankingTopLimit,
rewardRecordKeepDays: configForm.rewardRecordKeepDays,
ruleText,
sysOrigin: selectedSysOrigin.value,
timezone: RIYADH_TIMEZONE,
});
normalizeConfig(result);
message.success('保存成功');
} finally {
configSaving.value = false;
}
}
async function saveLevels() {
levelSaving.value = true;
try {
const result = await saveVoiceRoomRocketLevelConfigs({
levels: levelRows.value.map((row) => ({
enabled: row.enabled,
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,
sort: toNumber(row.sort, row.level),
})),
sysOrigin: selectedSysOrigin.value,
});
normalizeLevelRows(result);
message.success('保存成功');
} finally {
levelSaving.value = false;
}
}
function addRewardRow(scene = 'TOP1') {
rewardRows.value.push({
enabled: true,
expireDays: null,
level: rewardLevel.value,
localKey: `new-${Date.now()}-${rewardRows.value.length}`,
rewardAmount: 1,
rewardCover: '',
rewardItemId: null,
rewardName: scene === 'TOP1' ? '金币' : '',
rewardScene: scene,
rewardType: 'GOLD',
sort: rewardRows.value.length + 1,
weight: 1,
});
}
function removeRewardRow(index: number) {
rewardRows.value.splice(index, 1);
}
function handleRewardTypeChange(row: Record<string, any>) {
if (row.rewardType === 'GOLD') {
row.rewardItemId = null;
row.expireDays = null;
if (!row.rewardName) {
row.rewardName = '金币';
}
}
}
function validateRewards() {
for (const row of rewardRows.value) {
if (!row.rewardScene) {
message.warning('请选择奖励场景');
return false;
}
if (!row.rewardType) {
message.warning('请选择奖励类型');
return false;
}
if (!String(row.rewardName || '').trim()) {
message.warning('请输入奖励名称');
return false;
}
if (row.rewardType !== 'GOLD' && !String(row.rewardItemId || '').trim()) {
message.warning('非金币奖励必须填写资源ID');
return false;
}
}
return true;
}
async function saveRewards() {
if (!validateRewards()) {
return;
}
rewardSaving.value = true;
try {
const result = await saveVoiceRoomRocketRewardConfigs({
level: rewardLevel.value,
rewards: rewardRows.value.map((row) => ({
enabled: row.enabled,
expireDays: toOptionalNumber(row.expireDays),
id: row.id,
level: rewardLevel.value,
rewardAmount: toNumber(row.rewardAmount, 1),
rewardCover: row.rewardCover,
rewardItemId:
row.rewardType === 'GOLD' ? undefined : toOptionalString(row.rewardItemId),
rewardName: row.rewardName.trim(),
rewardScene: row.rewardScene,
rewardType: row.rewardType,
sort: toNumber(row.sort, 1),
weight: toNumber(row.weight, 1),
})),
sysOrigin: selectedSysOrigin.value,
});
normalizeRewardRows(result);
message.success('保存成功');
} finally {
rewardSaving.value = false;
}
}
watch(
sysOriginOptions,
(options) => {
if (!selectedSysOrigin.value && options[0]?.value) {
selectedSysOrigin.value = String(options[0].value);
}
},
{ immediate: true },
);
watch(
selectedSysOrigin,
() => {
void loadAll();
},
{ immediate: true },
);
watch(rewardLevel, () => {
void loadRewards();
});
</script>
<template>
<Page title="火箭">
<div class="voice-room-rocket-page">
<Card>
<Space wrap>
<span>系统</span>
<Select
v-model:value="selectedSysOrigin"
:options="sysOriginOptions"
style="width: 180px"
/>
<Tag :color="statusMeta.color">{{ statusMeta.text }}</Tag>
<Tag color="processing">利雅得时间每日 00:00 重置</Tag>
<span v-if="configForm.updateTime" class="muted-text">
更新时间{{ configForm.updateTime }}
</span>
</Space>
</Card>
<Tabs v-model:active-key="activeTab">
<TabPane key="config" tab="基础配置">
<Spin :spinning="configLoading">
<Card>
<div class="config-grid">
<label class="field-row">
<span>启用火箭</span>
<Switch v-model:checked="configForm.enabled" />
</label>
<label class="field-row">
<span>活动时区</span>
<Input
v-model:value="configForm.timezone"
class="field-control"
disabled
/>
</label>
<label class="field-row">
<span>最大等级</span>
<InputNumber
v-model:value="configForm.maxLevel"
:min="1"
:max="6"
:precision="0"
class="field-control"
disabled
/>
</label>
<label class="field-row">
<span>榜单人数</span>
<InputNumber
v-model:value="configForm.rankingTopLimit"
:min="1"
:max="100"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row">
<span>发奖记录保留天数</span>
<InputNumber
v-model:value="configForm.rewardRecordKeepDays"
:min="1"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row">
<span>发射广播秒数</span>
<InputNumber
v-model:value="configForm.broadcastDurationSeconds"
:min="1"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row">
<span>在房奖励延迟秒数</span>
<InputNumber
v-model:value="configForm.inRoomRewardDelaySeconds"
:min="1"
:precision="0"
class="field-control"
/>
</label>
<label class="field-row field-wide">
<span>能量规则 JSON</span>
<TextArea
v-model:value="configForm.energyRuleJsonText"
:rows="4"
class="field-control"
/>
</label>
<label class="field-row field-wide">
<span>规则说明 JSON</span>
<TextArea
v-model:value="configForm.ruleTextText"
:rows="4"
class="field-control"
/>
</label>
</div>
<div class="actions">
<Space>
<Button @click="loadConfig">刷新</Button>
<Button type="primary" :loading="configSaving" @click="saveConfig">
保存基础配置
</Button>
</Space>
</div>
</Card>
</Spin>
</TabPane>
<TabPane key="levels" tab="等级进度">
<Card>
<Table
:columns="levelColumns"
:data-source="levelRows"
:loading="levelLoading"
:pagination="false"
:scroll="{ x: 1420 }"
row-key="level"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<Tag color="blue">Lv.{{ record.level }}</Tag>
</template>
<template v-else-if="column.key === 'needEnergy'">
<InputNumber
v-model:value="record.needEnergy"
:min="1"
:precision="0"
class="table-number"
/>
</template>
<template v-else-if="column.key === 'shakeThresholdPercent'">
<Input
v-model:value="record.shakeThresholdPercent"
class="table-input"
/>
</template>
<template v-else-if="column.key === 'rocketIconUrl'">
<Input v-model:value="record.rocketIconUrl" class="table-input" />
</template>
<template v-else-if="column.key === 'rocketAnimationUrl'">
<Input
v-model:value="record.rocketAnimationUrl"
class="table-input"
/>
</template>
<template v-else-if="column.key === 'progressBarUrl'">
<Input v-model:value="record.progressBarUrl" class="table-input" />
</template>
<template v-else-if="column.key === 'enabled'">
<Switch v-model:checked="record.enabled" />
</template>
<template v-else-if="column.key === 'sort'">
<InputNumber
v-model:value="record.sort"
:min="1"
:precision="0"
class="table-number"
/>
</template>
</template>
</Table>
<div class="actions">
<Space>
<Button @click="loadLevels">刷新</Button>
<Button type="primary" :loading="levelSaving" @click="saveLevels">
保存等级进度
</Button>
</Space>
</div>
</Card>
</TabPane>
<TabPane key="rewards" tab="奖励配置">
<Card>
<Space class="reward-toolbar" wrap>
<span>等级</span>
<Select
v-model:value="rewardLevel"
:options="levelOptions"
style="width: 120px"
/>
<Button @click="loadRewards">刷新</Button>
<Button @click="addRewardRow('TOP1')">添加贡献第一</Button>
<Button @click="addRewardRow('IGNITE')">添加点火人</Button>
<Button @click="addRewardRow('IN_ROOM')">添加在房用户</Button>
<Button type="primary" :loading="rewardSaving" @click="saveRewards">
保存当前等级奖励
</Button>
</Space>
<Table
:columns="rewardColumns"
:data-source="rewardRows"
:loading="rewardLoading"
:pagination="false"
:scroll="{ x: 1660 }"
row-key="localKey"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'rewardScene'">
<Select
v-model:value="record.rewardScene"
:options="sceneOptions"
class="table-select"
/>
</template>
<template v-else-if="column.key === 'rewardType'">
<Select
v-model:value="record.rewardType"
:options="rewardTypeOptions"
class="table-select"
@change="() => handleRewardTypeChange(record)"
/>
</template>
<template v-else-if="column.key === 'rewardItemId'">
<Input
v-model:value="record.rewardItemId"
:disabled="record.rewardType === 'GOLD'"
class="table-input-small"
/>
</template>
<template v-else-if="column.key === 'rewardName'">
<Input v-model:value="record.rewardName" class="table-input-small" />
</template>
<template v-else-if="column.key === 'rewardCover'">
<Input v-model:value="record.rewardCover" class="table-input" />
</template>
<template v-else-if="column.key === 'rewardAmount'">
<InputNumber
v-model:value="record.rewardAmount"
:min="1"
:precision="0"
class="table-number"
/>
</template>
<template v-else-if="column.key === 'expireDays'">
<InputNumber
v-model:value="record.expireDays"
:disabled="record.rewardType === 'GOLD'"
:min="0"
:precision="0"
class="table-number"
/>
</template>
<template v-else-if="column.key === 'weight'">
<InputNumber
v-model:value="record.weight"
:min="0"
:precision="0"
class="table-number"
/>
</template>
<template v-else-if="column.key === 'enabled'">
<Switch v-model:checked="record.enabled" />
</template>
<template v-else-if="column.key === 'sort'">
<InputNumber
v-model:value="record.sort"
:min="1"
:precision="0"
class="table-number"
/>
</template>
<template v-else-if="column.key === 'actions'">
<Button danger size="small" @click="removeRewardRow(index)">
删除
</Button>
</template>
</template>
</Table>
<div class="reward-summary">
<Tag v-for="scene in sceneOptions" :key="scene.value">
{{ scene.label }}
{{
rewardRows.filter((item) => item.rewardScene === scene.value).length
}}
</Tag>
<Tag v-for="type in rewardTypeOptions" :key="type.value">
{{ type.label }}
{{ rewardRows.filter((item) => item.rewardType === type.value).length }}
</Tag>
</div>
</Card>
</TabPane>
</Tabs>
</div>
</Page>
</template>
<style scoped>
.voice-room-rocket-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.field-row {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
color: #4b5563;
}
.field-wide {
grid-column: 1 / -1;
}
.field-control {
width: 100%;
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.muted-text {
color: #8c8c8c;
}
.reward-toolbar {
margin-bottom: 16px;
}
.reward-summary {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.table-input {
width: 240px;
}
.table-input-small,
.table-select {
width: 140px;
}
.table-number {
width: 110px;
}
</style>

View File

@ -753,3 +753,67 @@ LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = parent_role.role_id
AND existing_role_menu.menu_id = target_menu.id
WHERE existing_role_menu.id IS NULL;
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
next_id,
@resident_activity_id,
'火箭',
'resident-activity/voice-room-rocket',
2,
'form',
'0',
'0',
10,
0,
'voice-room-rocket',
'ResidentVoiceRoomRocket'
FROM (
SELECT COALESCE(MAX(id), 1700000000) + 1 AS next_id
FROM sys_menu
) AS menu_id
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentVoiceRoomRocket'
);
UPDATE sys_menu
SET
parent_id = @resident_activity_id,
menu_name = '火箭',
path = 'resident-activity/voice-room-rocket',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 10,
status = 0,
router = 'voice-room-rocket'
WHERE alias = 'ResidentVoiceRoomRocket';
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
parent_role.role_id,
target_menu.id
FROM sys_role_menu AS parent_role
JOIN sys_menu AS parent_menu
ON parent_menu.id = parent_role.menu_id
AND parent_menu.alias = 'ResidentActivityManager'
JOIN sys_menu AS target_menu
ON target_menu.alias = 'ResidentVoiceRoomRocket'
LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = parent_role.role_id
AND existing_role_menu.menu_id = target_menu.id
WHERE existing_role_menu.id IS NULL;