重新优化中奖权重计算方式

This commit is contained in:
2026-03-11 15:40:15 +08:00
parent bb166350fd
commit 2af7fedcce
13 changed files with 330 additions and 322 deletions

View File

@@ -22,6 +22,7 @@
// 数据配置
data: () => [0, 0, 0, 0, 0, 0, 0],
xAxisData: () => [],
xAxisName: '',
barWidth: '40%',
stack: false,
@@ -124,24 +125,30 @@
} = useChartComponent({
props,
checkEmpty: () => {
// 检查单数据情况
if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
if (!Array.isArray(props.data) || !props.data.length) return true
const first = props.data[0]
// 单数据情况number[] 或可转为数字的数组(兼容后端 int 返回为 string
if (typeof first === 'number' && !Number.isNaN(first)) {
const singleData = props.data as number[]
return !singleData.length || singleData.every((val) => val === 0)
return singleData.every((val) => val === 0 || Number.isNaN(Number(val)))
}
if (typeof first === 'string' && !Number.isNaN(Number(first))) {
const singleData = props.data.map((v) => Number(v))
return singleData.every((val) => val === 0 || Number.isNaN(val))
}
// 检查多数据情况
if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
// 多数据情况
if (typeof first === 'object' && first !== null && 'name' in first) {
const multiData = props.data as BarDataItem[]
return (
!multiData.length ||
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
return multiData.every(
(item) => !item.data?.length || item.data.every((val) => val === 0 || Number.isNaN(Number(val)))
)
}
return true
},
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
watchSources: [() => props.data, () => props.xAxisData, () => props.xAxisName, () => props.colors],
generateOptions: (): EChartsOption => {
const options: EChartsOption = {
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
@@ -152,6 +159,9 @@
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
xAxis: {
type: 'category',
name: props.xAxisName || undefined,
nameLocation: 'middle',
nameGap: 25,
data: props.xAxisData,
axisTick: getAxisTickStyle(),
axisLine: getAxisLineStyle(props.showAxisLine),

View File

@@ -117,6 +117,8 @@ export interface BarChartProps extends BaseChartProps, AxisDisplayProps, Interac
data: number[] | BarDataItem[]
/** X轴标签数据 */
xAxisData?: string[]
/** X轴名称色子点数 */
xAxisName?: string
/** 柱状图宽度 */
barWidth?: string | number
/** 是否堆叠显示 */

View File

@@ -336,6 +336,13 @@
if (formData.roll_number == null && formData.rollArrayItems.length === 5) {
formData.roll_number = formData.rollArrayItems.reduce((s, n) => (s ?? 0) + (n ?? 0), 0) || null
}
// 点数和有值但五个点数为空或无效时,根据 roll_number 补全显示(兼容历史错误数据)
const sum = formData.roll_number != null ? Number(formData.roll_number) : 0
const hasNull = formData.rollArrayItems.some((n) => n == null)
const itemsSum = formData.rollArrayItems.reduce((s, n) => (s ?? 0) + (n ?? 0), 0) ?? 0
if (sum >= 5 && sum <= 30 && (hasNull || itemsSum !== sum)) {
formData.rollArrayItems = defaultRollArrayItems(sum)
}
}
/** 将接口的 roll_array 转为固定 5 项数组,不足补 null */
@@ -360,6 +367,16 @@
return items.slice(0, 5)
}
/** 点数和有值但五个点数缺失时,根据点数和生成默认 5 个数(与后端 defaultRollArrayForSum 一致) */
function defaultRollArrayItems(sum: number): (number | null)[] {
const s = Math.max(5, Math.min(30, Math.floor(Number(sum))))
const base = Math.floor(s / 5)
const rem = s - 5 * base
const arr: number[] = Array(5).fill(base)
for (let i = 0; i < rem; i++) arr[i]++
return arr.map((v) => Math.max(1, Math.min(6, v)))
}
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()

View File

@@ -114,7 +114,7 @@
{ prop: 'ui_text', label: '前端显示文本', align: 'center' },
{ prop: 'real_ev', label: '真实资金结算', align: 'center' },
{ prop: 'tier', label: '所属档位', sortable: true, align: 'center' },
{ prop: 'weight', label: '权重(%)', width: 100, align: 'center' },
{ prop: 'weight', label: '权重(1-10000)', width: 110, align: 'center' },
// { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' },
{
prop: 'operation',

View File

@@ -37,19 +37,25 @@
<el-option label="BIGWIN超级大奖" value="BIGWIN" />
</el-select>
</el-form-item>
<el-form-item v-if="formData.tier === 'BIGWIN'" label="权重(%)" prop="weight">
<el-slider
<el-form-item label="权重(1-10000)" prop="weight">
<el-input-number
v-model="formData.weight"
:min="0"
:max="100"
:step="0.01"
:disabled="isWeightFixed100"
show-input
:min="1"
:max="10000"
:step="1"
:disabled="isWeightFixed10000"
placeholder="1-10000"
/>
<div v-if="isWeightFixed100" class="weight-fixed-hint">
色子点数 530 固定 100% 豹子不可修改权重
<div v-if="isWeightFixed10000" class="weight-fixed-hint">
色子点数 530 固定 10000不可修改权重
</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>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
@@ -102,8 +108,8 @@
set: (value) => emit('update:modelValue', value)
})
/** BIGWIN 且 grid_number 为 5 或 30 时豹子概率固定为 100%(禁止手动调整) */
const isWeightFixed100 = computed(
/** BIGWIN 且 grid_number 为 5 或 30 时权重固定为 10000(禁止手动调整) */
const isWeightFixed10000 = computed(
() =>
formData.tier === 'BIGWIN' &&
(formData.grid_number === 5 || formData.grid_number === 30)
@@ -120,20 +126,18 @@
weight: [
{
validator: (_rule: unknown, value: number | null, callback: (e?: Error) => void) => {
if (formData.tier !== 'BIGWIN') {
callback()
return
}
const n = value != null ? Number(value) : NaN
if (Number.isNaN(n) || n < 0 || n > 100) {
callback(new Error('权重仅 BIGWIN 可设定,且必须为 0-100%'))
if (Number.isNaN(n) || n < 1 || n > 10000) {
callback(new Error('权重必须为 1-10000'))
return
}
callback()
},
trigger: 'blur'
}
]
],
n_start_index: [],
s_start_index: []
})
/**
@@ -145,7 +149,9 @@
ui_text: '',
real_ev: '',
tier: '',
weight: 0 as number,
weight: 1 as number,
n_start_index: 0 as number,
s_start_index: 0 as number,
remark: ''
}
@@ -166,7 +172,7 @@
}
)
/** 当 BIGWIN 且 grid_number 为 5 或 30 时,权重固定为 100 便于展示 */
/** 当 BIGWIN 且 grid_number 为 5 或 30 时,权重固定为 10000 便于展示 */
watch(
() => [formData.tier, formData.grid_number],
() => {
@@ -174,7 +180,7 @@
formData.tier === 'BIGWIN' &&
(formData.grid_number === 5 || formData.grid_number === 30)
) {
formData.weight = 100
formData.weight = 10000
}
}
)
@@ -197,7 +203,7 @@
*/
const initForm = () => {
if (!props.data) return
const numKeys = ['id', 'grid_number', 'real_ev', 'weight']
const numKeys = ['id', 'grid_number', 'real_ev', 'weight', 'n_start_index', 's_start_index']
for (const key of Object.keys(formData)) {
if (!(key in props.data)) continue
const val = props.data[key]
@@ -228,18 +234,13 @@
try {
await formRef.value.validate()
const payload = { ...formData }
if (payload.tier !== 'BIGWIN') {
payload.weight = 0
} else if (payload.grid_number === 5 || payload.grid_number === 30) {
payload.weight = 100
} 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))
}
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
}
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('新增成功')

View File

@@ -13,19 +13,18 @@
<template v-else>
<div class="chart-wrap">
<ArtBarChart
x-axis-name="色子点数"
: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%
当前档位权重合计<strong>{{ getTierSumForValidation(t) }}</strong>
各条权重 1-10000档位内按权重比抽取 grid_number和不限制
</div>
<div class="weight-sum weight-sum-bigwin" v-else>
BIGWIN 为豹子权重单独设定每条 0100%无合计要求
BIGWIN 为豹子权重每条权重 1-10000
</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column label="色子点数" prop="grid_number" width="50" align="center" />
@@ -50,20 +49,20 @@
align="center"
show-overflow-tooltip
/>
<el-table-column label="权重(%)" min-width="200" align="center">
<el-table-column label="权重(1-10000)" 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"
:min="1"
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) => setItemWeight(row, normalizeSliderValue(v))
(v: number | number[]) => setItemWeightByRow(t, row, normalizeSliderValue(v))
"
/>
</div>
@@ -71,28 +70,29 @@
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 0"
@click="adjustWeight(row, -1)"
native-type="button"
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 1"
@click.prevent="adjustWeightByRow(t, row, -1)"
>
</el-button>
<el-input-number
:model-value="getItemWeight(row)"
:min="0"
:max="100"
:min="1"
:max="10000"
: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)"
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
/>
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 100"
@click="adjustWeight(row, 1)"
native-type="button"
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 10000"
@click.prevent="adjustWeightByRow(t, row, 1)"
>
</el-button>
@@ -156,30 +156,59 @@
return grouped.value[tier] ?? []
}
/** 图表横坐标为色子点数grid_number */
function getTierChartLabels(tier: string): string[] {
const items = getTierItems(tier)
return items.map((r) => (r.ui_text ? String(r.ui_text) : `点数 ${r.grid_number ?? ''}`))
return items.map((r) => String(r.grid_number ?? ''))
}
function getTierChartData(tier: string): number[] {
const items = getTierItems(tier)
return items.map((r) => getItemWeight(r))
return items.map((r) => getItemWeight(r)).map((n) => Number(n))
}
/** 用于 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 {
/** 读取权重1-10000 整数(兼容后端 int 返回为 number 或 string */
function getItemWeight(row: Record<string, unknown> & { weight?: number | string }): number {
const w = row.weight
if (typeof w === 'number' && !Number.isNaN(w)) return Math.max(0, Math.min(100, w))
return 0
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
}
function setItemWeight(row: Record<string, unknown> & { weight: number }, value: number) {
row.weight = Math.max(0, Math.min(100, value))
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) {
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))
if (idx >= 0) {
list[idx].weight = v
} else {
row.weight = 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) {
@@ -187,9 +216,8 @@
setItemWeight(row, cur + delta)
}
/** T4、T5 及 BIGWIN 的 grid_number=5、30 不可修改权重(固定 100% */
/** BIGWIN 的 grid_number=5、30 不可修改权重(固定 10000 */
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
}
@@ -242,13 +270,11 @@
}
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
}))
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
})
@@ -258,23 +284,11 @@
}
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。
* 收集所有档位的 id + weight1-10000BIGWIN 的 5/30 提交时固定为 10000
*/
function collectItems(): Array<{ id: number; weight: number }> {
const byId = new Map<number, number>()
@@ -282,8 +296,8 @@
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))
const w = isWeightDisabled(row, t) ? 10000 : getItemWeight(row)
byId.set(id, w)
}
}
}