| | |
| | | # CRM Admin 项目开发指南(CLAUDE.md) |
| | | |
| | | > Vite 6 + React 18 + TypeScript + Ant Design 5 + SCSS 的后台管理平台。 |
| | | > 源码位于 `src/`,使用 BrowserRouter 的 SPA 架构,包含多级路由菜单、登录 / 退出与登录态校验。 |
| | | > Vite 6 + React 18 + TypeScript(strict)+ Ant Design 5 + `@ant-design/pro-components` + Zustand 5 + Axios + SCSS 的后台管理平台。 |
| | | > BrowserRouter SPA,包含多级路由菜单、登录 / 退出与登录态校验;日期库为 Day.js(已配置中文 locale)。 |
| | | |
| | | ## 一、常用命令 |
| | | |
| | |
| | | |
| | | 改动代码后至少执行 `npm run typecheck`;涉及构建配置或样式时再执行 `npm run build`。 |
| | | |
| | | ## 二、技术栈 |
| | | |
| | | - **构建**:Vite 6 |
| | | - **框架**:React 18 + TypeScript(strict) |
| | | - **路由**:react-router-dom 6 |
| | | - **UI**:Ant Design 5 + `@ant-design/icons` |
| | | - **高级组件**:`@ant-design/pro-components`(表格优先使用其 `ProTable`) |
| | | - **状态**:Zustand 5(登录态持久化到 localStorage) |
| | | - **请求**:Axios(统一封装在 `src/api/request.ts`) |
| | | - **样式**:SCSS + Ant Design 主题 token + CSS 变量 |
| | | - **日期**:Day.js(已配置中文 locale) |
| | | |
| | | ## 三、目录结构 |
| | | ## 二、目录与核心架构 |
| | | |
| | | ```text |
| | | src/ |
| | | ├── api/ # 接口层:request.ts(axios 封装)与登录 auth.ts |
| | | ├── components/ # 通用组件:AntdAppBridge(message 注入) |
| | | ├── layouts/ # BasicLayout:侧边栏、头部、内容区、面包屑 |
| | | ├── pages/ # 页面,按业务域分目录 |
| | | │ ├── login/ |
| | | │ ├── dashboard/ |
| | | │ ├── system/users/ |
| | | │ ├── business/orders/ |
| | | │ ├── customers/list/ |
| | | │ └── error/NotFound.tsx |
| | | │ # 页面目录:index.tsx、columns.tsx、types.ts、service.ts、components/(按需)、同名 .scss(按需) |
| | | ├── router/ |
| | | │ ├── routeConfig.tsx # 路由 + 菜单唯一数据源(含页面组件) |
| | | │ ├── menu.tsx # 菜单过滤与 antd Menu 结构生成 |
| | | │ └── index.tsx # 配置转 React Router 嵌套路由 |
| | | ├── store/ # Zustand 登录态 |
| | | ├── styles/ # 全局 SCSS、通用页面样式 |
| | | ├── types/ # 全局类型 |
| | | ├── utils/ # 工具:feedback.ts(全局错误提示) |
| | | └── main.tsx # ConfigProvider + AntdApp + BrowserRouter |
| | | ├── api/ # request.ts(axios 封装)、auth.ts、authHeaders.ts |
| | | ├── components/ # 通用组件(AntdAppBridge 等) |
| | | ├── layouts/ # BasicLayout:侧边栏、头部、内容区、面包屑 |
| | | ├── pages/ # 页面,按业务域分目录(页面目录结构见「三、3」) |
| | | ├── router/ # routeConfig.tsx(路由 + 菜单唯一数据源)、menu.tsx、index.tsx |
| | | ├── store/ # Zustand 登录态 |
| | | ├── styles/ # 全局 SCSS、通用页面样式 |
| | | ├── types/ # 全局类型(PageResult 等) |
| | | ├── utils/ # 工具(feedback.ts 全局错误提示) |
| | | └── main.tsx # ConfigProvider + AntdApp + BrowserRouter |
| | | ``` |
| | | |
| | | ## 四、核心架构 |
| | | - **路由与菜单**:`routeConfig.tsx` 是唯一数据源,配置 `path`、`title`、`icon`、`element` 后侧边栏与面包屑自动生效。支持 2-3 级嵌套;分组路由(无 `element`)通过 `<Outlet />` 渲染并默认跳转到第一个子页面;未匹配路由显示 404。 |
| | | - **路由懒加载**:页面组件统一通过 `lazy(() => import('@/pages/...'))` 注册,`BasicLayout` 的内容区已提供 `<Suspense>` 边界;新增页面不要用静态 import,避免所有页面打进入口 chunk。 |
| | | - **登录态**:`src/store/auth.ts` 持久化 `token` 与 `user`;`src/api/auth.ts` 调用 `POST /auth/tenant-login`(`{ phone, password, tenantCode }`)。未登录访问受保护页面跳转 `/login`,登录成功后返回原地址。 |
| | | - **布局与样式**:页面只负责内容区。全局 CSS 变量 `--app-primary`、`--app-bg`、`--app-border`、`--app-text`、`--app-text-secondary`;通用类 `.page-header`、`.page-toolbar`、`.filter-card`、`.table-card`、`.text-muted`;Ant Design 主题在 `main.tsx` 的 `ConfigProvider` 统一配置。 |
| | | |
| | | ### 1. 路由与菜单 |
| | | |
| | | `src/router/routeConfig.tsx` 是**唯一数据源**。新增页面只需在这里配置 `path`、`title`、`icon`、`element`,侧边栏、面包屑会自动读取。 |
| | | |
| | | - 支持 2-3 级嵌套,例如:`用户与权限 → 用户管理`。 |
| | | - `src/router/index.tsx` 把菜单配置转换成 React Router 的嵌套路由。 |
| | | - 分组路由(无 `element`)通过 `<Outlet />` 渲染子页面,默认跳转到第一个子页面。 |
| | | - 未匹配路由显示 404,普通页面组件位于 `BasicLayout` 内。 |
| | | |
| | | ### 2. 登录态 |
| | | |
| | | - `src/store/auth.ts`:Zustand store,持久化 `token` 与 `user`。 |
| | | - 登录 / 退出:`src/api/auth.ts` 调用 `POST /auth/tenant-login`,参数为 `{ phone, password, tenantCode }`。 |
| | | - 接口基础地址由 `.env` 中的 `VITE_API_BASE_URL` 配置。 |
| | | - 未登录访问受保护页面会跳转 `/login`,登录成功后返回原地址。 |
| | | |
| | | ### 3. 布局与样式 |
| | | |
| | | - 统一使用 `BasicLayout`,页面只负责内容区域。 |
| | | - 全局 CSS 变量:`--app-primary`、`--app-bg`、`--app-border`、`--app-text`、`--app-text-secondary` 等。 |
| | | - 通用页面类:`.page-header`、`.page-toolbar`、`.filter-card`、`.table-card`、`.text-muted`。 |
| | | - 页面级样式与页面同目录,使用 BEM 风格类名(如 `.user-cell__name`)。 |
| | | - Ant Design 主题在 `src/main.tsx` 的 `ConfigProvider` 中统一配置。 |
| | | |
| | | ## 五、代码风格(必遵守) |
| | | ## 三、代码风格(必遵守) |
| | | |
| | | ### 1. 统一使用箭头函数 |
| | | |
| | | 组件、工具函数、Hook、事件处理函数都使用箭头函数,不新增 `function` 声明。 |
| | | |
| | | ```tsx |
| | | // ✅ 组件 |
| | | const UserList = () => { |
| | | const handleSubmit = async () => { |
| | | // ... |
| | |
| | | }; |
| | | |
| | | export default UserList; |
| | | |
| | | // ✅ 工具 / Hook |
| | | export const useDebounce = () => { |
| | | // ... |
| | | }; |
| | | |
| | | export const formatAmount = (value: number) => `¥${value.toLocaleString()}`; |
| | | |
| | | // ❌ 不要这样写 |
| | | export default function UserList() {} |
| | | export function formatAmount(value: number) {} |
| | | ``` |
| | | |
| | | ### 2. 优先使用现成组件 |
| | | |
| | | 写功能前先找现成能力,不要重复造轮子: |
| | | |
| | | 1. **优先使用 Ant Design 组件**:Table、Form、Modal、Select、DatePicker、Card、Tabs、Drawer 等。 |
| | | 2. **优先使用项目已有封装**: |
| | | - 布局:`src/layouts/BasicLayout.tsx` |
| | | - 登录校验:`src/router/index.tsx` 中的 `RequireAuth` |
| | | - 全局提示 / 确认框:使用 `<App>` 提供的 `AntdApp.useApp()` 中的 `message`、`modal`,**不要**直接使用 `message.success()` / `Modal.confirm()` 静态方法。 |
| | | 3. **表格优先使用 ProTable**:新增列表页时优先使用 `@ant-design/pro-components` 提供的 `ProTable`,其搜索、分页、工具栏等能力开箱即用,不要手写筛选表单 + Table 的组合。 |
| | | 4. **表单内的选择控件优先使用 ProForm 组件**:`Form` 内的 Select、TreeSelect、Cascader 等选择控件,只要选项来自接口(包括父组件传入的接口数据,如组织树、角色列表),必须使用 `@ant-design/pro-components` 提供的 `ProFormSelect`、`ProFormTreeSelect`、`ProFormCascader` 等,不要用 antd 原生控件包在 `Form.Item` 中;静态枚举选项不受此限制。远程搜索使用 `request`(配合 `debounceTime`),已有选项数据通过 `options` 或 `fieldProps` 传入,校验规则直接写在组件的 `rules` 上。 |
| | | ```tsx |
| | | // ✅ 选项来自接口 |
| | | <ProFormSelect |
| | | name="leaderId" |
| | | label="负责人" |
| | | showSearch |
| | | debounceTime={300} |
| | | request={async ({ keyWords }) => getLeaderOptions({ keyword: keyWords })} |
| | | /> |
| | | - **优先使用 Ant Design 组件**与已有封装:布局 `BasicLayout`、登录校验 `RequireAuth`、提示与确认框使用 `AntdApp.useApp()` 的 `message` / `modal`(**不要**用 `message.success()` / `Modal.confirm()` 静态方法)。 |
| | | - **表格优先使用 ProTable**:搜索、分页、工具栏开箱即用,不要手写「筛选表单 + Table」。 |
| | | - **表单内的接口选项必须用 ProForm 组件**:Select、TreeSelect、Cascader 等只要选项来自接口(含父组件传入的接口数据),使用 `ProFormSelect` / `ProFormTreeSelect` / `ProFormCascader`,不要用原生控件包 `Form.Item`;静态枚举不受此限制。远程搜索用 `request`(配合 `debounceTime`),已有数据通过 `options` / `fieldProps` 传入,校验规则直接写在组件的 `rules` 上。 |
| | | |
| | | // ✅ 选项由父组件传入 |
| | | <ProFormTreeSelect |
| | | name="parentId" |
| | | label="上级组织" |
| | | fieldProps={{ treeData: orgTree, showSearch: true }} |
| | | /> |
| | | ```tsx |
| | | // ✅ 选项来自接口 |
| | | <ProFormSelect |
| | | name="leaderId" |
| | | label="负责人" |
| | | showSearch |
| | | debounceTime={300} |
| | | request={async ({ keyWords }) => getLeaderOptions({ keyword: keyWords })} |
| | | /> |
| | | |
| | | // ❌ 不要这样写 |
| | | <Form.Item name="leaderId" label="负责人"> |
| | | <Select options={leaderOptions} /> |
| | | </Form.Item> |
| | | ``` |
| | | 5. **不要手写**弹窗、Toast、下拉菜单、分页、表格、表单校验等基础交互。 |
| | | 6. 不引入新的 UI 组件库;确需新组件时,先检查是否已有可复用实现,再考虑在 `src/components/` 下新增。 |
| | | 7. 使用 Ant Design 5 的 API,不使用已废弃属性。例如 Card 使用 `variant="borderless"`,不要使用 `bordered={false}`。 |
| | | // ❌ 原生控件包在 Form.Item 中 |
| | | <Form.Item name="leaderId" label="负责人"> |
| | | <Select options={leaderOptions} /> |
| | | </Form.Item> |
| | | ``` |
| | | |
| | | ### 3. 独立组件必须放到 `components/`,超长页面拆分 |
| | | - 不要手写弹窗、Toast、下拉菜单、分页、表格、表单校验等基础交互。不引入新的 UI 组件库;确需新组件时,先检查是否已有可复用实现,再考虑在 `src/components/` 下新增。 |
| | | - 使用 Ant Design 5 的 API,不使用已废弃属性(如 Card 用 `variant="borderless"`,不用 `bordered={false}`)。 |
| | | |
| | | 单独封装的组件不写在页面目录根部,统一放到页面目录下的 `components/` 子目录。 |
| | | ### 3. 独立组件放 `components/`,超长页面拆分 |
| | | |
| | | - Drawer、Modal 等弹层组件,以及详情卡片、表单、局部业务块等可独立成文件的组件,放到 `src/pages/<业务域>/<页面>/components/` 下。 |
| | | - 组件文件使用大驼峰命名,如 `UserFormDrawer.tsx`、`CustomerDetailDrawer.tsx`;组件私有的 `.scss` 与组件同目录,由组件自身引入。 |
| | | - 组件只接收 `props`,接口调用仍放在页面目录的 `service.ts`,由页面或组件按需调用;不要跨页面直接引用其他页面目录下的组件。 |
| | | - 单个页面文件(含 `index.tsx`)超过 300 行时必须拆分:按功能块抽取到 `components/`,`columns.tsx`、`types.ts`、`service.ts`、`utils.ts` 等继续单独维护。 |
| | | - 大于 300 行的复杂组件同样按功能拆分,不要在同一个文件里堆叠多个组件。 |
| | | - Drawer、Modal 等弹层组件,以及详情卡片、表单、局部业务块等可独立成文件的组件,放到页面目录下的 `components/`,文件名使用大驼峰;组件私有 `.scss` 与组件同目录,由组件自身引入。 |
| | | - 组件只接收 `props`;接口调用仍放在页面目录的 `service.ts`,由页面或组件按需调用;不要跨页面引用其他页面目录下的组件。 |
| | | - 单个页面文件(含 `index.tsx`)超过 300 行必须拆分,按功能块抽取到 `components/`;超过 300 行的复杂组件同样按功能拆分,不要在同一个文件里堆叠多个组件。 |
| | | |
| | | ```text |
| | | src/pages/system/users/ |
| | | ├── index.tsx # 页面组件(尽量精简) |
| | | ├── columns.tsx # 表格列定义 |
| | | ├── types.ts |
| | | ├── service.ts |
| | | ├── utils.ts # 按需 |
| | | ├── Users.scss |
| | | ├── index.tsx # 页面组件(尽量精简) |
| | | ├── columns.tsx # 表格列定义 |
| | | ├── types.ts # 页面类型 |
| | | ├── service.ts # 接口调用 |
| | | ├── utils.ts # 按需 |
| | | ├── Users.scss # 页面样式 |
| | | └── components/ |
| | | ├── UserFormDrawer.tsx |
| | | ├── UserDetailDrawer.tsx |
| | | └── UserImportModal.tsx |
| | | └── UserDetailDrawer.tsx |
| | | ``` |
| | | |
| | | ### 4. Table columns 必须单独提取 |
| | | ## 四、新增页面流程 |
| | | |
| | | 所有表格的 `columns` 不得内联在 `<Table />` 的 JSX 中,也不写在页面组件内部。统一提取到页面目录单独维护。 |
| | | |
| | | 推荐结构: |
| | | |
| | | ```text |
| | | src/pages/system/users/ |
| | | ├── index.tsx # 页面组件 |
| | | ├── columns.tsx # 表格列定义 |
| | | └── Users.scss # 页面样式 |
| | | ``` |
| | | |
| | | ```tsx |
| | | // columns.tsx |
| | | import type { TableProps } from 'antd'; |
| | | |
| | | export const userColumns: TableProps<UserRecord>['columns'] = [ |
| | | { |
| | | title: '用户名', |
| | | dataIndex: 'username', |
| | | }, |
| | | // ... |
| | | ]; |
| | | |
| | | // 需要事件回调时,导出工厂函数 |
| | | export const createUserColumns = (options: { |
| | | onEdit: (record: UserRecord) => void; |
| | | onDelete: (record: UserRecord) => void; |
| | | }): TableProps<UserRecord>['columns'] => [ |
| | | // ... |
| | | { |
| | | title: '操作', |
| | | key: 'action', |
| | | render: (_value, record) => ( |
| | | <Button type="link" size="small" onClick={() => options.onEdit(record)}> |
| | | 编辑 |
| | | </Button> |
| | | ), |
| | | }, |
| | | ]; |
| | | ``` |
| | | |
| | | ```tsx |
| | | // index.tsx |
| | | const columns = useMemo(() => createUserColumns({ onEdit, onDelete }), []); |
| | | |
| | | <Table<UserRecord> rowKey="key" columns={columns} dataSource={dataSource} />; |
| | | ``` |
| | | |
| | | 约定: |
| | | |
| | | - columns 只负责展示,数据与业务逻辑留在页面组件。 |
| | | - 列类型使用 `TableProps<T>['columns']`;使用 `ProTable` 时使用 `ProColumns<T>[]`,不要使用 `any`。 |
| | | - 若列配置依赖页面状态或回调,使用 `createColumns(options)` 工厂 + `useMemo`,不要每次渲染重新创建。 |
| | | |
| | | > 说明:存量页面中部分 columns 仍定义在页面组件内部、部分函数使用 `function` 声明。新增和修改代码一律遵循上述规范,存量代码在后续迭代中逐步迁移。 |
| | | |
| | | ## 六、新增页面流程 |
| | | |
| | | **新增页面统一使用 `/new-page` skill**(定义在 `.claude/skills/new-page/`),不要手工复制模板或另起方式搭脚手架。该 skill 会先选择表格页 / 非表格页分支并整体套用对应模板,再统一完成代码规范、路由注册、文档更新与类型检查。 |
| | | |
| | | 调用示例: |
| | | **新增页面统一使用 `/new-page` skill**(`.claude/skills/new-page/`):它会选择表格页 / 非表格页分支并整体套用模板,再完成代码规范、路由注册、文档更新与类型检查。不要手工复制模板或另起方式搭脚手架。 |
| | | |
| | | ```bash |
| | | /new-page 根据 <接口文档路径>,添加一个【页面名】页面,放到【分组名】目录下 |
| | | ``` |
| | | |
| | | > 仅修改现有页面(如调整某一列)等不涉及脚手架的场景,才直接改代码;即使手工创建页面,也须按以下流程执行。 |
| | | 仅改动现有页面(如调整某一列)等不涉及脚手架的场景才直接改代码。新页面必须在 `src/router/routeConfig.tsx` 用完整路径注册(`path`、`title`、`icon`、`element`,需要分组时使用 `children`),页面组件使用 `lazy(() => import(...))` 懒加载,完成后执行 `npm run typecheck`。 |
| | | |
| | | 1. 在 `src/pages/<业务域>/<页面>/` 下创建 `index.tsx`(有表格时增加 `columns.tsx`,独立组件放 `components/`,接口调用放在 `service.ts`,类型放 `types.ts`,有独立样式时增加同名 `.scss`)。 |
| | | 2. 在 `src/router/routeConfig.tsx` 中增加路由配置: |
| | | - 使用完整路径(如 `/system/users`)。 |
| | | - 配置 `title`、`icon`、`element`。 |
| | | - 需要分组时使用 `children`。 |
| | | 3. 执行 `npm run typecheck` 验证(涉及样式或构建配置时再执行 `npm run build`)。 |
| | | ## 五、接口与状态约定 |
| | | |
| | | 示例: |
| | | |
| | | ```tsx |
| | | { |
| | | path: '/business/invoices', |
| | | title: '发票管理', |
| | | icon: <FileTextOutlined />, |
| | | element: <Invoices />, |
| | | } |
| | | ``` |
| | | |
| | | ## 七、接口与状态约定 |
| | | |
| | | 以下四条为强制要求,新增与修改代码必须遵守: |
| | | 以下四条为强制要求: |
| | | |
| | | 1. **所有接口必须使用 `src/api/request.ts`**:统一通过其导出的 `request` 发起请求,不要在页面、组件或 `service.ts` 中直接使用 axios、`fetch` 或其他请求库。 |
| | | 2. **每个接口的 request 与 response 都必须定义 `interface`**:在 `service.ts` 中通过 `request<TReq, TRes>` 显式标注泛型,禁止使用 `any` 或省略类型。 |
| | | - 类型统一定义在页面目录的 `types.ts` 中(跨页面复用的放在 `src/api/` 对应业务域文件)。 |
| | | - 命名约定:`<业务>Request` / `<业务>Response`,例如 `TenantRoleListRequest`、`TenantRoleListResponse`。 |
| | | - 请求体 / 查询参数建模为 `XxxRequest`;响应建模为 `XxxResponse`,对应接口返回 `data` 的结构(若后端是 `Result<T>`,`Response` 指 `data` 部分)。 |
| | | - 无请求参数时也应显式定义,例如 `type TenantRoleListRequest = Record<string, never>`,不要留空或使用 `undefined`。 |
| | | - 类型定义在页面目录的 `types.ts`(跨页面复用的放到 `src/api/` 对应业务域文件)。 |
| | | - 命名:请求体 / 查询参数为 `<业务>Request`,响应为 `<业务>Response`,响应对应接口返回 `data` 的结构(后端为 `Result<T>` 时取 `data` 部分)。 |
| | | - 无请求参数时也要显式定义,例如 `type TenantRoleListRequest = Record<string, never>`。 |
| | | |
| | | ```ts |
| | | // types.ts |
| | |
| | | }); |
| | | ``` |
| | | |
| | | 3. **接口调用必须使用 `.then` 链式处理,禁止 `await` + `try/catch`**:成功逻辑写在 `.then`,失败逻辑写在 `.catch`(错误已由 `request` 统一提示时可只保留空 `catch`),收尾逻辑写在 `.finally`。 |
| | | 3. **接口调用必须使用 `.then` 链式处理,禁止 `await` + `try/catch`**:成功逻辑写在 `.then`,失败逻辑写在 `.catch`(错误已由 `request` 统一提示时可留空),收尾逻辑写在 `.finally`。 |
| | | |
| | | ```ts |
| | | // ✅ 使用 .then 链式处理 |
| | | // ✅ |
| | | getOrgList({ page: 1, size: 20 }) |
| | | .then((data) => { |
| | | setDataSource(data.records); |
| | |
| | | setLoading(false); |
| | | }); |
| | | |
| | | // ❌ 不要使用 await + try/catch |
| | | // ❌ |
| | | try { |
| | | const data = await getOrgList({ page: 1, size: 20 }); |
| | | setDataSource(data.records); |
| | |
| | | ``` |
| | | |
| | | 4. **接口按作用域拆分文件**: |
| | | - 页面接口放在页面目录下的 `service.ts` 中,页面与组件通过它调用,不要在组件内直接写请求实现。 |
| | | - 页面接口放在页面目录下的 `service.ts`,页面与组件通过它调用,不要在组件内直接写请求实现。 |
| | | - 跨页面复用的公共接口按业务域拆分到 `src/api/` 下(如 `src/api/auth.ts`)。 |
| | | |
| | | 补充说明: |
| | | |
| | | - 开发环境通过 Vite 代理 `/proxy-api` 转发到 `VITE_API_PROXY_TARGET`(未配置时取 `VITE_API_BASE_URL`),因此浏览器端不会产生跨域请求;生产环境需后端配置 CORS,或使用同域反向代理。 |
| | | - 泛型:`request<TReq, TRes>(config, options)`,分别表示请求参数/请求体与响应 `data` 的类型。 |
| | | - 默认只返回响应 `data`;传 `parse: false` 时返回完整 `AxiosResponse`。 |
| | | - 默认自动提示错误,`showError: false` 可关闭;失败抛出 `ApiError`,包含 `status`、`data`、`response`、`config` 与原始错误。 |
| | | - 默认合并方法、URL、参数与请求体都相同的并发请求;`merge: false` 时每次都单独发送。 |
| | | - 通过 `signal: AbortSignal` 取消当前调用方的请求。 |
| | | - `src/api/auth.ts` 调用真实登录接口,登录成功后仍由 `src/store/auth.ts` 写入登录态。 |
| | | - 全局登录态只放在 `src/store/auth.ts`;页面局部状态使用 `useState` / `useMemo`。 |
| | | - 不要在前端持久化敏感信息;真实项目的 Token 建议使用 HttpOnly Cookie,并由后端处理过期与刷新。 |
| | | - `request<TReq, TRes>(config, options)`:泛型分别表示请求参数 / 请求体与响应 `data` 的类型;默认只返回 `data`,`parse: false` 返回完整 `AxiosResponse`;失败抛出 `ApiError`(含 `status`、`data`、`response`、`config`);默认合并相同并发请求(`merge: false` 关闭);`showError: false` 关闭自动提示;`signal` 取消当前调用。 |
| | | - `src/api/auth.ts` 调用真实登录接口,登录成功后由 `src/store/auth.ts` 写入登录态;全局登录态只放在 `src/store/auth.ts`,页面局部状态使用 `useState` / `useMemo`。 |
| | | - 不要在前端持久化敏感信息;真实项目的 Token 建议使用 HttpOnly Cookie,由后端处理过期与刷新。 |
| | | |
| | | ## 八、样式与可访问性 |
| | | ## 六、样式与可访问性 |
| | | |
| | | - 使用 SCSS,不在 JSX 中堆叠大量内联样式;动态颜色等少量样式可使用 `style`。 |
| | | - 颜色、圆角优先使用已有的 CSS 变量或 Ant Design token。 |
| | | - 使用 SCSS,不在 JSX 中堆叠内联样式;动态颜色等少量样式可使用 `style`。 |
| | | - 颜色、圆角优先使用已有 CSS 变量或 Ant Design token;页面级样式与页面同目录,使用 BEM 类名(如 `.user-cell__name`)。 |
| | | - 交互元素保持可读的 hover / focus 状态;图标按钮补充 `aria-label`。 |
| | | - 页面需兼容窄屏:表格使用 `scroll={{ x: ... }}`,左右布局使用栅格或 flex 换行。 |
| | | - 兼容窄屏:表格使用 `scroll={{ x: ... }}`,左右布局使用栅格或 flex 换行。 |
| | | |
| | | ## 九、部署注意 |
| | | ## 七、部署注意 |
| | | |
| | | - 使用 BrowserRouter,生产环境需要配置 SPA fallback:所有前端路由回退到 `index.html`。 |
| | | - 构建产物在 `dist/`。 |
| | | - 开发环境的跨域由 Vite 代理 `/proxy-api` 处理;生产环境需要后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。 |
| | | - 当前主包较大(Ant Design 全量依赖),后续可通过路由懒加载 / 手动分包优化。 |
| | | - 使用 BrowserRouter,生产环境需配置 SPA fallback:所有前端路由回退到 `index.html`;构建产物在 `dist/`。 |
| | | - 开发环境通过 Vite 代理 `/proxy-api` 转发到 `VITE_API_PROXY_TARGET`(未配置时取 `VITE_API_BASE_URL`),浏览器端不会跨域;生产环境需后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。 |
| | | - 已启用路由懒加载与依赖拆包:`vite.config.ts` 的 `manualChunks` 将 React、antd 核心、其余依赖拆成长缓存 chunk,ProComponents 及业务页面依赖由 Rollup 按需拆分为异步 chunk。新增页面时保持懒加载即可,无需再调整拆包配置。 |