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

575 lines
19 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.weightEdit.title')"
width="900px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<div class="global-tip">
{{ $t('page.weightEdit.globalTip') }}
</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">{{ $t('page.weightShared.emptyTier') }}</div>
<template v-else>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<div class="chart-row">
<ArtBarChart
:x-axis-name="$t('page.weightShared.xAxisEndIndex')"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t, 'clockwise')"
height="180px"
/>
<ArtBarChart
:x-axis-name="$t('page.weightShared.xAxisEndIndex')"
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t, 'counterclockwise')"
height="180px"
/>
</div>
</div>
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
{{
$t('page.weightShared.sumLineDual', {
cw: getTierSum(t, 'clockwise'),
ccw: getTierSum(t, 'counterclockwise')
})
}}
</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.colEndIndexId')"
prop="id"
width="90"
align="center"
show-overflow-tooltip
/>
<el-table-column
:label="$t('page.weightShared.colDicePoints')"
prop="grid_number"
width="80"
align="center"
/>
<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="$t('page.weightShared.colWeightCwDir')" 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="$t('page.weightShared.colWeightCcwDir')" 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">{{ $t('page.weightShared.btnCancel') }}</el-button>
<el-button 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'
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)
}
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
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(t('page.weightShared.fetchFail'))
})
.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(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()
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>