[游戏管理]用户管理-优化
This commit is contained in:
@@ -1,19 +1,45 @@
|
||||
export default {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
uuid: 'uuid',
|
||||
phone: 'phone',
|
||||
remark: 'remark',
|
||||
coin: 'coin',
|
||||
status: 'status',
|
||||
'status 0': 'status 0',
|
||||
'status 1': 'status 1',
|
||||
channel_id: 'channel_id',
|
||||
channel__name: 'name',
|
||||
admin_id: 'admin_id',
|
||||
admin__username: 'username',
|
||||
create_time: 'create_time',
|
||||
update_time: 'update_time',
|
||||
'quick Search Fields': 'id,username,phone',
|
||||
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',
|
||||
}
|
||||
|
||||
@@ -4,16 +4,42 @@ export default {
|
||||
password: '密码',
|
||||
uuid: '用户唯一标识',
|
||||
phone: '手机号',
|
||||
email: '邮箱',
|
||||
email_placeholder: '可选,与手机号二选一注册时填写',
|
||||
head_image: '头像',
|
||||
remark: '备注',
|
||||
coin: '平台币',
|
||||
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: '用户名',
|
||||
admin_id: '归属管理员',
|
||||
admin__username: '管理员',
|
||||
create_time: '创建时间',
|
||||
update_time: '修改时间',
|
||||
'quick Search Fields': 'ID、用户名、手机号',
|
||||
'quick Search Fields': 'ID、用户名、手机号、邮箱、邀请码',
|
||||
section_basic: '账号信息',
|
||||
section_register: '注册与邀请',
|
||||
section_finance: '资金与流水',
|
||||
section_risk: '风控',
|
||||
section_streak: '连胜(兜底)',
|
||||
section_other: '其他',
|
||||
}
|
||||
|
||||
@@ -2,19 +2,13 @@
|
||||
<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') })"
|
||||
></TableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!-- 表格列有多种自定义渲染方式,比如自定义组件、具名插槽等,参见文档 -->
|
||||
<!-- 要使用 el-table 组件原有的属性,直接加在 Table 标签上即可 -->
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<!-- 表单 -->
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,9 +31,30 @@ const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
/**
|
||||
* baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件
|
||||
*/
|
||||
function formatCoin(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
}
|
||||
const s = String(cellValue).trim().replace(',', '.')
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(cellValue)
|
||||
}
|
||||
return n.toFixed(4)
|
||||
}
|
||||
|
||||
/** 返回多标签文案数组,供 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'))
|
||||
return parts
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/game.User/'),
|
||||
{
|
||||
@@ -55,6 +70,24 @@ const baTable = new baTableClass(
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ label: t('game.user.phone'), prop: 'phone', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
|
||||
{
|
||||
label: t('game.user.email'),
|
||||
prop: 'email',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
showOverflowTooltip: true,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.user.head_image'),
|
||||
prop: 'head_image',
|
||||
align: 'center',
|
||||
width: 72,
|
||||
operator: false,
|
||||
render: 'image',
|
||||
},
|
||||
{
|
||||
label: t('game.user.uuid'),
|
||||
prop: 'uuid',
|
||||
@@ -64,8 +97,57 @@ const baTable = new baTableClass(
|
||||
sortable: false,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{ 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.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('game.user.total_deposit_coin'),
|
||||
prop: 'total_deposit_coin',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
formatter: formatCoin,
|
||||
},
|
||||
{
|
||||
label: t('game.user.total_valid_bet_coin'),
|
||||
prop: 'total_valid_bet_coin',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
sortable: false,
|
||||
operator: 'RANGE',
|
||||
formatter: formatCoin,
|
||||
},
|
||||
{
|
||||
label: t('game.user.risk_flags'),
|
||||
prop: 'risk_flags',
|
||||
align: 'center',
|
||||
render: 'tags',
|
||||
minWidth: 200,
|
||||
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',
|
||||
},
|
||||
},
|
||||
{ label: t('game.user.current_streak'), prop: 'current_streak', align: 'center', width: 90, operator: 'RANGE' },
|
||||
{
|
||||
label: t('game.user.last_bet_period_no'),
|
||||
prop: 'last_bet_period_no',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
showOverflowTooltip: true,
|
||||
operator: 'LIKE',
|
||||
},
|
||||
{
|
||||
label: t('game.user.status'),
|
||||
prop: 'status',
|
||||
@@ -95,7 +177,6 @@ const baTable = new baTableClass(
|
||||
render: 'tags',
|
||||
operator: 'LIKE',
|
||||
comSearchRender: 'string',
|
||||
//修改tag颜色
|
||||
customRenderAttr: {
|
||||
tag: () => ({
|
||||
color: '#e8f3ff',
|
||||
@@ -138,7 +219,18 @@ const baTable = new baTableClass(
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: { status: '1' },
|
||||
defaultItems: {
|
||||
status: '1',
|
||||
coin: '0.0000',
|
||||
total_deposit_coin: '0.0000',
|
||||
total_valid_bet_coin: '0.0000',
|
||||
risk_flags: 0,
|
||||
current_streak: 0,
|
||||
last_bet_period_no: '',
|
||||
register_invite_code: '',
|
||||
email: '',
|
||||
head_image: '',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<template>
|
||||
<!-- 对话框表单 -->
|
||||
<!-- 建议使用 Prettier 格式化代码 -->
|
||||
<!-- el-form 内可以混用 el-form-item、FormItem、ba-input 等输入组件 -->
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
@@ -29,6 +26,11 @@
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-alert class="game-user-form-tip" type="info" :closable="false" show-icon>
|
||||
{{ t('game.user.form_tip') }}
|
||||
</el-alert>
|
||||
|
||||
<el-divider content-position="left">{{ t('game.user.section_basic') }}</el-divider>
|
||||
<FormItem
|
||||
:label="t('game.user.username')"
|
||||
type="string"
|
||||
@@ -50,6 +52,124 @@
|
||||
prop="phone"
|
||||
:placeholder="t('Please input field', { field: t('game.user.phone') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.user.email')"
|
||||
type="string"
|
||||
v-model="baTable.form.items!.email"
|
||||
prop="email"
|
||||
:placeholder="t('game.user.email_placeholder')"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.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-tree-select
|
||||
v-model="baTable.form.items!.admin_id"
|
||||
class="w100"
|
||||
clearable
|
||||
filterable
|
||||
:data="adminScopeTree"
|
||||
:props="treeProps"
|
||||
:render-after-expand="false"
|
||||
:placeholder="t('game.user.admin_affiliation_placeholder')"
|
||||
@update:model-value="onAdminTreeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<FormItem
|
||||
v-if="baTable.form.operate === 'Edit'"
|
||||
:label="t('game.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>
|
||||
<FormItem
|
||||
:label="t('game.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')"
|
||||
/>
|
||||
|
||||
<el-divider content-position="left">{{ t('game.user.section_finance') }}</el-divider>
|
||||
<FormItem
|
||||
:label="t('game.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')"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.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')"
|
||||
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')">
|
||||
<div class="risk-flag-row">
|
||||
<el-tag
|
||||
:type="riskBits.noLogin ? 'danger' : 'info'"
|
||||
:effect="riskBits.noLogin ? 'dark' : 'plain'"
|
||||
class="risk-flag-tag"
|
||||
@click="riskBits.noLogin = !riskBits.noLogin"
|
||||
>
|
||||
{{ t('game.user.risk_no_login') }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="riskBits.noBet ? 'danger' : 'info'"
|
||||
:effect="riskBits.noBet ? 'dark' : 'plain'"
|
||||
class="risk-flag-tag"
|
||||
@click="riskBits.noBet = !riskBits.noBet"
|
||||
>
|
||||
{{ t('game.user.risk_no_bet') }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="riskBits.noWithdraw ? 'danger' : 'info'"
|
||||
:effect="riskBits.noWithdraw ? 'dark' : 'plain'"
|
||||
class="risk-flag-tag"
|
||||
@click="riskBits.noWithdraw = !riskBits.noWithdraw"
|
||||
>
|
||||
{{ t('game.user.risk_no_withdraw') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('game.user.section_streak') }}</el-divider>
|
||||
<FormItem
|
||||
:label="t('game.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')"
|
||||
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')"
|
||||
/>
|
||||
|
||||
<el-divider content-position="left">{{ t('game.user.section_other') }}</el-divider>
|
||||
<FormItem
|
||||
:label="t('game.user.remark')"
|
||||
type="textarea"
|
||||
@@ -60,14 +180,6 @@
|
||||
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
|
||||
:placeholder="t('Please input field', { field: t('game.user.remark') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.user.coin')"
|
||||
type="number"
|
||||
v-model="baTable.form.items!.coin"
|
||||
prop="coin"
|
||||
:input-attr="{ step: 1 }"
|
||||
:placeholder="t('Please input field', { field: t('game.user.coin') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('game.user.status')"
|
||||
type="switch"
|
||||
@@ -75,19 +187,6 @@
|
||||
prop="status"
|
||||
:input-attr="{ content: { '0': t('game.user.status 0'), '1': t('game.user.status 1') } }"
|
||||
/>
|
||||
<el-form-item :label="t('game.user.channel_id')" prop="admin_id">
|
||||
<el-tree-select
|
||||
v-model="baTable.form.items!.admin_id"
|
||||
class="w100"
|
||||
clearable
|
||||
filterable
|
||||
:data="channelAdminTree"
|
||||
:props="treeProps"
|
||||
:render-after-expand="false"
|
||||
:placeholder="t('Please select field', { field: t('game.user.admin_id') })"
|
||||
@change="onAdminTreeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
@@ -104,7 +203,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import { inject, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { inject, nextTick, onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
@@ -112,6 +211,10 @@ import type baTableClass from '/@/utils/baTable'
|
||||
import { buildValidatorData, regularPassword } from '/@/utils/validate'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
const RISK_LOGIN = 1
|
||||
const RISK_BET = 2
|
||||
const RISK_WITHDRAW = 4
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
@@ -123,12 +226,14 @@ type TreeNode = {
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children?: TreeNode[]
|
||||
channel_id?: number
|
||||
channel_id?: number | null
|
||||
is_leaf?: boolean
|
||||
invite_code?: string
|
||||
}
|
||||
|
||||
const channelAdminTree = ref<TreeNode[]>([])
|
||||
const adminScopeTree = ref<TreeNode[]>([])
|
||||
const adminIdToChannelId = ref<Record<string, number>>({})
|
||||
const adminIdToInviteCode = ref<Record<string, string>>({})
|
||||
|
||||
const treeProps = {
|
||||
value: 'value',
|
||||
@@ -137,41 +242,143 @@ const treeProps = {
|
||||
disabled: 'disabled',
|
||||
}
|
||||
|
||||
const loadChannelAdminTree = async () => {
|
||||
const res = await createAxios({
|
||||
url: '/admin/channel/adminTree',
|
||||
method: 'get',
|
||||
})
|
||||
const list = (res.data?.list ?? []) as TreeNode[]
|
||||
channelAdminTree.value = list
|
||||
const riskBits = reactive({
|
||||
noLogin: false,
|
||||
noBet: false,
|
||||
noWithdraw: false,
|
||||
})
|
||||
|
||||
const map: Record<string, number> = {}
|
||||
const walk = (nodes: TreeNode[]) => {
|
||||
for (const n of nodes) {
|
||||
if (n.children && n.children.length) {
|
||||
function syncRiskFromFlags(flags: unknown) {
|
||||
const f = typeof flags === 'number' ? flags : parseInt(String(flags ?? '0'), 10) || 0
|
||||
riskBits.noLogin = (f & RISK_LOGIN) !== 0
|
||||
riskBits.noBet = (f & RISK_BET) !== 0
|
||||
riskBits.noWithdraw = (f & RISK_WITHDRAW) !== 0
|
||||
}
|
||||
|
||||
function packRiskFlags(): number {
|
||||
let r = 0
|
||||
if (riskBits.noLogin) r |= RISK_LOGIN
|
||||
if (riskBits.noBet) r |= RISK_BET
|
||||
if (riskBits.noWithdraw) r |= RISK_WITHDRAW
|
||||
return r
|
||||
}
|
||||
|
||||
/** 识别管理员叶子:value 非 group_* 且无子节点(不依赖 is_leaf,避免 el-tree 丢弃自定义字段) */
|
||||
function isAdminTreeLeaf(n: TreeNode): boolean {
|
||||
const v = n.value
|
||||
if (v === undefined || v === null) return false
|
||||
if (String(v).startsWith('group_')) return false
|
||||
if (n.children && n.children.length > 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function buildAdminMapsFromTree(nodes: TreeNode[]) {
|
||||
const mapCh: Record<string, number> = {}
|
||||
const mapInv: Record<string, string> = {}
|
||||
const walk = (arr: TreeNode[]) => {
|
||||
for (const n of arr) {
|
||||
if (n.children?.length) {
|
||||
walk(n.children)
|
||||
} else if (n.is_leaf && n.channel_id !== undefined) {
|
||||
map[n.value] = n.channel_id
|
||||
}
|
||||
if (!isAdminTreeLeaf(n)) {
|
||||
continue
|
||||
}
|
||||
const id = String(n.value)
|
||||
if (n.channel_id !== undefined && n.channel_id !== null && n.channel_id !== '') {
|
||||
mapCh[id] = typeof n.channel_id === 'number' ? n.channel_id : parseInt(String(n.channel_id), 10)
|
||||
}
|
||||
if (typeof n.invite_code === 'string') {
|
||||
mapInv[id] = n.invite_code
|
||||
} else if (n.invite_code !== undefined && n.invite_code !== null) {
|
||||
mapInv[id] = String(n.invite_code)
|
||||
} else {
|
||||
mapInv[id] = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(list)
|
||||
adminIdToChannelId.value = map
|
||||
walk(nodes)
|
||||
return { mapCh, mapInv }
|
||||
}
|
||||
|
||||
/** 映射未命中时从原始树查找(防止 props 裁剪或异步时序问题) */
|
||||
function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?: number; invite_code: string } | null {
|
||||
const target = String(adminId).trim()
|
||||
for (const n of nodes) {
|
||||
if (n.children?.length) {
|
||||
const nested = findAdminMetaInTree(n.children, adminId)
|
||||
if (nested) {
|
||||
return nested
|
||||
}
|
||||
}
|
||||
if (!isAdminTreeLeaf(n)) {
|
||||
continue
|
||||
}
|
||||
if (String(n.value) !== target) {
|
||||
continue
|
||||
}
|
||||
let channelId: number | undefined
|
||||
if (n.channel_id !== undefined && n.channel_id !== null && n.channel_id !== '') {
|
||||
channelId = typeof n.channel_id === 'number' ? n.channel_id : parseInt(String(n.channel_id), 10)
|
||||
}
|
||||
const invite =
|
||||
typeof n.invite_code === 'string' ? n.invite_code : n.invite_code != null ? String(n.invite_code) : ''
|
||||
return { channel_id: channelId, invite_code: invite }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const loadAdminScopeTree = async () => {
|
||||
const res = await createAxios({
|
||||
url: '/admin/game.User/adminScopeTree',
|
||||
method: 'get',
|
||||
})
|
||||
const list = (res.data?.list ?? []) as TreeNode[]
|
||||
adminScopeTree.value = list
|
||||
|
||||
const { mapCh, mapInv } = buildAdminMapsFromTree(list)
|
||||
adminIdToChannelId.value = mapCh
|
||||
adminIdToInviteCode.value = mapInv
|
||||
|
||||
await nextTick()
|
||||
const aid = baTable.form.items?.admin_id
|
||||
if (aid !== undefined && aid !== null && aid !== '') {
|
||||
onAdminTreeChange(aid as string | number)
|
||||
}
|
||||
}
|
||||
|
||||
const onAdminTreeChange = (val: string | number | null) => {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
if (!baTable.form.items) {
|
||||
return
|
||||
}
|
||||
const key = typeof val === 'number' ? String(val) : val
|
||||
const channelId = adminIdToChannelId.value[key]
|
||||
if (channelId !== undefined) {
|
||||
baTable.form.items!.channel_id = channelId
|
||||
if (val === null || val === undefined || val === '') {
|
||||
baTable.form.items.register_invite_code = ''
|
||||
return
|
||||
}
|
||||
const key = typeof val === 'number' ? String(val) : String(val).trim()
|
||||
|
||||
let channelId = adminIdToChannelId.value[key]
|
||||
let inv = adminIdToInviteCode.value[key]
|
||||
|
||||
if (channelId === undefined || inv === undefined) {
|
||||
const meta = findAdminMetaInTree(adminScopeTree.value, key)
|
||||
if (meta) {
|
||||
if (channelId === undefined && meta.channel_id !== undefined) {
|
||||
channelId = meta.channel_id
|
||||
}
|
||||
if (inv === undefined) {
|
||||
inv = meta.invite_code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId !== undefined) {
|
||||
baTable.form.items.channel_id = channelId
|
||||
}
|
||||
baTable.form.items.register_invite_code = inv !== undefined && inv !== null ? inv : ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadChannelAdminTree()
|
||||
loadAdminScopeTree()
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -182,32 +389,111 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 树数据异步加载完成后,根据已选管理员同步渠道与邀请码(编辑回显)
|
||||
watch(
|
||||
() => [baTable.form.items?.admin_id, adminScopeTree.value.length] as const,
|
||||
() => {
|
||||
const id = baTable.form.items?.admin_id
|
||||
if (id === undefined || id === null || id === '' || adminScopeTree.value.length === 0) {
|
||||
return
|
||||
}
|
||||
onAdminTreeChange(id as string | number)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => baTable.form.items?.risk_flags,
|
||||
(v) => {
|
||||
syncRiskFromFlags(v)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
riskBits,
|
||||
() => {
|
||||
if (!baTable.form.items) return
|
||||
baTable.form.items.risk_flags = packRiskFlags()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => baTable.form.operate,
|
||||
(op) => {
|
||||
if (op === 'Add') {
|
||||
syncRiskFromFlags(0)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 与 el-tree-select 的 value(字符串)一致,避免编辑回显选不中
|
||||
watch(
|
||||
() => [baTable.form.operate, baTable.form.items?.id] as const,
|
||||
() => {
|
||||
const items = baTable.form.items
|
||||
if (!items || baTable.form.operate !== 'Edit') {
|
||||
return
|
||||
}
|
||||
if (items.admin_id !== undefined && items.admin_id !== null && items.admin_id !== '') {
|
||||
items.admin_id = String(items.admin_id) as any
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
|
||||
const operate = baTable.form.operate
|
||||
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 (!regularPassword(v)) return callback(new Error(t('validate.Please enter the correct password')))
|
||||
return callback()
|
||||
}
|
||||
|
||||
// 编辑:可空;非空则校验格式
|
||||
if (!v) return callback()
|
||||
if (!regularPassword(v)) return callback(new Error(t('validate.Please enter the correct password')))
|
||||
return callback()
|
||||
}
|
||||
|
||||
const decimalRule = (fieldTitle: string): FormItemRule => ({
|
||||
trigger: 'blur',
|
||||
validator: (_rule, val, callback) => {
|
||||
if (val === null || val === undefined || val === '') {
|
||||
return callback()
|
||||
}
|
||||
const n = typeof val === 'number' ? val : parseFloat(String(val).trim().replace(',', '.'))
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
return callback(new Error(t('Please enter the correct field', { field: fieldTitle })))
|
||||
}
|
||||
return callback()
|
||||
},
|
||||
})
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('game.user.username') })],
|
||||
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
|
||||
phone: [buildValidatorData({ name: 'required', title: t('game.user.phone') })],
|
||||
coin: [buildValidatorData({ name: 'number', title: t('game.user.coin') })],
|
||||
admin_id: [buildValidatorData({ name: 'required', title: t('game.user.admin_id') })],
|
||||
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') })],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.game-user-form-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.risk-flag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.risk-flag-tag {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user