Files
hms/apps/web/src/pages/Users.tsx
iven d436888ca5
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
refactor(web): 系统设置模块页面表单一致性重构
- 新增 useCrudDrawer hook 封装 CRUD Drawer 通用模式(状态管理/提交/错误处理)
- 新增 useListData hook 封装非分页列表数据获取
- 11 个页面统一迁移到 DrawerForm + 共享 hooks,消除重复代码
- 错误处理统一使用 useApiRequest.execute(),移除内联 try-catch
- Modal 全部替换为 DrawerForm,保持 UI 一致性
- 净减少 ~1300 行代码(858 增 / 2136 删)
2026-05-04 11:57:38 +08:00

243 lines
9.1 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import {
Table,
Button,
Space,
Form,
Input,
Tag,
Popconfirm,
Checkbox,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
UserOutlined,
SafetyCertificateOutlined,
StopOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import {
listUsers,
createUser,
updateUser,
deleteUser,
assignRoles,
type CreateUserRequest,
type UpdateUserRequest,
} from '../api/users';
import { listRoles, type RoleInfo } from '../api/roles';
import type { UserInfo } from '../api/auth';
import { PageContainer } from '../components/PageContainer';
import { DrawerForm } from '../components/DrawerForm';
import { useCrudDrawer } from '../hooks/useCrudDrawer';
import { usePaginatedData } from '../hooks/usePaginatedData';
import { useApiRequest } from '../hooks/useApiRequest';
import { useThemeMode } from '../hooks/useThemeMode';
const STATUS_COLOR_MAP: Record<string, string> = { active: '#059669', disabled: '#dc2626', locked: '#d97706' };
const STATUS_BG_MAP: Record<string, string> = { active: '#ECFDF5', disabled: '#FEF2F2', locked: '#FFFBEB' };
const STATUS_LABEL_MAP: Record<string, string> = { active: '正常', disabled: '禁用', locked: '锁定' };
export default function Users() {
const isDark = useThemeMode();
const { execute } = useApiRequest();
const {
data: users, total, page, loading, refresh,
} = usePaginatedData<UserInfo>(async (p, pageSize, search) => {
const result = await listUsers(p, pageSize, search);
return { data: result.data, total: result.total };
}, 20);
const [allRoles, setAllRoles] = useState<RoleInfo[]>([]);
const [roleDrawerOpen, setRoleDrawerOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserInfo | null>(null);
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
const fetchRoles = useCallback(async () => {
try {
const result = await listRoles();
setAllRoles(result.data);
} catch { /* silent */ }
}, []);
useEffect(() => { fetchRoles(); }, [fetchRoles]);
const userDrawer = useCrudDrawer<UserInfo>({
getId: (r) => r.id,
onCreate: async (values) => {
await createUser(values as unknown as CreateUserRequest);
},
onUpdate: async (id, values) => {
await updateUser(id, values as unknown as UpdateUserRequest);
},
onSuccess: refresh,
});
const handleDelete = async (id: string) => {
await execute(() => deleteUser(id), '用户已删除');
refresh();
};
const handleToggleStatus = async (id: string, status: string) => {
const user = users.find(u => u.id === id);
if (!user) return;
await execute(() => updateUser(id, { status, version: user.version }), status === 'disabled' ? '用户已禁用' : '用户已启用');
refresh();
};
const handleAssignRoles = async () => {
if (!selectedUser) return;
await execute(() => assignRoles(selectedUser.id, selectedRoleIds), '角色分配成功');
setRoleDrawerOpen(false);
refresh();
};
const openRoleDrawer = (user: UserInfo) => {
setSelectedUser(user);
setSelectedRoleIds(user.roles.map((r) => r.id));
setRoleDrawerOpen(true);
};
const columns = [
{
title: '用户', dataIndex: 'username', key: 'username',
render: (v: string, record: UserInfo) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #2563eb, #60a5fa)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 13, fontWeight: 600,
}}>
{(record.display_name?.[0] || v?.[0] || 'U').toUpperCase()}
</div>
<div>
<div style={{ fontWeight: 500, fontSize: 14 }}>{v}</div>
{record.display_name && <div style={{ fontSize: 12, color: isDark ? '#475569' : '#94a3b8' }}>{record.display_name}</div>}
</div>
</div>
),
},
{ title: '邮箱', dataIndex: 'email', key: 'email', render: (v?: string) => v || '-' },
{ title: '电话', dataIndex: 'phone', key: 'phone', render: (v?: string) => v || '-' },
{
title: '状态', dataIndex: 'status', key: 'status', width: 100,
render: (status: string) => (
<Tag style={{ color: STATUS_COLOR_MAP[status] || '#62625b', background: STATUS_BG_MAP[status] || '#f8fafc', border: 'none', fontWeight: 500 }}>
{STATUS_LABEL_MAP[status] || status}
</Tag>
),
},
{
title: '角色', dataIndex: 'roles', key: 'roles',
render: (roles: RoleInfo[]) =>
roles.length > 0
? roles.map((r) => <Tag key={r.id} style={{ background: isDark ? '#0f172a' : '#f8fafc', border: 'none', color: isDark ? '#CBD5E1' : '#475569' }}>{r.name}</Tag>)
: <span style={{ color: isDark ? '#475569' : '#CBD5E1' }}>-</span>,
},
{
title: '操作', key: 'actions', width: 240,
render: (_: unknown, record: UserInfo) => (
<Space size={4}>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => userDrawer.openEdit(record, (r) => ({
username: r.username, display_name: r.display_name, email: r.email, phone: r.phone,
}))} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
<Button size="small" type="text" icon={<SafetyCertificateOutlined />} onClick={() => openRoleDrawer(record)} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
{record.status === 'active' ? (
<Popconfirm title="确定禁用此用户?" onConfirm={() => handleToggleStatus(record.id, 'disabled')}>
<Button size="small" type="text" icon={<StopOutlined />} danger />
</Popconfirm>
) : (
<Button size="small" type="text" icon={<CheckCircleOutlined />} onClick={() => handleToggleStatus(record.id, 'active')} style={{ color: '#059669' }} />
)}
<Popconfirm title="确定删除此用户?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
</Space>
),
},
];
return (
<PageContainer
title="用户管理"
subtitle="管理系统用户账户、角色分配和状态"
actions={<Button type="primary" icon={<PlusOutlined />} onClick={() => userDrawer.openCreate()}></Button>}
>
<Table
columns={columns}
dataSource={users}
rowKey="id"
loading={loading}
pagination={{
current: page, total, pageSize: 20,
onChange: (p) => refresh(p),
showTotal: (t) => `${t} 条记录`,
}}
/>
{/* 新建/编辑用户 Drawer */}
<DrawerForm
title={userDrawer.editingRecord ? '编辑用户' : '新建用户'}
open={userDrawer.open}
onClose={userDrawer.close}
onSubmit={userDrawer.handleSubmit}
initialValues={userDrawer.initialValues}
loading={userDrawer.loading}
width={480}
columns={1}
>
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input prefix={<UserOutlined style={{ color: '#94a3b8' }} />} disabled={!!userDrawer.editingRecord} />
</Form.Item>
{!userDrawer.editingRecord && (
<Form.Item name="password" label="密码" rules={[{ required: true, message: '请输入密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password />
</Form.Item>
)}
<Form.Item name="display_name" label="显示名"><Input /></Form.Item>
<Form.Item name="email" label="邮箱" rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}>
<Input type="email" />
</Form.Item>
<Form.Item name="phone" label="电话"><Input /></Form.Item>
</DrawerForm>
{/* 角色分配 Drawer */}
<DrawerForm
title={`分配角色 - ${selectedUser?.username || ''}`}
open={roleDrawerOpen}
onClose={() => setRoleDrawerOpen(false)}
onSubmit={async () => { await handleAssignRoles(); }}
initialValues={{}}
loading={false}
width={520}
columns={1}
>
<div style={{ marginTop: 8 }}>
<Checkbox.Group
value={selectedRoleIds}
onChange={(values) => setSelectedRoleIds(values as string[])}
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
>
{allRoles.map((r) => (
<div key={r.id} style={{
padding: '10px 14px', borderRadius: 8,
border: `1px solid ${isDark ? '#0f172a' : '#E2E8F0'}`,
background: isDark ? '#0B0F1A' : '#f1f5f9',
}}>
<Checkbox value={r.id}>
<span style={{ fontWeight: 500 }}>{r.name}</span>
<span style={{ color: isDark ? '#475569' : '#94a3b8', marginLeft: 8, fontSize: 12 }}>{r.code}</span>
</Checkbox>
</div>
))}
</Checkbox.Group>
</div>
</DrawerForm>
</PageContainer>
);
}