edit | blame | history | raw

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)。

一、常用命令

npm install        # 安装依赖
npm run dev        # 启动开发服务器(http://localhost:5173)
npm run typecheck  # TypeScript 类型检查
npm run build      # 类型检查 + 生产构建
npm run preview    # 预览生产构建

改动代码后至少执行 npm run typecheck;涉及构建配置或样式时再执行 npm run build。

二、目录与核心架构

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)通过 <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. 统一使用箭头函数

组件、工具函数、Hook、事件处理函数都使用箭头函数,不新增 function 声明。

const UserList = () => {
  const handleSubmit = async () => {
    // ...
  };

  return <Table />;
};

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 上。
// ✅ 选项来自接口
<ProFormSelect
  name="leaderId"
  label="负责人"
  showSearch
  debounceTime={300}
  request={async ({ keyWords }) => getLeaderOptions({ keyword: keyWords })}
/>

// ❌ 原生控件包在 Form.Item 中
<Form.Item name="leaderId" label="负责人">
  <Select options={leaderOptions} />
</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 行的复杂组件同样按功能拆分,不要在同一个文件里堆叠多个组件。
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/):它会选择表格页 / 非表格页分支并整体套用模板,再完成代码规范、路由注册、文档更新与类型检查。不要手工复制模板或另起方式搭脚手架。

/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<TReq, TRes> 显式标注泛型,禁止使用 any 或省略类型。
  • 类型定义在页面目录的 types.ts(跨页面复用的放到 src/api/ 对应业务域文件)。
  • 命名:请求体 / 查询参数为 <业务>Request,响应为 <业务>Response,响应对应接口返回 data 的结构(后端为 Result<T> 时取 data 部分)。
  • 无请求参数时也要显式定义,例如 type TenantRoleListRequest = Record<string, never>。

```ts
// types.ts
export interface TenantRoleToggleStatusRequest {
id: number;
}
export interface TenantRoleToggleStatusResponse {
id: number;
status: RoleStatus;
}

// service.ts
export const toggleRoleStatus = ({ id }: TenantRoleToggleStatusRequest) =>
request<TenantRoleToggleStatusRequest, TenantRoleToggleStatusResponse>({
url: /tenant/roles/${id}/toggle-status,
method: 'PUT',
headers: getAuthHeaders(),
});
```

  1. 接口调用必须使用 .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);
}
```

  1. 接口按作用域拆分文件:
  • 页面接口放在页面目录下的 service.ts,页面与组件通过它调用,不要在组件内直接写请求实现。
  • 跨页面复用的公共接口按业务域拆分到 src/api/ 下(如 src/api/auth.ts)。

补充说明:

  • 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;页面级样式与页面同目录,使用 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。新增页面时保持懒加载即可,无需再调整拆包配置。