Files
dafuweng-saiadmin6.x/saiadmin-artd/src/views/plugin/dice/reward/index/modules/weight-ratio-dialog.vue

612 lines
22 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="$t('page.weightRatio.title')"
width="900px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<div class="global-tip">
{{ $t('page.weightRatio.globalTip') }}
</div>
<div v-loading="loading" class="dialog-body">
<!-- 一级方向懒加载避免逆时针柱状图在隐藏容器内初始化导致不显示二级档位 -->
<el-tabs v-model="activeDirection" type="card" class="direction-tabs" :lazy="true">
<el-tab-pane :label="$t('page.weightRatio.tabClockwise')" 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">{{ $t('page.weightShared.emptyTier') }}</div>
<template v-else>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<ArtBarChart
:key="'cw-' + activeDirection + '-' + t"
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartDataForCurrentDirection(t)"
height="180px"
/>
</div>
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
{{
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
}}
</div>
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column
:label="$t('page.weightShared.colGridNumber')"
prop="grid_number"
width="110"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colEndIndexId')"
prop="id"
width="90"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colRealEv')"
prop="real_ev"
width="90"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<span>{{ formatMoney2(row?.real_ev) }}</span>
</template>
</el-table-column>
<el-table-column
:label="$t('page.weightShared.colUiText')"
prop="ui_text"
min-width="70"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colRemark')"
prop="remark"
min-width="70"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="currentWeightColumnLabel"
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="$t('page.weightRatio.tabCounterclockwise')" 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">{{ $t('page.weightShared.emptyTier') }}</div>
<template v-else>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<ArtBarChart
:key="'ccw-' + activeDirection + '-' + t"
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartDataForCurrentDirection(t)"
height="180px"
/>
</div>
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
{{
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
}}
</div>
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column
:label="$t('page.weightShared.colGridNumber')"
prop="grid_number"
width="110"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colEndIndexId')"
prop="id"
width="90"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colRealEv')"
prop="real_ev"
width="90"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colUiText')"
prop="ui_text"
min-width="70"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colRemark')"
prop="remark"
min-width="70"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="currentWeightColumnLabel"
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">{{ $t('page.weightShared.btnCancel') }}</el-button>
<el-button
v-permission="'dice:reward:index:batchUpdateWeights'"
type="primary"
:loading="submitting"
@click="handleSubmit"
>{{ $t('page.weightShared.btnSubmit') }}</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'
import { useI18n } from 'vue-i18n'
function formatMoney2(val: unknown): string {
if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-'
return n.toFixed(2)
}
const { t, locale } = useI18n()
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 currentWeightColumnLabel = computed(() => {
locale.value
const dirLabel =
activeDirection.value === '0'
? t('page.weightRatio.tabClockwise')
: t('page.weightRatio.tabCounterclockwise')
return `${dirLabel} ${t('page.weightShared.weightColSuffix')}`
})
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(t('page.weightShared.fetchFail'))
})
.finally(() => {
loading.value = false
})
}
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, reward_id, weight },后端按 id 更新reward_id 作回退) */
function collectItems(): Array<{ id: number; reward_id: number; weight: number }> {
const items: Array<{ id: number; reward_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, reward_id: rid, weight: w })
}
}
}
return items
}
function handleSubmit() {
const items = collectItems()
if (items.length === 0) {
ElMessage.info(t('page.weightShared.nothingToSubmit'))
return
}
submitting.value = true
api
.batchUpdateWeights(items)
.then(() => {
ElMessage.success(t('page.weightShared.saveSuccess'))
emit('success')
handleClose()
})
.catch((e: { message?: string }) => {
ElMessage.error(e?.message ?? t('page.weightShared.submitFail'))
})
.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;
min-height: 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;
.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>