Files
webman-buildadmin/web/src/views/backend/game/config/popupForm.vue

527 lines
20 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>
<!-- 对话框表单 -->
<!-- 建议使用 Prettier 格式化代码 -->
<!-- el-form 内可以混用 el-form-itemFormItemba-input 等输入组件 -->
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
@close="baTable.toggleForm"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div
class="ba-operate-form"
:class="'ba-' + baTable.form.operate + '-form'"
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
>
<el-form
v-if="!baTable.form.loading"
ref="formRef"
@submit.prevent=""
@keyup.enter="baTable.onSubmit(formRef)"
:model="baTable.form.items"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="baTable.form.labelWidth + 'px'"
:rules="rules"
>
<FormItem
:label="t('game.config.channel_id')"
type="remoteSelect"
v-model="baTable.form.items!.channel_id"
prop="channel_id"
:input-attr="{ ...channelRemoteAttr, disabled: metaFieldsDisabled }"
:placeholder="t('Please select field', { field: t('game.config.channel_id') })"
/>
<FormItem
:label="t('game.config.group')"
type="select"
v-model="baTable.form.items!.group"
prop="group"
:input-attr="{ content: groupSelectContentFiltered, disabled: metaFieldsDisabled }"
:placeholder="t('Please select field', { field: t('game.config.group') })"
/>
<FormItem
:label="t('game.config.name')"
type="select"
v-model="baTable.form.items!.name"
prop="name"
:input-attr="{ content: nameSelectContent, disabled: metaFieldsDisabled }"
:placeholder="t('Please select field', { field: t('game.config.name') })"
/>
<FormItem
:label="t('game.config.title')"
type="string"
v-model="baTable.form.items!.title"
prop="title"
:input-attr="{ disabled: metaFieldsDisabled }"
:placeholder="t('Please input field', { field: t('game.config.title') })"
/>
<!-- game_weight数组形式编辑存库仍为 JSON 字符串 -->
<el-form-item v-if="isGameWeight" :label="t('game.config.value')" prop="value">
<div class="weight-value-editor">
<div v-for="(row, idx) in weightRows" :key="idx" class="weight-value-row">
<el-input
v-model="row.key"
class="weight-key"
:readonly="weightKeyReadonly"
:clearable="!weightKeyReadonly"
:placeholder="t('Please input field', { field: t('game.config.weight key') })"
@input="onWeightRowChange"
/>
<span class="weight-sep">:</span>
<el-input
v-model="row.val"
class="weight-val"
:placeholder="t('Please input field', { field: t('game.config.weight value') })"
clearable
:disabled="isDefaultBigwinWeight && isBigwinDiceLockedKey(row.key)"
@input="onWeightRowChange"
/>
<el-button v-if="canEditWeightStructure" type="danger" link @click="removeWeightRow(idx)">
{{ t('Delete') }}
</el-button>
</div>
<el-button v-if="canEditWeightStructure" type="primary" link @click="addWeightRow">{{ t('Add') }}</el-button>
<div v-if="isDefaultBigwinWeight" class="form-help">{{ t('game.config.default_bigwin_weight_help') }}</div>
</div>
</el-form-item>
<FormItem
v-else
:label="t('game.config.value')"
type="textarea"
v-model="baTable.form.items!.value"
prop="value"
:input-attr="{ rows: 3 }"
@keyup.enter.stop=""
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
:placeholder="t('Please input field', { field: t('game.config.value') })"
/>
<FormItem
:label="t('game.config.sort')"
type="number"
v-model="baTable.form.items!.sort"
prop="sort"
:input-attr="{ step: 1 }"
:placeholder="t('Please input field', { field: t('game.config.sort') })"
/>
<FormItem
:label="t('game.config.instantiation')"
type="switch"
v-model="baTable.form.items!.instantiation"
prop="instantiation"
:input-attr="{ content: { '0': t('game.config.instantiation 0'), '1': t('game.config.instantiation 1') } }"
/>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import { useAdminInfo } from '/@/stores/adminInfo'
import type baTableClass from '/@/utils/baTable'
import { buildValidatorData } from '/@/utils/validate'
import {
fixedRowsFromKeys,
getFixedKeysForGameConfigName,
isBigwinDiceLockedKey,
jsonStringFromFixedKeys,
normalizeGameWeightConfigName,
parseWeightJsonToMap,
rowsToMap,
weightRowsMatchBigwinDiceKeys,
type WeightRow,
} from '/@/utils/gameWeightFixed'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const adminInfo = useAdminInfo()
const { t } = useI18n()
const isSuperAdmin = computed(() => adminInfo.super === true)
/** 编辑且非超级管理员:渠道、分组、配置标识、配置名称不可改 */
const metaFieldsDisabled = computed(() => !isSuperAdmin.value && baTable.form.operate === 'Edit')
const channelRemoteAttr = {
pk: 'game_channel.id',
field: 'name',
remoteUrl: '/admin/game.Channel/index',
}
const groupSelectBase = {
game_config: 'game_config',
game_weight: 'game_weight',
}
/** 非超级管理员新增时不可选 game_weight需先由超级管理员建好键结构 */
const groupSelectContentFiltered = computed(() => {
if (!isSuperAdmin.value && baTable.form.operate === 'Add') {
return { game_config: groupSelectBase.game_config }
}
return groupSelectBase
})
/** default_tier_weight / default_bigwin_weight及 default_kill_score_weight键固定仅值可改不可增删行 */
const isFixedGameWeightConfig = computed(() => getFixedKeysForGameConfigName(baTable.form.items?.name) !== null)
/** game_weight编辑或非超管时键只读仅超管新增非固定项时可增删行、改键 */
const weightKeyReadonly = computed(() => {
if (!isGameWeight.value) return false
if (isFixedGameWeightConfig.value) return true
if (baTable.form.operate === 'Edit') return true
return !isSuperAdmin.value
})
const canEditWeightStructure = computed(
() => isGameWeight.value && baTable.form.operate === 'Add' && isSuperAdmin.value && !isFixedGameWeightConfig.value
)
/** 默认大奖权重:仅校验每项整数与 0100005/30 固定 10000不参与 tier/kill 的「和≤100」 */
const isDefaultBigwinWeight = computed(() => normalizeGameWeightConfigName(baTable.form.items?.name) === 'default_bigwin_weight')
const weightRows = ref<WeightRow[]>([{ key: '', val: '' }])
/** default_tier_weight / default_kill_score_weight每项≤100且权重之和必须=100 */
const WEIGHT_SUM100_NAMES = ['default_tier_weight', 'default_kill_score_weight']
/** 配置标识:按分组限定可选项;编辑时若库中旧值不在列表中则临时追加一条 */
const nameSelectContent = computed((): Record<string, string> => {
const g = baTable.form.items?.group
let base: Record<string, string> = {}
if (g === 'game_config') {
base = {
game_rule: t('game.config.name opt game_rule'),
game_rule_en: t('game.config.name opt game_rule_en'),
}
} else if (g === 'game_weight') {
base = {
default_tier_weight: t('game.config.name opt default_tier_weight'),
default_kill_score_weight: t('game.config.name opt default_kill_score_weight'),
default_bigwin_weight: t('game.config.name opt default_bigwin_weight'),
}
}
const n = baTable.form.items?.name
if (typeof n === 'string' && n.trim() !== '' && base[n] === undefined) {
const norm = normalizeGameWeightConfigName(n)
if (norm !== '' && base[norm] !== undefined) {
return { ...base, [n]: base[norm] }
}
return { ...base, [n]: n }
}
return base
})
const isGameWeight = computed(() => baTable.form.items?.group === 'game_weight')
function parseValueToWeightRows(raw: unknown): WeightRow[] {
if (raw === null || raw === undefined || raw === '') {
return [{ key: '', val: '' }]
}
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return [{ key: '', val: '' }]
try {
const parsed = JSON.parse(s)
return arrayToWeightRows(parsed)
} catch {
return [{ key: '', val: '' }]
}
}
if (Array.isArray(raw)) {
return arrayToWeightRows(raw)
}
return [{ key: '', val: '' }]
}
function arrayToWeightRows(arr: unknown): WeightRow[] {
if (!Array.isArray(arr)) {
return [{ key: '', val: '' }]
}
const out: WeightRow[] = []
for (const item of arr) {
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
for (const [k, v] of Object.entries(item)) {
out.push({ key: k, val: v === null || v === undefined ? '' : String(v) })
}
}
}
return out.length ? out : [{ key: '', val: '' }]
}
function weightRowsToJsonString(rows: WeightRow[]): string {
const pairs: Record<string, string>[] = []
for (const r of rows) {
const k = r.key.trim()
if (k === '') continue
const one: Record<string, string> = {}
one[k] = r.val
pairs.push(one)
}
return JSON.stringify(pairs)
}
function syncWeightRowsToFormValue() {
const items = baTable.form.items
if (!items) return
const fixedKeys = getFixedKeysForGameConfigName(items.name)
if (fixedKeys) {
const map = rowsToMap(weightRows.value)
items.value = jsonStringFromFixedKeys(fixedKeys, map)
return
}
items.value = weightRowsToJsonString(weightRows.value)
}
function enforceDefaultBigwinLockedValues() {
if (normalizeGameWeightConfigName(baTable.form.items?.name) !== 'default_bigwin_weight') {
return
}
for (const r of weightRows.value) {
if (isBigwinDiceLockedKey(r.key)) {
r.val = '10000'
}
}
}
function onWeightRowChange() {
if (isGameWeight.value) {
enforceDefaultBigwinLockedValues()
syncWeightRowsToFormValue()
}
}
function addWeightRow() {
if (!canEditWeightStructure.value) return
weightRows.value.push({ key: '', val: '' })
syncWeightRowsToFormValue()
}
function removeWeightRow(idx: number) {
if (!canEditWeightStructure.value) return
if (weightRows.value.length <= 1) {
weightRows.value = [{ key: '', val: '' }]
} else {
weightRows.value.splice(idx, 1)
}
syncWeightRowsToFormValue()
}
function hydrateWeightRowsFromForm() {
if (!isGameWeight.value) return
const fixedKeys = getFixedKeysForGameConfigName(baTable.form.items?.name)
if (fixedKeys) {
const map = parseWeightJsonToMap(baTable.form.items?.value)
weightRows.value = fixedRowsFromKeys(fixedKeys, map)
enforceDefaultBigwinLockedValues()
syncWeightRowsToFormValue()
return
}
weightRows.value = parseValueToWeightRows(baTable.form.items?.value)
}
watch(isGameWeight, (gw) => {
if (gw) {
hydrateWeightRowsFromForm()
}
})
watch(
() => baTable.form.loading,
(loading) => {
if (loading === false && baTable.form.items?.group === 'game_weight') {
hydrateWeightRowsFromForm()
}
}
)
watch(
() => baTable.form.items?.group,
() => {
if (baTable.form.items?.group === 'game_weight') {
hydrateWeightRowsFromForm()
}
const items = baTable.form.items
if (!items || !isSuperAdmin.value) {
return
}
const c = nameSelectContent.value
const keys = Object.keys(c)
if (keys.length === 0) {
return
}
if (typeof items.name !== 'string' || items.name === '' || c[items.name] === undefined) {
items.name = keys[0]
}
}
)
watch(
() => baTable.form.items?.name,
() => {
if (baTable.form.items?.group === 'game_weight') {
hydrateWeightRowsFromForm()
}
}
)
function validateGameWeightRules(): string | undefined {
if (baTable.form.items?.group !== 'game_weight') {
return undefined
}
const configName = normalizeGameWeightConfigName(baTable.form.items?.name)
const fixedKeys = getFixedKeysForGameConfigName(configName)
const nums: number[] = []
if (fixedKeys) {
const map = rowsToMap(weightRows.value)
if (configName === 'default_bigwin_weight') {
for (const k of fixedKeys) {
const vs = (map[k] ?? '').trim()
if (vs === '') {
return t('Please input field', { field: t('game.config.weight value') })
}
const n = Number(vs)
if (!Number.isFinite(n)) {
return t('game.config.weight value numeric')
}
if (isBigwinDiceLockedKey(k)) {
if (n !== 10000) {
return t('game.config.bigwin weight locked 5 30')
}
} else if (n < 0 || n > 10000) {
return t('game.config.bigwin weight each 0 10000')
}
nums.push(n)
}
} else {
for (const k of fixedKeys) {
const vs = (map[k] ?? '').trim()
if (vs === '') {
return t('Please input field', { field: t('game.config.weight value') })
}
const n = Number(vs)
if (!Number.isFinite(n)) {
return t('game.config.weight value numeric')
}
if (n > 100) {
return t('game.config.weight each max 100')
}
nums.push(n)
}
}
} else {
// 非固定键名但行结构已是 530 骰子时,按大奖 010000 校验(避免 name 格式异常时误走每项≤10000
const treatAsBigwin = configName === 'default_bigwin_weight' || weightRowsMatchBigwinDiceKeys(weightRows.value)
for (const r of weightRows.value) {
const k = r.key.trim()
if (k === '') continue
const vs = r.val.trim()
if (vs === '') {
return t('Please input field', { field: t('game.config.weight value') })
}
const n = Number(vs)
if (!Number.isFinite(n)) {
return t('game.config.weight value numeric')
}
if (treatAsBigwin) {
if (isBigwinDiceLockedKey(k)) {
if (n !== 10000) {
return t('game.config.bigwin weight locked 5 30')
}
} else if (n < 0 || n > 10000) {
return t('game.config.bigwin weight each 0 10000')
}
} else if (n > 10000) {
return t('game.config.weight each max 10000')
}
nums.push(n)
}
if (nums.length === 0) {
return t('Please input field', { field: t('game.config.value') })
}
}
if (WEIGHT_SUM100_NAMES.includes(configName)) {
let sum = 0
for (const x of nums) {
sum += x
}
if (Math.abs(sum - 100) > 0.000001) {
return t('game.config.weight sum must 100')
}
}
return undefined
}
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
group: [buildValidatorData({ name: 'required', title: t('game.config.group') })],
name: [buildValidatorData({ name: 'required', title: t('game.config.name') })],
title: [buildValidatorData({ name: 'required', title: t('game.config.title') })],
sort: [buildValidatorData({ name: 'number', title: t('game.config.sort') })],
instantiation: [buildValidatorData({ name: 'number', title: t('game.config.instantiation') })],
value: [
{
validator: (_rule, _val, callback) => {
const err = validateGameWeightRules()
if (err) {
callback(new Error(err))
return
}
callback()
},
trigger: ['blur', 'change'],
},
],
create_time: [buildValidatorData({ name: 'date', title: t('game.config.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('game.config.update_time') })],
})
</script>
<style scoped lang="scss">
.weight-value-editor {
width: 100%;
}
.weight-value-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.weight-key {
max-width: 140px;
}
.weight-val {
flex: 1;
min-width: 80px;
}
.weight-sep {
flex-shrink: 0;
color: var(--el-text-color-secondary);
}
.form-help {
margin-top: 8px;
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
}
</style>