.claude/skills/new-page/SKILL.md
New file @@ -0,0 +1,139 @@ --- name: new-page description: 当用户要创建新页面、新建新页面、新建页面、新增页面、加一个页面或搭建页面脚手架时使用。先选择分支:表格/列表页使用 ProTable 模板,表单/详情/仪表盘等非表格页使用 Ant Design 模板;两个分支都会生成 server.ts 维护接口调用,并统一执行代码规范、路由注册、文档更新与类型检查。 --- # 新增页面 为 CRM 管理后台新增页面。先选择分支并套用该分支的唯一模板,再执行公共步骤。 ## 1. 选择分支 | 分支 | 适用场景 | 模板(整体复制) | | --- | --- | --- | | **表格页** | 列表、数据表格、带搜索/筛选/分页 | `templates/table-page/` | | **非表格页** | 表单、详情、仪表盘、设置页、纯展示页 | `templates/non-table-page/` | 无法从需求判断时向用户确认。两个分支都不重复维护路由等公共逻辑,完成后统一执行第 4 节。 同时确认: | 项目 | 说明 | 示例 | | --- | --- | --- | | 业务域 | 决定目录 `src/pages/<domain>/` | `system`、`business` | | 页面名 | 目录名与组件名 | `invoices` / `Invoices` | | 路由 | 完整路径,同时作为菜单 key | `/business/invoices` | | 菜单标题 / 图标 | 中文标题 + `@ant-design/icons` | 发票管理 / `FileTextOutlined` | | 数据来源 | 接口调用放在同目录 `server.ts`;演示阶段可先用本地 mock | `server.ts` | | 是否分组 | 是否放入现有的「系统管理 / 业务管理」分组 | `children` | 表格页额外确认:字段、列展示方式、可搜索字段、分页大小。 非表格页额外确认:表单字段与校验、详情字段、布局形式。 ## 2. 分支 A:表格页 模板目录:`templates/table-page/`,整体复制到 `src/pages/<domain>/<page>/`: ```text src/pages/<domain>/<page>/ ├── index.tsx # 页面组件(默认导出) ├── columns.tsx # ProColumns<T>[] 列定义 ├── types.ts # 记录类型与查询参数 ├── server.ts # 接口调用(列表查询、删除等) └── <Page>.scss # 页面级样式(按需新增) ``` 替换占位符:`Xxx` → 实体名(大驼峰),`xxx` → 目录/样式名(小写)。 该分支要求: - 使用 `ProTable`,不要手写「筛选表单 + Table」。 - `columns` 必须放在独立 `columns.tsx`,类型为 `ProColumns<T>[]`;需要回调时导出 `createXxxColumns(options)` 工厂函数。 - 页面中用 `useMemo` 缓存 columns,用 `useCallback` 包裹事件处理函数。 - 页面组件不直接写请求:列表查询、删除等接口调用统一放在同目录的 `server.ts`,组件只调用导出的方法。 - `server.ts` 使用 `@/api/request` 提供的 `request` 发送请求,不要直接使用 axios;泛型顺序为 `request<请求参数, 响应数据>`。 - 分页使用 `defaultPageSize`(不要用受控的 `pageSize`);需要切换每页条数时设置 `showSizeChanger: true`。 - 不需要右侧工具栏图标时用 `options={false}`。 - 表格补充 `scroll={{ x: ... }}` 以兼容窄屏。 ## 3. 分支 B:非表格页 模板文件:`templates/non-table-page/index.tsx`,复制到 `src/pages/<domain>/<page>/index.tsx`: ```text src/pages/<domain>/<page>/ ├── index.tsx # 页面组件(默认导出) ├── types.ts # 表单 / 详情类型 ├── server.ts # 接口调用(保存、详情查询等) └── <Page>.scss # 页面级样式(按需新增) ``` 表单页直接使用模板;详情页把 `Form` 换成 `Descriptions`,仪表盘使用 `Card` + `Statistic` + `Row`/`Col`。 该分支要求: - **不要创建 `columns.tsx`,不要使用 `ProTable`。** - 表单使用 Ant Design `Form` + `Form.Item` + `rules` 校验。 - 数据展示优先使用 `Card`、`Descriptions`、`Statistic`、`Tag` 等 Ant Design 组件。 - 页面局部状态使用 `useState`,派生数据使用 `useMemo`。 - 保存、详情查询等接口调用统一放在同目录的 `server.ts`,组件只调用导出的方法。 - `server.ts` 使用 `@/api/request` 提供的 `request` 发送请求,不要直接使用 axios。 ## 4. 公共步骤(两个分支都执行) ### 4.1 代码规范 - 页面级接口调用统一放在页面目录的 `server.ts`,不要写在组件内部。 - 统一使用 `src/api/request.ts` 的 `request`,不要直接使用 axios。默认会合并相同并发请求、自动提示错误,并只返回响应 `data`。 - 请求选项:`merge` 控制是否合并、`showError` 控制是否提示错误、`parse: false` 返回完整 `AxiosResponse`、`signal` 用于取消。 - 全部使用箭头函数,不新增 `function` 声明。 - 提示与确认框使用 `AntdApp.useApp()` 提供的 `message` / `modal`,不要用静态方法。 - 使用 Ant Design 5 API,不使用已废弃属性(如 Card 用 `variant="borderless"`)。 - 页面只负责内容区域,外层布局由 `BasicLayout` 提供。 - 页面级样式与页面同目录,使用 BEM 类名。 - 不添加权限相关代码:本项目已移除权限体系。 ### 4.2 注册路由 在 `src/router/routeConfig.tsx` 中增加配置,并补充组件与图标的 import: ```tsx { path: '/business/invoices', title: '发票管理', icon: <FileTextOutlined />, element: <Invoices />, }, ``` 放入现有分组时写在其 `children` 中;新增一级分组时保持与现有分组相同的结构。 ### 4.3 更新文档 如果新增的是演示页面,同步更新 `README.md` 的「演示页面」表格。不需要更新 `CLAUDE.md`。 ### 4.4 验证 ```bash npm run typecheck ``` 涉及样式或构建配置时再执行: ```bash npm run build ``` 完成后简要说明:分支类型、新增文件、路由地址,以及验证结果。 ### 4.5 完成检查清单 - [ ] 已选择正确分支并整体使用对应模板 - [ ] 表格页使用 ProTable,columns 独立在 `columns.tsx` - [ ] 非表格页未创建 `columns.tsx`,使用合适的 Ant Design 组件 - [ ] 两个分支均已创建 `server.ts`,组件内没有直接写请求 - [ ] 类型使用 `ProColumns<T>` / 明确接口,没有 `any` - [ ] 全部使用箭头函数 - [ ] 提示与确认框通过 `AntdApp.useApp()` 获取 - [ ] 路由已注册,菜单与面包屑可正常显示 - [ ] `npm run typecheck` 通过 .claude/skills/new-page/templates/non-table-page/index.tsx
New file @@ -0,0 +1,63 @@ import { useState } from 'react'; import { App as AntdApp, Button, Card, Form, Input, Space } from 'antd'; import { saveXxx } from './server'; import type { XxxFormValues } from './types'; import './Xxx.scss'; const Xxx = () => { const [submitting, setSubmitting] = useState(false); const [form] = Form.useForm<XxxFormValues>(); const { message } = AntdApp.useApp(); const handleFinish = async (values: XxxFormValues) => { setSubmitting(true); try { await saveXxx(values); message.success('保存成功'); form.resetFields(); } catch (error) { message.error(error instanceof Error ? error.message : '保存失败,请稍后重试'); } finally { setSubmitting(false); } }; return ( <div className="xxx-page"> <div className="page-header"> <div> <h1 className="page-header__title">页面标题</h1> <p className="page-header__desc">页面说明。</p> </div> </div> <Card variant="borderless"> <Form<XxxFormValues> form={form} layout="vertical" requiredMark={false} onFinish={handleFinish} > <Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]} > <Input placeholder="请输入名称" /> </Form.Item> <Form.Item> <Space> <Button type="primary" htmlType="submit" loading={submitting}> 保存 </Button> <Button onClick={() => form.resetFields()}>重置</Button> </Space> </Form.Item> </Form> </Card> </div> ); }; export default Xxx; .claude/skills/new-page/templates/non-table-page/server.ts
New file @@ -0,0 +1,12 @@ import { request } from '@/api/request'; import type { XxxFormValues } from './types'; // TODO: 替换为真实接口地址与字段。 /** 保存表单。 */ export const saveXxx = (values: XxxFormValues) => request<XxxFormValues, void>({ url: '/api/xxx', method: 'POST', data: values, }); .claude/skills/new-page/templates/non-table-page/types.ts
New file @@ -0,0 +1,3 @@ export interface XxxFormValues { name: string; } .claude/skills/new-page/templates/table-page/columns.tsx
New file @@ -0,0 +1,58 @@ import { Button, Space, Tag } from 'antd'; import type { ProColumns } from '@ant-design/pro-components'; import type { XxxRecord, XxxStatus } from './types'; const statusMeta: Record<XxxStatus, { text: string; color: string }> = { active: { text: '启用', color: 'success' }, disabled: { text: '停用', color: 'default' }, }; export const createXxxColumns = (options: { onEdit?: (record: XxxRecord) => void; onDelete: (record: XxxRecord) => void; }): ProColumns<XxxRecord>[] => [ { title: '名称', dataIndex: 'name', width: 240, fieldProps: { placeholder: '请输入名称' }, }, { title: '状态', dataIndex: 'status', width: 110, valueEnum: { active: { text: '启用' }, disabled: { text: '停用' }, }, render: (_value, record) => ( <Tag color={statusMeta[record.status].color} bordered={false}> {statusMeta[record.status].text} </Tag> ), }, { title: '创建时间', dataIndex: 'createdAt', width: 170, search: false, }, { title: '操作', key: 'action', valueType: 'option', width: 140, render: (_value, record) => ( <Space size={0}> {options.onEdit && ( <Button type="link" size="small" onClick={() => options.onEdit?.(record)}> 编辑 </Button> )} <Button type="link" size="small" danger onClick={() => options.onDelete(record)}> 删除 </Button> </Space> ), }, ]; .claude/skills/new-page/templates/table-page/index.tsx
New file @@ -0,0 +1,77 @@ import { useCallback, useMemo, useRef } from 'react'; import { PlusOutlined } from '@ant-design/icons'; import { ProTable } from '@ant-design/pro-components'; import type { ActionType } from '@ant-design/pro-components'; import { App as AntdApp, Button } from 'antd'; import { createXxxColumns } from './columns'; import { deleteXxx, getXxxList } from './server'; import type { XxxQuery, XxxRecord } from './types'; import './Xxx.scss'; const Xxx = () => { const actionRef = useRef<ActionType>(); const { message, modal } = AntdApp.useApp(); const handleDelete = useCallback( (record: XxxRecord) => { modal.confirm({ title: '删除记录', content: `确定删除「${record.name}」吗?删除后不可恢复。`, okText: '删除', cancelText: '取消', okButtonProps: { danger: true }, onOk: async () => { await deleteXxx(record.key); message.success('已删除'); actionRef.current?.reload(); }, }); }, [message, modal], ); const columns = useMemo( () => createXxxColumns({ onDelete: handleDelete }), [handleDelete], ); return ( <div className="xxx-page"> <div className="page-header"> <div> <h1 className="page-header__title">页面标题</h1> <p className="page-header__desc">页面说明。</p> </div> </div> <ProTable<XxxRecord, XxxQuery> rowKey="key" actionRef={actionRef} columns={columns} cardProps={{ className: 'xxx-table-card' }} search={{ labelWidth: 'auto', defaultCollapsed: false }} scroll={{ x: 900 }} options={false} pagination={{ defaultPageSize: 10, showSizeChanger: true, hideOnSinglePage: false, showTotal: (total) => `共 ${total} 条`, }} toolBarRender={() => [ <Button key="create" type="primary" icon={<PlusOutlined />} onClick={() => message.info('TODO: 打开新增表单')} > 新增 </Button>, ]} request={(params) => getXxxList(params)} /> </div> ); }; export default Xxx; .claude/skills/new-page/templates/table-page/server.ts
New file @@ -0,0 +1,19 @@ import { request } from '@/api/request'; import type { XxxListParams, XxxListResult } from './types'; // TODO: 替换为真实接口地址与字段。 /** 查询列表。 */ export const getXxxList = (params: XxxListParams) => request<XxxListParams, XxxListResult>({ url: '/api/xxx', method: 'GET', params, }); /** 删除记录。 */ export const deleteXxx = (key: string) => request<void, void>({ url: `/api/xxx/${key}`, method: 'DELETE', }); .claude/skills/new-page/templates/table-page/types.ts
New file @@ -0,0 +1,25 @@ export type XxxStatus = 'active' | 'disabled'; export interface XxxRecord { key: string; name: string; status: XxxStatus; createdAt: string; } /** ProTable 搜索参数。 */ export interface XxxQuery { name?: string; status?: XxxStatus; } export interface XxxListParams extends XxxQuery { current?: number; pageSize?: number; } export interface XxxListResult { data: XxxRecord[]; total: number; success: boolean; } .env
New file @@ -0,0 +1,3 @@ VITE_API_BASE_URL=http://121.41.54.56:9090/ # 开发环境代理目标(Vite 将 /proxy-api 转发到该地址) VITE_API_PROXY_TARGET=http://121.41.54.56:9090 CLAUDE.md
@@ -23,6 +23,7 @@ - **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) @@ -30,7 +31,8 @@ ```text src/ ├── api/ # 接口层;当前只有模拟登录 auth.ts ├── api/ # 接口层:request.ts(axios 封装)与登录 auth.ts ├── components/ # 通用组件:AntdAppBridge(message 注入) ├── layouts/ # BasicLayout:侧边栏、头部、内容区、面包屑 ├── pages/ # 页面,按业务域分目录 │ ├── login/ @@ -38,6 +40,7 @@ │ ├── system/users/ │ ├── business/{orders,customers}/ │ └── error/NotFound.tsx │ # 页面目录:index.tsx、columns.tsx、types.ts、server.ts、同名 .scss(按需) ├── router/ │ ├── routeConfig.tsx # 路由 + 菜单唯一数据源(含页面组件) │ ├── menu.tsx # 菜单过滤与 antd Menu 结构生成 @@ -45,6 +48,7 @@ ├── store/ # Zustand 登录态 ├── styles/ # 全局 SCSS、通用页面样式 ├── types/ # 全局类型 ├── utils/ # 工具:feedback.ts(全局错误提示) └── main.tsx # ConfigProvider + AntdApp + BrowserRouter ``` @@ -62,10 +66,8 @@ ### 2. 登录态 - `src/store/auth.ts`:Zustand store,持久化 `token` 与 `user`。 - 登录 / 退出:`src/api/auth.ts` 提供模拟登录。 - 演示账号: - `admin / admin123`:超级管理员。 - `user / user123`:普通运营账号。 - 登录 / 退出:`src/api/auth.ts` 调用 `POST /auth/tenant-login`,参数为 `{ phone, password, tenantCode }`。 - 接口基础地址由 `.env` 中的 `VITE_API_BASE_URL` 配置。 - 未登录访问受保护页面会跳转 `/login`,登录成功后返回原地址。 ### 3. 布局与样式 @@ -180,7 +182,7 @@ ## 六、新增页面流程 1. 在 `src/pages/<业务域>/<页面>/` 下创建 `index.tsx`(有表格时增加 `columns.tsx`,有独立样式时增加同名 `.scss`)。 1. 在 `src/pages/<业务域>/<页面>/` 下创建 `index.tsx`(有表格时增加 `columns.tsx`,接口调用放在 `server.ts`,类型放 `types.ts`,有独立样式时增加同名 `.scss`)。 2. 在 `src/router/routeConfig.tsx` 中增加路由配置: - 使用完整路径(如 `/system/users`)。 - 配置 `title`、`icon`、`element`。 @@ -200,8 +202,16 @@ ## 七、接口与状态约定 - 所有请求统一放在 `src/api/` 下,按业务域拆分文件;不要在页面内直接写请求实现。 - 当前 `src/api/auth.ts` 是前端模拟接口。接入真实后端时替换为实际请求,并保留登录态写入 store 的流程。 - 页面级接口调用统一放在页面目录下的 `server.ts`,页面组件通过它调用接口,不要在组件内直接写请求实现。 - 跨页面复用的接口可放在 `src/api/` 下,按业务域拆分文件。 - 网络请求统一使用 `src/api/request.ts` 的 `request`(基于 axios),不要在页面或 `server.ts` 中直接使用 axios。 - 开发环境通过 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,并由后端处理过期与刷新。 @@ -216,4 +226,5 @@ - 使用 BrowserRouter,生产环境需要配置 SPA fallback:所有前端路由回退到 `index.html`。 - 构建产物在 `dist/`。 - 开发环境的跨域由 Vite 代理 `/proxy-api` 处理;生产环境需要后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。 - 当前主包较大(Ant Design 全量依赖),后续可通过路由懒加载 / 手动分包优化。 README.md
@@ -20,20 +20,26 @@ npm run typecheck # 仅做 TypeScript 类型检查 ``` ## 演示账号 ## 登录接口 | 账号 | 密码 | 说明 | | ------- | ---------- | ------------ | | `admin` | `admin123` | 超级管理员 | | `user` | `user123` | 普通运营账号 | 登录使用 `POST /auth/tenant-login`: 登录页提供了一键填充演示账号的按钮。 ```json { "phone": "string", "password": "string", "tenantCode": "string" } ``` - **开发环境**:请求走 Vite 代理 `/proxy-api`,代理目标由 `.env` 中的 `VITE_API_PROXY_TARGET` 配置(未配置时取 `VITE_API_BASE_URL`),浏览器端不会产生跨域请求。 - **生产环境**:请求直接使用 `VITE_API_BASE_URL`;需要在后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。 ## 功能说明 ### 1. 登录 / 退出 - 登录页:`src/pages/login/index.tsx` - 登录页:`src/pages/login/index.tsx`,调用 `POST /auth/tenant-login` - 登录状态使用 Zustand + `localStorage` 持久化:`src/store/auth.ts` - 未登录访问受保护页面时,会跳转登录页并记录原地址,登录成功后自动返回 - 顶部用户菜单支持退出登录(带二次确认),退出后回到登录页 @@ -68,7 +74,28 @@ - 受保护路由统一包裹登录校验,未登录时跳转 `/login` 并带上 `state.from` - 登录成功后自动返回原地址 ### 4. 演示页面 ### 4. 网络请求 统一使用 `src/api/request.ts` 的 `request`(基于 axios): ```ts const result = await request<{ keyword: string }, PageResult<User>>({ url: '/api/users', method: 'GET', params: { keyword }, }); ``` - 泛型 `request<TReq, TRes>` 分别表示请求参数/请求体与响应 `data` 的类型。 - 默认只返回响应 `data`;传 `parse: false` 返回完整 `AxiosResponse`。 - 默认自动提示错误,`showError: false` 可关闭。 - 默认合并方法、URL、参数与请求体都相同的并发请求;`merge: false` 时每次都单独发送。 - 通过 `signal: AbortSignal` 取消调用方的请求。 - 失败时抛出 `ApiError`,保留 `status`、`data`、`response`、`config` 与原始错误。 页面级接口调用放在页面目录的 `server.ts`,组件通过它调用请求。 ### 5. 演示页面 | 页面 | 路由 | 说明 | | -------- | ----------------------- | ---------------------------------- | @@ -82,7 +109,8 @@ ```text src/ ├── api/ # 模拟登录接口(替换为真实后端) ├── api/ # request.ts(axios 封装)与登录接口 ├── components/ # 通用组件(如 AntdAppBridge) ├── layouts/ # 后台整体布局(侧边栏、头部、内容区) ├── pages/ # 业务页面 │ ├── login/ @@ -96,6 +124,7 @@ │ └── index.tsx # React Router 路由生成 + 登录校验 ├── store/ # Zustand 登录状态 ├── styles/ # 全局 SCSS ├── utils/ # feedback.ts(全局错误提示) ├── types/ └── main.tsx ``` @@ -109,10 +138,6 @@ ## 安全说明 本项目中的登录接口与账号数据均为 **前端模拟**,仅用于演示。 接入真实后端时: - 登录、菜单、用户信息应由后端接口返回; - 登录、菜单、用户信息由后端接口返回; - 所有敏感接口必须在服务端校验身份与权限; - Token 建议使用 HttpOnly Cookie 或安全的存储方案,并支持过期 / 刷新机制。 package-lock.json
@@ -1282,6 +1282,14 @@ "object-assign": "4.x" } }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "requires": { "debug": "4" } }, "antd": { "version": "5.29.3", "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", @@ -1338,6 +1346,22 @@ "throttle-debounce": "^5.0.2" } }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "requires": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "baseline-browser-mapping": { "version": "2.11.29", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", @@ -1355,6 +1379,15 @@ "electron-to-chromium": "^1.5.439", "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" } }, "call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "caniuse-lite": { @@ -1376,6 +1409,14 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } }, "compute-scroll-into-view": { "version": "3.1.1", @@ -1410,10 +1451,14 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { "ms": "^2.1.3" } }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "dequal": { "version": "2.0.3", @@ -1427,11 +1472,50 @@ "dev": true, "optional": true }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "electron-to-chromium": { "version": "1.5.459", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz", "integrity": "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==", "dev": true }, "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "requires": { "es-errors": "^1.3.0" } }, "es-set-tostringtag": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "requires": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "esbuild": { "version": "0.25.12", @@ -1479,6 +1563,23 @@ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true }, "follow-redirects": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==" }, "form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1486,11 +1587,77 @@ "dev": true, "optional": true }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { "has-symbols": "^1.0.3" } }, "hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "requires": { "function-bind": "^1.1.2" } }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "requires": { "agent-base": "6", "debug": "4" } }, "immutable": { "version": "5.1.9", @@ -1567,11 +1734,28 @@ "yallist": "^3.0.2" } }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { "mime-db": "1.52.0" } }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nanoid": { "version": "3.3.20", @@ -1642,6 +1826,11 @@ } } }, "proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==" }, "rc-cascader": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", package.json
@@ -13,6 +13,7 @@ "@ant-design/icons": "^5.5.2", "@ant-design/pro-components": "^2.8.10", "antd": "^5.22.5", "axios": "^1.20.0", "dayjs": "^1.11.13", "react": "^18.3.1", "react-dom": "^18.3.1", src/api/api-tenant.md
New file @@ -0,0 +1,1502 @@ # SaaS 系统管理服务 API(租户版) > **版本**: 1.0.0 > **描述**: 多租户 SaaS 平台 — 用户、角色、权限、租户管理接口 > **License**: Apache 2.0 > **Contact**: SaaS Platform Team --- ## 基本信息 | 项目 | 说明 | |------|------| | Base URL | `http://121.41.54.56:9090` | | OpenAPI | 3.0.1 | | 认证方式 | Bearer JWT(Header: `Authorization: Bearer <token>`) | | 文档源 | `/tenant/v3/api-docs` | | Swagger UI | http://121.41.54.56:9090/webjars/swagger-ui/index.html?urls.primaryName=%E7%A7%9F%E6%88%B7%E7%AE%A1%E7%90%86%E6%9C%8D%E5%8A%A1 | --- ## 通用响应结构 几乎所有接口返回统一包装: ```json { "code": 0, "message": "success", "data": { ... }, "timestamp": 1690000000000, "success": true } ``` | 字段 | 类型 | 说明 | |------|------|------| | `code` | integer | 业务状态码,通常 `0` 表示成功 | | `message` | string | 提示信息 | | `data` | object | 业务数据(见各接口返回模型) | | `timestamp` | long | 毫秒时间戳 | | `success` | boolean | 是否成功 | --- ## 接口列表 ### 租户角色管理 #### `GET` `/tenant/roles` **当前租户角色列表(含预设角色)** - OperationId: `list` **响应 (Response)** **`200` OK** 返回包装类型: [`ResultListRoleVO`](#model-resultlistrolevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `array\<RoleVO\>` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/roles/{id}` **获取角色详情** - OperationId: `getById` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 角色 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultRoleVO`](#model-resultrolevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`RoleVO`](#model-rolevo) > 角色响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `roleName` | string | 否 | 角色名称 | 租户管理员 | | `roleCode` | string | 否 | 角色编码(预置角色常量如 TENANT_ADMIN/PLATFORM_ADMIN;自建角色为自定义) | TENANT_ADMIN | | `description` | string | 否 | 角色描述 | 企业管理者 | | `roleType` | string<br>枚举: PRESET / CUSTOM | 否 | 角色类型(PRESET 预置,全租户共享 / CUSTOM 自建,本租户) | PRESET | | `status` | string<br>枚举: ENABLED / DISABLED / DELETED | 否 | 状态(ENABLED 启用 / DISABLED 停用 / DELETED 已删除) | ENABLED | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `PUT` `/tenant/roles/{id}/toggle-status` **启用/停用角色** - OperationId: `toggleStatus` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 角色 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultVoid`](#model-resultvoid) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `object` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) ### 组织管理 #### `POST` `/tenant/organizations` **创建组织** - OperationId: `create` **请求体 (Request Body)** Content-Type: `application/json` 类型: [`OrgCreateDTO`](#model-orgcreatedto) > 创建组织请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `orgName` | string | 是 | 组织名称 | 华东分公司 | | `orgType` | string<br>枚举: HQ / BRANCH / DEALER / AGENT / DEPT / TEAM | 否 | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队);默认 DEPT | BRANCH | | `parentId` | integer (int64) | 否 | 上级组织 id;根组织留空 | | | `leaderId` | integer (int64) | 否 | 负责人员工 id;可空 | | | `region` | string | 否 | 所在地区;可空 | 上海 | | `sortOrder` | integer (int32) | 否 | 同级内显示排序(数字越小越靠前);可空 | 1 | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultOrgVO`](#model-resultorgvo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`OrgVO`](#model-orgvo) > 组织响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `orgName` | string | 否 | 组织名称 | 华东分公司 | | `orgType` | string<br>枚举: HQ / BRANCH / DEALER / AGENT / DEPT / TEAM | 否 | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队) | BRANCH | | `parentId` | integer (int64) | 否 | 上级组织 id(根组织为 null) | | | `orgPath` | string | 否 | 组织路径(从根到本节点的全路径,如 /默认租户/华东分公司) | | | `leaderId` | integer (int64) | 否 | 负责人员工 id(可空) | | | `region` | string | 否 | 所在地区 | 上海 | | `sortOrder` | integer (int32) | 否 | 同级内显示排序(数字越小越靠前) | | | `status` | boolean | 否 | 启用状态(true 启用 / false 停用) | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/organizations/tree` **获取组织树** - OperationId: `getTree` **响应 (Response)** **`200` OK** 返回包装类型: [`ResultListOrgVO`](#model-resultlistorgvo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `array\<OrgVO\>` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `PUT` `/tenant/organizations/{id}` **更新组织** - OperationId: `update` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 组织 id | **请求体 (Request Body)** Content-Type: `application/json` 类型: [`OrgCreateDTO`](#model-orgcreatedto) > 创建组织请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `orgName` | string | 是 | 组织名称 | 华东分公司 | | `orgType` | string<br>枚举: HQ / BRANCH / DEALER / AGENT / DEPT / TEAM | 否 | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队);默认 DEPT | BRANCH | | `parentId` | integer (int64) | 否 | 上级组织 id;根组织留空 | | | `leaderId` | integer (int64) | 否 | 负责人员工 id;可空 | | | `region` | string | 否 | 所在地区;可空 | 上海 | | `sortOrder` | integer (int32) | 否 | 同级内显示排序(数字越小越靠前);可空 | 1 | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultOrgVO`](#model-resultorgvo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`OrgVO`](#model-orgvo) > 组织响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `orgName` | string | 否 | 组织名称 | 华东分公司 | | `orgType` | string<br>枚举: HQ / BRANCH / DEALER / AGENT / DEPT / TEAM | 否 | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队) | BRANCH | | `parentId` | integer (int64) | 否 | 上级组织 id(根组织为 null) | | | `orgPath` | string | 否 | 组织路径(从根到本节点的全路径,如 /默认租户/华东分公司) | | | `leaderId` | integer (int64) | 否 | 负责人员工 id(可空) | | | `region` | string | 否 | 所在地区 | 上海 | | `sortOrder` | integer (int32) | 否 | 同级内显示排序(数字越小越靠前) | | | `status` | boolean | 否 | 启用状态(true 启用 / false 停用) | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `DELETE` `/tenant/organizations/{id}` **删除组织** - OperationId: `delete` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 组织 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultVoid`](#model-resultvoid) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `object` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `PUT` `/tenant/organizations/{id}/toggle-status` **启用/停用组织** - OperationId: `toggleStatus_1` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 组织 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultVoid`](#model-resultvoid) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `object` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) ### 员工管理 #### `GET` `/tenant/employees` **员工分页列表** - OperationId: `list_1` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `query` | query | EmployeePageQuery | 是 | | | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultPageResultEmployeeVO`](#model-resultpageresultemployeevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`PageResultEmployeeVO`](#model-pageresultemployeevo) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int64) | 否 | | | | `size` | integer (int64) | 否 | | | | `total` | integer (int64) | 否 | | | | `totalPages` | integer (int64) | 否 | | | | `records` | array\<EmployeeVO\> | 否 | | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `POST` `/tenant/employees` **创建员工** - OperationId: `create_1` **请求体 (Request Body)** Content-Type: `application/json` 类型: [`EmployeeCreateDTO`](#model-employeecreatedto) > 创建员工请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `name` | string | 是 | 员工姓名 | 张三 | | `phone` | string | 是 | 登录/联系手机号(同租户内唯一建议) | 13800138000 | | `orgId` | integer (int64) | 是 | 所属组织 id | 1 | | `roleId` | integer (int64) | 是 | 角色 id(关联 crm_role.id;2 为 TENANT_ADMIN 预置角色) | 2 | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `avatarUrl` | string | 否 | 头像 URL | | | `holdLimit` | integer (int32) | 否 | 客户持有上限(可负责客户数) | 50 | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultEmployeeVO`](#model-resultemployeevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`EmployeeVO`](#model-employeevo) > 员工响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `name` | string | 否 | 员工姓名 | 张三 | | `phone` | string | 否 | 登录/联系手机号 | 13800138000 | | `orgId` | integer (int64) | 否 | 所属组织 id | | | `roleId` | integer (int64) | 否 | 角色 id(关联 crm_role.id;为 2 即 TENANT_ADMIN 预置角色) | | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `avatarUrl` | string | 否 | 头像 URL | | | `status` | string<br>枚举: ACTIVE / DISABLED | 否 | 状态(ACTIVE 在职 / DISABLED 停用) | ACTIVE | | `holdLimit` | integer (int32) | 否 | 客户持有上限(可负责的客户数) | | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | | `hireDate` | string (date) | 否 | 入职日期 | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/employees/by-org/{orgId}` **按组织查询员工列表** - OperationId: `getByOrgId` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `orgId` | path | integer (int64) | 是 | | 组织 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultListEmployeeVO`](#model-resultlistemployeevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `array\<EmployeeVO\>` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/employees/by-role/{roleId}` **按角色查询员工列表** - OperationId: `getByRoleId` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `roleId` | path | integer (int64) | 是 | | 角色 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultListEmployeeVO`](#model-resultlistemployeevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `array\<EmployeeVO\>` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/employees/count-by-role/{roleId}` **统计角色关联的员工数** - OperationId: `countByRoleId` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `roleId` | path | integer (int64) | 是 | | 角色 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultLong`](#model-resultlong) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `integer (int64)` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/employees/export` **导出员工(Excel)** - OperationId: `exportEmployees` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `orgId` | query | integer (int64) | 否 | | 按组织 id 过滤;留空=全部 | **响应 (Response)** - `200` OK - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `POST` `/tenant/employees/import` **批量导入员工(Excel)** - OperationId: `importEmployees` **请求体 (Request Body)** > ⚠️ 规范中可能标注为 `application/json` + binary,实际调用请使用 `multipart/form-data` 上传 Excel,字段名 `file`。 Content-Type: `multipart/form-data` | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `file` | string (binary) | 是 | Excel 文件(multipart 字段名 file) | | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultInteger`](#model-resultinteger) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `integer (int32)` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/employees/{id}` **获取员工详情** - OperationId: `getById_1` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 员工 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultEmployeeVO`](#model-resultemployeevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`EmployeeVO`](#model-employeevo) > 员工响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `name` | string | 否 | 员工姓名 | 张三 | | `phone` | string | 否 | 登录/联系手机号 | 13800138000 | | `orgId` | integer (int64) | 否 | 所属组织 id | | | `roleId` | integer (int64) | 否 | 角色 id(关联 crm_role.id;为 2 即 TENANT_ADMIN 预置角色) | | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `avatarUrl` | string | 否 | 头像 URL | | | `status` | string<br>枚举: ACTIVE / DISABLED | 否 | 状态(ACTIVE 在职 / DISABLED 停用) | ACTIVE | | `holdLimit` | integer (int32) | 否 | 客户持有上限(可负责的客户数) | | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | | `hireDate` | string (date) | 否 | 入职日期 | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `PUT` `/tenant/employees/{id}` **更新员工** - OperationId: `update_1` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 员工 id | **请求体 (Request Body)** Content-Type: `application/json` 类型: [`EmployeeUpdateDTO`](#model-employeeupdatedto) > 编辑员工请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `name` | string | 否 | 员工姓名 | 张三 | | `phone` | string | 否 | 登录/联系手机号 | 13800138000 | | `orgId` | integer (int64) | 否 | 所属组织 id | | | `roleId` | integer (int64) | 否 | 角色 id(关联 crm_role.id) | | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `holdLimit` | integer (int32) | 否 | 客户持有上限 | 50 | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultEmployeeVO`](#model-resultemployeevo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`EmployeeVO`](#model-employeevo) > 员工响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `name` | string | 否 | 员工姓名 | 张三 | | `phone` | string | 否 | 登录/联系手机号 | 13800138000 | | `orgId` | integer (int64) | 否 | 所属组织 id | | | `roleId` | integer (int64) | 否 | 角色 id(关联 crm_role.id;为 2 即 TENANT_ADMIN 预置角色) | | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `avatarUrl` | string | 否 | 头像 URL | | | `status` | string<br>枚举: ACTIVE / DISABLED | 否 | 状态(ACTIVE 在职 / DISABLED 停用) | ACTIVE | | `holdLimit` | integer (int32) | 否 | 客户持有上限(可负责的客户数) | | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | | `hireDate` | string (date) | 否 | 入职日期 | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `DELETE` `/tenant/employees/{id}` **删除员工** - OperationId: `delete_1` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 员工 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultVoid`](#model-resultvoid) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: `object` - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) ### 客户管理(后台 · 只读) #### `GET` `/tenant/customers` **本租户客户列表(只读)** - OperationId: `list_2` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `query` | query | TenantCustomerPageQuery | 是 | | | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultPageResultCustomerVO`](#model-resultpageresultcustomervo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`PageResultCustomerVO`](#model-pageresultcustomervo) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int64) | 否 | | | | `size` | integer (int64) | 否 | | | | `total` | integer (int64) | 否 | | | | `totalPages` | integer (int64) | 否 | | | | `records` | array\<CustomerVO\> | 否 | | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/customers/{id}` **客户详情(只读)** - OperationId: `detail` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 客户 id | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultCustomerVO`](#model-resultcustomervo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`CustomerVO`](#model-customervo) > 客户响应体(后台 · 只读) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `name` | string | 否 | 客户名称(企业名或个人名) | 上海特斯拉 | | `customerType` | string<br>枚举: ENTERPRISE / PERSONAL | 否 | 客户类型(ENTERPRISE 企业 / PERSONAL 个人) | ENTERPRISE | | `industry` | string | 否 | 所属行业 | 汽车行业 | | `region` | string | 否 | 所在地区 | 上海 | | `contactName` | string | 否 | 主要联系人姓名 | 张经理 | | `contactPhone` | string | 否 | 主要联系电话(手机) | 13800138000 | | `contactPosition` | string | 否 | 主要联系人职位 | 采购经理 | | `contactTel` | string | 否 | 备用/座机电话 | 021-12345678 | | `email` | string | 否 | 客户邮箱 | contact@example.com | | `address` | string | 否 | 客户地址 | 上海市浦东新区 | | `source` | string<br>枚举: MINI_PROGRAM / SALES_INPUT / EXHIBITION / REFERRAL | 否 | 客户来源(小程序注册/销售录入/展会/转介绍) | SALES_INPUT | | `level` | string<br>枚举: VIP / A / B / C | 否 | 客户级别(VIP/A/B/C) | VIP | | `tags` | string | 否 | 标签(逗号分隔的自由文本) | 重点,高意向 | | `orgId` | integer (int64) | 否 | 所属组织 id(可空) | | | `ownerId` | integer (int64) | 否 | 客户归属人(销售员)员工 id | | | `stage` | string<br>枚举: NEW_LEAD / PENDING_ASSIGN / CONTACTING / NEEDS_COMM / QUOTING / NEGOTIATING / DEAL / LOST | 否 | 当前客户阶段(NEW_LEAD新线索 / PENDING_ASSIGN待分配 / CONTACTING接洽中 / NEEDS_COMM需求沟通 / QUOTING报价中 / NEGOTIATING议价中 / DEAL成交 / LOST流失) | NEW_LEAD | | `lastFollowAt` | string (date-time) | 否 | 最近一次跟进时间 | | | `nextFollowAt` | string (date-time) | 否 | 下次计划跟进时间 | | | `dealAt` | string (date-time) | 否 | 成交时间(阶段到 DEAL 时写入) | | | `lostAt` | string (date-time) | 否 | 流失时间(阶段到 LOST 时写入) | | | `lostReason` | string | 否 | 流失原因 | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) #### `GET` `/tenant/customers/{id}/follow-ups` **客户跟进历史(只读)** - OperationId: `followUps` **请求参数** | 名称 | 位置 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|------|--------|------| | `id` | path | integer (int64) | 是 | | 客户 id | | `query` | query | FollowUpPageQuery | 是 | | | **响应 (Response)** **`200` OK** 返回包装类型: [`ResultPageResultFollowUpVO`](#model-resultpageresultfollowupvo) **统一包装字段:** > 统一响应包装字段 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | **`data` 业务字段:** 类型: [`PageResultFollowUpVO`](#model-pageresultfollowupvo) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int64) | 否 | | | | `size` | integer (int64) | 否 | | | | `total` | integer (int64) | 否 | | | | `totalPages` | integer (int64) | 否 | | | | `records` | array\<FollowUpVO\> | 否 | | | - `400` Bad Request → [`ResultVoid`](#model-resultvoid) - `405` Method Not Allowed → [`ResultVoid`](#model-resultvoid) - `500` Internal Server Error → [`ResultVoid`](#model-resultvoid) --- ## 数据模型完整参考 > 接口章节已内联展示 Request / Response 字段说明,此处便于全局查阅。 ### EmployeeCreateDTO <a id="model-employeecreatedto"></a> > 创建员工请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `name` | string | 是 | 员工姓名 | 张三 | | `phone` | string | 是 | 登录/联系手机号(同租户内唯一建议) | 13800138000 | | `orgId` | integer (int64) | 是 | 所属组织 id | 1 | | `roleId` | integer (int64) | 是 | 角色 id(关联 crm_role.id;2 为 TENANT_ADMIN 预置角色) | 2 | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `avatarUrl` | string | 否 | 头像 URL | | | `holdLimit` | integer (int32) | 否 | 客户持有上限(可负责客户数) | 50 | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | ### EmployeePageQuery <a id="model-employeepagequery"></a> > 员工分页查询条件 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int32) | 否 | | | | `size` | integer (int32) | 否 | | | | `sortBy` | string | 否 | | | | `sortDir` | string | 否 | | | | `orgId` | integer (int64) | 否 | 按所属组织 id 过滤;留空=不按组织过滤 | | | `roleId` | integer (int64) | 否 | 按角色 id 过滤;留空=不按角色过滤 | | | `status` | string<br>枚举: ACTIVE / DISABLED | 否 | 按员工状态过滤(ACTIVE 在职 / DISABLED 停用);留空=全部 | | | `keyword` | string | 否 | 关键词(模糊匹配员工姓名/手机号等);可空 | 张三 | | `offset` | integer (int32) | 否 | | | ### EmployeeUpdateDTO <a id="model-employeeupdatedto"></a> > 编辑员工请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `name` | string | 否 | 员工姓名 | 张三 | | `phone` | string | 否 | 登录/联系手机号 | 13800138000 | | `orgId` | integer (int64) | 否 | 所属组织 id | | | `roleId` | integer (int64) | 否 | 角色 id(关联 crm_role.id) | | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `holdLimit` | integer (int32) | 否 | 客户持有上限 | 50 | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | ### FollowUpPageQuery <a id="model-followuppagequery"></a> > 客户跟进历史分页查询条件(后台只读) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int32) | 否 | | | | `size` | integer (int32) | 否 | | | | `sortBy` | string | 否 | | | | `sortDir` | string | 否 | | | | `offset` | integer (int32) | 否 | | | ### OrgCreateDTO <a id="model-orgcreatedto"></a> > 创建组织请求体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `orgName` | string | 是 | 组织名称 | 华东分公司 | | `orgType` | string<br>枚举: HQ / BRANCH / DEALER / AGENT / DEPT / TEAM | 否 | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队);默认 DEPT | BRANCH | | `parentId` | integer (int64) | 否 | 上级组织 id;根组织留空 | | | `leaderId` | integer (int64) | 否 | 负责人员工 id;可空 | | | `region` | string | 否 | 所在地区;可空 | 上海 | | `sortOrder` | integer (int32) | 否 | 同级内显示排序(数字越小越靠前);可空 | 1 | ### TenantCustomerPageQuery <a id="model-tenantcustomerpagequery"></a> > 本租户客户分页查询条件(后台只读) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int32) | 否 | | | | `size` | integer (int32) | 否 | | | | `sortBy` | string | 否 | | | | `sortDir` | string | 否 | | | | `ownerId` | integer (int64) | 否 | 按客户归属人(销售员)员工 id 过滤;留空=全部 | | | `offset` | integer (int32) | 否 | | | ### CustomerVO <a id="model-customervo"></a> > 客户响应体(后台 · 只读) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `name` | string | 否 | 客户名称(企业名或个人名) | 上海特斯拉 | | `customerType` | string<br>枚举: ENTERPRISE / PERSONAL | 否 | 客户类型(ENTERPRISE 企业 / PERSONAL 个人) | ENTERPRISE | | `industry` | string | 否 | 所属行业 | 汽车行业 | | `region` | string | 否 | 所在地区 | 上海 | | `contactName` | string | 否 | 主要联系人姓名 | 张经理 | | `contactPhone` | string | 否 | 主要联系电话(手机) | 13800138000 | | `contactPosition` | string | 否 | 主要联系人职位 | 采购经理 | | `contactTel` | string | 否 | 备用/座机电话 | 021-12345678 | | `email` | string | 否 | 客户邮箱 | contact@example.com | | `address` | string | 否 | 客户地址 | 上海市浦东新区 | | `source` | string<br>枚举: MINI_PROGRAM / SALES_INPUT / EXHIBITION / REFERRAL | 否 | 客户来源(小程序注册/销售录入/展会/转介绍) | SALES_INPUT | | `level` | string<br>枚举: VIP / A / B / C | 否 | 客户级别(VIP/A/B/C) | VIP | | `tags` | string | 否 | 标签(逗号分隔的自由文本) | 重点,高意向 | | `orgId` | integer (int64) | 否 | 所属组织 id(可空) | | | `ownerId` | integer (int64) | 否 | 客户归属人(销售员)员工 id | | | `stage` | string<br>枚举: NEW_LEAD / PENDING_ASSIGN / CONTACTING / NEEDS_COMM / QUOTING / NEGOTIATING / DEAL / LOST | 否 | 当前客户阶段(NEW_LEAD新线索 / PENDING_ASSIGN待分配 / CONTACTING接洽中 / NEEDS_COMM需求沟通 / QUOTING报价中 / NEGOTIATING议价中 / DEAL成交 / LOST流失) | NEW_LEAD | | `lastFollowAt` | string (date-time) | 否 | 最近一次跟进时间 | | | `nextFollowAt` | string (date-time) | 否 | 下次计划跟进时间 | | | `dealAt` | string (date-time) | 否 | 成交时间(阶段到 DEAL 时写入) | | | `lostAt` | string (date-time) | 否 | 流失时间(阶段到 LOST 时写入) | | | `lostReason` | string | 否 | 流失原因 | | ### EmployeeVO <a id="model-employeevo"></a> > 员工响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `name` | string | 否 | 员工姓名 | 张三 | | `phone` | string | 否 | 登录/联系手机号 | 13800138000 | | `orgId` | integer (int64) | 否 | 所属组织 id | | | `roleId` | integer (int64) | 否 | 角色 id(关联 crm_role.id;为 2 即 TENANT_ADMIN 预置角色) | | | `position` | string | 否 | 职位 | 销售主管 | | `email` | string | 否 | 邮箱 | zhangsan@example.com | | `avatarUrl` | string | 否 | 头像 URL | | | `status` | string<br>枚举: ACTIVE / DISABLED | 否 | 状态(ACTIVE 在职 / DISABLED 停用) | ACTIVE | | `holdLimit` | integer (int32) | 否 | 客户持有上限(可负责的客户数) | | | `region` | string | 否 | 负责区域 | 华东 | | `productTags` | string | 否 | 擅长产品标签(逗号分隔自由文本) | 汽车,零售 | | `hireDate` | string (date) | 否 | 入职日期 | | ### FollowUpVO <a id="model-followupvo"></a> > 跟进记录响应体(后台 · 只读) | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `customerId` | integer (int64) | 否 | 关联客户 id | | | `followerId` | integer (int64) | 否 | 跟进人(销售员)员工 id | | | `followAt` | string (date-time) | 否 | 本次跟进实际发生时间 | | | `followMethod` | string<br>枚举: PHONE / WECHAT / VISIT / EMAIL / MEETING | 否 | 跟进方式(PHONE 电话 / WECHAT 微信 / VISIT 上门拜访 / EMAIL 邮件 / MEETING 会议) | PHONE | | `content` | string | 否 | 跟进内容/沟通纪要 | | | `nextFollowAt` | string (date-time) | 否 | 下次计划跟进时间(可空) | | | `nextFollowPlan` | string | 否 | 下次跟进计划/备注(可空) | | | `followStatus` | string<br>枚举: DONE / PENDING / OVERDUE | 否 | 跟进状态(DONE 已完成 / PENDING 待跟进 / OVERDUE 已超时) | DONE | ### OrgVO <a id="model-orgvo"></a> > 组织响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `orgName` | string | 否 | 组织名称 | 华东分公司 | | `orgType` | string<br>枚举: HQ / BRANCH / DEALER / AGENT / DEPT / TEAM | 否 | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队) | BRANCH | | `parentId` | integer (int64) | 否 | 上级组织 id(根组织为 null) | | | `orgPath` | string | 否 | 组织路径(从根到本节点的全路径,如 /默认租户/华东分公司) | | | `leaderId` | integer (int64) | 否 | 负责人员工 id(可空) | | | `region` | string | 否 | 所在地区 | 上海 | | `sortOrder` | integer (int32) | 否 | 同级内显示排序(数字越小越靠前) | | | `status` | boolean | 否 | 启用状态(true 启用 / false 停用) | | ### ResultListRoleVO <a id="model-resultlistrolevo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | array\<RoleVO\> | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultRoleVO <a id="model-resultrolevo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | RoleVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### RoleVO <a id="model-rolevo"></a> > 角色响应体 | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `id` | integer (int64) | 否 | 主键 id | | | `createdAt` | string (date-time) | 否 | 创建时间 | | | `updatedAt` | string (date-time) | 否 | 更新时间 | | | `createdBy` | string | 否 | 创建人(操作人标识,如 Sa-Token loginId 字符串或 system) | | | `updatedBy` | string | 否 | 更新人 | | | `tenantId` | string | 否 | 所属租户 id(平台管理员等跨租户场景为 null) | | | `deleted` | integer (int32) | 否 | 软删除标记(0=未删除,1=已删除) | | | `roleName` | string | 否 | 角色名称 | 租户管理员 | | `roleCode` | string | 否 | 角色编码(预置角色常量如 TENANT_ADMIN/PLATFORM_ADMIN;自建角色为自定义) | TENANT_ADMIN | | `description` | string | 否 | 角色描述 | 企业管理者 | | `roleType` | string<br>枚举: PRESET / CUSTOM | 否 | 角色类型(PRESET 预置,全租户共享 / CUSTOM 自建,本租户) | PRESET | | `status` | string<br>枚举: ENABLED / DISABLED / DELETED | 否 | 状态(ENABLED 启用 / DISABLED 停用 / DELETED 已删除) | ENABLED | ### PageResultCustomerVO <a id="model-pageresultcustomervo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int64) | 否 | | | | `size` | integer (int64) | 否 | | | | `total` | integer (int64) | 否 | | | | `totalPages` | integer (int64) | 否 | | | | `records` | array\<CustomerVO\> | 否 | | | ### PageResultEmployeeVO <a id="model-pageresultemployeevo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int64) | 否 | | | | `size` | integer (int64) | 否 | | | | `total` | integer (int64) | 否 | | | | `totalPages` | integer (int64) | 否 | | | | `records` | array\<EmployeeVO\> | 否 | | | ### PageResultFollowUpVO <a id="model-pageresultfollowupvo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `page` | integer (int64) | 否 | | | | `size` | integer (int64) | 否 | | | | `total` | integer (int64) | 否 | | | | `totalPages` | integer (int64) | 否 | | | | `records` | array\<FollowUpVO\> | 否 | | | ### ResultCustomerVO <a id="model-resultcustomervo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | CustomerVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultEmployeeVO <a id="model-resultemployeevo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | EmployeeVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultInteger <a id="model-resultinteger"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | integer (int32) | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultListEmployeeVO <a id="model-resultlistemployeevo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | array\<EmployeeVO\> | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultListOrgVO <a id="model-resultlistorgvo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | array\<OrgVO\> | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultLong <a id="model-resultlong"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | integer (int64) | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultOrgVO <a id="model-resultorgvo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | OrgVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultPageResultCustomerVO <a id="model-resultpageresultcustomervo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | PageResultCustomerVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultPageResultEmployeeVO <a id="model-resultpageresultemployeevo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | PageResultEmployeeVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultPageResultFollowUpVO <a id="model-resultpageresultfollowupvo"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | PageResultFollowUpVO | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | ### ResultVoid <a id="model-resultvoid"></a> | 字段 | 类型 | 必填 | 说明 | 示例 | |------|------|------|------|------| | `code` | integer (int32) | 否 | | | | `message` | string | 否 | | | | `data` | object | 否 | | | | `timestamp` | integer (int64) | 否 | | | | `success` | boolean | 否 | | | --- ## 枚举值汇总 | 模型.字段 | 枚举值 | 说明 | |-----------|--------|------| | `CustomerVO.customerType` | `ENTERPRISE` · `PERSONAL` | 客户类型(ENTERPRISE 企业 / PERSONAL 个人) | | `CustomerVO.level` | `VIP` · `A` · `B` · `C` | 客户级别(VIP/A/B/C) | | `CustomerVO.source` | `MINI_PROGRAM` · `SALES_INPUT` · `EXHIBITION` · `REFERRAL` | 客户来源(小程序注册/销售录入/展会/转介绍) | | `CustomerVO.stage` | `NEW_LEAD` · `PENDING_ASSIGN` · `CONTACTING` · `NEEDS_COMM` · `QUOTING` · `NEGOTIATING` · `DEAL` · `LOST` | 当前客户阶段(NEW_LEAD新线索 / PENDING_ASSIGN待分配 / CONTACTING接洽中 / NEEDS_COMM需求沟通 / QUOTING报价中 / NEGOTIATING议价中 / DEAL成交 / LOST流失) | | `EmployeePageQuery.status` | `ACTIVE` · `DISABLED` | 按员工状态过滤(ACTIVE 在职 / DISABLED 停用);留空=全部 | | `EmployeeVO.status` | `ACTIVE` · `DISABLED` | 状态(ACTIVE 在职 / DISABLED 停用) | | `FollowUpVO.followMethod` | `PHONE` · `WECHAT` · `VISIT` · `EMAIL` · `MEETING` | 跟进方式(PHONE 电话 / WECHAT 微信 / VISIT 上门拜访 / EMAIL 邮件 / MEETING 会议) | | `FollowUpVO.followStatus` | `DONE` · `PENDING` · `OVERDUE` | 跟进状态(DONE 已完成 / PENDING 待跟进 / OVERDUE 已超时) | | `OrgCreateDTO.orgType` | `HQ` · `BRANCH` · `DEALER` · `AGENT` · `DEPT` · `TEAM` | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队);默认 DEPT | | `OrgVO.orgType` | `HQ` · `BRANCH` · `DEALER` · `AGENT` · `DEPT` · `TEAM` | 组织类型(HQ 公司本部 / BRANCH 分公司 / DEALER 经销商 / AGENT 代理商 / DEPT 部门 / TEAM 团队) | | `RoleVO.roleType` | `PRESET` · `CUSTOM` | 角色类型(PRESET 预置,全租户共享 / CUSTOM 自建,本租户) | | `RoleVO.status` | `ENABLED` · `DISABLED` · `DELETED` | 状态(ENABLED 启用 / DISABLED 停用 / DELETED 已删除) | --- ## 注意事项 1. **认证**:所有接口需在 Header 中携带 `Authorization: Bearer <JWT>`。 2. **多租户**:路径统一以 `/tenant/` 开头,租户上下文一般从 JWT 解析。 3. **统一响应**:请以 `success` / `code` 判断业务结果,不要只依赖 HTTP 状态码。 4. **分页**:分页参数默认 `page=1`,`size=20`。 5. **员工导入**:实际请使用 `multipart/form-data` 上传 Excel,字段名 `file`。 6. **员工导出**:返回 Excel 文件流。 7. **字段说明**:Request / Response 字段的「说明」「示例」均来自 OpenAPI `description` 与 `example`。 --- *文档由 OpenAPI 规范自动生成(租户版) 源地址: `http://121.41.54.56:9090/tenant/v3/api-docs`* src/api/auth.ts
@@ -1,47 +1,56 @@ import type { LoginParams, LoginResult, UserInfo } from '@/types/auth'; import { ApiError, request } from '@/api/request'; import type { LoginParams, LoginResult, TenantLoginData, UserInfo, } from '@/types/auth'; interface MockAccount { interface TenantLoginRequest { phone: string; password: string; user: UserInfo; tenantCode: string; } /** * 演示环境下的本地账号。 * 真实项目请替换为后端登录接口。 */ const MOCK_ACCOUNTS: Record<string, MockAccount> = { admin: { password: 'admin123', user: { id: 1, username: 'admin', nickname: '超级管理员', role: 'admin', }, }, user: { password: 'user123', user: { id: 2, username: 'user', nickname: '普通运营', role: 'user', }, }, const toUserInfo = (data: TenantLoginData, params: LoginParams): UserInfo => { const phone = data.phone ?? params.phone.trim(); const loginType = data.loginType?.toLowerCase() ?? ''; return { id: data.employeeId ?? 0, username: phone, nickname: data.employeeName ?? phone, role: loginType.includes('admin') ? 'admin' : 'user', tenant: data.tenantCode ?? params.tenant.trim(), phone, }; }; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); /** 租户登录。 */ export const loginApi = async (params: LoginParams): Promise<LoginResult> => { await delay(500); const data = await request<TenantLoginRequest, TenantLoginData>( { url: '/auth/tenant-login', method: 'POST', data: { phone: params.phone.trim(), password: params.password, tenantCode: params.tenant.trim(), }, }, { // 登录有副作用,不合并;错误由登录页统一提示。 merge: false, showError: false, }, ); const account = MOCK_ACCOUNTS[params.username]; if (!account || account.password !== params.password) { throw new Error('用户名或密码错误'); if (!data?.token) { throw new ApiError('登录响应中未找到 token', { data }); } return { token: `mock-token-${account.user.id}-${Date.now()}`, user: account.user, token: data.token, user: toUserInfo(data, params), }; }; src/api/request.ts
New file @@ -0,0 +1,302 @@ import axios from 'axios'; import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, } from 'axios'; import { showErrorMessage } from '@/utils/feedback'; /** 一个请求的配置。TReq 为请求参数 / 请求体的类型。 */ export type ApiRequestConfig<TReq = unknown> = Omit< AxiosRequestConfig<TReq>, 'signal' > & { url: string; }; /** 每个调用方可以单独设置的请求选项。 */ export interface RequestOptions<TParse extends boolean = boolean> { /** 请求失败时是否自动提示错误信息,默认 true。 */ showError?: boolean; /** 是否合并相同的并发请求,默认 true;为 false 时每次都单独发送。 */ merge?: boolean; /** 是否解析响应:true 返回 response.data,false 返回完整 AxiosResponse。默认 true。 */ parse?: TParse; /** 用于取消当前调用方发起的请求。 */ signal?: AbortSignal; } /** 请求失败时抛出的统一错误,保留 Axios 返回的完整信息。 */ export class ApiError extends Error { readonly code?: string; readonly status?: number; readonly data?: unknown; readonly response?: AxiosResponse; readonly config?: InternalAxiosRequestConfig; readonly isCanceled: boolean; readonly originalError?: unknown; /** 相同请求被多个调用方共享时,保证错误信息只提示一次。 */ notified = false; constructor( message: string, options: { code?: string; status?: number; data?: unknown; response?: AxiosResponse; config?: InternalAxiosRequestConfig; isCanceled?: boolean; originalError?: unknown; } = {}, ) { super(message); this.name = 'ApiError'; this.code = options.code; this.status = options.status; this.data = options.data; this.response = options.response; this.config = options.config; this.isCanceled = options.isCanceled ?? false; this.originalError = options.originalError; } } const http = axios.create({ // 开发环境走 Vite 代理(同源),生产环境使用真实接口地址。 baseURL: import.meta.env.DEV ? '/proxy-api' : import.meta.env.VITE_API_BASE_URL ?? '', timeout: 15000, }); /** 递归排序对象字段,保证相同参数得到相同的请求 key。 */ const sortValue = (value: unknown): unknown => { if (Array.isArray(value)) return value.map(sortValue); if (value && typeof value === 'object') { return Object.keys(value as Record<string, unknown>) .sort() .reduce<Record<string, unknown>>((result, key) => { result[key] = sortValue((value as Record<string, unknown>)[key]); return result; }, {}); } return value; }; const stringify = (value: unknown): string => { if (value === undefined) return ''; try { return JSON.stringify(sortValue(value)); } catch { return String(value); } }; const getRequestKey = <TReq>(config: ApiRequestConfig<TReq>): string => [ (config.method ?? 'GET').toUpperCase(), config.url, stringify(config.params), stringify(config.data), ].join('|'); /** 正在进行中的相同请求,值为完整的 AxiosResponse。 */ const pendingRequests = new Map<string, Promise<AxiosResponse>>(); const createCanceledError = () => new ApiError('请求已取消', { isCanceled: true, code: 'ERR_CANCELED' }); const toApiError = (error: unknown): ApiError => { if (error instanceof ApiError) return error; const axiosError = error as AxiosError<{ message?: string; msg?: string }>; const response = axiosError?.response; const data = response?.data; const serverMessage = typeof data?.message === 'string' ? data.message : typeof data?.msg === 'string' ? data.msg : undefined; const isCanceled = axios.isCancel(error) || axiosError?.code === 'ERR_CANCELED'; return new ApiError( isCanceled ? '请求已取消' : serverMessage ?? axiosError?.message ?? '网络请求失败', { code: axiosError?.code, status: response?.status, data, response, config: axiosError?.config, isCanceled, originalError: error, }, ); }; /** 发送底层请求并始终保留完整响应;解析与提示交给每个调用方处理。 */ const execute = async <TReq, TRes>( config: ApiRequestConfig<TReq>, options: RequestOptions, ): Promise<AxiosResponse<TRes>> => { try { return await http.request<TRes, AxiosResponse<TRes>, TReq>({ ...config, // 合并请求时不能把某个调用方的 signal 交给底层请求,否则会取消其他调用方。 signal: options.merge === false ? options.signal : undefined, }); } catch (error) { throw toApiError(error); } }; /** 后端统一响应包装:{ code, message, data, success }。 */ interface ResultEnvelope { code: number | string; message?: string; data?: unknown; success?: boolean; } const isResultEnvelope = (body: unknown): body is ResultEnvelope => { if (!body || typeof body !== 'object') return false; const record = body as Record<string, unknown>; return ( 'code' in record && ('data' in record || 'success' in record || 'message' in record) ); }; const isSuccessCode = (code: number | string) => code === 200; const formatResponse = <TRes, TParse extends boolean>( response: AxiosResponse<TRes>, options: RequestOptions<TParse>, ): TParse extends true ? TRes : AxiosResponse<TRes> => { if (options.parse === false) { return response as TParse extends true ? TRes : AxiosResponse<TRes>; } const body = response.data as unknown; if (isResultEnvelope(body)) { const succeeded = isSuccessCode(body.code) || body.success === true ; if (!succeeded) { throw new ApiError( body.message || '请求失败', { code: String(body.code), status: response.status, data: body, response, config: response.config, }, ); } return body.data as TParse extends true ? TRes : AxiosResponse<TRes>; } return body as TParse extends true ? TRes : AxiosResponse<TRes>; }; const notifyError = (error: ApiError, options: RequestOptions) => { if (error.isCanceled || options.showError === false || error.notified) return; error.notified = true; showErrorMessage(error.message); }; /** 让共享请求对每个调用方单独支持取消。 */ const raceWithSignal = <T>(promise: Promise<T>, signal: AbortSignal): Promise<T> => { if (signal.aborted) return Promise.reject(createCanceledError()); return new Promise<T>((resolve, reject) => { const onAbort = () => reject(createCanceledError()); signal.addEventListener('abort', onAbort, { once: true }); promise.then( (value) => { signal.removeEventListener('abort', onAbort); resolve(value); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); }, ); }); }; /** * 发送网络请求。 * * 默认合并相同的并发请求、自动提示错误,并且只返回服务端的 data。 * * @example * const result = await request<{ keyword: string }, PageResult<User>>({ * url: '/api/users', * method: 'GET', * params: { keyword }, * }); * * // 不解析,拿到完整的 AxiosResponse * const response = await request<void, User[]>( * { url: '/api/users' }, * { parse: false }, * ); * * // 不合并、不自动提示,并通过 signal 取消 * const controller = new AbortController(); * await request({ url: '/api/users' }, { * merge: false, * showError: false, * signal: controller.signal, * }); */ export const request = async < TReq = unknown, TRes = unknown, TParse extends boolean = true, >( config: ApiRequestConfig<TReq>, options: RequestOptions<TParse> = {}, ): Promise<TParse extends true ? TRes : AxiosResponse<TRes>> => { const { signal, merge = true } = options; if (signal?.aborted) { throw createCanceledError(); } let task: Promise<AxiosResponse<TRes>>; if (merge) { const key = getRequestKey(config); const existing = pendingRequests.get(key) as Promise<AxiosResponse<TRes>> | undefined; if (existing) { task = existing; } else { task = execute<TReq, TRes>(config, options); pendingRequests.set(key, task); const cleanup = () => { if (pendingRequests.get(key) === task) { pendingRequests.delete(key); } }; task.then(cleanup, cleanup); } } else { task = execute<TReq, TRes>(config, options); } try { const response = merge && signal ? await raceWithSignal(task, signal) : await task; console.log('response', response); return formatResponse(response, options); } catch (error) { const apiError = toApiError(error); notifyError(apiError, options); throw apiError; } }; src/components/AntdAppBridge.tsx
New file @@ -0,0 +1,16 @@ import { useEffect } from 'react'; import { App } from 'antd'; import { setMessageInstance } from '@/utils/feedback'; /** 把 AntdApp 上下文中的 message 注入到非组件模块(如 API 请求)。 */ const AntdAppBridge = () => { const { message } = App.useApp(); useEffect(() => { setMessageInstance(message); }, [message]); return null; }; export default AntdAppBridge; src/main.tsx
@@ -5,6 +5,7 @@ import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { BrowserRouter } from 'react-router-dom'; import AntdAppBridge from '@/components/AntdAppBridge'; import AppRouter from '@/router'; import '@/styles/global.scss'; @@ -38,6 +39,7 @@ }} > <AntdApp> <AntdAppBridge /> <BrowserRouter> <AppRouter /> </BrowserRouter> src/pages/business/orders/columns.tsx
New file @@ -0,0 +1,84 @@ import { Button, Tag } from 'antd'; import type { ProColumns } from '@ant-design/pro-components'; import { ORDER_STATUS_META, type OrderRecord } from './types'; export const createOrderColumns = (options: { onProcess: (record: OrderRecord) => void; }): ProColumns<OrderRecord>[] => [ { title: '订单号', dataIndex: 'id', width: 160, render: (_value, record) => <code className="order-id">{record.id}</code>, fieldProps: { placeholder: '请输入订单号' }, }, { title: '客户', dataIndex: 'customer', width: 240, render: (_value, record) => ( <div className="order-customer"> <strong>{record.customer}</strong> <span>{record.contact}</span> </div> ), fieldProps: { placeholder: '请输入客户名称' }, }, { title: '订单金额', dataIndex: 'amount', width: 120, align: 'right', search: false, render: (_value, record) => ( <strong className="order-amount">¥{record.amount.toLocaleString()}</strong> ), }, { title: '来源渠道', dataIndex: 'channel', width: 110, search: false, render: (_value, record) => ( <Tag bordered={false} color="geekblue"> {record.channel} </Tag> ), }, { title: '状态', dataIndex: 'status', width: 110, valueEnum: { pending: { text: '待付款' }, processing: { text: '处理中' }, completed: { text: '已完成' }, cancelled: { text: '已取消' }, }, render: (_value, record) => ( <Tag color={ORDER_STATUS_META[record.status].color} bordered={false}> {ORDER_STATUS_META[record.status].label} </Tag> ), }, { title: '创建时间', dataIndex: 'createdAt', width: 170, search: false, }, { title: '操作', key: 'action', valueType: 'option', width: 100, render: (_value, record) => record.status === 'completed' ? ( <span className="text-muted">已完成</span> ) : ( <Button type="link" size="small" onClick={() => options.onProcess(record)}> 处理 </Button> ), }, ]; src/pages/business/orders/index.tsx
@@ -1,36 +1,17 @@ import { useMemo, useState } from 'react'; import { DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; import { App as AntdApp, Button, Card, Input, Select, Table, Tag, } from 'antd'; import type { TableProps } from 'antd'; import { useCallback, useMemo, useState } from 'react'; import { DownloadOutlined } from '@ant-design/icons'; import { ProTable } from '@ant-design/pro-components'; import { App as AntdApp, Button } from 'antd'; import { createOrderColumns } from './columns'; import type { OrderRecord, OrderStatus } from './types'; import './Orders.scss'; type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled'; interface OrderRecord { key: string; id: string; customer: string; contact: string; amount: number; channel: string; status: OrderStatus; createdAt: string; interface OrderQuery { id?: string; customer?: string; status?: OrderStatus; orders: OrderRecord[]; } const statusMeta: Record<OrderStatus, { label: string; color: string }> = { pending: { label: '待付款', color: 'orange' }, processing: { label: '处理中', color: 'processing' }, completed: { label: '已完成', color: 'success' }, cancelled: { label: '已取消', color: 'default' }, }; const initialOrders: OrderRecord[] = [ { @@ -137,103 +118,42 @@ const Orders = () => { const [orders, setOrders] = useState<OrderRecord[]>(initialOrders); const [keyword, setKeyword] = useState(''); const [status, setStatus] = useState<OrderStatus | 'all'>('all'); const [summary, setSummary] = useState(() => ({ count: initialOrders.length, amount: initialOrders.reduce((sum, order) => sum + order.amount, 0), })); const { message, modal } = AntdApp.useApp(); const filteredOrders = useMemo(() => { const normalizedKeyword = keyword.trim().toLowerCase(); return orders.filter((order) => { const matchesKeyword = !normalizedKeyword || order.id.toLowerCase().includes(normalizedKeyword) || order.customer.toLowerCase().includes(normalizedKeyword); const matchesStatus = status === 'all' || order.status === status; return matchesKeyword && matchesStatus; }); }, [orders, keyword, status]); const tableParams = useMemo(() => ({ orders }), [orders]); const handleProcess = (record: OrderRecord) => { modal.confirm({ title: '处理订单', content: `确定将订单「${record.id}」标记为已完成吗?`, okText: '标记完成', cancelText: '取消', onOk: () => { setOrders((prev) => prev.map((order) => order.key === record.key ? { ...order, status: 'completed' } : order, ), ); message.success('订单状态已更新'); }, }); }; const handleProcess = useCallback( (record: OrderRecord) => { modal.confirm({ title: '处理订单', content: `确定将订单「${record.id}」标记为已完成吗?`, okText: '标记完成', cancelText: '取消', onOk: () => { setOrders((prev) => prev.map((order) => order.key === record.key ? { ...order, status: 'completed' } : order, ), ); message.success('订单状态已更新'); }, }); }, [message, modal], ); const columns = useMemo( () => createOrderColumns({ onProcess: handleProcess }), [handleProcess], ); const handleExport = () => { message.success(`已导出 ${filteredOrders.length} 条订单记录(演示)`); message.success(`已导出 ${summary.count} 条订单记录(演示)`); }; const columns: TableProps<OrderRecord>['columns'] = [ { title: '订单号', dataIndex: 'id', width: 160, render: (value: string) => <code className="order-id">{value}</code>, }, { title: '客户', dataIndex: 'customer', width: 240, render: (_value, record) => ( <div className="order-customer"> <strong>{record.customer}</strong> <span>{record.contact}</span> </div> ), }, { title: '订单金额', dataIndex: 'amount', width: 120, align: 'right', render: (value: number) => <strong className="order-amount">¥{value.toLocaleString()}</strong>, }, { title: '来源渠道', dataIndex: 'channel', width: 110, render: (value: string) => ( <Tag bordered={false} color="geekblue"> {value} </Tag> ), }, { title: '状态', dataIndex: 'status', width: 100, render: (value: OrderStatus) => ( <Tag color={statusMeta[value].color} bordered={false}> {statusMeta[value].label} </Tag> ), }, { title: '创建时间', dataIndex: 'createdAt', width: 160 }, { title: '操作', key: 'action', width: 100, render: (_value, record) => record.status === 'completed' ? ( <span className="text-muted">已完成</span> ) : ( <Button type="link" size="small" onClick={() => handleProcess(record)}> 处理 </Button> ), }, ]; return ( <div className="orders-page"> @@ -244,61 +164,59 @@ 跟踪销售订单的处理进度,支持筛选与导出。 </p> </div> <div className="page-header__actions"> <Button icon={<ReloadOutlined />} onClick={() => message.success('订单数据已刷新')} > 刷新 </Button> <Button type="primary" icon={<DownloadOutlined />} onClick={handleExport}> 导出订单 </Button> </div> </div> <Card className="filter-card" variant="borderless"> <div className="page-toolbar"> <Input allowClear prefix={<SearchOutlined />} placeholder="搜索订单号 / 客户名称" value={keyword} onChange={(event) => setKeyword(event.target.value)} style={{ width: 280 }} /> <Select<OrderStatus | 'all'> value={status} onChange={setStatus} style={{ width: 150 }} options={[ { value: 'all', label: '全部状态' }, { value: 'pending', label: '待付款' }, { value: 'processing', label: '处理中' }, { value: 'completed', label: '已完成' }, { value: 'cancelled', label: '已取消' }, ]} /> <span className="text-muted"> 共 {filteredOrders.length} 条订单 · 合计 ¥ {filteredOrders.reduce((sum, order) => sum + order.amount, 0).toLocaleString()} </span> </div> </Card> <ProTable<OrderRecord, OrderQuery> rowKey="key" columns={columns} params={tableParams} cardProps={{ className: 'orders-table-card' }} search={{ labelWidth: 'auto', defaultCollapsed: false }} scroll={{ x: 1000 }} options={false} pagination={{ defaultPageSize: 8, showSizeChanger: true, hideOnSinglePage: false, showTotal: (total) => `共 ${total} 条`, }} toolBarRender={() => [ <span key="summary" className="text-muted"> 共 {summary.count} 条订单 · 合计 ¥{summary.amount.toLocaleString()} </span>, <Button key="export" type="primary" icon={<DownloadOutlined />} onClick={handleExport}> 导出订单 </Button>, ]} request={async (params) => { const { current = 1, pageSize = 8, id, customer, status, orders: dataSource } = params; const normalizedId = id?.trim().toLowerCase(); const normalizedCustomer = customer?.trim().toLowerCase(); <Card className="table-card" variant="borderless"> <Table<OrderRecord> rowKey="key" columns={columns} dataSource={filteredOrders} scroll={{ x: 1000 }} pagination={{ pageSize: 8, showSizeChanger: false, showTotal: (total) => `共 ${total} 条`, }} /> </Card> const filteredOrders = dataSource.filter((order) => { const matchesId = !normalizedId || order.id.toLowerCase().includes(normalizedId); const matchesCustomer = !normalizedCustomer || order.customer.toLowerCase().includes(normalizedCustomer); const matchesStatus = !status || order.status === status; return matchesId && matchesCustomer && matchesStatus; }); setSummary({ count: filteredOrders.length, amount: filteredOrders.reduce((sum, order) => sum + order.amount, 0), }); const start = (current - 1) * pageSize; return { data: filteredOrders.slice(start, start + pageSize), total: filteredOrders.length, success: true, }; }} /> </div> ); }; src/pages/business/orders/types.ts
New file @@ -0,0 +1,19 @@ export type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled'; export interface OrderRecord { key: string; id: string; customer: string; contact: string; amount: number; channel: string; status: OrderStatus; createdAt: string; } export const ORDER_STATUS_META: Record<OrderStatus, { label: string; color: string }> = { pending: { label: '待付款', color: 'orange' }, processing: { label: '处理中', color: 'processing' }, completed: { label: '已完成', color: 'success' }, cancelled: { label: '已取消', color: 'default' }, }; src/pages/login/Login.scss
@@ -195,60 +195,6 @@ } } .login-demo { margin-top: 28px; padding-top: 22px; border-top: 1px dashed var(--app-border); &__title { display: flex; align-items: center; gap: 7px; margin-bottom: 12px; font-size: 12px; color: var(--app-text-secondary); } &__accounts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; button { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; padding: 10px 12px; border: 1px solid var(--app-border); border-radius: 9px; background: #fafbff; cursor: pointer; text-align: left; transition: border-color 0.2s ease, background 0.2s ease; &:hover { border-color: #c7d2fe; background: #f5f3ff; } span { font-size: 12px; font-weight: 600; color: var(--app-text); } code { font-size: 11px; color: var(--app-text-secondary); font-family: 'SFMono-Regular', Consolas, monospace; } } } } @media (max-width: 900px) { .login-shell { grid-template-columns: 1fr; @@ -276,9 +222,5 @@ .login-panel { padding: 32px 24px; } .login-demo__accounts { grid-template-columns: 1fr; } } src/pages/login/index.tsx
@@ -1,9 +1,9 @@ import { useState } from 'react'; import { ApartmentOutlined, CheckCircleFilled, LockOutlined, SafetyCertificateOutlined, UserOutlined, MobileOutlined, } from '@ant-design/icons'; import { App as AntdApp, Button, Form, Input, Tag } from 'antd'; import { Navigate, useLocation, useNavigate } from 'react-router-dom'; @@ -39,10 +39,6 @@ } finally { setLoading(false); } }; const fillAccount = (username: string, password: string) => { form.setFieldsValue({ username, password }); }; return ( @@ -92,18 +88,35 @@ layout="vertical" size="large" requiredMark={false} initialValues={{ username: 'admin', password: 'admin123' }} onFinish={handleFinish} > <Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]} name="tenant" label="租户" rules={[ { required: true, whitespace: true, message: '请输入租户标识' }, ]} > <Input prefix={<UserOutlined />} placeholder="请输入用户名" autoComplete="username" prefix={<ApartmentOutlined />} placeholder="请输入租户标识" autoComplete="organization" /> </Form.Item> <Form.Item name="phone" label="手机号" rules={[ { required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }, ]} > <Input prefix={<MobileOutlined />} placeholder="请输入手机号" autoComplete="tel" maxLength={11} /> </Form.Item> @@ -128,22 +141,6 @@ 登 录 </Button> </Form> <div className="login-demo"> <div className="login-demo__title"> <SafetyCertificateOutlined /> 演示账号 </div> <div className="login-demo__accounts"> <button type="button" onClick={() => fillAccount('admin', 'admin123')}> <span>管理员</span> <code>admin / admin123</code> </button> <button type="button" onClick={() => fillAccount('user', 'user123')}> <span>普通用户</span> <code>user / user123</code> </button> </div> </div> </div> </section> </div> src/types/auth.ts
@@ -5,13 +5,28 @@ username: string; nickname: string; role: UserRole; tenant: string; phone: string; } export interface LoginParams { username: string; tenant: string; phone: string; password: string; } /** `/auth/tenant-login` 返回的业务数据。 */ export interface TenantLoginData { token: string; employeeId?: number; employeeName?: string; phone?: string; tenantId?: string; tenantCode?: string; tenantName?: string; loginType?: string; } export interface LoginResult { token: string; user: UserInfo; src/utils/feedback.ts
New file @@ -0,0 +1,15 @@ import type { MessageInstance } from 'antd/es/message/interface'; /** * 非组件模块(如 API 请求)无法直接使用 `App.useApp()`, * 由 `AntdAppBridge` 把 App 上下文中的 message 实例注入到这里。 */ let messageInstance: MessageInstance | null = null; export const setMessageInstance = (instance: MessageInstance) => { messageInstance = instance; }; export const showErrorMessage = (content: string) => { messageInstance?.error(content); }; vite.config.ts
@@ -1,16 +1,30 @@ import { fileURLToPath, URL } from 'node:url'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import { defineConfig, loadEnv } from 'vite'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), /** 开发环境通过 Vite 代理访问后端,避免浏览器跨域。 */ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); const proxyTarget = env.VITE_API_PROXY_TARGET || env.VITE_API_BASE_URL || 'http://localhost:9090'; return { plugins: [react()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, }, server: { port: 5173, host: true, }, server: { port: 5173, host: true, proxy: { '/proxy-api': { target: proxyTarget, changeOrigin: true, rewrite: (path) => path.replace(/^\/proxy-api/, ''), }, }, }, }; });