[游戏管理]用户管理

This commit is contained in:
2026-04-15 10:19:30 +08:00
parent 66c14eacb3
commit 7b002c9410
11 changed files with 386 additions and 28 deletions

View File

@@ -5,6 +5,13 @@ export default {
avatar: 'Avatar',
email: 'Email',
mobile: 'Mobile Number',
invite_code: 'Invite code',
commission_rate: 'Commission rate(%)',
commission_rate_desc_title: 'Admin commission notes',
commission_rate_desc_1: 'Admin commission means this admin allocation ratio inside assigned group.',
commission_rate_desc_2: 'Current admin commission = current group commission × current admin commission rate.',
commission_rate_desc_3: 'Within same group, total admin commission rate cannot exceed 100%; exceed and remaining are returned on validation.',
'Please select exactly one group': 'Please select exactly one group',
'Last login': 'Last login',
Password: 'Password',
'Please leave blank if not modified': 'Please leave blank if you do not modify.',

View File

@@ -9,8 +9,8 @@ export default {
status: 'status',
'status 0': 'status 0',
'status 1': 'status 1',
game_channel_id: 'game_channel_id',
gamechannel__name: 'name',
channel_id: 'channel_id',
channel__name: 'name',
admin_id: 'admin_id',
admin__username: 'username',
create_time: 'create_time',

View File

@@ -5,6 +5,13 @@ export default {
avatar: '头像',
email: '电子邮箱',
mobile: '手机号',
invite_code: '邀请码',
commission_rate: '分红比(%)',
commission_rate_desc_title: '管理员分红说明',
commission_rate_desc_1: '管理员分红用于该管理员在所属角色组内的分配比例。',
commission_rate_desc_2: '当前管理员分红=当前角色分红×当前管理员分红比例。',
commission_rate_desc_3: '同一角色组内管理员分红比例总和不能超过100%;超额会提示超出值与剩余额度。',
'Please select exactly one group': '请选择且仅选择一个角色组',
'Last login': '最后登录',
Password: '密码',
'Please leave blank if not modified': '不修改请留空',

View File

@@ -9,8 +9,8 @@ export default {
status: '状态',
'status 0': '禁用',
'status 1': '启用',
game_channel_id: '所属渠道',
gamechannel__name: '渠道名',
channel_id: '所属渠道',
channel__name: '渠道名',
admin_id: '所属管理员',
admin__username: '用户名',
create_time: '创建时间',

View File

@@ -40,6 +40,13 @@ optButtons[1].display = (row) => {
return row.id != adminInfo.id
}
const formatRatePercent = (_row: any, _column: any, cellValue: number | string | null) => {
if (cellValue === null || cellValue === undefined || cellValue === '') return '--'
const num = Number(cellValue)
if (Number.isNaN(num)) return '--'
return `${num.toFixed(2)}%`
}
const baTable = new baTableClass(
new baTableApi('/admin/auth.Admin/'),
{
@@ -48,7 +55,23 @@ const baTable = new baTableClass(
{ label: t('Id'), prop: 'id', align: 'center', operator: '=', operatorPlaceholder: t('Id'), width: 70 },
{ label: t('auth.admin.username'), prop: 'username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.nickname'), prop: 'nickname', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.group'), prop: 'group_name_arr', align: 'center', operator: false, render: 'tags' },
{
label: t('auth.admin.group'),
prop: 'group_name_arr',
align: 'center',
minWidth: 150,
operator: false,
render: 'tags',
},
{ label: t('auth.admin.invite_code'), prop: 'invite_code', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{
label: t('auth.admin.commission_rate'),
prop: 'commission_rate',
align: 'center',
minWidth: 90,
operator: 'RANGE',
formatter: formatRatePercent,
},
{ label: t('auth.admin.avatar'), prop: 'avatar', align: 'center', render: 'image', operator: false },
{ label: t('auth.admin.email'), prop: 'email', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.mobile'), prop: 'mobile', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },

View File

@@ -43,18 +43,43 @@
/>
<FormItem
:label="t('auth.admin.group')"
v-model="baTable.form.items!.group_arr"
v-model="singleGroupValue"
prop="group_arr"
type="remoteSelect"
:key="'group-' + baTable.form.items!.id"
:input-attr="{
multiple: true,
multiple: false,
disabled: adminInfo.id == baTable.form.items!.id,
params: { isTree: true, absoluteAuth: adminInfo.id == baTable.form.items!.id ? 0 : 1 },
field: 'name',
remoteUrl: '/admin/auth.Group/index',
placeholder: t('Click select'),
}"
/>
<FormItem
:label="t('auth.admin.commission_rate')"
v-model="baTable.form.items!.commission_rate"
type="number"
prop="commission_rate"
:input-attr="{ step: 0.01, precision: 2, min: 0, max: 100, disabled: shouldDisableCommissionRate() }"
:placeholder="t('Please input field', { field: t('auth.admin.commission_rate') })"
/>
<el-alert class="commission-rate-alert" :title="t('auth.admin.commission_rate_desc_title')" type="info" :closable="false" show-icon>
<template #default>
<ul class="commission-rate-desc-list">
<li>{{ t('auth.admin.commission_rate_desc_1') }}</li>
<li>{{ t('auth.admin.commission_rate_desc_2') }}</li>
<li>{{ t('auth.admin.commission_rate_desc_3') }}</li>
</ul>
</template>
</el-alert>
<FormItem
v-if="baTable.form.operate == 'Edit'"
:label="t('auth.admin.invite_code')"
v-model="baTable.form.items!.invite_code"
type="string"
:input-attr="{ readonly: true, disabled: true }"
/>
<FormItem :label="t('auth.admin.avatar')" type="image" v-model="baTable.form.items!.avatar" />
<FormItem
:label="t('auth.admin.email')"
@@ -115,7 +140,7 @@
</template>
<script setup lang="ts">
import { reactive, inject, watch, useTemplateRef } from 'vue'
import { computed, reactive, inject, watch, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import type baTableClass from '/@/utils/baTable'
import { regularPassword, buildValidatorData } from '/@/utils/validate'
@@ -131,10 +156,69 @@ const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
/** 解析管理员分红比例:与后端 isValidCommissionRate 一致地拒绝非法字符,避免 Number('30,00') 等为 NaN 导致误报 */
function parseAdminCommissionRateInput(raw: unknown): { kind: 'empty' } | { kind: 'invalid' } | { kind: 'ok'; value: number } {
if (raw === null || raw === undefined || raw === '') {
return { kind: 'empty' }
}
if (typeof raw === 'number') {
if (!Number.isFinite(raw)) {
return { kind: 'invalid' }
}
return { kind: 'ok', value: raw }
}
const s = String(raw).trim()
if (s === '') {
return { kind: 'empty' }
}
const normalized = s.replace(',', '.')
const n = parseFloat(normalized)
if (!Number.isFinite(n)) {
return { kind: 'invalid' }
}
return { kind: 'ok', value: n }
}
const shouldDisableCommissionRate = () => {
return adminInfo.id == baTable.form.items!.id
}
const singleGroupValue = computed({
get: () => {
const group = baTable.form.items?.group_arr
if (Array.isArray(group)) {
return group.length > 0 ? group[0] : ''
}
return group ?? ''
},
set: (value) => {
if (!baTable.form.items) {
return
}
baTable.form.items.group_arr = value === '' || value === null || value === undefined ? [] : [value]
},
})
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
username: [buildValidatorData({ name: 'required', title: t('auth.admin.username') }), buildValidatorData({ name: 'account' })],
nickname: [buildValidatorData({ name: 'required', title: t('auth.admin.nickname') })],
group_arr: [buildValidatorData({ name: 'required', message: t('Please select field', { field: t('auth.admin.group') }) })],
group_arr: [
{
required: true,
validator: (_rule: any, val: unknown, callback: Function) => {
if (Array.isArray(val)) {
if (val.length !== 1) {
return callback(new Error(t('auth.admin.Please select exactly one group')))
}
return callback()
}
if (val === null || val === undefined || val === '') {
return callback(new Error(t('Please select field', { field: t('auth.admin.group') })))
}
return callback()
},
trigger: 'change',
},
],
email: [buildValidatorData({ name: 'email', message: t('Please enter the correct field', { field: t('auth.admin.email') }) })],
mobile: [buildValidatorData({ name: 'mobile', message: t('Please enter the correct field', { field: t('auth.admin.mobile') }) })],
password: [
@@ -157,6 +241,26 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
trigger: 'blur',
},
],
commission_rate: [
{
validator: (_rule: unknown, val: unknown, callback: (e?: Error) => void) => {
const parsed = parseAdminCommissionRateInput(val)
if (parsed.kind === 'empty') {
return callback()
}
if (parsed.kind === 'invalid') {
return callback(new Error(t('Please enter the correct field', { field: t('auth.admin.commission_rate') })))
}
const n = parsed.value
const rounded = Math.round(n * 100) / 100
if (rounded < -0.000001 || rounded > 100.000001) {
return callback(new Error(t('Please enter the correct field', { field: t('auth.admin.commission_rate') })))
}
return callback()
},
trigger: ['blur', 'change'],
},
],
})
watch(
@@ -181,6 +285,14 @@ watch(
width: 110px;
height: 110px;
}
.commission-rate-alert {
margin-bottom: 12px;
}
.commission-rate-desc-list {
margin: 6px 0 0;
padding-left: 18px;
line-height: 1.6;
}
.avatar-uploader:hover {
border-color: var(--el-color-primary);
}

View File

@@ -76,8 +76,8 @@ const baTable = new baTableClass(
replaceValue: { '0': t('game.user.status 0'), '1': t('game.user.status 1') },
},
{
label: t('game.user.gamechannel__name'),
prop: 'gameChannel.name',
label: t('game.user.channel__name'),
prop: 'channel.name',
align: 'center',
minWidth: 100,
operatorPlaceholder: t('Fuzzy query'),

View File

@@ -75,7 +75,7 @@
prop="status"
:input-attr="{ content: { '0': t('game.user.status 0'), '1': t('game.user.status 1') } }"
/>
<el-form-item :label="t('game.user.game_channel_id')" prop="admin_id">
<el-form-item :label="t('game.user.channel_id')" prop="admin_id">
<el-tree-select
v-model="baTable.form.items!.admin_id"
class="w100"
@@ -139,7 +139,7 @@ const treeProps = {
const loadChannelAdminTree = async () => {
const res = await createAxios({
url: '/admin/game.Channel/adminTree',
url: '/admin/channel/adminTree',
method: 'get',
})
const list = (res.data?.list ?? []) as TreeNode[]
@@ -166,7 +166,7 @@ const onAdminTreeChange = (val: string | number | null) => {
const key = typeof val === 'number' ? String(val) : val
const channelId = adminIdToChannelId.value[key]
if (channelId !== undefined) {
baTable.form.items!.game_channel_id = channelId
baTable.form.items!.channel_id = channelId
}
}