adspecs 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/.adspecs/paths.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codebuddy-plugin/plugin.json +1 -1
  5. package/.qoder-plugin/plugin.json +1 -1
  6. package/.workbuddy-plugin/plugin.json +1 -1
  7. package/CLAUDE.md +2 -2
  8. package/README.md +137 -108
  9. package/package.json +1 -1
  10. package/references/ant6-front-standard/02-/347/273/204/344/273/266/350/247/204/350/214/203.md +7 -0
  11. package/references/ant6-front-standard/03-/345/210/227/350/241/250/350/247/204/350/214/203.md +222 -147
  12. package/references/ant6-front-standard/04-/350/241/250/345/215/225/350/247/204/350/214/203.md +8 -0
  13. package/references/ant6-front-standard/09-/345/270/270/350/247/201/351/227/256/351/242/230/350/247/204/350/214/203.md +1 -1
  14. package/references/ant6-front-standard/index.md +100 -99
  15. package/references/yudaocloud-end-standard/01-Java/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +1166 -0
  16. package/references/yudaocloud-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +1023 -0
  17. package/references/yudaocloud-end-standard/03-/346/225/260/346/215/256/345/255/227/345/205/270/344/270/216/350/217/234/345/215/225/350/247/204/350/214/203.md +336 -0
  18. package/references/yudaocloud-end-standard/index.md +14 -3
  19. package/references/yudaocloud-end-standard/system_dict_type.sql +186 -0
  20. package/skills/adspecs-plan/SKILL.md +1 -1
  21. package/skills/adspecs-utest/SKILL.md +62 -40
  22. package/skills/project-init/SKILL.md +4 -4
  23. package/src/lib/paths-defaults.js +1 -1
  24. package/src/lib/readme-gen.js +1 -1
  25. package/references/ant6-front-standard/05-/345/211/215/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +0 -1443
@@ -1,1443 +0,0 @@
1
- # 前端编码规范
2
-
3
- > **版本**: v3.3
4
- > **创建日期**: 2026-03-17
5
- > **更新日期**: 2026-06-05
6
- > **适用范围**: React 19 + TypeScript 6 + Ant Design 6 + ProComponents 项目
7
- > **文档目的**: 统一前端编码规范,提升代码质量和团队协作效率
8
-
9
- ---
10
-
11
- ## 📋 目录
12
-
13
- 1. [技术栈基线](#技术栈基线)
14
- 2. [目录结构规范](#目录结构规范)
15
- 3. [命名规范](#命名规范)
16
- 4. [编码风格规范](#编码风格规范)
17
- 5. [React 组件规范](#react-组件规范)
18
- 6. [列表页标准模式(ProTable)](#列表页标准模式protable)
19
- 7. [Modal / Drawer 标准模式](#modal--drawer-标准模式)
20
- 8. [TypeScript 规范](#typescript-规范)
21
- 9. [样式规范](#样式规范)
22
- 10. [路由规范](#路由规范)
23
- 11. [状态管理规范](#状态管理规范)
24
- 12. [API 接口规范](#api-接口规范)
25
- 13. [国际化(i18n)规范](#国际化i18n规范)
26
- 14. [字典系统规范](#字典系统规范)
27
- 15. [权限系统规范](#权限系统规范)
28
- 16. [Mock 规范](#mock-规范)
29
- 17. [上传组件规范](#上传组件规范)
30
- 18. [时间格式化规范](#时间格式化规范)
31
- 19. [性能优化规范](#性能优化规范)
32
- 20. [代码质量标准](#代码质量标准)
33
- 21. [常见问题规范(必读陷阱)](#常见问题规范必读陷阱)
34
- 22. [Vue → React 迁移映射表](#vue--react-迁移映射表)
35
-
36
- ---
37
-
38
- ## 一、技术栈基线
39
-
40
- | 维度 | 选型 | 备注 |
41
- | ------ | ----------------------------------------------- | ----------------------------------------------------------------- |
42
- | 框架 | React 19.2 + TypeScript 6 | strict 模式 |
43
- | UI | `antd` ^6.3 + `@ant-design/pro-components` ^3.1 | 列表统一用 `ProTable` |
44
- | 图标 | `@ant-design/icons` | 按需引入;后端图标名通过 `<Icon icon="ep:xxx">` 走 `iconMap` 转换 |
45
- | 路由 | `react-router` ^7 | `createBrowserRouter` + 动态路由 patch |
46
- | 状态 | `zustand` ^5 | `useAuthStore` / `useAppStore` / `useDictStore` |
47
- | 网络 | `axios` ^1.15 | 统一实例 `@/utils/request` |
48
- | Mock | `msw` ^2.13 | 仅 dev,受 `VITE_MOCK_ENABLED` 控制 |
49
- | 国际化 | `react-i18next` ^17 | 默认中文 |
50
- | 构建 | Vite 8 | `npm run build` 必须通过 `tsc -b && vite build` |
51
- | 风格 | Prettier 单引号 / 分号 / 100 列宽 / 2 空格 | ESLint flat config + `react-hooks` |
52
-
53
- > **禁用**:Tailwind、CSS-in-JS 库、TanStack Query、Redux、Class 组件、`element-plus` 残留代码。
54
-
55
- ---
56
-
57
- ## 二、目录结构规范
58
-
59
- ```
60
- src/
61
- ├── api/ # axios 接口模块(按域拆分)
62
- │ ├── ai/ # /ai/* 接口
63
- │ ├── infra/
64
- │ ├── system/
65
- │ └── auth.ts
66
- ├── components/ # 通用组件(首字母大写文件夹 + index.tsx 默认导出)
67
- │ ├── Access/ # 权限包裹
68
- │ ├── DictTag/ # 字典标签
69
- │ ├── Footer/ # 页脚
70
- │ ├── Icon/ # iconify → AntD 图标
71
- │ ├── Message/ # Header 铃铛 + 未读列表
72
- │ ├── UploadFile/ # UploadFile / UploadImg / UploadImgs
73
- │ ├── UserSelectForm/
74
- │ └── DeptSelectForm/
75
- ├── hooks/ # 自定义 hook,use 前缀
76
- ├── layouts/ # 仅一个 AdminLayout
77
- ├── pages/ # 路由对应的页面
78
- │ ├── ai/ # 与后端菜单 path 完全对齐
79
- │ ├── infra/
80
- │ ├── system/
81
- │ ├── profile/
82
- │ └── login/
83
- ├── mocks/ # MSW
84
- │ ├── data/index.ts # 所有 mock 数据 + 字典 + 菜单
85
- │ └── handlers/ # 按模块拆 ai.ts / infra.ts / notify.ts ...
86
- ├── router/ # createBrowserRouter + dynamicRoutes
87
- ├── stores/ # zustand store
88
- ├── types/ # 跨模块共享类型
89
- └── utils/ # request / formatTime / token / menuConverter ...
90
- ```
91
-
92
- **关键约定**:
93
-
94
- 1. **页面路径必须等于后端菜单 `component` 字段**。例如后端返回 `ai/chat/index/index` → 文件必须落在 `src/pages/ai/chat/index/index.tsx`,否则 `dynamicRoutes` 会报 `Component not found` 警告并显示 404。
95
- 2. 子组件统一放在该页面同级 `components/` 下:`pages/<feature>/components/XxxModal.tsx`。
96
- 3. 通用业务组件命名遵循 `components/<PascalCase>/index.tsx`。
97
-
98
- ---
99
-
100
- ## 三、命名规范
101
-
102
- | 对象 | 规则 | 示例 |
103
- | ------------- | ------------------------------- | --------------------------------------- |
104
- | 组件文件 | PascalCase | `DeptModal.tsx` / `index.tsx` |
105
- | 页面目录 | kebab-case 或保留后端 path | `chat-role/`、`data-source-config/` |
106
- | Hook | `useXxx.ts` | `useAccess.ts` |
107
- | 工具/常量文件 | camelCase | `formatTime.ts`、`token.ts` |
108
- | 类型 | `XxxVO`/`XxxParams`/`SystemXxx` | `ChatMessageVO` |
109
- | 常量 | UPPER_SNAKE | `MAX_AVATAR_MB` |
110
- | API 函数 | 动词 + 资源 | `getDeptList`、`updateDataSourceConfig` |
111
- | 普通变量 | camelCase | `userName` |
112
- | 布尔变量 | is/has/can/should 开头 | `isValid`, `hasPermission` |
113
-
114
- ---
115
-
116
- ## 四、编码风格规范
117
-
118
- ### 4.1 代码格式化
119
-
120
- - 使用 2 个空格缩进
121
- - 优先使用单引号,如 `'string'`
122
- - JSX 属性使用双引号,如 `<div className="container">`
123
- - 模板字符串使用反引号
124
- - 使用分号
125
- - 函数调用和对象字面量末尾使用尾随逗号
126
- - 每行最多 100 个字符
127
-
128
- ### 4.2 注释规范
129
-
130
- ```typescript
131
- /**
132
- * 获取用户列表
133
- * @param params - 查询参数
134
- * @param params.pageNum - 页码
135
- * @param params.pageSize - 每页数量
136
- */
137
- async function getUserList(params: UserPageParams): Promise<User[]> {
138
- // 关键业务逻辑必须添加注释
139
- }
140
- ```
141
-
142
- - 关键业务逻辑必须添加注释
143
- - 复杂算法必须添加说明
144
- - 临时解决方案需要标记 `// TODO: 说明`
145
-
146
- ---
147
-
148
- ## 五、React 组件规范
149
-
150
- ### 5.1 函数组件结构
151
-
152
- ```tsx
153
- // 1. 导入语句(按类型分组:React -> 第三方库 -> 本地模块 -> 样式)
154
- import { useState, useEffect, useCallback, useRef } from 'react';
155
- import { Form, Table, Button, Modal, Space } from 'antd';
156
- import { PlusOutlined } from '@ant-design/icons';
157
- import { ProTable, type ProColumns, type ActionType } from '@ant-design/pro-components';
158
- import { getUserList, deleteUser } from '@/api/system/user';
159
- import type { User } from '@/types/system/user';
160
- import Access from '@/components/Access';
161
-
162
- // 2. 类型定义(复杂类型建议放在 src/types/ 下)
163
- interface UserListProps {
164
- departmentId?: number;
165
- }
166
-
167
- // 3. 组件定义(使用 function 声明)
168
- export default function UserList({ departmentId }: UserListProps) {
169
- // 4. Hooks 调用(按顺序:ref/state -> custom hooks -> effects)
170
- const { message, modal } = App.useApp();
171
- const actionRef = useRef<ActionType>(undefined);
172
- const [loading, setLoading] = useState(false);
173
- const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
174
-
175
- // 5. 事件处理函数
176
- const handleDelete = useCallback(async (record: User) => {
177
- modal.confirm({
178
- title: '确认删除?',
179
- okType: 'danger',
180
- content: `确认删除「${record.name}」吗?`,
181
- onOk: async () => {
182
- await deleteUser(record.id!);
183
- message.success('删除成功');
184
- actionRef.current?.reload();
185
- },
186
- });
187
- }, [message, modal]);
188
-
189
- // 6. 列定义
190
- const columns: ProColumns<User>[] = [
191
- { title: '名称', dataIndex: 'name', width: 200 },
192
- { title: '操作', key: 'action', search: false, render: (_, record) => (
193
- <Space>
194
- <Access code="system:user:update">
195
- <a onClick={() => handleEdit(record)}>编辑</a>
196
- </Access>
197
- <Access code="system:user:delete">
198
- <a style={{ color: '#ff4d4f' }} onClick={() => handleDelete(record)}>删除</a>
199
- </Access>
200
- </Space>
201
- )},
202
- ];
203
-
204
- // 7. 渲染
205
- return (
206
- <ProTable<User>
207
- columns={columns}
208
- actionRef={actionRef}
209
- rowKey="id"
210
- request={async (params) => {
211
- const { current, pageSize, ...rest } = params;
212
- const { data } = await getUserList({ pageNo: current, pageSize, ...rest });
213
- return { data: data.list, total: data.total, success: true };
214
- }}
215
- toolBarRender={() => [
216
- <Access code="system:user:create" key="add">
217
- <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
218
- </Access>,
219
- ]}
220
- />
221
- );
222
- }
223
- ```
224
-
225
- ### 5.2 Props 定义
226
-
227
- ```typescript
228
- // 使用 interface 定义 props
229
- interface UserCardProps {
230
- user: User; // 必填
231
- showActions?: boolean; // 可选
232
- size?: 'small' | 'medium' | 'large'; // 带默认值
233
- onDelete: (id: number) => void; // 回调
234
- children?: React.ReactNode; // 子组件
235
- }
236
-
237
- // 使用 destructuring + 默认值
238
- export default function UserCard({
239
- user,
240
- showActions = false,
241
- size = 'medium',
242
- onDelete,
243
- children,
244
- }: UserCardProps) {
245
- return <div>{/* ... */}</div>;
246
- }
247
- ```
248
-
249
- ### 5.3 组件拆分原则
250
-
251
- - 单个组件不超过 300 行,超出按职责拆分
252
- - 页面内的子组件(Modal、Drawer 等)放在 `src/pages/<feature>/components/` 目录
253
- - 全局复用组件放在 `src/components/`
254
- - 避免在组件内部定义类型,应放在 `src/types/` 目录
255
-
256
- ### 5.4 自定义 Hook 规范
257
-
258
- ```typescript
259
- // hooks/useAccess.ts — 以 use 开头
260
- import { useAuthStore } from '@/stores/useAuthStore';
261
-
262
- export function useAccess(code: string): boolean {
263
- const hasPermission = useAuthStore((s) => s.hasPermission);
264
- return hasPermission(code);
265
- }
266
- ```
267
-
268
- ---
269
-
270
- ## 六、列表页标准模式(ProTable)
271
-
272
- > 所有「带筛选 + 分页」的列表都**必须**使用 `ProTable`,**不要再用** `Card + Form + Table`。
273
- > 树形列表(dept/menu)例外,用本地 `dataSource` + `pagination={false}` + `expandable`。
274
-
275
- ### 6.1 标准模板
276
-
277
- ```tsx
278
- import { useRef, useState } from 'react';
279
- import { App, Button, Space } from 'antd';
280
- import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
281
- import { ProTable, type ProColumns, type ActionType } from '@ant-design/pro-components';
282
- import { getXxxPage, deleteXxx, deleteXxxList, type XxxVO } from '@/api/xxx';
283
- import Access from '@/components/Access';
284
- import DictTag from '@/components/DictTag';
285
- import { dateFormatter } from '@/utils/formatTime';
286
- import { useDictStore } from '@/stores/useDictStore';
287
- import XxxModal from './components/XxxModal';
288
-
289
- const XxxPage: React.FC = () => {
290
- const { message, modal } = App.useApp();
291
- const actionRef = useRef<ActionType>(undefined);
292
- const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
293
- const [modalOpen, setModalOpen] = useState(false);
294
- const [editId, setEditId] = useState<number | null>(null);
295
- const getDictOptions = useDictStore((s) => s.getDictOptions);
296
-
297
- const reload = () => actionRef.current?.reload();
298
-
299
- const handleDelete = (record: XxxVO) => {
300
- modal.confirm({
301
- title: '确认删除?', okType: 'danger',
302
- content: `确认删除「${record.name}」吗?`,
303
- onOk: async () => {
304
- await deleteXxx(record.id!);
305
- message.success('删除成功'); reload();
306
- },
307
- });
308
- };
309
-
310
- const handleDeleteBatch = () => {
311
- modal.confirm({
312
- title: '确认批量删除?', okType: 'danger',
313
- content: `确认删除选中的 ${selectedRowKeys.length} 项?`,
314
- onOk: async () => {
315
- await deleteXxxList(selectedRowKeys.map(Number));
316
- message.success('删除成功');
317
- setSelectedRowKeys([]);
318
- reload();
319
- },
320
- });
321
- };
322
-
323
- const columns: ProColumns<XxxVO>[] = [
324
- { title: '名称', dataIndex: 'name', width: 200 },
325
- {
326
- title: '状态', dataIndex: 'status', width: 100,
327
- valueType: 'select',
328
- valueEnum: Object.fromEntries(
329
- getDictOptions('common_status').map((d) => [d.value, { text: d.label }]),
330
- ),
331
- render: (_, r) => <DictTag type="common_status" value={r.status} />,
332
- },
333
- {
334
- title: '创建时间', dataIndex: 'createTime', width: 180,
335
- valueType: 'dateRange',
336
- search: {
337
- transform: (value) => {
338
- if (!Array.isArray(value)) return {};
339
- return { createTimeStart: value[0], createTimeEnd: value[1] };
340
- },
341
- },
342
- render: (_, r) => dateFormatter(r.createTime) || '-',
343
- },
344
- {
345
- title: '操作', key: 'action', width: 140, fixed: 'right', search: false,
346
- render: (_, record) => (
347
- <Space>
348
- <Access code="xxx:update">
349
- <a onClick={() => { setEditId(record.id!); setModalOpen(true); }}>编辑</a>
350
- </Access>
351
- <Access code="xxx:delete">
352
- <a style={{ color: '#ff4d4f' }} onClick={() => handleDelete(record)}>删除</a>
353
- </Access>
354
- </Space>
355
- ),
356
- },
357
- ];
358
-
359
- return (
360
- <>
361
- <ProTable<XxxVO>
362
- columns={columns}
363
- actionRef={actionRef}
364
- rowKey="id"
365
- scroll={{ x: 1200 }}
366
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
367
- request={async (params: any) => {
368
- const { current, pageSize, ...rest } = params;
369
- const { data } = await getXxxPage({ pageNo: current ?? 1, pageSize: pageSize ?? 10, ...rest });
370
- return { data: data.list, total: data.total, success: true };
371
- }}
372
- pagination={{ defaultPageSize: 10, showSizeChanger: true }}
373
- rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
374
- toolBarRender={() => [
375
- <Access code="xxx:create" key="add">
376
- <Button type="primary" icon={<PlusOutlined />}
377
- onClick={() => { setEditId(null); setModalOpen(true); }}>新增</Button>
378
- </Access>,
379
- <Access code="xxx:delete" key="batch">
380
- <Button danger icon={<DeleteOutlined />}
381
- disabled={selectedRowKeys.length === 0}
382
- onClick={handleDeleteBatch}>批量删除</Button>
383
- </Access>,
384
- ]}
385
- />
386
- <XxxModal open={modalOpen} id={editId}
387
- onSuccess={() => { setModalOpen(false); reload(); }}
388
- onCancel={() => setModalOpen(false)} />
389
- </>
390
- );
391
- };
392
-
393
- export default XxxPage;
394
- ```
395
-
396
- ### 6.2 ProTable 列定义关键规则
397
-
398
- | 场景 | 写法 |
399
- | ------------- | ------------------------------------------------------------------------------------------ |
400
- | 隐藏搜索 | `search: false`(**不是** `hideInSearch`) |
401
- | 下拉搜索 | `valueType: 'select'` + `valueEnum: Object.fromEntries(getDictOptions(...).map(...))` |
402
- | 日期区间 | `valueType: 'dateRange'` + `search.transform` 拆 `xxxStart/xxxEnd` |
403
- | 日期区间渲染 | **必须用 `render`,不要用 `renderText`** |
404
- | 长 label 搜索 | `search={{ labelWidth: 'auto' }}`(在 ProTable 上) |
405
- | request 参数 | 固定加 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 允许 `params: any` |
406
- | 树形表 | 去掉 `request`,改用 `dataSource={tree}` + `pagination={false}` + `expandable` |
407
-
408
- ---
409
-
410
- ## 七、Modal / Drawer 使用标准规范
411
-
412
- ### 7.1 整体布局原则
413
-
414
- 1、表单较简单(字段 <4个),推荐使用 Modal。
415
-
416
- 2、表单较复杂(字段 ≥ 4 个)或业务流程需要连续操作时,推荐使用右侧 Drawer 替代 Modal。
417
-
418
- > 根据表单字段数量分级选择布局和 Drawer 宽度。
419
-
420
- | 字段数量 | 布局 | Drawer 宽度 | Form layout | 说明 |
421
- | ---------- | ------------------------------ | ----------- | ------------ | -------------------------- |
422
- | 4 ~ 10 个 | **一栏**,单列 | **15%** | `vertical` | 字段少、简单,垂直单栏即可 |
423
- | 10 ~ 20 个 | **两栏** `Row`+`Col span={12}` | **30%** | `horizontal` | 字段较多,两栏紧凑排列 |
424
- | 21 ~ 30 个 | **三栏** `Row`+`Col span={8}` | **45%** | `horizontal` | 字段很多,三栏并排 |
425
- | > 30 个 | **四栏** `Row`+`Col span={6}` | **60%** | `horizontal` | 大批量字段,四栏最大化空间 |
426
-
427
- ### 7.2 Modal 标准模板
428
-
429
- > 表单较简单(字段 <4个),推荐使用 Modal。
430
-
431
- ```tsx
432
- interface Props {
433
- open: boolean;
434
- id: number | null;
435
- onSuccess: () => void;
436
- onCancel: () => void;
437
- }
438
-
439
- const DEFAULT_FORM = { name: '', status: 0 };
440
-
441
- const XxxModal: React.FC<Props> = ({ open, id, onSuccess, onCancel }) => {
442
- const { message } = App.useApp();
443
- const [form] = Form.useForm();
444
- const [confirmLoading, setConfirmLoading] = useState(false);
445
- // 保留服务端原始字段,避免编辑时丢掉未在表单里出现的字段
446
- const originData = useRef<Partial<XxxVO>>({});
447
-
448
- useEffect(() => {
449
- if (!open) return;
450
- form.resetFields();
451
- originData.current = {};
452
- if (id != null) {
453
- getXxx(id).then(({ data }) => {
454
- originData.current = { ...data };
455
- form.setFieldsValue(data);
456
- });
457
- } else {
458
- form.setFieldsValue(DEFAULT_FORM);
459
- }
460
- }, [open, id, form]);
461
-
462
- const handleOk = async () => {
463
- let values: XxxVO;
464
- try { values = await form.validateFields(); } catch { return; }
465
- setConfirmLoading(true);
466
- try {
467
- const payload = { ...originData.current, ...values, ...(id != null ? { id } : {}) } as XxxVO;
468
- if (id != null) {
469
- await updateXxx(payload); message.success('修改成功');
470
- } else {
471
- await createXxx(payload); message.success('新增成功');
472
- }
473
- onSuccess();
474
- } catch (err) {
475
- message.error((err as Error).message || '操作失败');
476
- } finally {
477
- setConfirmLoading(false);
478
- }
479
- };
480
-
481
- return (
482
- <Modal title={id != null ? '编辑' : '新增'} open={open}
483
- confirmLoading={confirmLoading}
484
- onOk={handleOk} onCancel={onCancel}
485
- width={580} destroyOnHidden>
486
- <Form form={form} initialValues={DEFAULT_FORM}>
487
- {/* fields */}
488
- </Form>
489
- </Modal>
490
- );
491
- };
492
- ```
493
-
494
- ### 7.3 Drawer 标准模板
495
-
496
- > 表单较复杂(字段 ≥ 4 个)或业务流程需要连续操作时,推荐使用右侧 Drawer 替代 Modal。
497
-
498
- ```tsx
499
- import { Drawer, Form, Input, Select, InputNumber, DatePicker, Button, Space, Row, Col, message } from 'antd';
500
-
501
- interface Props {
502
- open: boolean;
503
- id: number | null;
504
- onSuccess: () => void;
505
- onClose: () => void; // ← Drawer 用 onClose,不用 onCancel
506
- }
507
-
508
- const XxxDrawer: React.FC<Props> = ({ open, id, onSuccess, onClose }) => {
509
- const [form] = Form.useForm();
510
- const isEdit = id != null;
511
-
512
- useEffect(() => {
513
- if (open && id) {
514
- getXxx(id).then(({ data }) => {
515
- form.setFieldsValue({ ...data, dateField: dayjs(data.dateField) });
516
- });
517
- } else {
518
- form.resetFields();
519
- }
520
- }, [open, id, form]);
521
-
522
- const handleOk = async () => {
523
- try {
524
- const values = await form.validateFields();
525
- const params = { ...values, id: id || undefined };
526
- if (isEdit) {
527
- await updateXxx(params);
528
- } else {
529
- await createXxx(params);
530
- }
531
- message.success('操作成功');
532
- onSuccess();
533
- } catch (error) {
534
- // Validation failed
535
- }
536
- };
537
-
538
- return (
539
- <Drawer
540
- title={isEdit ? '编辑' : '新增'}
541
- open={open}
542
- onClose={onClose}
543
- destroyOnHidden={true}
544
- size={600}
545
- closable={{ placement: 'end' }}
546
- mask={{ closable: false }}
547
- styles={{ body: { background: '#f5f5f5' } }}
548
- footer={
549
- <Space style={{ float: 'right' }}>
550
- <Button onClick={onClose}>取消</Button>
551
- <Button type="primary" onClick={handleOk}>确定</Button>
552
- </Space>
553
- }
554
- >
555
- <Card style={{ border: 'none' }}>
556
- <Form form={form} layout="vertical">
557
- {/* fields — 根据字段数量选择布局,见 7.1 整体布局原则 */}
558
- </Form>
559
- </Card>
560
- </Drawer>
561
- );
562
- };
563
- ```
564
-
565
-
566
- ### 7.4 表单一栏布局
567
-
568
- > 字段 4~10 个时使用,Drawer 宽度 256,`layout="vertical"` 垂直单列排列。
569
-
570
- ```tsx
571
- // 一栏布局:垂直单列,layout="vertical",无需 Row/Col
572
- <Form form={form} layout="vertical">
573
- <Form.Item name="fieldA" label="字段A" rules={[{ required: true }]}>
574
- <Input />
575
- </Form.Item>
576
- <Form.Item name="fieldB" label="字段B" rules={[{ required: true }]}>
577
- <Select options={[...]} />
578
- </Form.Item>
579
- <Form.Item name="fieldC" label="字段C" rules={[{ required: true }]}>
580
- <InputNumber style={{ width: '100%' }} />
581
- </Form.Item>
582
- <Form.Item name="fieldD" label="字段D" rules={[{ required: true }]}>
583
- <DatePicker style={{ width: '100%' }} />
584
- </Form.Item>
585
- {/* 继续追加字段即可,无需 Row/Col 包裹 */}
586
- </Form>
587
- ```
588
-
589
- ### 7.5 表单两栏布局
590
-
591
- > 字段 10~20 个时使用,每行两个 `Col span={12}`,`gutter={16}` 控制列间距。
592
-
593
- ```tsx
594
- // 两栏布局:每行两个 Col span={12},gutter={16} 控制列间距
595
- <Form form={form} layout="vertical">
596
- <Row gutter={16}>
597
- <Col span={12}>
598
- <Form.Item name="fieldA" label="字段A" rules={[{ required: true }]}>
599
- <Input />
600
- </Form.Item>
601
- </Col>
602
- <Col span={12}>
603
- <Form.Item name="fieldB" label="字段B" rules={[{ required: true }]}>
604
- <Select options={[...]} />
605
- </Form.Item>
606
- </Col>
607
- </Row>
608
- <Row gutter={16}>
609
- <Col span={12}>
610
- <Form.Item name="fieldC" label="字段C" rules={[{ required: true }]}>
611
- <InputNumber style={{ width: '100%' }} />
612
- </Form.Item>
613
- </Col>
614
- <Col span={12}>
615
- <Form.Item name="fieldD" label="字段D" rules={[{ required: true }]}>
616
- <DatePicker style={{ width: '100%' }} />
617
- </Form.Item>
618
- </Col>
619
- </Row>
620
- {/* 奇数个字段时,最后一行可只放一个 Col span={12} */}
621
- <Row gutter={16}>
622
- <Col span={12}>
623
- <Form.Item name="fieldE" label="字段E" rules={[{ required: true }]}>
624
- <InputNumber style={{ width: '100%' }} />
625
- </Form.Item>
626
- </Col>
627
- </Row>
628
- </Form>
629
- ```
630
-
631
- **关键规则**:
632
- - 一栏:`layout="vertical"`,无需 `labelCol`/`wrapperCol`,直接排列 `Form.Item`
633
- - 两栏及以上:Form 设置 `layout="horizontal"` + `labelCol={{ span: 8 }}`,每行 `Row gutter={16}`
634
- - 每个 `Col span={12}` 放一个 `Form.Item`,输入组件统一 `style={{ width: '100%' }}` 撑满
635
- - Label 较短的字段可适当调小 `labelCol` 和 `wrapperCol` 比例
636
- - 奇数字段最后一行放剩余 `Col` 即可(不足不补)
637
-
638
- ### 7.6 表单三栏布局
639
-
640
- > 字段在 20~30 个之间时,使用三栏布局,每行三个 `Col span={8}`。
641
-
642
- ```tsx
643
- // 三栏布局:每行三个 Col span={8},gutter={16}
644
- <Form form={form} layout="vertical" >
645
- <Row gutter={16}>
646
- <Col span={8}>
647
- <Form.Item name="fieldA" label="字段A" rules={[{ required: true }]}>
648
- <Input />
649
- </Form.Item>
650
- </Col>
651
- <Col span={8}>
652
- <Form.Item name="fieldB" label="字段B" rules={[{ required: true }]}>
653
- <Select options={[...]} />
654
- </Form.Item>
655
- </Col>
656
- <Col span={8}>
657
- <Form.Item name="fieldC" label="字段C" rules={[{ required: true }]}>
658
- <InputNumber style={{ width: '100%' }} />
659
- </Form.Item>
660
- </Col>
661
- </Row>
662
- <Row gutter={16}>
663
- <Col span={8}>
664
- <Form.Item name="fieldD" label="字段D" rules={[{ required: true }]}>
665
- <DatePicker style={{ width: '100%' }} />
666
- </Form.Item>
667
- </Col>
668
- {/* 尾部不足 3 个时按实际数量放即可,无需补空白 Col */}
669
- </Row>
670
- </Form>
671
- ```
672
-
673
- ### 7.7 表单四栏布局
674
-
675
- > 字段超过 30 个时,使用四栏布局,每行四个 `Col span={6}`,Drawer 宽度 1024。
676
-
677
- ```tsx
678
- // 四栏布局:每行四个 Col span={6}
679
- <Form form={form} layout="vertical">
680
- <Row gutter={16}>
681
- <Col span={6}>
682
- <Form.Item name="fieldA" label="字段A" rules={[{ required: true }]}>
683
- <Input />
684
- </Form.Item>
685
- </Col>
686
- <Col span={6}>
687
- <Form.Item name="fieldB" label="字段B" rules={[{ required: true }]}>
688
- <Select options={[...]} />
689
- </Form.Item>
690
- </Col>
691
- <Col span={6}>
692
- <Form.Item name="fieldC" label="字段C" rules={[{ required: true }]}>
693
- <InputNumber style={{ width: '100%' }} />
694
- </Form.Item>
695
- </Col>
696
- <Col span={6}>
697
- <Form.Item name="fieldD" label="字段D" rules={[{ required: true }]}>
698
- <DatePicker style={{ width: '100%' }} />
699
- </Form.Item>
700
- </Col>
701
- </Row>
702
- </Form>
703
- ```
704
-
705
- ### 7.8 约定
706
-
707
- - Props 固定四件套:`open`, `id`, `onSuccess`, `onCancel`(Modal)/ `onClose`(Drawer),**不要**再用 `defineExpose / ref.open()`
708
- - Drawer 标题统一「新增 / 编辑」,Modal 同理
709
- - `originData = useRef()` 保留服务端字段,提交时合并:`{ ...originData.current, ...values, id }`
710
- - 编辑模式下隐藏字段用 `hidden={!!id}` 而非条件渲染(条件渲染会让 `validateFields` 跳过该字段)
711
- - 用 `destroyOnHidden` 自动清理状态,**不要**手动 reset
712
-
713
- ---
714
-
715
- ## 八、TypeScript 规范
716
-
717
- ### 8.1 类型定义
718
-
719
- ```typescript
720
- // 优先使用 interface(可扩展),联合类型 / 工具类型使用 type
721
- interface User {
722
- id: number;
723
- name: string;
724
- email: string;
725
- active: boolean;
726
- }
727
-
728
- // 联合类型
729
- type Status = 'success' | 'warning' | 'danger';
730
-
731
- // API 响应类型(来自 @/types/api,不要重写)
732
- interface ApiResponse<T> {
733
- code: number;
734
- message: string;
735
- data: T;
736
- }
737
-
738
- interface PageResult<T> {
739
- total: number;
740
- rows: T[];
741
- }
742
- ```
743
-
744
- ### 8.2 类型规则
745
-
746
- - **禁用 `any`**;必须时单行 `// eslint-disable-next-line @typescript-eslint/no-explicit-any`
747
- - API 入参 / 出参 / store 必须显式类型
748
- - Hook 暴露的 ref 类型用 `forwardRef<Handle, Props>` + `useImperativeHandle`
749
- - 不要写 `as any`;要么写正确类型,要么 `as unknown as Xxx`
750
- - 类型文件放在 `src/types/`,按领域组织
751
-
752
- ---
753
-
754
- ## 九、样式规范
755
-
756
- ### 9.1 CSS 文件
757
-
758
- - 使用独立的 `.css` 文件,搭配功能名前缀类名(如 `.user-list`)
759
- - 类名使用 kebab-case
760
- - 避免使用缩写,如 `.btn` → `.button`
761
-
762
- ### 9.2 Ant Design 样式定制
763
-
764
- - 优先使用 Ant Design 组件的内置属性(`size`, `type`, `variant`)
765
- - 全局主题定制通过 `ConfigProvider` 的 `theme` 属性
766
- - 局部覆盖样式使用 CSS 类 + 选择器,避免 `!important`
767
- - **自定义 SCSS 变量覆盖 → 改用 `ConfigProvider` + `theme.token`**
768
-
769
- ### 9.3 响应式设计
770
-
771
- - 使用 Ant Design 的 `Grid` 组件(`Row` / `Col`)进行响应式布局
772
- - 断点参考 Ant Design 预设:`xs < 576 < sm < 768 < md < 992 < lg < 1200 < xl`
773
- - **左右分栏用原生 `display: flex`**,不要用 `Row/Col`(`Row` 默认 `flex-wrap: wrap` 会被换行)
774
-
775
- ---
776
-
777
- ## 十、路由规范
778
-
779
- ### 10.1 路由定义
780
-
781
- ```typescript
782
- import { createBrowserRouter } from 'react-router';
783
-
784
- const router = createBrowserRouter([
785
- { path: '/login', Component: LoginPage },
786
- {
787
- path: '/',
788
- Component: AdminLayout,
789
- children: [
790
- { path: 'dashboard', Component: DashboardPage },
791
- { path: 'profile', Component: ProfilePage },
792
- { path: '*', Component: NotFoundPage },
793
- ],
794
- },
795
- ]);
796
- ```
797
-
798
- ### 10.2 动态路由注入
799
-
800
- - 登录成功后通过 `addDynamicRoutes(menus)` 动态注入路由
801
- - 使用 `import.meta.glob('@/pages/**/index.{tsx,jsx}')` 实现懒加载
802
- - 配合 `Suspense` + `Spin` 提供加载态反馈
803
- - 退出登录时调用 `resetDynamicRoutes()` 清除动态路由
804
- - **外部链接(http/https 路径)在路由生成时被过滤**
805
-
806
- ### 10.3 路由导航
807
-
808
- ```typescript
809
- import { useNavigate, useSearchParams, useParams } from 'react-router';
810
-
811
- const navigate = useNavigate();
812
- const [searchParams] = useSearchParams();
813
- const { id } = useParams();
814
-
815
- navigate('/user/list');
816
- navigate(`/user/detail/${id}`);
817
- navigate(-1); // 返回上一页
818
- ```
819
-
820
- ### 10.4 路由守卫
821
-
822
- - 使用 `<AuthGuard>` 组件保护受保护的路由
823
- - AuthGuard 检查 token 和 userInfo,未登录则重定向至 `/login`
824
- - 携带 `redirect` 参数以便登录后自动跳转回原页面
825
- - AuthGuard 使用 `useRef` 守卫防止重复 fetch
826
-
827
- ---
828
-
829
- ## 十一、状态管理规范
830
-
831
- ### 11.1 Zustand Store 结构
832
-
833
- ```typescript
834
- // stores/useAuthStore.ts
835
- import { create } from 'zustand';
836
-
837
- interface AuthState {
838
- token: string | null;
839
- userInfo: UserInfo | null;
840
- roles: string[];
841
- permissions: string[];
842
- menus: Permission[];
843
- setToken: (token: string) => void;
844
- login: (username: string, password: string) => Promise<void>;
845
- logout: () => void;
846
- hasPermission: (code: string) => boolean;
847
- }
848
-
849
- export const useAuthStore = create<AuthState>((set, get) => ({
850
- token: null,
851
- userInfo: null,
852
- roles: [],
853
- permissions: [],
854
- menus: [],
855
- setToken: (token) => set({ token }),
856
- login: async (username, password) => {
857
- const res = await login({ username, password });
858
- set({ token: res.data.accessToken });
859
- },
860
- logout: () => set({ token: null, userInfo: null, roles: [], permissions: [], menus: [] }),
861
- hasPermission: (code) => {
862
- const { permissions } = get();
863
- return permissions.includes('*:*:*') || permissions.includes(code);
864
- },
865
- }));
866
- ```
867
-
868
- ### 11.2 Store 使用规范
869
-
870
- ```typescript
871
- import { useAuthStore } from '@/stores/useAuthStore';
872
-
873
- export default function UserList() {
874
- // 选择器模式避免不必要的重渲染
875
- const { token, hasPermission } = useAuthStore();
876
- const userName = useAuthStore((state) => state.userInfo?.name);
877
- const canDelete = hasPermission('system:user:delete');
878
-
879
- return <div>{userName}</div>;
880
- }
881
- ```
882
-
883
- ### 11.3 Store 分类
884
-
885
- | Store | 职责 | 持久化 |
886
- | -------------- | --------------------------- | ----------------------------------- |
887
- | `useAuthStore` | token、用户信息、权限、菜单 | 否(页面刷新清空) |
888
- | `useAppStore` | 侧边栏折叠状态、暗色模式 | 是(localStorage: `admin-pro-app`) |
889
- | `useDictStore` | 字典类型到数据的映射 | 否 |
890
-
891
- ---
892
-
893
- ## 十二、API 接口规范
894
-
895
- ### 12.1 文件组织
896
-
897
- ```typescript
898
- // api/system/user.ts
899
- import request from '@/utils/request';
900
- import type { ApiResponse, PageResult, PageParams } from '@/types/api';
901
-
902
- export interface UserVO {
903
- id?: number;
904
- name: string;
905
- createTime?: string;
906
- }
907
-
908
- export const getUserPage = (params: PageParams) =>
909
- request.get<unknown, ApiResponse<PageResult<UserVO>>>('/system/user/page', { params });
910
-
911
- export const getUser = (id: number) =>
912
- request.get<unknown, ApiResponse<UserVO>>(`/system/user/get?id=${id}`);
913
-
914
- export const createUser = (data: UserVO) =>
915
- request.post<unknown, ApiResponse<void>>('/system/user/create', data);
916
-
917
- export const updateUser = (data: UserVO) =>
918
- request.put<unknown, ApiResponse<void>>('/system/user/update', data);
919
-
920
- export const deleteUser = (id: number) =>
921
- request.delete<unknown, ApiResponse<void>>(`/system/user/delete?id=${id}`);
922
-
923
- export const deleteUserList = (ids: number[]) =>
924
- request.delete<unknown, ApiResponse<void>>(`/system/user/delete-list?ids=${ids.join(',')}`);
925
- ```
926
-
927
- ### 12.2 约定
928
-
929
- - `ApiResponse<T>`、`PageParams`、`PageResult<T>` 来自 `@/types/api`,**不要重写**
930
- - `code === 0` 时拦截器自动解包;`code === 401` 自动跳登录;其余 `Promise.reject` 并 `message.error`
931
- - 文件下载用 `download(url, params)`(`@/utils/request` 导出,自动 blob + Content-Disposition 解析)
932
- - Axios 实例超时时间 30s
933
- - 请求拦截器自动附加 `Bearer` token 和 `tenant-id` 头
934
- - **Token 刷新**:并发 401 请求排队,刷新 token 后重放
935
-
936
- ### 12.3 响应码
937
-
938
- | code | 含义 |
939
- | ----- | --------------------------------- |
940
- | `0` | 成功 |
941
- | `401` | 未授权(自动刷新 token 或跳登录) |
942
- | 其他 | 失败,`message` 为错误提示 |
943
-
944
- ---
945
-
946
- ## 十三、国际化(i18n)规范
947
-
948
- 项目使用 `react-i18next` + `i18next-resources-to-backend` 实现多语言支持,通过 Vite 的 `import.meta.glob` 自动加载所有 locale 文件,**无需手动注册**。
949
-
950
- ### 13.1 语言包目录结构
951
-
952
- ```
953
- src/locales/
954
- ├── zh-CN/ # 简体中文(默认语言)
955
- │ ├── system/
956
- │ │ ├── post.ts
957
- │ │ └── user.ts
958
- │ ├── mdm/
959
- │ │ ├── codeRule.ts
960
- │ │ └── codeMapping.ts
961
- │ ├── login.ts
962
- │ ├── common.ts
963
- │ └── ...
964
- ├── en-US/ # 英文
965
- │ ├── system/
966
- │ ├── mdm/
967
- │ └── ...
968
- ├── ja-JP/ # 日语
969
- ├── fr-FR/ # 法语
970
- ├── es-ES/ # 西班牙语
971
- ├── ar-SA/ # 阿拉伯语
972
- ├── ru-RU/ # 俄语
973
- └── de-DE/ # 德语
974
- ```
975
-
976
- ### 13.2 命名空间(Namespace)
977
-
978
- - 每个 locale 文件的路径即为命名空间名。例如 `src/locales/zh-CN/mdm/codeRule.ts` 对应 `useTranslation('mdm/codeRule')`。
979
- - **命名空间路径规则**:按模块目录组织,文件名与业务模块对应。
980
- - 通用文案放在 `common.ts`(`common` 命名空间),页面/模块级文案放在对应子目录。
981
-
982
- ### 13.3 语言包文件格式
983
-
984
- ```typescript
985
- // src/locales/zh-CN/mdm/codeRule.ts
986
- export default {
987
- table: {
988
- id: '编号',
989
- ruleName: '规则名称',
990
- createTime: '创建时间',
991
- action: '操作',
992
- },
993
- form: {
994
- ruleName: '规则名称',
995
- inputRuleName: '请输入规则名称',
996
- editTitle: '编辑编码规则',
997
- addTitle: '新增编码规则',
998
- },
999
- btn: {
1000
- add: '新增',
1001
- edit: '编辑',
1002
- delete: '删除',
1003
- },
1004
- msg: {
1005
- deleteSuccess: '删除成功',
1006
- updateSuccess: '更新成功',
1007
- createSuccess: '创建成功',
1008
- deleteConfirm: '确认删除该编码规则?',
1009
- },
1010
- };
1011
- ```
1012
-
1013
- **结构约定**:
1014
- - 统一使用嵌套对象,**不导出具名变量**。
1015
- - 按 UI 区域分组:`table`(列标题)、`form`(表单标签/占位符/弹窗标题)、`btn`(按钮文案)、`msg`(提示/确认消息)。
1016
- - key 用 camelCase,不使用中文拼音或缩写。
1017
-
1018
- ### 13.4 组件中使用
1019
-
1020
- ```tsx
1021
- import { useTranslation } from 'react-i18next';
1022
-
1023
- const { t } = useTranslation('mdm/codeRule');
1024
-
1025
- // 表格列
1026
- { title: t('table.ruleName'), dataIndex: 'ruleName' }
1027
-
1028
- // 按钮文案
1029
- <Button>{t('btn.add')}</Button>
1030
-
1031
- // 成功提示
1032
- message.success(t('msg.createSuccess'));
1033
-
1034
- // 带插值
1035
- t('msg.deleteConfirm', { name: record.ruleName })
1036
- // 对应语言包中: "确认删除「{{name}}」吗?"
1037
- ```
1038
-
1039
- ### 13.5 Modal 标题动态化
1040
-
1041
- ```tsx
1042
- <Modal title={id ? t('form.editTitle') : t('form.addTitle')} ...>
1043
- ```
1044
-
1045
- ### 13.6 新增模块步骤
1046
-
1047
- **每新增一个业务模块,必须为所有 8 种语言创建对应的 locale 文件**:
1048
-
1049
- 1. 在 `src/locales/<lng>/<module>/` 下创建语言包文件(codeRule.ts、codeMapping.ts 等)
1050
- 2. 确保 8 种语言包(zh-CN / en-US / ja-JP / fr-FR / es-ES / ar-SA / ru-RU / de-DE)都包含相同的 key 结构
1051
- 3. 组件中使用 `useTranslation('<module>/<file>')` 引用
1052
-
1053
- > **注意**:`i18next` 通过 `import.meta.glob('@/locales/**/*.ts')` 自动加载所有文件,**不需要**在 `src/i18n/index.ts` 中手动注册。
1054
-
1055
- ### 13.7 语言切换
1056
-
1057
- 用户通过 Header 的语言切换组件切换语言,切换后调用 `i18n.changeLanguage(lng)` 并持久化到 `localStorage('app-language')`。
1058
-
1059
- ### 13.8 不支持的语言
1060
-
1061
- `fallbackLng` 配置为 `'zh-CN'`,任何缺失的 key 都会回落到简体中文。
1062
-
1063
- ---
1064
-
1065
- ## 十四、字典系统规范
1066
-
1067
- 字典在登录后由 `AuthGuard` 一次性 `useDictStore.fetchDictData()` 全部缓存到内存,所有页面直接读:
1068
-
1069
- ```tsx
1070
- const getDictOptions = useDictStore((s) => s.getDictOptions);
1071
- const getDictLabel = useDictStore((s) => s.getDictLabel);
1072
-
1073
- // 渲染标签
1074
- <DictTag type="common_status" value={record.status} />
1075
-
1076
- // ProTable 搜索下拉
1077
- valueEnum: Object.fromEntries(
1078
- getDictOptions('ai_platform').map((d) => [d.value, { text: d.label }])
1079
- )
1080
-
1081
- // Form Select 下拉
1082
- options={getDictOptions('system_org_type').map((d) => ({
1083
- label: d.label, value: Number(d.value), // ← 字段是 number 时记得转
1084
- }))}
1085
- ```
1086
-
1087
- **新增字典必须三处都加**(`src/mocks/data/index.ts`):
1088
-
1089
- 1. `mockDictTypes` — 字典类型表
1090
- 2. `mockDictData` — 字典数据表(注意 value 始终是 string)
1091
- 3. 业务代码的 `<DictTag type="...">` 与 `getDictOptions('...')`
1092
-
1093
- > **注意**:字典 `value` 在数据库是 string;如果业务字段类型是 number(如 `status: number`),表单 Select 的 `value` 要 `Number(d.value)` 转回去。
1094
-
1095
- ---
1096
-
1097
- ## 十五、权限系统规范
1098
-
1099
- ### 14.1 组件级权限
1100
-
1101
- ```tsx
1102
- import Access from '@/components/Access';
1103
- import { useAccess } from '@/components/Access';
1104
-
1105
- // 组件级权限控制
1106
- <Access code="system:user:create">
1107
- <Button type="primary">新增</Button>
1108
- </Access>
1109
-
1110
- // Hook 级权限控制
1111
- const canUpdate = useAccess('system:menu:update');
1112
- ```
1113
-
1114
- ### 14.2 权限检查
1115
-
1116
- - 权限码格式:`system:user:create`(模块:资源:操作)
1117
- - `*:*:*` 代表超级管理员,拥有所有权限
1118
- - `checkPermi(permissions[])` 检查是否有**任意一个**权限
1119
- - `checkRole(roles[])` 检查是否有**任意一个**角色(`super_admin` 跳过)
1120
-
1121
- ### 14.3 新增菜单+按钮权限三步
1122
-
1123
- 1. `mockAdminBackendMenus`(或子菜单数组)加 type=2 菜单 + type=3 子按钮(path 为空,name 中文)
1124
- 2. `collectPermissionsFromBackendMenus` 的 `codeMap` 注册 `中文名 → 权限码`
1125
- 3. 业务代码用 `<Access code="xxx">` 包裹按钮
1126
-
1127
- ---
1128
-
1129
- ## 十六、Mock 规范
1130
-
1131
- ### 15.1 新建模块步骤
1132
-
1133
- 1. `src/mocks/data/index.ts` 加 mock 类型 + seed 数据 + 必要字典
1134
- 2. `src/mocks/handlers/<module>.ts` 写 MSW handler,统一 `success(data)` helper
1135
- 3. 在 `src/mocks/handlers/index.ts` 注册到 `handlers` 数组
1136
- 4. 路径必须以 `/admin-api/` 开头
1137
-
1138
- ### 15.2 Handler 模板
1139
-
1140
- ```ts
1141
- import { http, HttpResponse } from 'msw';
1142
-
1143
- function success<T>(data: T) {
1144
- return HttpResponse.json({ code: 0, message: 'ok', data });
1145
- }
1146
-
1147
- http.get('/admin-api/xxx/page', async ({ request }) => {
1148
- await delay(150);
1149
- const url = new URL(request.url);
1150
- const pageNo = Number(url.searchParams.get('pageNo') || '1');
1151
- const pageSize = Number(url.searchParams.get('pageSize') || '10');
1152
- // 过滤、分页
1153
- return success({ list, total });
1154
- }),
1155
- ```
1156
-
1157
- > **注意**:MSW handler 路径**必须**带 `/admin-api/` 前缀,否则拦截不到。
1158
-
1159
- ---
1160
-
1161
- ## 十七、上传组件规范
1162
-
1163
- ### 16.1 标准用法
1164
-
1165
- ```tsx
1166
- import { Upload } from 'antd';
1167
- import { getAccessToken } from '@/utils/token';
1168
- import { useAuthStore } from '@/stores/useAuthStore';
1169
-
1170
- <Upload
1171
- name="file"
1172
- action="/admin-api/infra/file/upload"
1173
- headers={{
1174
- Authorization: `Bearer ${getAccessToken() ?? ''}`,
1175
- 'tenant-id': String(useAuthStore.getState().tenantId ?? ''),
1176
- }}
1177
- beforeUpload={...}
1178
- onChange={({ file }) => {
1179
- if (file.status === 'done') {
1180
- const resp = file.response as { code?: number; data?: string };
1181
- if (resp?.code === 0 && resp.data) {
1182
- form.setFieldsValue({ avatar: resp.data });
1183
- }
1184
- }
1185
- }}
1186
- />
1187
- ```
1188
-
1189
- 或直接使用项目封装:`import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'`。
1190
-
1191
- ### 16.2 Upload 在 Form 中的正确使用
1192
-
1193
- > AntD `Upload` 不识别 `value` 属性。`<Form.Item name="logo">` 默认会注入 `value` → Upload 报警告。
1194
-
1195
- **正确做法**:外层 `Form.Item` 仅做标签(不带 `name`),里面套一个 `<Form.Item name="logo" noStyle hidden><Input /></Form.Item>` 持有值,Upload 通过 `form.setFieldsValue({ logo: url })` 写入。
1196
-
1197
- ---
1198
-
1199
- ## 十八、时间格式化规范
1200
-
1201
- **统一使用 `dateFormatter`**,禁止页面内手写 `dayjs().format(...)` 或字符串拼接:
1202
-
1203
- ```tsx
1204
- import { dateFormatter } from '@/utils/formatTime';
1205
-
1206
- // ProTable 列
1207
- render: (_, record) => dateFormatter(record.createTime) || '-',
1208
-
1209
- // Descriptions
1210
- <Descriptions.Item label="创建时间">{dateFormatter(data.createTime) || '-'}</Descriptions.Item>
1211
- ```
1212
-
1213
- `dateFormatter` 接受 `string | number | Date | null`,统一返回 `YYYY-MM-DD HH:mm:ss`,空值返回空串。
1214
-
1215
- > **注意**:如果后端 `createTime` 可能是 number/Date,统一用 `new Date(value).getTime()` 包装再比较。
1216
-
1217
- ---
1218
-
1219
- ## 十九、性能优化规范
1220
-
1221
- ### 18.1 组件优化
1222
-
1223
- ```tsx
1224
- // 路由级代码分割
1225
- import { lazy, Suspense } from 'react';
1226
- import { Spin } from 'antd';
1227
-
1228
- const UserList = lazy(() => import('@/pages/user-list'));
1229
-
1230
- <Suspense fallback={<Spin size="large" />}>
1231
- <UserList />
1232
- </Suspense>;
1233
-
1234
- // 避免不必要的重渲染
1235
- export const UserItem = React.memo(function UserItem({ user }: { user: User }) {
1236
- return <div>{user.name}</div>;
1237
- });
1238
- ```
1239
-
1240
- ### 18.2 Hooks 优化
1241
-
1242
- ```tsx
1243
- // useCallback 缓存事件处理函数
1244
- const handleDelete = useCallback(async (id: number) => {
1245
- await deleteUser(id);
1246
- fetchList();
1247
- }, [fetchList]);
1248
-
1249
- // useMemo 缓存计算结果
1250
- const filteredUsers = useMemo(
1251
- () => users.filter((user) => user.active),
1252
- [users],
1253
- );
1254
- ```
1255
-
1256
- ### 18.3 列表渲染
1257
-
1258
- ```tsx
1259
- // 必须提供稳定的 key
1260
- {users.map((user) => <UserItem key={user.id} user={user} />)}
1261
- ```
1262
-
1263
- ---
1264
-
1265
- ## 二十、代码质量标准
1266
-
1267
- ### 19.1 代码复杂度
1268
-
1269
- - 单个函数不超过 50 行
1270
- - 单个文件不超过 300 行,超出按职责拆分
1271
- - 嵌套层级不超过 3 层
1272
- - 圈复杂度不超过 10
1273
-
1274
- ### 19.2 错误处理
1275
-
1276
- ```typescript
1277
- async function handleSubmit(values: UserFormValues) {
1278
- try {
1279
- await createUser(values);
1280
- message.success('新增成功');
1281
- closeModal();
1282
- } catch (error) {
1283
- // request 拦截器已弹出错误提示,此处可做额外处理
1284
- console.error('创建用户失败:', error);
1285
- }
1286
- }
1287
- ```
1288
-
1289
- ### 19.3 ESLint 规则
1290
-
1291
- - 使用 `typescript-eslint` 进行 TypeScript 感知检查
1292
- - 启用 `eslint-plugin-react-hooks` 检查 Hooks 使用规则
1293
- - 启用 `eslint-plugin-react-refresh` 检查 HMR 兼容性
1294
- - 禁止在 `useEffect` 中遗漏依赖项
1295
- - 提交前必须本地通过:`npx tsc -b` 与 `npx eslint <changed-files>`
1296
-
1297
- ### 19.4 构建
1298
-
1299
- - 构建必须通过:`npm run build`(等价于 `tsc -b && vite build`)
1300
-
1301
- ---
1302
-
1303
- ## 二十一、常见问题规范(必读陷阱)
1304
-
1305
- > 以下均为项目实际踩过的坑,新增代码时必须注意。
1306
-
1307
- ### 20.1 `form.validateFields()` 只返回已挂载 Form.Item
1308
-
1309
- - 编辑模式隐藏字段(如 `username/password`)不会被收集
1310
- - 提交时必须 `{ ...originData.current, ...values, id }` 合并
1311
- - 隐藏字段用 `hidden={!!id}`,**不要**用 `{!id && <Form.Item>}`
1312
-
1313
- ### 20.2 AntD `Upload` 不识别 `value` 属性
1314
-
1315
- - `<Form.Item name="logo">` 默认会注入 `value` → Upload 报警告
1316
- - **正确做法**:外层 `Form.Item` 仅做标签(不带 `name`),里面套 `<Form.Item name="logo" noStyle hidden><Input /></Form.Item>` 持有值
1317
-
1318
- ### 20.3 AntD Tree `onSelect` 点击已选节点返回空数组
1319
-
1320
- - 不要自己做 toggle,直接判断 `keys.length === 0`
1321
-
1322
- ### 20.4 AntD Tree 勾选要收集 `halfCheckedKeys`
1323
-
1324
- - 菜单权限分配父节点半选也要提交,用 `onCheck` 的 `info.halfCheckedKeys`
1325
-
1326
- ### 20.5 Tree 编辑回显只勾选叶子节点
1327
-
1328
- - 直接 `setCheckedKeys(所有menuIds)` 会让父节点全选 → 子节点都被选
1329
- - 用 `getLeafKeys()` 只勾叶子
1330
-
1331
- ### 20.6 `react-hooks/set-state-in-effect`
1332
-
1333
- - `useEffect` 内调 `setState` 或调用内部包含 setState 的函数会被 lint 标记
1334
- - **统一处理**:在 setState 行(或 `void fetchList()` 行)上方加 `// eslint-disable-next-line react-hooks/set-state-in-effect`
1335
-
1336
- ### 20.7 MSW 路径必须 `/admin-api/` 前缀
1337
-
1338
- - `baseURL = http://127.0.0.1:48080/admin-api`,handler 不带 `/admin-api/` 拦截不到
1339
-
1340
- ### 20.8 flex 代替 Row/Col 做左右分栏
1341
-
1342
- - AntD `Row` 默认 `flex-wrap: wrap`,左侧树+右侧列表会被换行
1343
- - 用原生 `display: flex` + `flexShrink: 0`
1344
-
1345
- ### 20.9 Drawer 宽度直接使用 `width` 属性
1346
-
1347
- - Ant Design 6 中 `width` 属性直接可用:`<Drawer width={600}>`
1348
- - 默认宽度 378px,常用值:480 / 520 / 600 / 720
1349
- - 也可用 `size="large"` 取预设(约 736px)
1350
-
1351
- ### 20.10 Badge 显示数字而非红点
1352
-
1353
- - `<Badge count={n} overflowCount={99} size="small">` 而非 `dot`
1354
- - count 为 0 时 antd 自动隐藏徽标
1355
-
1356
- ### 20.11 `localeCompare` 仅对 string 有效
1357
-
1358
- - 如果后端 `createTime` 可能是 number/Date,统一用 `new Date(v).getTime()` 比较
1359
-
1360
- ### 20.12 ProTable `valueType: 'dateRange'` 列必须用 `render` 而非 `renderText`
1361
-
1362
- - `renderText` 会被 valueType 的 dateRange 处理逻辑覆盖,单元格显示空白
1363
-
1364
- ### 20.13 页面文件路径必须等于后端菜单 `component`
1365
-
1366
- - 例:后端给 `infra/data-source-config/index` → 文件 `src/pages/infra/data-source-config/index.tsx`
1367
- - 错位会出现 `Component not found` 警告,并 404
1368
-
1369
- ### 20.14 mock 菜单注册点要更新 codeMap
1370
-
1371
- - 加新权限按钮时除了菜单数组,还要在 `collectPermissionsFromBackendMenus` 内的 `codeMap` 注册
1372
-
1373
- ### 20.15 避免缓存数据为空导致 Select 显示原始 ID
1374
-
1375
- - 父组件应在挂载时一次性加载下拉数据:`useEffect(() => { fetch(); }, [])`,**不要**只在「新增」按钮 onClick 中拉
1376
-
1377
- ### 20.16 CRLF/LF 警告
1378
-
1379
- - Windows 上 git add 会提示 `LF will be replaced by CRLF`,无需处理
1380
-
1381
- ---
1382
-
1383
- ## 二十二、Vue → React 迁移映射表
1384
-
1385
- | Vue (Element Plus) | React (Ant Design 6) |
1386
- | --------------------------------------- | -------------------------------------------------------------------- |
1387
- | `<el-form v-model>` | `<Form form={form}>` + `Form.useForm()` |
1388
- | `ref.validate()` | `await form.validateFields()`(**只返回已挂载字段**) |
1389
- | `<el-dialog :visible>` | `<Modal open={open}>` |
1390
- | `<el-drawer :visible>` | `<Drawer open={open} onClose={...}>` |
1391
- | `defineExpose({ open })` | props: `open, id, onSuccess, onCancel`(Modal)/ `onClose`(Drawer) |
1392
- | `<el-table><el-table-column>` | `<ProTable columns={[...]} request={...} />` |
1393
- | `v-model` 双向绑定 | `useState` + `value/onChange` |
1394
- | `reactive() / ref()` | `useState()` |
1395
- | `onMounted()` | `useEffect(() => {}, [])` |
1396
- | `watch()` | `useEffect(() => {}, [dep])` |
1397
- | `computed()` | `useMemo()` |
1398
- | `defineEmits(['success'])` | `props.onSuccess()` |
1399
- | `pinia store` | `zustand store` |
1400
- | `v-hasPermi` | `<Access code="xxx">` |
1401
- | `v-loading` | `<Spin spinning>` 或组件自身 `loading` 属性 |
1402
- | `message.delConfirm()` | `modal.confirm({ title: '确认删除?', okType: 'danger', onOk })` |
1403
- | `getStrDictOptions / getIntDictOptions` | `getDictOptions(type)` 统一返回 |
1404
- | `dateFormatter` | 复用项目同名 `dateFormatter` |
1405
- | `el-tree show-checkbox` | `<Tree checkable selectable={false}>` + `onClick` 切换 |
1406
- | `v-model:visible` Drawer/Modal | `open` + `onClose/onCancel` |
1407
- | `el-input v-model` | `<Input value/onChange>` 受控 |
1408
- | `el-switch v-model` | `<Switch checked/onChange>` |
1409
- | 原生 Date | `dayjs(value)` 或 `new Date(value).getTime()` |
1410
- | 自定义 SCSS 变量覆盖 | `ConfigProvider` + `theme.token` |
1411
-
1412
- > **Vue 路径 → React 文件**:后端菜单 `component` 字段决定 React 文件位置。`ai/chat/index/index` → `src/pages/ai/chat/index/index.tsx`。
1413
-
1414
- ---
1415
-
1416
- ## 总结
1417
-
1418
- 本规范文档涵盖了 React 19 + TypeScript 6 + Ant Design 6 项目的完整开发规范。团队成员必须严格遵守,以确保代码质量和协作效率。
1419
-
1420
- **重要提醒**:
1421
- 1. 所有新代码必须符合本规范
1422
- 2. 代码评审时重点检查规范遵守情况
1423
- 3. 定期更新规范以适应项目发展
1424
- 4. 遇到规范未覆盖的情况,及时讨论并补充
1425
-
1426
- **新模块自检清单**:
1427
-
1428
- - [ ] 文件路径与后端菜单 path 对齐
1429
- - [ ] API、Mock handler、Mock data、菜单、权限码 五处全部更新
1430
- - [ ] 所有时间字段用 `dateFormatter` 渲染
1431
- - [ ] 所有字典字段用 `<DictTag>` + `valueEnum`
1432
- - [ ] 所有按钮用 `<Access code="...">` 包裹
1433
- - [ ] Modal 用 `open/id/onSuccess/onCancel` 四件套 + `originData` 合并
1434
- - [ ] ProTable 而非手撸 Card+Form+Table
1435
- - [ ] `npm run build` 通过
1436
- - [ ] `npx eslint <new-files>` 零错误
1437
- - [ ] 关键逻辑加中文注释,复杂分支说明「为什么这么写」
1438
-
1439
- ---
1440
-
1441
- **文档结束**
1442
-
1443
- *本文档作为前端开发的标准规范,所有开发人员必须严格遵守。*