# CRM Admin 项目开发指南(CLAUDE.md)
> Vite 6 + React 18 + TypeScript(strict)+ Ant Design 5 + `@ant-design/pro-components` + Zustand 5 + Axios + SCSS 的后台管理平台。
> BrowserRouter SPA,包含多级路由菜单、登录 / 退出与登录态校验;日期库为 Day.js(已配置中文 locale)。
## 一、常用命令
```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`。
## 二、目录与核心架构
```text
src/
├── 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`)通过 `` 渲染并默认跳转到第一个子页面;未匹配路由显示 404。
- **路由懒加载**:页面组件统一通过 `lazy(() => import('@/pages/...'))` 注册,`BasicLayout` 的内容区已提供 `` 边界;新增页面不要用静态 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. 统一使用箭头函数
组件、工具函数、Hook、事件处理函数都使用箭头函数,不新增 `function` 声明。
```tsx
const UserList = () => {
const handleSubmit = async () => {
// ...
};
return ;
};
export default UserList;
```
### 2. 优先使用现成组件
写功能前先找现成能力,不要重复造轮子:
- **优先使用 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` 上。
```tsx
// ✅ 选项来自接口
getLeaderOptions({ keyword: keyWords })}
/>
// ❌ 原生控件包在 Form.Item 中
```
- 不要手写弹窗、Toast、下拉菜单、分页、表格、表单校验等基础交互。不引入新的 UI 组件库;确需新组件时,先检查是否已有可复用实现,再考虑在 `src/components/` 下新增。
- 使用 Ant Design 5 的 API,不使用已废弃属性(如 Card 用 `variant="borderless"`,不用 `bordered={false}`)。
### 3. 独立组件放 `components/`,超长页面拆分
- 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 # 页面样式
└── components/
├── UserFormDrawer.tsx
└── UserDetailDrawer.tsx
```
## 四、新增页面流程
**新增页面统一使用 `/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/api/request.ts`**:统一通过其导出的 `request` 发起请求,不要在页面、组件或 `service.ts` 中直接使用 axios、`fetch` 或其他请求库。
2. **每个接口的 request 与 response 都必须定义 `interface`**:在 `service.ts` 中通过 `request` 显式标注泛型,禁止使用 `any` 或省略类型。
- 类型定义在页面目录的 `types.ts`(跨页面复用的放到 `src/api/` 对应业务域文件)。
- 命名:请求体 / 查询参数为 `<业务>Request`,响应为 `<业务>Response`,响应对应接口返回 `data` 的结构(后端为 `Result` 时取 `data` 部分)。
- 无请求参数时也要显式定义,例如 `type TenantRoleListRequest = Record`。
```ts
// types.ts
export interface TenantRoleToggleStatusRequest {
id: number;
}
export interface TenantRoleToggleStatusResponse {
id: number;
status: RoleStatus;
}
// service.ts
export const toggleRoleStatus = ({ id }: TenantRoleToggleStatusRequest) =>
request({
url: `/tenant/roles/${id}/toggle-status`,
method: 'PUT',
headers: getAuthHeaders(),
});
```
3. **接口调用必须使用 `.then` 链式处理,禁止 `await` + `try/catch`**:成功逻辑写在 `.then`,失败逻辑写在 `.catch`(错误已由 `request` 统一提示时可留空),收尾逻辑写在 `.finally`。
```ts
// ✅
getOrgList({ page: 1, size: 20 })
.then((data) => {
setDataSource(data.records);
})
.catch(() => {
// 错误已由 request 统一提示
})
.finally(() => {
setLoading(false);
});
// ❌
try {
const data = await getOrgList({ page: 1, size: 20 });
setDataSource(data.records);
} catch {
// ...
} finally {
setLoading(false);
}
```
4. **接口按作用域拆分文件**:
- 页面接口放在页面目录下的 `service.ts`,页面与组件通过它调用,不要在组件内直接写请求实现。
- 跨页面复用的公共接口按业务域拆分到 `src/api/` 下(如 `src/api/auth.ts`)。
补充说明:
- `request(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;页面级样式与页面同目录,使用 BEM 类名(如 `.user-cell__name`)。
- 交互元素保持可读的 hover / focus 状态;图标按钮补充 `aria-label`。
- 兼容窄屏:表格使用 `scroll={{ x: ... }}`,左右布局使用栅格或 flex 换行。
## 七、部署注意
- 使用 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。新增页面时保持懒加载即可,无需再调整拆包配置。