448 lines
17 KiB
Vue
448 lines
17 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="visible"
|
||
title="权重配比"
|
||
width="900px"
|
||
align-center
|
||
:close-on-click-modal="false"
|
||
@close="handleClose"
|
||
>
|
||
<div class="global-tip">
|
||
配置<strong>奖励对照表(dice_reward)</strong>的权重,一级按<strong>方向</strong>(顺时针/逆时针),二级按<strong>档位</strong>(T1-T5);各条权重 1-10000,档位内按权重比抽取。
|
||
</div>
|
||
<div v-loading="loading" class="dialog-body">
|
||
<!-- 一级:方向;二级档位放在各方向 pane 内,切换方向时二级能正常显示 -->
|
||
<el-tabs v-model="activeDirection" type="card" class="direction-tabs">
|
||
<el-tab-pane label="顺时针" name="0">
|
||
<el-tabs v-model="activeTier" type="card" class="tier-tabs">
|
||
<el-tab-pane v-for="t in tierKeys" :key="'cw-' + 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'">
|
||
<ArtBarChart
|
||
x-axis-name="点数"
|
||
:x-axis-data="getTierChartLabels(t)"
|
||
:data="getTierChartDataForCurrentDirection(t)"
|
||
height="180px"
|
||
/>
|
||
</div>
|
||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||
当前档位权重合计:<strong>{{ getTierSumForCurrentDirection(t) }}</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="点数(grid_number)" prop="grid_number" width="110" align="center" show-overflow-tooltip />
|
||
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
|
||
<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="currentDirectionLabel + ' 权重(1-10000)'" min-width="200" align="center">
|
||
<template #default="{ row }">
|
||
<div class="weight-cell-vertical">
|
||
<div class="weight-slider-wrap">
|
||
<el-slider
|
||
:model-value="getItemWeightForCurrentDirection(row)"
|
||
:min="1"
|
||
:max="10000"
|
||
:step="1"
|
||
size="small"
|
||
:disabled="isWeightDisabled(row, t)"
|
||
class="weight-slider"
|
||
@update:model-value="(v: number | number[]) => setItemWeightForCurrentDirection(t, row, Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
|
||
/>
|
||
</div>
|
||
<div class="weight-input-wrap">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1"
|
||
@click="setItemWeightForCurrentDirection(t, row, Math.max(1, getItemWeightForCurrentDirection(row) - 1))"
|
||
>-</el-button>
|
||
<el-input-number
|
||
:model-value="getItemWeightForCurrentDirection(row)"
|
||
: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) => setItemWeightForCurrentDirection(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) >= 10000"
|
||
@click="setItemWeightForCurrentDirection(t, row, Math.min(10000, getItemWeightForCurrentDirection(row) + 1))"
|
||
>+</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="逆时针 (direction=1)" name="1">
|
||
<el-tabs v-model="activeTier" type="card" class="tier-tabs">
|
||
<el-tab-pane v-for="t in tierKeys" :key="'ccw-' + 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'">
|
||
<ArtBarChart
|
||
x-axis-name="点数"
|
||
:x-axis-data="getTierChartLabels(t)"
|
||
:data="getTierChartDataForCurrentDirection(t)"
|
||
height="180px"
|
||
/>
|
||
</div>
|
||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||
当前档位权重合计:<strong>{{ getTierSumForCurrentDirection(t) }}</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="点数(grid_number)" prop="grid_number" width="110" align="center" show-overflow-tooltip />
|
||
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
|
||
<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="currentDirectionLabel + ' 权重(1-10000)'" min-width="200" align="center">
|
||
<template #default="{ row }">
|
||
<div class="weight-cell-vertical">
|
||
<div class="weight-slider-wrap">
|
||
<el-slider
|
||
:model-value="getItemWeightForCurrentDirection(row)"
|
||
:min="1"
|
||
:max="10000"
|
||
:step="1"
|
||
size="small"
|
||
:disabled="isWeightDisabled(row, t)"
|
||
class="weight-slider"
|
||
@update:model-value="(v: number | number[]) => setItemWeightForCurrentDirection(t, row, Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
|
||
/>
|
||
</div>
|
||
<div class="weight-input-wrap">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1"
|
||
@click="setItemWeightForCurrentDirection(t, row, Math.max(1, getItemWeightForCurrentDirection(row) - 1))"
|
||
>-</el-button>
|
||
<el-input-number
|
||
:model-value="getItemWeightForCurrentDirection(row)"
|
||
: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) => setItemWeightForCurrentDirection(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) >= 10000"
|
||
@click="setItemWeightForCurrentDirection(t, row, Math.min(10000, getItemWeightForCurrentDirection(row) + 1))"
|
||
>+</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</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'
|
||
|
||
/** 当前方向下的行:reward_id=DiceReward 主键(用于提交更新),id=end_index 展示用,weight 为当前方向权重 */
|
||
interface WeightRow {
|
||
reward_id?: number
|
||
id?: number
|
||
grid_number?: number
|
||
real_ev?: number
|
||
ui_text?: string
|
||
remark?: string
|
||
weight: number
|
||
}
|
||
|
||
/** 按档位、方向分组:tier -> { '0': 顺时针行列表, '1': 逆时针行列表 } */
|
||
type GroupedByTierDirection = Record<string, { '0': WeightRow[]; '1': WeightRow[] }>
|
||
|
||
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 activeDirection = ref<'0' | '1'>('0')
|
||
const activeTier = ref('T1')
|
||
const submitting = ref(false)
|
||
const loading = ref(false)
|
||
const grouped = ref<GroupedByTierDirection>({
|
||
T1: { '0': [], '1': [] },
|
||
T2: { '0': [], '1': [] },
|
||
T3: { '0': [], '1': [] },
|
||
T4: { '0': [], '1': [] },
|
||
T5: { '0': [], '1': [] }
|
||
})
|
||
|
||
const currentDirectionLabel = computed(() =>
|
||
activeDirection.value === '0' ? '顺时针' : '逆时针'
|
||
)
|
||
|
||
const tierKeys = TIER_KEYS
|
||
|
||
function getTierItems(tier: string): WeightRow[] {
|
||
const dir = activeDirection.value
|
||
const tierData = grouped.value[tier]
|
||
if (!tierData || !tierData[dir]) return []
|
||
return tierData[dir]
|
||
}
|
||
|
||
function getTierChartLabels(tier: string): string[] {
|
||
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
|
||
}
|
||
|
||
function getTierChartDataForCurrentDirection(tier: string): number[] {
|
||
return getTierItems(tier).map((r) => toWeightPrecision(r.weight ?? 1))
|
||
}
|
||
|
||
function getTierSumForCurrentDirection(tier: string): number {
|
||
return getTierItems(tier).reduce((s, r) => s + toWeightPrecision(r.weight ?? 1), 0)
|
||
}
|
||
|
||
function getItemWeightForCurrentDirection(row: WeightRow): number {
|
||
const w = row.weight
|
||
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 setItemWeightForCurrentDirection(tier: string, row: WeightRow, value: number) {
|
||
const v = toWeightPrecision(value)
|
||
const dir = activeDirection.value
|
||
const tierData = grouped.value[tier]
|
||
if (!tierData || !tierData[dir]) return
|
||
const list = tierData[dir]
|
||
const rid = row.reward_id != null ? row.reward_id : undefined
|
||
list.forEach((r) => {
|
||
if (r === row || (rid != null && r.reward_id != null && r.reward_id === rid)) r.weight = v
|
||
})
|
||
grouped.value[tier][dir] = [...list]
|
||
}
|
||
|
||
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: [] },每行含 reward_id, id(end_index), grid_number, ui_text, real_ev, remark, weight */
|
||
function parsePayload(res: any): GroupedByTierDirection {
|
||
const empty: GroupedByTierDirection = {
|
||
T1: { '0': [], '1': [] },
|
||
T2: { '0': [], '1': [] },
|
||
T3: { '0': [], '1': [] },
|
||
T4: { '0': [], '1': [] },
|
||
T5: { '0': [], '1': [] }
|
||
}
|
||
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
|
||
out[t] = {
|
||
'0': Array.isArray(tierData[0])
|
||
? tierData[0].map((r: any) => ({
|
||
...r,
|
||
reward_id: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||
weight: normalizeWeightValue(r.weight)
|
||
}))
|
||
: [],
|
||
'1': Array.isArray(tierData[1])
|
||
? tierData[1].map((r: any) => ({
|
||
...r,
|
||
reward_id: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||
weight: normalizeWeightValue(r.weight)
|
||
}))
|
||
: []
|
||
}
|
||
}
|
||
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 },直接用于后端按 id 更新 */
|
||
function collectItems(): Array<{ id: number; weight: number }> {
|
||
const items: Array<{ id: number; weight: number }> = []
|
||
for (const t of TIER_KEYS) {
|
||
const tierData = grouped.value[t]
|
||
if (!tierData) continue
|
||
for (const dir of ['0', '1'] as const) {
|
||
const list = tierData[dir] ?? []
|
||
for (const row of list) {
|
||
const rid = row.reward_id != null ? Number(row.reward_id) : 0
|
||
if (rid <= 0) continue
|
||
const w = isWeightDisabled(row, t) ? 10000 : toWeightPrecision(row.weight ?? 1)
|
||
items.push({ id: rid, weight: w })
|
||
}
|
||
}
|
||
}
|
||
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()
|
||
activeDirection.value = '0'
|
||
activeTier.value = 'T1'
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.direction-tabs {
|
||
margin-bottom: 8px;
|
||
}
|
||
.tier-tabs {
|
||
margin-top: 8px;
|
||
}
|
||
.chart-wrap {
|
||
margin-bottom: 12px;
|
||
}
|
||
.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;
|
||
.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>
|