Compare commits
2 Commits
311ce55fb7
...
ff54a2bba5
| Author | SHA1 | Date | |
|---|---|---|---|
| ff54a2bba5 | |||
| 5d8ddd2bd7 |
10
AGENTS.md
10
AGENTS.md
@@ -158,9 +158,15 @@ const res = await get<User[]>('/api/users');
|
||||
// res.code / res.msg / res.data / res.ok / res.time
|
||||
```
|
||||
|
||||
## API 层规范
|
||||
|
||||
- **数据结构不导出**:`src/api/` 目录下的 interface / type 不得加 `export`,禁止其他模块 import
|
||||
- **组件自定类型**:页面组件如需使用数据结构,在组件文件内自行定义,不依赖 API 层的类型
|
||||
- **mock 文件同理**:`src/mock/` 下的文件也需自行定义类型,不导入 `src/api/` 中的类型
|
||||
- API 函数可以内部使用类型,但签名中避免使用导出的复杂类型(可用 `Record<string, unknown>` 或 `any` 代替)
|
||||
|
||||
## 参考文档
|
||||
|
||||
- Rsbuild: https://rsbuild.rs/llms.txt
|
||||
- Rspack: https://rspack.rs/llms.txt
|
||||
- antd v6 完整文档(中文): https://ant.design/llms-full-cn.txt
|
||||
- antd v6 组件单页(中文): https://ant.design/components/{组件名}-cn.md(如 button-cn.md)
|
||||
- antd v6 组件导航文档: https://ant.design/llms.txt
|
||||
|
||||
@@ -4,4 +4,7 @@ import { pluginReact } from '@rsbuild/plugin-react';
|
||||
// Docs: https://rsbuild.rs/config/
|
||||
export default defineConfig({
|
||||
plugins: [pluginReact()],
|
||||
server: {
|
||||
port: 6000,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
|
||||
/** 部门节点数据结构 */
|
||||
export interface DeptNode {
|
||||
key: string;
|
||||
/** 部门节点数据结构(不导出,仅供本文件内使用) */
|
||||
interface DeptNode {
|
||||
id: string;
|
||||
title: string;
|
||||
parentKey: string | null;
|
||||
parentId: string | null;
|
||||
children?: DeptNode[];
|
||||
}
|
||||
|
||||
/** 新增/编辑部门的请求参数 */
|
||||
export interface DeptParams {
|
||||
/** 新增/编辑部门的请求参数(不导出) */
|
||||
interface DeptParams {
|
||||
title: string;
|
||||
parentKey?: string | null;
|
||||
parentId?: string | null;
|
||||
}
|
||||
|
||||
/** 用户记录数据结构 */
|
||||
export interface UserRecord {
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
/** 用户记录数据结构(不导出,仅供本文件内使用) */
|
||||
interface UserRecord {
|
||||
id: string;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
status: string;
|
||||
deptId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门树
|
||||
*/
|
||||
/** 新增/编辑用户的请求参数(不导出) */
|
||||
interface UserParams {
|
||||
userName: string;
|
||||
nickName: string;
|
||||
status: string;
|
||||
deptId: string;
|
||||
}
|
||||
|
||||
// ───────────── 部门接口 ─────────────
|
||||
|
||||
/** 获取部门树 */
|
||||
export const deptTree = () => get<DeptNode[]>('/api/system/dept/tree');
|
||||
|
||||
/**
|
||||
@@ -35,21 +44,43 @@ export const addDept = (params: DeptParams) => post<DeptNode>('/api/system/dept/
|
||||
|
||||
/**
|
||||
* 编辑部门
|
||||
* @param key 部门 key
|
||||
* @param id 部门 ID
|
||||
* @param params 部门参数
|
||||
*/
|
||||
export const editDept = (key: string, params: DeptParams) =>
|
||||
post<DeptNode>('/api/system/dept/edit', { key, ...params });
|
||||
export const editDept = (id: string, params: DeptParams) =>
|
||||
post<DeptNode>('/api/system/dept/edit', { id, ...params });
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param key 部门 key
|
||||
* @param id 部门 ID
|
||||
*/
|
||||
export const delDept = (key: string) => post('/api/system/dept/del', { key });
|
||||
export const delDept = (id: string) => post('/api/system/dept/del', { id });
|
||||
|
||||
// ───────────── 用户接口 ─────────────
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @param deptKey 部门 key
|
||||
* @param deptId 部门 ID
|
||||
*/
|
||||
export const listUser = (deptKey: string) =>
|
||||
get<UserRecord[]>('/api/system/user/list', { deptKey });
|
||||
export const listUser = (deptId: string) =>
|
||||
get<UserRecord[]>('/api/system/user/list', { deptId });
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
* @param params 用户参数
|
||||
*/
|
||||
export const addUser = (params: UserParams) => post<UserRecord>('/api/system/user/add', params);
|
||||
|
||||
/**
|
||||
* 编辑用户
|
||||
* @param id 用户 ID
|
||||
* @param params 用户参数
|
||||
*/
|
||||
export const editUser = (id: string, params: UserParams) =>
|
||||
post<UserRecord>('/api/system/user/edit', { id, ...params });
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param id 用户 ID
|
||||
*/
|
||||
export const delUser = (id: string) => post('/api/system/user/del', { id });
|
||||
|
||||
@@ -1,71 +1,157 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
// ── 本地类型定义(不依赖 API 层)───────────
|
||||
|
||||
/** 部门节点(mock 本地定义) */
|
||||
interface MockDeptNode {
|
||||
id: string;
|
||||
title: string;
|
||||
parentId: string | null;
|
||||
children?: MockDeptNode[];
|
||||
}
|
||||
|
||||
/** 用户记录(mock 本地定义) */
|
||||
interface MockUserRecord {
|
||||
id: string;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
status: string;
|
||||
deptId?: string;
|
||||
}
|
||||
|
||||
// ── mock 数据 ────────────────────────────────
|
||||
|
||||
/** 部门树 mock 数据 */
|
||||
const deptList = [
|
||||
const deptList: MockDeptNode[] = [
|
||||
{
|
||||
key: '1',
|
||||
id: '1',
|
||||
title: '总公司',
|
||||
parentKey: null,
|
||||
parentId: null,
|
||||
children: [
|
||||
{
|
||||
key: '1-1',
|
||||
id: '1-1',
|
||||
title: '技术部',
|
||||
parentKey: '1',
|
||||
parentId: '1',
|
||||
children: [
|
||||
{ key: '1-1-1', title: '前端组', parentKey: '1-1', children: [] },
|
||||
{ key: '1-1-2', title: '后端组', parentKey: '1-1', children: [] },
|
||||
{ id: '1-1-1', title: '前端组', parentId: '1-1', children: [] },
|
||||
{ id: '1-1-2', title: '后端组', parentId: '1-1', children: [] },
|
||||
],
|
||||
},
|
||||
{ key: '1-2', title: '产品部', parentKey: '1', children: [] },
|
||||
{ key: '1-3', title: '运营部', parentKey: '1', children: [] },
|
||||
{ id: '1-2', title: '产品部', parentId: '1', children: [] },
|
||||
{ id: '1-3', title: '运营部', parentId: '1', children: [] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** 用户列表 mock 数据,按部门 key 分组 */
|
||||
const userMap: Record<string, object[]> = {
|
||||
'1': [{ username: 'admin', name: '管理员', email: 'admin@example.com', status: '启用' }],
|
||||
/** 用户列表 mock 数据,按部门 id 分组(运行时可变) */
|
||||
const userMap: Record<string, MockUserRecord[]> = {
|
||||
'1': [{ id: '1', userName: 'admin', nickName: '管理员', status: '启用', deptId: '1' }],
|
||||
'1-1': [
|
||||
{ username: 'zhangsan', name: '张三', email: 'zhangsan@example.com', status: '启用' },
|
||||
{ username: 'lisi', name: '李四', email: 'lisi@example.com', status: '启用' },
|
||||
{ id: '2', userName: 'zhangsan', nickName: '张三', status: '启用', deptId: '1-1' },
|
||||
{ id: '3', userName: 'lisi', nickName: '李四', status: '启用', deptId: '1-1' },
|
||||
],
|
||||
'1-1-1': [{ username: 'wangwu', name: '王五', email: 'wangwu@example.com', status: '启用' }],
|
||||
'1-1-2': [{ username: 'zhaoliu', name: '赵六', email: 'zhaoliu@example.com', status: '禁用' }],
|
||||
'1-2': [{ username: 'sunqi', name: '孙七', email: 'sunqi@example.com', status: '启用' }],
|
||||
'1-1-1': [{ id: '4', userName: 'wangwu', nickName: '王五', status: '启用', deptId: '1-1-1' }],
|
||||
'1-1-2': [{ id: '5', userName: 'zhaoliu', nickName: '赵六', status: '禁用', deptId: '1-1-2' }],
|
||||
'1-2': [{ id: '6', userName: 'sunqi', nickName: '孙七', status: '启用', deptId: '1-2' }],
|
||||
'1-3': [],
|
||||
};
|
||||
|
||||
/** 包装为统一响应格式 */
|
||||
const ok = (data: unknown) => ({ code: '0', msg: 'ok', data, time: Date.now(), ok: true });
|
||||
|
||||
// ── MSW handlers ─────────────────────────────
|
||||
|
||||
export const systemHandlers = [
|
||||
// 获取部门树
|
||||
// ───── 部门接口 ─────
|
||||
|
||||
http.get('/api/system/dept/tree', () => {
|
||||
return HttpResponse.json(ok(deptList));
|
||||
}),
|
||||
|
||||
// 新增部门
|
||||
http.post('/api/system/dept/add', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json(ok({ key: `dept_${Date.now()}`, ...body }));
|
||||
const newNode: MockDeptNode = {
|
||||
id: `dept_${Date.now()}`,
|
||||
title: body.title as string,
|
||||
parentId: body.parentId as string ?? null,
|
||||
children: [],
|
||||
};
|
||||
const addToTree = (nodes: MockDeptNode[]): boolean => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === body.parentId) {
|
||||
node.children = node.children ?? [];
|
||||
node.children.push(newNode);
|
||||
return true;
|
||||
}
|
||||
if (node.children && addToTree(node.children)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (body.parentId) addToTree(deptList);
|
||||
else deptList.push(newNode);
|
||||
return HttpResponse.json(ok(newNode));
|
||||
}),
|
||||
|
||||
// 编辑部门
|
||||
http.post('/api/system/dept/edit', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const editInTree = (nodes: MockDeptNode[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === body.id) {
|
||||
node.title = body.title as string;
|
||||
return true;
|
||||
}
|
||||
if (node.children && editInTree(node.children)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
editInTree(deptList);
|
||||
return HttpResponse.json(ok(body));
|
||||
}),
|
||||
|
||||
// 删除部门
|
||||
http.post('/api/system/dept/del', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json(ok({ key: body.key }));
|
||||
const body = await request.json() as { id: string };
|
||||
const removeFromTree = (nodes: MockDeptNode[]): boolean => {
|
||||
const idx = nodes.findIndex(n => n.id === body.id);
|
||||
if (idx !== -1) { nodes.splice(idx, 1); return true; }
|
||||
for (const node of nodes) {
|
||||
if (node.children && removeFromTree(node.children)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
removeFromTree(deptList);
|
||||
return HttpResponse.json(ok({ id: body.id }));
|
||||
}),
|
||||
|
||||
// 根据部门获取用户列表
|
||||
// ───── 用户接口 ─────
|
||||
|
||||
http.get('/api/system/user/list', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const deptKey = url.searchParams.get('deptKey') ?? '';
|
||||
return HttpResponse.json(ok(userMap[deptKey] ?? []));
|
||||
const deptId = url.searchParams.get('deptId') ?? '';
|
||||
return HttpResponse.json(ok(userMap[deptId] ?? []));
|
||||
}),
|
||||
|
||||
http.post('/api/system/user/add', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const newUser = { id: `user_${Date.now()}`, ...body } as MockUserRecord;
|
||||
const list = userMap[newUser.deptId ?? ''] ?? [];
|
||||
list.push(newUser);
|
||||
userMap[newUser.deptId ?? ''] = list;
|
||||
return HttpResponse.json(ok(newUser));
|
||||
}),
|
||||
|
||||
http.post('/api/system/user/edit', async ({ request }) => {
|
||||
const body = await request.json() as MockUserRecord;
|
||||
const list = userMap[body.deptId ?? ''] ?? [];
|
||||
const idx = list.findIndex((u) => u.id === body.id);
|
||||
if (idx !== -1) list[idx] = body;
|
||||
return HttpResponse.json(ok(body));
|
||||
}),
|
||||
|
||||
http.post('/api/system/user/del', async ({ request }) => {
|
||||
const body = await request.json() as { id: string };
|
||||
for (const key of Object.keys(userMap)) {
|
||||
userMap[key] = userMap[key].filter((u) => u.id !== body.id);
|
||||
}
|
||||
return HttpResponse.json(ok({ id: body.id }));
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2,16 +2,18 @@ import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Popconfirm, Space, Spin, Tree } from 'antd';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
addDept,
|
||||
delDept,
|
||||
deptTree,
|
||||
editDept,
|
||||
type DeptNode,
|
||||
} from '@/api/system/user';
|
||||
import { addDept, delDept, deptTree, editDept } from '@/api/system/user';
|
||||
import DeptModal from './DeptModal';
|
||||
import type { DeptFormValues } from './DeptModal';
|
||||
|
||||
/** 部门节点(页面本地定义,不依赖 API 层类型) */
|
||||
interface DeptNode {
|
||||
id: string;
|
||||
title: string;
|
||||
parentId: string | null;
|
||||
children?: DeptNode[];
|
||||
}
|
||||
|
||||
/** 单个树节点标题,含悬浮操作按钮 */
|
||||
const DeptTreeNode = ({
|
||||
node,
|
||||
@@ -20,9 +22,9 @@ const DeptTreeNode = ({
|
||||
onDelete,
|
||||
}: {
|
||||
node: DeptNode;
|
||||
onAdd: (key: string) => void;
|
||||
onAdd: (id: string) => void;
|
||||
onEdit: (node: DeptNode) => void;
|
||||
onDelete: (key: string) => Promise<void>;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
@@ -44,7 +46,7 @@ const DeptTreeNode = ({
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
title="新增子部门"
|
||||
onClick={() => onAdd(node.key)}
|
||||
onClick={() => onAdd(node.id)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -60,7 +62,7 @@ const DeptTreeNode = ({
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => onDelete(node.key)}
|
||||
onConfirm={() => onDelete(node.id)}
|
||||
>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} title="删除" />
|
||||
</Popconfirm>
|
||||
@@ -72,27 +74,27 @@ const DeptTreeNode = ({
|
||||
/** 递归将 DeptNode 转为 antd Tree 的 DataNode */
|
||||
const toTreeData = (
|
||||
nodes: DeptNode[],
|
||||
onAdd: (key: string) => void,
|
||||
onAdd: (id: string) => void,
|
||||
onEdit: (node: DeptNode) => void,
|
||||
onDelete: (key: string) => Promise<void>,
|
||||
onDelete: (id: string) => Promise<void>,
|
||||
): DataNode[] =>
|
||||
nodes.map((node) => ({
|
||||
key: node.key,
|
||||
key: node.id, // antd Tree 必需
|
||||
title: <DeptTreeNode node={node} onAdd={onAdd} onEdit={onEdit} onDelete={onDelete} />,
|
||||
children: node.children ? toTreeData(node.children, onAdd, onEdit, onDelete) : undefined,
|
||||
}));
|
||||
|
||||
interface DeptTreeProps {
|
||||
/** 当前选中的部门 key */
|
||||
selectedKey: string;
|
||||
/** 当前选中的部门 id */
|
||||
selectedId: string;
|
||||
/** 选中部门回调 */
|
||||
onSelect: (key: string) => void;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
/** 弹窗模式:新增根部门 | 新增子部门 | 编辑 */
|
||||
type ModalMode = 'addRoot' | 'addChild' | 'edit';
|
||||
|
||||
const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => {
|
||||
const DeptTree = ({ selectedId, onSelect }: DeptTreeProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deptData, setDeptData] = useState<DeptNode[]>([]);
|
||||
|
||||
@@ -125,7 +127,7 @@ const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => {
|
||||
const [modalMode, setModalMode] = useState<ModalMode>('addRoot');
|
||||
const [modalTitle, setModalTitle] = useState('新增部门');
|
||||
const [editingNode, setEditingNode] = useState<DeptNode | null>(null);
|
||||
const [parentKey, setParentKey] = useState<string>('');
|
||||
const [parentId, setParentId] = useState<string>('');
|
||||
|
||||
/** 打开新增根部门弹窗 */
|
||||
const handleAddRoot = () => {
|
||||
@@ -136,10 +138,10 @@ const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => {
|
||||
};
|
||||
|
||||
/** 打开新增子部门弹窗 */
|
||||
const handleAddChild = (pKey: string) => {
|
||||
const handleAddChild = (pId: string) => {
|
||||
setModalMode('addChild');
|
||||
setModalTitle('新增子部门');
|
||||
setParentKey(pKey);
|
||||
setParentId(pId);
|
||||
setEditingNode(null);
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -153,8 +155,8 @@ const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => {
|
||||
};
|
||||
|
||||
/** 删除部门,由 Popconfirm 确认后调用 */
|
||||
const handleDelete = async (key: string) => {
|
||||
await delDept(key);
|
||||
const handleDelete = async (id: string) => {
|
||||
await delDept(id);
|
||||
// 删除成功后重新加载部门树
|
||||
loadDeptTree();
|
||||
};
|
||||
@@ -162,11 +164,11 @@ const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => {
|
||||
/** Modal 确认,根据模式调用对应接口后刷新部门树 */
|
||||
const handleModalOk = async (values: DeptFormValues) => {
|
||||
if (modalMode === 'addRoot') {
|
||||
await addDept({ title: values.name, parentKey: null });
|
||||
await addDept({ title: values.name, parentId: null });
|
||||
} else if (modalMode === 'addChild') {
|
||||
await addDept({ title: values.name, parentKey: parentKey });
|
||||
await addDept({ title: values.name, parentId });
|
||||
} else if (modalMode === 'edit' && editingNode) {
|
||||
await editDept(editingNode.key, { title: values.name });
|
||||
await editDept(editingNode.id, { title: values.name });
|
||||
}
|
||||
setModalOpen(false);
|
||||
// 操作成功后重新加载部门树
|
||||
@@ -191,7 +193,7 @@ const DeptTree = ({ selectedKey, onSelect }: DeptTreeProps) => {
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
defaultExpandAll
|
||||
selectedKeys={[selectedKey]}
|
||||
selectedKeys={[selectedId]}
|
||||
blockNode
|
||||
onSelect={(keys) => {
|
||||
if (keys.length > 0) onSelect(String(keys[0]));
|
||||
|
||||
79
src/pages/system/user/UserModal.tsx
Normal file
79
src/pages/system/user/UserModal.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Form, Input, Modal, Select } from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/** 用户表单字段(页面本地定义) */
|
||||
export interface UserFormValues {
|
||||
userName: string;
|
||||
nickName: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface UserModalProps {
|
||||
/** 弹窗是否可见 */
|
||||
open: boolean;
|
||||
/** 弹窗标题 */
|
||||
title: string;
|
||||
/** 编辑时的初始值 */
|
||||
initialValues?: Partial<UserFormValues>;
|
||||
/** 确认回调 */
|
||||
onOk: (values: UserFormValues) => void;
|
||||
/** 取消回调 */
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const UserModal = ({ open, title, initialValues, onOk, onCancel }: UserModalProps) => {
|
||||
const [form] = Form.useForm<UserFormValues>();
|
||||
|
||||
/** 每次打开时重置并填充表单 */
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
if (initialValues) {
|
||||
form.setFieldsValue({
|
||||
userName: initialValues.userName,
|
||||
nickName: initialValues.nickName,
|
||||
status: initialValues.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [open, initialValues, form]);
|
||||
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
onOk(values);
|
||||
};
|
||||
|
||||
const isEdit = !!initialValues;
|
||||
|
||||
return (
|
||||
<Modal title={title} open={open} onOk={handleOk} onCancel={onCancel} destroyOnClose>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="userName"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
{/* 编辑时用户名不可修改 */}
|
||||
<Input placeholder="请输入用户名" disabled={isEdit} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="昵称"
|
||||
name="nickName"
|
||||
rules={[{ required: true, message: '请输入昵称' }]}
|
||||
>
|
||||
<Input placeholder="请输入昵称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="状态"
|
||||
name="status"
|
||||
initialValue="启用"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select options={[{ label: '启用', value: '启用' }, { label: '禁用', value: '禁用' }]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserModal;
|
||||
@@ -1,6 +1,19 @@
|
||||
import { Card, Col, Table, Tag } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { App, Button, Card, Col, Popconfirm, Space, Table, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { listUser, type UserRecord } from '@/api/system/user';
|
||||
import { addUser, delUser, editUser, listUser } from '@/api/system/user';
|
||||
import UserModal from './UserModal';
|
||||
import type { UserFormValues } from './UserModal';
|
||||
|
||||
/** 用户记录(页面本地定义,不依赖 API 层类型) */
|
||||
interface UserRecord {
|
||||
id: string;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
status: string;
|
||||
deptId?: string;
|
||||
}
|
||||
|
||||
/** 状态值与 Tag 颜色的映射 */
|
||||
const statusColorMap: Record<string, string> = {
|
||||
@@ -8,35 +21,30 @@ const statusColorMap: Record<string, string> = {
|
||||
禁用: 'error',
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={statusColorMap[status] ?? 'default'}>{status}</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
interface UserTableProps {
|
||||
deptKey: string;
|
||||
/** 当前选中的部门 id */
|
||||
deptId: string;
|
||||
}
|
||||
|
||||
const UserTable = ({ deptKey }: UserTableProps) => {
|
||||
const UserTable = ({ deptId }: UserTableProps) => {
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<UserRecord[]>([]);
|
||||
|
||||
// tick 变化时触发用户列表重新加载
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
/** 触发用户列表刷新 */
|
||||
const refresh = () => setTick((n) => n + 1);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listUser(deptKey);
|
||||
const res = await listUser(deptId);
|
||||
// 用 cancelled 标志位防止组件卸载后的竞态更新
|
||||
if (!cancelled) setData(res.data ?? []);
|
||||
} finally {
|
||||
@@ -46,19 +54,122 @@ const UserTable = ({ deptKey }: UserTableProps) => {
|
||||
|
||||
load();
|
||||
return () => { cancelled = true; };
|
||||
}, [deptKey]);
|
||||
}, [deptId, tick]);
|
||||
|
||||
// 弹窗状态
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalTitle, setModalTitle] = useState('新增用户');
|
||||
const [editingRecord, setEditingRecord] = useState<UserRecord | undefined>();
|
||||
|
||||
/** 打开新增弹窗 */
|
||||
const handleAdd = () => {
|
||||
setModalTitle('新增用户');
|
||||
setEditingRecord(undefined);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
/** 打开编辑弹窗 */
|
||||
const handleEdit = (record: UserRecord) => {
|
||||
setModalTitle('编辑用户');
|
||||
setEditingRecord(record);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
/** 删除用户 */
|
||||
const handleDelete = async (id: string) => {
|
||||
await delUser(id);
|
||||
message.success('删除成功');
|
||||
refresh();
|
||||
};
|
||||
|
||||
/** Modal 确认,根据是否有 editingRecord 判断新增/编辑 */
|
||||
const handleModalOk = async (values: UserFormValues) => {
|
||||
if (editingRecord) {
|
||||
await editUser(editingRecord.id, { ...values, deptId });
|
||||
message.success('编辑成功');
|
||||
} else {
|
||||
await addUser({ ...values, deptId });
|
||||
message.success('新增成功');
|
||||
}
|
||||
setModalOpen(false);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const columns: ColumnsType<UserRecord> = [
|
||||
{
|
||||
title: '序列',
|
||||
width: 80,
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, _record: UserRecord, index: number) => index + 1,
|
||||
},
|
||||
{ title: '用户名', dataIndex: 'userName', key: 'userName' },
|
||||
{ title: '昵称', dataIndex: 'nickName', key: 'nickName' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={statusColorMap[status] ?? 'default'}>{status}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
render: (_: unknown, record: UserRecord) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除"
|
||||
description={`确定要删除用户「${record.nickName}」吗?`}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Col xs={24} sm={24} md={17} lg={18} xl={19} style={{ display: 'flex' }}>
|
||||
<Card title="用户列表" style={{ width: '100%' }}>
|
||||
<Table
|
||||
<Card
|
||||
title="用户列表"
|
||||
style={{ width: '100%' }}
|
||||
extra={
|
||||
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table<UserRecord>
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="username"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<UserModal
|
||||
open={modalOpen}
|
||||
title={modalTitle}
|
||||
initialValues={editingRecord}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,16 +3,18 @@ import { useState } from 'react';
|
||||
import DeptTree from './DeptTree';
|
||||
import UserTable from './UserTable';
|
||||
|
||||
/** 用户管理主页面:左侧部门树 + 右侧用户表格 */
|
||||
const UserManagement = () => {
|
||||
const [selectedDeptKey, setSelectedDeptKey] = useState('1');
|
||||
// 当前选中的部门 id,由部门树点击触发
|
||||
const [selectedDeptId, setSelectedDeptId] = useState<string>('1');
|
||||
|
||||
return (
|
||||
<Row
|
||||
gutter={[16, 16]}
|
||||
style={{ padding: 24, height: '100%', alignContent: 'flex-start' }}
|
||||
>
|
||||
<DeptTree selectedKey={selectedDeptKey} onSelect={setSelectedDeptKey} />
|
||||
<UserTable deptKey={selectedDeptKey} />
|
||||
<DeptTree selectedId={selectedDeptId} onSelect={setSelectedDeptId} />
|
||||
<UserTable deptId={selectedDeptId} />
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user