feat: 新增客服备用金台账与后台用户中文显示名

- 新增备用金账户、流水、支出、对账与权限
- 后台用户支持中文显示名并同步备用金姓名
- 审计、登录记录、任务及工单操作人优先展示中文名
- 补充备用金与人工支出方案文档
This commit is contained in:
yml2213
2026-10-18 13:55:24 +08:00
parent 7bacb7d5eb
commit 7542d547bf
55 changed files with 6557 additions and 105 deletions
+101 -15
View File
@@ -27,12 +27,13 @@ import {
fetchAdminRolePermissions,
fetchAdminUsers,
resetAdminUserPassword,
updateAdminUserDisplayName,
updateAdminUserRole,
updateAdminUserStatus,
updateAdminRolePermissions,
} from '@/services/admin'
import type { AdminRole, AdminUserListItem } from '@/types/admin'
import { getAdminUserId, hasAdminRole } from '@/utils/admin-auth'
import { getAdminUserId, hasAdminRole, setAdminDisplayName } from '@/utils/admin-auth'
import { adminUserRoleOptions, adminUserStatusOptions } from '@/utils/admin-options'
import {
ADMIN_DEFAULT_PAGE_SIZE,
@@ -48,11 +49,16 @@ type UserFilterForm = {
}
type CreateUserForm = {
displayName?: string
username: string
password: string
role: AdminRole
}
type DisplayNameForm = {
displayName: string
}
const USER_ROLE_OPTIONS = [
{ label: '普通运营', value: 'operator' },
{ label: '客服', value: 'support' },
@@ -119,6 +125,7 @@ export default function AdminUsersPage() {
try {
await createAdminUser({
username: values.username.trim(),
displayName: values.displayName?.trim() || '',
password: values.password.trim(),
role: values.role,
status: 'active',
@@ -139,7 +146,7 @@ export default function AdminUsersPage() {
modal.confirm({
title: '确认操作',
content: `确认将 ${item.username} 调整为${formatRoleLabel(nextRole)}吗?`,
content: `确认将 ${displayNameOf(item)} 调整为${formatRoleLabel(nextRole)}吗?`,
okText: '继续执行',
cancelText: '取消',
centered: true,
@@ -163,7 +170,7 @@ export default function AdminUsersPage() {
modal.confirm({
title: '确认操作',
content: `确认将 ${item.username}${nextStatus === 'active' ? '启用' : '停用'}吗?`,
content: `确认将 ${displayNameOf(item)}${nextStatus === 'active' ? '启用' : '停用'}吗?`,
okText: '继续执行',
cancelText: '取消',
centered: true,
@@ -182,6 +189,66 @@ export default function AdminUsersPage() {
})
}
function openEditDisplayNameModal(item: AdminUserListItem) {
const form = ModalFormStore.create<DisplayNameForm>()
modal.confirm({
title: '修改姓名',
content: (
<Form
layout="vertical"
preserve={false}
initialValues={{ displayName: item.displayName || item.username }}
ref={form.bind}
className="modal-form"
>
<Form.Item
name="displayName"
label="姓名(展示用,可输入中文)"
rules={[{ max: 32, message: '姓名最多 32 个字符' }]}
extra="留空时回退显示登录账号"
>
<Input placeholder="例如:张三" maxLength={32} showCount />
</Form.Item>
</Form>
),
okText: '保存',
cancelText: '取消',
centered: true,
onOk: async () => {
const values = await form.validate()
setActionLoadingId(item.userId)
try {
const response = await updateAdminUserDisplayName(item.userId, {
displayName: values.displayName?.trim() || '',
})
if (item.userId === currentUserId) {
setAdminDisplayName(response.data.user.displayName)
}
message.success('用户姓名已更新')
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-session'] }),
queryClient.invalidateQueries({ queryKey: ['admin-users'] }),
queryClient.invalidateQueries({ queryKey: ['admin-audit-logs'] }),
queryClient.invalidateQueries({ queryKey: ['admin-login-logs'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-overview'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-expenses'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-reconciliations'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-account-detail'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-options'] }),
queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] }),
])
await reloadUsers()
} catch (error) {
message.error(error instanceof Error ? error.message : '更新用户姓名失败')
} finally {
setActionLoadingId(null)
}
},
})
}
function openResetPasswordModal(item: AdminUserListItem) {
const form = ModalFormStore.create<{ password: string }>()
@@ -197,7 +264,7 @@ export default function AdminUsersPage() {
>
<Form.Item
name="password"
label={`请输入 ${item.username} 的新密码`}
label={`请输入 ${displayNameOf(item)} 的新密码`}
rules={[
{ required: true, message: '请输入新密码' },
{ min: 8, message: '密码至少 8 位' },
@@ -251,16 +318,22 @@ export default function AdminUsersPage() {
align: 'center',
},
{
title: '账号',
dataIndex: 'username',
minWidth: 150,
render: (value, row) => (
title: '姓名',
dataIndex: 'displayName',
minWidth: 160,
render: (value: string, row) => (
<Space>
<Typography.Text strong>{value}</Typography.Text>
<Typography.Text strong>{value || row.username}</Typography.Text>
{row.userId === currentUserId ? <Tag>当前账号</Tag> : null}
</Space>
),
},
{
title: '账号',
dataIndex: 'username',
minWidth: 140,
render: (value: string) => <Typography.Text type="secondary">{value}</Typography.Text>,
},
{
title: '角色',
dataIndex: 'role',
@@ -287,7 +360,7 @@ export default function AdminUsersPage() {
},
{
title: '操作',
width: 310,
width: 390,
render: (_, row) => {
const isCurrent = row.userId === currentUserId
const roleMenuItems: MenuProps['items'] = USER_ROLE_OPTIONS.map((item) => ({
@@ -298,6 +371,12 @@ export default function AdminUsersPage() {
return (
<Space wrap>
<Button
loading={actionLoadingId === row.userId}
onClick={() => openEditDisplayNameModal(row)}
>
改名
</Button>
<Dropdown
disabled={isCurrent}
menu={{
@@ -333,7 +412,7 @@ export default function AdminUsersPage() {
<section className="page-stack">
<PageHeader
title="后台用户"
description="管理员可维护后台账号、角色和启停状态,避免继续使用单一口令。"
description="管理员可维护后台账号、姓名、角色和启停状态,避免继续使用单一口令。"
/>
<Card className="users-action-card">
@@ -347,7 +426,7 @@ export default function AdminUsersPage() {
className="filter-form users-action-form"
>
<Form.Item name="username">
<Input allowClear placeholder="账号筛选" className="users-input-md" />
<Input allowClear placeholder="姓名/账号筛选" className="users-input-md" />
</Form.Item>
<Form.Item name="role">
<Select
@@ -387,8 +466,11 @@ export default function AdminUsersPage() {
onFinish={submitCreate}
className="filter-form users-action-form"
>
<Form.Item name="displayName" rules={[{ max: 32, message: '姓名最多 32 个字符' }]}>
<Input placeholder="姓名(可中文)" className="users-input-md" maxLength={32} />
</Form.Item>
<Form.Item name="username" rules={[{ required: true, message: '请输入新账号' }]}>
<Input placeholder="新账号" className="users-input-md" />
<Input placeholder="登录账号(英文/数字)" className="users-input-md" />
</Form.Item>
<Form.Item
name="password"
@@ -424,7 +506,7 @@ export default function AdminUsersPage() {
title="用户列表"
extra={
<Typography.Text type="secondary">
共 {data?.pagination.total || 0} 个账号
共 {data?.pagination.total || 0} 个用户
</Typography.Text>
}
>
@@ -434,7 +516,7 @@ export default function AdminUsersPage() {
columns={columns}
dataSource={data?.items || []}
locale={{ emptyText: <Empty description="暂无用户" /> }}
scroll={{ x: 1060 }}
scroll={{ x: 1220 }}
pagination={buildAdminTablePagination({
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
@@ -487,6 +569,10 @@ function formatRoleLabel(role: AdminRole) {
return '普通运营'
}
function displayNameOf(user: AdminUserListItem) {
return user.displayName || user.username
}
function cleanParams(params: Record<string, unknown>) {
const next = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {