feat: add rocket configuration page

This commit is contained in:
zhx 2026-07-13 16:06:30 +08:00
parent e989b13e92
commit 3364e53445
6 changed files with 529 additions and 0 deletions

19
src/api/rocket-config.js Normal file
View File

@ -0,0 +1,19 @@
import request from '@/utils/request'
// 查询全部火箭等级配置
export function listRocketConfigs(params) {
return request({
url: '/admin/rocket/config/list',
method: 'get',
params
})
}
// 更新指定火箭等级的配置
export function updateRocketConfig(data) {
return request({
url: '/admin/rocket/config/update',
method: 'post',
data
})
}

View File

@ -44,6 +44,7 @@ export default {
'参数配置': 'Parameter Settings', '参数配置': 'Parameter Settings',
'幸运礼物规则配置': 'Lucky Gift Rule Configuration', '幸运礼物规则配置': 'Lucky Gift Rule Configuration',
'幸运礼物规格配置': 'Lucky Gift Specification Configuration', '幸运礼物规格配置': 'Lucky Gift Specification Configuration',
'火箭配置': 'Rocket Configuration',
'用户观看广告配置': 'User Ad View Configuration', '用户观看广告配置': 'User Ad View Configuration',
'礼物管理': 'Gift Management', '礼物管理': 'Gift Management',
'IM账号管理': 'IM Account Management', 'IM账号管理': 'IM Account Management',

View File

@ -8003,6 +8003,53 @@ export default {
resetSuccess: 'Reset successful', resetSuccess: 'Reset successful',
yoloWalletConfigShared: 'The remaining YOLO settings share the USD wallet configuration.' yoloWalletConfigShared: 'The remaining YOLO settings share the USD wallet configuration.'
} }
},
rocketConfig: {
alert: 'A rocket launches after reaching the configured level threshold. Rewards in the same pool currently have equal probability.',
refresh: 'Refresh',
level: 'Rocket Level',
levelValue: 'Level {level} Rocket',
maxEnergy: 'Energy Threshold',
maxEnergyPlaceholder: 'Enter a positive integer',
maxEnergyHelp: 'Thresholds should increase from level 1 through level 3.',
rewardPool: 'Random Reward Pool',
emptyReward: 'No rewards configured',
status: 'Status',
enabled: 'Enabled',
disabled: 'Disabled',
updatedAt: 'Updated At',
actions: 'Actions',
edit: 'Edit',
editTitle: 'Edit {level}',
rewardHelp: 'One reward is selected from the pool with equal probability.',
addReward: 'Add Reward',
rewardNumber: 'Reward {number}',
removeReward: 'Remove',
rewardType: 'Type',
rewardName: 'Name',
quantity: 'Quantity/Days',
detailType: 'Prop Type',
detailTypePlaceholder: 'Enter a prop type recognized by the server',
cover: 'Cover URL',
coverPlaceholder: 'Optional; shown in the claim result',
goldAmount: 'Gold Amount',
resourceId: 'Resource ID',
cancel: 'Cancel',
save: 'Save',
positiveIntegerRequired: 'Enter a positive integer',
maxEnergyRequired: 'Enter an energy threshold',
statusRequired: 'Select a status',
rewardTypeRequired: 'Select a reward type',
rewardContentRequired: 'Enter a gold amount or resource ID',
rewardNameRequired: 'Enter a reward name',
detailTypeRequired: 'Enter a prop type',
rewardRequired: 'Configure at least one reward',
types: {
gold: 'Gold',
props: 'Prop',
badge: 'Badge',
propCoupon: 'Prop Coupon'
}
} }
} }
} }

View File

@ -8000,6 +8000,53 @@ export default {
resetSuccess: '重置成功', resetSuccess: '重置成功',
yoloWalletConfigShared: 'YOLO其余配置共用美金钱包的配置' yoloWalletConfigShared: 'YOLO其余配置共用美金钱包的配置'
} }
},
rocketConfig: {
alert: '火箭能量达到对应等级阈值后可发射;同一等级奖励池中的奖励当前为等概率随机。',
refresh: '刷新',
level: '火箭等级',
levelValue: '{level}级火箭',
maxEnergy: '能量阈值',
maxEnergyPlaceholder: '请输入大于0的整数',
maxEnergyHelp: '各等级阈值应按一级、二级、三级递增配置。',
rewardPool: '随机奖励池',
emptyReward: '暂未配置奖励',
status: '状态',
enabled: '启用',
disabled: '禁用',
updatedAt: '更新时间',
actions: '操作',
edit: '编辑',
editTitle: '编辑{level}',
rewardHelp: '领取时从奖励池中等概率随机一项。',
addReward: '添加奖励',
rewardNumber: '奖励 {number}',
removeReward: '删除',
rewardType: '类型',
rewardName: '名称',
quantity: '数量/天数',
detailType: '道具类型',
detailTypePlaceholder: '请输入服务端识别的道具类型',
cover: '封面地址',
coverPlaceholder: '选填,用于领取结果展示',
goldAmount: '金币数量',
resourceId: '资源ID',
cancel: '取消',
save: '保存',
positiveIntegerRequired: '请输入大于0的整数',
maxEnergyRequired: '请输入能量阈值',
statusRequired: '请选择状态',
rewardTypeRequired: '请选择奖励类型',
rewardContentRequired: '请输入金币数量或资源ID',
rewardNameRequired: '请输入奖励名称',
detailTypeRequired: '请输入道具类型',
rewardRequired: '至少需要配置一项奖励',
types: {
gold: '金币',
props: '道具',
badge: '徽章',
propCoupon: '道具券'
}
} }
} }
} }

View File

@ -854,6 +854,12 @@ export const mockRouters = [
component: () => import('@/views/game/lucky-gift/standard-config/index'), component: () => import('@/views/game/lucky-gift/standard-config/index'),
meta: { title: '幸运礼物规格配置', icon: 'form' } meta: { title: '幸运礼物规格配置', icon: 'form' }
}, },
{
path: '/rocket/config',
name: 'RocketConfig',
component: () => import('@/views/rocket/config/index'),
meta: { title: '火箭配置', icon: 'form' }
},
{ {
path: '/game/lucky-gift/give-record', path: '/game/lucky-gift/give-record',
name: 'GameDoubleLayerFruit', name: 'GameDoubleLayerFruit',

View File

@ -0,0 +1,409 @@
<template>
<div class="app-container rocket-config-page">
<el-alert
:title="$t('pages.rocketConfig.alert')"
type="info"
:closable="false"
show-icon
/>
<div class="filter-container">
<el-button
class="filter-item"
type="primary"
icon="el-icon-refresh"
:loading="listLoading"
@click="loadConfigs"
>
{{ $t('pages.rocketConfig.refresh') }}
</el-button>
</div>
<el-table
v-loading="listLoading"
:data="configList"
fit
border
highlight-current-row
>
<el-table-column prop="levelName" :label="$t('pages.rocketConfig.level')" align="center" width="140">
<template slot-scope="scope">
{{ scope.row.levelName || formatLevel(scope.row.level) }}
</template>
</el-table-column>
<el-table-column prop="maxEnergy" :label="$t('pages.rocketConfig.maxEnergy')" align="center" width="180" />
<el-table-column :label="$t('pages.rocketConfig.rewardPool')" min-width="360">
<template slot-scope="scope">
<el-tag
v-for="(reward, index) in scope.row.rewardConfig || []"
:key="index"
class="reward-tag"
size="small"
>
{{ formatReward(reward) }}
</el-tag>
<span v-if="!scope.row.rewardConfig || scope.row.rewardConfig.length === 0" class="empty-text">
{{ $t('pages.rocketConfig.emptyReward') }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" :label="$t('pages.rocketConfig.status')" align="center" width="110">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.status === 1 ? $t('pages.rocketConfig.enabled') : $t('pages.rocketConfig.disabled') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('pages.rocketConfig.updatedAt')" align="center" width="180">
<template slot-scope="scope">
{{ scope.row.updatedAt | dateFormat }}
</template>
</el-table-column>
<el-table-column :label="$t('pages.rocketConfig.actions')" align="center" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-edit" @click="openEdit(scope.row)">
{{ $t('pages.rocketConfig.edit') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
:title="$t('pages.rocketConfig.editTitle', { level: formatLevel(form.level) })"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="900px"
@closed="resetForm"
>
<el-form ref="configForm" :model="form" :rules="rules" label-width="130px">
<el-form-item :label="$t('pages.rocketConfig.level')">
<el-input :value="formatLevel(form.level)" disabled />
</el-form-item>
<el-form-item :label="$t('pages.rocketConfig.maxEnergy')" prop="maxEnergy">
<el-input
v-model.trim="form.maxEnergy"
:placeholder="$t('pages.rocketConfig.maxEnergyPlaceholder')"
/>
<div class="form-help">{{ $t('pages.rocketConfig.maxEnergyHelp') }}</div>
</el-form-item>
<el-form-item :label="$t('pages.rocketConfig.status')" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">{{ $t('pages.rocketConfig.enabled') }}</el-radio>
<el-radio :label="0">{{ $t('pages.rocketConfig.disabled') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('pages.rocketConfig.rewardPool')" required>
<div class="reward-toolbar">
<span class="form-help">{{ $t('pages.rocketConfig.rewardHelp') }}</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="addReward">
{{ $t('pages.rocketConfig.addReward') }}
</el-button>
</div>
<div v-for="(reward, index) in form.rewardConfig" :key="index" class="reward-editor">
<div class="reward-editor__header">
<strong>{{ $t('pages.rocketConfig.rewardNumber', { number: index + 1 }) }}</strong>
<el-button type="text" class="danger-text" @click="removeReward(index)">
{{ $t('pages.rocketConfig.removeReward') }}
</el-button>
</div>
<el-row :gutter="12">
<el-col :span="6">
<el-form-item
class="inner-form-item"
:label="$t('pages.rocketConfig.rewardType')"
:prop="'rewardConfig.' + index + '.type'"
:rules="rewardRules.type"
label-width="80px"
>
<el-select v-model="reward.type" style="width: 100%" @change="handleRewardTypeChange(reward)">
<el-option
v-for="item in rewardTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
class="inner-form-item"
:label="contentLabel(reward.type)"
:prop="'rewardConfig.' + index + '.content'"
:rules="rewardRules.content"
label-width="80px"
>
<el-input v-model.trim="reward.content" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
class="inner-form-item"
:label="$t('pages.rocketConfig.rewardName')"
:prop="'rewardConfig.' + index + '.name'"
:rules="rewardRules.name"
label-width="80px"
>
<el-input v-model.trim="reward.name" />
</el-form-item>
</el-col>
<el-col v-if="reward.type !== 'GOLD'" :span="6">
<el-form-item
class="inner-form-item"
:label="$t('pages.rocketConfig.quantity')"
:prop="'rewardConfig.' + index + '.quantity'"
:rules="rewardRules.quantity"
label-width="80px"
>
<el-input v-model.trim="reward.quantity" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col v-if="reward.type === 'PROPS'" :span="12">
<el-form-item
class="inner-form-item"
:label="$t('pages.rocketConfig.detailType')"
:prop="'rewardConfig.' + index + '.detailType'"
:rules="rewardRules.detailType"
label-width="80px"
>
<el-input v-model.trim="reward.detailType" :placeholder="$t('pages.rocketConfig.detailTypePlaceholder')" />
</el-form-item>
</el-col>
<el-col :span="reward.type === 'PROPS' ? 12 : 24">
<el-form-item class="inner-form-item" :label="$t('pages.rocketConfig.cover')" label-width="80px">
<el-input v-model.trim="reward.cover" :placeholder="$t('pages.rocketConfig.coverPlaceholder')" />
</el-form-item>
</el-col>
</el-row>
</div>
<el-empty
v-if="form.rewardConfig.length === 0"
:description="$t('pages.rocketConfig.emptyReward')"
:image-size="80"
/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">{{ $t('pages.rocketConfig.cancel') }}</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitForm">
{{ $t('pages.rocketConfig.save') }}
</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listRocketConfigs, updateRocketConfig } from '@/api/rocket-config'
const createEmptyReward = () => ({
type: 'GOLD',
content: '',
name: '',
quantity: 1,
detailType: '',
cover: ''
})
const createEmptyForm = () => ({
id: null,
level: null,
maxEnergy: '',
status: 1,
rewardConfig: []
})
export default {
name: 'RocketConfig',
data() {
const validatePositiveInteger = (rule, value, callback) => {
if (!/^\d+$/.test(String(value || '')) || Number(value) <= 0) {
callback(new Error(this.$t('pages.rocketConfig.positiveIntegerRequired')))
return
}
callback()
}
return {
listLoading: false,
submitLoading: false,
dialogVisible: false,
configList: [],
form: createEmptyForm(),
rules: {
maxEnergy: [
{ required: true, message: this.$t('pages.rocketConfig.maxEnergyRequired'), trigger: 'blur' },
{ validator: validatePositiveInteger, trigger: 'blur' }
],
status: [
{ required: true, message: this.$t('pages.rocketConfig.statusRequired'), trigger: 'change' }
]
},
rewardRules: {
type: [{ required: true, message: this.$t('pages.rocketConfig.rewardTypeRequired'), trigger: 'change' }],
content: [{ required: true, message: this.$t('pages.rocketConfig.rewardContentRequired'), trigger: 'blur' }],
name: [{ required: true, message: this.$t('pages.rocketConfig.rewardNameRequired'), trigger: 'blur' }],
quantity: [{ validator: validatePositiveInteger, trigger: 'blur' }],
detailType: [{ required: true, message: this.$t('pages.rocketConfig.detailTypeRequired'), trigger: 'blur' }]
}
}
},
computed: {
rewardTypes() {
return [
{ value: 'GOLD', label: this.$t('pages.rocketConfig.types.gold') },
{ value: 'PROPS', label: this.$t('pages.rocketConfig.types.props') },
{ value: 'BADGE', label: this.$t('pages.rocketConfig.types.badge') },
{ value: 'PROP_COUPON', label: this.$t('pages.rocketConfig.types.propCoupon') }
]
}
},
created() {
this.loadConfigs()
},
methods: {
loadConfigs() {
this.listLoading = true
listRocketConfigs().then(response => {
this.configList = response.body || []
}).finally(() => {
this.listLoading = false
})
},
formatLevel(level) {
return this.$t('pages.rocketConfig.levelValue', { level: level || '-' })
},
formatReward(reward) {
const type = this.rewardTypes.find(item => item.value === reward.type)
const typeName = type ? type.label : reward.type
const amount = reward.type === 'GOLD' ? reward.content : reward.quantity
return `${reward.name || typeName} (${amount || '-'})`
},
contentLabel(type) {
return type === 'GOLD'
? this.$t('pages.rocketConfig.goldAmount')
: this.$t('pages.rocketConfig.resourceId')
},
openEdit(row) {
//
const rewardConfig = (row.rewardConfig || []).map(item => Object.assign(createEmptyReward(), JSON.parse(JSON.stringify(item))))
this.form = {
id: row.id,
level: row.level,
maxEnergy: String(row.maxEnergy || ''),
status: row.status,
rewardConfig
}
this.dialogVisible = true
this.$nextTick(() => this.$refs.configForm && this.$refs.configForm.clearValidate())
},
resetForm() {
this.form = createEmptyForm()
if (this.$refs.configForm) {
this.$refs.configForm.clearValidate()
}
},
addReward() {
this.form.rewardConfig.push(createEmptyReward())
},
removeReward(index) {
this.form.rewardConfig.splice(index, 1)
},
handleRewardTypeChange(reward) {
if (reward.type === 'GOLD') {
reward.quantity = 1
reward.detailType = ''
}
},
submitForm() {
if (this.form.rewardConfig.length === 0) {
this.$message.warning(this.$t('pages.rocketConfig.rewardRequired'))
return
}
this.$refs.configForm.validate(valid => {
if (!valid) {
return
}
// 使
const payload = {
id: this.form.id,
maxEnergy: this.form.maxEnergy,
status: this.form.status,
rewardConfig: this.form.rewardConfig.map(item => {
const reward = Object.assign({}, item)
reward.content = String(reward.content)
if (reward.type !== 'GOLD') {
reward.quantity = Number(reward.quantity)
}
return reward
})
}
this.submitLoading = true
updateRocketConfig(payload).then(() => {
this.$opsMessage.success()
this.dialogVisible = false
this.loadConfigs()
}).finally(() => {
this.submitLoading = false
})
})
}
}
}
</script>
<style scoped lang="scss">
.rocket-config-page {
.filter-container {
margin-top: 16px;
}
.reward-tag {
margin: 3px 6px 3px 0;
}
.empty-text,
.form-help {
color: #909399;
font-size: 12px;
}
.reward-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.reward-editor {
margin-bottom: 14px;
padding: 14px 14px 0;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fafafa;
}
.reward-editor__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.inner-form-item {
margin-bottom: 18px;
}
.danger-text {
color: #f56c6c;
}
}
</style>