import { useMemo, useState } from 'react';
|
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
import {
|
App as AntdApp,
|
Avatar,
|
Card,
|
Form,
|
Input,
|
Modal,
|
Select,
|
Space,
|
Table,
|
Tag,
|
} from 'antd';
|
import type { TableProps } from 'antd';
|
import dayjs from 'dayjs';
|
import AuthButton from '@/components/AuthButton';
|
import { PERMISSION } from '@/constants/permissions';
|
import { usePermission } from '@/hooks/usePermission';
|
import './Users.scss';
|
|
type UserStatus = 'active' | 'disabled';
|
|
interface UserRecord {
|
key: number;
|
username: string;
|
nickname: string;
|
role: string;
|
status: UserStatus;
|
createdAt: string;
|
}
|
|
interface UserFormValues {
|
username: string;
|
nickname: string;
|
role: string;
|
status: UserStatus;
|
}
|
|
const roleOptions = [
|
{ value: '超级管理员', label: '超级管理员' },
|
{ value: '运营管理员', label: '运营管理员' },
|
{ value: '销售专员', label: '销售专员' },
|
{ value: '访客', label: '访客' },
|
];
|
|
const roleColor: Record<string, string> = {
|
超级管理员: 'purple',
|
运营管理员: 'blue',
|
销售专员: 'cyan',
|
访客: 'default',
|
};
|
|
const statusMeta: Record<UserStatus, { label: string; color: string }> = {
|
active: { label: '启用', color: 'green' },
|
disabled: { label: '禁用', color: 'default' },
|
};
|
|
const initialUsers: UserRecord[] = [
|
{
|
key: 1,
|
username: 'admin',
|
nickname: '陈一鸣',
|
role: '超级管理员',
|
status: 'active',
|
createdAt: '2026-01-08 09:30',
|
},
|
{
|
key: 2,
|
username: 'lin.xia',
|
nickname: '林夏',
|
role: '运营管理员',
|
status: 'active',
|
createdAt: '2026-03-12 14:20',
|
},
|
{
|
key: 3,
|
username: 'zhou.yang',
|
nickname: '周洋',
|
role: '销售专员',
|
status: 'active',
|
createdAt: '2026-04-02 10:05',
|
},
|
{
|
key: 4,
|
username: 'su.wen',
|
nickname: '苏雯',
|
role: '销售专员',
|
status: 'active',
|
createdAt: '2026-05-18 16:42',
|
},
|
{
|
key: 5,
|
username: 'he.jun',
|
nickname: '何俊',
|
role: '运营管理员',
|
status: 'disabled',
|
createdAt: '2026-06-09 11:18',
|
},
|
{
|
key: 6,
|
username: 'yang.li',
|
nickname: '杨莉',
|
role: '销售专员',
|
status: 'active',
|
createdAt: '2026-07-21 15:54',
|
},
|
{
|
key: 7,
|
username: 'wang.qi',
|
nickname: '王琪',
|
role: '访客',
|
status: 'disabled',
|
createdAt: '2026-08-03 09:12',
|
},
|
{
|
key: 8,
|
username: 'zhao.min',
|
nickname: '赵敏',
|
role: '销售专员',
|
status: 'active',
|
createdAt: '2026-09-15 13:26',
|
},
|
];
|
|
export default function Users() {
|
const [users, setUsers] = useState<UserRecord[]>(initialUsers);
|
const [keyword, setKeyword] = useState('');
|
const [status, setStatus] = useState<UserStatus | 'all'>('all');
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [editingUser, setEditingUser] = useState<UserRecord | null>(null);
|
const [form] = Form.useForm<UserFormValues>();
|
const { message, modal } = AntdApp.useApp();
|
const { hasPermission } = usePermission();
|
|
const canOperate =
|
hasPermission(PERMISSION.USER_EDIT) || hasPermission(PERMISSION.USER_DELETE);
|
|
const filteredUsers = useMemo(() => {
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
return users.filter((user) => {
|
const matchesKeyword =
|
!normalizedKeyword ||
|
user.username.toLowerCase().includes(normalizedKeyword) ||
|
user.nickname.toLowerCase().includes(normalizedKeyword);
|
const matchesStatus = status === 'all' || user.status === status;
|
return matchesKeyword && matchesStatus;
|
});
|
}, [users, keyword, status]);
|
|
const openCreateModal = () => {
|
setEditingUser(null);
|
form.resetFields();
|
form.setFieldsValue({ status: 'active', role: '销售专员' });
|
setIsModalOpen(true);
|
};
|
|
const openEditModal = (record: UserRecord) => {
|
setEditingUser(record);
|
form.setFieldsValue({
|
username: record.username,
|
nickname: record.nickname,
|
role: record.role,
|
status: record.status,
|
});
|
setIsModalOpen(true);
|
};
|
|
const closeModal = () => {
|
setIsModalOpen(false);
|
form.resetFields();
|
};
|
|
const handleSubmit = async () => {
|
try {
|
const values = await form.validateFields();
|
const duplicate = users.some(
|
(user) => user.username === values.username && user.key !== editingUser?.key,
|
);
|
if (duplicate) {
|
message.error('用户名已存在,请更换一个');
|
return;
|
}
|
|
if (editingUser) {
|
setUsers((prev) =>
|
prev.map((user) =>
|
user.key === editingUser.key ? { ...user, ...values } : user,
|
),
|
);
|
message.success('用户信息已更新');
|
} else {
|
setUsers((prev) => [
|
{
|
key: Date.now(),
|
...values,
|
createdAt: dayjs().format('YYYY-MM-DD HH:mm'),
|
},
|
...prev,
|
]);
|
message.success('用户创建成功');
|
}
|
closeModal();
|
} catch {
|
// 表单校验失败时由 antd 自动展示错误信息
|
}
|
};
|
|
const handleDelete = (record: UserRecord) => {
|
modal.confirm({
|
title: '删除用户',
|
content: `确定要删除用户「${record.nickname}」吗?删除后不可恢复。`,
|
okText: '删除',
|
cancelText: '取消',
|
okButtonProps: { danger: true },
|
onOk: () => {
|
setUsers((prev) => prev.filter((user) => user.key !== record.key));
|
message.success('用户已删除');
|
},
|
});
|
};
|
|
const columns: TableProps<UserRecord>['columns'] = [
|
{
|
title: '用户',
|
dataIndex: 'nickname',
|
width: 220,
|
render: (_value, record) => (
|
<div className="user-cell">
|
<Avatar className="user-cell__avatar">{record.nickname.charAt(0)}</Avatar>
|
<div className="user-cell__meta">
|
<span className="user-cell__name">{record.nickname}</span>
|
<span className="user-cell__username">@{record.username}</span>
|
</div>
|
</div>
|
),
|
},
|
{
|
title: '角色',
|
dataIndex: 'role',
|
width: 140,
|
render: (value: string) => (
|
<Tag color={roleColor[value] ?? 'blue'} bordered={false}>
|
{value}
|
</Tag>
|
),
|
},
|
{
|
title: '状态',
|
dataIndex: 'status',
|
width: 100,
|
render: (value: UserStatus) => (
|
<Tag color={statusMeta[value].color} bordered={false}>
|
{statusMeta[value].label}
|
</Tag>
|
),
|
},
|
{ title: '创建时间', dataIndex: 'createdAt', width: 170 },
|
{
|
title: '操作',
|
key: 'action',
|
width: 150,
|
render: (_value, record) =>
|
canOperate ? (
|
<Space size={0}>
|
<AuthButton
|
type="link"
|
size="small"
|
permission={PERMISSION.USER_EDIT}
|
onClick={() => openEditModal(record)}
|
>
|
编辑
|
</AuthButton>
|
<AuthButton
|
type="link"
|
size="small"
|
danger
|
permission={PERMISSION.USER_DELETE}
|
onClick={() => handleDelete(record)}
|
>
|
删除
|
</AuthButton>
|
</Space>
|
) : (
|
<span className="text-muted">-</span>
|
),
|
},
|
];
|
|
return (
|
<div className="users-page">
|
<div className="page-header">
|
<div>
|
<h1 className="page-header__title">用户管理</h1>
|
<p className="page-header__desc">
|
维护平台账号、角色与启用状态,按钮会根据当前账号的权限码自动显隐。
|
</p>
|
</div>
|
<div className="page-header__actions">
|
<AuthButton
|
type="primary"
|
icon={<PlusOutlined />}
|
permission={PERMISSION.USER_ADD}
|
onClick={openCreateModal}
|
>
|
新增用户
|
</AuthButton>
|
</div>
|
</div>
|
|
<Card className="filter-card" variant="borderless">
|
<div className="page-toolbar">
|
<Input
|
allowClear
|
prefix={<SearchOutlined />}
|
placeholder="搜索用户名 / 昵称"
|
value={keyword}
|
onChange={(event) => setKeyword(event.target.value)}
|
style={{ width: 260 }}
|
/>
|
<Select<UserStatus | 'all'>
|
value={status}
|
onChange={setStatus}
|
style={{ width: 150 }}
|
options={[
|
{ value: 'all', label: '全部状态' },
|
{ value: 'active', label: '启用' },
|
{ value: 'disabled', label: '禁用' },
|
]}
|
/>
|
<span className="text-muted">共 {filteredUsers.length} 条记录</span>
|
</div>
|
</Card>
|
|
<Card className="table-card" variant="borderless">
|
<Table<UserRecord>
|
rowKey="key"
|
columns={columns}
|
dataSource={filteredUsers}
|
scroll={{ x: 820 }}
|
pagination={{
|
pageSize: 8,
|
showSizeChanger: false,
|
showTotal: (total) => `共 ${total} 条`,
|
}}
|
/>
|
</Card>
|
|
<Modal
|
title={editingUser ? '编辑用户' : '新增用户'}
|
open={isModalOpen}
|
onOk={handleSubmit}
|
onCancel={closeModal}
|
okText={editingUser ? '保存' : '创建'}
|
cancelText="取消"
|
forceRender
|
>
|
<Form form={form} layout="vertical" preserve={false}>
|
<Form.Item
|
name="username"
|
label="用户名"
|
rules={[
|
{ required: true, message: '请输入用户名' },
|
{ pattern: /^[a-zA-Z][a-zA-Z0-9._-]{2,19}$/, message: '3-20 位,字母开头' },
|
]}
|
>
|
<Input placeholder="请输入登录用户名" disabled={Boolean(editingUser)} />
|
</Form.Item>
|
<Form.Item
|
name="nickname"
|
label="姓名"
|
rules={[{ required: true, message: '请输入姓名' }]}
|
>
|
<Input placeholder="请输入用户姓名" />
|
</Form.Item>
|
<Form.Item
|
name="role"
|
label="角色"
|
rules={[{ required: true, message: '请选择角色' }]}
|
>
|
<Select placeholder="请选择角色" options={roleOptions} />
|
</Form.Item>
|
<Form.Item
|
name="status"
|
label="状态"
|
rules={[{ required: true, message: '请选择状态' }]}
|
>
|
<Select
|
options={[
|
{ value: 'active', label: '启用' },
|
{ value: 'disabled', label: '禁用' },
|
]}
|
/>
|
</Form.Item>
|
</Form>
|
</Modal>
|
</div>
|
);
|
}
|