diff --git a/AGENTS.md b/AGENTS.md index 0533607..01bd66c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -158,9 +158,15 @@ const res = await get('/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` 或 `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 diff --git a/rsbuild.config.ts b/rsbuild.config.ts index c55b3e1..bb8dab9 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -4,4 +4,7 @@ import { pluginReact } from '@rsbuild/plugin-react'; // Docs: https://rsbuild.rs/config/ export default defineConfig({ plugins: [pluginReact()], + server: { + port: 6000, + }, }); diff --git a/src/api/system/user.ts b/src/api/system/user.ts index b0f70b4..49a82cd 100644 --- a/src/api/system/user.ts +++ b/src/api/system/user.ts @@ -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('/api/system/dept/tree'); /** @@ -35,21 +44,43 @@ export const addDept = (params: DeptParams) => post('/api/system/dept/ /** * 编辑部门 - * @param key 部门 key + * @param id 部门 ID * @param params 部门参数 */ -export const editDept = (key: string, params: DeptParams) => - post('/api/system/dept/edit', { key, ...params }); +export const editDept = (id: string, params: DeptParams) => + post('/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('/api/system/user/list', { deptKey }); +export const listUser = (deptId: string) => + get('/api/system/user/list', { deptId }); + +/** + * 新增用户 + * @param params 用户参数 + */ +export const addUser = (params: UserParams) => post('/api/system/user/add', params); + +/** + * 编辑用户 + * @param id 用户 ID + * @param params 用户参数 + */ +export const editUser = (id: string, params: UserParams) => + post('/api/system/user/edit', { id, ...params }); + +/** + * 删除用户 + * @param id 用户 ID + */ +export const delUser = (id: string) => post('/api/system/user/del', { id }); diff --git a/src/mock/system.ts b/src/mock/system.ts index 6fec522..ee59a8a 100644 --- a/src/mock/system.ts +++ b/src/mock/system.ts @@ -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 = { - '1': [{ username: 'admin', name: '管理员', email: 'admin@example.com', status: '启用' }], +/** 用户列表 mock 数据,按部门 id 分组(运行时可变) */ +const userMap: Record = { + '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; - 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; + 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; - 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; + 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 })); }), ]; diff --git a/src/pages/system/user/DeptTree.tsx b/src/pages/system/user/DeptTree.tsx index fde5711..f132854 100644 --- a/src/pages/system/user/DeptTree.tsx +++ b/src/pages/system/user/DeptTree.tsx @@ -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; + onDelete: (id: string) => Promise; }) => { const [hovered, setHovered] = useState(false); @@ -44,7 +46,7 @@ const DeptTreeNode = ({ size="small" icon={} title="新增子部门" - onClick={() => onAdd(node.key)} + onClick={() => onAdd(node.id)} />