fix: harden user management operations

This commit is contained in:
yml2213
2026-08-13 15:07:33 +08:00
parent b07c5fdbf6
commit 090698e6b2
9 changed files with 286 additions and 33 deletions
+8 -2
View File
@@ -1,8 +1,8 @@
import { lazy, Suspense, useState, useCallback, type ReactNode } from 'react';
import { lazy, Suspense, useState, useCallback, useEffect, type ReactNode } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { App as AntdApp, ConfigProvider, Spin, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { getUser } from './store/auth';
import { fetchCurrentUser, getUser } from './store/auth';
import { ThemeProvider } from './store/theme';
import { useTheme } from './store/useTheme';
import AntdMessageBridge from './utils/AntdMessageBridge';
@@ -44,6 +44,12 @@ function AppContent() {
const isLoggedIn = !!getUser();
const { isDark } = useTheme();
// localStorage 仅是前端缓存;每次加载应用时以服务端的实时权限为准。
useEffect(() => {
if (!getUser()) return;
void fetchCurrentUser().finally(refreshAuth);
}, [refreshAuth]);
return (
<ConfigProvider
locale={zhCN}
+2
View File
@@ -7,6 +7,8 @@ export const userApi = {
api.post<UserInfo, UserInfo>('/users', data),
update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) =>
api.put<UserInfo, UserInfo>(`/users/${id}`, data),
rename: (id: number, username: string) =>
api.patch<UserInfo, UserInfo>(`/users/${id}/username`, { username }),
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/users/${id}`),
listPermissions: () => api.get<PermissionsListResult, PermissionsListResult>('/users/permissions/list'),
};
+50 -23
View File
@@ -1,8 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import {
Table, Button, Modal, Form, Input, Select, Tag, Popconfirm, Space,
Checkbox, Divider, Alert, Tooltip,
Checkbox, Divider, Alert,
} from 'antd';
import type { TableProps } from 'antd';
import { message } from '../utils/antdMessage';
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons';
import { userApi, type UserInfo } from '../api/modules';
@@ -61,6 +62,11 @@ export default function UsersPage() {
const { can } = usePermissions();
const canAssignPerm = can('user:assign_permissions');
const canCreate = can('user:create');
const canEdit = can('user:edit');
const canRename = can('user:rename');
const canDelete = can('user:delete');
const canManageUser = canEdit || canRename;
const loadUsers = useCallback(async () => {
setLoading(true);
@@ -87,6 +93,7 @@ export default function UsersPage() {
const handleEdit = (user: UserInfo) => {
setEditing(user);
form.resetFields();
form.setFieldsValue({
username: user.username,
role: user.role,
@@ -99,9 +106,14 @@ export default function UsersPage() {
try {
const values = await form.validateFields();
if (editing) {
const updateData: { role: string; remark: string; password?: string } = { role: values.role, remark: values.remark };
if (values.password) updateData.password = values.password;
await userApi.update(editing.id, updateData);
if (canRename && values.username !== editing.username) {
await userApi.rename(editing.id, values.username);
}
if (canEdit) {
const updateData: { role: string; remark: string; password?: string } = { role: values.role, remark: values.remark };
if (values.password) updateData.password = values.password;
await userApi.update(editing.id, updateData);
}
message.success('已更新');
} else {
await userApi.create(values);
@@ -170,12 +182,13 @@ export default function UsersPage() {
setSelectedPerms(rolePerms);
};
const columns = [
const columns: TableProps<UserInfo>['columns'] = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '用户名', dataIndex: 'username' },
{ title: '用户名', dataIndex: 'username', width: 128, ellipsis: true },
{
title: '角色',
dataIndex: 'role',
width: 100,
render: (role: string) => {
const label = ROLE_OPTIONS.find((r) => r.value === role)?.label || role;
return <Tag color={ROLE_COLORS[role] || 'default'}>{label}</Tag>;
@@ -196,21 +209,27 @@ export default function UsersPage() {
width: 80,
render: (active: boolean) => active ? <Tag color="green"></Tag> : <Tag></Tag>,
},
{ title: '备注', dataIndex: 'remark', ellipsis: true },
{ title: '备注', dataIndex: 'remark', width: 180, ellipsis: true },
{
title: '操作',
width: 200,
fixed: 'right',
align: 'left',
render: (_: unknown, record: UserInfo) => (
<Space>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
{canAssignPerm && (
<Tooltip title="权限管理">
<Button size="small" icon={<SafetyOutlined />} onClick={() => handlePermClick(record)}></Button>
</Tooltip>
<Space size={4} wrap={false}>
{canManageUser && (
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
)}
{record.role !== 'super_admin' && (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />}></Button>
{canAssignPerm && (
<Button size="small" type="text" icon={<SafetyOutlined />} onClick={() => handlePermClick(record)}></Button>
)}
{canDelete && record.role !== 'super_admin' && (
<Popconfirm
title="确认删除该用户?"
description="该用户将被停用并解除账号分配,历史任务会保留。"
onConfirm={() => handleDelete(record.id)}
>
<Button danger size="small" type="text" icon={<DeleteOutlined />}></Button>
</Popconfirm>
)}
</Space>
@@ -222,7 +241,7 @@ export default function UsersPage() {
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h2></h2>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}></Button>
{canCreate && <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}></Button>}
</div>
<Table
columns={columns}
@@ -230,6 +249,8 @@ export default function UsersPage() {
rowKey="id"
loading={loading}
size="small"
tableLayout="fixed"
scroll={{ x: 844 }}
pagination={{
current: currentPage,
pageSize,
@@ -252,24 +273,30 @@ export default function UsersPage() {
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={handleSave}
okText="保存"
okText={editing && !canEdit ? '保存用户名' : '保存'}
width={560}
>
<Form form={form} layout="vertical">
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input disabled={!!editing} />
<Form.Item
name="username"
label="用户名"
extra={editing && canRename ? '改名不会改变用户 ID,账号分配、任务、支付和审计记录均会保留。' : undefined}
rules={[{ required: true, message: '请输入用户名' }, { min: 2, max: 64, message: '用户名长度需为 2-64 个字符' }]}
>
<Input disabled={!!editing && !canRename} autoComplete="off" />
</Form.Item>
<Form.Item
name="password"
label={editing ? '新密码(留空不修改)' : '密码'}
rules={editing ? [] : [{ required: true, message: '请输入密码' }]}
>
<Input.Password />
<Input.Password disabled={!!editing && !canEdit} autoComplete="new-password" placeholder={editing ? '留空不修改' : undefined} />
</Form.Item>
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
<Select options={ROLE_OPTIONS} />
<Select disabled={!!editing && !canEdit} options={ROLE_OPTIONS} />
</Form.Item>
<Form.Item name="remark" label="备注">
<Input />
<Input disabled={!!editing && !canEdit} />
</Form.Item>
</Form>
</Modal>