feat(用户管理): 新增用户管理页面

- 实现用户列表展示功能
- 添加用户搜索和筛选
- 集成表格分页组件
- 支持用户查看、编辑、删除操作
This commit is contained in:
gaoziman 2025-11-05 23:48:21 +08:00
parent d5dcd6780c
commit a44b06cc15
6 changed files with 697 additions and 0 deletions

View File

@ -0,0 +1,117 @@
import React from 'react';
import { Button, Typography, Badge, Space } from '@arco-design/web-react';
import { IconEye, IconEdit, IconDelete } from '@arco-design/web-react/icon';
import dayjs from 'dayjs';
const { Text } = Typography;
export const RoleType = ['管理员', '普通用户', '访客'];
export const Status = ['禁用', '启用'];
export function getColumns(
t: any,
callback: (record: Record<string, any>, type: string) => Promise<void>
) {
return [
{
title: t['userManagement.columns.id'],
dataIndex: 'id',
width: 100,
render: (value) => <Text copyable>{value}</Text>,
},
{
title: t['userManagement.columns.name'],
dataIndex: 'name',
width: 100,
},
{
title: t['userManagement.columns.username'],
dataIndex: 'username',
width: 120,
},
{
title: t['userManagement.columns.email'],
dataIndex: 'email',
width: 200,
},
{
title: t['userManagement.columns.phone'],
dataIndex: 'phone',
width: 130,
},
{
title: t['userManagement.columns.role'],
dataIndex: 'role',
width: 100,
render: (value) => {
const roleMap = {
0: { text: t['userManagement.role.admin'], color: 'red' },
1: { text: t['userManagement.role.user'], color: 'blue' },
2: { text: t['userManagement.role.guest'], color: 'gray' },
};
const role = roleMap[value] || roleMap[1];
return <Badge color={role.color} text={role.text} />;
},
},
{
title: t['userManagement.columns.department'],
dataIndex: 'department',
width: 120,
},
{
title: t['userManagement.columns.createdTime'],
dataIndex: 'createdTime',
width: 180,
render: (x) => dayjs().subtract(x, 'days').format('YYYY-MM-DD HH:mm:ss'),
sorter: (a, b) => b.createdTime - a.createdTime,
},
{
title: t['userManagement.columns.status'],
dataIndex: 'status',
width: 100,
render: (x) => {
if (x === 0) {
return <Badge status="error" text={Status[x]}></Badge>;
}
return <Badge status="success" text={Status[x]}></Badge>;
},
},
{
title: t['userManagement.columns.operations'],
dataIndex: 'operations',
width: 240,
fixed: 'right' as const,
render: (_, record) => (
<Space>
<Button
type="text"
size="small"
icon={<IconEye />}
onClick={() => callback(record, 'view')}
>
{t['userManagement.columns.operations.view']}
</Button>
<Button
type="text"
size="small"
icon={<IconEdit />}
onClick={() => callback(record, 'edit')}
>
{t['userManagement.columns.operations.edit']}
</Button>
<Button
type="text"
size="small"
status="danger"
icon={<IconDelete />}
onClick={() => callback(record, 'delete')}
>
{t['userManagement.columns.operations.delete']}
</Button>
</Space>
),
},
];
}
export default () => RoleType;

View File

@ -0,0 +1,160 @@
import React, { useContext } from 'react';
import dayjs from 'dayjs';
import {
Form,
Input,
Select,
DatePicker,
Button,
Grid,
} from '@arco-design/web-react';
import { GlobalContext } from '@/context';
import locale from './locale';
import useLocale from '@/utils/useLocale';
import { IconRefresh, IconSearch } from '@arco-design/web-react/icon';
import { RoleType, Status } from './constants';
import styles from './style/index.module.less';
const { Row, Col } = Grid;
const { useForm } = Form;
function SearchForm(props: {
onSearch: (values: Record<string, any>) => void;
}) {
const { lang } = useContext(GlobalContext);
const t = useLocale(locale);
const [form] = useForm();
const handleSubmit = () => {
const values = form.getFieldsValue();
props.onSearch(values);
};
const handleReset = () => {
form.resetFields();
props.onSearch({});
};
const colSpan = lang === 'zh-CN' ? 8 : 12;
return (
<div className={styles['search-form-wrapper']}>
<Form
form={form}
className={styles['search-form']}
labelAlign="left"
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
>
<Row gutter={24}>
<Col span={colSpan}>
<Form.Item label={t['userManagement.columns.id']} field="id">
<Input
placeholder={t['userManagement.form.id.placeholder']}
allowClear
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item label={t['userManagement.columns.name']} field="name">
<Input
allowClear
placeholder={t['userManagement.form.name.placeholder']}
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
label={t['userManagement.columns.username']}
field="username"
>
<Input
allowClear
placeholder={t['userManagement.form.username.placeholder']}
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item label={t['userManagement.columns.email']} field="email">
<Input
allowClear
placeholder={t['userManagement.form.email.placeholder']}
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item label={t['userManagement.columns.phone']} field="phone">
<Input
allowClear
placeholder={t['userManagement.form.phone.placeholder']}
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item label={t['userManagement.columns.role']} field="role">
<Select
placeholder={t['userManagement.form.all.placeholder']}
options={RoleType.map((item, index) => ({
label: item,
value: index,
}))}
mode="multiple"
allowClear
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
label={t['userManagement.columns.department']}
field="department"
>
<Input
allowClear
placeholder={t['userManagement.form.department.placeholder']}
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
label={t['userManagement.columns.status']}
field="status"
>
<Select
placeholder={t['userManagement.form.all.placeholder']}
options={Status.map((item, index) => ({
label: item,
value: index,
}))}
mode="multiple"
allowClear
/>
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item
label={t['userManagement.columns.createdTime']}
field="createdTime"
>
<DatePicker.RangePicker
allowClear
style={{ width: '100%' }}
disabledDate={(date) => dayjs(date).isAfter(dayjs())}
/>
</Form.Item>
</Col>
</Row>
</Form>
<div className={styles['right-button']}>
<Button type="primary" icon={<IconSearch />} onClick={handleSubmit}>
{t['userManagement.form.search']}
</Button>
<Button icon={<IconRefresh />} onClick={handleReset}>
{t['userManagement.form.reset']}
</Button>
</div>
</div>
);
}
export default SearchForm;

View File

@ -0,0 +1,171 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Table,
Card,
PaginationProps,
Button,
Space,
Typography,
Message,
} from '@arco-design/web-react';
import PermissionWrapper from '@/components/PermissionWrapper';
import Pagination from '@/components/Pagination';
import {
IconDownload,
IconPlus,
IconDelete,
} from '@arco-design/web-react/icon';
import axios from 'axios';
import useLocale from '@/utils/useLocale';
import SearchForm from './form';
import locale from './locale';
import styles from './style/index.module.less';
import './mock';
import { getColumns } from './constants';
const { Title } = Typography;
function UserManagement() {
const t = useLocale(locale);
const tableCallback = async (record, type) => {
console.log(record, type);
// 简单的操作提示
if (type === 'view') {
Message.info(`查看用户: ${record.name}`);
} else if (type === 'edit') {
Message.info(`编辑用户: ${record.name}`);
} else if (type === 'delete') {
Message.warning(`删除用户: ${record.name}`);
}
};
const columns = useMemo(() => getColumns(t, tableCallback), [t]);
const [data, setData] = useState([]);
const [pagination, setPagination] = useState<PaginationProps>({
sizeCanChange: true,
showTotal: true,
pageSize: 10,
current: 1,
pageSizeChangeResetCurrent: true,
});
const [loading, setLoading] = useState(true);
const [formParams, setFormParams] = useState({});
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
useEffect(() => {
fetchData();
}, [pagination.current, pagination.pageSize, JSON.stringify(formParams)]);
function fetchData() {
const { current, pageSize } = pagination;
setLoading(true);
axios
.get('/api/user-management', {
params: {
page: current,
pageSize,
...formParams,
},
})
.then((res) => {
setData(res.data.list);
setPagination({
...pagination,
current,
pageSize,
total: res.data.total,
});
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}
function onChangeTable(current: number, pageSize: number) {
setPagination({
...pagination,
current,
pageSize,
});
}
function handleSearch(params) {
setPagination({ ...pagination, current: 1 });
setFormParams(params);
}
function handleAdd() {
Message.info(t['userManagement.operations.add']);
}
function handleBatchDelete() {
if (selectedRowKeys.length === 0) {
Message.warning('请选择要删除的用户');
return;
}
Message.warning(`批量删除 ${selectedRowKeys.length} 个用户`);
}
function handleExport() {
Message.info(t['userManagement.operations.export']);
}
return (
<Card>
<Title heading={6}>{t['menu.list.userManagement']}</Title>
<SearchForm onSearch={handleSearch} />
<PermissionWrapper
requiredPermissions={[
{ resource: 'menu.list.userManagement', actions: ['write'] },
]}
>
<div className={styles['button-group']}>
<Space>
<Button type="primary" icon={<IconPlus />} onClick={handleAdd}>
{t['userManagement.operations.add']}
</Button>
<Button icon={<IconDelete />} onClick={handleBatchDelete}>
{t['userManagement.operations.batchDelete']}
</Button>
</Space>
<Space>
<Button icon={<IconDownload />} onClick={handleExport}>
{t['userManagement.operations.export']}
</Button>
</Space>
</div>
</PermissionWrapper>
<Table
rowKey="id"
loading={loading}
pagination={false}
columns={columns}
data={data}
rowSelection={{
type: 'checkbox',
selectedRowKeys,
onChange: (selectedRowKeys) => {
setSelectedRowKeys(selectedRowKeys);
},
}}
scroll={{ x: 1640 }}
/>
<Pagination
total={pagination.total}
current={pagination.current}
pageSize={pagination.pageSize}
sizeCanChange={pagination.sizeCanChange}
onChange={onChangeTable}
onPageSizeChange={(pageSize) => {
setPagination({ ...pagination, current: 1, pageSize });
}}
/>
</Card>
);
}
export default UserManagement;

View File

@ -0,0 +1,74 @@
const i18n = {
'en-US': {
'menu.list.userManagement': 'User Management',
'userManagement.form.search': 'Search',
'userManagement.form.reset': 'Reset',
'userManagement.columns.id': 'User ID',
'userManagement.columns.name': 'Name',
'userManagement.columns.username': 'Username',
'userManagement.columns.email': 'Email',
'userManagement.columns.phone': 'Phone',
'userManagement.columns.role': 'Role',
'userManagement.columns.department': 'Department',
'userManagement.columns.status': 'Status',
'userManagement.columns.createdTime': 'Created Time',
'userManagement.columns.operations': 'Operations',
'userManagement.columns.operations.view': 'View',
'userManagement.columns.operations.edit': 'Edit',
'userManagement.columns.operations.delete': 'Delete',
'userManagement.columns.operations.enable': 'Enable',
'userManagement.columns.operations.disable': 'Disable',
'userManagement.operations.add': 'Add User',
'userManagement.operations.batchDelete': 'Batch Delete',
'userManagement.operations.export': 'Export',
'userManagement.role.admin': 'Administrator',
'userManagement.role.user': 'User',
'userManagement.role.guest': 'Guest',
'userManagement.status.enabled': 'Enabled',
'userManagement.status.disabled': 'Disabled',
'userManagement.form.id.placeholder': 'Please enter user ID',
'userManagement.form.name.placeholder': 'Please enter name',
'userManagement.form.username.placeholder': 'Please enter username',
'userManagement.form.email.placeholder': 'Please enter email',
'userManagement.form.phone.placeholder': 'Please enter phone',
'userManagement.form.department.placeholder': 'Please enter department',
'userManagement.form.all.placeholder': 'All',
},
'zh-CN': {
'menu.list.userManagement': '用户管理',
'userManagement.form.search': '查询',
'userManagement.form.reset': '重置',
'userManagement.columns.id': '用户ID',
'userManagement.columns.name': '姓名',
'userManagement.columns.username': '用户名',
'userManagement.columns.email': '邮箱',
'userManagement.columns.phone': '手机号',
'userManagement.columns.role': '角色',
'userManagement.columns.department': '部门',
'userManagement.columns.status': '状态',
'userManagement.columns.createdTime': '创建时间',
'userManagement.columns.operations': '操作',
'userManagement.columns.operations.view': '查看',
'userManagement.columns.operations.edit': '编辑',
'userManagement.columns.operations.delete': '删除',
'userManagement.columns.operations.enable': '启用',
'userManagement.columns.operations.disable': '禁用',
'userManagement.operations.add': '新建用户',
'userManagement.operations.batchDelete': '批量删除',
'userManagement.operations.export': '导出',
'userManagement.role.admin': '管理员',
'userManagement.role.user': '普通用户',
'userManagement.role.guest': '访客',
'userManagement.status.enabled': '启用',
'userManagement.status.disabled': '禁用',
'userManagement.form.id.placeholder': '请输入用户ID',
'userManagement.form.name.placeholder': '请输入姓名',
'userManagement.form.username.placeholder': '请输入用户名',
'userManagement.form.email.placeholder': '请输入邮箱',
'userManagement.form.phone.placeholder': '请输入手机号',
'userManagement.form.department.placeholder': '请输入部门',
'userManagement.form.all.placeholder': '全部',
},
};
export default i18n;

View File

@ -0,0 +1,134 @@
import Mock from 'mockjs';
import qs from 'query-string';
import dayjs from 'dayjs';
import setupMock from '@/utils/setupMock';
const { list } = Mock.mock({
'list|100': [
{
id: /[0-9]{8}/,
name: () => Mock.Random.cname(),
username: () => Mock.Random.word(5, 10),
email: () => Mock.Random.email(),
phone: /1[3-9]\d{9}/,
'role|0-2': 0,
department: () =>
Mock.Random.pick([
'技术部',
'产品部',
'运营部',
'市场部',
'人力资源部',
'财务部',
]),
'status|0-1': 0,
'createdTime|1-365': 0,
},
],
});
const filterData = (
rest: {
id?: string;
name?: string;
username?: string;
email?: string;
phone?: string;
'role[]'?: string[];
department?: string;
'status[]'?: string[];
'createdTime[]'?: string[];
} = {}
) => {
const {
id,
name,
username,
email,
phone,
'role[]': role,
department,
'status[]': status,
'createdTime[]': createdTime,
} = rest;
if (id) {
return list.filter((item) => item.id === id);
}
let result = [...list];
if (name) {
result = result.filter((item) => {
return (item.name as string).toLowerCase().includes(name.toLowerCase());
});
}
if (username) {
result = result.filter((item) => {
return (item.username as string)
.toLowerCase()
.includes(username.toLowerCase());
});
}
if (email) {
result = result.filter((item) => {
return (item.email as string).toLowerCase().includes(email.toLowerCase());
});
}
if (phone) {
result = result.filter((item) => {
return (item.phone as string).includes(phone);
});
}
if (role && role.length > 0) {
result = result.filter((item) => role.includes(item.role.toString()));
}
if (department) {
result = result.filter((item) => {
return (item.department as string).includes(department);
});
}
if (status && status.length > 0) {
result = result.filter((item) => status.includes(item.status.toString()));
}
if (createdTime && createdTime.length === 2) {
const [begin, end] = createdTime;
result = result.filter((item) => {
const time = dayjs()
.subtract(item.createdTime, 'days')
.format('YYYY-MM-DD HH:mm:ss');
return (
!dayjs(time).isBefore(dayjs(begin)) && !dayjs(time).isAfter(dayjs(end))
);
});
}
return result;
};
setupMock({
setup: () => {
Mock.mock(new RegExp('/api/user-management'), (params) => {
const {
page = 1,
pageSize = 10,
...rest
} = qs.parseUrl(params.url).query;
const p = page as number;
const ps = pageSize as number;
const result = filterData(rest);
return {
list: result.slice((p - 1) * ps, p * ps),
total: result.length,
};
});
},
});

View File

@ -0,0 +1,41 @@
.toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
}
.operations {
display: flex;
}
.search-form-wrapper {
display: flex;
border-bottom: 1px solid var(--color-border-1);
margin-bottom: 20px;
.right-button {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 20px;
margin-bottom: 20px;
border-left: 1px solid var(--color-border-2);
box-sizing: border-box;
}
}
.button-group {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.search-form {
padding-right: 20px;
:global(.arco-form-label-item-left) {
> label {
white-space: nowrap;
}
}
}