# CRM Admin 项目开发指南(CLAUDE.md)
> Vite 6 + React 18 + TypeScript + Ant Design 5 + SCSS 的后台管理平台。
> 源码位于 `src/`,使用 BrowserRouter 的 SPA 架构,包含多级路由菜单、登录 / 退出与登录态校验。
## 一、常用命令
```bash
npm install # 安装依赖
npm run dev # 启动开发服务器(http://localhost:5173)
npm run typecheck # TypeScript 类型检查
npm run build # 类型检查 + 生产构建
npm run preview # 预览生产构建
```
改动代码后至少执行 `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、server.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
```
## 四、核心架构
### 1. 路由与菜单
`src/router/routeConfig.tsx` 是**唯一数据源**。新增页面只需在这里配置 `path`、`title`、`icon`、`element`,侧边栏、面包屑会自动读取。
- 支持 2-3 级嵌套,例如:`用户与权限 → 用户管理`。
- `src/router/index.tsx` 把菜单配置转换成 React Router 的嵌套路由。
- 分组路由(无 `element`)通过 `` 渲染子页面,默认跳转到第一个子页面。
- 未匹配路由显示 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 () => {
// ...
};
return
;
};
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`
- 全局提示 / 确认框:使用 `` 提供的 `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
// ✅ 选项来自接口
getLeaderOptions({ keyword: keyWords })}
/>
// ✅ 选项由父组件传入
// ❌ 不要这样写
```
5. **不要手写**弹窗、Toast、下拉菜单、分页、表格、表单校验等基础交互。
6. 不引入新的 UI 组件库;确需新组件时,先检查是否已有可复用实现,再考虑在 `src/components/` 下新增。
7. 使用 Ant Design 5 的 API,不使用已废弃属性。例如 Card 使用 `variant="borderless"`,不要使用 `bordered={false}`。
### 3. 独立组件必须放到 `components/`,超长页面拆分
单独封装的组件不写在页面目录根部,统一放到页面目录下的 `components/` 子目录。
- Drawer、Modal 等弹层组件,以及详情卡片、表单、局部业务块等可独立成文件的组件,放到 `src/pages/<业务域>/<页面>/components/` 下。
- 组件文件使用大驼峰命名,如 `UserFormDrawer.tsx`、`CustomerDetailDrawer.tsx`;组件私有的 `.scss` 与组件同目录,由组件自身引入。
- 组件只接收 `props`,接口调用仍放在页面目录的 `server.ts`,由页面或组件按需调用;不要跨页面直接引用其他页面目录下的组件。
- 单个页面文件(含 `index.tsx`)超过 300 行时必须拆分:按功能块抽取到 `components/`,`columns.tsx`、`types.ts`、`server.ts`、`utils.ts` 等继续单独维护。
- 大于 300 行的复杂组件同样按功能拆分,不要在同一个文件里堆叠多个组件。
```text
src/pages/system/users/
├── index.tsx # 页面组件(尽量精简)
├── columns.tsx # 表格列定义
├── types.ts
├── server.ts
├── utils.ts # 按需
├── Users.scss
└── components/
├── UserFormDrawer.tsx
├── UserDetailDrawer.tsx
└── UserImportModal.tsx
```
### 4. Table columns 必须单独提取
所有表格的 `columns` 不得内联在 `` 的 JSX 中,也不写在页面组件内部。统一提取到页面目录单独维护。
推荐结构:
```text
src/pages/system/users/
├── index.tsx # 页面组件
├── columns.tsx # 表格列定义
└── Users.scss # 页面样式
```
```tsx
// columns.tsx
import type { TableProps } from 'antd';
export const userColumns: TableProps['columns'] = [
{
title: '用户名',
dataIndex: 'username',
},
// ...
];
// 需要事件回调时,导出工厂函数
export const createUserColumns = (options: {
onEdit: (record: UserRecord) => void;
onDelete: (record: UserRecord) => void;
}): TableProps['columns'] => [
// ...
{
title: '操作',
key: 'action',
render: (_value, record) => (
),
},
];
```
```tsx
// index.tsx
const columns = useMemo(() => createUserColumns({ onEdit, onDelete }), []);
rowKey="key" columns={columns} dataSource={dataSource} />;
```
约定:
- columns 只负责展示,数据与业务逻辑留在页面组件。
- 列类型使用 `TableProps['columns']`;使用 `ProTable` 时使用 `ProColumns[]`,不要使用 `any`。
- 若列配置依赖页面状态或回调,使用 `createColumns(options)` 工厂 + `useMemo`,不要每次渲染重新创建。
> 说明:存量页面中部分 columns 仍定义在页面组件内部、部分函数使用 `function` 声明。新增和修改代码一律遵循上述规范,存量代码在后续迭代中逐步迁移。
## 六、新增页面流程
1. 在 `src/pages/<业务域>/<页面>/` 下创建 `index.tsx`(有表格时增加 `columns.tsx`,接口调用放在 `server.ts`,类型放 `types.ts`,有独立样式时增加同名 `.scss`)。
2. 在 `src/router/routeConfig.tsx` 中增加路由配置:
- 使用完整路径(如 `/system/users`)。
- 配置 `title`、`icon`、`element`。
- 需要分组时使用 `children`。
3. 执行 `npm run typecheck` 验证。
示例:
```tsx
{
path: '/business/invoices',
title: '发票管理',
icon: ,
element: ,
}
```
## 七、接口与状态约定
- **每个接口都必须为其请求(request)与响应(response)定义 `interface`**,并在 `server.ts` 中通过 `request` 显式标注泛型;禁止使用 `any` 或省略类型。
- 类型统一定义在页面目录的 `types.ts` 中(跨页面复用的放在 `src/api/` 对应业务域文件)。
- 命名约定:`<业务>Request` / `<业务>Response`,例如 `TenantRoleListRequest`、`TenantRoleListResponse`。
- 请求体 / 查询参数建模为 `XxxRequest`;响应建模为 `XxxResponse`,对应接口返回 `data` 的结构(若后端是 `Result`,`Response` 指 `data` 部分)。
- 无请求参数时也应显式定义,例如 `type TenantRoleListRequest = Record`,不要留空或使用 `undefined`。
```ts
// types.ts
export interface TenantRoleToggleStatusRequest {
id: number;
}
export interface TenantRoleToggleStatusResponse {
id: number;
status: RoleStatus;
}
// server.ts
export const toggleRoleStatus = ({ id }: TenantRoleToggleStatusRequest) =>
request({
url: `/tenant/roles/${id}/toggle-status`,
method: 'PUT',
headers: getAuthHeaders(),
});
```
- 页面级接口调用统一放在页面目录下的 `server.ts`,页面组件通过它调用接口,不要在组件内直接写请求实现。
- 跨页面复用的接口可放在 `src/api/` 下,按业务域拆分文件。
- 网络请求统一使用 `src/api/request.ts` 的 `request`(基于 axios),不要在页面或 `server.ts` 中直接使用 axios。
- **接口调用使用 `.then` 链式处理,不要使用 `await` + `try/catch`**:成功逻辑写在 `.then`,失败逻辑写在 `.catch`(错误已由 `request` 统一提示时可只保留空 `catch`),收尾逻辑写在 `.finally`。禁止写成 `try { const data = await xxx() } catch {}`。(存量代码在后续迭代中逐步迁移。)
```ts
// ✅ 使用 .then 链式处理
getOrgList({ page: 1, size: 20 })
.then((data) => {
setDataSource(data.records);
})
.catch(() => {
// 错误已由 request 统一提示
})
.finally(() => {
setLoading(false);
});
// ❌ 不要使用 await + try/catch
try {
const data = await getOrgList({ page: 1, size: 20 });
setDataSource(data.records);
} catch {
// ...
} finally {
setLoading(false);
}
```
- 开发环境通过 Vite 代理 `/proxy-api` 转发到 `VITE_API_PROXY_TARGET`(未配置时取 `VITE_API_BASE_URL`),因此浏览器端不会产生跨域请求;生产环境需后端配置 CORS,或使用同域反向代理。
- 泛型:`request(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,并由后端处理过期与刷新。
## 八、样式与可访问性
- 使用 SCSS,不在 JSX 中堆叠大量内联样式;动态颜色等少量样式可使用 `style`。
- 颜色、圆角优先使用已有的 CSS 变量或 Ant Design token。
- 交互元素保持可读的 hover / focus 状态;图标按钮补充 `aria-label`。
- 页面需兼容窄屏:表格使用 `scroll={{ x: ... }}`,左右布局使用栅格或 flex 换行。
## 九、部署注意
- 使用 BrowserRouter,生产环境需要配置 SPA fallback:所有前端路由回退到 `index.html`。
- 构建产物在 `dist/`。
- 开发环境的跨域由 Vite 代理 `/proxy-api` 处理;生产环境需要后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。
- 当前主包较大(Ant Design 全量依赖),后续可通过路由懒加载 / 手动分包优化。