[游戏管理]用户管理-优化表单样式

This commit is contained in:
2026-04-15 17:46:04 +08:00
parent c213b2d9e9
commit 56df105af6
8 changed files with 199 additions and 182 deletions

View File

@@ -0,0 +1,45 @@
export default {
id: 'ID',
username: 'Username',
password: 'Password',
uuid: 'UUID',
phone: 'Phone',
email: 'Email',
email_placeholder: 'Optional (register with phone or email)',
head_image: 'Avatar',
remark: 'Remark',
coin: 'Coin balance',
coin_placeholder: 'decimal(18,4)',
total_deposit_coin: 'Total deposit (coin)',
total_valid_bet_coin: 'Total valid bet (coin)',
risk_flags: 'Risk',
risk_none: 'None',
risk_no_login: 'No login',
risk_no_bet: 'No bet',
risk_no_withdraw: 'No withdraw',
current_streak: 'Win streak',
last_bet_period_no: 'Last bet period',
last_bet_period_no_placeholder: 'DB fallback for streak',
register_invite_code: 'Invite code (snapshot)',
register_invite_code_placeholder: 'Invite code at registration',
status: 'Status',
'status 0': 'Disabled',
'status 1': 'Enabled',
section_admin_attribution: 'Administrator',
admin_affiliation: 'Assigned admin',
admin_affiliation_placeholder: 'Role group tree — only admins in your scope',
register_invite_code_auto_placeholder: 'Filled from selected admin invite code',
channel_id: 'Channel',
channel__name: 'Channel',
admin_id: 'Admin',
admin__username: 'Admin',
create_time: 'Created',
update_time: 'Updated',
'quick Search Fields': 'ID, username, phone, email, invite code',
section_basic: 'Account',
section_register: 'Registration',
section_finance: 'Balance & turnover',
section_risk: 'Risk control',
section_streak: 'Streak (fallback)',
section_other: 'Other',
}

View File

@@ -0,0 +1,45 @@
export default {
id: 'ID',
username: '用户名',
password: '密码',
uuid: '用户唯一标识',
phone: '手机号',
email: '邮箱',
email_placeholder: '可选,与手机号二选一注册时填写',
head_image: '头像',
remark: '备注',
coin: '游戏币余额',
coin_placeholder: 'decimal(18,4),禁止业务用浮点存库',
total_deposit_coin: '累计充值(币)',
total_valid_bet_coin: '累计有效投注(币)',
risk_flags: '风控',
risk_none: '无限制',
risk_no_login: '禁止登录',
risk_no_bet: '禁止下注',
risk_no_withdraw: '禁止提现',
current_streak: '当前连胜',
last_bet_period_no: '最近下注期号',
last_bet_period_no_placeholder: '连胜兜底同步用,可与 Redis 对照',
register_invite_code: '注册邀请码快照',
register_invite_code_placeholder: '注册时绑定渠道/代理邀请码',
status: '状态',
'status 0': '禁用',
'status 1': '启用',
section_admin_attribution: '管理员归属',
admin_affiliation: '归属管理员',
admin_affiliation_placeholder: '按角色组展开,仅展示您可管理范围内的管理员',
register_invite_code_auto_placeholder: '随所选管理员邀请码自动带出',
channel_id: '所属渠道',
channel__name: '渠道名',
admin_id: '归属管理员',
admin__username: '管理员',
create_time: '创建时间',
update_time: '修改时间',
'quick Search Fields': 'ID、用户名、手机号、邮箱、邀请码',
section_basic: '账号信息',
section_register: '注册与邀请',
section_finance: '资金与流水',
section_risk: '风控',
section_streak: '连胜(兜底)',
section_other: '其他',
}

View File

@@ -1,10 +1,10 @@
<template>
<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.user.quick Search Fields') })"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('user.user.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
@@ -24,7 +24,7 @@ import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'game/user',
name: 'user/user',
})
const { t } = useI18n()
@@ -43,36 +43,36 @@ function formatCoin(_row: anyObj, _column: any, cellValue: unknown) {
return n.toFixed(4)
}
/** 返回多标签文案数组,供 render: tags 使用 */
/** 杩斿洖澶氭爣绛炬枃妗堟暟缁勶紝渚?render: tags 浣跨敤 */
function formatRiskFlags(row: anyObj, _column: any, cellValue: unknown) {
const raw = cellValue !== undefined && cellValue !== null && cellValue !== '' ? cellValue : row.risk_flags
const f = typeof raw === 'number' ? raw : parseInt(String(raw ?? '0'), 10) || 0
const parts: string[] = []
if (f & 1) parts.push(t('game.user.risk_no_login'))
if (f & 2) parts.push(t('game.user.risk_no_bet'))
if (f & 4) parts.push(t('game.user.risk_no_withdraw'))
if (!parts.length) parts.push(t('game.user.risk_none'))
if (f & 1) parts.push(t('user.user.risk_no_login'))
if (f & 2) parts.push(t('user.user.risk_no_bet'))
if (f & 4) parts.push(t('user.user.risk_no_withdraw'))
if (!parts.length) parts.push(t('user.user.risk_none'))
return parts
}
const baTable = new baTableClass(
new baTableApi('/admin/game.User/'),
new baTableApi('/admin/user.User/'),
{
pk: 'id',
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('game.user.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{ label: t('user.user.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' },
{
label: t('game.user.username'),
label: t('user.user.username'),
prop: 'username',
align: 'center',
operatorPlaceholder: t('Fuzzy query'),
sortable: false,
operator: 'LIKE',
},
{ label: t('game.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{ label: t('user.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{
label: t('game.user.email'),
label: t('user.user.email'),
prop: 'email',
align: 'center',
minWidth: 120,
@@ -81,7 +81,7 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.user.head_image'),
label: t('user.user.head_image'),
prop: 'head_image',
align: 'center',
width: 72,
@@ -89,7 +89,7 @@ const baTable = new baTableClass(
render: 'image',
},
{
label: t('game.user.uuid'),
label: t('user.user.uuid'),
prop: 'uuid',
align: 'center',
showOverflowTooltip: true,
@@ -98,16 +98,16 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.user.register_invite_code'),
label: t('user.user.register_invite_code'),
prop: 'register_invite_code',
align: 'center',
minWidth: 100,
showOverflowTooltip: true,
operator: 'LIKE',
},
{ label: t('game.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE', formatter: formatCoin },
{ label: t('user.user.coin'), prop: 'coin', align: 'center', sortable: false, operator: 'RANGE', formatter: formatCoin },
{
label: t('game.user.total_deposit_coin'),
label: t('user.user.total_deposit_coin'),
prop: 'total_deposit_coin',
align: 'center',
minWidth: 110,
@@ -116,7 +116,7 @@ const baTable = new baTableClass(
formatter: formatCoin,
},
{
label: t('game.user.total_valid_bet_coin'),
label: t('user.user.total_valid_bet_coin'),
prop: 'total_valid_bet_coin',
align: 'center',
minWidth: 110,
@@ -125,7 +125,7 @@ const baTable = new baTableClass(
formatter: formatCoin,
},
{
label: t('game.user.risk_flags'),
label: t('user.user.risk_flags'),
prop: 'risk_flags',
align: 'center',
render: 'tags',
@@ -133,15 +133,15 @@ const baTable = new baTableClass(
operator: false,
formatter: formatRiskFlags,
custom: {
[t('game.user.risk_none')]: 'primary',
[t('game.user.risk_no_login')]: 'danger',
[t('game.user.risk_no_bet')]: 'danger',
[t('game.user.risk_no_withdraw')]: 'danger',
[t('user.user.risk_none')]: 'primary',
[t('user.user.risk_no_login')]: 'danger',
[t('user.user.risk_no_bet')]: 'danger',
[t('user.user.risk_no_withdraw')]: 'danger',
},
},
{ label: t('game.user.current_streak'), prop: 'current_streak', align: 'center', width: 90, operator: 'RANGE' },
{ label: t('user.user.current_streak'), prop: 'current_streak', align: 'center', width: 90, operator: 'RANGE' },
{
label: t('game.user.last_bet_period_no'),
label: t('user.user.last_bet_period_no'),
prop: 'last_bet_period_no',
align: 'center',
minWidth: 120,
@@ -149,16 +149,16 @@ const baTable = new baTableClass(
operator: 'LIKE',
},
{
label: t('game.user.status'),
label: t('user.user.status'),
prop: 'status',
align: 'center',
operator: 'eq',
sortable: false,
render: 'switch',
replaceValue: { '0': t('game.user.status 0'), '1': t('game.user.status 1') },
replaceValue: { '0': t('user.user.status 0'), '1': t('user.user.status 1') },
},
{
label: t('game.user.channel__name'),
label: t('user.user.channel__name'),
prop: 'channel.name',
align: 'center',
minWidth: 100,
@@ -168,7 +168,7 @@ const baTable = new baTableClass(
comSearchRender: 'string',
},
{
label: t('game.user.admin__username'),
label: t('user.user.admin__username'),
prop: 'admin.username',
align: 'center',
minWidth: 90,
@@ -185,7 +185,7 @@ const baTable = new baTableClass(
},
},
{
label: t('game.user.remark'),
label: t('user.user.remark'),
prop: 'remark',
align: 'center',
minWidth: 100,
@@ -193,7 +193,7 @@ const baTable = new baTableClass(
operatorPlaceholder: t('Fuzzy query'),
},
{
label: t('game.user.create_time'),
label: t('user.user.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
@@ -204,7 +204,7 @@ const baTable = new baTableClass(
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('game.user.update_time'),
label: t('user.user.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
@@ -247,3 +247,5 @@ onMounted(() => {
</script>
<style scoped lang="scss"></style>

View File

@@ -27,47 +27,47 @@
:rules="rules"
>
<!-- <el-alert class="game-user-form-tip" type="info" :closable="false" show-icon>-->
<!-- {{ t('game.user.form_tip') }}-->
<!-- {{ t('user.user.form_tip') }}-->
<!-- </el-alert>-->
<el-divider content-position="left">{{ t('game.user.section_basic') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_basic') }}</el-divider>
<FormItem
:label="t('game.user.username')"
:label="t('user.user.username')"
type="string"
v-model="baTable.form.items!.username"
prop="username"
:placeholder="t('Please input field', { field: t('game.user.username') })"
:placeholder="t('Please input field', { field: t('user.user.username') })"
/>
<FormItem
:label="t('game.user.password')"
:label="t('user.user.password')"
type="password"
v-model="baTable.form.items!.password"
prop="password"
:placeholder="t('Please input field', { field: t('game.user.password') })"
:placeholder="t('Please input field', { field: t('user.user.password') })"
/>
<FormItem
:label="t('game.user.phone')"
:label="t('user.user.phone')"
type="string"
v-model="baTable.form.items!.phone"
prop="phone"
:placeholder="t('Please input field', { field: t('game.user.phone') })"
:placeholder="t('Please input field', { field: t('user.user.phone') })"
/>
<FormItem
:label="t('game.user.email')"
:label="t('user.user.email')"
type="string"
v-model="baTable.form.items!.email"
prop="email"
:placeholder="t('game.user.email_placeholder')"
:placeholder="t('user.user.email_placeholder')"
/>
<FormItem
:label="t('game.user.head_image')"
:label="t('user.user.head_image')"
type="image"
v-model="baTable.form.items!.head_image"
prop="head_image"
/>
<el-divider content-position="left">{{ t('game.user.section_admin_attribution') }}</el-divider>
<el-form-item :label="t('game.user.admin_affiliation')" prop="admin_id">
<el-divider content-position="left">{{ t('user.user.section_admin_attribution') }}</el-divider>
<el-form-item :label="t('user.user.admin_affiliation')" prop="admin_id">
<el-tree-select
v-model="baTable.form.items!.admin_id"
class="w100"
@@ -76,55 +76,55 @@
:data="adminScopeTree"
:props="treeProps"
:render-after-expand="false"
:placeholder="t('game.user.admin_affiliation_placeholder')"
:placeholder="t('user.user.admin_affiliation_placeholder')"
@update:model-value="onAdminTreeChange"
/>
</el-form-item>
<FormItem
v-if="baTable.form.operate === 'Edit'"
:label="t('game.user.uuid')"
:label="t('user.user.uuid')"
type="string"
v-model="baTable.form.items!.uuid"
prop="uuid"
:input-attr="{ readonly: true, disabled: true }"
/>
<el-divider content-position="left">{{ t('game.user.section_register') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_register') }}</el-divider>
<FormItem
:label="t('game.user.register_invite_code')"
:label="t('user.user.register_invite_code')"
type="string"
v-model="baTable.form.items!.register_invite_code"
prop="register_invite_code"
:input-attr="{ readonly: true }"
:placeholder="t('game.user.register_invite_code_auto_placeholder')"
:placeholder="t('user.user.register_invite_code_auto_placeholder')"
/>
<el-divider content-position="left">{{ t('game.user.section_finance') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_finance') }}</el-divider>
<FormItem
:label="t('game.user.coin')"
:label="t('user.user.coin')"
type="number"
v-model="baTable.form.items!.coin"
prop="coin"
:input-attr="{ step: 0.0001, min: 0, precision: 4 }"
:placeholder="t('game.user.coin_placeholder')"
:placeholder="t('user.user.coin_placeholder')"
/>
<FormItem
:label="t('game.user.total_deposit_coin')"
:label="t('user.user.total_deposit_coin')"
type="number"
v-model="baTable.form.items!.total_deposit_coin"
prop="total_deposit_coin"
:input-attr="{ step: 0.0001, min: 0, precision: 4 }"
/>
<FormItem
:label="t('game.user.total_valid_bet_coin')"
:label="t('user.user.total_valid_bet_coin')"
type="number"
v-model="baTable.form.items!.total_valid_bet_coin"
prop="total_valid_bet_coin"
:input-attr="{ step: 0.0001, min: 0, precision: 4 }"
/>
<el-divider content-position="left">{{ t('game.user.section_risk') }}</el-divider>
<el-form-item :label="t('game.user.risk_flags')">
<el-divider content-position="left">{{ t('user.user.section_risk') }}</el-divider>
<el-form-item :label="t('user.user.risk_flags')">
<div class="risk-flag-row">
<el-tag
:type="riskBits.noLogin ? 'danger' : 'info'"
@@ -132,7 +132,7 @@
class="risk-flag-tag"
@click="riskBits.noLogin = !riskBits.noLogin"
>
{{ t('game.user.risk_no_login') }}
{{ t('user.user.risk_no_login') }}
</el-tag>
<el-tag
:type="riskBits.noBet ? 'danger' : 'info'"
@@ -140,7 +140,7 @@
class="risk-flag-tag"
@click="riskBits.noBet = !riskBits.noBet"
>
{{ t('game.user.risk_no_bet') }}
{{ t('user.user.risk_no_bet') }}
</el-tag>
<el-tag
:type="riskBits.noWithdraw ? 'danger' : 'info'"
@@ -148,44 +148,44 @@
class="risk-flag-tag"
@click="riskBits.noWithdraw = !riskBits.noWithdraw"
>
{{ t('game.user.risk_no_withdraw') }}
{{ t('user.user.risk_no_withdraw') }}
</el-tag>
</div>
</el-form-item>
<el-divider content-position="left">{{ t('game.user.section_streak') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_streak') }}</el-divider>
<FormItem
:label="t('game.user.current_streak')"
:label="t('user.user.current_streak')"
type="number"
v-model="baTable.form.items!.current_streak"
prop="current_streak"
:input-attr="{ step: 1, min: 0, precision: 0 }"
/>
<FormItem
:label="t('game.user.last_bet_period_no')"
:label="t('user.user.last_bet_period_no')"
type="string"
v-model="baTable.form.items!.last_bet_period_no"
prop="last_bet_period_no"
:placeholder="t('game.user.last_bet_period_no_placeholder')"
:placeholder="t('user.user.last_bet_period_no_placeholder')"
/>
<el-divider content-position="left">{{ t('game.user.section_other') }}</el-divider>
<el-divider content-position="left">{{ t('user.user.section_other') }}</el-divider>
<FormItem
:label="t('game.user.remark')"
:label="t('user.user.remark')"
type="textarea"
v-model="baTable.form.items!.remark"
prop="remark"
:input-attr="{ rows: 3 }"
@keyup.enter.stop=""
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
:placeholder="t('Please input field', { field: t('game.user.remark') })"
:placeholder="t('Please input field', { field: t('user.user.remark') })"
/>
<FormItem
:label="t('game.user.status')"
:label="t('user.user.status')"
type="switch"
v-model="baTable.form.items!.status"
prop="status"
:input-attr="{ content: { '0': t('game.user.status 0'), '1': t('game.user.status 1') } }"
:input-attr="{ content: { '0': t('user.user.status 0'), '1': t('user.user.status 1') } }"
/>
</el-form>
</div>
@@ -263,7 +263,7 @@ function packRiskFlags(): number {
return r
}
/** 识别管理员叶子:value group_* 且无子节点(不依赖 is_leaf,避免 el-tree 丢弃自定义字段) */
/** 璇嗗埆绠$悊鍛樺彾瀛愶細value 闈?group_* 涓旀棤瀛愯妭鐐癸紙涓嶄緷璧?is_leaf锛岄伩鍏?el-tree 涓㈠純鑷畾涔夊瓧娈碉級 */
function isAdminTreeLeaf(n: TreeNode): boolean {
const v = n.value
if (v === undefined || v === null) return false
@@ -300,7 +300,7 @@ function buildAdminMapsFromTree(nodes: TreeNode[]) {
return { mapCh, mapInv }
}
/** 映射未命中时从原始树查找(防止 props 裁剪或异步时序问题) */
/** 鏄犲皠鏈懡涓椂浠庡師濮嬫爲鏌ユ壘锛堥槻姝?props 瑁佸壀鎴栧紓姝ユ椂搴忛棶棰橈級 */
function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?: number; invite_code: string } | null {
const target = String(adminId).trim()
for (const n of nodes) {
@@ -329,7 +329,7 @@ function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?:
const loadAdminScopeTree = async () => {
const res = await createAxios({
url: '/admin/game.User/adminScopeTree',
url: '/admin/user.User/adminScopeTree',
method: 'get',
})
const list = (res.data?.list ?? []) as TreeNode[]
@@ -389,7 +389,7 @@ watch(
}
)
//
//
watch(
() => [baTable.form.items?.admin_id, adminScopeTree.value.length] as const,
() => {
@@ -426,7 +426,7 @@ watch(
}
)
// el-tree-select value
// el-tree-select value
watch(
() => [baTable.form.operate, baTable.form.items?.id] as const,
() => {
@@ -445,7 +445,7 @@ const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Er
const v = typeof val === 'string' ? val.trim() : ''
if (operate === 'Add') {
if (!v) return callback(new Error(t('Please input field', { field: t('game.user.password') })))
if (!v) return callback(new Error(t('Please input field', { field: t('user.user.password') })))
if (!regularPassword(v)) return callback(new Error(t('validate.Please enter the correct password')))
return callback()
}
@@ -470,15 +470,15 @@ const decimalRule = (fieldTitle: string): FormItemRule => ({
})
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
username: [buildValidatorData({ name: 'required', title: t('game.user.username') })],
username: [buildValidatorData({ name: 'required', title: t('user.user.username') })],
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
coin: [decimalRule(t('game.user.coin'))],
total_deposit_coin: [decimalRule(t('game.user.total_deposit_coin'))],
total_valid_bet_coin: [decimalRule(t('game.user.total_valid_bet_coin'))],
admin_id: [buildValidatorData({ name: 'required', title: t('game.user.admin_affiliation') })],
create_time: [buildValidatorData({ name: 'date', title: t('game.user.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('game.user.update_time') })],
phone: [buildValidatorData({ name: 'required', title: t('user.user.phone') })],
coin: [decimalRule(t('user.user.coin'))],
total_deposit_coin: [decimalRule(t('user.user.total_deposit_coin'))],
total_valid_bet_coin: [decimalRule(t('user.user.total_valid_bet_coin'))],
admin_id: [buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') })],
create_time: [buildValidatorData({ name: 'date', title: t('user.user.create_time') })],
update_time: [buildValidatorData({ name: 'date', title: t('user.user.update_time') })],
})
</script>
@@ -497,3 +497,5 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
user-select: none;
}
</style>