edit | blame | history | raw

CRM Admin 项目开发指南(CLAUDE.md)

Vite 6 + React 18 + TypeScript + Ant Design 5 + SCSS 的后台管理平台。
源码位于 src/,使用 BrowserRouter 的 SPA 架构,包含多级路由菜单、登录 / 退出与登录态校验。

一、常用命令

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)

三、目录结构

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

四、核心架构

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 声明。

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

  return <Table />;
};

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() 静态方法。
  1. 表格优先使用 ProTable:新增列表页时优先使用 @ant-design/pro-components 提供的 ProTable,其搜索、分页、工具栏等能力开箱即用,不要手写筛选表单 + Table 的组合。
  2. 表单内的选择控件优先使用 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 })}
    />

// ✅ 选项由父组件传入
<ProFormTreeSelect
name="parentId"
label="上级组织"
fieldProps={{ treeData: orgTree, showSearch: true }}
/>

// ❌ 不要这样写
<Form.Item name="leaderId" label="负责人">

</Form.Item>
`` 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,接口调用仍放在页面目录的 service.ts,由页面或组件按需调用;不要跨页面直接引用其他页面目录下的组件。
  • 单个页面文件(含 index.tsx)超过 300 行时必须拆分:按功能块抽取到 components/,columns.tsx、types.ts、service.ts、utils.ts 等继续单独维护。
  • 大于 300 行的复杂组件同样按功能拆分,不要在同一个文件里堆叠多个组件。
src/pages/system/users/
├── index.tsx                # 页面组件(尽量精简)
├── columns.tsx              # 表格列定义
├── types.ts
├── service.ts
├── utils.ts                 # 按需
├── Users.scss
└── components/
    ├── UserFormDrawer.tsx
    ├── UserDetailDrawer.tsx
    └── UserImportModal.tsx

4. Table columns 必须单独提取

所有表格的 columns 不得内联在 <Table /> 的 JSX 中,也不写在页面组件内部。统一提取到页面目录单独维护。

推荐结构:

src/pages/system/users/
├── index.tsx       # 页面组件
├── columns.tsx     # 表格列定义
└── Users.scss      # 页面样式
// 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>
    ),
  },
];
// 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 根据 <接口文档路径>,添加一个【页面名】页面,放到【分组名】目录下

仅修改现有页面(如调整某一列)等不涉及脚手架的场景,才直接改代码;即使手工创建页面,也须按以下流程执行。

  1. 在 src/pages/<业务域>/<页面>/ 下创建 index.tsx(有表格时增加 columns.tsx,独立组件放 components/,接口调用放在 service.ts,类型放 types.ts,有独立样式时增加同名 .scss)。
  2. 在 src/router/routeConfig.tsx 中增加路由配置:
  • 使用完整路径(如 /system/users)。
  • 配置 title、icon、element。
  • 需要分组时使用 children。
  1. 执行 npm run typecheck 验证(涉及样式或构建配置时再执行 npm run build)。

示例:

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

```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 统一提示时可只保留空 catch),收尾逻辑写在 .finally。

```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);
}
```

  1. 接口按作用域拆分文件:
  • 页面接口放在页面目录下的 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,并由后端处理过期与刷新。

八、样式与可访问性

  • 使用 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 全量依赖),后续可通过路由懒加载 / 手动分包优化。