优化玩游戏中奖权重逻辑

This commit is contained in:
2026-03-10 16:27:11 +08:00
parent 296991f53a
commit e56c3ada34
8 changed files with 539 additions and 36 deletions

View File

@@ -61,5 +61,24 @@ export default {
url: '/core/dice/reward_config/DiceRewardConfig/destroy',
data: params
})
},
/**
* T1-T5、BIGWIN 权重配比:按档位分组获取配置列表
*/
weightRatioList() {
return request.get<Api.Common.ApiData>({
url: '/core/dice/reward_config/DiceRewardConfig/weightRatioList'
})
},
/**
* T1-T5、BIGWIN 权重配比:批量更新权重(同一档位权重之和必须等于 100%
*/
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
return request.post<any>({
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
data: { items }
})
}
}

View File

@@ -8,27 +8,14 @@
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<ElSpace wrap>
<!-- <ElButton-->
<!-- v-permission="'dice:reward_config:index:save'"-->
<!-- @click="showDialog('add')"-->
<!-- v-ripple-->
<!-- >-->
<!-- <template #icon>-->
<!-- <ArtSvgIcon icon="ri:add-fill" />-->
<!-- </template>-->
<!-- 新增-->
<!-- </ElButton>-->
<!-- <ElButton-->
<!-- v-permission="'dice:reward_config:index:destroy'"-->
<!-- :disabled="selectedRows.length === 0"-->
<!-- @click="deleteSelectedRows(api.delete, refreshData)"-->
<!-- v-ripple-->
<!-- >-->
<!-- <template #icon>-->
<!-- <ArtSvgIcon icon="ri:delete-bin-5-line" />-->
<!-- </template>-->
<!-- 删除-->
<!-- </ElButton>-->
<ElButton
v-permission="'dice:reward_config:index:update'"
type="primary"
@click="weightRatioVisible = true"
v-ripple
>
T1-T5 BIGWIN 权重配比
</ElButton>
</ElSpace>
</template>
</ArtTableHeader>
@@ -71,6 +58,8 @@
:data="dialogData"
@success="refreshData"
/>
<!-- T1-T5BIGWIN 权重配比弹窗 -->
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
</div>
</template>
@@ -80,6 +69,9 @@
import api from '../../api/reward_config/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
const weightRatioVisible = ref(false)
// 搜索表单
const searchForm = ref<Record<string, unknown>>({

View File

@@ -102,9 +102,11 @@
set: (value) => emit('update:modelValue', value)
})
/** tier=BIGWIN 且 grid_number 为 5 或 30 时权重固定 100%不可修改 */
const isWeightFixed100 = computed(
() => formData.tier === 'BIGWIN' && (formData.grid_number === 5 || formData.grid_number === 30)
/** BIGWIN 且 grid_number 为 5 或 30 时豹子概率不可修改 */
const isWeightDisabled = computed(
() =>
formData.tier === 'BIGWIN' &&
(formData.grid_number === 5 || formData.grid_number === 30)
)
/**
@@ -231,8 +233,12 @@
} else if (payload.grid_number === 5 || payload.grid_number === 30) {
payload.weight = 100
} else {
const w = Number(payload.weight)
payload.weight = Number.isNaN(w) ? 0 : Math.max(0, Math.min(100, w))
if (payload.grid_number === 5 || payload.grid_number === 30) {
payload.weight = 100
} else {
const w = Number(payload.weight)
payload.weight = Number.isNaN(w) ? 0 : Math.max(0, Math.min(100, w))
}
}
if (props.dialogType === 'add') {
await api.save(payload)

View File

@@ -0,0 +1,375 @@
<template>
<el-dialog
v-model="visible"
title="T1-T5 与 BIGWIN 权重配比"
width="600px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<el-tabs v-model="activeTier" type="card">
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
<div v-if="getTierItems(t).length === 0" class="empty-tip"> 该档位暂无配置数据 </div>
<template v-else>
<div class="chart-wrap">
<ArtBarChart
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t)"
height="220px"
/>
</div>
<div class="weight-sum" v-if="t !== 'BIGWIN'">
当前档位权重合计<strong :class="{ over: getTierSumForValidation(t) !== 100 }">{{
getTierSumForValidation(t).toFixed(1)
}}</strong>
/ 100须等于 100%
</div>
<div class="weight-sum weight-sum-bigwin" v-else>
BIGWIN 为豹子权重单独设定每条 0100%无合计要求
</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column label="色子点数" prop="grid_number" width="50" align="center" />
<el-table-column
label="实际中奖金额"
prop="real_ev"
width="90"
align="center"
show-overflow-tooltip
/>
<el-table-column
label="显示文本"
prop="ui_text"
min-width="80"
align="center"
show-overflow-tooltip
/>
<el-table-column
label="备注"
prop="remark"
min-width="80"
align="center"
show-overflow-tooltip
/>
<el-table-column label="权重(%)" min-width="200" align="center">
<template #default="{ row }">
<div class="weight-cell-vertical">
<div class="weight-slider-wrap">
<el-slider
:model-value="getItemWeight(row)"
:min="0"
:max="100"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) => setItemWeight(row, normalizeSliderValue(v))
"
/>
</div>
<div class="weight-input-wrap">
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 0"
@click="adjustWeight(row, -1)"
>
</el-button>
<el-input-number
:model-value="getItemWeight(row)"
:min="0"
:max="100"
:step="1"
:precision="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@update:model-value="(v: number | undefined) => setItemWeight(row, v ?? 0)"
/>
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 100"
@click="adjustWeight(row, 1)"
>
</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
</template>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '../../../api/reward_config/index'
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
import { ElMessage } from 'element-plus'
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'] as const
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false
})
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const tierKeys = TIER_KEYS
const activeTier = ref('T1')
const submitting = ref(false)
/** 按档位分组的数据:{ T1: [...], T2: [...], ... },每项为可修改 weight 的副本 */
const grouped = ref<Record<string, Array<Record<string, unknown> & { weight: number }>>>({
T1: [],
T2: [],
T3: [],
T4: [],
T5: [],
BIGWIN: []
})
function getTierItems(tier: string) {
return grouped.value[tier] ?? []
}
function getTierChartLabels(tier: string): string[] {
const items = getTierItems(tier)
return items.map((r) => (r.ui_text ? String(r.ui_text) : `点数 ${r.grid_number ?? ''}`))
}
function getTierChartData(tier: string): number[] {
const items = getTierItems(tier)
return items.map((r) => getItemWeight(r))
}
/** 用于 T1T5 校验与展示的档位权重和(仅 T1T5 使用BIGWIN 不要求合计) */
function getTierSumForValidation(tier: string): number {
const items = getTierItems(tier)
return items.reduce((s, r) => s + getItemWeight(r), 0)
}
function getItemWeight(row: Record<string, unknown> & { weight?: number }): number {
const w = row.weight
if (typeof w === 'number' && !Number.isNaN(w)) return Math.max(0, Math.min(100, w))
return 0
}
function setItemWeight(row: Record<string, unknown> & { weight: number }, value: number) {
row.weight = Math.max(0, Math.min(100, value))
}
function adjustWeight(row: Record<string, unknown> & { weight: number }, delta: number) {
const cur = getItemWeight(row)
setItemWeight(row, cur + delta)
}
/** T4、T5 及 BIGWIN 的 grid_number=5、30 不可修改权重(固定 100% */
function isWeightDisabled(row: Record<string, unknown>, tier: string): boolean {
if (tier === 'T4' || tier === 'T5') return true
if (tier === 'BIGWIN' && (row.grid_number === 5 || row.grid_number === 30)) return true
return false
}
function normalizeSliderValue(v: number | number[]): number {
if (Array.isArray(v)) return (v[0] ?? 0) as number
return (v ?? 0) as number
}
/**
* 从接口返回值中解析出按档位分组的对象。
* 兼容1) 直接返回 { T1: [], T2: [], ... } 2) 包装在 data 中 { data: { T1: [], ... } } 3) 平铺数组按 tier 分组
*/
function parseWeightRatioPayload(res: any): Record<string, Array<Record<string, unknown>>> {
if (!res || typeof res !== 'object') return {}
const hasTierKeys = (obj: any) =>
obj && typeof obj === 'object' && TIER_KEYS.some((k) => Array.isArray(obj[k]))
if (hasTierKeys(res)) return res as Record<string, Array<Record<string, unknown>>>
if (hasTierKeys(res?.data)) return res.data as Record<string, Array<Record<string, unknown>>>
if (hasTierKeys(res?.data?.data))
return res.data.data as Record<string, Array<Record<string, unknown>>>
if (Array.isArray(res)) {
return res.reduce(
(acc: Record<string, Array<Record<string, unknown>>>, r: Record<string, unknown>) => {
const t = (r.tier as string) || ''
if (t && TIER_KEYS.includes(t as (typeof TIER_KEYS)[number])) {
if (!acc[t]) acc[t] = []
acc[t].push(r)
}
return acc
},
{} as Record<string, Array<Record<string, unknown>>>
)
}
return {}
}
function loadData() {
api
.weightRatioList()
.then((res: any) => {
const data = parseWeightRatioPayload(res)
const next: Record<string, Array<Record<string, unknown> & { weight: number }>> = {
T1: [],
T2: [],
T3: [],
T4: [],
T5: [],
BIGWIN: []
}
for (const t of TIER_KEYS) {
const list = Array.isArray(data[t]) ? data[t] : []
next[t] = list.map((r) => ({
...r,
weight:
typeof r.weight === 'number' && !Number.isNaN(r.weight)
? r.weight
: Number(r.weight) || 0
}))
}
grouped.value = next
})
.catch(() => {
ElMessage.error('获取权重配比数据失败')
})
}
function validateAll(): boolean {
for (const t of TIER_KEYS) {
if (t === 'BIGWIN') continue
const items = getTierItems(t)
if (items.length === 0) continue
const sum = getTierSumForValidation(t)
if (Math.abs(sum - 100) > 0.01) {
ElMessage.warning(`档位 ${t} 的权重之和必须等于 100%,当前为 ${sum.toFixed(1)}%`)
activeTier.value = t
return false
}
}
return true
}
/**
* 收集所有档位的 id + weight同一 id 出现多次时合并权重(避免后端只保留最后一次导致档位合计不足 100%)。
* T4、T5、BIGWIN 的 5/30 不可修改,提交时固定为 100。
*/
function collectItems(): Array<{ id: number; weight: number }> {
const byId = new Map<number, number>()
for (const t of TIER_KEYS) {
for (const row of getTierItems(t)) {
const id = row.id != null ? Number(row.id) : 0
if (id >= 0 && !Number.isNaN(id)) {
const w = isWeightDisabled(row, t) ? 100 : getItemWeight(row)
byId.set(id, Math.min(100, (byId.get(id) ?? 0) + w))
}
}
}
return Array.from(byId.entries()).map(([id, weight]) => ({ id, weight }))
}
function handleSubmit() {
if (!validateAll()) return
const items = collectItems()
if (items.length === 0) {
ElMessage.info('没有可提交的配置')
return
}
submitting.value = true
api
.batchUpdateWeights(items)
.then(() => {
ElMessage.success('保存成功')
emit('success')
handleClose()
})
.catch((e: { message?: string }) => {
ElMessage.error(e?.message ?? '保存失败')
})
.finally(() => {
submitting.value = false
})
}
function handleClose() {
visible.value = false
}
watch(
() => props.modelValue,
(open) => {
if (open) {
loadData()
activeTier.value = 'T1'
}
}
)
</script>
<style lang="scss" scoped>
.chart-wrap {
margin-bottom: 12px;
}
.weight-sum {
margin-bottom: 12px;
font-size: 13px;
.over {
color: var(--el-color-danger);
}
}
.weight-sum-bigwin {
color: var(--el-text-color-secondary);
}
.weight-table {
margin-top: 8px;
}
.weight-cell-vertical {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.weight-slider-wrap {
width: 100%;
min-width: 100px;
padding: 0 8px;
.weight-slider {
width: 100%;
}
}
.weight-input-wrap {
display: flex;
align-items: center;
gap: 6px;
.weight-input {
width: 100px;
}
}
.empty-tip {
padding: 24px;
text-align: center;
color: var(--el-text-color-secondary);
}
</style>