优化中奖权重计算方式
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="奖励对照表(dice_reward)权重配比"
|
||||
width="900px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="global-tip">
|
||||
编辑的是<strong>奖励对照表(dice_reward / DiceReward 模型)</strong>的权重,按<strong>结束索引(end_index)</strong>区分
|
||||
<strong>顺时针</strong>与<strong>逆时针</strong>两套权重;抽奖时按当前方向取对应权重。
|
||||
</div>
|
||||
<div v-loading="loading" class="dialog-body">
|
||||
<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" 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 !== 'T4' && t !== 'T5'">
|
||||
当前档位权重合计(顺时针):<strong>{{ getTierSum(t, 'clockwise') }}</strong>
|
||||
;逆时针:<strong>{{ getTierSum(t, 'counterclockwise') }}</strong>
|
||||
(各条 1-10000,和不限制)
|
||||
</div>
|
||||
<div class="weight-sum weight-sum-t4t5" v-else>T4、T5 仅单一结果,无需配置权重。</div>
|
||||
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||
<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="顺时针权重(direction=0)" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="weight-cell-vertical">
|
||||
<div class="weight-slider-wrap">
|
||||
<el-slider
|
||||
: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, 'clockwise', 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, 'clockwise') <= 1"
|
||||
@click="setItemWeightByRow(t, row, 'clockwise', Math.max(1, getItemWeight(row, 'clockwise') - 1))"
|
||||
>-</el-button>
|
||||
<el-input-number
|
||||
:model-value="getItemWeight(row, 'clockwise')"
|
||||
: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,
|
||||
'clockwise',
|
||||
typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
: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="逆时针权重(direction=1)" 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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<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/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'] 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
|
||||
}
|
||||
|
||||
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 activeTier = ref('T1')
|
||||
const submitting = ref(false)
|
||||
const loading = ref(false)
|
||||
const grouped = ref<Record<string, WeightRow[]>>({
|
||||
T1: [],
|
||||
T2: [],
|
||||
T3: [],
|
||||
T4: [],
|
||||
T5: []
|
||||
})
|
||||
|
||||
function getTierItems(tier: string): WeightRow[] {
|
||||
return grouped.value[tier] ?? []
|
||||
}
|
||||
|
||||
function getTierChartLabels(tier: string): string[] {
|
||||
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
|
||||
}
|
||||
|
||||
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 getTierSum(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)
|
||||
}
|
||||
|
||||
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
|
||||
return toWeightPrecision(num)
|
||||
}
|
||||
|
||||
function toWeightPrecision(value: number): number {
|
||||
return Math.max(1, Math.min(10000, Math.floor(value)))
|
||||
}
|
||||
|
||||
function setItemWeightByRow(tier: string, row: WeightRow, dir: DirectionKey, value: number) {
|
||||
const v = toWeightPrecision(typeof value === 'number' && !Number.isNaN(value) ? value : 1)
|
||||
const list = grouped.value[tier]
|
||||
if (!list) return
|
||||
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||
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][key] = v
|
||||
else row[key] = v
|
||||
}
|
||||
|
||||
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
||||
if (tier === 'T4' || tier === 'T5') return true
|
||||
return false
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
/** 兼容接口返回 tier -> { 0: [], 1: [] },按 grid_number 合并为每档位一行,含 reward_id 与双方向权重 */
|
||||
function parsePayload(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,
|
||||
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,
|
||||
ui_text: r.ui_text,
|
||||
remark: r.remark,
|
||||
weight_clockwise: 1,
|
||||
weight_counterclockwise: normalizeWeightValue(r.weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
out[t] = Array.from(byGrid.values())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
loading.value = true
|
||||
api
|
||||
.weightRatioListWithDirection()
|
||||
.then((res: any) => {
|
||||
grouped.value = parsePayload(res)
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('获取权重数据失败')
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, weight } */
|
||||
function collectItems(): Array<{ id: number; weight: number }> {
|
||||
const items: Array<{ id: number; weight: number }> = []
|
||||
for (const t of TIER_KEYS) {
|
||||
for (const row of getTierItems(t)) {
|
||||
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 items
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
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;
|
||||
}
|
||||
.chart-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
> * {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
.weight-sum {
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.weight-cell-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.weight-slider-wrap {
|
||||
width: 100%;
|
||||
min-width: 80px;
|
||||
padding: 0 8px;
|
||||
:deep(.weight-slider) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.weight-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.empty-tip {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.dialog-body {
|
||||
min-height: 320px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user