优化中奖权重计算方式

This commit is contained in:
2026-03-12 17:17:00 +08:00
parent 064ce06393
commit 7e4ba86afa
25 changed files with 2344 additions and 403 deletions

View File

@@ -10,11 +10,13 @@
<ElSpace wrap>
<ElButton
v-permission="'dice:reward_config:index:update'"
type="primary"
@click="weightRatioVisible = true"
type="warning"
:loading="createRewardLoading"
@click="handleCreateRewardReference"
v-ripple
title="按规则start_index=config(grid_number).id顺时针 end_index=(start_index+grid_number)%26逆时针 end_index=start_index-grid_number≥0?start_index-grid_number:26+start_index-grid_number"
>
T1-T5 BIGWIN 权重配比
创建奖励对照
</ElButton>
<ElButton
v-permission="'dice:reward_config:index:index'"
@@ -66,24 +68,52 @@
:data="dialogData"
@success="refreshData"
/>
<!-- T1-T5BIGWIN 权重配比弹窗 -->
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
<!-- 权重配比测试弹窗 -->
<WeightTestDialog v-model="weightTestVisible" />
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
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'
import WeightTestDialog from './modules/weight-test-dialog.vue'
const weightRatioVisible = ref(false)
const weightTestVisible = ref(false)
const createRewardLoading = ref(false)
async function handleCreateRewardReference() {
try {
await ElMessageBox.confirm(
'按规则创建奖励对照:起始索引 start_index=奖励配置中 grid_number 对应格位的 id顺时针 end_index=(start_index+摇取点数)%26逆时针 end_index=start_index-摇取点数≥0 则取该值,否则 26+start_index-摇取点数。先清空现有数据再为 5-30 共 26 个点数、顺/逆时针分别生成。是否继续?',
'创建奖励对照',
{
confirmButtonText: '确定创建',
cancelButtonText: '取消',
type: 'warning'
}
)
} catch {
return
}
createRewardLoading.value = true
try {
const res: any = await api.createRewardReference()
const data = res?.data ?? res
const msg =
typeof data === 'object' && data !== null
? `已按 5-30 共26个点数、顺时针+逆时针创建:顺时针新增 ${data.created_clockwise ?? 0} 条、逆时针新增 ${data.created_counterclockwise ?? 0} 条;顺时针更新 ${data.updated_clockwise ?? 0} 条、逆时针更新 ${data.updated_counterclockwise ?? 0}${(data.skipped ?? 0) > 0 ? `${data.skipped} 个点数使用兜底起始索引` : ''}`
: '创建成功'
ElMessage.success(msg)
} catch (e: any) {
ElMessage.error(e?.message ?? '创建奖励对照失败')
} finally {
createRewardLoading.value = false
}
}
// 搜索表单
const searchForm = ref<Record<string, unknown>>({
@@ -121,12 +151,12 @@
apiParams: { limit: 100 },
columnsFactory: () => [
// { type: 'selection' },
{ prop: 'id', label: 'ID(索引)', width: 80, align: 'center' },
{ prop: 'grid_number', label: '色子点数', align: 'center' },
{ prop: 'ui_text', label: '前端显示文本', align: 'center' },
{ prop: 'real_ev', label: '真实资金结算', align: 'center' },
{ prop: 'tier', label: '所属档位', sortable: true, align: 'center' },
{ prop: 'weight', label: '权重(1-10000)', width: 110, align: 'center' },
{ prop: 'weight', label: '权重(1-10000)', sortable: true, align: 'center' },
// 权重已迁移至「权重配比」弹窗dice_reward 表,区分顺时针/逆时针)
// { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' },
{
prop: 'operation',

View File

@@ -37,25 +37,22 @@
<el-option label="BIGWIN超级大奖" value="BIGWIN" />
</el-select>
</el-form-item>
<el-form-item label="权重(1-10000)" prop="weight">
<!-- BIGWIN 时可编辑权重10000=100% 中奖0=0% 中奖点数 530 固定 100% 不可改 -->
<el-form-item v-if="formData.tier === 'BIGWIN'" label="大奖权重" prop="weight">
<el-input-number
v-model="formData.weight"
:min="1"
:min="0"
:max="10000"
:step="1"
:disabled="isWeightFixed10000"
placeholder="1-10000"
:step="100"
placeholder="0~1000010000=100%中奖"
:disabled="isBigwinWeightDisabled"
/>
<div v-if="isWeightFixed10000" class="weight-fixed-hint">
色子点数 530 固定 10000不可修改权重
<div v-if="isBigwinWeightDisabled" class="form-tip">
点数 530 摇到必中大奖权重固定 10000
</div>
<div v-else class="form-tip">10000=100% 中奖0=0% 中奖仅对点数 10/15/20/25 生效</div>
</el-form-item>
<el-form-item label="顺时针起始索引" prop="s_start_index">
<el-input-number v-model="formData.s_start_index" :min="0" :step="1" placeholder="s_start_index" />
</el-form-item>
<el-form-item label="逆时针起始索引" prop="n_start_index">
<el-input-number v-model="formData.n_start_index" :min="0" :step="1" placeholder="n_start_index" />
</el-form-item>
<!-- 权重已迁移至T1-T5 BIGWIN 权重配比弹窗dice_reward BIGWIN 时本弹窗可编辑 weight起始索引已迁移至 dice_reward.start_index -->
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
@@ -108,38 +105,22 @@
set: (value) => emit('update:modelValue', value)
})
/** BIGWIN 且 grid_number 为 5 或 30 时权重固定为 10000禁止手动调整 */
const isWeightFixed10000 = computed(
() =>
formData.tier === 'BIGWIN' &&
(formData.grid_number === 5 || formData.grid_number === 30)
)
/**
* 表单验证规则
* 表单验证规则(权重已迁移至权重配比弹窗)
*/
const rules = reactive<FormRules>({
grid_number: [{ required: true, message: '色子点数必需填写', trigger: 'blur' }],
ui_text: [{ required: true, message: '前端显示文本必需填写', trigger: 'blur' }],
real_ev: [{ required: true, message: '真实资金结算必需填写', trigger: 'blur' }],
tier: [{ required: true, message: '所属档位必需填写', trigger: 'blur' }],
weight: [
{
validator: (_rule: unknown, value: number | null, callback: (e?: Error) => void) => {
const n = value != null ? Number(value) : NaN
if (Number.isNaN(n) || n < 1 || n > 10000) {
callback(new Error('权重必须为 1-10000'))
return
}
callback()
},
trigger: 'blur'
}
],
n_start_index: [],
s_start_index: []
weight: [{ type: 'number', min: 0, max: 10000, message: '大奖权重 0~10000', trigger: 'blur' }]
})
/** 点数 5、30 固定 100% 中大奖,权重不可改 */
const isBigwinWeightDisabled = computed(
() => formData.tier === 'BIGWIN' && [5, 30].includes(Number(formData.grid_number))
)
/**
* 初始数据
*/
@@ -149,10 +130,8 @@
ui_text: '',
real_ev: '',
tier: '',
weight: 1 as number,
n_start_index: 0 as number,
s_start_index: 0 as number,
remark: ''
remark: '',
weight: 10000 as number
}
/**
@@ -172,19 +151,6 @@
}
)
/** 当 BIGWIN 且 grid_number 为 5 或 30 时,权重固定为 10000 便于展示 */
watch(
() => [formData.tier, formData.grid_number],
() => {
if (
formData.tier === 'BIGWIN' &&
(formData.grid_number === 5 || formData.grid_number === 30)
) {
formData.weight = 10000
}
}
)
/**
* 初始化页面数据
*/
@@ -203,7 +169,7 @@
*/
const initForm = () => {
if (!props.data) return
const numKeys = ['id', 'grid_number', 'real_ev', 'weight', 'n_start_index', 's_start_index']
const numKeys = ['id', 'grid_number', 'real_ev', 'weight']
for (const key of Object.keys(formData)) {
if (!(key in props.data)) continue
const val = props.data[key]
@@ -211,11 +177,20 @@
if (numKeys.includes(key)) {
const numVal = Number(val)
;(formData as Record<string, unknown>)[key] =
key === 'id' ? numVal || null : Number.isNaN(numVal) ? 0 : numVal
key === 'id'
? numVal || null
: Number.isNaN(numVal)
? key === 'weight'
? 10000
: 0
: numVal
} else {
;(formData as Record<string, unknown>)[key] = val ?? ''
}
}
if (formData.tier === 'BIGWIN' && (formData.weight === undefined || formData.weight === null)) {
formData.weight = [5, 30].includes(Number(formData.grid_number)) ? 10000 : 0
}
}
/**
@@ -233,14 +208,13 @@
if (!formRef.value) return
try {
await formRef.value.validate()
const payload = { ...formData }
const w = Number(payload.weight)
payload.weight = Number.isNaN(w) ? 1 : Math.max(1, Math.min(10000, w))
if (payload.tier === 'BIGWIN' && (payload.grid_number === 5 || payload.grid_number === 30)) {
payload.weight = 10000
const payload = { ...formData } as Record<string, unknown>
if (formData.tier === 'BIGWIN') {
const w = Number(formData.weight)
payload.weight = isBigwinWeightDisabled.value ? 10000 : Number.isNaN(w) ? 10000 : w
} else {
delete payload.weight
}
payload.n_start_index = Number(payload.n_start_index) || 0
payload.s_start_index = Number(payload.s_start_index) || 0
if (props.dialogType === 'add') {
await api.save(payload)
ElMessage.success('新增成功')
@@ -257,9 +231,9 @@
</script>
<style lang="scss" scoped>
.weight-fixed-hint {
margin-top: 6px;
.form-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
</style>

View File

@@ -1,83 +1,73 @@
<template>
<el-dialog
v-model="visible"
title="T1-T5 与 BIGWIN 权重配比"
width="600px"
title="T1-T5 权重配比(顺时针/逆时针)"
width="900px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<div class="global-tip">
权重来自<strong>奖励对照表dice_reward</strong><strong>结束索引DiceRewardConfig.id</strong>区分<strong>顺时针</strong><strong>逆时针</strong>两套权重抽奖时按当前方向取对应权重
</div>
<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-name="色子点数"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t)"
height="220px"
/>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<div class="chart-row">
<ArtBarChart
x-axis-name="结束索引"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t, 'clockwise')"
height="180px"
/>
<ArtBarChart
x-axis-name="结束索引"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t, 'counterclockwise')"
height="180px"
/>
</div>
</div>
<div class="weight-sum" v-if="t !== 'BIGWIN'">
当前档位权重合计<strong>{{ getTierSumForValidation(t) }}</strong>
各条权重 1-10000档位内按权重比抽取 grid_number和不限制
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
当前档位权重合计顺时针<strong>{{ getTierSumForValidation(t, 'clockwise') }}</strong>
逆时针<strong>{{ getTierSumForValidation(t, 'counterclockwise') }}</strong>
各条 1-10000档位内按权重比抽取和不限制
</div>
<div class="weight-sum weight-sum-bigwin" v-else>
BIGWIN 为豹子权重每条权重 1-10000
<div class="weight-sum weight-sum-t4t5" v-else>
T4T5 档位抽中时仅有一个结果无需配置权重
</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="权重(1-10000)" min-width="200" align="center">
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
<el-table-column label="色子点数" prop="grid_number" width="80" 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="70" align="center" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="70" align="center" show-overflow-tooltip />
<el-table-column label="顺时针权重(1-10000)" min-width="160" align="center">
<template #default="{ row }">
<div class="weight-cell-vertical">
<div class="weight-slider-wrap">
<el-slider
:model-value="getItemWeight(row)"
:model-value="getItemWeight(row, 'clockwise')"
:min="1"
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) => setItemWeightByRow(t, row, normalizeSliderValue(v))
"
@update:model-value="(v: number | number[]) => setItemWeightByRow(t, row, 'clockwise', Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
/>
</div>
<div class="weight-input-wrap">
<el-button
type="primary"
link
native-type="button"
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 1"
@click.prevent="adjustWeightByRow(t, row, -1)"
>
</el-button>
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') <= 1"
@click="setItemWeightByRow(t, row, 'clockwise', Math.max(1, getItemWeight(row, 'clockwise') - 1))"
></el-button>
<el-input-number
:model-value="getItemWeight(row)"
:model-value="getItemWeight(row, 'clockwise')"
:min="1"
:max="10000"
:step="1"
@@ -85,17 +75,57 @@
controls-position="right"
size="small"
class="weight-input"
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, 'clockwise', typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
/>
<el-button
type="primary"
link
native-type="button"
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 10000"
@click.prevent="adjustWeightByRow(t, row, 1)"
>
</el-button>
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') >= 10000"
@click="setItemWeightByRow(t, row, 'clockwise', Math.min(10000, getItemWeight(row, 'clockwise') + 1))"
></el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="逆时针权重(1-10000)" min-width="160" align="center">
<template #default="{ row }">
<div class="weight-cell-vertical">
<div class="weight-slider-wrap">
<el-slider
:model-value="getItemWeight(row, 'counterclockwise')"
:min="1"
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="(v: number | number[]) => setItemWeightByRow(t, row, 'counterclockwise', Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
/>
</div>
<div class="weight-input-wrap">
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') <= 1"
@click="setItemWeightByRow(t, row, 'counterclockwise', Math.max(1, getItemWeight(row, 'counterclockwise') - 1))"
></el-button>
<el-input-number
:model-value="getItemWeight(row, 'counterclockwise')"
:min="1"
:max="10000"
:step="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, 'counterclockwise', typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
/>
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') >= 10000"
@click="setItemWeightByRow(t, row, 'counterclockwise', Math.min(10000, getItemWeight(row, 'counterclockwise') + 1))"
></el-button>
</div>
</div>
</template>
@@ -116,7 +146,21 @@
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
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
type DirectionKey = 'clockwise' | 'counterclockwise'
interface WeightRow {
reward_id_clockwise?: number
reward_id_counterclockwise?: number
id?: number
grid_number?: number
real_ev?: number
ui_text?: string
remark?: string
tier?: string
weight_clockwise: number
weight_counterclockwise: number
}
interface Props {
modelValue: boolean
@@ -142,170 +186,176 @@
const activeTier = ref('T1')
const submitting = ref(false)
/** 按档位分组的数据:{ T1: [...], T2: [...], ... },每项为可修改 weight 的副本 */
const grouped = ref<Record<string, Array<Record<string, unknown> & { weight: number }>>>({
const grouped = ref<Record<string, WeightRow[]>>({
T1: [],
T2: [],
T3: [],
T4: [],
T5: [],
BIGWIN: []
T5: []
})
function getTierItems(tier: string) {
function getTierItems(tier: string): WeightRow[] {
return grouped.value[tier] ?? []
}
/** 图表横坐标为色子点数grid_number */
function getTierChartLabels(tier: string): string[] {
const items = getTierItems(tier)
return items.map((r) => String(r.grid_number ?? ''))
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
}
function getTierChartData(tier: string): number[] {
const items = getTierItems(tier)
return items.map((r) => getItemWeight(r)).map((n) => Number(n))
function getTierChartData(tier: string, dir: DirectionKey): number[] {
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
return getTierItems(tier).map((r) => toWeightPrecision(r[key] ?? 1))
}
/** 档位权重和(仅展示用,不限制) */
function getTierSumForValidation(tier: string): number {
const items = getTierItems(tier)
return items.reduce((s, r) => s + getItemWeight(r), 0)
function getTierSumForValidation(tier: string, dir: DirectionKey): number {
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
return getTierItems(tier).reduce((s, r) => s + toWeightPrecision(r[key] ?? 1), 0)
}
/** 读取权重1-10000 整数(兼容后端 int 返回为 number 或 string */
function getItemWeight(row: Record<string, unknown> & { weight?: number | string }): number {
const w = row.weight
function getItemWeight(row: WeightRow, dir: DirectionKey): number {
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
const w = row[key]
const num = typeof w === 'number' && !Number.isNaN(w) ? w : Number(w)
if (Number.isNaN(num)) return 1
const n = Math.max(1, Math.min(10000, Math.floor(num)))
return n
return toWeightPrecision(num)
}
function setItemWeight(row: Record<string, unknown> & { weight: number }, value: number) {
const v = toWeightPrecision(value)
row.weight = v
}
/** 权重 1-10000 整数 */
function toWeightPrecision(value: number): number {
const n = Math.max(1, Math.min(10000, Math.floor(value)))
return n
}
/** 按档位与行更新权重:原地修改 list 中对应项 */
function setItemWeightByRow(tier: string, row: Record<string, unknown> & { weight: number }, value: number) {
function setItemWeightByRow(tier: string, row: WeightRow, dir: DirectionKey, value: number) {
const v = toWeightPrecision(value)
const list = grouped.value[tier]
if (!list) return
const idx = list.findIndex((r) => r === row || (r.id != null && row.id != null && r.id === row.id))
const rid = dir === 'clockwise' ? row.reward_id_clockwise : row.reward_id_counterclockwise
const idx = list.findIndex(
(r) =>
r === row ||
(rid != null && (dir === 'clockwise' ? r.reward_id_clockwise : r.reward_id_counterclockwise) === rid)
)
if (idx >= 0) {
list[idx].weight = v
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
list[idx][key] = v
} else {
row.weight = v
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
row[key] = v
}
}
function adjustWeightByRow(tier: string, row: Record<string, unknown> & { weight: number }, delta: number) {
const cur = getItemWeight(row)
setItemWeightByRow(tier, row, cur + delta)
}
function adjustWeight(row: Record<string, unknown> & { weight: number }, delta: number) {
const cur = getItemWeight(row)
setItemWeight(row, cur + delta)
}
/** BIGWIN 的 grid_number=5、30 不可修改权重(固定 10000 */
function isWeightDisabled(row: Record<string, unknown>, tier: string): boolean {
if (tier === 'BIGWIN' && (row.grid_number === 5 || row.grid_number === 30)) return true
function isWeightDisabled(row: WeightRow, tier: string): boolean {
if (tier === 'T4' || tier === 'T5') 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
/** 解析 tier -> { 0: [], 1: [] },按 grid_number 合并为每档位一行,含 reward_id 与双方向权重 */
function parseWeightRatioPayload(res: any): Record<string, WeightRow[]> {
const empty: Record<string, WeightRow[]> = {
T1: [],
T2: [],
T3: [],
T4: [],
T5: []
}
if (!res || typeof res !== 'object') return empty
const raw = res?.data ?? res
if (!raw || typeof raw !== 'object') return empty
const out = { ...empty }
for (const t of TIER_KEYS) {
const tierData = raw[t]
if (!tierData || typeof tierData !== 'object') continue
const list0 = Array.isArray(tierData[0]) ? tierData[0] : []
const list1 = Array.isArray(tierData[1]) ? tierData[1] : []
const byGrid = new Map<
number,
{
reward_id_clockwise?: number
reward_id_counterclockwise?: number
id?: number
grid_number?: number
real_ev?: number
ui_text?: string
remark?: string
weight_clockwise: number
weight_counterclockwise: number
}
>()
for (const r of list0) {
const gn = r.grid_number != null ? Number(r.grid_number) : NaN
if (!Number.isNaN(gn)) {
byGrid.set(gn, {
reward_id_clockwise: r.reward_id != null ? Number(r.reward_id) : undefined,
id: r.id != null ? Number(r.id) : undefined,
grid_number: gn,
real_ev: r.real_ev != null ? Number(r.real_ev) : undefined,
ui_text: r.ui_text,
remark: r.remark,
weight_clockwise: normalizeWeightValue(r.weight),
weight_counterclockwise: 1
})
}
}
for (const r of list1) {
const gn = r.grid_number != null ? Number(r.grid_number) : NaN
if (!Number.isNaN(gn)) {
const cur = byGrid.get(gn)
if (cur) {
cur.reward_id_counterclockwise = r.reward_id != null ? Number(r.reward_id) : undefined
cur.weight_counterclockwise = normalizeWeightValue(r.weight)
} else {
byGrid.set(gn, {
reward_id_counterclockwise: r.reward_id != null ? Number(r.reward_id) : undefined,
id: r.id != null ? Number(r.id) : undefined,
grid_number: gn,
real_ev: r.real_ev != null ? Number(r.real_ev) : undefined,
ui_text: r.ui_text,
remark: r.remark,
weight_clockwise: 1,
weight_counterclockwise: normalizeWeightValue(r.weight)
})
}
}
}
out[t] = Array.from(byGrid.values())
}
return out
}
/**
* 从接口返回值中解析出按档位分组的对象。
* 兼容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 normalizeWeightValue(v: unknown): number {
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
if (Number.isNaN(num)) return 1
return Math.max(1, Math.min(10000, Math.floor(num)))
}
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) => {
const raw =
typeof r.weight === 'number' && !Number.isNaN(r.weight) ? r.weight : Number(r.weight) || 1
return { ...r, weight: Math.max(1, Math.min(10000, Math.floor(raw))) }
})
}
grouped.value = next
grouped.value = parseWeightRatioPayload(res)
})
.catch(() => {
ElMessage.error('获取权重配比数据失败')
})
}
function validateAll(): boolean {
return true
}
/**
* 收集所有档位的 id + weight1-10000BIGWIN 的 5/30 提交时固定为 10000。
*/
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, weight } */
function collectItems(): Array<{ id: number; weight: number }> {
const byId = new Map<number, number>()
const items: Array<{ id: number; weight: 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) ? 10000 : getItemWeight(row)
byId.set(id, w)
}
const w0 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'clockwise')
const w1 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'counterclockwise')
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
if (rid1 > 0) items.push({ id: rid1, weight: w1 })
}
}
return Array.from(byId.entries()).map(([id, weight]) => ({ id, weight }))
return items
}
function handleSubmit() {
if (!validateAll()) return
const items = collectItems()
if (items.length === 0) {
ElMessage.info('没有可提交的配置')
@@ -346,16 +396,31 @@
.chart-wrap {
margin-bottom: 12px;
}
.chart-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
> * {
flex: 1;
min-width: 200px;
}
}
.weight-sum {
margin-bottom: 12px;
font-size: 13px;
.over {
color: var(--el-color-danger);
}
}
.weight-sum-bigwin {
.weight-sum-t4t5 {
color: var(--el-text-color-secondary);
}
.global-tip {
margin-bottom: 12px;
padding: 10px 12px;
font-size: 13px;
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
border-radius: 6px;
line-height: 1.5;
}
.weight-table {
margin-top: 8px;
}
@@ -367,7 +432,7 @@
}
.weight-slider-wrap {
width: 100%;
min-width: 100px;
min-width: 80px;
padding: 0 8px;
.weight-slider {
width: 100%;
@@ -377,9 +442,6 @@
display: flex;
align-items: center;
gap: 6px;
.weight-input {
width: 100px;
}
}
.empty-tip {
padding: 24px;