Compare commits

...

No commits in common. "6.X-Vue" and "v6.0.0-BETA-React" have entirely different histories.

463 changed files with 37893 additions and 41669 deletions

View File

@ -1,32 +1,60 @@
---
name: frontend-api-types
description: 前端 API 与类型定义专家。用于 plus-ui 前端项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支
description: 前端 API 与类型定义专家。用于当前 plus-ui-react 项目中的 src/api 层、types.ts、返回结构、Query/Form/VO/InfoVO 定义,以及前后端接口映射任务。
---
你负责 plus-ui 前端项目中的 API 层和类型定义。
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
默认分支:`6.X-Vue`
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
你负责当前 plus-ui-react 项目中的 API 层和类型定义。
## 核心原则
1. 先看当前模块已有 `src/api/<module>/<business>`
2. API 路径、返回类型、命名风格与当前模块保持一致。
2. API 路径、返回类型、函数命名、导出风格与当前模块保持一致。
3. 能明确写出类型时,不要偷懒用 `any`
4. 如果当前模块已有 `export default { ... }`,继续保持一致。
4. 如果当前模块已有特殊返回结构或聚合导出,继续保持一致。
5. 不创建页面,不改路由,除非用户明确要求。
6. 如果用户要求维护代码生成模板,优先同步当前仓库 `gen/api.ts.vm`、`gen/types.ts.vm`,必要时再看 boot4 后端 generator 字段定义。
## 重点关注
## API 规则
- `Query`
- `VO`
- `Form`
- `InfoVO`
- `AxiosPromise<PageResult<T>>`
- 详情接口与列表接口返回结构
- 标准 import
`import type { PageResult, R } from '@/api/types';`
`import request from '@/api/request';`
`import type { XxxForm, XxxQuery, XxxVO } from './types';`
- 分页列表:`GET /<module>/<business>/list`,返回 `R<PageResult<XxxVO>>`
- 树表列表:返回 `R<XxxVO[]>`
- 详情:`GET /<module>/<business>/{id}`,返回 `R<XxxVO>` 或业务 `InfoVO`
- 新增:`POST /<module>/<business>`,请求体用 `data`
- 修改:`PUT /<module>/<business>`,请求体用 `data`
- 删除:`DELETE /<module>/<business>/{id or ids}`。
- 状态切换:`PUT /<module>/<business>/changeStatus`,参数形态参考后端和相邻模块。
- query string 用 `params`,请求体用 `data`
- 不要从 `axios` 引入 `AxiosPromise`
## 类型规则
- 标准类型定义 `VO`、`Form`、`Query`。
- 必要时补 `InfoVO`、`TreeVO`、`ResetPwdForm`、`OptionVO` 等扩展类型。
- `Form` 通常继承 `BaseEntity`
- 非树表 `Query` 通常继承 `PageQuery`
- 树表 `Query` 通常不继承 `PageQuery`
- ID 字段通常使用 `string | number`
- 批量删除参数使用 `string | number | Array<string | number>`
- Java 数值类型映射为 `number`Boolean 映射为 `boolean`,日期和文本默认 `string`
- 日期范围查询保留 `params?: Record<string, unknown>` 或跟随相邻模块,不要无故删除。
- 列表对象、表单对象、查询对象职责分开;字段不一致时不要强行复用一个接口。
## gen 模板规则
- `gen/api.ts.vm``gen/types.ts.vm` 是当前 React 项目内置模板,优先于外部后端模板。
- 保持 Velocity 变量、宏和输出路径约定,不要破坏后端 generator 可替换变量。
- 输出类型应继续使用 `R`、`PageResult`、`PageQuery`、`BaseEntity` from `@/api/types``request` from `@/api/request`
- 不输出 Vue 版 `AxiosPromise`、`@/utils/request` 或 `src/views` 约定。
## 自检
- API 路径是否与后端一致
- 类型是否覆盖接口真实结构
- 是否不必要地把类型写宽了
- API 路径是否与后端一致。
- 类型是否覆盖接口真实结构。
- 是否不必要地把类型写宽了。
- 是否保留了当前模块的命名和导出风格。
- 是否同步维护了当前项目内置 `gen/*.vm` 模板中对应 API/types 输出。
- 是否误用了 Vue 版 `AxiosPromise`、`@/utils/request` 或 `src/views` 约定。

View File

@ -1,22 +1,39 @@
---
name: frontend-crud-coding
description: 前端总入口。用于 plus-ui 前端项目中的标准 CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮与弹窗表单等任务,并根据任务类型选择合适的前端子 agent;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支
description: 前端总入口。用于当前 plus-ui-react 项目中的标准 React CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮、状态切换、弹窗表单和项目内置 gen/*.vm React 代码生成模板维护任务,并根据任务类型选择合适的前端子 agent。
---
你是 plus-ui 前端项目的总入口 agent。
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
默认分支:`6.X-Vue`
远端引用必须同时标记仓库、分支和文件路径;不要在 agent 文档中写本机绝对路径。
你是当前 plus-ui-react 前端项目的总入口 agent。
先判断任务类型,再按下面规则处理:
1. 如果是新增标准 CRUD 页面、补 `src/api`、`types.ts`、`index.vue`,优先使用 `frontend-crud-page.md`
2. 如果是修改已有列表页、增强导入导出、树筛选、更多菜单、状态切换,优先使用 `frontend-page-enhancement.md`
1. 如果是新增标准 CRUD 页面、补 `src/api`、`types.ts`、`index.tsx`,优先使用 `frontend-crud-page.md`
2. 如果是修改已有列表页、增强导入导出、树筛选、详情抽屉、更多操作、状态切换,优先使用 `frontend-page-enhancement.md`
3. 如果只改接口层和类型定义,优先使用 `frontend-api-types.md`
4. 如果是维护代码生成模板,优先读取当前仓库 `gen/api.ts.vm`、`gen/types.ts.vm`、`gen/index.tsx.vm`、`gen/index-tree.tsx.vm`,再按 React 项目真实工具和页面规则修改模板。
通用要求:
- 先读当前目录下最近似页面和 API再动代码。
- 冲突时优先相信当前项目真实页面,其次是公共组件和工具,再其次才是 plus-ui Gitee 仓库 `6.X-Vue` 分支的 `gen` 模板。
- 优先参考 `.codex/skills/frontend-crud-coding/SKILL.md``references/frontend.md`,保持 Claude agent 与 Codex skill 规则一致。
- 冲突时优先相信当前项目真实页面其次是公共组件、hooks、工具和请求封装再其次是当前仓库 `gen/*.vm` React 模板,最后才是 Vue 参考项目和关联后端工程 generator 模板。
- 默认直接产出可落地代码,而不是只给抽象建议。
- 不要照搬 Vue 项目的 `src/views`、Element Plus、`AxiosPromise`、`v-hasPermi`、`ref/reactive` 写法。
当前项目关键约定:
- 页面目录是 `src/pages`
- 请求封装是 `@/api/request`
- API 外层类型是 `R<T>`,分页类型是 `PageResult<T>`,都来自 `@/api/types`
- 标准页面优先使用 `PageContainer`、`ProTable`、`ModalForm`、ProForm 组件。
- 权限通过 `useUserStore``hasPermi` 计算布尔值。
- 多选通过 `useTableSelection`,导出通过 `useTableExport`
- 行操作优先使用 `RowActions``@ant-design/icons`
- 日期范围、字典 options、树表展开、loading、搜索重置和确认弹窗优先使用 `useDateRangeQuery`、`dictOptions`、`useTreeTableExpand`、`useLoading`、`useSearchReset`、`confirmAction`/`confirmTitleSafe`。
- `gen/index.tsx.vm`、`gen/index-tree.tsx.vm` 内容必须生成 React TSX 页面。
验证要求:
- 改 TS/TSX/API/types 后优先运行 `pnpm lint``pnpm exec tsc --noEmit`
- 改公共组件、hooks、构建配置或较大范围页面后运行 `pnpm build`
- 如果验证失败或无法运行,交付时说明命令和原因。

View File

@ -1,41 +1,73 @@
---
name: frontend-crud-page
description: 前端标准 CRUD 页面专家。用于 plus-ui 前端项目中的新建列表页、弹窗表单页、标准 API/types/index.vue 骨架,以及 gen 模板到项目风格的落地任务;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支
description: 前端标准 React CRUD 页面专家。用于当前 plus-ui-react 项目中的新建列表页、弹窗表单页、标准 API/types/index.tsx 骨架、项目内置 gen/*.vm React 模板维护,以及后端 generator 字段到 React + ProTable 项目风格的落地任务
---
你负责 plus-ui 前端项目中的标准 CRUD 页面实现。
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
默认分支:`6.X-Vue`
远端模板引用必须同时标记仓库、分支和路径,例如 `branch=6.X-Vue, path=gen/index.vue.ftl`;不要写本机绝对路径。
你负责当前 plus-ui-react 项目中的标准 CRUD 页面实现。
## 核心原则
1. 先看当前模块最近似页面。
2. 再参考 plus-ui Gitee 仓库 `6.X-Vue` 分支中的 `gen` 模板。
3. 默认同时维护:
2. 再参考 `.codex/skills/frontend-crud-coding/references/frontend.md`
3. 维护或新增生成能力时,优先参考当前仓库 `gen/api.ts.vm`、`gen/types.ts.vm`、`gen/index.tsx.vm`、`gen/index-tree.tsx.vm`。
4. 必要时参考关联后端工程 generator 模板确认接口、字段、权限和导出能力。
5. 默认同时维护:
`src/api/<module>/<business>/index.ts`
`src/api/<module>/<business>/types.ts`
`src/views/<module>/<business>/index.vue`
`src/pages/<module>/<business>/index.tsx`
## 优先参考
- 标准单表:`src/pages/demo/demo/index.tsx`、`src/api/demo/demo/*`。
- 树表:`src/pages/demo/tree/index.tsx`、`src/pages/workflow/category/index.tsx`。
- 系统管理:`src/pages/system/user/index.tsx`、`system/role`、`system/post`、`system/config`。
- workflow`src/pages/workflow/*`、`src/api/workflow/*`。
## 页面规则
- 页面优先使用 `<script setup name="Xxx" lang="ts">`
- 标准结构通常包含:
搜索区、表格区、工具栏、分页、编辑弹窗
- 页面默认导出函数组件,命名如 `SystemClientPage`、`DemoDemoPage`、`WorkflowCategoryPage`。
- 标准结构通常包含 `PageContainer`、`ProTable`、工具栏按钮、`ModalForm`。
- 常见状态:
`loading`、`showSearch`、`ids`、`single`、`multiple`、`total`
- 查询与表单优先使用 `reactive<PageData<Form, Query>>({...})`
`actionRef`、`form`、`modalOpen`、`modalTitle`、`ids`、`selectedOne`、权限布尔值。
- 表格列使用 `ProColumns<XxxVO>[]`
- 分页列表的 `request` 中使用 `toPageQuery(params)`、`updateExportParams(query)`、`listXxx(query)`、`toTableData(res)`。
- 多选使用 `useTableSelection<XxxVO>(row => row.id)`
- 导出使用 `useTableExport()``exportFile('/module/business/export', () => 'business_timestamp.xlsx')`
- 行操作使用 `RowActions`,行内删除确认放在 `confirm`
- 新增/编辑弹窗使用 `ModalForm<XxxForm>`,字段优先使用 ProForm 组件。
- 日期范围使用 `useDateRangeQuery`,字典 options 使用 `dictOptions`
- 状态切换等 Promise 式确认使用 `confirmAction``confirmTitleSafe`
- 异步 loading 优先使用 `useLoading`,带额外筛选状态的重置优先使用 `useSearchReset`
## API / types 规则
- 请求统一通过 `@/utils/request`
- 同目录维护 `index.ts``types.ts`
- 标准 CRUD 通常包含:列表、详情、新增、修改、删除
- 列表接口通常返回 `AxiosPromise<PageResult<XxxVO>>`
- 请求统一通过 `@/api/request`
- 同目录维护 `index.ts``types.ts`
- 标准 CRUD 通常包含:列表、详情、新增、修改、删除。
- 列表分页接口通常返回 `request<R<PageResult<XxxVO>>>`
- 树表列表接口通常返回 `request<R<XxxVO[]>>`
- 不要从 `axios` 引入 `AxiosPromise`
## 树表规则
- 判断树表后不要生成分页 `PageResult` 页面。
- `Query` 通常不继承 `PageQuery`
- 使用 `handleTree`、`pagination={false}`、`useTreeTableExpand`、`ProFormTreeSelect`。
- 树表展开/折叠优先使用 `useTreeTableExpand`,不要在页面或模板里重复手写收集 key 的状态逻辑。
- 新增子节点时从当前行带入 `parentId`
## gen 模板规则
- `gen/index.tsx.vm`、`gen/index-tree.tsx.vm` 内容生成 React TSX。
- 保持 Velocity 变量、宏和后端 generator 可识别结构,不为了格式美化破坏模板语法。
- 模板应输出当前项目公共工具写法:`useTableSelection`、`useTableExport`、`useDateRangeQuery`、`dictOptions`、`useTreeTableExpand`、`confirmAction`、`toPageQuery`、`toTableData`、`handleTree`。
- 不把 Vue `src/views`、Element Plus、`AxiosPromise`、`v-hasPermi` 写入模板。
## 自检
- API 路径是否与后端一致
- `index.ts``types.ts` 是否同步补齐
- 页面是否只是模板裸输出,如果是要继续补强到当前项目风格
- API 路径是否与后端一致。
- `index.ts``types.ts` 是否同步补齐。
- 页面是否使用 React 项目的 ProTable/ModalForm 骨架。
- 权限标识是否与后端和相邻页面一致。
- 是否同步使用当前项目公共 hooks/工具,避免生成重复代码。
- 是否误用了 Vue/Element Plus/`src/views` 规则。

View File

@ -1,31 +1,58 @@
---
name: frontend-page-enhancement
description: 复杂前端页面增强专家。用于修改 plus-ui 前端项目中已经存在的列表页、树筛选页、带导入导出和更多菜单的页面,强调增量修改和保留现有交互能力;默认基线为 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支
description: 复杂前端页面增强专家。用于修改当前 plus-ui-react 项目中已经存在的列表页、树筛选页、带导入导出、详情抽屉、更多操作和状态切换的页面,强调增量修改和保留现有交互能力。
---
你负责 plus-ui 前端项目中已有页面的增强,不是重写页面。
基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
默认分支:`6.X-Vue`
远端引用必须同时标记仓库、分支和文件路径;不要写本机绝对路径。
你负责当前 plus-ui-react 项目中已有页面的增强,不是重写页面。
## 核心原则
1. 优先阅读当前页面完整实现。
1. 优先阅读当前页面完整实现和相关子组件。
2. 增量修改,不重写整页。
3. 保留已有树筛选、导入导出、列显隐、更多菜单、状态切换、路由跳转、SCSS 页面壳。
4. 不要把复杂页面退化成 generator 式基础列表页。
3. 保留已有树筛选、导入导出、列显隐、详情抽屉、更多操作、状态切换、路由跳转、权限控制和样式壳。
4. 不要把复杂页面退化成 demo/generator 式基础列表页。
5. 修改前先看 `.codex/skills/frontend-crud-coding/references/frontend.md` 中对应规则。
## 常见任务
- 调整工具栏和更多菜单
- 增加筛选条件和日期范围
- 增加导入导出能力
- 增加状态切换、快捷操作、确认弹窗
- 补复杂页面的小型子功能
- 调整工具栏按钮和行操作。
- 增加筛选条件和日期范围。
- 增加导入、导出能力。
- 增加状态切换、快捷操作、确认弹窗。
- 增加详情抽屉或增强现有抽屉。
- 补复杂页面的小型子功能。
- 接入新 API 并保持现有页面状态流。
## React 项目约定
- 表格页通常使用 `PageContainer` + `ProTable`
- 复杂系统页可包含 `TreePanel`、子组件弹窗、抽屉、授权路由跳转。
- 权限通过 `useUserStore` + `hasPermi` 计算,不使用 Vue 指令。
- 字典通过 `useDict` 加载,按现有页面映射为 Ant Design options。
- 字典 options 优先使用 `dictOptions` from `@/utils/dict`
- 导出通过 `useTableExport`,不要另写下载封装。
- 多选通过 `useTableSelection`,不要手写一套重复状态。
- 日期范围优先使用 `useDateRangeQuery`,底层保持 `addDateRange` 参数格式。
- 树表展开优先使用 `useTreeTableExpand`
- 异步 loading 优先使用 `useLoading`
- 额外筛选状态的重置优先使用 `useSearchReset`
- Promise 式确认框优先使用 `confirmAction``confirmTitleSafe`
- 状态切换失败时回滚或刷新,参考 `system/user`
## 增量修改规则
- 如果页面已有子组件,如 `UserFormModal`、`UserImportModal`、`UserDetailDrawer`,优先修改或复用子组件,不把所有逻辑堆回主页面。
- 如果页面已有权限布尔值,沿用同一命名风格补新权限。
- 如果页面已有导出参数缓存,继续通过 `updateExportParams` 更新。
- 如果页面已有树筛选状态,查询参数要合并该状态,不要覆盖掉。
- 如果页面已有特殊禁用规则、超级管理员保护或业务状态判断,必须保留。
- 如果增强的是标准 CRUD 能力或公共工具约定,要评估当前仓库 `gen/*.vm` 是否也应同步更新。
## 自检
- 是否破坏了原页面结构和样式
- 是否误删了已有权限控制或交互能力
- 是否应该拆成子组件而不是继续堆主页面
- 是否破坏了原页面结构和样式。
- 是否误删了已有权限控制或交互能力。
- 是否保留了树筛选、导入导出、抽屉、路由跳转等复杂能力。
- 是否应该拆成子组件而不是继续堆主页面。
- 是否使用了当前项目已有工具,避免重复手写日期范围、字典 options、树展开、loading、确认框。
- 是否误用了 Vue/Element Plus/generator 的简化逻辑。

View File

@ -1,41 +1,31 @@
---
name: frontend-crud-coding
description: 在 plus-ui 前端项目中按真实 Vue 3 + TypeScript + Element Plus + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入和样式壳。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与后端接口对应的 src/api、types 和 src/views 代码;默认参考 Gitee 仓库 JavaLionLi/plus-ui 的 6.X-Vue 分支,触发后应先读取适用 references再阅读目标模块真实代码和项目内 gen 代码生成模板。
description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript + Umi Max + Ant Design ProComponents + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入、样式和项目内置 gen/*.ftl 代码生成模板。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与 RuoYi-Vue-Plus boot4 后端接口对应的 src/api、types 和 src/pages 代码,或维护 gen/api.ts.ftl、gen/types.ts.ftl、gen/index.tsx.ftl、gen/index-tree.tsx.ftl触发后应先读取适用 references再阅读目标模块真实代码和项目内 gen 模板。
---
# 前端编码规范
先对齐当前前端项目里的真实实现,再参考项目内 `gen` 目录下的代码生成模板。不要只套通用 Vue 模板,也不要把 generator 模板原样复制进来而忽略当前项目已经演进出的 hooks、页面壳、类型入口和下载方式。
## 项目基线
- 基线仓库:`https://gitee.com/JavaLionLi/plus-ui`
- 默认分支:`6.X-Vue`
- 远端引用必须同时标记仓库、分支和文件路径,例如 `https://gitee.com/JavaLionLi/plus-ui/blob/6.X-Vue/gen/index.vue.ftl``branch=6.X-Vue, path=gen/index.vue.ftl`
- 不要在 skill、reference 或 agent 文档中写本机绝对路径;本地文件读取以运行时工作目录为准。
- 如果用户指定其他分支,先按用户分支读取同一仓库的对应文件,并在结果中说明使用的分支。
先对齐当前 React 项目里的真实实现,再参考项目内置 `gen/` 代码生成模板。Vue 版本 skill 和关联后端工程的代码生成器模板只用于理解字段、权限、任务分型和迁移意图。不要直接套 Vue 3、Element Plus、AxiosPromise 或 `src/views` 规则;要落成当前仓库的 `src/pages`、`src/api`、ProTable、ModalForm、权限、导出和公共工具方式。
## 执行流程
1. 判断任务类型:新增标准 CRUD、树表、已有页面增强、复杂业务页、只补 API/types。
2. 按“文档读取规则”读取必要 reference不一次性展开所有资料。
3. 阅读目标目录下最近似的真实代码:
- 标准单表优先看 `src/views/demo/demo/index.vue`、`src/api/demo/demo/*`。
- 树表优先看 `src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue`。
- 系统复杂页优先看 `src/views/system/user/index.vue`、`system/role`、`system/post`、`system/config`。
- workflow 业务页优先看 `src/views/workflow/*` 同类页面。
4. 新增标准页面前,对照项目内模板确认基础骨架:
- API 模板:`gen/api.ts.ftl`
- types 模板:`gen/types.ts.ftl`
- 标准单表页模板:`gen/index.vue.ftl`
- 树表页模板:`gen/index-tree.vue.ftl`
5. 新增代码时通常同步维护 `src/api/<module>/<business>/index.ts`、`types.ts`、`src/views/<module>/<business>/index.vue`。
6. 增强已有页面时只做增量修改,保留原页面的树筛选、导入导出、列显隐、权限、字典、弹窗和路由跳转能力。
7. 修改完成后按影响范围运行验证:优先 `pnpm exec vue-tsc --noEmit`,改动页面或导入时再跑 `pnpm lint`,大范围变更再跑 `pnpm build`
- 标准单表优先看 `src/pages/demo/demo/index.tsx`、`src/api/demo/demo/*`。
- 树表优先看 `src/pages/demo/tree/index.tsx`、`src/pages/workflow/category/index.tsx`。
- 系统复杂页优先看 `src/pages/system/user/index.tsx`、`system/role`、`system/post`、`system/config`。
- workflow 业务页优先看 `src/pages/workflow/*``src/api/workflow/*` 同类页面。
4. 新增或维护代码生成能力时,优先阅读当前仓库 `gen/api.ts.ftl`、`gen/types.ts.ftl`、`gen/index.tsx.ftl`、`gen/index-tree.tsx.ftl`。这些是 React 版内置 FreeMarker 生成模板,页面模板必须输出当前项目 React/TSX 风格。
5. 需要从 Vue 版本迁移经验时,参考 `D:\git-sources\Plus相关\plus-ui-new\.codex\skills\frontend-crud-coding``.claude\agents`,只吸收任务分型、增量修改和自检原则。
6. 新增标准页面前,可以对照后端工程 `D:\git-sources\Plus相关\RuoYi-Vue-Plus-boot4\ruoyi-modules\ruoyi-gen\src\main\resources\fm` 确认字段、权限和导出能力,但输出和 `gen/` 模板必须保持 React 项目风格。
7. 新增代码时通常同步维护 `src/api/<module>/<business>/index.ts`、`types.ts`、`src/pages/<module>/<business>/index.tsx`;如果是生成模板优化,同步维护 `gen/` 对应 `.ftl`
8. 增强已有页面时只做增量修改,保留原页面的树筛选、导入导出、列显隐、权限、字典、弹窗、抽屉和路由跳转能力。
9. 修改完成后按影响范围运行验证:优先 `pnpm exec tsc --noEmit``pnpm lint`;大范围页面、公共组件、构建配置变更再跑 `pnpm build`
## 文档读取规则
- 前端 API、types、页面、hooks、样式和验证规则先读 [references/frontend.md](references/frontend.md)。
- 前端 API、types、页面、hooks、样式、权限、导出和验证规则,先读 [references/frontend.md](references/frontend.md)。
- 不确定任务边界、需要标准用例或提问方式时,再读 [references/examples.md](references/examples.md)。
- reference 只约束实现方式和自检范围;发生冲突时,以当前模块真实代码和实际调用点为准。
@ -44,41 +34,46 @@ description: 在 plus-ui 前端项目中按真实 Vue 3 + TypeScript + Element P
发生冲突时按下面顺序决策:
1. 目标目录下最近似页面、API、types 的真实实现。
2. 当前项目公共 hooks、组件、工具和样式约定。
3. 项目内 `gen` 代码生成模板。
4. 通用 Vue 3 / Element Plus 习惯。
2. 当前项目公共 hooks、组件、工具、样式和请求封装约定。
3. 当前项目内置 `gen/*.ftl` React 代码生成模板。
4. Vue 参考项目 `.codex` / `.claude` 中的任务分型和工作流。
5. 关联后端工程 generator 模板。
6. 通用 React / Ant Design ProComponents 习惯。
也就是说:
- 同模块已有页面怎么写,优先怎么写。
- 没有现成页面时,使用项目内 `gen` 模板作为骨架,再改成当前项目风格
- 没有现成页面时,使用 demo 页面作为骨架,再按后端接口字段和权限补齐
- 复杂模块不能为了“标准 CRUD”退化成裸模板页。
- Vue 版本只作为迁移参考,不复制 Vue 组件、hooks、指令、样式类和类型导入。
## 仓库通用规则
- 遵循 [`.editorconfig`](../../../.editorconfig)UTF-8、LF、2 空格缩进Markdown 例外。
- 当前仓库没有 `.prettierrc`,格式脚本是 `pnpm run fmt` 调用 `oxfmt .`lint 脚本是 `pnpm lint` 调用 `oxlint src`
- 页面优先使用 `<script setup name="Xxx" lang="ts">`
- API 返回类型优先从 `@/utils/api-types` 引入 `AxiosPromise`,分页结果从 `@/api/types` 引入 `PageResult`
- 请求统一通过 `@/utils/request`,导出下载使用 `import { download as requestDownload } from '@/utils/request';`
- 标准列表页优先复用 `useLoading`、`useSearchToggle`、`useSearchReset`、`useTableSelection`、`useFormDialog`、`useDateRangeQuery`。
- 页面壳优先使用 `p-2 app-container <module>-<business>-page`、`search-panel`、`toolbar-shell`、`data-table`、`right-toolbar`、`pagination`。
- 新页面不要无故引入另一套状态管理、请求封装、样式体系或权限写法。
- 遵循 `.editorconfig` 和仓库现状UTF-8、2 空格缩进、TypeScript、TSX。
- 包管理使用 `pnpm`;格式脚本是 `pnpm run fmt`lint 脚本是 `pnpm lint`
- 技术栈是 React 19 + TypeScript + Umi Max + Ant Design 6 + Ant Design ProComponents + ahooks + TanStack Query + Zustand。
- 页面放在 `src/pages`,不是 Vue 项目的 `src/views`
- 请求统一通过 `src/api/request.ts`API 返回类型使用 `Promise<R<T>>`,不要从 `axios` 或 Vue 项目引入 `AxiosPromise`
- 分页结果使用 `PageResult<T>` from `@/api/types`,接口外层使用 `R<T>` from `@/api/types`
- 标准列表页优先复用 `ProTable`、`ModalForm`、`RowActions`、`useTableSelection`、`useTableExport`、`toPageQuery`、`toTableData`。
- 查询、字典、树表、loading、重置和确认弹窗优先复用 `useDateRangeQuery`、`dictOptions`、`useTreeTableExpand`、`useLoading`、`useSearchReset`、`confirmAction`/`confirmTitleSafe`。
- 权限用 `const userInfo = useUserStore(state => state.userInfo)``hasPermi(userInfo, ['module:business:action'])` 计算布尔值。
- 新页面不要无故引入另一套状态管理、请求封装、表格封装、样式体系或权限写法。
## 目录映射规则
通常按下面关系组织代码:
- 后端 `/system/user/*` 对应 `src/api/system/user/*``src/views/system/user/*`
- 后端 `/monitor/xxx/*` 对应 `src/api/monitor/xxx/*``src/views/monitor/xxx/*`
- 后端 `/workflow/xxx/*` 对应 `src/api/workflow/xxx/*``src/views/workflow/xxx/*`
- 后端 `/demo/xxx/*` 对应 `src/api/demo/xxx/*``src/views/demo/xxx/*`
- 后端 `/system/user/*` 对应 `src/api/system/user/*``src/pages/system/user/*`
- 后端 `/monitor/xxx/*` 对应 `src/api/monitor/xxx/*``src/pages/monitor/xxx/*`
- 后端 `/workflow/xxx/*` 对应 `src/api/workflow/xxx/*``src/pages/workflow/xxx/*`
- 后端 `/demo/xxx/*` 对应 `src/api/demo/xxx/*``src/pages/demo/xxx/*`
标准新增通常至少包含:
- `src/api/<module>/<business>/index.ts`
- `src/api/<module>/<business>/types.ts`
- `src/views/<module>/<business>/index.vue`
- `src/pages/<module>/<business>/index.tsx`
按业务复杂度,可能继续补:
@ -87,17 +82,17 @@ description: 在 plus-ui 前端项目中按真实 Vue 3 + TypeScript + Element P
- 树筛选面板
- 列显隐配置
- 分配/授权子页面
- 自定义 SCSS 样式
- 自定义 Less 样式
## 任务分型
### 1. 标准单表 CRUD
`gen/index.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl` 和 `src/views/demo/demo/index.vue` 为主要起点,补齐列表、搜索、分页、新增、编辑、删除、导出、权限、类型和验证。
`src/pages/demo/demo/index.tsx` 和 `src/api/demo/demo/*` 为主要起点,补齐列表、搜索、分页、新增、编辑、删除、导出、权限、类型和验证。
### 2. 树表 CRUD
`src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue` 为主要起点。列表接口通常返回数组而不是 `PageResult`,页面使用 `handleTree`、`useTreeTableExpand``Query` 通常不继承 `PageQuery`
`src/pages/demo/tree/index.tsx`、`src/pages/workflow/category/index.tsx` 为主要起点。列表接口通常返回数组,不使用分页 `PageResult`;页面使用 `handleTree`、`useTreeTableExpand`、`ProFormTreeSelect`
### 3. 强业务页面
@ -105,12 +100,23 @@ description: 在 plus-ui 前端项目中按真实 Vue 3 + TypeScript + Element P
### 4. 工作流页面
workflow 目录优先参考 `src/views/workflow/*`。流程定义、流程实例、任务列表、请假申请等页面通常有业务按钮、弹窗和路由跳转,不要硬套 system 模块。
workflow 目录优先参考 `src/pages/workflow/*`。流程定义、流程实例、任务列表、请假申请等页面通常有业务按钮、弹窗和路由跳转,不要硬套 system 模块。
### 5. 只补 API 和 types
只维护 `src/api/<module>/<business>/index.ts``types.ts`,但仍要与后端路由、返回结构、当前模块导入方式和类型入口一致。
### 6. 维护 React 生成模板
模板只放在当前仓库 `gen/` 下,除非用户明确要求写入 boot4 后端工程。维护时保持 FreeMarker 变量和文件名不变:
- `gen/api.ts.ftl`
- `gen/types.ts.ftl`
- `gen/index.tsx.ftl`
- `gen/index-tree.tsx.ftl`
模板内容必须生成当前 React 项目代码,并优先使用已有公共工具:`useDateRangeQuery`、`dictOptions`、`useTreeTableExpand`、`useTableSelection`、`useTableExport`、`confirmAction`、`toPageQuery`、`toTableData`、`handleTree`、`formatDateTimeFields`、`toDayjsFields`。不要把 Vue 模板、Element Plus 组件或 `src/views` 路径写入这些模板。
## 输出要求
使用本 skill 时,默认期望产出应满足:
@ -119,21 +125,26 @@ workflow 目录优先参考 `src/views/workflow/*`。流程定义、流程实例
- API 路径、函数名、权限标识与后端接口保持一致。
- 标准页查询、重置、分页、弹窗、提交、删除、导出流程闭环完整。
- 复杂页面保留原有交互能力和业务约束。
- 代码体现当前项目 hooks、页面壳和下载方式而不是 `gen` 模板裸输出。
- 代码体现当前项目 hooks、页面壳和下载方式而不是 Vue 版本或 generator 裸输出。
- 生成模板变更要同步反映当前项目公共 hooks/工具,避免生成的新页面重复手写日期范围、字典 options、树展开、loading、搜索重置和确认框逻辑。
- 交付前说明运行过的验证命令;如果无法验证,说明原因。
## 快速检查清单
- `AxiosPromise` 是否来自 `@/utils/api-types`。
- `PageResult` 是否来自 `@/api/types`
- API 是否从 `@/api/request` 引入 `request`。
- `R`、`PageResult`、`PageQuery`、`BaseEntity` 是否来自 `@/api/types`
- API `params``data` 是否与后端方法一致。
- 日期范围是否通过 `useDateRangeQuery` 或附近页面现有方式处理。
- 列表 loading 是否通过 `useLoading` 或原页面方式维护。
- 弹窗是否通过 `useFormDialog` 或原页面方式维护。
- 多选状态是否通过 `useTableSelection` 或原页面方式维护。
- 权限指令是否保持同文件一致,默认使用当前项目主流 `v-hasPermi`
- 导出是否使用 `requestDownload('<module>/<business>/export', { ...queryParams.value }, '<name>_<time>.xlsx')`
- 页面壳是否保留 `search-panel`、`table-panel`、`toolbar-shell`、`data-table`、`right-toolbar`、`pagination`。
- 分页表格 request 是否通过 `toPageQuery(params)``toTableData(res)`
- 导出是否通过 `useTableExport``exportFile('/module/business/export', () => 'name_timestamp.xlsx')`
- 多选是否通过 `useTableSelection<T>(row => row.id)`
- 行操作是否优先使用 `RowActions` 和 Ant Design 图标。
- 权限是否通过 `useUserStore` + `hasPermi` 计算,不要写 Vue 指令。
- 日期范围是否通过 `useDateRangeQuery``addDateRange` 相关工具处理。
- 字典 options 是否通过 `dictOptions`
- 树表是否使用 `handleTree`、`pagination={false}`、`useTreeTableExpand`、`ProFormTreeSelect`。
- loading 是否优先使用 `useLoading`,搜索重置是否优先使用 `useSearchReset`
- 需要 Promise 式确认时是否使用 `confirmAction``confirmTitleSafe`
- 代码生成模板是否仍输出 React TSX 项目风格。
## 推荐提问方式
@ -147,5 +158,5 @@ workflow 目录优先参考 `src/views/workflow/*`。流程定义、流程实例
例如:
- 使用 `$frontend-crud-coding``/system/client` 补一套标准 CRUD 页面,参考 `gen` 模板、`demo/demo` 和 `system/client`
- 使用 `$frontend-crud-coding` 修改 `workflow/category` 列表页,增加导出按钮和状态筛选,保持当前 workflow 风格。
- 使用 `$frontend-crud-coding``/system/client` 补一套 React 标准 CRUD 页面,参考 `demo/demo`、现有 `system/client` 和 boot4 generator 模板
- 使用 `$frontend-crud-coding` 修改 `workflow/category` 列表页,增加导出按钮和状态筛选,保持当前 workflow 树表风格。

View File

@ -1,7 +1,7 @@
interface:
display_name: "前端编码"
short_description: "按 plus-ui 6.X-Vue 真实代码和 gen 模板编写前端 CRUD"
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference再按 plus-ui Gitee 仓库 6.X-Vue 分支的真实页面、hooks、API/types 约定和项目内 gen 模板实现前端修改。"
short_description: "按 plus-ui-react 真实代码和 gen 模板编写 React CRUD"
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference再按 plus-ui-react 真实页面、项目内置 gen 模板、API/types、ProTable、ModalForm、权限、导出和公共工具约定实现前端修改。"
policy:
allow_implicit_invocation: true

View File

@ -5,18 +5,19 @@
### 用户提问示例
```text
使用 $frontend-crud-coding 为 system/client 补一套前端 CRUD 页面。
使用 $frontend-crud-coding 为 system/client 补一套 React 前端 CRUD 页面。
后端接口已经有 /system/client/list、/system/client/{id}、POST /system/client、PUT /system/client、DELETE /system/client/{ids}。
请参考 plus-ui Gitee 仓库 6.X-Vue 分支的项目内 gen 模板、src/views/demo/demo/index.vue 和现有 system/client 风格实现。
请参考 src/pages/demo/demo/index.tsx 和现有 system/client 风格实现。
```
### 期望执行方式
- 先看 `src/api/system/client/*``src/views/system/client/index.vue` 是否已存在。
- 再看 `src/views/demo/demo/index.vue` 的标准 hooks 版 CRUD 骨架。
- 对照项目内 `gen/api.ts.ftl`、`gen/types.ts.ftl`、`gen/index.vue.ftl`。
- 生成或修改 `api/index.ts`、`types.ts`、`views/.../index.vue`。
- 使用 `AxiosPromise` from `@/utils/api-types`、`PageResult` from `@/api/types`、`useLoading`、`useFormDialog`、`useSearchReset`、`useTableSelection`。
- 先看 `src/api/system/client/*``src/pages/system/client/index.tsx` 是否已存在。
- 再看 `src/pages/demo/demo/index.tsx` 的标准 React CRUD 骨架。
- 新增标准生成能力时优先对照当前仓库 `gen/index.tsx.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`boot4 generator 只用于核对字段、权限和接口。
- 生成或修改 `api/index.ts`、`types.ts`、`pages/.../index.tsx`。
- 使用 `request<R<PageResult<T>>>`、`ProTable`、`ModalForm`、`RowActions`、`useTableSelection`、`useTableExport`。
- 日期范围、字典 options、确认框、loading 等优先使用 `useDateRangeQuery`、`dictOptions`、`confirmAction`、`useLoading` 等项目工具。
## 案例 2新增树表页面
@ -24,17 +25,33 @@
```text
使用 $frontend-crud-coding 为 demo/tree2 新增树表 CRUD接口返回数组字段包含 id、parentId、name、orderNum。
参考 src/views/demo/tree/index.vue 和 workflow/category。
参考 src/pages/demo/tree/index.tsx 和 workflow/category。
```
### 期望执行方式
- 优先判断这是树表,不生成分页 `PageResult` 页面。
- API 列表返回 `AxiosPromise<Tree2VO[]>`。
- 判断这是树表,不生成分页 `PageResult` 页面。
- API 列表返回 `R<Tree2VO[]>`。
- `Query` 不继承 `PageQuery`
- 页面使用 `handleTree`、`row-key`、`tree-props`、`useTreeTableExpand`、`el-tree-select`。
- 页面使用 `handleTree`、`pagination={false}`、`useTreeTableExpand`、`ProFormTreeSelect`。
- 新增子节点时从当前行带入 `parentId`
## 案例 2.1:优化 React 代码生成模板
### 用户提问示例
```text
使用 $frontend-crud-coding 优化当前项目 gen 目录下的 React 代码生成模板,让生成页面继续使用新加的工具。
```
### 期望执行方式
- 读取 `gen/api.ts.ftl`、`gen/types.ts.ftl`、`gen/index.tsx.ftl`、`gen/index-tree.tsx.ftl`。
- 保持 FreeMarker 变量、宏和文件名不变。
- 普通表模板继续使用 `useTableSelection`、`useTableExport`、`useDateRangeQuery`、`dictOptions`、`confirmAction`。
- 树表模板继续使用 `handleTree`、`filterTree`、`useTreeTableExpand`、`dictOptions`。
- 输出仍是 React TSX 项目代码,不写 Vue `src/views`、Element Plus 或 `AxiosPromise`
## 案例 3修改已有复杂列表页
### 用户提问示例
@ -42,15 +59,15 @@
```text
使用 $frontend-crud-coding 修改 system/user 页面:
1. 新增一个创建时间快捷筛选
2. 导出按钮保留在更多菜单
3. 保持现有树筛选、导入、列显隐、详情抽屉和角色分配不变
2. 导出按钮保留在工具栏
3. 保持现有树筛选、导入、详情抽屉和角色分配不变
```
### 期望执行方式
- 判断这是“已有复杂页面增强”,不是重新生成 CRUD。
- 优先阅读 `src/views/system/user/index.vue`
- 保留 `TreePanel`、导入弹窗、`right-toolbar` 列显隐、`UserViewDrawer`、角色分配路由、权限控制。
- 优先阅读 `src/pages/system/user/index.tsx` 和相关子组件
- 保留 `TreePanel`、导入弹窗、`UserDetailDrawer`、角色分配路由、权限控制。
- 只增量修改搜索和查询参数处理。
## 案例 4修改 workflow 页面
@ -63,8 +80,8 @@
### 期望执行方式
- 优先看 `src/views/workflow/category/index.vue` 和 `src/api/workflow/category/*`
- 判断是否需要后端新增导出接口;前端导出路径保持 `workflow/category/export`。
- 优先看 `src/pages/workflow/category/index.tsx` 和 `src/api/workflow/category/*`
- 判断是否需要后端新增导出接口;前端导出路径保持 `/workflow/category/export`。
- 不迁移 system/user 的用户专属逻辑。
- 保留树表、`useTreeTableExpand`、`handleTree` 和分类弹窗逻辑。
@ -79,8 +96,8 @@
### 期望执行方式
- 只维护 `src/api/monitor/cache/index.ts``src/api/monitor/cache/types.ts`
- 仍然检查同目录 monitor API 的 `export function` / `export const` 风格。
- 返回类型使用 `AxiosPromise` from `@/utils/api-types`。
- 仍然检查同目录 monitor API 的命名、返回结构和导出风格。
- 返回类型使用 `R<T>` 和 `PageResult<T>` from `@/api/types`。
- 不创建页面,不改路由。
## 案例 6接入后端新增状态切换接口
@ -88,26 +105,26 @@
### 用户提问示例
```text
使用 $frontend-crud-coding 给 system/client 页面接入 PUT /system/client/changeStatus状态字段 status参考 gen 模板
使用 $frontend-crud-coding 给 system/client 页面接入 PUT /system/client/changeStatus状态字段 status参考 system/user
```
### 期望执行方式
- API 增加 `changeClientStatus(id, status)`
- types 确认 `status` 类型是 string、number 还是 boolean。
- 表格列用 `el-switch`active/inactive 值跟后端字段类型一致。
- 切换失败时回滚原状态。
- 表格列用 Ant Design `Switch`checked 值跟后端字段类型一致。
- 切换失败时回滚原状态或刷新列表
- 权限使用 `system:client:edit` 或后端实际权限。
## 推荐的高质量任务描述
```text
使用 $frontend-crud-coding 在当前前端项目中`/system/notice` 列表页增强
使用 $frontend-crud-coding 在当前 React 前端项目中增 `/system/notice` 列表页:
1. 保留现有页面
2. 新增状态筛选和导出
3. API 路径沿用后端接口
4. 参考 system/config 的工具栏与导出交互
5. 参考 plus-ui Gitee 仓库 6.X-Vue 分支的 gen 模板补齐缺失 types
5. 参考当前项目 gen 模板和 boot4 generator 字段补齐缺失 types
```
## 不推荐的任务描述

View File

@ -2,41 +2,36 @@
## 优先参考的代码来源
- 默认远端基线:`https://gitee.com/JavaLionLi/plus-ui`,分支 `6.X-Vue`。引用远端文件时必须同时写明 `branch=6.X-Vue` 和仓库内路径。
- 当前目标目录下最近似页面、API、types。
- 标准单表:`src/views/demo/demo/index.vue`、`src/api/demo/demo/index.ts`、`src/api/demo/demo/types.ts`。
- 树表:`src/views/demo/tree/index.vue`、`src/views/workflow/category/index.vue`。
- 复杂系统页:`src/views/system/user/index.vue`、`src/views/system/role/index.vue`、`src/views/system/post/index.vue`、`src/views/system/config/index.vue`。
- workflow 页:`src/views/workflow/*`、`src/api/workflow/*`。
- 监控页:`src/views/monitor/*`、`src/api/monitor/*`。
- 公共 hooks`src/hooks/async/useLoading.ts`、`src/hooks/dialog/*`、`src/hooks/form/*`、`src/hooks/table/*`、`src/hooks/tree/*`。
- 项目内 generator 模板:
`gen/api.ts.ftl`
`gen/types.ts.ftl`
`gen/index.vue.ftl`
`gen/index-tree.vue.ftl`
- 当前仓库内置 React 生成模板:`gen/api.ts.ftl`、`gen/types.ts.ftl`、`gen/index.tsx.ftl`、`gen/index-tree.tsx.ftl`。
- 标准单表:`src/pages/demo/demo/index.tsx`、`src/api/demo/demo/index.ts`、`src/api/demo/demo/types.ts`。
- 树表:`src/pages/demo/tree/index.tsx`、`src/pages/workflow/category/index.tsx`。
- 复杂系统页:`src/pages/system/user/index.tsx`、`src/pages/system/role/index.tsx`、`src/pages/system/post/index.tsx`、`src/pages/system/config/index.tsx`。
- workflow 页:`src/pages/workflow/*`、`src/api/workflow/*`。
- 监控页:`src/pages/monitor/*`、`src/api/monitor/*`。
- 公共 hooks`src/hooks/useTableSelection.ts`、`src/hooks/useTableExport.ts`、`src/hooks/useDict.ts`、`src/hooks/useDateRangeQuery.ts`、`src/hooks/useTreeTableExpand.ts`、`src/hooks/useLoading.ts`、`src/hooks/useSearchReset.ts`。
- 公共组件:`src/components/common/RowActions.tsx`、`TreePanel.tsx`、`RightToolbar.tsx`、`DictTag.tsx`、上传/预览组件。
- 工具函数:`src/utils/ruoyi.ts`、`src/utils/permission.ts`、`src/utils/download.ts`、`src/utils/dict.ts`、`src/utils/modal.ts`。
## 基础栈与格式
- 技术栈是 Vue 3 + TypeScript + Element Plus + Vite + Pinia
- 技术栈是 React + TypeScript + Umi Max + Ant Design + ProComponents + ahooks + Zustand + TanStack Query
- 包管理按仓库现状使用 pnpm。
- `.editorconfig` 要求 UTF-8、LF、2 空格缩进。
- 当前仓库没有 `.prettierrc`;格式化使用 `pnpm run fmt`lint 使用 `pnpm lint`
- `.editorconfig` 要求 UTF-8、2 空格缩进。
- 当前仓库没有 Prettier;格式化使用 `pnpm run fmt`lint 使用 `pnpm lint`
- 不要在一个页面里混入与仓库不一致的格式和写法。
- 不要把本机绝对路径写进 skill 输出或参考文档;跨环境参考统一使用 Gitee 仓库 URL、分支和仓库内相对路径。
## API 文件规则
- 标准 API 文件放在 `src/api/<module>/<business>/index.ts`,同目录维护 `types.ts`
- import 顺序优先跟随附近文件,标准生成页常见形式:
`import type { XxxForm, XxxQuery, XxxVO } from '@/api/<module>/<business>/types';`
`import type { PageResult } from '@/api/types';`
`import type { AxiosPromise } from '@/utils/api-types';`
`import request from '@/utils/request';`
- 不要从 `axios` 引入 `AxiosPromise`
- 列表分页接口通常返回 `AxiosPromise<PageResult<XxxVO>>`
- 树表列表接口通常返回 `AxiosPromise<XxxVO[]>`
- 详情接口返回 `AxiosPromise<XxxVO>`;复杂详情返回单独的 `InfoVO`
- import 顺序优先跟随附近文件,标准形式:
`import type { PageResult, R } from '@/api/types';`
`import request from '@/api/request';`
`import type { XxxForm, XxxQuery, XxxVO } from './types';`
- 不要从 `axios` 引入 `AxiosPromise`;当前 `request<T>()` 返回 `Promise<T>`,接口函数通常返回 `Promise<R<T>>`
- 列表分页接口通常返回 `request<R<PageResult<XxxVO>>>({ url, method: 'get', params: query })`
- 树表列表接口通常返回 `request<R<XxxVO[]>>({ url, method: 'get', params: query })`
- 详情接口返回 `R<XxxVO>`;复杂详情返回单独的 `InfoVO`
- 标准函数命名:
`listXxx` -> `GET /<module>/<business>/list`
`getXxx` -> `GET /<module>/<business>/{id}`
@ -46,8 +41,8 @@
`changeXxxStatus` -> `PUT /<module>/<business>/changeStatus`
- query string 用 `params`,请求体用 `data`
- 加密、防重复提交等 headers 直接写在请求配置里,例如用户重置密码中的 `isEncrypt`、`repeatSubmit`。
- 当前仓库有些 API 使用 `export const`,有些使用 `export function`;新增标准 CRUD 优先跟随 `gen/api.ts.ftl`相邻模块。
- 只有相邻模块已有 `export default { ... }` 聚合时才新增默认导出。
- 当前仓库多数 API 使用 `export function`;新增标准 CRUD 优先跟随相邻模块。
- 只有相邻模块已有聚合对象时才新增默认导出。
## 类型文件规则
@ -57,100 +52,103 @@
- 树表 `Query` 通常不继承 `PageQuery`
- ID 字段通常使用 `string | number`,批量删除参数使用 `string | number | Array<string | number>`
- Java 数值类型映射为 `number`Boolean 映射为 `boolean`,日期/文本默认 `string`
- 日期范围查询保留 `params?: any`,不要因为它看起来宽松就删掉。
- 日期范围查询保留 `params?: Record<string, unknown>` 或跟随相邻页面现有类型,不要因为它看起来宽松就删掉。
- 列表对象、表单对象、查询对象职责分开;字段不一致时不要强行复用一个接口。
- 能明确写出类型时不要用 `any`;组件库、字典或历史接口确实无法收窄时再保留。
## Vue 页面结构规则
## React 页面结构规则
- 页面优先使用 `<script setup name="Xxx" lang="ts">`
- 标准根节点使用 `class="p-2 app-container <module>-<business>-page"`;已有页面是特殊布局时保持原样。
- 标准列表页结构:
搜索卡片 `search-panel`
表格卡片 `table-panel`
工具栏 `toolbar-shell`
表格 `data-table`
`right-toolbar`
`pagination`
新增/编辑 `el-dialog`
- 搜索区通过 `useSearchToggle` 控制 `showSearch`,头部点击切换。
- 列表 loading 通过 `useLoading(true)``withLoading`
- 选择状态通过 `useTableSelection<XxxVO>(item => item.id)` 返回 `ids`、`single`、`multiple`、`handleSelectionChange`。
- 表单弹窗优先使用 `useFormDialog({ form, formRef, initialFormData })`,返回 `dialog`、`resetForm`、`openDialog`、`showDialog`、`closeDialog`。
- 仅需要简单弹窗状态或一个页面多个弹窗时使用 `useDialogState`
- 日期范围查询优先使用 `useDateRangeQuery()`;带后端参数名时使用 `useDateRangeQuery('CreateTime')` 等相邻页面模式。
- 查询和表单状态通常放在 `reactive<PageData<Form, Query>>({ form, queryParams, rules })`,再 `toRefs(data)`
- 页面默认导出函数组件,命名如 `SystemUserPage`、`DemoDemoPage`、`WorkflowCategoryPage`。
- 标准页使用 `PageContainer` 包裹 `ProTable``ModalForm`
- 标准列表页常见状态:
`actionRef`、`form`、`modalOpen`、`modalTitle`、`ids`、`selectedOne`、权限布尔值。
- 弹窗状态优先使用 `useBoolean(false)`,表单用 `Form.useForm<XxxForm>()`
- ProTable 使用:
`rowKey`
`columns`
`search={{ labelWidth: 90 }}`
`pagination={{ defaultPageSize: 10, showSizeChanger: true }}`
`request={async params => { ... }}`
`toolBarRender={() => [...]}`
- 分页查询使用 `toPageQuery(params)`,然后 `updateExportParams(query)`,最后 `return toTableData(res)`
- 表格列类型使用 `ProColumns<XxxVO>[]`
- 新增/编辑弹窗使用 `ModalForm<XxxForm>`,字段优先使用 `ProFormText`、`ProFormDigit`、`ProFormSelect`、`ProFormTreeSelect`、`ProFormDateTimeRangePicker` 等 ProForm 组件。
- ModalForm 成功提交后返回 `true`,并刷新 `actionRef.current?.reload()`
## 页面行为规则
- `getList` 负责设置 loading、调用列表接口、回填列表和 `total`
- `handleQuery` 先把 `queryParams.value.pageNum = 1`,再调用 `getList()`;树表无分页时只调用 `getList()`
- `resetQuery` 使用 `useSearchReset`,分页页传 `pageNumKey: 'pageNum'`,需要时传 `pageSizeKey``resetExtras`
- `handleAdd` 使用 `openDialog('添加xxx')`;如果有树/联动选项,打开前后按现有页面加载选项。
- `handleUpdate``reset()`,再按行或 `ids.value[0]` 查详情,`Object.assign(form.value, res.data)`,最后 `showDialog('修改xxx')`
- `submitForm` 表单校验通过后设置 `buttonLoading`,根据主键判断新增或修改,成功后 `modal.msgSuccess('操作成功')`、关闭弹窗、刷新列表。
- `handleDelete` 使用 `modal.confirm(...)` 二次确认,再调用删除接口,成功提示并刷新。
- `handleExport` 使用 `requestDownload('<module>/<business>/export', { ...queryParams.value }, '<business>_<timestamp>.xlsx')`
- 状态切换失败时要把 switch 值回滚,参考 generator 模板和现有 `system/user`、`system/role`。
- 导入上传使用 `globalHeaders()`、`import.meta.env.VITE_APP_BASE_API`、`ElUpload`,优先参考 `system/user` 或流程定义页面。
- `openAdd` 负责 `form.resetFields()`、设置默认值、设置标题、打开弹窗。
- `openEdit` 先按行或 `selectedOne` 找目标,再查详情,`form.resetFields()``form.setFieldsValue(res.data)`,最后打开弹窗。
- `submitForm` 根据主键判断新增或修改,成功后 `message.success('操作成功')`、重置表单、刷新列表。
- `remove``handleDelete` 支持行删除和批量删除,成功后 `message.success('删除成功')`、`clearSelection()`、`reloadAndRest` 或 `reload`
- 批量按钮通常用 `Popconfirm`,行操作确认优先放在 `RowActions``confirm`
- 状态切换失败时要回滚或刷新,参考 `src/pages/system/user/index.tsx`
- 日期范围查询优先使用 `useDateRangeQuery`,底层通过 `addDateRange` 写入 `params.beginXxx/endXxx`
- 字典 options 优先使用 `dictOptions(dicts.xxx)`
- 导入上传参考 `system/user/components/UserImportModal.tsx` 或流程定义导入弹窗,保留 `globalHeaders()``appEnv.baseApi` 相关方式。
## 字典、权限与公共工具
- 字典使用 `useDict`
`const { sys_normal_disable } = toRefs<any>(useDict('sys_normal_disable'));`
- 新增代码默认使用当前项目主流 `v-hasPermi`
- 如果正在修改的文件已经混用或使用 `v-has-permi`,保持同文件现状,不为统一写法而重排无关代码。
- `el-dropdown-item` 延迟加载导致权限指令不可靠时,使用 `v-if="checkPermi([...])"`,参考 `system/user`
- 字典使用 `const dicts = useDict('sys_normal_disable', 'sys_user_gender')`
- 字典 options 使用 `dictOptions` from `@/utils/dict` 映射为 `{ label, value }`
- 权限使用:
`const userInfo = useUserStore(state => state.userInfo);`
`const canAdd = hasPermi(userInfo, ['system:user:add']);`
- 不要使用 Vue 权限指令 `v-hasPermi`
- 常用工具:
`modal` from `@/plugins/modal`
`download as requestDownload` from `@/utils/request`
`useDict` from `@/utils/dict`
`checkPermi` from `@/utils/permission`
`handleTree`、`parseStrEmpty` from `@/utils/ruoyi`
`hasPermi` from `@/utils/permission`
`handleTree`、`parseStrEmpty`、`toPageQuery`、`toTableData`、`addDateRange`、`formatDateTimeRange` from `@/utils/ruoyi`
`dictOptions` from `@/utils/dict`
`confirmAction`、`confirmTitleSafe` from `@/utils/modal`
`download` via `useTableExport`
## 组件与样式规则
- 优先复用公共组件:`right-toolbar`、`pagination`、`DictTag` / `dict-tag`、`ImageUpload` / `image-upload`、`ImagePreview` / `image-preview`、`FileUpload` / `file-upload`、`Editor` / `editor`、`TreePanel`。
- 标准页面尽量使用已有页面壳类,不堆大量内联样式。
- 复杂页面需要 scoped SCSS 时优先复用:
`@use '@/assets/styles/components/page-shell' as pageShell;`
再按现有 mixin 使用。
- 行操作优先复用 `RowActions`,并使用 `@ant-design/icons` 图标。
- 树筛选优先复用 `TreePanel`,参考 `system/user`
- 导出优先复用 `useTableExport`
- 多选优先复用 `useTableSelection`
- 异步 loading 优先复用 `useLoading`,不要重复手写 `setLoading(true)` + `finally setLoading(false)`
- ProTable 重置后还要清空额外筛选状态时,优先复用 `useSearchReset`
- Promise 式确认框优先复用 `confirmAction``confirmTitleSafe`
- 标准页面尽量使用 `PageContainer`、`ProTable`、ProComponents 页面壳,不堆大量内联样式。
- 需要自定义布局时先查 `src/assets/styles` 是否已有 `.tree-table-page`、`.table-panel`、`.page-surface` 等类。
- 不要为了单页需求修改全局组件样式。
- 类名保持模块语义:`system-client-page`、`workflow-category-page`、`demo-demo-page`。
## 树表规则
- 树表列表接口通常返回数组,页面通过 `handleTree<T>(res.data, 'id', 'parentId')` 组树。
- 使用 `row-key`、`:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"`。
- 展开/折叠使用 `useTreeTableExpand`
- 表单中上级节点使用 `el-tree-select`
- 树表列表接口通常返回数组,页面通过 `handleTree<T>(res.data || [], 'id', 'parentId')` 组树。
- `ProTable` 使用 `pagination={false}`
- 使用 `useTreeTableExpand` 控制 `expandedRowKeys`、`onExpandedRowsChange`、`syncExpandedRows`、`toggleExpandAll`。
- 展开/折叠按钮参考 `demo/tree``workflow/category`,用 `SortAscendingOutlined`
- 表单中上级节点使用 `ProFormTreeSelect`
- 新增子节点时从当前行回填 `parentId`
- 删除确认文案优先使用业务名称,而不是批量 ID 文案。
## 与 gen 模板的关系
## 与 gen 模板、Vue 参考项目和后端生成器的关系
- `gen` 是 plus-ui 前端项目内的生成模板;默认远端位置是 `https://gitee.com/JavaLionLi/plus-ui/tree/6.X-Vue/gen`,分支必须标记为 `6.X-Vue`
- 本地仓库存在对应文件时优先读本地 `gen`,但不要把本地绝对路径写入 skill 或交付内容。
- 新增标准单表页面时读取 `gen/index.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
- 新增树表页面时读取 `gen/index-tree.vue.ftl`、`gen/api.ts.ftl`、`gen/types.ts.ftl`。
- `gen` 模板是标准骨架,不是最终答案;落地时仍要对照目标模块真实页面和公共 hooks。
- 当前前端项目已经把生成页升级为 hooks 版:`useLoading`、`useFormDialog`、`useSearchReset`、`useTableSelection`、`useDateRangeQuery`。
- 新增标准 CRUD 时,先从 `gen` 确认字段、权限、导出、状态切换、排序、日期范围等,再落成当前项目的实际页面壳。
- 修改已有页面时,不要把现有强业务逻辑替换回 `gen` 的简化逻辑。
- 当前仓库 `gen/*.ftl` 是 React 版内置代码生成模板,是维护生成能力时的第一参考。
- `gen/index.tsx.ftl`、`gen/index-tree.tsx.ftl` 内容必须生成 React TSX 页面。
- 修改公共 hooks/工具后,如果它们能简化标准生成页,要同步评估 `gen/*.ftl` 是否需要更新。
- Vue skill 和 `.claude/agents` 提供的是任务分型、优先级、增量修改、自检方式,不是 React 实现模板。
- boot4 后端 generator 模板可用于确认字段、权限、导出、状态切换、排序、日期范围等,但不是最终答案。
- 当前 React 项目的核心骨架是 `ProTable`、`ModalForm`、`RowActions`、`useTableSelection`、`useTableExport`。
- 修改已有页面时,不要把现有强业务逻辑替换回 generator 的简化逻辑。
## 验证规则
- 只改文档或 skill运行 skill 基础校验即可
- 改前端 TS/Vue/API/types优先运行 `pnpm exec vue-tsc --noEmit`。
- 改页面模板、import、权限或较多文件运行 `pnpm lint`
- 只改文档、skill 或 `gen/*.ftl` 模板:至少运行 skill 基础校验或 `git diff --check`;如果模板改动依赖项目工具,优先再跑 `pnpm lint`
- 改前端 TS/TSX/API/types优先运行 `pnpm exec tsc --noEmit``pnpm lint`。
- 改页面、import、权限或较多文件运行 `pnpm lint`
- 改公共 hooks、组件、构建相关或大范围页面再运行 `pnpm build`
- 如果验证因为环境、依赖或权限失败,交付时说明失败命令和原因。
## 避免事项
- 不要从 `axios` 引入 `AxiosPromise`
- 不要绕开 `request` 或 `requestDownload` 自造请求/下载封装。
- 不要绕开 `@/api/request` 或 `useTableExport` 自造请求/下载封装。
- 不要跳过 `types.ts`,把类型全写在页面里。
- 不要删除日期范围 `params`、权限指令、导出、导入、树筛选、列显隐等现有能力。
- 不要把 `src/pages` 写成 Vue 项目的 `src/views`
- 不要使用 Vue 的 `ref`、`reactive`、`toRefs`、`v-hasPermi`、Element Plus 组件或 SCSS 页面壳。
- 不要删除日期范围 `params`、权限判断、导出、导入、树筛选、列显隐等现有能力。
- 不要为了“更整洁”重写复杂页面的大块业务逻辑。
- 不要在新增标准页里使用与仓库不一致的 UI 壳或状态管理方式。

View File

@ -1,21 +1,9 @@
# 告诉EditorConfig插件这是根文件不用继续往上查找
root = true
# 匹配全部文件
[*]
# 缩进风格可选space、tab
indent_style = space
# 缩进的空格数
indent_size = 2
# 设置字符集
charset = utf-8
# 结尾换行符可选lf、cr、crlf
end_of_line = lf
# 在文件结尾插入新行
trim_trailing_whitespace = true
# 删除一行中的前后空格
insert_final_newline = true
indent_style = space
indent_size = 2
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@ -1,42 +1,15 @@
# 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
# 开发环境配置
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = '/dev-api'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'
# 监控地址
VITE_APP_MONITOR_ADMIN = 'http://localhost:9090/admin/applications'
# SnailJob 控制台地址
VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
# SnailAI 控制台地址
VITE_APP_SNAILAI_ADMIN = 'http://localhost:8900/snail-ai'
VITE_APP_PORT = 80
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT = true
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
# 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
# 统一消息推送开关
VITE_APP_MESSAGE_ENABLED = true
# sse / websocket
VITE_APP_MESSAGE_TRANSPORT = 'sse'
# 统一消息推送路径
VITE_APP_MESSAGE_PATH = '/resource/message'
VITE_APP_TITLE=RuoYi-React-Plus后台管理系统
VITE_APP_LOGO_TITLE=RuoYi-React-Plus
VITE_APP_ENV=development
VITE_APP_BASE_API=/dev-api
VITE_APP_CONTEXT_PATH=/
VITE_APP_PORT=8000
VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
VITE_APP_MESSAGE_ENABLED=true
VITE_APP_MESSAGE_TRANSPORT=sse
VITE_APP_MESSAGE_PATH=/resource/message
VITE_APP_ENCRYPT=true
VITE_APP_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB
VITE_APP_RSA_PRIVATE_KEY=MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A==
VITE_APP_MONITOR_ADMIN=http://localhost:9090/admin/applications
VITE_APP_SNAILJOB_ADMIN=http://localhost:8800/snail-job

View File

@ -1,45 +1,15 @@
# 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
# 生产环境配置
VITE_APP_ENV = 'production'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'
# 监控地址
VITE_APP_MONITOR_ADMIN = '/admin/applications'
# SnailJob 控制台地址
VITE_APP_SNAILJOB_ADMIN = '/snail-job'
# SnailAI 控制台地址
VITE_APP_SNAILAI_ADMIN = '/snail-ai'
# 生产环境
VITE_APP_BASE_API = '/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip
VITE_APP_PORT = 80
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT = true
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
# 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
# 统一消息推送开关
VITE_APP_MESSAGE_ENABLED = true
# sse / websocket
VITE_APP_MESSAGE_TRANSPORT = 'sse'
# 统一消息推送路径
VITE_APP_MESSAGE_PATH = '/resource/message'
VITE_APP_TITLE=RuoYi-React-Plus后台管理系统
VITE_APP_LOGO_TITLE=RuoYi-React-Plus
VITE_APP_ENV=production
VITE_APP_BASE_API=/prod-api
VITE_APP_CONTEXT_PATH=/
VITE_APP_PORT=8000
VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
VITE_APP_MESSAGE_ENABLED=true
VITE_APP_MESSAGE_TRANSPORT=sse
VITE_APP_MESSAGE_PATH=/resource/message
VITE_APP_ENCRYPT=true
VITE_APP_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB
VITE_APP_RSA_PRIVATE_KEY=MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A==
VITE_APP_MONITOR_ADMIN=/admin/applications
VITE_APP_SNAILJOB_ADMIN=/snail-job

37
.gitignore vendored
View File

@ -1,29 +1,10 @@
.DS_Store
.history
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
node_modules
dist
.umi
.umi-production
src/.umi
src/.umi-production
.env.local
*.log
.idea/
*.iml
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock
# 编译生成的文件
auto-imports.d.ts
components.d.ts

10
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="BUILD_PROCESS_HEAP_SIZE" value="2048" />
</component>
</project>

3
.idea/dictionaries/LionLi.xml generated Normal file
View File

@ -0,0 +1,3 @@
<component name="ProjectDictionaryState">
<dictionary name="LionLi" />
</component>

6
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

17
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<selected-state>
<State>
<id>User defined</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="liberica-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/plus-ui-react.iml" filepath="$PROJECT_DIR$/.idea/plus-ui-react.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,13 +1,6 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": [
"src/types/components.d.ts",
"src/types/auto-imports.d.ts",
".ai_state",
".claude",
".codex",
"doc"
],
"ignorePatterns": ["src/.umi", "src/.umi-production", ".ai_state", ".claude", ".codex", "dist", "node_modules"],
"printWidth": 120,
"singleQuote": true,
"trailingComma": "none",

View File

@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "vue"],
"plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "react"],
"rules": {
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
@ -12,7 +12,13 @@
"import/no-unassigned-import": "off",
"import/no-named-as-default-member": "off",
"import/no-named-as-default": "off",
"react/react-in-jsx-scope": "off",
"react/no-unstable-nested-components": "off",
"react/iframe-missing-sandbox": "off",
"react-hooks/exhaustive-deps": "off",
"no-shadow": "off",
"unicorn/no-array-sort": "off",
"unicorn/no-useless-fallback-in-spread": "off",
"unicorn/prefer-add-event-listener": "off",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-instanceof-builtins": "off"
@ -21,5 +27,14 @@
"correctness": "error",
"suspicious": "error"
},
"ignorePatterns": [".ai_state", ".claude", ".codex", "doc", "dist/**", "dist-ssr/**", "coverage/**"]
"ignorePatterns": [
"src/.umi",
"src/.umi-production",
".ai_state",
".claude",
".codex",
"dist/**",
"dist-ssr/**",
"coverage/**"
]
}

20
LICENSE
View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2019 RuoYi-Vue-Plus
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,21 +1,23 @@
# RuoYi-React-Plus
## 平台简介
- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本
- 官方项目: 基于 React + Ant Design 版本前端项目 [plus-ui-react](https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React/)
- 本仓库为前端技术栈 [React](https://react.dev/) + [TypeScript](https://www.typescriptlang.org/) + [Ant Design](https://ant.design/) + [Umi Max](https://umijs.org/) + [Vite](https://vitejs.dev/)
- 官方项目: 基于 Vue + ElementPlus 版本前端项目 [plus-ui](https://gitee.com/JavaLionLi/plus-ui)
- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
- 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
## 配套后端代码仓库地址
| 介绍 | 项目名 | 项目地址 |
| ----------------- | :--------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 🔥 分布式集群框架 | RuoYi-Vue-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus)<br> - [GitHub](https://github.com/dromara/RuoYi-Vue-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) |
| 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
| 介绍 | 项目名 | 项目地址 |
| -------------- | :--------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 分布式集群框架 | RuoYi-Vue-Plus | [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus) / [GitHub](https://github.com/dromara/RuoYi-Vue-Plus) / [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) |
| 微服务框架 | RuoYi-Cloud-Plus | [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus) / [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus) / [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
## 分支说明
- 6.X分支(稳定发布主分支 生产可用)
- dev分支(开发分支 开发过程中使用)
- 6.X-react分支(稳定发布主分支 生产可用)
- dev-react分支(开发分支 开发过程中使用)
## 前端运行
@ -29,7 +31,7 @@ pnpm dev
# 构建生产环境
pnpm build:prod
# 前端访问地址 http://localhost:80
# 前端访问地址 http://localhost:8000
```
## 本框架与RuoYi的业务差异
@ -61,16 +63,16 @@ pnpm build:prod
| | |
|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| ![输入图片说明](https://foruda.gitee.com/images/1780299033689126697/868ef1ea_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299052163404649/8d94165d_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299074949590692/27f5bfb5_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299108816841231/619a7c57_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299089818500856/862ba805_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299173744267947/95cb0cd3_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299193694706123/28257dc1_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299147525013883/ebcd9dfe_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299220007761523/dc7e27c9_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299235966983519/35b047e1_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299250884681522/e5731314_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299267028602229/230d5428_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299293149732467/19abcf6c_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299311267192779/e665c668_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299327888096947/283a177f_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299348897579356/caac864e_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299376680669014/452585fb_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299843158459866/ceebbb63_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299432356918392/07abdf6a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299465584172180/a2b2be12_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299491233431530/d88bfa35_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299513358913413/f2f90032_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299527419238776/549cb852_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299553918371792/43bd3bff_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780299586662735625/1107a3ee_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780299613342135530/526d7859_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300015922774817/47c1f39f_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300034170229441/1608ee0a_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300053860691133/f8d334ce_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300066022559831/7c8e74cd_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300081538003511/906c028f_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300104233264980/8df4a03d_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300116889441614/7206e749_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300133555040293/6f4067f0_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300380395316233/d1cc70c4_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300232306768234/1f474295_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300245151922243/132ae6e0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300259301543383/a0c982b3_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300401947949756/673c7fdc_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300417037474904/2fd89565_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300430575382678/cef384a4_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300446747725208/413ba289_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300471231552129/5d45227c_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300496559910468/0008fa0c_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300562713799535/a15b018d_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300575582711290/d440b1c9_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300617515185769/db2968e4_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300631462725775/30b9cd2c_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300644824617448/12b7e446_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300665964630901/07f14841_1766278.png "屏幕截图") |
| ![输入图片说明](https://foruda.gitee.com/images/1780300695487754948/17569a2e_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1780300715114472355/ebb81e7c_1766278.png "屏幕截图") |

40
config/config.ts Normal file
View File

@ -0,0 +1,40 @@
import { defineConfig } from '@umijs/max';
import { createUmiAppConfig } from '../vite.config';
const appConfig = createUmiAppConfig();
export default defineConfig({
title: appConfig.title,
antd: {},
access: {},
model: {},
initialState: {},
vite: appConfig.vite,
npmClient: 'pnpm',
hash: true,
esbuildMinifyIIFE: true,
history: {
type: 'browser'
},
base: appConfig.base,
publicPath: appConfig.publicPath,
routes: [
{ path: '/login', component: './login', layout: false },
{ path: '/register', component: './register', layout: false },
{ path: '/social-callback', component: './socialCallback', layout: false },
{ path: '/401', component: './error/401', layout: false },
{ path: '/404', component: './error/404', layout: false },
{ path: '/redirect/*', component: './redirect', layout: false },
{
path: '/',
component: '../layouts/BasicLayout',
routes: [
{ path: '/', redirect: '/index' },
{ path: '/index', component: './index' },
{ path: '/user/profile', component: './system/user/profile' },
{ path: '*', component: './dynamicPage' }
]
}
],
proxy: appConfig.proxy
});

View File

@ -1,104 +1,98 @@
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
<#if !table.tree>
import type { PageResult } from '@/api/types';
</#if>
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { <#if !table.tree>PageResult, </#if> R } from '@/api/types';
import request from '@/api/request';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from './types';
/**
* 查询${functionName}列表
* @param query
* @returns {*}
*/
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
return request({
export function list${BusinessName}(query?: ${BusinessName}Query) {
return request<R<<#if table.tree>${BusinessName}VO[]<#else> PageResult<${BusinessName}VO></#if>>>({
url: '/${moduleName}/${businessName}/list',
method: 'get',
params: query
});
};
}
/**
* 查询${functionName}详细
* @param ${pkColumn.javaField}
*/
export const get${BusinessName} = (${pkColumn.javaField}: string | number): AxiosPromise<${BusinessName}VO> => {
return request({
export function get${BusinessName}(${pkColumn.javaField}: string | number) {
return request<R<${BusinessName}VO>>({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'get'
});
};
}
/**
* 新增${functionName}
* @param data
*/
export const add${BusinessName} = (data: ${BusinessName}Form) => {
return request({
export function add${BusinessName}(data: ${BusinessName}Form) {
return request<R>({
url: '/${moduleName}/${businessName}',
method: 'post',
data: data
data
});
};
}
/**
* 修改${functionName}
* @param data
*/
export const update${BusinessName} = (data: ${BusinessName}Form) => {
return request({
export function update${BusinessName}(data: ${BusinessName}Form) {
return request<R>({
url: '/${moduleName}/${businessName}',
method: 'put',
data: data
data
});
};
}
<#if enableStatus>
/**
* 修改${functionName}状态
* @param ${pkColumn.javaField}
* @param status
*/
export const change${BusinessName}Status = (${pkColumn.javaField}: string | number, status: <#if statusColumn.javaType == 'Boolean'>boolean<#elseif statusColumn.javaType == 'String'>string<#else> number</#if>) => {
return request({
export function change${BusinessName}Status(
${pkColumn.javaField}: string | number,
${statusField}: <#if statusColumn.javaType == 'Boolean'>boolean<#elseif statusColumn.javaType == 'Integer' || statusColumn.javaType == 'Long'>number<#else> string</#if>
) {
return request<R>({
url: '/${moduleName}/${businessName}/changeStatus',
method: 'put',
data: {
${pkColumn.javaField},
${statusField}: status
${statusField}
}
});
};
</#if>
}
</#if>
<#if enableSort>
/**
* 调整${functionName}排序
* @param ${pkColumn.javaField}
* @param sortValue
*/
export const update${BusinessName}Sort = (${pkColumn.javaField}: string | number, sortValue: <#if sortColumn.javaType == 'String' || sortColumn.javaType == 'LocalDateTime'>string<#else> number</#if>) => {
return request({
export function update${BusinessName}Sort(
${pkColumn.javaField}: string | number,
${sortField}: <#if sortColumn.javaType == 'LocalDateTime' || sortColumn.javaType == 'String'>string<#else> number</#if>
) {
return request<R>({
url: '/${moduleName}/${businessName}/updateSort',
method: 'put',
data: {
${pkColumn.javaField},
${sortField}: sortValue
${sortField}
}
});
};
</#if>
}
</#if>
/**
* 删除${functionName}
* @param ${pkColumn.javaField}
*/
export const del${BusinessName} = (${pkColumn.javaField}: string | number | Array<string | number>) => {
return request({
export function del${BusinessName}(${pkColumn.javaField}: string | number | Array<string | number>) {
return request<R>({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'delete'
});
};
}

427
gen/index-tree.tsx.ftl Normal file
View File

@ -0,0 +1,427 @@
import { DeleteOutlined<#if enableExport>, DownloadOutlined</#if>, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
<#if needCheckbox>
ProFormCheckbox,
</#if>
<#if needDateField>
ProFormDateTimePicker,
</#if>
<#if needDigit>
ProFormDigit,
</#if>
<#if needSelect>
ProFormSelect,
</#if>
<#if needTextArea>
ProFormTextArea,
</#if>
ProFormText,
ProFormTreeSelect,
ProTable,
type ActionType,
type ProColumns
} from '@ant-design/pro-components';
import { useBoolean } from 'ahooks';
import { Button, Form, message<#if enableStatus || needSwitchField>, Switch</#if><#if enableSort>, InputNumber</#if> } from 'antd';
import { useMemo, useRef, useState } from 'react';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
<#if needDict>
import DictTag from '@/components/common/DictTag';
</#if>
<#if needFileUpload>
import FileUpload from '@/components/common/FileUpload';
</#if>
<#if needImagePreview>
import ImagePreview from '@/components/common/ImagePreview';
</#if>
<#if needImageUpload>
import ImageUpload from '@/components/common/ImageUpload';
</#if>
<#if needEditor>
import RichTextEditor from '@/components/common/RichTextEditor';
</#if>
import RowActions from '@/components/common/RowActions';
<#if needDict>
import { useDict } from '@/hooks/useDict';
</#if>
<#if enableExport>
import { useTableExport } from '@/hooks/useTableExport';
</#if>
import { useTreeTableExpand } from '@/hooks/useTreeTableExpand';
import { useUserStore } from '@/stores/userStore';
<#if needDict>
import { dictOptions } from '@/utils/dict';
</#if>
<#if enableStatus>
import { confirmAction } from '@/utils/modal';
</#if>
import { hasPermi } from '@/utils/permission';
import { filterTree<#if needDateField>, formatDateTimeFields</#if>, handleTree<#if needDateField>, toDayjsFields</#if> } from '@/utils/ruoyi';
const default${BusinessName}Form: ${BusinessName}Form = {
${treeParentCode}: ${treeRootValueTsLiteral},
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk && column.htmlType == "checkbox">
${column.javaField}: [],
</#if>
</#list>
};
interface ${BusinessName}SelectNode {
title: string;
value: string | number;
children?: ${BusinessName}SelectNode[];
}
function toTreeSelectData(nodes: ${BusinessName}VO[]): ${BusinessName}SelectNode[] {
return nodes.map(node => ({
title: String(node.${treeName} || ''),
value: node.${treeCode},
children: node.children ? toTreeSelectData(node.children) : undefined
}));
}
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
export default function ${BusinessName}Page() {
const actionRef = useRef<ActionType | undefined>(undefined);
const [form] = Form.useForm<${BusinessName}Form>();
const userInfo = useUserStore(state => state.userInfo);
<#if needDict>
const dicts = useDict(${dicts});
</#if>
const [treeOptions, setTreeOptions] = useState<${BusinessName}VO[]>([]);
const [tableRows, setTableRows] = useState<${BusinessName}VO[]>([]);
const { expandAll, expandedRowKeys, onExpandedRowsChange, syncExpandedRows, toggleExpandAll } =
useTreeTableExpand<${BusinessName}VO>(row => row.${treeCode});
const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
const [modalTitle, setModalTitle] = useState('');
<#if enableExport>
const { updateExportParams, exportFile } = useTableExport();
</#if>
const canAdd = hasPermi(userInfo, ['${permissionPrefix}:add']);
const canEdit = hasPermi(userInfo, ['${permissionPrefix}:edit']);
const canRemove = hasPermi(userInfo, ['${permissionPrefix}:remove']);
<#if enableExport>
const canExport = hasPermi(userInfo, ['${permissionPrefix}:export']);
</#if>
const treeSelectData = useMemo(
() => [{ title: '顶级节点', value: ${treeRootValueTsLiteral}, children: toTreeSelectData(treeOptions) }],
[treeOptions]
);
const loadTreeOptions = async (excludeId?: string | number) => {
const res = await list${BusinessName}();
const rows = handleTree<${BusinessName}VO>(res.data || [], '${treeCode}', '${treeParentCode}');
setTreeOptions(excludeId ? filterTree(rows, node => node.${treeCode} !== excludeId) : rows);
};
const openAdd = async (row?: ${BusinessName}VO) => {
await loadTreeOptions();
form.resetFields();
form.setFieldsValue({ ...default${BusinessName}Form, ${treeParentCode}: row?.${treeCode} || treeRootValueTsLiteral });
setModalTitle('添加${functionName}');
openModal();
};
const openEdit = async (row: ${BusinessName}VO) => {
await loadTreeOptions(row.${treeCode});
const res = await get${BusinessName}(row.${pkColumn.javaField});
const data = <#if needDateField>toDayjsFields({ ...res.data }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...res.data }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (typeof data.${column.javaField} === 'string') {
data.${column.javaField} = data.${column.javaField}.split(',');
}
</#if>
</#list>
form.resetFields();
form.setFieldsValue(data);
setModalTitle('修改${functionName}');
openModal();
};
const submitForm = async (values: ${BusinessName}Form) => {
const submitValues = <#if needDateField>formatDateTimeFields({ ...values }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...values }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (Array.isArray(submitValues.${column.javaField})) {
submitValues.${column.javaField} = submitValues.${column.javaField}.join(',');
}
</#if>
</#list>
submitValues.${pkColumn.javaField} ? await update${BusinessName}(submitValues) : await add${BusinessName}(submitValues);
message.success('操作成功');
form.resetFields();
actionRef.current?.reload();
return true;
};
const remove = async (row: ${BusinessName}VO) => {
await del${BusinessName}(row.${pkColumn.javaField});
message.success('删除成功');
actionRef.current?.reload();
};
<#if enableStatus>
const handleStatusChange = async (row: ${BusinessName}VO, checked: boolean) => {
const previousStatus = row.${statusField};
const status = checked ? ${statusField}ActiveValue : ${statusField}InactiveValue;
const text = checked ? '启用' : '停用';
try {
await confirmAction(`确认要"${r'${text}'}"吗?`);
await change${BusinessName}Status(row.${pkColumn.javaField}, status);
message.success(`${r'${text}'}成功`);
actionRef.current?.reload();
} catch {
row.${statusField} = previousStatus;
actionRef.current?.reload();
}
};
</#if>
<#if enableSort>
const handleSortChange = async (row: ${BusinessName}VO, value?: number | null) => {
await update${BusinessName}Sort(row.${pkColumn.javaField}, value || 0);
message.success('排序更新成功');
actionRef.current?.reload();
};
</#if>
const columns: ProColumns<${BusinessName}VO>[] = [
<#list columns as column>
<#if column.list && !column.pk>
<#if enableStatus && statusField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
<#if column.dictType?has_content>
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
</#if>
render: (_, row) => (
<Switch
checked={row.${column.javaField} === ${statusField}ActiveValue}
disabled={!canEdit}
onChange={checked => handleStatusChange(row, checked)}
/>
)
},
<#elseif enableSort && sortField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
search: false,
width: 130,
render: (_, row) => <InputNumber min={0} value={row.${column.javaField} as number} onChange={value => handleSortChange(row, value)} />
},
<#elseif column.htmlType == "datetime">
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}', valueType: 'dateTime', search: <#if column.query>true<#else> false</#if>, width: 170 },
<#elseif column.htmlType == "imageUpload">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}Url',
search: false,
width: 100,
render: (_, row) => <ImagePreview src={row.${column.javaField}Url || String(row.${column.javaField} || '')} width={50} height={50} />
},
<#elseif column.dictColumn>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
render: (_, row) => <DictTag options={dicts.${column.dictType}} value={row.${column.javaField}} />
},
<#elseif column.htmlType == "switch">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
render: (_, row) => <Switch checked={row.${column.javaField} === ${column.switchActiveValue}} disabled />
},
<#else>
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}'<#if !column.query>, search: false</#if> },
</#if>
</#if>
</#list>
{
title: '操作',
valueType: 'option',
width: 130,
fixed: 'right',
render: (_, row) => (
<RowActions
actions={[
canEdit && { key: 'edit', label: '修改', icon: <EditOutlined />, onClick: () => openEdit(row) },
canAdd && { key: 'add', label: '新增', icon: <PlusOutlined />, onClick: () => openAdd(row) },
canRemove && {
key: 'delete',
label: '删除',
icon: <DeleteOutlined />,
danger: true,
confirm: `是否确认删除${functionName}编号为"${r'${row.'}${pkColumn.javaField}${r'}'}"的数据项?`,
onClick: () => remove(row)
}
]}
/>
)
}
];
return (
<PageContainer title="${functionName}">
<ProTable<${BusinessName}VO, ${BusinessName}Query>
actionRef={actionRef}
rowKey="${treeCode}"
columns={columns}
scroll={{ x: 1000 }}
pagination={false}
search={{ labelWidth: 90 }}
expandable={{
expandedRowKeys,
onExpandedRowsChange
}}
request={async params => {
const res = await list${BusinessName}(params);
const rows = handleTree<${BusinessName}VO>(res.data || [], '${treeCode}', '${treeParentCode}');
setTableRows(rows);
syncExpandedRows(rows, expandAll);
<#if enableExport>
updateExportParams(params);
</#if>
return { data: rows, total: rows.length, success: true };
}}
toolbar={{ title: '${functionName}列表' }}
toolBarRender={() => [
canAdd && (
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openAdd()}>
新增
</Button>
),
<Button
key="expand"
icon={<SortAscendingOutlined />}
onClick={() => toggleExpandAll(tableRows)}
>
展开/折叠
</Button><#if enableExport>,
canExport && (
<Button
key="export"
icon={<DownloadOutlined />}
onClick={() => exportFile('/${moduleName}/${businessName}/export', () => `${businessName}_${r'${Date.now()}'}.xlsx`)}
>
导出
</Button>
)</#if>
]}
/>
<ModalForm<${BusinessName}Form>
title={modalTitle}
open={modalOpen}
width={560}
form={form}
layout="vertical"
initialValues={default${BusinessName}Form}
modalProps={{ destroyOnHidden: true, onCancel: closeModal }}
onOpenChange={open => !open && closeModal()}
onFinish={submitForm}
>
<ProFormText name="${pkColumn.javaField}" hidden />
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#assign field = column.javaField>
<#if field == treeParentCode>
<ProFormTreeSelect
name="${column.javaField}"
label="${column.columnLabel}"
<#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>
fieldProps={{
allowClear: true,
treeDefaultExpandAll: true,
treeData: treeSelectData,
placeholder: '请选择${column.columnLabel}'
}}
/>
<#elseif column.htmlType == "input">
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "textarea">
<ProFormTextArea name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "inputNumber">
<ProFormDigit name="${column.javaField}" label="${column.columnLabel}" min={0} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif (column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch") && column.dictType?has_content>
<ProFormSelect name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<ProFormCheckbox.Group name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "switch">
<Form.Item
name="${column.javaField}"
label="${column.columnLabel}"
valuePropName="checked"
getValueProps={value => ({ checked: value === ${column.switchActiveValue} })}
normalize={checked => (checked ? ${column.switchActiveValue} : ${column.switchInactiveValue})}
>
<Switch />
</Form.Item>
<#elseif column.htmlType == "datetime">
<ProFormDateTimePicker name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "imageUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<ImageUpload />
</Form.Item>
<#elseif column.htmlType == "fileUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<FileUpload />
</Form.Item>
<#elseif column.htmlType == "editor">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<RichTextEditor />
</Form.Item>
<#else>
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
</#if>
</#if>
</#list>
</ModalForm>
</PageContainer>
);
}

View File

@ -1,691 +0,0 @@
<template>
<div class="p-2 page-shell ${moduleName}-${businessName}-page">
<div class="search-wrap">
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
<template #header>
<div class="panel-heading search-panel-toggle" @click.stop="showSearch = !showSearch">
<div><h3>筛选条件</h3></div>
</div>
</template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<#list columns as column>
<#if column.query>
<#if column.htmlType == "input" || column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="queryParams.${column.javaField}" placeholder="请输入${column.columnLabel}" clearable @keyup.enter="handleQuery" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="queryParams.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<#if column.javaType == "Boolean">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
<#elseif column.javaType == "Integer" || column.javaType == "Long">
<el-option label="开启" :value="0" />
<el-option label="关闭" :value="1" />
<#else>
<el-option label="开启" value="0" />
<el-option label="关闭" value="1" />
</#if>
</el-select>
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable>
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType != "BETWEEN">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN">
<el-form-item label="${column.columnLabel}" style="width: 308px">
<el-date-picker
v-model="dateRange${column.capJavaField}"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
/>
</el-form-item>
</#if>
</#if>
</#list>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card shadow="hover" class="table-panel">
<template #header>
<div class="toolbar-shell">
<div class="table-heading">
<h3>${functionName}列表</h3>
</div>
<div class="toolbar-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
<#if enableExport>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['${moduleName}:${businessName}:export']">导出</el-button>
</#if>
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
</div>
</div>
</template>
<el-table
ref="${businessName}TableRef"
v-loading="loading"
class="data-table"
:data="${businessName}List"
row-key="${treeCode}"
border
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<#assign firstTreeListField = "">
<#list columns as tempColumn>
<#if !tempColumn.pk && tempColumn.list && "" != tempColumn.javaField && firstTreeListField == "">
<#assign firstTreeListField = tempColumn.javaField>
</#if>
</#list>
<#list columns as column>
<#if column.pk>
<#elseif enableStatus && statusField == column.javaField>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
</#if>
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
:active-value="${statusField}ActiveValue"
:inactive-value="${statusField}InactiveValue"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<#elseif enableSort && sortField == column.javaField>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" width="160">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="160">
</#if>
<template #default="scope">
<#if column.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${column.columnLabel}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${column.javaField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "switch">
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" width="120">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="120">
</#if>
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
disabled
/>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "datetime">
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" width="180">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="180">
</#if>
<template #default="scope">
<span>{{ parseTime(scope.row.${column.javaField}, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "imageUpload">
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}Url" width="100">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}Url" width="100">
</#if>
<template #default="scope">
<image-preview :src="scope.row.${column.javaField}Url" :width="50" :height="50"/>
</template>
</el-table-column>
<#elseif column.list && column.dictColumn>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}">
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
</#if>
<template #default="scope">
<#if column.htmlType == "checkbox">
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField} ? scope.row.${column.javaField}.split(',') : []"/>
<#else>
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField}"/>
</#if>
</template>
</el-table-column>
<#elseif column.list && "" != column.javaField>
<#if column.javaField == firstTreeListField>
<el-table-column label="${column.columnLabel}" prop="${column.javaField}" />
<#else>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" />
</#if>
</#if>
</#list>
<#if enableStatus && !statusColumn.list>
<el-table-column label="${statusColumn.columnComment}" align="center" prop="${statusField}">
<template #default="scope">
<el-switch
v-model="scope.row.${statusField}"
:active-value="${statusField}ActiveValue"
:inactive-value="${statusField}InactiveValue"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
</#if>
<#if enableSort && !sortColumn.list>
<el-table-column label="${sortColumn.columnComment}" align="center" prop="${sortField}" width="160">
<template #default="scope">
<#if sortColumn.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${sortField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${sortColumn.columnComment}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${sortField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
</#if>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" />
</el-tooltip>
<el-tooltip content="新增" placement="top">
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${moduleName}:${businessName}:add']" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#if "" != treeParentCode && column.javaField == treeParentCode>
<el-form-item label="${column.columnLabel}" prop="${treeParentCode}">
<el-tree-select
v-model="form.${treeParentCode}"
:data="${businessName}Options"
:props="{ value: '${treeCode}', label: '${treeName}', children: 'children' } as any"
value-key="${treeCode}"
placeholder="请选择${column.columnLabel}"
check-strictly
/>
</el-form-item>
<#elseif column.htmlType == "input">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="form.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif column.htmlType == "imageUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<image-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "fileUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<file-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "editor">
<el-form-item label="${column.columnLabel}">
<editor v-model="form.${column.javaField}" :min-height="192"/>
</el-form-item>
<#elseif column.htmlType == "select" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.label"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
></el-option>
</el-select>
</el-form-item>
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "radio" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio
v-for="dict in ${column.dictType}"
:key="dict.value"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio value="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-switch
v-model="form.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
/>
</el-form-item>
<#elseif column.htmlType == "datetime">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="form.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" type="textarea" placeholder="请输入内容" />
</el-form-item>
</#if>
</#if>
</#list>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="${BusinessName}" lang="ts">
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
import { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import { useLoading } from '@/hooks/async/useLoading';
import { useFormDialog } from '@/hooks/dialog/useFormDialog';
<#if needAddDateRange>
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery';
</#if>
import { useSearchReset } from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle';
import { useTreeTableExpand } from '@/hooks/tree/useTreeTableExpand';
<#if needDict>
import { useDict } from '@/utils/dict';
</#if>
import modal from '@/plugins/modal';
import { handleTree } from '@/utils/ruoyi';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
</#if>
<#if needDict>
const { ${dictsNoSymbol} } = toRefs<any>(useDict(${dicts}));
</#if>
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
type ${BusinessName}Option = {
${treeCode}: <#if treeParentColumn.javaType == 'String'>string<#else> number</#if>;
${treeName}: string;
children?: ${BusinessName}Option[];
};
const ${businessName}List = ref<${BusinessName}VO[]>([]);
const ${businessName}Options = ref<${BusinessName}Option[]>([]);
const all${BusinessName}Options = ref<${BusinessName}Option[]>([]);
const buttonLoading = ref(false);
const { showSearch } = useSearchToggle();
const { loading, setLoading, withLoading } = useLoading();
const queryFormRef = ref<ElFormInstance>();
const ${businessName}FormRef = ref<ElFormInstance>();
const ${businessName}TableRef = ref<ElTableInstance>();
const { isExpandAll, handleToggleExpandAll } = useTreeTableExpand<${BusinessName}VO>({
tableRef: ${businessName}TableRef,
data: ${businessName}List
});
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
const {
dateRange: dateRange${column.capJavaField},
applyDateRange: apply${column.capJavaField}DateRange,
resetDateRange: reset${column.capJavaField}DateRange
} = useDateRangeQuery('${column.capJavaField}');
</#if>
</#list>
const initFormData: ${BusinessName}Form = {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.htmlType == "checkbox">
${column.javaField}: [],
<#else>
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
const data = reactive<PageData<${BusinessName}Form, ${BusinessName}Query>>({
form: {...initFormData},
queryParams: {
<#list columns as column>
<#if column.query>
<#if column.htmlType != "datetime" || column.queryType != "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
params: {
<#list columns as column>
<#if column.query>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
},
rules: {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.required>
${column.javaField}: [
{ required: true, message: "${column.columnLabel}不能为空", trigger: <#if column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch" || column.htmlType == "inputNumber">"change"<#else>"blur"</#if> }
],
</#if>
</#if>
</#list>
}
});
const { queryParams, form, rules } = toRefs(data);
const { dialog, resetForm: reset, openDialog, showDialog, closeDialog } = useFormDialog({
form,
formRef: ${businessName}FormRef,
initialFormData: initFormData
});
/** 查询${functionName}列表 */
const getList = async () => {
await withLoading(async () => {
<#if needAddDateRange>
let params = queryParams.value;
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
params = apply${column.capJavaField}DateRange(params);
</#if>
</#list>
const res = await list${BusinessName}(params);
<#else>
const res = await list${BusinessName}(queryParams.value);
</#if>
const data = handleTree<${BusinessName}VO>(res.data, '${treeCode}', '${treeParentCode}');
if (data) {
${businessName}List.value = data;
}
});
};
/** 查询${functionName}下拉树结构 */
const getTreeselect = async (excludeId?: string | number) => {
const res = await list${BusinessName}();
const data: ${BusinessName}Option = { ${treeCode}: ${treeRootValueTsLiteral}, ${treeName}: '顶级节点', children: [] };
data.children = handleTree<${BusinessName}Option>(res.data, '${treeCode}', '${treeParentCode}');
all${BusinessName}Options.value = [data];
${businessName}Options.value = excludeId != null ? filterTreeOptions(all${BusinessName}Options.value, excludeId) : all${BusinessName}Options.value;
};
/** 取消按钮 */
const cancel = () => {
reset();
closeDialog();
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
const { resetQuery } = useSearchReset({
queryFormRef,
queryParams,
resetExtras: () => {
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
reset${column.capJavaField}DateRange();
</#if>
</#list>
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = (row?: Partial<${BusinessName}VO>) => {
openDialog('添加${functionName}');
getTreeselect();
if (row != null && row.${treeCode}) {
form.value.${treeParentCode} = row.${treeCode};
} else {
form.value.${treeParentCode} = ${treeRootValueTsLiteral};
}
};
/** 修改按钮操作 */
const handleUpdate = async (row: Partial<${BusinessName}VO>) => {
reset();
await getTreeselect(row.${treeCode});
if (row != null) {
form.value.${treeParentCode} = row.${treeParentCode};
}
const res = await get${BusinessName}(row.${pkColumn.javaField});
Object.assign(form.value, res.data);
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.split(",");
</#if>
</#list>
showDialog('修改${functionName}');
};
/** 提交按钮 */
const submitForm = () => {
${businessName}FormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.join(",");
</#if>
</#list>
if (form.value.${pkColumn.javaField}) {
await update${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
} else {
await add${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row: Partial<${BusinessName}VO>) => {
await modal.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?');
setLoading(true);
await del${BusinessName}(row.${pkColumn.javaField}).finally(() => setLoading(false));
await getList();
modal.msgSuccess('删除成功');
};
const filterTreeOptions = (options: ${BusinessName}Option[], excludeId: string | number): ${BusinessName}Option[] => {
return options
.filter(item => item.${treeCode} !== excludeId)
.map(item => ({
...item,
children: item.children ? filterTreeOptions(item.children, excludeId) : []
}));
};
<#if enableStatus>
/** 状态修改 */
const handleStatusChange = async (row: Partial<${BusinessName}VO>) => {
const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用';
try {
await modal.confirm('确认要"' + text + '"吗?');
await change${BusinessName}Status(row.${pkColumn.javaField}, row.${statusField});
modal.msgSuccess(text + '成功');
} catch (err) {
row.${statusField} = row.${statusField} === ${statusField}ActiveValue ? ${statusField}InactiveValue : ${statusField}ActiveValue;
}
};
</#if>
<#if enableSort>
/** 排序调整 */
const handleSortChange = async (row: Partial<${BusinessName}VO>) => {
try {
await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField});
modal.msgSuccess('排序更新成功');
} catch (err) {
await getList();
}
};
</#if>
<#if enableExport>
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'${moduleName}/${businessName}/export',
{
...queryParams.value
},
`${businessName}_${r'${new Date().getTime()}'}.xlsx`
);
};
</#if>
onMounted(() => {
getList();
});
</script>

409
gen/index.tsx.ftl Normal file
View File

@ -0,0 +1,409 @@
import { DeleteOutlined<#if enableExport>, DownloadOutlined</#if>, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
<#if needCheckbox>
ProFormCheckbox,
</#if>
<#if needDateField>
ProFormDateTimePicker,
</#if>
<#if needDigit>
ProFormDigit,
</#if>
<#if needSelect>
ProFormSelect,
</#if>
<#if needTextArea>
ProFormTextArea,
</#if>
ProFormText,
ProTable,
type ActionType,
type ProColumns
} from '@ant-design/pro-components';
import { useBoolean } from 'ahooks';
import { Button, Form, message, Popconfirm<#if enableStatus || needSwitchField>, Switch</#if><#if enableSort>, InputNumber</#if> } from 'antd';
import { useRef, useState } from 'react';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
<#if needDict>
import DictTag from '@/components/common/DictTag';
</#if>
<#if needFileUpload>
import FileUpload from '@/components/common/FileUpload';
</#if>
<#if needImagePreview>
import ImagePreview from '@/components/common/ImagePreview';
</#if>
<#if needImageUpload>
import ImageUpload from '@/components/common/ImageUpload';
</#if>
<#if needEditor>
import RichTextEditor from '@/components/common/RichTextEditor';
</#if>
import RowActions from '@/components/common/RowActions';
<#if needDict>
import { useDict } from '@/hooks/useDict';
</#if>
<#if needDateRange>
import { useDateRangeQuery } from '@/hooks/useDateRangeQuery';
</#if>
<#if enableExport>
import { useTableExport } from '@/hooks/useTableExport';
</#if>
import { useTableSelection } from '@/hooks/useTableSelection';
import { useUserStore } from '@/stores/userStore';
<#if needDict>
import { dictOptions } from '@/utils/dict';
</#if>
<#if enableStatus>
import { confirmAction } from '@/utils/modal';
</#if>
import { hasPermi } from '@/utils/permission';
import { <#if needDateField>formatDateTimeFields, toDayjsFields, </#if> toPageQuery, toTableData } from '@/utils/ruoyi';
const default${BusinessName}Form: ${BusinessName}Form = {
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk && column.htmlType == "checkbox">
${column.javaField}: [],
</#if>
</#list>
};
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
export default function ${BusinessName}Page() {
const actionRef = useRef<ActionType | undefined>(undefined);
const [form] = Form.useForm<${BusinessName}Form>();
const userInfo = useUserStore(state => state.userInfo);
<#if needDict>
const dicts = useDict(${dicts});
</#if>
const { ids, selectedOne, handleSelectionChange, clearSelection } = useTableSelection<${BusinessName}VO>(
row => row.${pkColumn.javaField}
);
const [modalOpen, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
const [modalTitle, setModalTitle] = useState('');
<#if enableExport>
const { updateExportParams, exportFile } = useTableExport();
</#if>
<#list columns as column>
<#if column.query && column.htmlType == "datetime" && column.queryType == "BETWEEN">
const { applyDateRange: apply${column.capJavaField}DateRange } = useDateRangeQuery('${column.capJavaField}');
</#if>
</#list>
const canAdd = hasPermi(userInfo, ['${permissionPrefix}:add']);
const canEdit = hasPermi(userInfo, ['${permissionPrefix}:edit']);
const canRemove = hasPermi(userInfo, ['${permissionPrefix}:remove']);
<#if enableExport>
const canExport = hasPermi(userInfo, ['${permissionPrefix}:export']);
</#if>
const openAdd = () => {
form.resetFields();
form.setFieldsValue(default${BusinessName}Form);
setModalTitle('添加${functionName}');
openModal();
};
const openEdit = async (row?: ${BusinessName}VO) => {
const target = row || selectedOne;
if (!target?.${pkColumn.javaField}) return;
const res = await get${BusinessName}(target.${pkColumn.javaField});
const data = <#if needDateField>toDayjsFields({ ...res.data }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...res.data }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (typeof data.${column.javaField} === 'string') {
data.${column.javaField} = data.${column.javaField}.split(',');
}
</#if>
</#list>
form.resetFields();
form.setFieldsValue(data);
setModalTitle('修改${functionName}');
openModal();
};
const submitForm = async (values: ${BusinessName}Form) => {
const submitValues = <#if needDateField>formatDateTimeFields({ ...values }, [
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "datetime">
'${column.javaField}',
</#if>
</#list>
])<#else>{ ...values }</#if>;
<#list columns as column>
<#if (column.insert || column.edit) && column.htmlType == "checkbox">
if (Array.isArray(submitValues.${column.javaField})) {
submitValues.${column.javaField} = submitValues.${column.javaField}.join(',');
}
</#if>
</#list>
submitValues.${pkColumn.javaField} ? await update${BusinessName}(submitValues) : await add${BusinessName}(submitValues);
message.success('操作成功');
form.resetFields();
actionRef.current?.reload();
return true;
};
const remove = async (row?: ${BusinessName}VO) => {
await del${BusinessName}(row?.${pkColumn.javaField} || ids);
message.success('删除成功');
clearSelection();
actionRef.current?.reloadAndRest?.();
};
<#if enableStatus>
const handleStatusChange = async (row: ${BusinessName}VO, checked: boolean) => {
const previousStatus = row.${statusField};
const status = checked ? ${statusField}ActiveValue : ${statusField}InactiveValue;
const text = checked ? '启用' : '停用';
try {
await confirmAction(`确认要"${r'${text}'}"吗?`);
await change${BusinessName}Status(row.${pkColumn.javaField}, status);
message.success(`${r'${text}'}成功`);
actionRef.current?.reload();
} catch {
row.${statusField} = previousStatus;
actionRef.current?.reload();
}
};
</#if>
<#if enableSort>
const handleSortChange = async (row: ${BusinessName}VO, value?: number | null) => {
await update${BusinessName}Sort(row.${pkColumn.javaField}, value || 0);
message.success('排序更新成功');
actionRef.current?.reload();
};
</#if>
const columns: ProColumns<${BusinessName}VO>[] = [
<#list columns as column>
<#if column.list>
<#if enableStatus && statusField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
<#if column.dictType?has_content>
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
</#if>
render: (_, row) => (
<Switch
checked={row.${column.javaField} === ${statusField}ActiveValue}
disabled={!canEdit}
onChange={checked => handleStatusChange(row, checked)}
/>
)
},
<#elseif enableSort && sortField == column.javaField>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
search: false,
width: 130,
render: (_, row) => <InputNumber min={0} value={row.${column.javaField} as number} onChange={value => handleSortChange(row, value)} />
},
<#elseif column.htmlType == "datetime">
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}', valueType: 'dateTime', search: <#if column.query && column.queryType != "BETWEEN">true<#else> false</#if>, width: 170 },
<#elseif column.htmlType == "imageUpload">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}Url',
search: false,
width: 100,
render: (_, row) => <ImagePreview src={row.${column.javaField}Url || String(row.${column.javaField} || '')} width={50} height={50} />
},
<#elseif column.dictColumn>
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
fieldProps: { options: dictOptions(dicts.${column.dictType}) },
render: (_, row) => <DictTag options={dicts.${column.dictType}} value={row.${column.javaField}} />
},
<#elseif column.htmlType == "switch">
{
title: '${column.columnLabel}',
dataIndex: '${column.javaField}',
valueType: 'select',
width: 100,
render: (_, row) => <Switch checked={row.${column.javaField} === ${column.switchActiveValue}} disabled />
},
<#else>
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}'<#if !column.query>, search: false</#if> },
</#if>
</#if>
<#if column.query && column.htmlType == "datetime" && column.queryType == "BETWEEN">
{ title: '${column.columnLabel}', dataIndex: '${column.javaField}Range', valueType: 'dateTimeRange', hideInTable: true },
</#if>
</#list>
{
title: '操作',
valueType: 'option',
width: 92,
fixed: 'right',
render: (_, row) => (
<RowActions
actions={[
canEdit && { key: 'edit', label: '修改', icon: <EditOutlined />, onClick: () => openEdit(row) },
canRemove && {
key: 'delete',
label: '删除',
icon: <DeleteOutlined />,
danger: true,
confirm: `是否确认删除${functionName}编号为"${r'${row.'}${pkColumn.javaField}${r'}'}"的数据项?`,
onClick: () => remove(row)
}
]}
/>
)
}
];
return (
<PageContainer title="${functionName}">
<ProTable<${BusinessName}VO, ${BusinessName}Query<#if needDateRange> & Record<string, unknown></#if>>
actionRef={actionRef}
rowKey="${pkColumn.javaField}"
columns={columns}
scroll={{ x: 1000 }}
search={{ labelWidth: 90 }}
pagination={{ defaultPageSize: 10, showSizeChanger: true }}
rowSelection={{ selectedRowKeys: ids, onChange: handleSelectionChange }}
request={async params => {
<#if needDateRange>
let query = toPageQuery(params);
<#list columns as column>
<#if column.query && column.htmlType == "datetime" && column.queryType == "BETWEEN">
query = apply${column.capJavaField}DateRange(query, params.${column.javaField}Range as [string, string] | undefined);
delete query.${column.javaField}Range;
</#if>
</#list>
<#else>
const query = toPageQuery(params);
</#if>
<#if enableExport>
updateExportParams(query);
</#if>
const res = await list${BusinessName}(query);
return toTableData(res);
}}
toolbar={{ title: '${functionName}列表' }}
toolBarRender={() => [
canAdd && (
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={openAdd}>
新增
</Button>
),
canEdit && (
<Button key="edit" disabled={!selectedOne} icon={<EditOutlined />} onClick={() => openEdit()}>
修改
</Button>
),
canRemove && (
<Popconfirm key="delete" title={`是否确认删除${functionName}编号为"${r'${ids}'}"的数据项?`} onConfirm={() => remove()}>
<Button danger disabled={!ids.length} icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
)<#if enableExport>,
canExport && (
<Button
key="export"
icon={<DownloadOutlined />}
onClick={() => exportFile('/${moduleName}/${businessName}/export', () => `${businessName}_${r'${Date.now()}'}.xlsx`)}
>
导出
</Button>
)</#if>
]}
/>
<ModalForm<${BusinessName}Form>
title={modalTitle}
open={modalOpen}
width={560}
form={form}
layout="vertical"
initialValues={default${BusinessName}Form}
modalProps={{ destroyOnHidden: true, onCancel: closeModal }}
onOpenChange={open => !open && closeModal()}
onFinish={submitForm}
>
<ProFormText name="${pkColumn.javaField}" hidden />
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#assign field = column.javaField>
<#if column.htmlType == "input">
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "textarea">
<ProFormTextArea name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "inputNumber">
<ProFormDigit name="${column.javaField}" label="${column.columnLabel}" min={0} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif (column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch") && column.dictType?has_content>
<ProFormSelect name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<ProFormCheckbox.Group name="${column.javaField}" label="${column.columnLabel}" options={dictOptions(dicts.${column.dictType})} <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "switch">
<Form.Item
name="${column.javaField}"
label="${column.columnLabel}"
valuePropName="checked"
getValueProps={value => ({ checked: value === ${column.switchActiveValue} })}
normalize={checked => (checked ? ${column.switchActiveValue} : ${column.switchInactiveValue})}
>
<Switch />
</Form.Item>
<#elseif column.htmlType == "datetime">
<ProFormDateTimePicker name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
<#elseif column.htmlType == "imageUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<ImageUpload />
</Form.Item>
<#elseif column.htmlType == "fileUpload">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<FileUpload />
</Form.Item>
<#elseif column.htmlType == "editor">
<Form.Item name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if>>
<RichTextEditor />
</Form.Item>
<#else>
<ProFormText name="${column.javaField}" label="${column.columnLabel}" <#if column.required>rules={[{ required: true, message: '${column.columnLabel}不能为空' }]}</#if> />
</#if>
</#if>
</#list>
</ModalForm>
</PageContainer>
);
}

View File

@ -1,615 +0,0 @@
<template>
<div class="p-2 page-shell ${moduleName}-${businessName}-page">
<div class="search-wrap">
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
<template #header>
<div class="panel-heading search-panel-toggle" @click.stop="showSearch = !showSearch">
<div><h3>筛选条件</h3></div>
</div>
</template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<#list columns as column>
<#if column.query>
<#if column.htmlType == "input" || column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="queryParams.${column.javaField}" placeholder="请输入${column.columnLabel}" clearable @keyup.enter="handleQuery" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="queryParams.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option v-for="dict in ${column.dictType}" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<#if column.javaType == "Boolean">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
<#elseif column.javaType == "Integer" || column.javaType == "Long">
<el-option label="开启" :value="0" />
<el-option label="关闭" :value="1" />
<#else>
<el-option label="开启" value="0" />
<el-option label="关闭" value="1" />
</#if>
</el-select>
</el-form-item>
<#elseif (column.htmlType == "select" || column.htmlType == "radio") && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${column.columnLabel}" clearable >
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType != "BETWEEN">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择${column.columnLabel}"
/>
</el-form-item>
<#elseif column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
<el-form-item label="${column.columnLabel}" style="width: 308px">
<el-date-picker
v-model="dateRange${column.capJavaField}"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
/>
</el-form-item>
</#if>
</#if>
</#list>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card shadow="hover" class="table-panel">
<template #header>
<div class="toolbar-shell">
<div class="table-heading">
<h3>${functionName}列表</h3>
</div>
<div class="toolbar-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
<#if enableExport>
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['${moduleName}:${businessName}:export']">导出</el-button>
</#if>
<right-toolbar v-model:show-search="showSearch" :search="false" @query-table="getList"></right-toolbar>
</div>
</div>
</template>
<el-table v-loading="loading" border class="data-table" :data="${businessName}List" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<#list columns as column>
<#if column.pk && column.list>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" />
<#elseif enableStatus && statusField == column.javaField>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
:active-value="${statusField}ActiveValue"
:inactive-value="${statusField}InactiveValue"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<#elseif enableSort && sortField == column.javaField>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="160">
<template #default="scope">
<#if column.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${column.columnLabel}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${column.javaField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "switch">
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
disabled
/>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "datetime">
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.${column.javaField}, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<#elseif column.list && column.htmlType == "imageUpload">
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}Url" width="100">
<template #default="scope">
<image-preview :src="scope.row.${column.javaField}Url" :width="50" :height="50"/>
</template>
</el-table-column>
<#elseif column.list && column.dictColumn>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}">
<template #default="scope">
<#if column.htmlType == "checkbox">
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField} ? scope.row.${column.javaField}.split(',') : []"/>
<#else>
<dict-tag :options="${column.dictType}" :value="scope.row.${column.javaField}"/>
</#if>
</template>
</el-table-column>
<#elseif column.list && "" != column.javaField>
<el-table-column label="${column.columnLabel}" align="center" prop="${column.javaField}" />
</#if>
</#list>
<#if enableStatus && !statusColumn.list>
<el-table-column label="${statusColumn.columnComment}" align="center" prop="${statusField}">
<template #default="scope">
<el-switch
v-model="scope.row.${statusField}"
<#if statusColumn.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
</#if>
<#if enableSort && !sortColumn.list>
<el-table-column label="${sortColumn.columnComment}" align="center" prop="${sortField}" width="160">
<template #default="scope">
<#if sortColumn.javaType == "LocalDateTime">
<el-date-picker
v-model="scope.row.${sortField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择${sortColumn.columnComment}"
@change="handleSortChange(scope.row)"
/>
<#else>
<el-input-number v-model="scope.row.${sortField}" controls-position="right" :min="0" @change="handleSortChange(scope.row)" />
</#if>
</template>
</el-table-column>
</#if>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="${businessName}FormRef" :model="form" :rules="rules" label-width="80px">
<#list columns as column>
<#if (column.insert || column.edit) && !column.pk>
<#if column.htmlType == "input">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnLabel}" />
</el-form-item>
<#elseif column.htmlType == "inputNumber">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input-number v-model="form.${column.javaField}" controls-position="right" />
</el-form-item>
<#elseif column.htmlType == "imageUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<image-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "fileUpload">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<file-upload v-model="form.${column.javaField}"/>
</el-form-item>
<#elseif column.htmlType == "editor">
<el-form-item label="${column.columnLabel}">
<editor v-model="form.${column.javaField}" :min-height="192"/>
</el-form-item>
<#elseif column.htmlType == "select" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.label"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
></el-option>
</el-select>
</el-form-item>
<#elseif column.htmlType == "select" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-select v-model="form.${column.javaField}" placeholder="请选择${column.columnLabel}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
<#elseif column.htmlType == "checkbox" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox
v-for="dict in ${column.dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "checkbox" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-checkbox-group v-model="form.${column.javaField}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
<#elseif column.htmlType == "radio" && column.dictType?has_content>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio
v-for="dict in ${column.dictType}"
:key="dict.value"
<#if column.javaType == "Integer" || column.javaType == "Long">
:value="parseInt(dict.value)"
<#else>
:value="dict.value"
</#if>
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "radio" && !(column.dictType?has_content)>
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-radio-group v-model="form.${column.javaField}">
<el-radio value="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
<#elseif column.htmlType == "switch">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-switch
v-model="form.${column.javaField}"
<#if column.javaType == "Boolean">
:active-value="true"
:inactive-value="false"
<#elseif column.javaType == "Integer" || column.javaType == "Long">
:active-value="0"
:inactive-value="1"
<#else>
active-value="0"
inactive-value="1"
</#if>
/>
</el-form-item>
<#elseif column.htmlType == "datetime">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-date-picker clearable
v-model="form.${column.javaField}"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择${column.columnLabel}">
</el-date-picker>
</el-form-item>
<#elseif column.htmlType == "textarea">
<el-form-item label="${column.columnLabel}" prop="${column.javaField}">
<el-input v-model="form.${column.javaField}" type="textarea" placeholder="请输入内容" />
</el-form-item>
</#if>
</#if>
</#list>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="${BusinessName}" lang="ts">
import {
add${BusinessName},
<#if enableStatus>
change${BusinessName}Status,
</#if>
del${BusinessName},
get${BusinessName},
list${BusinessName},
<#if enableSort>
update${BusinessName}Sort,
</#if>
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
import { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from '@/api/${moduleName}/${businessName}/types';
import { useLoading } from '@/hooks/async/useLoading';
import { useFormDialog } from '@/hooks/dialog/useFormDialog';
<#if needAddDateRange>
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery';
</#if>
import { useSearchReset } from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle';
import { useTableSelection } from '@/hooks/table/useTableSelection';
<#if needDict>
import { useDict } from '@/utils/dict';
</#if>
<#if needParseTime>
import { parseTime } from '@/utils/ruoyi';
</#if>
import modal from '@/plugins/modal';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
</#if>
<#if needDict>
const { ${dictsNoSymbol} } = toRefs<any>(useDict(${dicts}));
</#if>
<#if enableStatus>
const ${statusField}ActiveValue = <#if statusColumn.javaType == "Boolean">true<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">0<#else>'0'</#if>;
const ${statusField}InactiveValue = <#if statusColumn.javaType == "Boolean">false<#elseif statusColumn.javaType == "Integer" || statusColumn.javaType == "Long">1<#else>'1'</#if>;
</#if>
const ${businessName}List = ref<${BusinessName}VO[]>([]);
const buttonLoading = ref(false);
const { loading, withLoading } = useLoading(true);
const { showSearch } = useSearchToggle();
const total = ref(0);
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
const {
dateRange: dateRange${column.capJavaField},
applyDateRange: apply${column.capJavaField}DateRange,
resetDateRange: reset${column.capJavaField}DateRange
} = useDateRangeQuery('${column.capJavaField}');
</#if>
</#list>
const queryFormRef = ref<ElFormInstance>();
const ${businessName}FormRef = ref<ElFormInstance>();
const initFormData: ${BusinessName}Form = {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.htmlType == "checkbox">
${column.javaField}: [],
<#else>
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
const data = reactive<PageData<${BusinessName}Form, ${BusinessName}Query>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
<#list columns as column>
<#if column.query>
<#if column.htmlType != "datetime" || column.queryType != "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
params: {
<#list columns as column>
<#if column.query>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN">
${column.javaField}: undefined,
</#if>
</#if>
</#list>
}
},
rules: {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.required>
${column.javaField}: [
{ required: true, message: "${column.columnLabel}不能为空", trigger: <#if column.htmlType == "select" || column.htmlType == "radio" || column.htmlType == "switch" || column.htmlType == "inputNumber">"change"<#else>"blur"</#if> }
],
</#if>
</#if>
</#list>
}
});
const { queryParams, form, rules } = toRefs(data);
const { ids, single, multiple, handleSelectionChange } = useTableSelection<${BusinessName}VO>(item => item.${pkColumn.javaField});
const { dialog, resetForm: reset, openDialog, showDialog, closeDialog } = useFormDialog({
form,
formRef: ${businessName}FormRef,
initialFormData: initFormData
});
/** 查询${functionName}列表 */
const getList = async () => {
await withLoading(async () => {
<#if needAddDateRange>
let params = queryParams.value;
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
params = apply${column.capJavaField}DateRange(params);
</#if>
</#list>
const res = await list${BusinessName}(params);
<#else>
const res = await list${BusinessName}(queryParams.value);
</#if>
${businessName}List.value = res.data?.rows;
total.value = res.data?.total;
});
};
/** 取消按钮 */
const cancel = () => {
reset();
closeDialog();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
const { resetQuery } = useSearchReset({
queryFormRef,
queryParams,
pageNumKey: 'pageNum',
pageSizeKey: 'pageSize',
initialPageSize: 10,
resetExtras: () => {
<#list columns as column>
<#if column.htmlType == "datetime" && column.queryType == "BETWEEN" && column.query>
reset${column.capJavaField}DateRange();
</#if>
</#list>
},
afterReset: () => {
handleQuery();
}
});
/** 新增按钮操作 */
const handleAdd = () => {
openDialog('添加${functionName}');
};
/** 修改按钮操作 */
const handleUpdate = async (row?: Partial<${BusinessName}VO>) => {
reset();
const _${pkColumn.javaField} = row?.${pkColumn.javaField} || ids.value[0];
const res = await get${BusinessName}(_${pkColumn.javaField});
Object.assign(form.value, res.data);
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.split(",");
</#if>
</#list>
showDialog('修改${functionName}');
};
/** 提交按钮 */
const submitForm = () => {
${businessName}FormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
<#list columns as column>
<#if column.htmlType == "checkbox">
form.value.${column.javaField} = form.value.${column.javaField}.join(",");
</#if>
</#list>
if (form.value.${pkColumn.javaField}) {
await update${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
} else {
await add${BusinessName}(form.value).finally(() => (buttonLoading.value = false));
}
modal.msgSuccess('操作成功');
closeDialog();
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: Partial<${BusinessName}VO>) => {
const _${pkColumn.javaField}s = row?.${pkColumn.javaField} || ids.value;
await modal.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?');
await del${BusinessName}(_${pkColumn.javaField}s);
modal.msgSuccess('删除成功');
await getList();
};
<#if enableExport>
/** 导出按钮操作 */
const handleExport = () => {
requestDownload(
'${moduleName}/${businessName}/export',
{
...queryParams.value
},
`${businessName}_${r'${new Date().getTime()}'}.xlsx`
);
};
</#if>
<#if enableStatus>
/** 状态修改 */
const handleStatusChange = async (row: Partial<${BusinessName}VO>) => {
const text = row.${statusField} === ${statusField}ActiveValue ? '启用' : '停用';
try {
await modal.confirm('确认要"' + text + '"吗?');
await change${BusinessName}Status(row.${pkColumn.javaField}, row.${statusField});
modal.msgSuccess(text + '成功');
} catch (err) {
row.${statusField} = row.${statusField} === ${statusField}ActiveValue ? ${statusField}InactiveValue : ${statusField}ActiveValue;
}
};
</#if>
<#if enableSort>
/** 排序调整 */
const handleSortChange = async (row: Partial<${BusinessName}VO>) => {
try {
await update${BusinessName}Sort(row.${pkColumn.javaField}, row.${sortField});
modal.msgSuccess('排序更新成功');
} catch (err) {
await getList();
}
};
</#if>
onMounted(() => {
getList();
});
</script>

View File

@ -1,6 +1,8 @@
import type { BaseEntity<#if !table.tree>, PageQuery</#if> } from '@/api/types';
export interface ${BusinessName}VO {
<#list columns as column>
<#if column.list>
<#if column.list || column.pk>
/**
* ${column.columnComment}
*/
@ -9,7 +11,7 @@ export interface ${BusinessName}VO {
/**
* ${column.columnComment}Url
*/
${column.javaField}Url: string;
${column.javaField}Url?: string;
</#if>
</#if>
</#list>
@ -17,18 +19,22 @@ export interface ${BusinessName}VO {
/**
* 子对象
*/
children: ${BusinessName}VO[];
children?: ${BusinessName}VO[];
</#if>
}
export interface ${BusinessName}Form extends BaseEntity {
<#list columns as column>
<#if column.insert || column.edit>
<#if column.insert || column.edit || column.pk>
/**
* ${column.columnComment}
*/
<#if column.htmlType == "checkbox">
${column.javaField}?: string | string[];
<#else>
${column.javaField}?: ${column.tsType};
</#if>
</#if>
</#list>
}
@ -44,5 +50,8 @@ export interface ${BusinessName}Query<#if !table.tree> extends PageQuery</#if> {
/**
* 日期范围参数
*/
params?: any;
params?: Record<string, unknown>;
}

View File

@ -1,214 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="icon" href="/favicon.ico" />
<title>%VITE_APP_TITLE%</title>
<!--[if lt IE 11]>
<script>
window.location.href = '/ie.html';
</script>
<![endif]-->
<style>
html,
body,
#app {
height: 100%;
margin: 0px;
padding: 0px;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
#loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999;
}
#loader {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #fff;
-webkit-animation: spin 2s linear infinite;
-ms-animation: spin 2s linear infinite;
-moz-animation: spin 2s linear infinite;
-o-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
z-index: 1001;
}
#loader:before {
content: '';
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #fff;
-webkit-animation: spin 3s linear infinite;
-moz-animation: spin 3s linear infinite;
-o-animation: spin 3s linear infinite;
-ms-animation: spin 3s linear infinite;
animation: spin 3s linear infinite;
}
#loader:after {
content: '';
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #fff;
-moz-animation: spin 1.5s linear infinite;
-o-animation: spin 1.5s linear infinite;
-ms-animation: spin 1.5s linear infinite;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
#loader-wrapper .loader-section {
position: fixed;
top: 0;
width: 51%;
height: 100%;
background: #7171c6;
z-index: 1000;
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
#loader-wrapper .loader-section.section-left {
left: 0;
}
#loader-wrapper .loader-section.section-right {
right: 0;
}
.loaded #loader-wrapper .loader-section.section-left {
-webkit-transform: translateX(-100%);
-ms-transform: translateX(-100%);
transform: translateX(-100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader-wrapper .loader-section.section-right {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader {
opacity: 0;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.loaded #loader-wrapper {
visibility: hidden;
-webkit-transform: translateY(-100%);
-ms-transform: translateY(-100%);
transform: translateY(-100%);
-webkit-transition: all 0.3s 1s ease-out;
transition: all 0.3s 1s ease-out;
}
.no-js #loader-wrapper {
display: none;
}
.no-js h1 {
color: #222222;
}
#loader-wrapper .load_title {
font-family: 'Open Sans';
color: #fff;
font-size: 19px;
width: 100%;
text-align: center;
z-index: 9999999999999;
position: absolute;
top: 60%;
opacity: 1;
line-height: 30px;
}
#loader-wrapper .load_title span {
font-weight: normal;
font-style: italic;
font-size: 13px;
color: #fff;
opacity: 0.5;
}
</style>
</head>
<body>
<div id="app">
<div id="loader-wrapper">
<div id="loader"></div>
<div class="loader-section section-left"></div>
<div class="loader-section section-right"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,82 +1,66 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "ruoyi-vue-plus",
"version": "6.0.0",
"description": "RuoYi-Vue-Plus后台管理系统",
"license": "MIT",
"author": "LionLi",
"repository": {
"type": "git",
"url": "https://gitee.com/JavaLionLi/plus-ui.git"
},
"packageManager": "pnpm@10.34.5",
"name": "plus-ui-react",
"version": "6.0.0-BETA",
"private": true,
"description": "React + Ant Design version of RuoYi-Vue-Plus UI",
"type": "module",
"scripts": {
"build": "vite build --mode production",
"build:dev": "vite build --mode development",
"build:prod": "vite build --mode production",
"dev": "vite serve --mode development",
"build": "pnpm build:prod",
"build:dev": "cross-env UMI_ENV=development BABEL_POLYFILL=none max build",
"build:prod": "cross-env UMI_ENV=production BABEL_POLYFILL=none max build",
"dev": "cross-env UMI_ENV=development MOCK=none BABEL_POLYFILL=none max dev",
"fmt": "oxfmt .",
"lint": "oxlint src",
"lint:fix": "oxlint --fix src",
"preview": "vite preview"
"lint": "oxlint src config vite.config.ts && cross-env UMI_ENV=development BABEL_POLYFILL=none max setup && tsc --noEmit",
"lint:fix": "oxlint --fix src config vite.config.ts",
"openapi": "max openapi",
"preview": "max preview --port 8000",
"start": "cross-env UMI_ENV=development MOCK=none BABEL_POLYFILL=none max dev"
},
"dependencies": {
"@element-plus/icons-vue": "2.3.2",
"@highlightjs/vue-plugin": "2.1.2",
"@iconify/vue": "^5.0.1",
"@noble/ciphers": "^2.4.0",
"@vueuse/core": "14.4.0",
"@wangeditor-next/editor": "6.4.1",
"@wangeditor-next/editor-for-vue": "6.4.1",
"animate.css": "4.1.1",
"await-to-js": "3.0.0",
"axios": "1.20.0",
"echarts": "6.1.0",
"element-plus": "2.14.5",
"highlight.js": "11.12.0",
"image-conversion": "2.1.1",
"@ant-design/icons": "^6.2.5",
"@ant-design/pro-components": "3.1.12-0",
"@ant-design/x": "^2.7.0",
"@iconify/react": "^6.0.2",
"@tanstack/react-query": "^5.101.0",
"@umijs/max": "^4.6.61",
"@wangeditor-next/editor": "^5.7.10",
"@wangeditor-next/editor-for-react": "^2.0.2",
"ahooks": "^3.9.7",
"antd": "^6.4.3",
"antd-style": "^4.1.0",
"axios": "1.17.0",
"browser-image-compression": "^2.0.2",
"clsx": "^2.1.1",
"crypto-js": "4.2.0",
"dayjs": "^1.11.21",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"jsencrypt": "3.5.4",
"nprogress": "0.2.0",
"pinia": "4.0.3",
"vue": "3.5.42",
"vue-cropper": "1.1.4",
"vue-i18n": "11.4.10",
"vue-json-pretty": "2.6.0",
"vue-router": "5.3.1",
"vue-types": "7.0.0",
"vxe-pc-ui": "4.18.9",
"vxe-table": "4.21.9"
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-easy-crop": "^5.5.7",
"react-json-view-lite": "^2.5.0",
"react-syntax-highlighter": "^16.1.1",
"screenfull": "^6.0.2",
"zustand": "^5.0.14"
},
"devDependencies": {
"@types/node": "^26.1.1",
"@types/crypto-js": "4.2.2",
"@types/node": "^25.9.2",
"@types/nprogress": "0.2.3",
"@unocss/preset-attributify": "66.10.1",
"@unocss/preset-wind3": "66.10.1",
"@vitejs/plugin-vue": "^6.0.8",
"@vue/compiler-sfc": "3.5.42",
"autoprefixer": "10.5.5",
"oxfmt": "^0.67.0",
"oxlint": "^1.82.0",
"sass": "1.104.0",
"typescript": "^6.0.3",
"unocss": "66.10.1",
"unplugin-auto-import": "21.1.0",
"unplugin-vue-components": "32.1.0",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "^8.2.2",
"vite-plugin-svg-icons-ng": "^1.9.3",
"vitest": "4.1.11",
"vue-tsc": "^3.3.11"
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@umijs/lint": "^4.6.61",
"@umijs/max-plugin-openapi": "^2.0.3",
"cross-env": "^10.1.0",
"oxfmt": "^0.53.0",
"oxlint": "^1.68.0",
"typescript": "^6.0.3"
},
"browserslist": [
"Chrome >= 87",
"Edge >= 88",
"Safari >= 14",
"Firefox >= 78"
],
"engines": {
"node": ">=20.19.0",
"node": ">=20.0.0",
"pnpm": ">=10.0.0"
}
}

18536
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
packages:
- '.'
allowBuilds:
'@parcel/watcher': true
es5-ext: true
esbuild: true
overrides:
esbuild: 0.28.1

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
<template>
<el-config-provider :locale="appStore.locale" :size="appStore.size">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings';
import { handleThemeStyle } from '@/utils/theme';
const appStore = useAppStore();
onMounted(() => {
nextTick(() => {
//
handleThemeStyle(useSettingsStore().theme);
});
});
</script>

View File

@ -1,48 +0,0 @@
// 前缀
const animatePrefix = 'animate__animated ';
// 开启随机动画 随机动画值
const animateList: string[] = [
animatePrefix + 'animate__pulse',
animatePrefix + 'animate__rubberBand',
animatePrefix + 'animate__bounceIn',
animatePrefix + 'animate__bounceInLeft',
animatePrefix + 'animate__fadeIn',
animatePrefix + 'animate__fadeInLeft',
animatePrefix + 'animate__fadeInDown',
animatePrefix + 'animate__fadeInUp',
animatePrefix + 'animate__flipInX',
animatePrefix + 'animate__lightSpeedInLeft',
animatePrefix + 'animate__rotateInDownLeft',
animatePrefix + 'animate__rollIn',
animatePrefix + 'animate__rotateInDownLeft',
animatePrefix + 'animate__zoomIn',
animatePrefix + 'animate__zoomInDown',
animatePrefix + 'animate__slideInLeft',
animatePrefix + 'animate__lightSpeedIn'
];
// 关闭随机动画后的默认效果
const defaultAnimate = animatePrefix + 'animate__fadeIn';
// 搜索隐藏显示动画
const searchAnimate = {
enter: '',
leave: ''
};
// 菜单搜索动画
const menuSearchAnimate = {
enter: animatePrefix + 'animate__fadeIn',
leave: animatePrefix + 'animate__fadeOut'
};
// logo动画
const logoAnimate = {
enter: animatePrefix + 'animate__fadeIn',
leave: animatePrefix + 'animate__fadeOut'
};
export default {
animateList,
defaultAnimate,
searchAnimate,
menuSearchAnimate,
logoAnimate
};

View File

@ -1,10 +1,150 @@
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { SnailOpenApiUser } from './types';
import type { PageResult, R } from '@/api/types';
import request, { globalHeaders } from '@/api/request';
import { useAppStore } from '@/stores/appStore';
import { appEnv } from '@/utils/env';
import type {
AgentChatRequest,
AgentChatSyncResponse,
AgentItem,
ConversationMessage,
ConversationSummaryItem,
SnailOpenApiUser
} from './types';
export const registerCurrentSnailUser = (): AxiosPromise<SnailOpenApiUser> => {
return request({
export function fetchMyAgents() {
return request<R<AgentItem[] | PageResult<AgentItem>>>({
url: '/snail-ai/agents',
method: 'get'
});
}
export function fetchAgentDetail(id: number) {
return request<R<AgentItem>>({
url: `/snail-ai/agent/${id}`,
method: 'get'
});
}
export function fetchAgentConversations(
id: number,
params: { page?: number; size?: number; start?: string; end?: string }
) {
return request<R<PageResult<ConversationSummaryItem> | ConversationSummaryItem[]>>({
url: `/snail-ai/agent/${id}/conversations`,
method: 'get',
params
});
}
export function fetchConversationMessages(agentId: number, conversationId: string) {
return request<R<ConversationMessage[] | PageResult<ConversationMessage>>>({
url: `/snail-ai/agent/${agentId}/conversation/${conversationId}/messages`,
method: 'get'
});
}
export function createConversation(agentId: number, data: { title?: string }) {
return request<R<ConversationSummaryItem>>({
url: `/snail-ai/agent/${agentId}/conversation`,
method: 'post',
data
});
}
export function deleteConversation(agentId: number, conversationId: string) {
return request<R>({
url: `/snail-ai/agent/${agentId}/conversation/${conversationId}`,
method: 'delete'
});
}
export function registerCurrentSnailUser() {
return request<R<SnailOpenApiUser>>({
url: '/snail-ai/user/register',
method: 'post'
});
};
}
export function fetchChatMode() {
return request<R<{ mode?: 'stream' | 'sync' }>>({
url: '/snail-ai/chat/mode',
method: 'get'
});
}
export async function fetchAgentChat(
agentId: number,
data: AgentChatRequest,
options: {
onMessage: (chunk: string) => void;
onThinking?: (chunk: string) => void;
onDone: () => void;
onError: (error: Error) => void;
signal?: AbortSignal;
}
) {
const baseURL = appEnv.baseApi;
const language = useAppStore.getState().appLocale;
try {
const response = await fetch(`${baseURL}/snail-ai/agent/${agentId}/chat/stream`, {
method: 'POST',
headers: {
...globalHeaders(),
'Content-Language': language,
Accept: 'text/event-stream',
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(data),
signal: options.signal
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `HTTP ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('ReadableStream not supported');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const eventBlocks = buffer.split(/\r?\n\r?\n/);
buffer = eventBlocks.pop() || '';
for (const block of eventBlocks) {
if (!block.trim()) continue;
let eventName = 'message';
let payload = '';
for (const line of block.split(/\r?\n/)) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) payload += line.slice(5).trim();
}
if (!payload && eventName !== 'done') continue;
if (eventName === 'thinking') {
options.onThinking?.(payload);
} else if (eventName === 'done') {
options.onDone();
return;
} else if (eventName === 'error') {
throw new Error(payload || 'SSE stream error');
} else {
options.onMessage(payload);
}
}
}
options.onDone();
} catch (error) {
if ((error as Error).name !== 'AbortError') options.onError(error as Error);
}
}
export function fetchAgentChatSync(agentId: number, data: AgentChatRequest) {
return request<R<AgentChatSyncResponse>>({
url: `/snail-ai/agent/${agentId}/chat/sync`,
method: 'post',
data
});
}

View File

@ -1,3 +1,58 @@
export interface AgentItem {
id: number;
name: string;
description?: string;
avatar?: string;
greeting?: string;
status?: number;
presetQuestions?: string[];
}
export interface ConversationSummaryItem {
conversationId: string;
agentId?: number;
title: string;
lastMessageDt?: string;
createDt?: string;
updateDt?: string;
}
export interface ConversationSummaryList {
total: number;
rows: ConversationSummaryItem[];
}
export interface ConversationMessage {
role?: string;
content?: string;
thinking?: string;
message?: string;
text?: string;
messageType?: string;
senderType?: string;
}
export interface AgentChatRequest {
conversationId?: string;
content: string;
disabledMcpServerIds?: number[];
disabledSkillIds?: number[];
}
export interface AgentChatSyncResponse {
conversationId?: string;
content?: string;
traceId?: string;
durationMs?: number;
text?: string;
message?: string;
answer?: string;
reply?: string;
outputText?: string;
result?: string;
messages?: ConversationMessage[];
}
export interface SnailOpenApiUser {
openId: string;
nickname?: string;

View File

@ -1,63 +1,41 @@
import type { DemoForm, DemoQuery, DemoVO } from '@/api/demo/demo/types';
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { DemoForm, DemoQuery, DemoVO } from './types';
/**
*
* @param query
* @returns {*}
*/
export const listDemo = (query?: DemoQuery): AxiosPromise<PageResult<DemoVO>> => {
return request({
export function listDemo(query?: DemoQuery) {
return request<R<PageResult<DemoVO>>>({
url: '/demo/demo/list',
method: 'get',
params: query
});
};
}
/**
*
* @param id
*/
export const getDemo = (id: string | number): AxiosPromise<DemoVO> => {
return request({
url: '/demo/demo/' + id,
export function getDemo(id: string | number) {
return request<R<DemoVO>>({
url: `/demo/demo/${id}`,
method: 'get'
});
};
}
/**
*
* @param data
*/
export const addDemo = (data: DemoForm) => {
return request({
export function addDemo(data: DemoForm) {
return request<R>({
url: '/demo/demo',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
*/
export const updateDemo = (data: DemoForm) => {
return request({
export function updateDemo(data: DemoForm) {
return request<R>({
url: '/demo/demo',
method: 'put',
data: data
data
});
};
}
/**
*
* @param id
*/
export const delDemo = (id: string | number | Array<string | number>) => {
return request({
url: '/demo/demo/' + id,
export function delDemo(id: string | number | Array<string | number>) {
return request<R>({
url: `/demo/demo/${id}`,
method: 'delete'
});
};
}

View File

@ -1,90 +1,27 @@
export interface DemoVO {
/**
*
*/
id: string | number;
/**
* id
*/
deptId: string | number;
/**
* id
*/
userId: string | number;
/**
*
*/
orderNum: number;
/**
* key键
*/
testKey: string;
/**
*
*/
value: string;
}
import type { BaseEntity, PageQuery } from '@/api/types';
export interface DemoForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* id
*/
deptId?: string | number;
/**
* id
*/
userId?: string | number;
/**
*
*/
orderNum?: number;
/**
* key键
*/
testKey?: string;
/**
*
*/
value?: string;
}
export interface DemoQuery extends PageQuery {
/**
* id
*/
deptId?: string | number;
/**
* id
*/
userId?: string | number;
/**
*
*/
orderNum?: number;
/**
* key键
*/
testKey?: string;
/**
*
*/
value?: string;
}
export interface DemoVO {
id: string | number;
deptId: string | number;
userId: string | number;
orderNum: number;
testKey: string;
value: string;
}

View File

@ -1,62 +1,41 @@
import type { TreeForm, TreeQuery, TreeVO } from '@/api/demo/tree/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { TreeForm, TreeQuery, TreeVO } from './types';
/**
*
* @param query
* @returns {*}
*/
export const listTree = (query?: TreeQuery): AxiosPromise<TreeVO[]> => {
return request({
export function listTree(query?: TreeQuery) {
return request<R<TreeVO[]>>({
url: '/demo/tree/list',
method: 'get',
params: query
});
};
}
/**
*
* @param id
*/
export const getTree = (id: string | number): AxiosPromise<TreeVO> => {
return request({
url: '/demo/tree/' + id,
export function getTree(id: string | number) {
return request<R<TreeVO>>({
url: `/demo/tree/${id}`,
method: 'get'
});
};
}
/**
*
* @param data
*/
export const addTree = (data: TreeForm) => {
return request({
export function addTree(data: TreeForm) {
return request<R>({
url: '/demo/tree',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
*/
export const updateTree = (data: TreeForm) => {
return request({
export function updateTree(data: TreeForm) {
return request<R>({
url: '/demo/tree',
method: 'put',
data: data
data
});
};
}
/**
*
* @param id
*/
export const delTree = (id: string | number | Array<string | number>) => {
return request({
url: '/demo/tree/' + id,
export function delTree(id: string | number | Array<string | number>) {
return request<R>({
url: `/demo/tree/${id}`,
method: 'delete'
});
};
}

View File

@ -1,80 +1,25 @@
export interface TreeVO {
/**
*
*/
id: string | number;
/**
* id
*/
parentId: string | number;
/**
* id
*/
deptId: string | number;
/**
* id
*/
userId: string | number;
/**
*
*/
treeName: string;
/**
*
*/
children: TreeVO[];
}
import type { BaseEntity } from '@/api/types';
export interface TreeForm extends BaseEntity {
/**
*
*/
id?: string | number;
/**
* id
*/
parentId?: string | number;
/**
* id
*/
deptId?: string | number;
/**
* id
*/
userId?: string | number;
/**
*
*/
treeName?: string;
}
export interface TreeQuery {
/**
* id
*/
parentId?: string | number;
/**
* id
*/
deptId?: string | number;
/**
* id
*/
userId?: string | number;
/**
*
*/
treeName?: string;
}
export interface TreeVO {
id: string | number;
parentId: string | number;
deptId: string | number;
userId: string | number;
treeName: string;
children?: TreeVO[];
}

View File

@ -1,106 +1,74 @@
import type { UserInfo } from '@/api/system/user/types';
import type { AxiosPromise } from '@/utils/api-types';
import { closePush } from '@/utils/push';
import request from '@/utils/request';
import type { LoginData, LoginResult, VerifyCodeResult } from './types';
import type { LoginParams, LoginResult, R, RegisterParams, VerifyCodeResult } from '@/api/types';
import request from '@/api/request';
import { appEnv } from '@/utils/env';
// pc端固定客户端授权id
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
/**
* @param data {LoginData}
* @returns
*/
export function login(data: LoginData): AxiosPromise<LoginResult> {
const params = {
...data,
clientId: data.clientId || clientId,
grantType: data.grantType || 'password'
};
return request({
export function login(data: LoginParams) {
return request<R<LoginResult>>({
url: '/auth/login',
method: 'post',
headers: {
isToken: false,
isEncrypt: true,
isEncrypt: 'true',
repeatSubmit: false
},
method: 'post',
data: params
data: {
...data,
clientId: data.clientId || appEnv.clientId,
grantType: data.grantType || 'password'
}
});
}
// 注册方法
export function register(data: any) {
const params = {
...data,
clientId: clientId,
grantType: 'password'
};
return request({
url: '/auth/register',
headers: {
isToken: false,
isEncrypt: true,
repeatSubmit: false
},
method: 'post',
data: params
});
}
/**
*
*/
export function logout() {
closePush();
if (
import.meta.env.VITE_APP_MESSAGE_ENABLED === 'true' &&
import.meta.env.VITE_APP_MESSAGE_TRANSPORT.toLowerCase() === 'sse'
) {
request({
url: import.meta.env.VITE_APP_MESSAGE_PATH + '/close',
method: 'get'
});
}
return request({
return request<R>({
url: '/auth/logout',
method: 'post'
});
}
/**
*
*/
export function getCodeImg(): AxiosPromise<VerifyCodeResult> {
return request({
export function register(data: RegisterParams) {
return request<R>({
url: '/auth/register',
method: 'post',
headers: {
isToken: false,
isEncrypt: 'true',
repeatSubmit: false
},
data: {
...data,
clientId: appEnv.clientId,
grantType: 'password'
}
});
}
export function callback(data: LoginParams) {
return request<R<LoginResult>>({
url: '/auth/social/callback',
method: 'post',
data: {
...data,
clientId: appEnv.clientId,
grantType: 'social'
}
});
}
export function getCodeImg() {
return request<R<VerifyCodeResult>>({
url: '/auth/code',
method: 'get',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
});
}
/**
*
*/
export function callback(data: LoginData): AxiosPromise<any> {
const LoginData = {
...data,
clientId: clientId,
grantType: 'social'
};
return request({
url: '/auth/social/callback',
method: 'post',
data: LoginData
});
}
// 获取用户详细信息
export function getInfo(): AxiosPromise<UserInfo> {
return request({
export function getInfo() {
return request<R<UserInfo>>({
url: '/system/user/getInfo',
method: 'get'
});

View File

@ -1,10 +1,8 @@
import type { RouteRecordRaw } from 'vue-router';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { BackendRoute, R } from '@/api/types';
import request from '@/api/request';
// 获取路由
export function getRouters(): AxiosPromise<RouteRecordRaw[]> {
return request({
export function getRouters() {
return request<R<BackendRoute[]>>({
url: '/system/menu/getRouters',
method: 'get'
});

View File

@ -1,12 +1,10 @@
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { CacheVO } from './types';
// 查询缓存详细
export function getCache(): AxiosPromise<CacheVO> {
return request({
export function getCache() {
return request<R<CacheVO>>({
url: '/monitor/cache',
method: 'get'
});
}

View File

@ -1,7 +1,5 @@
export interface CacheVO {
commandStats: Array<{ name: string; value: string }>;
export interface CacheVO {
commandStats: Array<{ name: string; value: string | number }>;
dbSize: number;
info: { [key: string]: string };
info: Record<string, string>;
}

View File

@ -1,36 +1,35 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { LoginInfoQuery, LoginInfoVO } from './types';
// 查询登录日志列表
export function list(query: LoginInfoQuery): AxiosPromise<PageResult<LoginInfoVO>> {
return request({
export function listLoginInfo(query: LoginInfoQuery) {
return request<R<PageResult<LoginInfoVO>>>({
url: '/monitor/loginInfo/list',
method: 'get',
params: query
});
}
// 删除登录日志
export function list(query: LoginInfoQuery) {
return listLoginInfo(query);
}
export function delLoginInfo(infoId: string | number | Array<string | number>) {
return request({
url: '/monitor/loginInfo/' + infoId,
return request<R>({
url: `/monitor/loginInfo/${infoId}`,
method: 'delete'
});
}
// 解锁用户登录状态
export function unlockLoginInfo(userName: string | Array<string>) {
return request({
url: '/monitor/loginInfo/unlock/' + userName,
export function unlockLoginInfo(userName: string | string[]) {
return request<R>({
url: `/monitor/loginInfo/unlock/${userName}`,
method: 'get'
});
}
// 清空登录日志
export function cleanLoginInfo() {
return request({
return request<R>({
url: '/monitor/loginInfo/clean',
method: 'delete'
});

View File

@ -1,20 +1,24 @@
export interface LoginInfoVO {
infoId: string | number;
tenantId: string | number;
userName: string;
status: string;
ipaddr: string;
loginLocation: string;
browser: string;
os: string;
msg: string;
loginTime: string;
import type { PageQuery } from '@/api/types';
export interface LoginInfoQuery extends Omit<PageQuery, 'isAsc'> {
ipaddr?: string;
userName?: string;
status?: string;
orderByColumn?: string;
isAsc?: string;
}
export interface LoginInfoQuery extends PageQuery {
ipaddr: string;
export interface LoginInfoVO {
infoId: string | number;
tenantId?: string | number;
userName: string;
clientKey?: string;
deviceType?: string;
status: string;
orderByColumn: string;
isAsc: string;
ipaddr?: string;
loginLocation?: string;
browser?: string;
os?: string;
msg?: string;
loginTime?: string;
}

View File

@ -1,37 +1,36 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { OnlineQuery, OnlineVO } from './types';
// 查询在线用户列表
export function list(query: OnlineQuery): AxiosPromise<PageResult<OnlineVO>> {
return request({
export function listOnline(query: OnlineQuery) {
return request<R<PageResult<OnlineVO>>>({
url: '/monitor/online/list',
method: 'get',
params: query
});
}
// 强退用户
export function list(query: OnlineQuery) {
return listOnline(query);
}
export function forceLogout(tokenId: string) {
return request({
url: '/monitor/online/' + tokenId,
return request<R>({
url: `/monitor/online/${tokenId}`,
method: 'delete'
});
}
// 获取当前用户登录在线设备
export function getOnline() {
return request({
return request<R<OnlineVO[] | PageResult<OnlineVO>>>({
url: '/monitor/online',
method: 'get'
});
}
// 删除当前在线设备
export function delOnline(tokenId: string) {
return request({
url: '/monitor/online/myself/' + tokenId,
return request<R>({
url: `/monitor/online/myself/${tokenId}`,
method: 'delete'
});
}

View File

@ -1,15 +1,19 @@
import type { BaseEntity, PageQuery } from '@/api/types';
export interface OnlineQuery extends PageQuery {
ipaddr: string;
userName: string;
ipaddr?: string;
userName?: string;
}
export interface OnlineVO extends BaseEntity {
tokenId: string;
deptName: string;
deptName?: string;
userName: string;
ipaddr: string;
loginLocation: string;
browser: string;
os: string;
loginTime: number;
clientKey?: string;
deviceType?: string;
ipaddr?: string;
loginLocation?: string;
browser?: string;
os?: string;
loginTime?: number | string;
}

View File

@ -1,28 +1,28 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { OperLogQuery, OperLogVO } from './types';
// 查询操作日志列表
export function list(query: OperLogQuery): AxiosPromise<PageResult<OperLogVO>> {
return request({
export function listOperlog(query: OperLogQuery) {
return request<R<PageResult<OperLogVO>>>({
url: '/monitor/operlog/list',
method: 'get',
params: query
});
}
// 删除操作日志
export function list(query: OperLogQuery) {
return listOperlog(query);
}
export function delOperlog(operId: string | number | Array<string | number>) {
return request({
url: '/monitor/operlog/' + operId,
return request<R>({
url: `/monitor/operlog/${operId}`,
method: 'delete'
});
}
// 清空操作日志
export function cleanOperlog() {
return request({
return request<R>({
url: '/monitor/operlog/clean',
method: 'delete'
});

View File

@ -1,71 +1,45 @@
export interface OperLogQuery extends PageQuery {
operIp: string;
title: string;
operName: string;
userId: string;
deptId: string;
clientKey: string;
deviceType: string;
browser: string;
os: string;
businessType: string;
status: string;
orderByColumn: string;
isAsc: string;
import type { BaseEntity, PageQuery } from '@/api/types';
export interface OperLogQuery extends Omit<PageQuery, 'isAsc'> {
operIp?: string;
title?: string;
operName?: string;
userId?: string;
deptId?: string;
clientKey?: string;
deviceType?: string;
browser?: string;
os?: string;
businessType?: string;
status?: string;
orderByColumn?: string;
isAsc?: string;
}
export interface OperLogVO extends BaseEntity {
operId: string | number;
tenantId: string;
title: string;
businessType: number;
businessTypes: number[] | undefined;
method: string;
requestMethod: string;
operatorType: number;
operName: string;
userId: string | number;
deptId: string | number;
deptName: string;
clientKey: string;
deviceType: string;
browser: string;
os: string;
operUrl: string;
operIp: string;
operLocation: string;
operParam: string;
jsonResult: string;
status: number;
errorMsg: string;
operTime: string;
costTime: number;
}
export interface OperLogForm {
operId: number | string | undefined;
tenantId: string | number | undefined;
title: string;
businessType: number;
businessTypes: number[] | undefined;
method: string;
requestMethod: string;
operatorType: number;
operName: string;
userId: string | number | undefined;
deptId: string | number | undefined;
deptName: string;
clientKey: string;
deviceType: string;
browser: string;
os: string;
operUrl: string;
operIp: string;
operLocation: string;
operParam: string;
jsonResult: string;
status: number;
errorMsg: string;
operTime: string;
costTime: number;
tenantId?: string;
title?: string;
businessType?: number | string;
businessTypes?: number[];
method?: string;
requestMethod?: string;
operatorType?: number;
operName?: string;
userId?: string | number;
deptId?: string | number;
deptName?: string;
clientKey?: string;
deviceType?: string;
browser?: string;
os?: string;
operUrl?: string;
operIp?: string;
operLocation?: string;
operParam?: string;
jsonResult?: string;
status?: number | string;
errorMsg?: string;
operTime?: string;
costTime?: number;
}

227
src/api/request.ts Normal file
View File

@ -0,0 +1,227 @@
import { history } from '@umijs/max';
import { Modal } from 'antd';
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
import { useAppStore } from '@/stores/appStore';
import { useTagsViewStore } from '@/stores/tagsViewStore';
import { getToken, removeToken } from '@/utils/auth';
import { decryptBase64, decryptWithAes, encryptBase64, encryptWithAes, generateAesKey } from '@/utils/crypto';
import { appEnv } from '@/utils/env';
import { decrypt, encrypt } from '@/utils/jsencrypt';
import { tansParams } from '@/utils/ruoyi';
const SUCCESS = 200;
const WARN = 601;
const SERVER_ERROR = 500;
const encryptHeader = 'encrypt-key';
const repeatSubmitStorageKey = 'sessionObj';
const repeatSubmitInterval = 500;
export interface RequestConfig<D = unknown> extends AxiosRequestConfig<D> {
headers?: AxiosRequestConfig<D>['headers'] & {
isToken?: false;
isEncrypt?: 'true' | 'false' | boolean;
repeatSubmit?: false;
};
}
export const isRelogin = { show: false };
type HandledRequestError = Error & { isHandled?: boolean };
function createHandledError(message: string) {
const error = new Error(message) as HandledRequestError;
error.isHandled = true;
return error;
}
export function isHandledRequestError(error: unknown) {
return Boolean((error as { isHandled?: boolean } | undefined)?.isHandled);
}
export function globalHeaders() {
return {
Authorization: `Bearer ${getToken() || ''}`,
clientid: appEnv.clientId
};
}
function stableDataString(data: unknown) {
if (data instanceof FormData) return '';
if (typeof data === 'string') return data;
if (typeof data === 'undefined' || data === null) return '';
if (typeof data === 'object') return JSON.stringify(data);
return String(data);
}
function getSessionSubmitRecord() {
try {
const text = sessionStorage.getItem(repeatSubmitStorageKey);
return text ? (JSON.parse(text) as { url?: string; data?: string; time?: number }) : undefined;
} catch {
sessionStorage.removeItem(repeatSubmitStorageKey);
return undefined;
}
}
function setSessionSubmitRecord(record: { url?: string; data?: string; time: number }) {
sessionStorage.setItem(repeatSubmitStorageKey, JSON.stringify(record));
}
const service = axios.create({
baseURL: appEnv.baseApi,
timeout: 50000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
clientid: appEnv.clientId
},
transitional: {
clarifyTimeoutError: true
}
});
service.interceptors.request.use(config => {
config.headers.set('Content-Language', useAppStore.getState().appLocale);
const isToken = config.headers.get('isToken') === false;
const isEncrypt = config.headers.get('isEncrypt') === 'true' || config.headers.get('isEncrypt') === true;
const isRepeatSubmitDisabled = config.headers.get('repeatSubmit') === false;
if (getToken() && !isToken) {
config.headers.set('Authorization', `Bearer ${getToken()}`);
}
if (config.method === 'get' && config.params) {
let url = `${config.url}?${tansParams(config.params)}`;
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmitDisabled && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: stableDataString(config.data),
time: Date.now()
};
const sessionObj = getSessionSubmitRecord();
if (
sessionObj &&
sessionObj.url === requestObj.url &&
sessionObj.data === requestObj.data &&
requestObj.time - (sessionObj.time || 0) < repeatSubmitInterval
) {
return Promise.reject(new Error('数据正在处理,请勿重复提交'));
}
setSessionSubmitRecord(requestObj);
}
if (appEnv.encryptEnabled && isEncrypt && (config.method === 'post' || config.method === 'put')) {
const aesKey = generateAesKey();
const encryptedKey = encrypt(encryptBase64(aesKey));
if (encryptedKey) {
config.headers.set(encryptHeader, encryptedKey);
}
const data = typeof config.data === 'object' ? JSON.stringify(config.data) : String(config.data ?? '');
config.data = encryptWithAes(data, aesKey);
}
if (config.data instanceof FormData) {
config.headers.delete('Content-Type');
}
config.headers.delete('isToken');
config.headers.delete('isEncrypt');
config.headers.delete('repeatSubmit');
return config;
});
function normalizeErrorMessage(message?: string) {
if (!message) return undefined;
if (message === 'Network Error') return '后端接口连接异常';
if (message.includes('timeout')) return '系统接口请求超时';
if (message.includes('Request failed with status code')) return `系统接口${message.slice(-3)}异常`;
return message;
}
async function parseResponseErrorData(data: unknown): Promise<string | undefined> {
if (!data) return undefined;
if (data instanceof Blob) return parseResponseErrorData(await data.text());
if (data instanceof ArrayBuffer) return parseResponseErrorData(new TextDecoder().decode(data));
if (typeof data === 'string') {
const text = data.trim();
if (!text) return undefined;
try {
return parseResponseErrorData(JSON.parse(text));
} catch {
return text;
}
}
if (typeof data === 'object') {
const payload = data as { msg?: string; message?: string };
return payload.msg || payload.message;
}
return undefined;
}
async function getErrorMessage(error: AxiosError) {
const responseMessage = await parseResponseErrorData(error.response?.data);
return responseMessage || normalizeErrorMessage(error.message) || '系统未知错误';
}
service.interceptors.response.use(
response => {
const keyStr = response.headers[encryptHeader];
if (appEnv.encryptEnabled && keyStr) {
const base64Str = decrypt(keyStr);
if (base64Str) {
const aesKey = decryptBase64(base64Str);
response.data = JSON.parse(decryptWithAes(response.data, aesKey));
}
}
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
return response.data;
}
const code = response.data?.code || SUCCESS;
const msg = response.data?.msg || '系统未知错误';
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
Modal.confirm({
title: '系统提示',
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
okText: '重新登录',
cancelText: '取消',
onOk: () => {
isRelogin.show = false;
removeToken();
useTagsViewStore.getState().resetTags();
history.replace(
`/login?redirect=${encodeURIComponent(`${window.location.pathname || '/'}${window.location.search || ''}`)}`
);
},
onCancel: () => {
isRelogin.show = false;
}
});
}
return Promise.reject(createHandledError('无效的会话,或者会话已过期,请重新登录。'));
}
if (code === SERVER_ERROR || code === WARN || code !== SUCCESS) {
return Promise.reject(new Error(msg));
}
return response.data;
},
async error => {
return Promise.reject(new Error(await getErrorMessage(error)));
}
);
export default function request<T = unknown, D = unknown>(config: RequestConfig<D>): Promise<T> {
return service.request<T, T, D>(config);
}

View File

@ -1,81 +1,49 @@
import type { ClientForm, ClientQuery, ClientVO } from '@/api/system/client/types';
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { ClientForm, ClientQuery, ClientVO } from './types';
/**
*
* @param query
* @returns {*}
*/
export const listClient = (query?: ClientQuery): AxiosPromise<PageResult<ClientVO>> => {
return request({
export function listClient(query?: ClientQuery) {
return request<R<PageResult<ClientVO>>>({
url: '/system/client/list',
method: 'get',
params: query
});
};
}
/**
*
* @param id
*/
export const getClient = (id: string | number): AxiosPromise<ClientVO> => {
return request({
url: '/system/client/' + id,
export function getClient(id: string | number) {
return request<R<ClientVO>>({
url: `/system/client/${id}`,
method: 'get'
});
};
}
/**
*
* @param data
*/
export const addClient = (data: ClientForm) => {
return request({
export function addClient(data: ClientForm) {
return request<R>({
url: '/system/client',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateClient = (data: ClientForm) => {
return request({
url: '/system/client',
method: 'put',
data: data
});
};
/**
*
* @param id
*/
export const delClient = (id: string | number | Array<string | number>) => {
return request({
url: '/system/client/' + id,
method: 'delete'
});
};
/**
*
* @param clientId id
* @param status
*/
export function changeStatus(clientId: string, status: string) {
const data = {
clientId,
status
};
return request({
url: '/system/client/changeStatus',
method: 'put',
data: data
data
});
}
export function updateClient(data: ClientForm) {
return request<R>({
url: '/system/client',
method: 'put',
data
});
}
export function delClient(id: string | number | Array<string | number>) {
return request<R>({
url: `/system/client/${id}`,
method: 'delete'
});
}
export function changeStatus(clientId: string, status: string) {
return request<R>({
url: '/system/client/changeStatus',
method: 'put',
data: { clientId, status }
});
}

View File

@ -1,185 +1,46 @@
export interface ClientVO {
/**
* id
*/
id: string | number;
/**
* id
*/
clientId: string;
/**
* key
*/
clientKey: string;
/**
*
*/
clientSecret: string;
/**
*
*/
grantTypeList: string[];
/**
*
*/
deviceType: string;
/**
* 访
*/
accessPath?: string;
/**
* 访
*/
accessPathList?: string[];
/**
* IP白名单
*/
ipWhitelist?: string;
/**
* IP白名单列表
*/
ipWhitelistList?: string[];
/**
* token活跃超时时间
*/
activeTimeout: number;
/**
* token固定超时
*/
timeout: number;
/**
* 0 1
*/
status: string;
}
import type { BaseEntity, PageQuery } from '@/api/types';
export interface ClientForm extends BaseEntity {
/**
* id
*/
id?: string | number;
/**
* id
*/
clientId?: string | number;
/**
* key
*/
clientKey?: string;
/**
*
*/
clientSecret?: string;
/**
*
*/
grantTypeList?: string[];
/**
*
*/
deviceType?: string;
/**
* 访
*/
accessPath?: string;
/**
* 访
*/
accessPathList?: string[];
/**
* IP白名单
*/
ipWhitelist?: string;
/**
* IP白名单列表
*/
ipWhitelistList?: string[];
/**
* token活跃超时时间
*/
activeTimeout?: number;
/**
* token固定超时
*/
timeout?: number;
/**
* 0 1
*/
status?: string;
}
export interface ClientQuery extends PageQuery {
/**
* id
*/
clientId?: string | number;
/**
* key
*/
clientKey?: string;
/**
*
*/
clientSecret?: string;
/**
*
*/
grantType?: string;
/**
*
*/
deviceType?: string;
/**
* 访
*/
accessPath?: string;
/**
* IP白名单
*/
ipWhitelist?: string;
/**
* token活跃超时时间
*/
activeTimeout?: number;
/**
* token固定超时
*/
timeout?: number;
/**
* 0 1
*/
status?: string;
}
export interface ClientVO {
id: string | number;
clientId: string;
clientKey: string;
clientSecret: string;
grantTypeList?: string[];
deviceType?: string;
accessPath?: string;
accessPathList?: string[];
ipWhitelist?: string;
ipWhitelistList?: string[];
activeTimeout?: number;
timeout?: number;
status: string;
}

View File

@ -1,54 +1,47 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { ConfigForm, ConfigQuery, ConfigVO } from './types';
// 查询参数列表
export function listConfig(query: ConfigQuery): AxiosPromise<PageResult<ConfigVO>> {
return request({
export function listConfig(query: ConfigQuery) {
return request<R<PageResult<ConfigVO>>>({
url: '/system/config/list',
method: 'get',
params: query
});
}
// 查询参数详细
export function getConfig(configId: string | number): AxiosPromise<ConfigVO> {
return request({
url: '/system/config/' + configId,
export function getConfig(configId: string | number) {
return request<R<ConfigVO>>({
url: `/system/config/${configId}`,
method: 'get'
});
}
// 根据参数键名查询参数值
export function getConfigKey(configKey: string): AxiosPromise<string> {
return request({
url: '/system/config/configKey/' + configKey,
export function getConfigKey(configKey: string) {
return request<R<string>>({
url: `/system/config/configKey/${configKey}`,
method: 'get'
});
}
// 新增参数配置
export function addConfig(data: ConfigForm) {
return request({
return request<R>({
url: '/system/config',
method: 'post',
data: data
data
});
}
// 修改参数配置
export function updateConfig(data: ConfigForm) {
return request({
return request<R>({
url: '/system/config',
method: 'put',
data: data
data
});
}
// 修改参数配置
export function updateConfigByKey(key: string, value: any) {
return request({
export function updateConfigByKey(key: string, value: unknown) {
return request<R>({
url: '/system/config/updateByKey',
method: 'put',
data: {
@ -58,18 +51,20 @@ export function updateConfigByKey(key: string, value: any) {
});
}
// 删除参数配置
export function delConfig(configId: string | number | Array<string | number>) {
return request({
url: '/system/config/' + configId,
return request<R>({
url: `/system/config/${configId}`,
method: 'delete'
});
}
// 刷新参数缓存
export function refreshCache() {
return request({
export function refreshConfigCache() {
return request<R>({
url: '/system/config/refreshCache',
method: 'delete'
});
}
export function refreshCache() {
return refreshConfigCache();
}

View File

@ -1,23 +1,24 @@
import type { BaseEntity, PageQuery } from '@/api/types';
export interface ConfigForm {
configId?: number | string;
configName?: string;
configKey?: string;
configValue?: string;
configType?: string;
remark?: string;
}
export interface ConfigQuery extends PageQuery {
configName?: string;
configKey?: string;
configType?: string;
}
export interface ConfigVO extends BaseEntity {
configId: number | string;
configName: string;
configKey: string;
configValue: string;
configType: string;
remark: string;
}
export interface ConfigForm {
configId: number | string | undefined;
configName: string;
configKey: string;
configValue: string;
configType: string;
remark: string;
}
export interface ConfigQuery extends PageQuery {
configName: string;
configKey: string;
configType: string;
}

View File

@ -1,65 +1,55 @@
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import { type DeptForm, type DeptQuery, type DeptVO } from './types';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { DeptForm, DeptQuery, DeptVO } from './types';
// 查询部门列表
export const listDept = (query?: DeptQuery) => {
return request({
export function listDept(query?: DeptQuery) {
return request<R<DeptVO[]>>({
url: '/system/dept/list',
method: 'get',
params: query
});
};
}
/**
* deptIds查询部门
* @param deptIds
*/
export const optionSelect = (deptIds: (number | string)[]): AxiosPromise<DeptVO[]> => {
return request({
url: '/system/dept/optionselect?deptIds=' + deptIds,
export function optionSelect(deptIds: Array<number | string>) {
return request<R<DeptVO[]>>({
url: `/system/dept/optionselect?deptIds=${deptIds}`,
method: 'get'
});
};
}
// 查询部门列表(排除节点)
export const listDeptExcludeChild = (deptId: string | number): AxiosPromise<DeptVO[]> => {
return request({
url: '/system/dept/list/exclude/' + deptId,
export function listDeptExcludeChild(deptId: string | number) {
return request<R<DeptVO[]>>({
url: `/system/dept/list/exclude/${deptId}`,
method: 'get'
});
};
}
// 查询部门详细
export const getDept = (deptId: string | number): AxiosPromise<DeptVO> => {
return request({
url: '/system/dept/' + deptId,
export function getDept(deptId: string | number) {
return request<R<DeptVO>>({
url: `/system/dept/${deptId}`,
method: 'get'
});
};
}
// 新增部门
export const addDept = (data: DeptForm) => {
return request({
export function addDept(data: DeptForm) {
return request<R>({
url: '/system/dept',
method: 'post',
data: data
data
});
};
}
// 修改部门
export const updateDept = (data: DeptForm) => {
return request({
export function updateDept(data: DeptForm) {
return request<R>({
url: '/system/dept',
method: 'put',
data: data
data
});
};
}
// 删除部门
export const delDept = (deptId: number | string) => {
return request({
url: '/system/dept/' + deptId,
export function delDept(deptId: number | string) {
return request<R>({
url: `/system/dept/${deptId}`,
method: 'delete'
});
};
}

View File

@ -1,48 +1,5 @@
/**
*
*/
export interface DeptQuery extends PageQuery {
deptName?: string;
deptCategory?: string;
status?: number;
}
import type { BaseEntity, PageQuery } from '@/api/types';
/**
*
*/
export interface DeptVO extends BaseEntity {
id: number | string;
parentName: string;
parentId: number | string;
children: DeptVO[];
deptId: number | string;
deptName: string;
deptCategory: string;
orderNum: number;
leader: string;
phone: string;
email: string;
status: string;
delFlag: string;
ancestors: string;
menuId: string | number;
}
/**
*
*/
export interface DeptTreeVO extends BaseEntity {
id: number | string;
label: string;
parentId: number | string;
weight: number;
children: DeptTreeVO[];
disabled: boolean;
}
/**
*
*/
export interface DeptForm {
parentName?: string;
parentId?: number | string;
@ -51,10 +8,43 @@ export interface DeptForm {
deptName?: string;
deptCategory?: string;
orderNum?: number;
leader?: string;
leader?: string | number;
phone?: string;
email?: string;
status?: string;
delFlag?: string;
ancestors?: string;
}
export interface DeptQuery extends PageQuery {
deptName?: string;
deptCategory?: string;
status?: string;
}
export interface DeptTreeVO extends BaseEntity {
id: number | string;
label: string;
parentId: number | string;
weight: number;
children?: DeptTreeVO[];
disabled?: boolean;
}
export interface DeptVO extends BaseEntity {
id?: number | string;
parentName?: string;
parentId?: number | string;
children?: DeptVO[];
deptId: number | string;
deptName: string;
deptCategory?: string;
orderNum?: number;
leader?: string;
phone?: string;
email?: string;
status?: string;
delFlag?: string;
ancestors?: string;
menuId?: string | number;
}

View File

@ -1,54 +1,48 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { DictDataForm, DictDataQuery, DictDataVO } from './types';
// 根据字典类型查询字典数据信息
export function getDicts(dictType: string): AxiosPromise<DictDataVO[]> {
return request({
url: '/system/dict/data/type/' + dictType,
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { DictData, DictDataForm, DictDataQuery } from './types';
export function getDicts(dictType: string) {
return request<R<DictData[]>>({
url: `/system/dict/data/type/${dictType}`,
method: 'get'
});
}
// 查询字典数据列表
export function listData(query: DictDataQuery): AxiosPromise<PageResult<DictDataVO>> {
return request({
export function listData(query: DictDataQuery) {
return request<R<PageResult<DictData>>>({
url: '/system/dict/data/list',
method: 'get',
params: query
});
}
// 查询字典数据详细
export function getData(dictCode: string | number): AxiosPromise<DictDataVO> {
return request({
url: '/system/dict/data/' + dictCode,
export function getData(dictCode: string | number) {
return request<R<DictData>>({
url: `/system/dict/data/${dictCode}`,
method: 'get'
});
}
// 新增字典数据
export function addData(data: DictDataForm) {
return request({
return request<R>({
url: '/system/dict/data',
method: 'post',
data: data
data
});
}
// 修改字典数据
export function updateData(data: DictDataForm) {
return request({
return request<R>({
url: '/system/dict/data',
method: 'put',
data: data
data
});
}
// 删除字典数据
export function delData(dictCode: string | number | Array<string | number>) {
return request({
url: '/system/dict/data/' + dictCode,
return request<R>({
url: `/system/dict/data/${dictCode}`,
method: 'delete'
});
}

View File

@ -1,26 +1,32 @@
export interface DictDataQuery extends PageQuery {
dictName: string;
dictType: string;
dictLabel: string;
}
import type { BaseEntity, PageQuery } from '@/api/types';
export interface DictDataVO extends BaseEntity {
dictCode: string;
export interface DictData extends BaseEntity {
dictCode: string | number;
dictSort: number;
dictLabel: string;
dictValue: string;
cssClass: string;
listClass: ElTagType;
dictSort: number;
remark: string;
dictType: string;
cssClass?: string;
listClass?: string;
isDefault?: string;
status: string;
}
export type DictDataVO = DictData;
export interface DictDataForm {
dictType?: string;
dictCode: string | undefined;
dictLabel: string;
dictValue: string;
cssClass: string;
listClass: ElTagType;
dictSort: number;
remark: string;
dictCode?: string | number;
dictLabel?: string;
dictValue?: string;
cssClass?: string;
listClass?: string;
dictSort?: number;
remark?: string;
}
export interface DictDataQuery extends PageQuery {
dictName?: string;
dictType?: string;
dictLabel?: string;
}

View File

@ -1,62 +1,54 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { DictTypeForm, DictTypeQuery, DictTypeVO } from './types';
// 查询字典类型列表
export function listType(query: DictTypeQuery): AxiosPromise<PageResult<DictTypeVO>> {
return request({
export function listType(query: DictTypeQuery) {
return request<R<PageResult<DictTypeVO>>>({
url: '/system/dict/type/list',
method: 'get',
params: query
});
}
// 查询字典类型详细
export function getType(dictId: number | string): AxiosPromise<DictTypeVO> {
return request({
url: '/system/dict/type/' + dictId,
export function getType(dictId: number | string) {
return request<R<DictTypeVO>>({
url: `/system/dict/type/${dictId}`,
method: 'get'
});
}
// 新增字典类型
export function addType(data: DictTypeForm) {
return request({
return request<R>({
url: '/system/dict/type',
method: 'post',
data: data
data
});
}
// 修改字典类型
export function updateType(data: DictTypeForm) {
return request({
return request<R>({
url: '/system/dict/type',
method: 'put',
data: data
data
});
}
// 删除字典类型
export function delType(dictId: string | number | Array<string | number>) {
return request({
url: '/system/dict/type/' + dictId,
return request<R>({
url: `/system/dict/type/${dictId}`,
method: 'delete'
});
}
// 刷新字典缓存
export function refreshCache() {
return request({
return request<R>({
url: '/system/dict/type/refreshCache',
method: 'delete'
});
}
// 获取字典选择框列表
export function optionselect(): AxiosPromise<DictTypeVO[]> {
return request({
export function optionselect() {
return request<R<DictTypeVO[]>>({
url: '/system/dict/type/optionselect',
method: 'get'
});

View File

@ -1,18 +1,19 @@
import type { BaseEntity, PageQuery } from '@/api/types';
export interface DictTypeForm {
dictId?: number | string;
dictName?: string;
dictType?: string;
remark?: string;
}
export interface DictTypeQuery extends PageQuery {
dictName?: string;
dictType?: string;
}
export interface DictTypeVO extends BaseEntity {
dictId: number | string;
dictName: string;
dictType: string;
remark: string;
}
export interface DictTypeForm {
dictId: number | string | undefined;
dictName: string;
dictType: string;
remark: string;
}
export interface DictTypeQuery extends PageQuery {
dictName: string;
dictType: string;
}

View File

@ -1,70 +1,63 @@
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { MenuForm, MenuQuery, MenuTreeOption, MenuVO, RoleMenuTree } from './types';
import type { RoleMenuTree } from '@/api/system/role/types';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { MenuForm, MenuQuery, MenuTreeOption, MenuVO } from './types';
// 查询菜单列表
export const listMenu = (query?: MenuQuery): AxiosPromise<MenuVO[]> => {
return request({
export function listMenu(query?: MenuQuery) {
return request<R<MenuVO[]>>({
url: '/system/menu/list',
method: 'get',
params: query
});
};
}
// 查询菜单详细
export const getMenu = (menuId: string | number): AxiosPromise<MenuVO> => {
return request({
url: '/system/menu/' + menuId,
export function getMenu(menuId: string | number) {
return request<R<MenuVO>>({
url: `/system/menu/${menuId}`,
method: 'get'
});
};
}
// 查询菜单下拉树结构
export const treeselect = (): AxiosPromise<MenuTreeOption[]> => {
return request({
export function treeselect() {
return request<R<MenuTreeOption[]>>({
url: '/system/menu/treeselect',
method: 'get'
});
};
}
// 根据角色ID查询菜单下拉树结构
export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => {
return request({
url: '/system/menu/roleMenuTreeselect/' + roleId,
export function roleMenuTreeselect(roleId: string | number) {
return request<R<RoleMenuTree>>({
url: `/system/menu/roleMenuTreeselect/${roleId}`,
method: 'get'
});
};
}
// 新增菜单
export const addMenu = (data: MenuForm) => {
return request({
export function addMenu(data: MenuForm) {
return request<R>({
url: '/system/menu',
method: 'post',
data: data
data
});
};
}
// 修改菜单
export const updateMenu = (data: MenuForm) => {
return request({
export function updateMenu(data: MenuForm) {
return request<R>({
url: '/system/menu',
method: 'put',
data: data
data
});
};
}
// 删除菜单
export const delMenu = (menuId: string | number) => {
return request({
url: '/system/menu/' + menuId,
export function delMenu(menuId: string | number) {
return request<R>({
url: `/system/menu/${menuId}`,
method: 'delete'
});
};
}
// 级联删除菜单
export const cascadeDelMenu = (menuIds: Array<string | number>) => {
return request({
url: '/system/menu/cascade/' + menuIds,
export function cascadeDelMenu(menuIds: Array<string | number>) {
return request<R>({
url: `/system/menu/cascade/${menuIds}`,
method: 'delete'
});
};
}

View File

@ -1,75 +1,51 @@
import type { MenuTypeEnum } from '@/enums/MenuTypeEnum';
/**
*
*/
export interface MenuTreeOption {
id: string | number;
label: string;
parentId: string | number;
weight: number;
menuType?: MenuTypeEnum | string;
visible?: string;
status?: string;
disabled?: boolean;
children?: MenuTreeOption[];
}
export interface RoleMenuTree {
menus: MenuTreeOption[];
checkedKeys: Array<string | number>;
}
/**
*
*/
export interface RoleMenuButtonOption {
menuId: string | number;
menuName: string;
parentId: string | number;
perms?: string;
status?: string;
disabled?: boolean;
}
/**
*
*/
export interface MenuQuery {
keywords?: string;
menuName?: string;
status?: string;
}
/**
*
*/
export interface MenuVO extends BaseEntity {
parentName: string;
parentId: string | number;
children: MenuVO[];
menuId: string | number;
menuName: string;
orderNum: number;
path: string;
component: string;
queryParam: string;
isFrame: string;
isCache: string;
menuType: MenuTypeEnum;
visible: string;
status: string;
icon: string;
activeMenu: string;
ext: string;
remark: string;
}
import type { BaseEntity } from '@/api/types';
export interface MenuForm {
parentName?: string;
parentId?: string | number;
children?: MenuForm[];
menuId?: string | number;
menuName?: string;
orderNum?: number;
path?: string;
component?: string;
queryParam?: string;
isFrame?: string;
isCache?: string;
menuType?: 'M' | 'C' | 'F' | string;
visible?: string;
status?: string;
icon?: string | null;
activeMenu?: string;
ext?: string;
remark?: string;
query?: string;
perms?: string;
}
export interface MenuQuery {
keywords?: string;
menuName?: string;
status?: string;
}
export interface MenuTreeOption {
id: string | number;
label: string;
parentId: string | number;
weight: number;
menuType?: 'M' | 'C' | 'F' | string;
visible?: string;
status?: string;
disabled?: boolean;
children?: MenuTreeOption[];
}
export interface MenuVO extends BaseEntity {
parentName?: string;
parentId: string | number;
children?: MenuVO[];
menuId: string | number;
menuName: string;
orderNum: number;
path: string;
@ -77,13 +53,11 @@ export interface MenuForm {
queryParam?: string;
isFrame?: string;
isCache?: string;
menuType?: MenuTypeEnum;
menuType: 'M' | 'C' | 'F' | string;
visible?: string;
status?: string;
icon?: string;
icon?: string | null;
activeMenu?: string;
ext?: string;
remark?: string;
query?: string;
perms?: string;
}

View File

@ -1,9 +1,9 @@
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { MessageBoxVO } from './types';
export function getMessageBox(): AxiosPromise<MessageBoxVO> {
return request({
export function getMessageBox() {
return request<R<MessageBoxVO>>({
url: '/resource/message/box',
method: 'get'
});

View File

@ -1,3 +1,5 @@
import type { BaseEntity } from '@/api/types';
export interface MessageVO extends BaseEntity {
messageId: number | string;
category: string;
@ -6,7 +8,7 @@ export interface MessageVO extends BaseEntity {
title: string;
message: string;
content?: string;
data?: Record<string, any> | null;
data?: Record<string, unknown> | null;
path?: string;
}

View File

@ -1,46 +1,41 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { NoticeForm, NoticeQuery, NoticeVO } from './types';
// 查询公告列表
export function listNotice(query: NoticeQuery): AxiosPromise<PageResult<NoticeVO>> {
return request({
export function listNotice(query: NoticeQuery) {
return request<R<PageResult<NoticeVO>>>({
url: '/system/notice/list',
method: 'get',
params: query
});
}
// 查询公告详细
export function getNotice(noticeId: string | number): AxiosPromise<NoticeVO> {
return request({
url: '/system/notice/' + noticeId,
export function getNotice(noticeId: string | number) {
return request<R<NoticeVO>>({
url: `/system/notice/${noticeId}`,
method: 'get'
});
}
// 新增公告
export function addNotice(data: NoticeForm) {
return request({
return request<R>({
url: '/system/notice',
method: 'post',
data: data
data
});
}
// 修改公告
export function updateNotice(data: NoticeForm) {
return request({
return request<R>({
url: '/system/notice',
method: 'put',
data: data
data
});
}
// 删除公告
export function delNotice(noticeId: string | number | Array<string | number>) {
return request({
url: '/system/notice/' + noticeId,
return request<R>({
url: `/system/notice/${noticeId}`,
method: 'delete'
});
}

View File

@ -1,26 +1,27 @@
export interface NoticeVO extends BaseEntity {
noticeId: number;
noticeTitle: string;
noticeType: string;
noticeContent: string;
status: string;
remark: string;
createByName: string;
import type { BaseEntity, PageQuery } from '@/api/types';
export interface NoticeForm {
noticeId?: number | string;
noticeTitle?: string;
noticeType?: string;
noticeContent?: string;
status?: string;
remark?: string;
createByName?: string;
}
export interface NoticeQuery extends PageQuery {
noticeTitle: string;
createByName: string;
status: string;
noticeType: string;
noticeTitle?: string;
createByName?: string;
status?: string;
noticeType?: string;
}
export interface NoticeForm {
noticeId: number | string | undefined;
export interface NoticeVO extends BaseEntity {
noticeId: number | string;
noticeTitle: string;
noticeType: string;
noticeContent: string;
status: string;
remark: string;
createByName: string;
createByName?: string;
}

View File

@ -1,38 +1,45 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { OssQuery, OssUploadVO, OssVO } from './types';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { OssQuery, OssUploadResult, OssVO } from './types';
// 查询OSS对象存储列表
export function listOss(query: OssQuery): AxiosPromise<PageResult<OssVO>> {
return request({
export function listOss(query: OssQuery) {
return request<R<PageResult<OssVO>>>({
url: '/resource/oss/list',
method: 'get',
params: query
});
}
// 查询OSS对象基于id串
export function listByIds(ossId: string | number): AxiosPromise<OssVO[]> {
return request({
url: '/resource/oss/listByIds/' + ossId,
export function listOssByIds(ossId: string | number) {
return request<R<OssVO[]>>({
url: `/resource/oss/listByIds/${ossId}`,
method: 'get'
});
}
// 上传OSS对象存储
export function uploadOss(data: FormData): AxiosPromise<OssUploadVO> {
return request({
export function listByIds(ossId: string | number) {
return listOssByIds(ossId);
}
export function uploadOss(data: FormData) {
return request<R<OssUploadResult>>({
url: '/resource/oss/upload',
method: 'post',
data
});
}
// 删除OSS对象存储
export function delOss(ossId: string | number | Array<string | number>) {
return request({
url: '/resource/oss/' + ossId,
return request<R>({
url: `/resource/oss/${ossId}`,
method: 'delete'
});
}
export function downloadOss(ossId: string | number) {
return request<Blob>({
url: `/resource/oss/download/${ossId}`,
method: 'get',
responseType: 'blob'
});
}

View File

@ -1,42 +1,27 @@
import type { BaseEntity, PageQuery } from '@/api/types';
export interface OssQuery extends Omit<PageQuery, 'isAsc'> {
fileName?: string;
originalName?: string;
fileSuffix?: string;
createTime?: string;
service?: string;
orderByColumn?: string;
isAsc?: string;
}
export interface OssUploadResult {
url?: string;
fileName?: string;
ossId?: string | number;
}
export interface OssVO extends BaseEntity {
ossId: string | number;
fileName: string;
originalName: string;
fileSuffix: string;
url: string;
createByName: string;
service: string;
}
export interface OssUploadVO {
url: string;
fileName: string;
ossId: string;
}
export interface OssQuery extends PageQuery {
fileName: string;
originalName: string;
fileSuffix: string;
createTime: string;
service: string;
orderByColumn: string;
isAsc: string;
}
export interface OssForm {
file: undefined | string;
}
export interface SysOssExt {
bizType?: string;
fileSize?: number;
contentType?: string;
source?: string;
uploadIp?: string;
remark?: string;
tags?: string[];
refId?: string;
refType?: string;
isTemp?: boolean;
md5?: string;
createByName?: string;
service?: string;
}

View File

@ -1,61 +1,49 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { OssConfigForm, OssConfigQuery, OssConfigVO } from './types';
// 查询对象存储配置列表
export function listOssConfig(query: OssConfigQuery): AxiosPromise<PageResult<OssConfigVO>> {
return request({
export function listOssConfig(query: OssConfigQuery) {
return request<R<PageResult<OssConfigVO>>>({
url: '/resource/oss/config/list',
method: 'get',
params: query
});
}
// 查询对象存储配置详细
export function getOssConfig(ossConfigId: string | number): AxiosPromise<OssConfigVO> {
return request({
url: '/resource/oss/config/' + ossConfigId,
export function getOssConfig(ossConfigId: string | number) {
return request<R<OssConfigVO>>({
url: `/resource/oss/config/${ossConfigId}`,
method: 'get'
});
}
// 新增对象存储配置
export function addOssConfig(data: OssConfigForm) {
return request({
return request<R>({
url: '/resource/oss/config',
method: 'post',
data: data
data
});
}
// 修改对象存储配置
export function updateOssConfig(data: OssConfigForm) {
return request({
return request<R>({
url: '/resource/oss/config',
method: 'put',
data: data
data
});
}
// 删除对象存储配置
export function delOssConfig(ossConfigId: string | number | Array<string | number>) {
return request({
url: '/resource/oss/config/' + ossConfigId,
return request<R>({
url: `/resource/oss/config/${ossConfigId}`,
method: 'delete'
});
}
// 对象存储状态修改
export function changeOssConfigStatus(ossConfigId: string | number, status: string, configKey: string) {
const data = {
ossConfigId,
status,
configKey
};
return request({
return request<R>({
url: '/resource/oss/config/changeStatus',
method: 'put',
data: data
data: { ossConfigId, status, configKey }
});
}

View File

@ -1,38 +1,39 @@
import type { BaseEntity, PageQuery } from '@/api/types';
export interface OssConfigForm {
ossConfigId?: string | number;
configKey?: string;
accessKey?: string;
secretKey?: string;
bucketName?: string;
prefix?: string;
endpoint?: string;
domainUrl?: string;
isHttps?: string;
accessPolicy?: string;
region?: string;
status?: string;
remark?: string;
}
export interface OssConfigQuery extends PageQuery {
configKey?: string;
bucketName?: string;
status?: string;
}
export interface OssConfigVO extends BaseEntity {
ossConfigId: number | string;
configKey: string;
accessKey: string;
secretKey: string;
bucketName: string;
prefix: string;
prefix?: string;
endpoint: string;
domainUrl: string;
domainUrl?: string;
isHttps: string;
region: string;
region?: string;
status: string;
ext1: string;
remark: string;
ext1?: string;
accessPolicy: string;
}
export interface OssConfigQuery extends PageQuery {
configKey: string;
bucketName: string;
status: string;
}
export interface OssConfigForm {
ossConfigId: string | number | undefined;
configKey: string;
accessKey: string;
secretKey: string;
bucketName: string;
prefix: string;
endpoint: string;
domainUrl: string;
isHttps: string;
accessPolicy: string;
region: string;
status: string;
remark: string;
}

View File

@ -1,70 +1,64 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { DeptTreeVO } from '../dept/types';
import type { DeptTreeVO } from '@/api/system/dept/types';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { PostForm, PostQuery, PostVO } from './types';
// 查询岗位列表
export function listPost(query: PostQuery): AxiosPromise<PageResult<PostVO>> {
return request({
export function listPost(query: PostQuery) {
return request<R<PageResult<PostVO>>>({
url: '/system/post/list',
method: 'get',
params: query
});
}
// 查询岗位详细
export function getPost(postId: string | number): AxiosPromise<PostVO> {
return request({
url: '/system/post/' + postId,
export function getPost(postId: string | number) {
return request<R<PostVO>>({
url: `/system/post/${postId}`,
method: 'get'
});
}
// 获取岗位选择框列表
export function optionselect(deptId?: number | string, postIds?: (number | string)[]): AxiosPromise<PostVO[]> {
return request({
export function optionSelect(deptId?: number | string, postIds?: Array<number | string>) {
return request<R<PostVO[]>>({
url: '/system/post/optionselect',
method: 'get',
params: {
postIds: postIds,
deptId: deptId
postIds,
deptId
}
});
}
// 新增岗位
export function addPost(data: PostForm) {
return request({
return request<R>({
url: '/system/post',
method: 'post',
data: data
data
});
}
// 修改岗位
export function updatePost(data: PostForm) {
return request({
return request<R>({
url: '/system/post',
method: 'put',
data: data
data
});
}
// 删除岗位
export function delPost(postId: string | number | (string | number)[]) {
return request({
url: '/system/post/' + postId,
export function delPost(postId: string | number | Array<string | number>) {
return request<R>({
url: `/system/post/${postId}`,
method: 'delete'
});
}
/**
*
*/
export const deptTreeSelect = (): AxiosPromise<DeptTreeVO[]> => {
return request({
export function postDeptTreeSelect() {
return request<R<DeptTreeVO[]>>({
url: '/system/post/deptTree',
method: 'get'
});
};
}
export function deptTreeSelect() {
return postDeptTreeSelect();
}

View File

@ -1,3 +1,25 @@
import type { BaseEntity, PageQuery } from '@/api/types';
export interface PostForm {
postId?: number | string;
deptId?: number | string;
postCode?: string;
postName?: string;
postCategory?: string;
postSort?: number;
status?: string;
remark?: string;
}
export interface PostQuery extends PageQuery {
deptId?: number | string;
belongDeptId?: number | string;
postCode?: string;
postName?: string;
postCategory?: string;
status?: string;
}
export interface PostVO extends BaseEntity {
postId: number | string;
deptId: number | string;
@ -7,25 +29,4 @@ export interface PostVO extends BaseEntity {
deptName: string;
postSort: number;
status: string;
remark: string;
}
export interface PostForm {
postId: number | string | undefined;
deptId: number | string | undefined;
postCode: string;
postName: string;
postCategory: string;
postSort: number;
status: string;
remark: string;
}
export interface PostQuery extends PageQuery {
deptId: number | string;
belongDeptId: number | string;
postCode: string;
postName: string;
postCategory: string;
status: string;
}

View File

@ -1,160 +1,115 @@
import type { UserQuery, UserVO } from '@/api/system/user/types';
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { RoleDeptTree, RoleQuery, RoleVO } from './types';
import type { UserQuery, UserVO } from '@/api/system/user/types';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { RoleDeptTree, RoleForm, RoleQuery, RoleVO } from './types';
export const listRole = (query: RoleQuery): AxiosPromise<PageResult<RoleVO>> => {
return request({
export function listRole(query: RoleQuery) {
return request<R<PageResult<RoleVO>>>({
url: '/system/role/list',
method: 'get',
params: query
});
};
}
/**
* roleIds查询角色
* @param roleIds
*/
export const optionSelect = (roleIds: (number | string)[]): AxiosPromise<RoleVO[]> => {
return request({
url: '/system/role/optionselect?roleIds=' + roleIds,
export function optionSelect(roleIds: Array<number | string>) {
return request<R<RoleVO[]>>({
url: `/system/role/optionselect?roleIds=${roleIds}`,
method: 'get'
});
};
}
/**
*
*/
export const getRole = (roleId: string | number): AxiosPromise<RoleVO> => {
return request({
url: '/system/role/' + roleId,
export function getRole(roleId: string | number) {
return request<R<RoleVO>>({
url: `/system/role/${roleId}`,
method: 'get'
});
};
}
/**
*
*/
export const addRole = (data: any) => {
return request({
export function addRole(data: RoleForm) {
return request<R>({
url: '/system/role',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
*/
export const updateRole = (data: any) => {
return request({
export function updateRole(data: RoleForm) {
return request<R>({
url: '/system/role',
method: 'put',
data: data
data
});
};
}
/**
* +
*/
export const updateRolePermission = (data: any) => {
return request({
export function updateRolePermission(data: RoleForm) {
return request<R>({
url: '/system/role/permission',
method: 'put',
data: data
data
});
};
}
/**
*
*/
export const changeRoleStatus = (roleId: string | number, status: string) => {
const data = {
roleId,
status
};
return request({
export function changeRoleStatus(roleId: string | number, status: string) {
return request<R>({
url: '/system/role/changeStatus',
method: 'put',
data: data
data: {
roleId,
status
}
});
};
}
/**
*
*/
export const delRole = (roleId: Array<string | number> | string | number) => {
return request({
url: '/system/role/' + roleId,
export function delRole(roleId: Array<string | number> | string | number) {
return request<R>({
url: `/system/role/${roleId}`,
method: 'delete'
});
};
}
/**
*
*/
export const allocatedUserList = (query: UserQuery): AxiosPromise<PageResult<UserVO>> => {
return request({
export function allocatedUserList(query: UserQuery) {
return request<R<PageResult<UserVO>>>({
url: '/system/role/authUser/allocatedList',
method: 'get',
params: query
});
};
}
/**
*
*/
export const unallocatedUserList = (query: UserQuery): AxiosPromise<PageResult<UserVO>> => {
return request({
export function unallocatedUserList(query: UserQuery) {
return request<R<PageResult<UserVO>>>({
url: '/system/role/authUser/unallocatedList',
method: 'get',
params: query
});
};
}
/**
*
*/
export const authUserCancel = (data: any) => {
return request({
export function authUserCancel(data: { roleId: string | number; userId: string | number }) {
return request<R>({
url: '/system/role/authUser/cancel',
method: 'put',
data: data
data
});
};
}
/**
*
*/
export const authUserCancelAll = (data: any) => {
return request({
export function authUserCancelAll(data: { roleId: string | number; userIds: string }) {
return request<R>({
url: '/system/role/authUser/cancelAll',
method: 'put',
params: data
});
};
}
/**
*
*/
export const authUserSelectAll = (data: any) => {
return request({
export function authUserSelectAll(data: { roleId: string | number; userIds: string }) {
return request<R>({
url: '/system/role/authUser/selectAll',
method: 'put',
params: data
});
};
// 根据角色ID查询部门树结构
export const deptTreeSelect = (roleId: string | number): AxiosPromise<RoleDeptTree> => {
return request({
url: '/system/role/deptTree/' + roleId,
}
export function deptTreeSelect(roleId: string | number) {
return request<R<RoleDeptTree>>({
url: `/system/role/deptTree/${roleId}`,
method: 'get'
});
};
export default {
optionSelect,
listRole
};
}

View File

@ -1,17 +1,35 @@
/**
*
*/
export interface DeptTreeOption {
id: string;
label: string;
parentId: string;
weight: number;
children?: DeptTreeOption[];
}
import type { DeptTreeVO } from '@/api/system/dept/types';
import type { MenuTreeOption } from '@/api/system/menu/types';
import type { BaseEntity, PageQuery } from '@/api/types';
export interface RoleDeptTree {
checkedKeys: string[];
depts: DeptTreeOption[];
checkedKeys: Array<string | number>;
depts: DeptTreeVO[];
}
export interface RoleForm {
roleId?: string | number;
roleName?: string;
roleKey?: string;
roleSort?: number;
status?: string;
menuCheckStrictly?: boolean;
deptCheckStrictly?: boolean;
remark?: string;
dataScope?: string;
menuIds?: Array<string | number>;
deptIds?: Array<string | number>;
}
export interface RoleMenuTree {
menus: MenuTreeOption[];
checkedKeys: Array<string | number>;
}
export interface RoleQuery extends PageQuery {
roleName?: string;
roleKey?: string;
status?: string;
}
export interface RoleVO extends BaseEntity {
@ -20,33 +38,11 @@ export interface RoleVO extends BaseEntity {
roleKey: string;
roleSort: number;
dataScope: string;
menuCheckStrictly: boolean;
deptCheckStrictly: boolean;
menuCheckStrictly?: boolean;
deptCheckStrictly?: boolean;
status: string;
delFlag: string;
remark?: any;
flag: boolean;
admin: boolean;
menuIds?: Array<string | number>;
deptIds?: Array<string | number>;
admin: boolean;
}
export interface RoleQuery extends PageQuery {
roleName: string;
roleKey: string;
status: string;
}
export interface RoleForm {
roleName: string;
roleKey: string;
roleSort: number;
status: string;
menuCheckStrictly: boolean;
deptCheckStrictly: boolean;
remark: string;
dataScope?: string;
roleId: string | undefined;
menuIds: Array<string | number>;
deptIds: Array<string | number>;
}

View File

@ -1,23 +1,23 @@
import request from '@/utils/request';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { SocialAuthVO } from './types';
// 获取跳转URL
export function authRouterUrl(source: string) {
return request({
url: '/auth/binding/' + source,
return request<R<string>>({
url: `/auth/binding/${source}`,
method: 'get'
});
}
// 解绑账号
export function authUnlock(authId: string) {
return request({
url: '/auth/unlock/' + authId,
return request<R>({
url: `/auth/unlock/${authId}`,
method: 'delete'
});
}
//获取授权列表
export function getAuthList() {
return request({
return request<R<SocialAuthVO[]>>({
url: '/system/social/list',
method: 'get'
});

View File

@ -0,0 +1,7 @@
export interface SocialAuthVO {
id: string;
source?: string;
avatar?: string;
userName?: string;
createTime?: string;
}

View File

@ -1,229 +1,154 @@
import type { DeptTreeVO } from '@/api/system/dept/types';
import type { RoleVO } from '@/api/system/role/types';
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import { parseStrEmpty } from '@/utils/ruoyi';
import type { DeptTreeVO } from './../dept/types';
import type { UserForm, UserInfoVO, UserProfileForm, UserQuery, UserVO } from './types';
/**
*
* @param query
*/
export const listUser = (query: UserQuery): AxiosPromise<PageResult<UserVO>> => {
return request({
export function listUser(query: UserQuery) {
return request<R<PageResult<UserVO>>>({
url: '/system/user/list',
method: 'get',
params: query
});
};
}
/**
* ids查询用户
* @param userIds
*/
export const optionSelect = (userIds: (number | string)[]): AxiosPromise<UserVO[]> => {
return request({
url: '/system/user/optionselect?userIds=' + userIds,
export function optionSelect(userIds: Array<number | string>) {
return request<R<UserVO[]>>({
url: `/system/user/optionselect?userIds=${userIds}`,
method: 'get'
});
};
}
/**
*
* @param userId
*/
export const getUser = (userId?: string | number): AxiosPromise<UserInfoVO> => {
return request({
url: '/system/user/' + parseStrEmpty(userId),
export function getUser(userId?: string | number) {
return request<R<UserInfoVO>>({
url: `/system/user/${parseStrEmpty(userId)}`,
method: 'get'
});
};
}
/**
*
*/
export const addUser = (data: UserForm) => {
return request({
export function addUser(data: UserForm) {
return request<R>({
url: '/system/user',
method: 'post',
data: data
data
});
};
}
/**
*
*/
export const updateUser = (data: UserForm) => {
return request({
export function updateUser(data: UserForm) {
return request<R>({
url: '/system/user',
method: 'put',
data: data
data
});
};
}
/**
*
* @param userId ID
*/
export const delUser = (userId: Array<string | number> | string | number) => {
return request({
url: '/system/user/' + userId,
export function delUser(userId: Array<string | number> | string | number) {
return request<R>({
url: `/system/user/${userId}`,
method: 'delete'
});
};
}
/**
*
* @param userId ID
* @param password
*/
export const resetUserPwd = (userId: string | number, password: string) => {
const data = {
userId,
password
};
return request({
export function resetUserPwd(userId: string | number, password: string) {
return request<R>({
url: '/system/user/resetPwd',
method: 'put',
headers: {
isEncrypt: true,
repeatSubmit: false
},
data: data
data: {
userId,
password
}
});
};
}
/**
*
* @param userId ID
* @param status
*/
export const changeUserStatus = (userId: number | string, status: string) => {
const data = {
userId,
status
};
return request({
export function changeUserStatus(userId: number | string, status: string) {
return request<R>({
url: '/system/user/changeStatus',
method: 'put',
data: data
data: {
userId,
status
}
});
};
}
/**
*
* @param userId ID
*/
export const unlockUser = (userId: number | string) => {
return request({
url: '/system/user/unlock/' + userId,
export function unlockUser(userId: number | string) {
return request<R>({
url: `/system/user/unlock/${userId}`,
method: 'get'
});
};
}
/**
*
*/
export const getUserProfile = (): AxiosPromise<UserInfoVO> => {
return request({
export function getUserProfile() {
return request<R<UserInfoVO>>({
url: '/system/user/profile',
method: 'get'
});
};
}
/**
*
* @param data
*/
export const updateUserProfile = (data: UserProfileForm) => {
return request({
export function updateUserProfile(data: UserProfileForm) {
return request<R>({
url: '/system/user/profile',
method: 'put',
data: data
data
});
};
}
/**
*
* @param oldPassword
* @param newPassword
*/
export const updateUserPwd = (oldPassword: string, newPassword: string) => {
const data = {
oldPassword,
newPassword
};
return request({
export function updateUserPwd(oldPassword: string, newPassword: string) {
return request<R>({
url: '/system/user/profile/updatePwd',
method: 'put',
headers: {
isEncrypt: true,
repeatSubmit: false
},
data: data
data: {
oldPassword,
newPassword
}
});
};
}
/**
*
* @param userId ID
*/
export const getAuthRole = (userId: string | number): AxiosPromise<{ user: UserVO; roles: RoleVO[] }> => {
return request({
url: '/system/user/authRole/' + userId,
export function importUser(data: FormData, updateSupport: boolean) {
return request<R>({
url: '/system/user/importData',
method: 'post',
params: {
updateSupport: updateSupport ? 1 : 0
},
data
});
}
export function getAuthRole(userId: string | number) {
return request<R<{ user: UserVO; roles: RoleVO[] }>>({
url: `/system/user/authRole/${userId}`,
method: 'get'
});
};
}
/**
*
* @param data ID
*/
export const updateAuthRole = (data: { userId: string; roleIds: string }) => {
return request({
export function updateAuthRole(data: { userId: string; roleIds: string }) {
return request<R>({
url: '/system/user/authRole',
method: 'put',
params: data
});
};
}
/**
*
* @param deptId
*/
export const listUserByDeptId = (deptId: string | number): AxiosPromise<UserVO[]> => {
return request({
url: '/system/user/list/dept/' + deptId,
method: 'get'
});
};
/**
*
*/
export const deptTreeSelect = (): AxiosPromise<DeptTreeVO[]> => {
return request({
export function deptTreeSelect() {
return request<R<DeptTreeVO[]>>({
url: '/system/user/deptTree',
method: 'get'
});
};
}
export default {
listUser,
getUser,
optionSelect,
addUser,
updateUser,
delUser,
resetUserPwd,
changeUserStatus,
unlockUser,
getUserProfile,
updateUserProfile,
updateUserPwd,
getAuthRole,
updateAuthRole,
deptTreeSelect,
listUserByDeptId
};
export function listUserByDeptId(deptId: string | number) {
return request<R<UserVO[]>>({
url: `/system/user/list/dept/${deptId}`,
method: 'get'
});
}

View File

@ -1,81 +1,35 @@
import type { DeptVO } from '@/api/system/dept/types';
import type { PostVO } from '@/api/system/post/types';
import type { RoleVO } from '@/api/system/role/types';
import type { BaseEntity, PageQuery } from '@/api/types';
/**
*
*/
export interface UserInfo {
user: UserVO;
user: {
userId: string | number;
userName: string;
nickName?: string;
avatar?: string;
deptName?: string;
};
roles: string[];
permissions: string[];
}
/**
*
*/
export interface UserQuery extends PageQuery {
export interface UserForm {
userId?: string | number;
deptId?: number | string;
userName?: string;
nickName?: string;
phoneNumber?: string;
status?: string;
deptId?: string | number;
roleId?: string | number;
userIds?: string | number | (string | number)[] | undefined;
}
/**
*
*/
export interface UserVO extends BaseEntity {
userId: string | number;
tenantId: string;
deptId: number;
userName: string;
nickName: string;
userType: string;
email: string;
phoneNumber: string;
gender: string;
avatar?: string | number;
avatarUrl?: string;
status: string;
delFlag: string;
loginIp: string;
loginDate: string;
remark: string;
deptName: string;
/** 详情接口可能返回嵌套部门 */
dept?: { deptName?: string };
roles: RoleVO[];
roleIds: any;
postIds: any;
roleId: any;
admin: boolean;
}
/**
*
*/
export interface UserForm {
id?: string;
userId?: string;
deptId?: number;
userName: string;
nickName?: string;
password: string;
password?: string;
phoneNumber?: string;
email?: string;
gender?: string;
status: string;
status?: string;
remark?: string;
avatar?: string | number;
postIds: string[];
roleIds: string[];
postIds?: Array<string | number>;
roleIds?: Array<string | number>;
}
/**
*
*/
export interface UserProfileForm {
nickName?: string;
phoneNumber?: string;
@ -85,17 +39,41 @@ export interface UserProfileForm {
}
export interface UserInfoVO {
user: UserVO;
user?: UserVO;
roles: RoleVO[];
roleIds: string[];
roleIds: Array<string | number>;
posts: PostVO[];
postIds: string[];
roleGroup: string;
postGroup: string;
postIds: Array<string | number>;
roleGroup?: string;
postGroup?: string;
}
export interface ResetPwdForm {
oldPassword: string;
newPassword: string;
confirmPassword: string;
export interface UserQuery extends PageQuery {
userName?: string;
nickName?: string;
phoneNumber?: string;
status?: string;
deptId?: string | number;
roleId?: string | number;
userIds?: string | number;
}
export interface UserVO extends BaseEntity {
userId?: string | number;
tenantId?: string;
deptId?: number | string;
userName?: string;
nickName?: string;
userType?: string;
email?: string;
phoneNumber?: string;
gender?: string;
avatar?: string;
status?: string;
deptName?: string;
roles?: RoleVO[];
admin?: boolean;
loginIp?: string;
loginDate?: string;
dept?: DeptVO;
}

View File

@ -1,81 +1,78 @@
import type { PageResult } from '@/api/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { DbTableForm, DbTableQuery, DbTableVO, GenTableDetailPayload, TableQuery, TableVO } from './types';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { DbTableQuery, DbTableVO, GenTableDetailPayload, TableQuery, TableVO } from './types';
export type { GenTableDetailPayload } from './types';
// 查询生成表数据
export const listTable = (query: TableQuery): AxiosPromise<PageResult<TableVO>> => {
return request({
export function listTable(query: TableQuery) {
return request<R<PageResult<TableVO>>>({
url: '/tool/gen/list',
method: 'get',
params: query
});
};
// 查询db数据库列表
export const listDbTable = (query: DbTableQuery): AxiosPromise<PageResult<DbTableVO>> => {
return request({
}
export function listDbTable(query: DbTableQuery) {
return request<R<PageResult<DbTableVO>>>({
url: '/tool/gen/db/list',
method: 'get',
params: query
});
};
}
// 查询表详细信息
export const getGenTable = (tableId: string | number): AxiosPromise<GenTableDetailPayload> => {
return request({
url: '/tool/gen/' + tableId,
export function getGenTable(tableId: string | number) {
return request<R<GenTableDetailPayload>>({
url: `/tool/gen/${tableId}`,
method: 'get'
});
};
}
// 修改代码生成信息
export const updateGenTable = (data: DbTableForm): AxiosPromise<unknown> => {
return request({
export function updateGenTable(data: DbTableVO & { columns?: unknown[]; params?: Record<string, unknown> }) {
return request<R>({
url: '/tool/gen',
method: 'put',
data: data
data
});
};
}
// 导入表
export const importTable = (data: { tables: string; dataName: string }): AxiosPromise<unknown> => {
return request({
export function importTable(data: { tables: string; dataName: string }) {
return request<R>({
url: '/tool/gen/importTable',
method: 'post',
params: data
});
};
}
// 预览生成代码
export const previewTable = (tableId: string | number) => {
return request({
url: '/tool/gen/preview/' + tableId,
export function previewTable(tableId: string | number) {
return request<R<Record<string, string>>>({
url: `/tool/gen/preview/${tableId}`,
method: 'get'
});
};
}
// 删除表数据
export const delTable = (tableId: string | number | Array<string | number>) => {
return request({
url: '/tool/gen/' + tableId,
export function delTable(tableId: string | number | Array<string | number>) {
return request<R>({
url: `/tool/gen/${tableId}`,
method: 'delete'
});
};
}
// 同步数据库
export const synchDb = (tableId: string | number) => {
return request({
url: '/tool/gen/synchDb/' + tableId,
export function synchDb(tableId: string | number) {
return request<R>({
url: `/tool/gen/synchDb/${tableId}`,
method: 'get'
});
};
}
// 获取数据源名称
export const getDataNames = () => {
return request({
export function getDataNames() {
return request<R<string[]>>({
url: '/tool/gen/getDataNames',
method: 'get'
});
};
}
export function batchGenCode(tableIdStr: string) {
return request<Blob>({
url: `/tool/gen/batchGenCode?tableIdStr=${tableIdStr}`,
method: 'get',
responseType: 'blob'
});
}

View File

@ -1,215 +1,91 @@
export interface TableVO extends BaseEntity {
createDept: number | string;
tableId: string | number;
dataName: string;
tableName: string;
tableComment: string;
className: string;
tplCategory: string;
frontendType: string;
packageName: string;
moduleName: string;
businessName: string;
functionName: string;
functionAuthor: string;
pkColumn?: any;
columns?: any;
options?: any;
remark?: any;
treeCode?: any;
treeParentCode?: any;
treeName?: any;
menuIds?: any;
parentMenuId?: any;
parentMenuName?: any;
enableExport?: boolean;
enableStatus?: boolean;
statusField?: string;
enableUnique?: boolean;
uniqueFields?: string[];
enableSort?: boolean;
sortField?: string;
treeRootValue?: string;
treeAncestorsField?: string;
treeOrderField?: string;
tree: boolean;
crud: boolean;
}
export interface TableQuery extends PageQuery {
tableName: string;
tableComment: string;
dataName: string;
}
import type { BaseEntity, PageQuery } from '@/api/types';
export interface DbColumnVO extends BaseEntity {
createDept?: any;
columnId?: any;
tableId?: any;
columnName?: any;
columnComment?: any;
columnType?: any;
javaType?: any;
javaField?: any;
isPk?: any;
isIncrement?: any;
isRequired?: any;
isInsert?: any;
isEdit?: any;
isList?: any;
isQuery?: any;
queryType?: any;
htmlType?: any;
dictType?: any;
sort?: any;
increment: boolean;
capJavaField?: any;
usableColumn: boolean;
superColumn: boolean;
list: boolean;
pk: boolean;
insert: boolean;
edit: boolean;
query: boolean;
required: boolean;
}
export interface DbTableVO {
createDept?: any;
tableId?: any;
tableName: string;
tableComment: string;
className?: any;
tplCategory?: any;
frontendType?: string;
packageName?: any;
moduleName?: any;
businessName?: any;
functionName?: any;
functionAuthor?: any;
pkColumn?: any;
columns: DbColumnVO[];
options?: any;
remark?: any;
treeCode?: any;
treeParentCode?: any;
treeName?: any;
menuIds?: any;
parentMenuId?: any;
parentMenuName?: any;
enableExport?: boolean;
enableStatus?: boolean;
statusField?: string;
enableUnique?: boolean;
uniqueFields?: string[];
enableSort?: boolean;
sortField?: string;
treeRootValue?: string;
treeAncestorsField?: string;
treeOrderField?: string;
tree: boolean;
crud: boolean;
columnId?: string | number;
tableId?: string | number;
columnName?: string;
columnComment?: string;
columnType?: string;
javaType?: string;
javaField?: string;
isPk?: string;
isIncrement?: string;
isRequired?: string;
isInsert?: string;
isEdit?: string;
isList?: string;
isQuery?: string;
queryType?: string;
htmlType?: string;
dictType?: string;
sort?: number;
increment?: boolean;
capJavaField?: string;
usableColumn?: boolean;
superColumn?: boolean;
list?: boolean;
pk?: boolean;
insert?: boolean;
edit?: boolean;
query?: boolean;
required?: boolean;
}
export interface DbTableQuery extends PageQuery {
dataName: string;
tableName: string;
tableComment: string;
dataName?: string;
tableName?: string;
tableComment?: string;
}
export interface DbTableVO extends Omit<TableVO, 'tableId'> {
tableId?: string | number;
columns?: DbColumnVO[];
options?: string;
remark?: string;
treeCode?: string;
treeParentCode?: string;
treeName?: string;
menuIds?: Array<string | number>;
parentMenuId?: string | number;
parentMenuName?: string;
enableExport?: boolean;
enableStatus?: boolean;
statusField?: string;
enableUnique?: boolean;
uniqueFields?: string[];
enableSort?: boolean;
sortField?: string;
frontendType?: string;
treeRootValue?: string;
treeAncestorsField?: string;
treeOrderField?: string;
}
/**
* data
* - info GenTable
* - rows GenTableColumn[]
*/
export interface GenTableDetailPayload {
info: DbTableVO;
rows: DbColumnVO[];
}
export interface DbColumnForm extends BaseEntity {
createDept: number;
columnId: string;
tableId: string;
columnName: string;
columnComment: string;
columnType: string;
javaType: string;
javaField: string;
isPk: string;
isIncrement: string;
isRequired: string;
isInsert?: any;
isEdit: string;
isList: string;
isQuery?: any;
queryType: string;
htmlType: string;
dictType: string;
sort: number;
increment: boolean;
capJavaField: string;
usableColumn: boolean;
superColumn: boolean;
list: boolean;
pk: boolean;
insert: boolean;
edit: boolean;
query: boolean;
required: boolean;
export interface TableQuery extends PageQuery {
tableName?: string;
tableComment?: string;
dataName?: string;
}
export interface DbParamForm {
treeCode?: any;
treeName?: any;
treeParentCode?: any;
parentMenuId: string;
enableExport?: boolean;
enableStatus?: boolean;
statusField?: string;
enableUnique?: boolean;
uniqueFields?: string[];
enableSort?: boolean;
sortField?: string;
treeRootValue?: string;
treeAncestorsField?: string;
treeOrderField?: string;
}
export interface DbTableForm extends BaseEntity {
createDept?: any;
tableId: string | string;
export interface TableVO extends BaseEntity {
createDept?: number | string;
tableId: string | number;
dataName?: string;
tableName: string;
tableComment: string;
className: string;
tplCategory: string;
frontendType: string;
packageName: string;
moduleName: string;
businessName: string;
functionName: string;
functionAuthor: string;
pkColumn?: any;
columns: DbColumnForm[];
options: string;
remark?: any;
treeCode?: any;
treeParentCode?: any;
treeName?: any;
menuIds?: any;
parentMenuId: string;
parentMenuName?: any;
enableExport?: boolean;
enableStatus?: boolean;
statusField?: string;
enableUnique?: boolean;
uniqueFields?: string[];
enableSort?: boolean;
sortField?: string;
treeRootValue?: string;
treeAncestorsField?: string;
treeOrderField?: string;
tree: boolean;
crud: boolean;
params: DbParamForm;
tableComment?: string;
className?: string;
tplCategory?: string;
frontendType?: string;
packageName?: string;
moduleName?: string;
businessName?: string;
functionName?: string;
functionAuthor?: string;
tree?: boolean;
crud?: boolean;
}

View File

@ -1,51 +1,88 @@
/**
*
*/
export type RegisterForm = {
username: string;
password: string;
confirmPassword?: string;
code?: string;
uuid?: string;
userType?: string;
};
import type { MenuDataItem } from '@ant-design/pro-components';
/**
*
*/
export interface LoginData {
export interface BaseEntity {
createBy?: string;
createTime?: string;
updateBy?: string;
updateTime?: string;
remark?: string;
}
export interface LoginParams {
username?: string;
password?: string;
code?: string;
uuid?: string;
rememberMe?: boolean;
socialCode?: string;
socialState?: string;
source?: string;
code?: string;
uuid?: string;
clientId: string;
grantType: string;
clientId?: string;
grantType?: string;
}
/**
*
*/
export interface LoginResult {
access_token: string;
}
/**
*
*/
export interface PageQuery {
pageNum?: number;
pageSize?: number;
orderByColumn?: string;
isAsc?: 'ascending' | 'descending';
params?: Record<string, unknown>;
}
export interface PageResult<T = unknown> {
total: number;
rows: T[];
}
export interface R<T = unknown> {
code: number;
msg: string;
data: T;
}
export interface RegisterParams {
username?: string;
password?: string;
confirmPassword?: string;
code?: string;
uuid?: string;
userType?: string;
clientId?: string;
grantType?: string;
}
export interface VerifyCodeResult {
captchaEnabled: boolean;
uuid?: string;
img?: string;
}
/**
*
*/
export interface PageResult<T = any> {
total: number;
rows: T[];
export interface RouteMeta {
title?: string;
icon?: string | null;
noCache?: boolean;
link?: string;
hidden?: boolean;
activeMenu?: string;
}
export interface BackendRoute {
name?: string;
path: string;
component?: string;
redirect?: string;
query?: string;
hidden?: boolean;
alwaysShow?: boolean;
meta?: RouteMeta;
children?: BackendRoute[];
}
export type RuntimeMenuItem = Omit<MenuDataItem, 'routes'> & {
backendRoute?: BackendRoute;
routes?: RuntimeMenuItem[];
};

View File

@ -1,76 +1,49 @@
import type { CategoryForm, CategoryQuery, CategoryTreeVO, CategoryVO } from '@/api/workflow/category/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { R } from '@/api/types';
import request from '@/api/request';
import type { CategoryForm, CategoryQuery, CategoryTreeVO, CategoryVO } from './types';
/**
*
* @param query
* @returns {*}
*/
export const listCategory = (query?: CategoryQuery): AxiosPromise<CategoryVO[]> => {
return request({
export function listCategory(query?: CategoryQuery) {
return request<R<CategoryVO[]>>({
url: '/workflow/category/list',
method: 'get',
params: query
});
};
}
/**
*
* @param categoryId
*/
export const getCategory = (categoryId: string | number): AxiosPromise<CategoryVO> => {
return request({
url: '/workflow/category/' + categoryId,
export function getCategory(categoryId: string | number) {
return request<R<CategoryVO>>({
url: `/workflow/category/${categoryId}`,
method: 'get'
});
};
}
/**
*
* @param data
*/
export const addCategory = (data: CategoryForm) => {
return request({
export function addCategory(data: CategoryForm) {
return request<R>({
url: '/workflow/category',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
*/
export const updateCategory = (data: CategoryForm) => {
return request({
export function updateCategory(data: CategoryForm) {
return request<R>({
url: '/workflow/category',
method: 'put',
data: data
data
});
};
}
/**
*
* @param categoryId
*/
export const delCategory = (categoryId: string | number | Array<string | number>) => {
return request({
url: '/workflow/category/' + categoryId,
export function delCategory(categoryId: string | number | Array<string | number>) {
return request<R>({
url: `/workflow/category/${categoryId}`,
method: 'delete'
});
};
}
/**
*
* @param query id
* @returns
*/
export const categoryTree = (query?: CategoryForm): AxiosPromise<CategoryTreeVO[]> => {
return request({
url: `/workflow/category/categoryTree`,
export function categoryTree(query?: CategoryForm) {
return request<R<CategoryTreeVO[]>>({
url: '/workflow/category/categoryTree',
method: 'get',
params: query
});
};
}

View File

@ -1,67 +1,29 @@
import type { BaseEntity } from '@/api/types';
export interface CategoryForm extends BaseEntity {
categoryId?: string | number;
categoryName?: string;
parentId?: string | number;
orderNum?: number;
}
export interface CategoryQuery {
categoryName?: string;
}
export interface CategoryTreeVO {
id: number | string;
label: string;
parentId: number | string;
weight: number;
children: CategoryTreeVO[];
children?: CategoryTreeVO[];
}
export interface CategoryVO {
/**
* ID
*/
categoryId: string | number;
/**
* id
*/
parentId: string | number;
/**
*
*/
categoryName: string;
/**
*
*/
orderNum: number;
/**
*
*/
createTime: string;
/**
*
*/
children: CategoryVO[];
}
export interface CategoryForm extends BaseEntity {
/**
* ID
*/
categoryId?: string | number;
/**
*
*/
categoryName?: string;
/**
* id
*/
parentId?: string | number;
/**
*
*/
orderNum?: number;
}
export interface CategoryQuery {
/**
*
*/
categoryName?: string;
categoryName: string;
orderNum: number;
createTime?: string;
children?: CategoryVO[];
}

View File

@ -1,176 +1,137 @@
import type { PageResult } from '@/api/types';
import type {
definitionXmlVO,
FlowDefinitionForm,
FlowDefinitionQuery,
FlowDefinitionVo
} from '@/api/workflow/definition/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { definitionXmlVO, FlowDefinitionForm, FlowDefinitionQuery, FlowDefinitionVO } from './types';
/**
*
* @param query id
* @returns
*/
export const listDefinition = (query: FlowDefinitionQuery): AxiosPromise<PageResult<FlowDefinitionVo>> => {
return request({
url: `/workflow/definition/list`,
export function listDefinition(query: FlowDefinitionQuery) {
return request<R<PageResult<FlowDefinitionVO>>>({
url: '/workflow/definition/list',
method: 'get',
params: query
});
};
/**
*
* @param query id
* @returns
*/
export const unPublishList = (query: FlowDefinitionQuery): AxiosPromise<PageResult<FlowDefinitionVo>> => {
return request({
url: `/workflow/definition/unPublishList`,
method: 'get',
params: query
});
};
/**
* id获取xml
* @param definitionId id
* @returns
*/
export const definitionXml = (definitionId: string): AxiosPromise<definitionXmlVO> => {
return request({
url: `/workflow/definition/definitionXml/${definitionId}`,
method: 'get'
});
};
/**
*
* @param id id
* @returns
*/
export const deleteDefinition = (id: string | string[]) => {
return request({
url: `/workflow/definition/${id}`,
method: 'delete'
});
};
/**
* /
* @param definitionId id
* @param activityStatus
* @returns
*/
export const active = (definitionId: string, activityStatus: boolean) => {
return request({
url: `/workflow/definition/active/${definitionId}`,
method: 'put',
params: {
active: activityStatus
}
});
};
/**
* zip或xml部署流程定义
* @returns
*/
export function importDef(data: any) {
return request({
url: '/workflow/definition/importDef',
method: 'post',
data: data,
headers: {
repeatSubmit: false
}
});
}
/**
*
* @param id id
* @returns
*/
export const publish = (id: string) => {
return request({
export function unPublishList(query: FlowDefinitionQuery) {
return request<R<PageResult<FlowDefinitionVO>>>({
url: '/workflow/definition/unPublishList',
method: 'get',
params: query
});
}
export function definitionXml(definitionId: string) {
return request<R<definitionXmlVO>>({
url: `/workflow/definition/definitionXml/${definitionId}`,
method: 'get'
});
}
export function deleteDefinition(id: string | string[]) {
return request<R>({
url: `/workflow/definition/${id}`,
method: 'delete'
});
}
export function activeDefinition(definitionId: string, activityStatus: boolean) {
return request<R>({
url: `/workflow/definition/active/${definitionId}`,
method: 'put',
params: { active: activityStatus }
});
}
export function active(definitionId: string, activityStatus: boolean) {
return activeDefinition(definitionId, activityStatus);
}
export function importDefinition(data: FormData) {
return request<R>({
url: '/workflow/definition/importDef',
method: 'post',
data,
headers: { repeatSubmit: false }
});
}
export function importDef(data: FormData) {
return importDefinition(data);
}
export function publishDefinition(id: string) {
return request<R>({
url: `/workflow/definition/publish/${id}`,
method: 'put'
});
};
}
/**
*
* @param id id
* @returns
*/
export const unPublish = (id: string) => {
return request({
export function publish(id: string) {
return publishDefinition(id);
}
export function unPublish(id: string) {
return request<R>({
url: `/workflow/definition/unPublish/${id}`,
method: 'put'
});
};
}
/**
* xml字符串
* @param id id
* @returns
*/
export const xmlString = (id: string) => {
return request({
export function xmlString(id: string) {
return request<R<string>>({
url: `/workflow/definition/xmlString/${id}`,
method: 'get'
});
};
}
/**
*
* @param data
* @returns
*/
export const add = (data: FlowDefinitionForm) => {
return request({
url: `/workflow/definition`,
export function addDefinition(data: FlowDefinitionForm) {
return request<R>({
url: '/workflow/definition',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
* @returns
*/
export const edit = (data: FlowDefinitionForm) => {
return request({
url: `/workflow/definition`,
export function add(data: FlowDefinitionForm) {
return addDefinition(data);
}
export function editDefinition(data: FlowDefinitionForm) {
return request<R>({
url: '/workflow/definition',
method: 'put',
data: data
data
});
};
}
/**
*
* @param id
* @returns
*/
export const getInfo = (id: number | string) => {
return request({
export function edit(data: FlowDefinitionForm) {
return editDefinition(data);
}
export function getDefinition(id: string) {
return request<R<FlowDefinitionVO>>({
url: `/workflow/definition/${id}`,
method: 'get'
});
};
}
/**
*
* @param id id
* @returns
*/
export const copy = (id: string) => {
return request({
export function getInfo(id: string | number) {
return getDefinition(String(id));
}
export function copyDefinition(id: string) {
return request<R>({
url: `/workflow/definition/copy/${id}`,
method: 'post'
});
};
}
export function copy(id: string) {
return copyDefinition(id);
}
export function exportDefinition(id: string) {
return request<Blob>({
url: `/workflow/definition/exportDef/${id}`,
method: 'post',
responseType: 'blob'
});
}

View File

@ -1,32 +1,41 @@
import type { PageQuery } from '@/api/types';
export interface FlowDefinitionForm {
id?: string;
flowName?: string;
flowCode?: string;
category?: string | number;
ext?: string;
formPath?: string;
formCustom?: string;
modelValue?: string;
}
export interface FlowDefinitionQuery extends PageQuery {
flowCode?: string;
flowName?: string;
category: string | number;
category?: string | number;
isPublish?: number;
}
export interface FlowDefinitionVo {
export interface FlowDefinitionVO {
id: string;
flowName: string;
flowCode: string;
formPath: string;
version: string;
isPublish: number;
activityStatus: number;
createTime: Date;
updateTime: Date;
category?: string | number;
categoryName?: string;
formPath?: string;
formCustom?: string;
modelValue?: string;
version?: string | number;
isPublish?: number;
activityStatus?: number;
ext?: string;
createTime?: string | Date;
updateTime?: string | Date;
}
export interface FlowDefinitionForm {
id: string;
flowName: string;
flowCode: string;
category: string;
ext: string;
formPath: string;
formCustom: string;
modelValue: string;
}
export type FlowDefinitionVo = FlowDefinitionVO;
export interface definitionXmlVO {
xml: string[];

View File

@ -1,126 +1,84 @@
import type { PageResult } from '@/api/types';
import type { FlowInstanceQuery, FlowInstanceVO } from '@/api/workflow/instance/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { FlowHisTaskResult, FlowInstanceQuery, FlowInstanceVO } from './types';
/**
*
* @param query
* @returns {*}
*/
export const pageByRunning = (query: FlowInstanceQuery): AxiosPromise<PageResult<FlowInstanceVO>> => {
return request({
export function pageByRunning(query: FlowInstanceQuery) {
return request<R<PageResult<FlowInstanceVO>>>({
url: '/workflow/instance/pageByRunning',
method: 'get',
params: query
});
};
}
/**
*
* @param query
* @returns {*}
*/
export const pageByFinish = (query: FlowInstanceQuery): AxiosPromise<PageResult<FlowInstanceVO>> => {
return request({
export function pageByFinish(query: FlowInstanceQuery) {
return request<R<PageResult<FlowInstanceVO>>>({
url: '/workflow/instance/pageByFinish',
method: 'get',
params: query
});
};
}
/**
* id获取历史流程图
*/
export const flowHisTaskList = (businessId: string | number) => {
return request({
url: `/workflow/instance/flowHisTaskList/${businessId}?t=${Math.random()}`,
method: 'get'
});
};
/**
*
* @param query
* @returns {*}
*/
export const pageByCurrent = (query: FlowInstanceQuery): AxiosPromise<PageResult<FlowInstanceVO>> => {
return request({
export function pageByCurrent(query: FlowInstanceQuery) {
return request<R<PageResult<FlowInstanceVO>>>({
url: '/workflow/instance/pageByCurrent',
method: 'get',
params: query
});
};
}
/**
*
* @param data
* @returns
*/
export const cancelProcessApply = (data: any) => {
return request({
url: `/workflow/instance/cancelProcessApply`,
export function cancelProcessApply(data: {
id?: string | number;
businessId?: string | number;
comment?: string;
message?: string;
}) {
return request<R>({
url: '/workflow/instance/cancelProcessApply',
method: 'put',
data: data
data
});
};
}
/**
*
* @param instanceId id
* @returns
*/
export const instanceVariable = (instanceId: string | number) => {
return request({
export function instanceVariable(instanceId: string | number) {
return request<R<{ variable: string }>>({
url: `/workflow/instance/instanceVariable/${instanceId}`,
method: 'get'
});
};
}
/**
*
* @param instanceIds id
* @returns
*/
export const deleteByInstanceIds = (instanceIds: Array<string | number> | string | number) => {
return request({
export function deleteByInstanceIds(instanceIds: Array<string | number> | string | number) {
return request<R>({
url: `/workflow/instance/deleteByInstanceIds/${instanceIds}`,
method: 'delete'
});
};
}
/**
*
* @param instanceIds
*/
export const deleteHisByInstanceIds = (instanceIds: Array<string | number> | string | number) => {
return request({
export function deleteHisByInstanceIds(instanceIds: Array<string | number> | string | number) {
return request<R>({
url: `/workflow/instance/deleteHisByInstanceIds/${instanceIds}`,
method: 'delete'
});
};
}
/**
*
* @param data
* @returns
*/
export const invalid = (data: any) => {
return request({
url: `/workflow/instance/invalid`,
export function invalid(data: { id: string | number; comment?: string }) {
return request<R>({
url: '/workflow/instance/invalid',
method: 'post',
data: data
data
});
};
/**
*
* @param data
* @returns
*/
export const updateVariable = (data: any) => {
return request({
url: `/workflow/instance/updateVariable`,
}
export function updateVariable(data: { instanceId?: string | number; key?: string; value?: string }) {
return request<R>({
url: '/workflow/instance/updateVariable',
method: 'put',
data: data
data
});
};
}
export function flowHisTaskList(businessId: string | number) {
return request<R<FlowHisTaskResult>>({
url: `/workflow/instance/flowHisTaskList/${businessId}?t=${Math.random()}`,
method: 'get'
});
}

View File

@ -1,4 +1,10 @@
import type { FlowTaskVO } from '@/api/workflow/task/types';
import type { BaseEntity, PageQuery } from '@/api/types';
import type { FlowHistoryVO, FlowTaskVO } from '@/api/workflow/task/types';
export interface FlowHisTaskResult {
list: FlowHistoryVO[];
instanceId?: string | number;
}
export interface FlowInstanceQuery extends PageQuery {
category?: string | number;
@ -11,18 +17,25 @@ export interface FlowInstanceQuery extends PageQuery {
export interface FlowInstanceVO extends BaseEntity {
id: string | number;
definitionId: string;
flowName: string;
flowCode: string;
version: string;
definitionId?: string;
flowName?: string;
flowCode?: string;
version?: string | number;
businessId: string;
activityStatus: number;
tenantId: string;
createTime: string;
createBy: string;
flowStatus: string;
flowStatusName: string;
flowTaskList: FlowTaskVO[];
businessCode: string;
businessTitle: string;
activityStatus?: number;
tenantId?: string;
createTime?: string;
updateTime?: string;
createBy?: string;
createByName?: string;
flowStatus?: string;
flowStatusName?: string;
flowTaskList?: FlowTaskVO[];
businessCode?: string;
businessTitle?: string;
nodeName?: string;
categoryName?: string;
isSuspended?: boolean;
formCustom?: string;
formPath?: string;
}

View File

@ -1,76 +1,49 @@
import type { PageResult } from '@/api/types';
import type { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { LeaveForm, LeaveQuery, LeaveVO } from './types';
/**
*
* @param query
* @returns {*}
*/
export const listLeave = (query?: LeaveQuery): AxiosPromise<PageResult<LeaveVO>> => {
return request({
export function listLeave(query?: LeaveQuery) {
return request<R<PageResult<LeaveVO>>>({
url: '/workflow/leave/list',
method: 'get',
params: query
});
};
}
/**
*
* @param id
*/
export const getLeave = (id: string | number): AxiosPromise<LeaveVO> => {
return request({
url: '/workflow/leave/' + id,
export function getLeave(id: string | number) {
return request<R<LeaveVO>>({
url: `/workflow/leave/${id}`,
method: 'get'
});
};
}
/**
*
* @param data
*/
export const addLeave = (data: LeaveForm): AxiosPromise<LeaveVO> => {
return request({
export function addLeave(data: LeaveForm) {
return request<R<LeaveVO>>({
url: '/workflow/leave',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
*/
export const submitAndFlowStart = (data: LeaveForm): AxiosPromise<LeaveVO> => {
return request({
export function submitAndFlowStart(data: LeaveForm) {
return request<R<LeaveVO>>({
url: '/workflow/leave/submitAndFlowStart',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
*/
export const updateLeave = (data: LeaveForm): AxiosPromise<LeaveVO> => {
return request({
export function updateLeave(data: LeaveForm) {
return request<R<LeaveVO>>({
url: '/workflow/leave',
method: 'put',
data: data
data
});
};
}
/**
*
* @param id
*/
export const delLeave = (id: string | number | Array<string | number>) => {
return request({
url: '/workflow/leave/' + id,
export function delLeave(id: string | number | Array<string | number>) {
return request<R>({
url: `/workflow/leave/${id}`,
method: 'delete'
});
};
}

View File

@ -1,13 +1,4 @@
export interface LeaveVO {
id: string | number;
applyCode?: string;
leaveType: string;
startDate: string;
endDate: string;
leaveDays: number;
remark: string;
status?: string;
}
import type { BaseEntity, PageQuery } from '@/api/types';
export interface LeaveForm extends BaseEntity {
id?: string | number;
@ -24,3 +15,14 @@ export interface LeaveQuery extends PageQuery {
startLeaveDays?: number;
endLeaveDays?: number;
}
export interface LeaveVO {
id: string | number;
applyCode?: string;
leaveType?: string;
startDate?: string;
endDate?: string;
leaveDays?: number;
remark?: string;
status?: string;
}

View File

@ -1,64 +1,41 @@
import type { PageResult } from '@/api/types';
import type { SpelForm, SpelQuery, SpelVO } from '@/api/workflow/spel/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import request from '@/api/request';
import type { SpelForm, SpelQuery, SpelVO } from './types';
/**
* spel表达式定义列表
* @param query
* @returns {*}
*/
export const listSpel = (query?: SpelQuery): AxiosPromise<PageResult<SpelVO>> => {
return request({
export function listSpel(query?: SpelQuery) {
return request<R<PageResult<SpelVO>>>({
url: '/workflow/spel/list',
method: 'get',
params: query
});
};
}
/**
* spel表达式定义详细
* @param id
*/
export const getSpel = (id: string | number): AxiosPromise<SpelVO> => {
return request({
url: '/workflow/spel/' + id,
export function getSpel(id: string | number) {
return request<R<SpelVO>>({
url: `/workflow/spel/${id}`,
method: 'get'
});
};
}
/**
* spel表达式定义
* @param data
*/
export const addSpel = (data: SpelForm) => {
return request({
export function addSpel(data: SpelForm) {
return request<R>({
url: '/workflow/spel',
method: 'post',
data: data
data
});
};
}
/**
* spel表达式定义
* @param data
*/
export const updateSpel = (data: SpelForm) => {
return request({
export function updateSpel(data: SpelForm) {
return request<R>({
url: '/workflow/spel',
method: 'put',
data: data
data
});
};
}
/**
* spel表达式定义
* @param id
*/
export const delSpel = (id: string | number | Array<string | number>) => {
return request({
url: '/workflow/spel/' + id,
export function delSpel(id: string | number | Array<string | number>) {
return request<R>({
url: `/workflow/spel/${id}`,
method: 'delete'
});
};
}

View File

@ -1,105 +1,29 @@
export interface SpelVO {
/**
* id
*/
id: string | number;
/**
*
*/
componentName: string;
/**
*
*/
methodName: string;
/**
*
*/
methodParams: string;
/**
* spel值
*/
viewSpel: string;
/**
* 0 1
*/
status: string;
/**
*
*/
remark?: string;
}
import type { BaseEntity, PageQuery } from '@/api/types';
export interface SpelForm extends BaseEntity {
/**
* id
*/
id?: string | number;
/**
*
*/
componentName?: string;
/**
*
*/
methodName?: string;
/**
*
*/
methodParams?: string;
/**
* spel值
*/
viewSpel?: string;
/**
* 0 1
*/
status?: string;
/**
*
*/
remark?: string;
}
export interface SpelQuery extends PageQuery {
/**
*
*/
componentName?: string;
/**
*
*/
methodName?: string;
/**
*
*/
methodParams?: string;
/**
* spel值
*/
viewSpel?: string;
/**
* 0 1
*/
status?: string;
/**
*
*/
params?: any;
}
export interface SpelVO {
id: string | number;
componentName?: string;
methodName?: string;
methodParams?: string;
viewSpel?: string;
status?: string;
remark?: string;
}

View File

@ -1,207 +1,129 @@
import type { PageResult } from '@/api/types';
import type { FlowTaskVO, TaskOperationBo, TaskQuery } from '@/api/workflow/task/types';
import type { AxiosPromise } from '@/utils/api-types';
import request from '@/utils/request';
import type { PageResult, R } from '@/api/types';
import type { StartProcessBo } from '@/api/workflow/workflowCommon/types';
import request from '@/api/request';
import type { FlowNextNodeVO, FlowTaskVO, StartWorkflowResult, TaskOperationBo, TaskQuery } from './types';
/**
*
* @param query
* @returns {*}
*/
export const pageByTaskWait = (query: TaskQuery): AxiosPromise<PageResult<FlowTaskVO>> => {
return request({
export function pageByTaskWait(query: TaskQuery) {
return request<R<PageResult<FlowTaskVO>>>({
url: '/workflow/task/pageByTaskWait',
method: 'get',
params: query
});
};
}
/**
*
* @param query
* @returns {*}
*/
export const pageByTaskFinish = (query: TaskQuery): AxiosPromise<PageResult<FlowTaskVO>> => {
return request({
export function pageByTaskFinish(query: TaskQuery) {
return request<R<PageResult<FlowTaskVO>>>({
url: '/workflow/task/pageByTaskFinish',
method: 'get',
params: query
});
};
}
/**
*
* @param query
* @returns {*}
*/
export const pageByTaskCopy = (query: TaskQuery): AxiosPromise<PageResult<FlowTaskVO>> => {
return request({
export function pageByTaskCopy(query: TaskQuery) {
return request<R<PageResult<FlowTaskVO>>>({
url: '/workflow/task/pageByTaskCopy',
method: 'get',
params: query
});
};
}
/**
*
* @param query
* @returns {*}
*/
export const pageByAllTaskWait = (query: TaskQuery): AxiosPromise<PageResult<FlowTaskVO>> => {
return request({
export function pageByAllTaskWait(query: TaskQuery) {
return request<R<PageResult<FlowTaskVO>>>({
url: '/workflow/task/pageByAllTaskWait',
method: 'get',
params: query
});
};
}
/**
*
* @param query
* @returns {*}
*/
export const pageByAllTaskFinish = (query: TaskQuery): AxiosPromise<PageResult<FlowTaskVO>> => {
return request({
export function pageByAllTaskFinish(query: TaskQuery) {
return request<R<PageResult<FlowTaskVO>>>({
url: '/workflow/task/pageByAllTaskFinish',
method: 'get',
params: query
});
};
}
/**
*
* @param data
* @returns {*}
*/
export const startWorkFlow = (data: object): any => {
return request({
export function startWorkFlow(data: StartProcessBo) {
return request<R<StartWorkflowResult>>({
url: '/workflow/task/startWorkFlow',
method: 'post',
data: data
data
});
};
}
/**
*
* @param data
* @returns {*}
*/
export const completeTask = (data: object) => {
return request({
url: '/workflow/task/completeTask',
method: 'post',
data: data
});
};
/**
*
* @param data
* @returns {*}
*/
export const backProcess = (data: any): any => {
return request({
url: '/workflow/task/backProcess',
method: 'post',
data: data
});
};
/**
*
* @param taskId
* @returns
*/
export const getTask = (taskId: string) => {
return request({
url: '/workflow/task/getTask/' + taskId,
method: 'get'
});
};
/**
*
* @param taskIdList
* @param userId
* @returns
*/
export const updateAssignee = (taskIdList: Array<string>, userId: string) => {
return request({
export function updateAssignee(taskIdList: Array<string | number>, userId: string | number) {
return request<R>({
url: `/workflow/task/updateAssignee/${userId}`,
method: 'put',
data: taskIdList
});
};
}
/**
*
* @returns
*/
export const terminationTask = (data: any) => {
return request({
url: `/workflow/task/terminationTask`,
export function urgeTask(data: { taskIdList: Array<string | number>; message?: string; messageType?: string[] }) {
return request<R>({
url: '/workflow/task/urgeTask',
method: 'post',
data: data
data
});
};
}
/**
*
* @returns
*/
export const getBackTaskNode = (taskId: string | number, nodeCode: string) => {
return request({
url: `/workflow/task/getBackTaskNode/${taskId}/${nodeCode}`,
export function completeTask(data: Record<string, unknown>) {
return request<R>({
url: '/workflow/task/completeTask',
method: 'post',
data
});
}
export function backProcess(data: Record<string, unknown>) {
return request<R>({
url: '/workflow/task/backProcess',
method: 'post',
data
});
}
export function getTask(taskId: string | number) {
return request<R<FlowTaskVO>>({
url: `/workflow/task/getTask/${taskId}`,
method: 'get'
});
};
}
/**
* delegateTask transferTask addSignature reductionSignature
* @returns
*/
export const taskOperation = (data: TaskOperationBo, operation: string) => {
return request({
export function terminationTask(data: { taskId?: string | number; comment?: string }) {
return request<R>({
url: '/workflow/task/terminationTask',
method: 'post',
data
});
}
export function getBackTaskNode(taskId: string | number, nodeCode?: string) {
return request<R<FlowNextNodeVO[]>>({
url: `/workflow/task/getBackTaskNode/${taskId}/${nodeCode || ''}`,
method: 'get'
});
}
export function taskOperation(data: TaskOperationBo, operation: string) {
return request<R>({
url: `/workflow/task/taskOperation/${operation}`,
method: 'post',
data: data
data
});
};
}
/**
*
* @param taskId id
* @returns
*/
export const currentTaskAllUser = (taskId: string | number) => {
return request({
export function currentTaskAllUser(taskId: string | number) {
return request<R<Array<{ userId: string | number; nickName: string; nodeName?: string }>>>({
url: `/workflow/task/currentTaskAllUser/${taskId}`,
method: 'get'
});
};
}
/**
*
* @param data参数
* @returns
*/
export const getNextNodeList = (data: any): any => {
return request({
export function getNextNodeList(data: { taskId?: string | number; variables?: Record<string, unknown> }) {
return request<R<FlowNextNodeVO[]>>({
url: '/workflow/task/getNextNodeList',
method: 'post',
data: data
data
});
};
/**
*
* @param data参数
* @returns
*/
export const urgeTask = (data: any): any => {
return request({
url: '/workflow/task/urgeTask',
method: 'post',
data: data
});
};
}

View File

@ -1,60 +1,82 @@
import type { OssVO } from '@/api/system/oss/types';
import type { PageQuery } from '@/api/types';
export interface ButtonList {
code: string;
show: boolean;
}
export interface FlowCopyVo {
userId: string | number;
nickName: string;
}
export interface FlowHistoryVO {
nodeName?: string;
approverName?: string;
flowStatus?: string;
message?: string;
createTime?: string;
updateTime?: string;
runDuration?: string;
ext?: string | number;
attachmentList?: OssVO[];
}
export interface FlowNextNodeVO {
nodeCode: string;
nodeName: string;
permissionFlag?: string | Array<string | number>;
}
export interface FlowTaskVO {
id: string | number;
createTime?: string | Date;
updateTime?: string | Date;
tenantId?: string;
definitionId?: string;
instanceId?: string;
flowName?: string;
businessId: string;
nodeCode?: string;
nodeName?: string;
flowCode?: string;
flowStatus?: string;
flowStatusName?: string;
flowTaskStatus?: string;
formCustom?: string;
formPath?: string;
nodeType?: number;
nodeRatio?: string | number;
version?: string;
applyNode?: boolean;
buttonList?: ButtonList[];
copyList?: FlowCopyVo[];
varList?: Record<string, string>;
businessCode?: string;
businessTitle?: string;
categoryName?: string;
createByName?: string;
assigneeNames?: string;
approverName?: string;
}
export interface StartWorkflowResult {
taskId?: string | number;
}
export interface TaskOperationBo {
taskId?: string | number;
userId?: string | number;
userIds?: Array<string | number>;
message?: string;
messageType?: string[];
variables?: Record<string, unknown>;
}
export interface TaskQuery extends PageQuery {
nodeName?: string;
flowCode?: string;
flowName?: string;
createByIds?: Array<string | number>;
}
export interface ParticipantVo {
groupIds?: string[] | number[];
candidate: string[] | number[];
candidateName: string[];
claim: boolean;
}
export interface FlowTaskVO {
id: string | number;
createTime?: Date;
updateTime?: Date;
tenantId?: string;
definitionId?: string;
instanceId: string;
flowName: string;
businessId: string;
nodeCode: string;
nodeName: string;
flowCode: string;
flowStatus: string;
formCustom: string;
formPath: string;
nodeType: number;
nodeRatio: string | number;
version?: string;
applyNode?: boolean;
buttonList?: ButtonList[];
copyList?: FlowCopyVo[];
varList?: Map<string, string>;
businessCode: string;
businessTitle: string;
}
export interface ButtonList {
code: string;
show: boolean;
}
export interface FlowCopyVo {
userId: string | number;
nickName: string;
}
export interface TaskOperationBo {
//委派/转办人的用户ID必填准对委派/转办人操作)
userId?: string;
//加签/减签人的用户ID列表必填针对加签/减签操作)
userIds?: string[];
//任务ID必填
taskId: string | number;
//消息类型
messageType?: string[];
//意见或备注信息(可选)
message?: string;
}

View File

@ -1,17 +1,17 @@
import type { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
import tab from '@/plugins/tab';
import router from '@/router';
import { history } from '@umijs/max';
import { normalizePath } from '@/utils/ruoyi';
import type { RouterJumpVO } from './types';
export function routerJump(routerJump: RouterJumpVO) {
history.push(
`${normalizePath(routerJump.formPath || '/workflow/leaveEdit/index')}?${new URLSearchParams({
id: routerJump.businessId,
type: routerJump.type,
taskId: String(routerJump.taskId)
}).toString()}`
);
}
export default {
routerJump(routerJumpVo: RouterJumpVo) {
tab.closePage(router.currentRoute.value);
router.push({
path: routerJumpVo.formPath,
query: {
id: routerJumpVo.businessId,
type: routerJumpVo.type,
taskId: routerJumpVo.taskId
}
});
}
routerJump
};

View File

@ -1,4 +1,4 @@
export interface RouterJumpVo {
export interface RouterJumpVO {
businessId: string;
taskId: string | number;
type: string;
@ -9,6 +9,6 @@ export interface RouterJumpVo {
export interface StartProcessBo {
businessId: string | number;
flowCode: string;
variables: any;
bizExt: any;
variables: Record<string, unknown>;
bizExt: Record<string, unknown>;
}

14
src/app.tsx Normal file
View File

@ -0,0 +1,14 @@
import type { ReactNode } from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { appEnv } from '@/utils/env';
import { queryClient } from '@/utils/queryClient';
export async function getInitialState() {
return {
name: appEnv.logoTitle
};
}
export function rootContainer(container: ReactNode) {
return <QueryClientProvider client={queryClient}>{container}</QueryClientProvider>;
}

Some files were not shown because too many files have changed in this diff Show More