优化中奖权重计算方式
This commit is contained in:
61
saiadmin-artd/src/views/plugin/dice/api/reward/index.ts
Normal file
61
saiadmin-artd/src/views/plugin/dice/api/reward/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 奖励对照(dice_reward)API
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 分页列表,按 direction 区分顺时针(0)/逆时针(1)
|
||||
* @param params direction(必), tier(选), page, limit, orderField, orderType
|
||||
*/
|
||||
list(params: Record<string, any>) {
|
||||
return request.get<Api.Common.ApiPage>({
|
||||
url: '/core/dice/reward/DiceReward/index',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重编辑弹窗:按档位分组获取当前方向的配置+权重(单方向)
|
||||
* @param direction 0=顺时针 1=逆时针
|
||||
*/
|
||||
weightRatioList(direction: 0 | 1) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dice/reward/DiceReward/weightRatioList',
|
||||
params: { direction }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重编辑弹窗:按档位分组获取配置+顺时针/逆时针权重(dice_reward 双方向)
|
||||
*/
|
||||
weightRatioListWithDirection() {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dice/reward/DiceReward/weightRatioListWithDirection'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重编辑弹窗:按 DiceReward 主键 id 批量更新 weight
|
||||
* @param items [{ id: DiceReward.id, weight: 1-10000 }, ...]
|
||||
*/
|
||||
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward/DiceReward/batchUpdateWeights',
|
||||
data: { items }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重编辑弹窗:批量更新当前方向的权重(单方向)
|
||||
*/
|
||||
batchUpdateWeightsByDirection(
|
||||
direction: 0 | 1,
|
||||
items: Array<{ id: number; weight: number }>
|
||||
) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward/DiceReward/batchUpdateWeightsByDirection',
|
||||
data: { direction, items }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,9 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* T1-T5、BIGWIN 权重配比:批量更新权重(同一档位权重之和必须等于 100%)
|
||||
* T1-T5、BIGWIN 权重配比:批量更新顺时针/逆时针权重(写入 dice_reward)
|
||||
*/
|
||||
/** 按 DiceReward 主键 id 批量更新 weight;items: [{ id, weight }, ...] */
|
||||
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
|
||||
@@ -82,6 +83,20 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建奖励对照:按当前奖励配置为顺时针(0)、逆时针(1)生成所有色子可能对应的 dice_reward 记录,权重默认 1,可在奖励对照页权重编辑中调整
|
||||
*/
|
||||
createRewardReference() {
|
||||
return request.post<{
|
||||
created_clockwise: number
|
||||
created_counterclockwise: number
|
||||
updated_clockwise: number
|
||||
updated_counterclockwise: number
|
||||
}>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重配比测试:按当前配置模拟 N 次抽奖,返回各 grid_number 落点次数
|
||||
* @param test_count 100 | 500 | 1000
|
||||
|
||||
127
saiadmin-artd/src/views/plugin/dice/reward/index/index.vue
Normal file
127
saiadmin-artd/src/views/plugin/dice/reward/index/index.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 方向切换 + 搜索 -->
|
||||
<div class="direction-bar">
|
||||
<el-radio-group v-model="currentDirection" size="default" @change="onDirectionChange">
|
||||
<el-radio-button :value="0">顺时针</el-radio-button>
|
||||
<el-radio-button :value="1">逆时针</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'dice:reward:index:update'"
|
||||
type="primary"
|
||||
@click="weightRatioVisible = true"
|
||||
v-ripple
|
||||
>
|
||||
权重配比
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
|
||||
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import api from '../../api/reward/index'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
|
||||
|
||||
const currentDirection = ref<0 | 1>(0)
|
||||
const weightRatioVisible = ref(false)
|
||||
|
||||
const searchForm = ref<Record<string, unknown>>({
|
||||
direction: 0,
|
||||
tier: undefined
|
||||
})
|
||||
|
||||
const listApi = (params: Record<string, any>) => {
|
||||
return api.list({ ...params, direction: currentDirection.value })
|
||||
}
|
||||
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, { ...params, direction: currentDirection.value })
|
||||
getData()
|
||||
}
|
||||
|
||||
const onDirectionChange = () => {
|
||||
searchForm.value.direction = currentDirection.value
|
||||
Object.assign(searchParams, { direction: currentDirection.value, tier: searchForm.value.tier })
|
||||
getData()
|
||||
}
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: listApi,
|
||||
apiParams: { direction: 0, limit: 100 },
|
||||
columnsFactory: () => [
|
||||
{ prop: 'start_index', label: '起始索引', width: 100, align: 'center' },
|
||||
{ prop: 'end_index', label: '结束索引(end_index)', width: 110, align: 'center' },
|
||||
{ prop: 'tier', label: '档位', width: 90, align: 'center', sortable: true },
|
||||
{
|
||||
prop: 'grid_number',
|
||||
label: '色子点数(摇取5-30)',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'ui_text',
|
||||
label: '显示文本',
|
||||
minWidth: 100,
|
||||
align: 'center',
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{ prop: 'real_ev', label: '实际中奖金额', width: 110, align: 'center' },
|
||||
{ prop: 'remark', label: '备注', minWidth: 80, align: 'center', showOverflowTooltip: true },
|
||||
{ prop: 'weight', label: '权重(1-10000)', width: 110, align: 'center' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
searchParams.direction = currentDirection.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.direction-bar {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
>
|
||||
<el-col v-bind="setSpan(8)">
|
||||
<el-form-item label="档位" prop="tier">
|
||||
<el-select v-model="formData.tier" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="T1" value="T1" />
|
||||
<el-option label="T2" value="T2" />
|
||||
<el-option label="T3" value="T3" />
|
||||
<el-option label="T4" value="T4" />
|
||||
<el-option label="T5" value="T5" />
|
||||
<el-option label="BIGWIN" value="BIGWIN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref?.resetFields?.()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
emit('search', { ...formData.value })
|
||||
}
|
||||
|
||||
const setSpan = (span: number) => ({
|
||||
span,
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 8,
|
||||
lg: span,
|
||||
xl: span
|
||||
})
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,447 @@
|
||||
<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>
|
||||
@@ -10,11 +10,13 @@
|
||||
<ElSpace wrap>
|
||||
<ElButton
|
||||
v-permission="'dice:reward_config:index:update'"
|
||||
type="primary"
|
||||
@click="weightRatioVisible = true"
|
||||
type="warning"
|
||||
:loading="createRewardLoading"
|
||||
@click="handleCreateRewardReference"
|
||||
v-ripple
|
||||
title="按规则:start_index=config(grid_number).id;顺时针 end_index=(start_index+grid_number)%26;逆时针 end_index=start_index-grid_number≥0?start_index-grid_number:26+start_index-grid_number"
|
||||
>
|
||||
T1-T5 与 BIGWIN 权重配比
|
||||
创建奖励对照
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'dice:reward_config:index:index'"
|
||||
@@ -66,24 +68,52 @@
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<!-- T1-T5、BIGWIN 权重配比弹窗 -->
|
||||
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
|
||||
<!-- 权重配比测试弹窗 -->
|
||||
<WeightTestDialog v-model="weightTestVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/reward_config/index'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
|
||||
import WeightTestDialog from './modules/weight-test-dialog.vue'
|
||||
|
||||
const weightRatioVisible = ref(false)
|
||||
const weightTestVisible = ref(false)
|
||||
const createRewardLoading = ref(false)
|
||||
|
||||
async function handleCreateRewardReference() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'按规则创建奖励对照:起始索引 start_index=奖励配置中 grid_number 对应格位的 id;顺时针 end_index=(start_index+摇取点数)%26;逆时针 end_index=start_index-摇取点数≥0 则取该值,否则 26+start_index-摇取点数。先清空现有数据再为 5-30 共 26 个点数、顺/逆时针分别生成。是否继续?',
|
||||
'创建奖励对照',
|
||||
{
|
||||
confirmButtonText: '确定创建',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
createRewardLoading.value = true
|
||||
try {
|
||||
const res: any = await api.createRewardReference()
|
||||
const data = res?.data ?? res
|
||||
const msg =
|
||||
typeof data === 'object' && data !== null
|
||||
? `已按 5-30 共26个点数、顺时针+逆时针创建:顺时针新增 ${data.created_clockwise ?? 0} 条、逆时针新增 ${data.created_counterclockwise ?? 0} 条;顺时针更新 ${data.updated_clockwise ?? 0} 条、逆时针更新 ${data.updated_counterclockwise ?? 0} 条${(data.skipped ?? 0) > 0 ? `;${data.skipped} 个点数使用兜底起始索引` : ''}`
|
||||
: '创建成功'
|
||||
ElMessage.success(msg)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? '创建奖励对照失败')
|
||||
} finally {
|
||||
createRewardLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<Record<string, unknown>>({
|
||||
@@ -121,12 +151,12 @@
|
||||
apiParams: { limit: 100 },
|
||||
columnsFactory: () => [
|
||||
// { type: 'selection' },
|
||||
{ prop: 'id', label: 'ID(索引)', width: 80, align: 'center' },
|
||||
{ prop: 'grid_number', label: '色子点数', align: 'center' },
|
||||
{ prop: 'ui_text', label: '前端显示文本', align: 'center' },
|
||||
{ prop: 'real_ev', label: '真实资金结算', align: 'center' },
|
||||
{ prop: 'tier', label: '所属档位', sortable: true, align: 'center' },
|
||||
{ prop: 'weight', label: '权重(1-10000)', width: 110, align: 'center' },
|
||||
{ prop: 'weight', label: '权重(1-10000)', sortable: true, align: 'center' },
|
||||
// 权重已迁移至「权重配比」弹窗(dice_reward 表,区分顺时针/逆时针)
|
||||
// { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' },
|
||||
{
|
||||
prop: 'operation',
|
||||
|
||||
@@ -37,25 +37,22 @@
|
||||
<el-option label="BIGWIN(超级大奖)" value="BIGWIN" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="权重(1-10000)" prop="weight">
|
||||
<!-- BIGWIN 时可编辑权重:10000=100% 中奖,0=0% 中奖;点数 5、30 固定 100% 不可改 -->
|
||||
<el-form-item v-if="formData.tier === 'BIGWIN'" label="大奖权重" prop="weight">
|
||||
<el-input-number
|
||||
v-model="formData.weight"
|
||||
:min="1"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
:disabled="isWeightFixed10000"
|
||||
placeholder="1-10000"
|
||||
:step="100"
|
||||
placeholder="0~10000,10000=100%中奖"
|
||||
:disabled="isBigwinWeightDisabled"
|
||||
/>
|
||||
<div v-if="isWeightFixed10000" class="weight-fixed-hint">
|
||||
色子点数 5、30 固定 10000,不可修改权重
|
||||
<div v-if="isBigwinWeightDisabled" class="form-tip">
|
||||
点数 5、30 摇到必中大奖,权重固定 10000
|
||||
</div>
|
||||
<div v-else class="form-tip">10000=100% 中奖,0=0% 中奖;仅对点数 10/15/20/25 生效</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="顺时针起始索引" prop="s_start_index">
|
||||
<el-input-number v-model="formData.s_start_index" :min="0" :step="1" placeholder="s_start_index" />
|
||||
</el-form-item>
|
||||
<el-form-item label="逆时针起始索引" prop="n_start_index">
|
||||
<el-input-number v-model="formData.n_start_index" :min="0" :step="1" placeholder="n_start_index" />
|
||||
</el-form-item>
|
||||
<!-- 权重已迁移至「T1-T5 与 BIGWIN 权重配比」弹窗(dice_reward 表);BIGWIN 时本弹窗可编辑 weight;起始索引已迁移至 dice_reward.start_index -->
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
@@ -108,38 +105,22 @@
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/** BIGWIN 且 grid_number 为 5 或 30 时权重固定为 10000(禁止手动调整) */
|
||||
const isWeightFixed10000 = computed(
|
||||
() =>
|
||||
formData.tier === 'BIGWIN' &&
|
||||
(formData.grid_number === 5 || formData.grid_number === 30)
|
||||
)
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
* 表单验证规则(权重已迁移至权重配比弹窗)
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
grid_number: [{ required: true, message: '色子点数必需填写', trigger: 'blur' }],
|
||||
ui_text: [{ required: true, message: '前端显示文本必需填写', trigger: 'blur' }],
|
||||
real_ev: [{ required: true, message: '真实资金结算必需填写', trigger: 'blur' }],
|
||||
tier: [{ required: true, message: '所属档位必需填写', trigger: 'blur' }],
|
||||
weight: [
|
||||
{
|
||||
validator: (_rule: unknown, value: number | null, callback: (e?: Error) => void) => {
|
||||
const n = value != null ? Number(value) : NaN
|
||||
if (Number.isNaN(n) || n < 1 || n > 10000) {
|
||||
callback(new Error('权重必须为 1-10000'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
n_start_index: [],
|
||||
s_start_index: []
|
||||
weight: [{ type: 'number', min: 0, max: 10000, message: '大奖权重 0~10000', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 点数 5、30 固定 100% 中大奖,权重不可改 */
|
||||
const isBigwinWeightDisabled = computed(
|
||||
() => formData.tier === 'BIGWIN' && [5, 30].includes(Number(formData.grid_number))
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
@@ -149,10 +130,8 @@
|
||||
ui_text: '',
|
||||
real_ev: '',
|
||||
tier: '',
|
||||
weight: 1 as number,
|
||||
n_start_index: 0 as number,
|
||||
s_start_index: 0 as number,
|
||||
remark: ''
|
||||
remark: '',
|
||||
weight: 10000 as number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,19 +151,6 @@
|
||||
}
|
||||
)
|
||||
|
||||
/** 当 BIGWIN 且 grid_number 为 5 或 30 时,权重固定为 10000 便于展示 */
|
||||
watch(
|
||||
() => [formData.tier, formData.grid_number],
|
||||
() => {
|
||||
if (
|
||||
formData.tier === 'BIGWIN' &&
|
||||
(formData.grid_number === 5 || formData.grid_number === 30)
|
||||
) {
|
||||
formData.weight = 10000
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
@@ -203,7 +169,7 @@
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (!props.data) return
|
||||
const numKeys = ['id', 'grid_number', 'real_ev', 'weight', 'n_start_index', 's_start_index']
|
||||
const numKeys = ['id', 'grid_number', 'real_ev', 'weight']
|
||||
for (const key of Object.keys(formData)) {
|
||||
if (!(key in props.data)) continue
|
||||
const val = props.data[key]
|
||||
@@ -211,11 +177,20 @@
|
||||
if (numKeys.includes(key)) {
|
||||
const numVal = Number(val)
|
||||
;(formData as Record<string, unknown>)[key] =
|
||||
key === 'id' ? numVal || null : Number.isNaN(numVal) ? 0 : numVal
|
||||
key === 'id'
|
||||
? numVal || null
|
||||
: Number.isNaN(numVal)
|
||||
? key === 'weight'
|
||||
? 10000
|
||||
: 0
|
||||
: numVal
|
||||
} else {
|
||||
;(formData as Record<string, unknown>)[key] = val ?? ''
|
||||
}
|
||||
}
|
||||
if (formData.tier === 'BIGWIN' && (formData.weight === undefined || formData.weight === null)) {
|
||||
formData.weight = [5, 30].includes(Number(formData.grid_number)) ? 10000 : 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,14 +208,13 @@
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
const payload = { ...formData }
|
||||
const w = Number(payload.weight)
|
||||
payload.weight = Number.isNaN(w) ? 1 : Math.max(1, Math.min(10000, w))
|
||||
if (payload.tier === 'BIGWIN' && (payload.grid_number === 5 || payload.grid_number === 30)) {
|
||||
payload.weight = 10000
|
||||
const payload = { ...formData } as Record<string, unknown>
|
||||
if (formData.tier === 'BIGWIN') {
|
||||
const w = Number(formData.weight)
|
||||
payload.weight = isBigwinWeightDisabled.value ? 10000 : Number.isNaN(w) ? 10000 : w
|
||||
} else {
|
||||
delete payload.weight
|
||||
}
|
||||
payload.n_start_index = Number(payload.n_start_index) || 0
|
||||
payload.s_start_index = Number(payload.s_start_index) || 0
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(payload)
|
||||
ElMessage.success('新增成功')
|
||||
@@ -257,9 +231,9 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.weight-fixed-hint {
|
||||
margin-top: 6px;
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,83 +1,73 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="T1-T5 与 BIGWIN 权重配比"
|
||||
width="600px"
|
||||
title="T1-T5 权重配比(顺时针/逆时针)"
|
||||
width="900px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="global-tip">
|
||||
权重来自<strong>奖励对照表(dice_reward)</strong>,按<strong>结束索引(DiceRewardConfig.id)</strong>区分<strong>顺时针</strong>与<strong>逆时针</strong>两套权重;抽奖时按当前方向取对应权重。
|
||||
</div>
|
||||
<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">
|
||||
<ArtBarChart
|
||||
x-axis-name="色子点数"
|
||||
:x-axis-data="getTierChartLabels(t)"
|
||||
:data="getTierChartData(t)"
|
||||
height="220px"
|
||||
/>
|
||||
<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 !== 'BIGWIN'">
|
||||
当前档位权重合计:<strong>{{ getTierSumForValidation(t) }}</strong>
|
||||
(各条权重 1-10000,档位内按权重比抽取 grid_number,和不限制)
|
||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||
当前档位权重合计(顺时针):<strong>{{ getTierSumForValidation(t, 'clockwise') }}</strong>
|
||||
;逆时针:<strong>{{ getTierSumForValidation(t, 'counterclockwise') }}</strong>
|
||||
(各条 1-10000,档位内按权重比抽取,和不限制)
|
||||
</div>
|
||||
<div class="weight-sum weight-sum-bigwin" v-else>
|
||||
BIGWIN 为豹子权重,每条权重 1-10000
|
||||
<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="色子点数" prop="grid_number" width="50" 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="80"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="备注"
|
||||
prop="remark"
|
||||
min-width="80"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="权重(1-10000)" min-width="200" align="center">
|
||||
<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="顺时针权重(1-10000)" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="weight-cell-vertical">
|
||||
<div class="weight-slider-wrap">
|
||||
<el-slider
|
||||
:model-value="getItemWeight(row)"
|
||||
: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, normalizeSliderValue(v))
|
||||
"
|
||||
@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
|
||||
native-type="button"
|
||||
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 1"
|
||||
@click.prevent="adjustWeightByRow(t, row, -1)"
|
||||
>
|
||||
-
|
||||
</el-button>
|
||||
: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)"
|
||||
:model-value="getItemWeight(row, 'clockwise')"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
@@ -85,17 +75,57 @@
|
||||
controls-position="right"
|
||||
size="small"
|
||||
class="weight-input"
|
||||
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||||
@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
|
||||
native-type="button"
|
||||
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 10000"
|
||||
@click.prevent="adjustWeightByRow(t, row, 1)"
|
||||
>
|
||||
+
|
||||
</el-button>
|
||||
: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="逆时针权重(1-10000)" 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>
|
||||
@@ -116,7 +146,21 @@
|
||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'] as const
|
||||
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
|
||||
@@ -142,170 +186,176 @@
|
||||
const activeTier = ref('T1')
|
||||
const submitting = ref(false)
|
||||
|
||||
/** 按档位分组的数据:{ T1: [...], T2: [...], ... },每项为可修改 weight 的副本 */
|
||||
const grouped = ref<Record<string, Array<Record<string, unknown> & { weight: number }>>>({
|
||||
const grouped = ref<Record<string, WeightRow[]>>({
|
||||
T1: [],
|
||||
T2: [],
|
||||
T3: [],
|
||||
T4: [],
|
||||
T5: [],
|
||||
BIGWIN: []
|
||||
T5: []
|
||||
})
|
||||
|
||||
function getTierItems(tier: string) {
|
||||
function getTierItems(tier: string): WeightRow[] {
|
||||
return grouped.value[tier] ?? []
|
||||
}
|
||||
|
||||
/** 图表横坐标为色子点数(grid_number) */
|
||||
function getTierChartLabels(tier: string): string[] {
|
||||
const items = getTierItems(tier)
|
||||
return items.map((r) => String(r.grid_number ?? ''))
|
||||
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
|
||||
}
|
||||
|
||||
function getTierChartData(tier: string): number[] {
|
||||
const items = getTierItems(tier)
|
||||
return items.map((r) => getItemWeight(r)).map((n) => Number(n))
|
||||
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 getTierSumForValidation(tier: string): number {
|
||||
const items = getTierItems(tier)
|
||||
return items.reduce((s, r) => s + getItemWeight(r), 0)
|
||||
function getTierSumForValidation(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)
|
||||
}
|
||||
|
||||
/** 读取权重,1-10000 整数(兼容后端 int 返回为 number 或 string) */
|
||||
function getItemWeight(row: Record<string, unknown> & { weight?: number | string }): number {
|
||||
const w = row.weight
|
||||
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
|
||||
const n = Math.max(1, Math.min(10000, Math.floor(num)))
|
||||
return n
|
||||
return toWeightPrecision(num)
|
||||
}
|
||||
|
||||
function setItemWeight(row: Record<string, unknown> & { weight: number }, value: number) {
|
||||
const v = toWeightPrecision(value)
|
||||
row.weight = v
|
||||
}
|
||||
|
||||
/** 权重 1-10000 整数 */
|
||||
function toWeightPrecision(value: number): number {
|
||||
const n = Math.max(1, Math.min(10000, Math.floor(value)))
|
||||
return n
|
||||
}
|
||||
|
||||
/** 按档位与行更新权重:原地修改 list 中对应项 */
|
||||
function setItemWeightByRow(tier: string, row: Record<string, unknown> & { weight: number }, value: number) {
|
||||
function setItemWeightByRow(tier: string, row: WeightRow, dir: DirectionKey, value: number) {
|
||||
const v = toWeightPrecision(value)
|
||||
const list = grouped.value[tier]
|
||||
if (!list) return
|
||||
const idx = list.findIndex((r) => r === row || (r.id != null && row.id != null && r.id === row.id))
|
||||
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].weight = v
|
||||
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||
list[idx][key] = v
|
||||
} else {
|
||||
row.weight = v
|
||||
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||
row[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
function adjustWeightByRow(tier: string, row: Record<string, unknown> & { weight: number }, delta: number) {
|
||||
const cur = getItemWeight(row)
|
||||
setItemWeightByRow(tier, row, cur + delta)
|
||||
}
|
||||
|
||||
function adjustWeight(row: Record<string, unknown> & { weight: number }, delta: number) {
|
||||
const cur = getItemWeight(row)
|
||||
setItemWeight(row, cur + delta)
|
||||
}
|
||||
|
||||
/** BIGWIN 的 grid_number=5、30 不可修改权重(固定 10000) */
|
||||
function isWeightDisabled(row: Record<string, unknown>, tier: string): boolean {
|
||||
if (tier === 'BIGWIN' && (row.grid_number === 5 || row.grid_number === 30)) return true
|
||||
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
||||
if (tier === 'T4' || tier === 'T5') return true
|
||||
return false
|
||||
}
|
||||
|
||||
function normalizeSliderValue(v: number | number[]): number {
|
||||
if (Array.isArray(v)) return (v[0] ?? 0) as number
|
||||
return (v ?? 0) as number
|
||||
/** 解析 tier -> { 0: [], 1: [] },按 grid_number 合并为每档位一行,含 reward_id 与双方向权重 */
|
||||
function parseWeightRatioPayload(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 != null ? Number(r.real_ev) : undefined,
|
||||
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 != null ? Number(r.real_ev) : undefined,
|
||||
ui_text: r.ui_text,
|
||||
remark: r.remark,
|
||||
weight_clockwise: 1,
|
||||
weight_counterclockwise: normalizeWeightValue(r.weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
out[t] = Array.from(byGrid.values())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 从接口返回值中解析出按档位分组的对象。
|
||||
* 兼容:1) 直接返回 { T1: [], T2: [], ... } 2) 包装在 data 中 { data: { T1: [], ... } } 3) 平铺数组按 tier 分组
|
||||
*/
|
||||
function parseWeightRatioPayload(res: any): Record<string, Array<Record<string, unknown>>> {
|
||||
if (!res || typeof res !== 'object') return {}
|
||||
const hasTierKeys = (obj: any) =>
|
||||
obj && typeof obj === 'object' && TIER_KEYS.some((k) => Array.isArray(obj[k]))
|
||||
if (hasTierKeys(res)) return res as Record<string, Array<Record<string, unknown>>>
|
||||
if (hasTierKeys(res?.data)) return res.data as Record<string, Array<Record<string, unknown>>>
|
||||
if (hasTierKeys(res?.data?.data))
|
||||
return res.data.data as Record<string, Array<Record<string, unknown>>>
|
||||
if (Array.isArray(res)) {
|
||||
return res.reduce(
|
||||
(acc: Record<string, Array<Record<string, unknown>>>, r: Record<string, unknown>) => {
|
||||
const t = (r.tier as string) || ''
|
||||
if (t && TIER_KEYS.includes(t as (typeof TIER_KEYS)[number])) {
|
||||
if (!acc[t]) acc[t] = []
|
||||
acc[t].push(r)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Array<Record<string, unknown>>>
|
||||
)
|
||||
}
|
||||
return {}
|
||||
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)))
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
api
|
||||
.weightRatioList()
|
||||
.then((res: any) => {
|
||||
const data = parseWeightRatioPayload(res)
|
||||
const next: Record<string, Array<Record<string, unknown> & { weight: number }>> = {
|
||||
T1: [],
|
||||
T2: [],
|
||||
T3: [],
|
||||
T4: [],
|
||||
T5: [],
|
||||
BIGWIN: []
|
||||
}
|
||||
for (const t of TIER_KEYS) {
|
||||
const list = Array.isArray(data[t]) ? data[t] : []
|
||||
next[t] = list.map((r) => {
|
||||
const raw =
|
||||
typeof r.weight === 'number' && !Number.isNaN(r.weight) ? r.weight : Number(r.weight) || 1
|
||||
return { ...r, weight: Math.max(1, Math.min(10000, Math.floor(raw))) }
|
||||
})
|
||||
}
|
||||
grouped.value = next
|
||||
grouped.value = parseWeightRatioPayload(res)
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('获取权重配比数据失败')
|
||||
})
|
||||
}
|
||||
|
||||
function validateAll(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有档位的 id + weight(1-10000),BIGWIN 的 5/30 提交时固定为 10000。
|
||||
*/
|
||||
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, weight } */
|
||||
function collectItems(): Array<{ id: number; weight: number }> {
|
||||
const byId = new Map<number, number>()
|
||||
const items: Array<{ id: number; weight: number }> = []
|
||||
for (const t of TIER_KEYS) {
|
||||
for (const row of getTierItems(t)) {
|
||||
const id = row.id != null ? Number(row.id) : 0
|
||||
if (id >= 0 && !Number.isNaN(id)) {
|
||||
const w = isWeightDisabled(row, t) ? 10000 : getItemWeight(row)
|
||||
byId.set(id, w)
|
||||
}
|
||||
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 Array.from(byId.entries()).map(([id, weight]) => ({ id, weight }))
|
||||
return items
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!validateAll()) return
|
||||
const items = collectItems()
|
||||
if (items.length === 0) {
|
||||
ElMessage.info('没有可提交的配置')
|
||||
@@ -346,16 +396,31 @@
|
||||
.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;
|
||||
.over {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
.weight-sum-bigwin {
|
||||
.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;
|
||||
}
|
||||
@@ -367,7 +432,7 @@
|
||||
}
|
||||
.weight-slider-wrap {
|
||||
width: 100%;
|
||||
min-width: 100px;
|
||||
min-width: 80px;
|
||||
padding: 0 8px;
|
||||
.weight-slider {
|
||||
width: 100%;
|
||||
@@ -377,9 +442,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
.weight-input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
.empty-tip {
|
||||
padding: 24px;
|
||||
|
||||
Reference in New Issue
Block a user