游戏-用户管理-优化样式增强验证

This commit is contained in:
2026-04-03 17:50:32 +08:00
parent 427024171e
commit cf381bf02c
8 changed files with 1125 additions and 2 deletions

View File

@@ -6,6 +6,22 @@ export default {
phone: 'phone',
remark: 'remark',
coin: 'coin',
tier_weight: 'tier weight',
bigwin_weight: 'big win weight',
tier_weight_preset: 'tier weight preset (game config)',
bigwin_weight_preset: 'big win weight preset (game config)',
'weight value': 'weight value',
'weight value numeric': 'Weight values must be valid numbers',
'weight each max 100': 'Each weight value must not exceed 100',
tier_weight_help: 'Sum of T1~T5 must not exceed 100',
tier_weight_sum_max_100: 'Sum of tier weights (T1~T5) must not exceed 100',
bigwin_weight_help: 'Only requires each item ≤ 10000; points 5 and 30 are guaranteed big wins, fixed to 10000',
bigwin_weight_each_max_10000: 'Each big win weight must not exceed 10000',
ticket_count: 'tickets',
ticket_ante: 'bets',
ticket_count_times: 'times',
'ticket row incomplete': 'Each row must have both ante and count',
'ticket row numeric': 'ante and count must be valid numbers',
status: 'status',
'status 0': 'status 0',
'status 1': 'status 1',

View File

@@ -6,6 +6,22 @@ export default {
phone: '手机号',
remark: '备注',
coin: '平台币',
tier_weight: '档位权重',
bigwin_weight: '中大奖权重',
tier_weight_preset: '档位权重模板(游戏配置)',
bigwin_weight_preset: '大奖权重模板(游戏配置)',
'weight value': '权重数值',
'weight value numeric': '权重值必须为有效数字',
'weight each max 100': '每项权重 100',
tier_weight_help: 'T1~T5 权重之和不能超过 100',
tier_weight_sum_max_100: '档位权重T1~T5之和不能超过 100',
bigwin_weight_help: '仅限制每项权重不超过 10000点数 5 和 30 为必中大奖组合,权重固定为 10000',
bigwin_weight_each_max_10000: '每项中大奖权重不能超过 10000',
ticket_count: '抽奖券',
ticket_ante: '注数',
ticket_count_times: '次数',
'ticket row incomplete': '每行需同时填写 ante 与 count',
'ticket row numeric': 'ante、count 须为有效数字',
status: '状态',
'status 0': '禁用',
'status 1': '启用',

View File

@@ -0,0 +1,117 @@
/** 档位权重固定键(与 GameConfig / GameUser JSON 一致) */
export const TIER_WEIGHT_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
/** 中大奖权重固定键 */
export const BIGWIN_WEIGHT_KEYS = ['5', '10', '15', '20', '25', '30'] as const
/** 默认大奖权重:点数 5、30 为豹子号必中,权重固定 10000 */
export function isBigwinDiceLockedKey(key: string): boolean {
return key === '5' || key === '30'
}
export type WeightRow = { key: string; val: string }
export function weightArrayToMap(arr: unknown): Record<string, string> {
const map: Record<string, string> = {}
if (!Array.isArray(arr)) {
return map
}
for (const item of arr) {
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
for (const [k, v] of Object.entries(item)) {
map[k] = v === null || v === undefined ? '' : String(v)
}
}
}
return map
}
/** 解析 game_weight JSON 为键值映射(键为 T1 / 5 等) */
export function parseWeightJsonToMap(raw: unknown): Record<string, string> {
if (raw === null || raw === undefined || raw === '') {
return {}
}
if (typeof raw === 'string') {
const s = raw.trim()
if (!s || s === '[]') {
return {}
}
try {
const parsed = JSON.parse(s)
return weightArrayToMap(parsed)
} catch {
return {}
}
}
if (Array.isArray(raw)) {
return weightArrayToMap(raw)
}
return {}
}
export function fixedRowsFromKeys(keys: readonly string[], map: Record<string, string>): WeightRow[] {
const rows: WeightRow[] = []
for (const k of keys) {
rows.push({ key: k, val: map[k] ?? '' })
}
return rows
}
export function rowsToMap(rows: WeightRow[]): Record<string, string> {
const m: Record<string, string> = {}
for (const r of rows) {
m[r.key] = r.val
}
return m
}
export function jsonStringFromFixedKeys(keys: readonly string[], map: Record<string, string>): string {
const pairs: Record<string, string>[] = []
for (const k of keys) {
const one: Record<string, string> = {}
one[k] = map[k] ?? ''
pairs.push(one)
}
return JSON.stringify(pairs)
}
/**
* 统一配置标识trim、去掉「 (说明)」「(说明)」「(说明)」等后缀、小写,避免下拉/接口返回不一致导致走错位校验如误报每项≤10000
*/
export function normalizeGameWeightConfigName(name: string | undefined): string {
if (name === undefined || name === null) {
return ''
}
let s = String(name).trim()
// 截到第一个半角/全角左括号之前(兼容 "key (说明)"、"key说明"、"key(说明)"、无前导空格)
const m = s.match(/^([^(]+)/u)
if (m) {
s = m[1].trim()
}
return s.toLowerCase()
}
/** 当前行是否为大奖骰子键 530与 BIGWIN_WEIGHT_KEYS 一致) */
export function weightRowsMatchBigwinDiceKeys(rows: { key: string }[]): boolean {
const keys = rows
.map((r) => r.key.trim())
.filter((k) => k !== '')
.sort((a, b) => Number(a) - Number(b))
if (keys.length !== BIGWIN_WEIGHT_KEYS.length) {
return false
}
return BIGWIN_WEIGHT_KEYS.every((k, i) => keys[i] === k)
}
/** GameConfig 中按 name 判断是否使用固定键编辑 */
export function getFixedKeysForGameConfigName(name: string | undefined): readonly string[] | null {
const n = normalizeGameWeightConfigName(name)
/** 击杀分权重与档位权重同为 T1T5库中 JSON 为 [{"T1":"0"},...],非骰子点 530 */
if (n === 'default_tier_weight' || n === 'default_kill_score_weight') {
return TIER_WEIGHT_KEYS
}
if (n === 'default_bigwin_weight') {
return BIGWIN_WEIGHT_KEYS
}
return null
}

View File

@@ -0,0 +1,83 @@
<template>
<div v-if="tagLabels.length" class="game-user-ticket-tags">
<el-tag v-for="(label, idx) in tagLabels" :key="idx" class="m-4" effect="light" type="success" size="default">
{{ label }}
</el-tag>
</div>
<span v-else class="game-user-json-plain">{{ plainText }}</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
renderRow: TableRow
renderField: TableColumn
renderValue: unknown
renderColumn: import('element-plus').TableColumnCtx<TableRow>
renderIndex: number
}>()
/** [{"ante":1,"count":1},...] */
function parseTicketTagLabels(raw: unknown): string[] {
if (raw === null || raw === undefined || raw === '') {
return []
}
let arr: unknown[] = []
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return []
try {
const parsed = JSON.parse(s)
arr = Array.isArray(parsed) ? parsed : []
} catch {
return []
}
} else if (Array.isArray(raw)) {
arr = raw
} else {
return []
}
const labels: string[] = []
for (const item of arr) {
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
const o = item as Record<string, unknown>
const ante = o.ante
const count = o.count
labels.push(`ante:${String(ante)} count:${String(count)}`)
}
}
return labels
}
const tagLabels = computed(() => parseTicketTagLabels(props.renderValue))
const plainText = computed(() => {
const v = props.renderValue
if (v === null || v === undefined) return ''
if (typeof v === 'object') {
try {
return JSON.stringify(v)
} catch {
return String(v)
}
}
return String(v)
})
</script>
<style scoped lang="scss">
.game-user-ticket-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px 0;
}
.m-4 {
margin: 4px;
}
.game-user-json-plain {
word-break: break-all;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div v-if="tagLabels.length" class="game-user-json-weight-tags">
<el-tag v-for="(label, idx) in tagLabels" :key="idx" class="m-4" effect="light" type="primary" size="default">
{{ label }}
</el-tag>
</div>
<span v-else class="game-user-json-plain">{{ plainText }}</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
renderRow: TableRow
renderField: TableColumn
renderValue: unknown
renderColumn: import('element-plus').TableColumnCtx<TableRow>
renderIndex: number
}>()
/** [{"T1":"5"},{"T2":"20"},...] */
function parseWeightTagLabels(raw: unknown): string[] {
if (raw === null || raw === undefined || raw === '') {
return []
}
let arr: unknown[] = []
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return []
try {
const parsed = JSON.parse(s)
arr = Array.isArray(parsed) ? parsed : []
} catch {
return []
}
} else if (Array.isArray(raw)) {
arr = raw
} else {
return []
}
const labels: string[] = []
for (const item of arr) {
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
for (const [k, v] of Object.entries(item as Record<string, unknown>)) {
labels.push(`${k}:${String(v)}`)
}
}
}
return labels
}
const tagLabels = computed(() => parseWeightTagLabels(props.renderValue))
const plainText = computed(() => {
const v = props.renderValue
if (v === null || v === undefined) return ''
if (typeof v === 'object') {
try {
return JSON.stringify(v)
} catch {
return String(v)
}
}
return String(v)
})
</script>
<style scoped lang="scss">
.game-user-json-weight-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px 0;
}
.m-4 {
margin: 4px;
}
.game-user-json-plain {
word-break: break-all;
}
</style>

View File

@@ -23,11 +23,14 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import GameUserTicketJsonCell from './GameUserTicketJsonCell.vue'
import GameUserWeightJsonCell from './GameUserWeightJsonCell.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
import { BIGWIN_WEIGHT_KEYS, TIER_WEIGHT_KEYS, jsonStringFromFixedKeys } from '/@/utils/gameWeightFixed'
defineOptions({
name: 'game/user',
@@ -66,6 +69,36 @@ const baTable = new baTableClass(
},
{ label: t('game.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{ label: t('game.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE' },
{
label: t('game.user.tier_weight'),
prop: 'tier_weight',
align: 'center',
minWidth: 200,
sortable: false,
operator: false,
render: 'customRender',
customRender: GameUserWeightJsonCell,
},
{
label: t('game.user.bigwin_weight'),
prop: 'bigwin_weight',
align: 'center',
minWidth: 200,
sortable: false,
operator: false,
render: 'customRender',
customRender: GameUserWeightJsonCell,
},
{
label: t('game.user.ticket_count'),
prop: 'ticket_count',
align: 'center',
minWidth: 220,
sortable: false,
operator: false,
render: 'customRender',
customRender: GameUserTicketJsonCell,
},
{
label: t('game.user.status'),
prop: 'status',
@@ -138,7 +171,12 @@ const baTable = new baTableClass(
dblClickNotEditColumn: [undefined, 'status'],
},
{
defaultItems: { status: '1' },
defaultItems: {
status: '1',
tier_weight: jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, {}),
bigwin_weight: jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, {}),
ticket_count: '[]',
},
}
)

View File

@@ -68,6 +68,65 @@
:input-attr="{ step: 1 }"
:placeholder="t('Please input field', { field: t('game.user.coin') })"
/>
<!-- 档位权重键固定 T1T5仅可改值 -->
<el-form-item :label="t('game.user.tier_weight')" prop="tier_weight">
<div class="weight-value-editor">
<div v-for="(row, idx) in tierWeightRows" :key="'tw-' + idx" class="weight-value-row">
<el-input v-model="row.key" class="weight-key" readonly tabindex="-1" />
<span class="weight-sep">:</span>
<el-input
v-model="row.val"
class="weight-val"
:placeholder="t('Please input field', { field: t('game.user.weight value') })"
clearable
@input="onTierWeightRowChange"
/>
</div>
<div class="form-help">{{ t('game.user.tier_weight_help') }}</div>
</div>
</el-form-item>
<!-- 中大奖权重键固定 530仅可改值 -->
<el-form-item :label="t('game.user.bigwin_weight')" prop="bigwin_weight">
<div class="weight-value-editor">
<div v-for="(row, idx) in bigwinWeightRows" :key="'bw-' + idx" class="weight-value-row">
<el-input v-model="row.key" class="weight-key" readonly tabindex="-1" />
<span class="weight-sep">:</span>
<el-input
v-model="row.val"
class="weight-val"
:placeholder="t('Please input field', { field: t('game.user.weight value') })"
clearable
:disabled="isBigwinValueLocked(row.key)"
@input="onBigwinWeightRowChange"
/>
</div>
<div class="form-help">{{ t('game.user.bigwin_weight_help') }}</div>
</div>
</el-form-item>
<!-- 抽奖券最多一条 JSON 元素 [{"ante":1,"count":1}]不可增行可删除清空 -->
<el-form-item :label="t('game.user.ticket_count')" prop="ticket_count">
<div class="ticket-value-editor">
<div v-for="(row, idx) in ticketRows" :key="'tk-' + idx" class="ticket-value-row">
<span class="ticket-label">{{ t('game.user.ticket_ante') }}</span>
<el-input
v-model="row.ante"
class="ticket-field"
clearable
:placeholder="t('Please input field', { field: t('game.user.ticket_ante') })"
@input="onTicketRowChange"
/>
<span class="ticket-label">{{ t('game.user.ticket_count_times') }}</span>
<el-input
v-model="row.count"
class="ticket-field"
clearable
:placeholder="t('Please input field', { field: t('game.user.ticket_count_times') })"
@input="onTicketRowChange"
/>
<el-button type="danger" link @click="removeTicketRow">{{ t('Delete') }}</el-button>
</div>
</div>
</el-form-item>
<FormItem
:label="t('game.user.status')"
type="switch"
@@ -107,17 +166,30 @@ import type { FormItemRule } from 'element-plus'
import { computed, inject, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import FormItem from '/@/components/formItem/index.vue'
import { useAdminInfo } from '/@/stores/adminInfo'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
import { buildValidatorData, regularPassword } from '/@/utils/validate'
import createAxios from '/@/utils/axios'
import {
BIGWIN_WEIGHT_KEYS,
TIER_WEIGHT_KEYS,
fixedRowsFromKeys,
jsonStringFromFixedKeys,
parseWeightJsonToMap,
rowsToMap,
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)
type TreeNode = {
value: string
label: string
@@ -194,6 +266,209 @@ onMounted(() => {
loadChannelAdminTree()
})
type TicketRow = { ante: string; count: string }
const tierWeightRows = ref<WeightRow[]>(fixedRowsFromKeys(TIER_WEIGHT_KEYS, {}))
const bigwinWeightRows = ref<WeightRow[]>(fixedRowsFromKeys(BIGWIN_WEIGHT_KEYS, {}))
const ticketRows = ref<TicketRow[]>([{ ante: '', count: '1' }])
function isBigwinValueLocked(key: string): boolean {
return key === '5' || key === '30'
}
function enforceBigwinFixedValues() {
for (const r of bigwinWeightRows.value) {
if (isBigwinValueLocked(r.key)) {
r.val = '10000'
}
}
}
function syncTierWeightToForm() {
const items = baTable.form.items
if (!items) return
items.tier_weight = jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, rowsToMap(tierWeightRows.value))
}
function syncBigwinWeightToForm() {
const items = baTable.form.items
if (!items) return
items.bigwin_weight = jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, rowsToMap(bigwinWeightRows.value))
}
function onTierWeightRowChange() {
syncTierWeightToForm()
}
function onBigwinWeightRowChange() {
enforceBigwinFixedValues()
syncBigwinWeightToForm()
}
function parseTicketRows(raw: unknown): TicketRow[] {
if (raw === null || raw === undefined || raw === '') {
return [{ ante: '', count: '1' }]
}
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return [{ ante: '', count: '1' }]
try {
const parsed = JSON.parse(s)
return arrayToTicketRows(parsed)
} catch {
return [{ ante: '', count: '1' }]
}
}
if (Array.isArray(raw)) {
return arrayToTicketRows(raw)
}
return [{ ante: '', count: '1' }]
}
function arrayToTicketRows(arr: unknown): TicketRow[] {
if (!Array.isArray(arr)) {
return [{ ante: '', count: '1' }]
}
const out: TicketRow[] = []
for (const item of arr) {
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
const ante = Reflect.get(item, 'ante')
const count = Reflect.get(item, 'count')
out.push({
ante: ante === null || ante === undefined ? '' : String(ante),
count: count === null || count === undefined || String(count) === '' ? '1' : String(count),
})
break
}
}
return out.length ? out : [{ ante: '', count: '1' }]
}
function ticketRowsToJsonString(rows: TicketRow[]): string {
const body: { ante: number; count: number }[] = []
for (const r of rows) {
const a = r.ante.trim()
const c = r.count.trim()
if (a === '' && c === '') continue
if (a === '' || c === '') continue
const na = Number(a)
const nc = Number(c)
body.push({ ante: na, count: nc })
}
return JSON.stringify(body)
}
function syncTicketToForm() {
const items = baTable.form.items
if (!items) return
const a = ticketRows.value[0]?.ante?.trim() ?? ''
const c = ticketRows.value[0]?.count?.trim() ?? ''
if (a === '' || c === '') {
items.ticket_count = ''
return
}
items.ticket_count = ticketRowsToJsonString(ticketRows.value)
}
function applyTierBigwinFromJson(tierJson: string, bigwinJson: string) {
const items = baTable.form.items
if (!items) return
const tm = parseWeightJsonToMap(tierJson)
const bm = parseWeightJsonToMap(bigwinJson)
items.tier_weight = jsonStringFromFixedKeys(TIER_WEIGHT_KEYS, tm)
items.bigwin_weight = jsonStringFromFixedKeys(BIGWIN_WEIGHT_KEYS, bm)
tierWeightRows.value = fixedRowsFromKeys(TIER_WEIGHT_KEYS, tm)
bigwinWeightRows.value = fixedRowsFromKeys(BIGWIN_WEIGHT_KEYS, bm)
enforceBigwinFixedValues()
syncBigwinWeightToForm()
}
async function loadAndApplyDefaultsForChannel(channelId: number) {
try {
const res = await createAxios(
{
url: '/admin/game.User/defaultWeightByChannel',
method: 'get',
params: { channel_id: channelId },
},
{
showErrorMessage: false,
showCodeMessage: false,
}
)
applyTierBigwinFromJson(res.data.tier_weight ?? '[]', res.data.bigwin_weight ?? '[]')
} catch {
// 路由或权限异常时不阻断打开表单,保持可手工编辑
}
}
function onTicketRowChange() {
syncTicketToForm()
}
/** 最多一条,删除/不填则 ticket_count 为空 */
function removeTicketRow() {
ticketRows.value = [{ ante: '', count: '1' }]
syncTicketToForm()
}
function hydrateJsonFieldsFromForm() {
const tm = parseWeightJsonToMap(baTable.form.items?.tier_weight)
const bm = parseWeightJsonToMap(baTable.form.items?.bigwin_weight)
tierWeightRows.value = fixedRowsFromKeys(TIER_WEIGHT_KEYS, tm)
bigwinWeightRows.value = fixedRowsFromKeys(BIGWIN_WEIGHT_KEYS, bm)
syncTierWeightToForm()
enforceBigwinFixedValues()
syncBigwinWeightToForm()
ticketRows.value = normalizeTicketRowsToOne(parseTicketRows(baTable.form.items?.ticket_count))
syncTicketToForm()
}
function normalizeTicketRowsToOne(rows: TicketRow[]): TicketRow[] {
if (rows.length === 0) {
return [{ ante: '', count: '1' }]
}
const first = rows[0]
return [
{
ante: first?.ante ?? '',
count: first?.count && String(first.count).trim() !== '' ? String(first.count) : '1',
},
]
}
watch(
() => baTable.form.loading,
(loading) => {
if (loading === false) {
hydrateJsonFieldsFromForm()
}
}
)
watch(
() => [baTable.form.operate, baTable.form.loading] as const,
async ([op, loading]) => {
if (op !== 'Add' || loading !== false) return
if (!isSuperAdmin.value) return
await loadAndApplyDefaultsForChannel(0)
}
)
watch(
() => baTable.form.items?.game_channel_id,
async (ch) => {
if (baTable.form.operate !== 'Add') return
if (ch === undefined || ch === null || ch === '') return
const cid = Number(ch)
if (!Number.isFinite(cid)) return
if (isSuperAdmin.value) {
return
}
await loadAndApplyDefaultsForChannel(cid)
}
)
watch(
() => baTable.form.items?.admin_id,
(val) => {
@@ -202,6 +477,62 @@ watch(
}
)
function validateTierWeightRows(): string | undefined {
let sum = 0
for (const r of tierWeightRows.value) {
const vs = r.val.trim()
if (vs === '') {
return t('Please input field', { field: t('game.user.weight value') })
}
const n = Number(vs)
if (!Number.isFinite(n)) {
return t('game.user.weight value numeric')
}
if (n > 100) {
return t('game.user.weight each max 100')
}
sum += n
}
if (sum > 100 + 0.000001) {
return t('game.user.tier_weight_sum_max_100')
}
return undefined
}
function validateBigwinWeightRows(): string | undefined {
for (const r of bigwinWeightRows.value) {
const vs = r.val.trim()
if (vs === '') {
return t('Please input field', { field: t('game.user.weight value') })
}
const n = Number(vs)
if (!Number.isFinite(n)) {
return t('game.user.weight value numeric')
}
if (n > 10000) {
return t('game.user.bigwin_weight_each_max_10000')
}
}
return undefined
}
function validateTicketRowsField(): string | undefined {
for (const r of ticketRows.value) {
const a = r.ante.trim()
const c = r.count.trim()
if (a === '' && c === '') continue
if (a === '' || c === '') {
return t('game.user.ticket row incomplete')
}
const na = Number(a)
const nc = Number(c)
if (!Number.isFinite(na) || !Number.isFinite(nc)) {
return t('game.user.ticket row numeric')
}
}
return undefined
}
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
const operate = baTable.form.operate
const v = typeof val === 'string' ? val.trim() : ''
@@ -224,10 +555,96 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
coin: [buildValidatorData({ name: 'number', title: t('game.user.coin') })],
tier_weight: [
{
validator: (_rule, _val, callback) => {
const err = validateTierWeightRows()
if (err) {
callback(new Error(err))
return
}
callback()
},
trigger: ['blur', 'change'],
},
],
bigwin_weight: [
{
validator: (_rule, _val, callback) => {
const err = validateBigwinWeightRows()
if (err) {
callback(new Error(err))
return
}
callback()
},
trigger: ['blur', 'change'],
},
],
ticket_count: [
{
validator: (_rule, _val, callback) => {
const err = validateTicketRowsField()
if (err) {
callback(new Error(err))
return
}
callback()
},
trigger: ['blur', 'change'],
},
],
admin_id: [buildValidatorData({ name: 'required', title: t('game.user.admin_id') })],
create_time: [buildValidatorData({ name: 'date', title: t('game.user.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('game.user.update_time') })],
})
</script>
<style scoped lang="scss"></style>
<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);
}
.ticket-value-editor {
width: 100%;
}
.ticket-value-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.ticket-field {
flex: 1;
min-width: 100px;
max-width: 160px;
}
.ticket-label {
flex-shrink: 0;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.form-help {
margin-top: 4px;
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
}
</style>