Files
dafuweng-saiadmin6.x/saiadmin-artd/src/views/plugin/dice/reward/index/modules/weight-edit-dialog.vue
2026-03-13 09:35:43 +08:00

480 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>T4T5 仅单一结果无需配置权重</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
/** 供模板 v-for 使用 */
const tierKeys = TIER_KEYS
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>