update 内置代码生成模板 优化skill

This commit is contained in:
疯狂的狮子Li 2026-06-03 15:20:07 +08:00
parent 7eccb9b8c1
commit bbf508e123
12 changed files with 1194 additions and 34 deletions

View File

@ -12,6 +12,7 @@ description: 前端 API 与类型定义专家。用于当前 plus-ui-react 项
3. 能明确写出类型时,不要偷懒用 `any`
4. 如果当前模块已有特殊返回结构或聚合导出,继续保持一致。
5. 不创建页面,不改路由,除非用户明确要求。
6. 如果用户要求维护代码生成模板,优先同步当前仓库 `gen/api.ts.vm`、`gen/types.ts.vm`,必要时再看 boot4 后端 generator 字段定义。
## API 规则
@ -42,10 +43,18 @@ description: 前端 API 与类型定义专家。用于当前 plus-ui-react 项
- 日期范围查询保留 `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 路径是否与后端一致。
- 类型是否覆盖接口真实结构。
- 是否不必要地把类型写宽了。
- 是否保留了当前模块的命名和导出风格。
- 是否同步维护了当前项目内置 `gen/*.vm` 模板中对应 API/types 输出。
- 是否误用了 Vue 版 `AxiosPromise`、`@/utils/request` 或 `src/views` 约定。

View File

@ -1,6 +1,6 @@
---
name: frontend-crud-coding
description: 前端总入口。用于当前 plus-ui-react 项目中的标准 React CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮、状态切换与弹窗表单等任务,并根据任务类型选择合适的前端子 agent。
description: 前端总入口。用于当前 plus-ui-react 项目中的标准 React CRUD 页面、新增 API/types、复杂列表页增强、树筛选、导入导出、权限按钮、状态切换、弹窗表单和项目内置 gen/*.vm React 代码生成模板维护任务,并根据任务类型选择合适的前端子 agent。
---
你是当前 plus-ui-react 前端项目的总入口 agent。
@ -10,12 +10,13 @@ description: 前端总入口。用于当前 plus-ui-react 项目中的标准 Rea
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.vue.vm`、`gen/index-tree.vue.vm`,再按 React 项目真实工具和页面规则修改模板。
通用要求:
- 先读当前目录下最近似页面和 API再动代码。
- 优先参考 `.codex/skills/frontend-crud-coding/SKILL.md``references/frontend.md`,保持 Claude agent 与 Codex skill 规则一致。
- 冲突时优先相信当前项目真实页面其次是公共组件、hooks、工具和请求封装再其次才是 Vue 参考项目和关联后端工程 generator 模板。
- 冲突时优先相信当前项目真实页面其次是公共组件、hooks、工具和请求封装再其次是当前仓库 `gen/*.vm` React 模板,最后才是 Vue 参考项目和关联后端工程 generator 模板。
- 默认直接产出可落地代码,而不是只给抽象建议。
- 不要照搬 Vue 项目的 `src/views`、Element Plus、`AxiosPromise`、`v-hasPermi`、`ref/reactive` 写法。
@ -28,6 +29,8 @@ description: 前端总入口。用于当前 plus-ui-react 项目中的标准 Rea
- 权限通过 `useUserStore``hasPermi` 计算布尔值。
- 多选通过 `useTableSelection`,导出通过 `useTableExport`
- 行操作优先使用 `RowActions``@ant-design/icons`
- 日期范围、字典 options、树表展开、loading、搜索重置和确认弹窗优先使用 `useDateRangeQuery`、`dictOptions`、`useTreeTableExpand`、`useLoading`、`useSearchReset`、`confirmAction`/`confirmTitleSafe`。
- `gen/index.vue.vm`、`gen/index-tree.vue.vm` 文件名保持不变,但内容必须生成 React TSX 页面。
验证要求:

View File

@ -1,6 +1,6 @@
---
name: frontend-crud-page
description: 前端标准 React CRUD 页面专家。用于当前 plus-ui-react 项目中的新建列表页、弹窗表单页、标准 API/types/index.tsx 骨架,以及后端 generator 字段到 React + ProTable 项目风格的落地任务。
description: 前端标准 React CRUD 页面专家。用于当前 plus-ui-react 项目中的新建列表页、弹窗表单页、标准 API/types/index.tsx 骨架、项目内置 gen/*.vm React 模板维护,以及后端 generator 字段到 React + ProTable 项目风格的落地任务。
---
你负责当前 plus-ui-react 项目中的标准 CRUD 页面实现。
@ -9,8 +9,9 @@ description: 前端标准 React CRUD 页面专家。用于当前 plus-ui-react
1. 先看当前模块最近似页面。
2. 再参考 `.codex/skills/frontend-crud-coding/references/frontend.md`
3. 必要时参考关联后端工程 generator 模板确认接口、字段、权限和导出能力。
4. 默认同时维护:
3. 维护或新增生成能力时,优先参考当前仓库 `gen/api.ts.vm`、`gen/types.ts.vm`、`gen/index.vue.vm`、`gen/index-tree.vue.vm`。
4. 必要时参考关联后端工程 generator 模板确认接口、字段、权限和导出能力。
5. 默认同时维护:
`src/api/<module>/<business>/index.ts`
`src/api/<module>/<business>/types.ts`
`src/pages/<module>/<business>/index.tsx`
@ -34,6 +35,9 @@ description: 前端标准 React CRUD 页面专家。用于当前 plus-ui-react
- 导出使用 `useTableExport()``exportFile('/module/business/export', () => 'business_timestamp.xlsx')`
- 行操作使用 `RowActions`,行内删除确认放在 `confirm`
- 新增/编辑弹窗使用 `ModalForm<XxxForm>`,字段优先使用 ProForm 组件。
- 日期范围使用 `useDateRangeQuery`,字典 options 使用 `dictOptions`
- 状态切换等 Promise 式确认使用 `confirmAction``confirmTitleSafe`
- 异步 loading 优先使用 `useLoading`,带额外筛选状态的重置优先使用 `useSearchReset`
## API / types 规则
@ -48,13 +52,22 @@ description: 前端标准 React CRUD 页面专家。用于当前 plus-ui-react
- 判断树表后不要生成分页 `PageResult` 页面。
- `Query` 通常不继承 `PageQuery`
- 使用 `handleTree`、`pagination={false}`、`expandedRowKeys`、`ProFormTreeSelect`。
- 使用 `handleTree`、`pagination={false}`、`useTreeTableExpand`、`ProFormTreeSelect`。
- 树表展开/折叠优先使用 `useTreeTableExpand`,不要在页面或模板里重复手写收集 key 的状态逻辑。
- 新增子节点时从当前行带入 `parentId`
## gen 模板规则
- `gen/index.vue.vm`、`gen/index-tree.vue.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` 是否同步补齐。
- 页面是否使用 React 项目的 ProTable/ModalForm 骨架。
- 权限标识是否与后端和相邻页面一致。
- 是否同步使用当前项目公共 hooks/工具,避免生成重复代码。
- 是否误用了 Vue/Element Plus/`src/views` 规则。

View File

@ -29,9 +29,14 @@ description: 复杂前端页面增强专家。用于修改当前 plus-ui-react
- 复杂系统页可包含 `TreePanel`、子组件弹窗、抽屉、授权路由跳转。
- 权限通过 `useUserStore` + `hasPermi` 计算,不使用 Vue 指令。
- 字典通过 `useDict` 加载,按现有页面映射为 Ant Design options。
- 字典 options 优先使用 `dictOptions` from `@/utils/dict`
- 导出通过 `useTableExport`,不要另写下载封装。
- 多选通过 `useTableSelection`,不要手写一套重复状态。
- 日期范围参考 `system/user`,使用 `formatDateTimeRange` + `addDateRange`
- 日期范围优先使用 `useDateRangeQuery`,底层保持 `addDateRange` 参数格式。
- 树表展开优先使用 `useTreeTableExpand`
- 异步 loading 优先使用 `useLoading`
- 额外筛选状态的重置优先使用 `useSearchReset`
- Promise 式确认框优先使用 `confirmAction``confirmTitleSafe`
- 状态切换失败时回滚或刷新,参考 `system/user`
## 增量修改规则
@ -41,6 +46,7 @@ description: 复杂前端页面增强专家。用于修改当前 plus-ui-react
- 如果页面已有导出参数缓存,继续通过 `updateExportParams` 更新。
- 如果页面已有树筛选状态,查询参数要合并该状态,不要覆盖掉。
- 如果页面已有特殊禁用规则、超级管理员保护或业务状态判断,必须保留。
- 如果增强的是标准 CRUD 能力或公共工具约定,要评估当前仓库 `gen/*.vm` 是否也应同步更新。
## 自检
@ -48,4 +54,5 @@ description: 复杂前端页面增强专家。用于修改当前 plus-ui-react
- 是否误删了已有权限控制或交互能力。
- 是否保留了树筛选、导入导出、抽屉、路由跳转等复杂能力。
- 是否应该拆成子组件而不是继续堆主页面。
- 是否使用了当前项目已有工具,避免重复手写日期范围、字典 options、树展开、loading、确认框。
- 是否误用了 Vue/Element Plus/generator 的简化逻辑。

View File

@ -1,11 +1,11 @@
---
name: frontend-crud-coding
description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript + Umi Max + Ant Design ProComponents + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入和样式。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与 RuoYi-Vue-Plus boot4 后端接口对应的 src/api、types 和 src/pages 代码;触发后应先读取适用 references再阅读目标模块真实代码和必要的 Vue 参考 skill/后端 generator 模板。
description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript + Umi Max + Ant Design ProComponents + oxlint/oxfmt 代码风格生成或修改页面、API、types、hooks 接入、样式和项目内置 gen/*.vm 代码生成模板。用于新增或修改标准 CRUD 列表页、树表页、系统管理页、监控页、workflow 页面、demo 页面,补齐与 RuoYi-Vue-Plus boot4 后端接口对应的 src/api、types 和 src/pages 代码,或维护 gen/api.ts.vm、gen/types.ts.vm、gen/index.vue.vm、gen/index-tree.vue.vm;触发后应先读取适用 references再阅读目标模块真实代码和项目内置 gen 模板。
---
# 前端编码规范
先对齐当前 React 项目里的真实实现,再参考 Vue 版本 skill 和关联后端工程的代码生成器模板。不要直接套 Vue 3、Element Plus、AxiosPromise 或 `src/views` 规则;要落成当前仓库的 `src/pages`、`src/api`、ProTable、ModalForm、权限和导出方式。
先对齐当前 React 项目里的真实实现,再参考项目内置 `gen/` 代码生成模板。Vue 版本 skill 和关联后端工程的代码生成器模板只用于理解字段、权限、任务分型和迁移意图。不要直接套 Vue 3、Element Plus、AxiosPromise 或 `src/views` 规则;要落成当前仓库的 `src/pages`、`src/api`、ProTable、ModalForm、权限、导出和公共工具方式。
## 执行流程
@ -16,11 +16,12 @@ description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript
- 树表优先看 `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. 需要从 Vue 版本迁移经验时,参考 `D:\git-sources\Plus相关\plus-ui-new\.codex\skills\frontend-crud-coding``.claude\agents`,只吸收任务分型、增量修改和自检原则。
5. 新增标准页面前,可以对照后端工程 `D:\git-sources\Plus相关\RuoYi-Vue-Plus-boot4\ruoyi-modules\ruoyi-gen\src\main\resources\vm` 确认接口、字段、权限和导出能力,但输出必须改成 React 项目风格。
6. 新增代码时通常同步维护 `src/api/<module>/<business>/index.ts`、`types.ts`、`src/pages/<module>/<business>/index.tsx`。
7. 增强已有页面时只做增量修改,保留原页面的树筛选、导入导出、列显隐、权限、字典、弹窗、抽屉和路由跳转能力。
8. 修改完成后按影响范围运行验证:优先 `pnpm exec tsc --noEmit``pnpm lint`;大范围页面、公共组件、构建配置变更再跑 `pnpm build`
4. 新增或维护代码生成能力时,优先阅读当前仓库 `gen/api.ts.vm`、`gen/types.ts.vm`、`gen/index.vue.vm`、`gen/index-tree.vue.vm`。这些是 React 版内置生成模板,即使文件名保留 `.vue.vm`,内容也应是当前项目 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\vm` 确认字段、权限和导出能力,但输出和 `gen/` 模板必须保持 React 项目风格。
7. 新增代码时通常同步维护 `src/api/<module>/<business>/index.ts`、`types.ts`、`src/pages/<module>/<business>/index.tsx`;如果是生成模板优化,同步维护 `gen/` 对应 `.vm`
8. 增强已有页面时只做增量修改,保留原页面的树筛选、导入导出、列显隐、权限、字典、弹窗、抽屉和路由跳转能力。
9. 修改完成后按影响范围运行验证:优先 `pnpm exec tsc --noEmit``pnpm lint`;大范围页面、公共组件、构建配置变更再跑 `pnpm build`
## 文档读取规则
@ -34,9 +35,10 @@ description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript
1. 目标目录下最近似页面、API、types 的真实实现。
2. 当前项目公共 hooks、组件、工具、样式和请求封装约定。
3. Vue 参考项目 `.codex` / `.claude` 中的任务分型和工作流。
4. 关联后端工程 generator 模板。
5. 通用 React / Ant Design ProComponents 习惯。
3. 当前项目内置 `gen/*.vm` React 代码生成模板。
4. Vue 参考项目 `.codex` / `.claude` 中的任务分型和工作流。
5. 关联后端工程 generator 模板。
6. 通用 React / Ant Design ProComponents 习惯。
也就是说:
@ -54,6 +56,7 @@ description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript
- 请求统一通过 `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'])` 计算布尔值。
- 新页面不要无故引入另一套状态管理、请求封装、表格封装、样式体系或权限写法。
@ -89,7 +92,7 @@ description: 在当前 plus-ui-react 前端项目中按真实 React + TypeScript
### 2. 树表 CRUD
`src/pages/demo/tree/index.tsx`、`src/pages/workflow/category/index.tsx` 为主要起点。列表接口通常返回数组,不使用分页 `PageResult`;页面使用 `handleTree`、`expandedRowKeys`、`ProFormTreeSelect`。
`src/pages/demo/tree/index.tsx`、`src/pages/workflow/category/index.tsx` 为主要起点。列表接口通常返回数组,不使用分页 `PageResult`;页面使用 `handleTree`、`useTreeTableExpand`、`ProFormTreeSelect`。
### 3. 强业务页面
@ -103,6 +106,17 @@ workflow 目录优先参考 `src/pages/workflow/*`。流程定义、流程实例
只维护 `src/api/<module>/<business>/index.ts``types.ts`,但仍要与后端路由、返回结构、当前模块导入方式和类型入口一致。
### 6. 维护 React 生成模板
模板只放在当前仓库 `gen/` 下,除非用户明确要求写入 boot4 后端工程。维护时保持 Velocity 变量和文件名不变:
- `gen/api.ts.vm`
- `gen/types.ts.vm`
- `gen/index.vue.vm`
- `gen/index-tree.vue.vm`
模板内容必须生成当前 React 项目代码,并优先使用已有公共工具:`useDateRangeQuery`、`dictOptions`、`useTreeTableExpand`、`useTableSelection`、`useTableExport`、`confirmAction`、`toPageQuery`、`toTableData`、`handleTree`、`formatDateTimeFields`、`toDayjsFields`。不要把 Vue 模板、Element Plus 组件或 `src/views` 路径写入这些模板。
## 输出要求
使用本 skill 时,默认期望产出应满足:
@ -112,6 +126,7 @@ workflow 目录优先参考 `src/pages/workflow/*`。流程定义、流程实例
- 标准页查询、重置、分页、弹窗、提交、删除、导出流程闭环完整。
- 复杂页面保留原有交互能力和业务约束。
- 代码体现当前项目 hooks、页面壳和下载方式而不是 Vue 版本或 generator 裸输出。
- 生成模板变更要同步反映当前项目公共 hooks/工具,避免生成的新页面重复手写日期范围、字典 options、树展开、loading、搜索重置和确认框逻辑。
- 交付前说明运行过的验证命令;如果无法验证,说明原因。
## 快速检查清单
@ -124,8 +139,12 @@ workflow 目录优先参考 `src/pages/workflow/*`。流程定义、流程实例
- 多选是否通过 `useTableSelection<T>(row => row.id)`
- 行操作是否优先使用 `RowActions` 和 Ant Design 图标。
- 权限是否通过 `useUserStore` + `hasPermi` 计算,不要写 Vue 指令。
- 日期范围是否通过 `formatDateTimeRange` + `addDateRange` 或附近页面现有方式处理。
- 树表是否使用 `handleTree`、`pagination={false}`、`expandedRowKeys`、`ProFormTreeSelect`。
- 日期范围是否通过 `useDateRangeQuery``addDateRange` 相关工具处理。
- 字典 options 是否通过 `dictOptions`
- 树表是否使用 `handleTree`、`pagination={false}`、`useTreeTableExpand`、`ProFormTreeSelect`。
- loading 是否优先使用 `useLoading`,搜索重置是否优先使用 `useSearchReset`
- 需要 Promise 式确认时是否使用 `confirmAction``confirmTitleSafe`
- 代码生成模板是否仍输出 React TSX 项目风格。
## 推荐提问方式

View File

@ -1,7 +1,7 @@
interface:
display_name: "前端编码"
short_description: "按 plus-ui-react 真实代码编写 React CRUD"
default_prompt: "使用 $frontend-crud-coding 先读取适用 reference再按 plus-ui-react 真实页面、API/types、ProTable、ModalForm、权限和导出约定实现前端修改。"
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

@ -14,9 +14,10 @@
- 先看 `src/api/system/client/*``src/pages/system/client/index.tsx` 是否已存在。
- 再看 `src/pages/demo/demo/index.tsx` 的标准 React CRUD 骨架。
- 必要时对照 boot4 generator 模板确认字段、权限和接口。
- 新增标准生成能力时优先对照当前仓库 `gen/index.vue.vm`、`gen/api.ts.vm`、`gen/types.ts.vm`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新增树表页面
@ -32,9 +33,25 @@
- 判断这是树表,不生成分页 `PageResult` 页面。
- API 列表返回 `R<Tree2VO[]>`
- `Query` 不继承 `PageQuery`
- 页面使用 `handleTree`、`pagination={false}`、`expandedRowKeys`、`ProFormTreeSelect`。
- 页面使用 `handleTree`、`pagination={false}`、`useTreeTableExpand`、`ProFormTreeSelect`。
- 新增子节点时从当前行带入 `parentId`
## 案例 2.1:优化 React 代码生成模板
### 用户提问示例
```text
使用 $frontend-crud-coding 优化当前项目 gen 目录下的 React 代码生成模板,让生成页面继续使用新加的工具。
```
### 期望执行方式
- 读取 `gen/api.ts.vm`、`gen/types.ts.vm`、`gen/index.vue.vm`、`gen/index-tree.vue.vm`。
- 保持 Velocity 变量、宏和文件名不变。
- 普通表模板继续使用 `useTableSelection`、`useTableExport`、`useDateRangeQuery`、`dictOptions`、`confirmAction`。
- 树表模板继续使用 `handleTree`、`filterTree`、`useTreeTableExpand`、`dictOptions`。
- 输出仍是 React TSX 项目代码,不写 Vue `src/views`、Element Plus 或 `AxiosPromise`
## 案例 3修改已有复杂列表页
### 用户提问示例
@ -66,7 +83,7 @@
- 优先看 `src/pages/workflow/category/index.tsx``src/api/workflow/category/*`
- 判断是否需要后端新增导出接口;前端导出路径保持 `/workflow/category/export`
- 不迁移 system/user 的用户专属逻辑。
- 保留树表、`expandedRowKeys`、`handleTree` 和分类弹窗逻辑。
- 保留树表、`useTreeTableExpand`、`handleTree` 和分类弹窗逻辑。
## 案例 5只补 API 和 types
@ -107,7 +124,7 @@
2. 新增状态筛选和导出
3. API 路径沿用后端接口
4. 参考 system/config 的工具栏与导出交互
5. 参考 boot4 generator 模板补齐缺失 types
5. 参考当前项目 gen 模板和 boot4 generator 字段补齐缺失 types
```
## 不推荐的任务描述

View File

@ -3,14 +3,15 @@
## 优先参考的代码来源
- 当前目标目录下最近似页面、API、types。
- 当前仓库内置 React 生成模板:`gen/api.ts.vm`、`gen/types.ts.vm`、`gen/index.vue.vm`、`gen/index-tree.vue.vm`。
- 标准单表:`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`。
- 公共 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/ruoyi.ts`、`src/utils/permission.ts`、`src/utils/download.ts`、`src/utils/dict.ts`、`src/utils/modal.ts`
## 基础栈与格式
@ -82,13 +83,14 @@
- `remove``handleDelete` 支持行删除和批量删除,成功后 `message.success('删除成功')`、`clearSelection()`、`reloadAndRest` 或 `reload`
- 批量按钮通常用 `Popconfirm`,行操作确认优先放在 `RowActions``confirm`
- 状态切换失败时要回滚或刷新,参考 `src/pages/system/user/index.tsx`
- 日期范围查询使用 `formatDateTimeRange``addDateRange`,参考 `system/user`
- 日期范围查询优先使用 `useDateRangeQuery`,底层通过 `addDateRange` 写入 `params.beginXxx/endXxx`
- 字典 options 优先使用 `dictOptions(dicts.xxx)`
- 导入上传参考 `system/user/components/UserImportModal.tsx` 或流程定义导入弹窗,保留 `globalHeaders()``appEnv.baseApi` 相关方式。
## 字典、权限与公共工具
- 字典使用 `const dicts = useDict('sys_normal_disable', 'sys_user_gender')`
- 字典 options 可按 `system/user` 中的 `dictOptions` 函数映射为 `{ label, value }`
- 字典 options 使用 `dictOptions` from `@/utils/dict` 映射为 `{ label, value }`
- 权限使用:
`const userInfo = useUserStore(state => state.userInfo);`
`const canAdd = hasPermi(userInfo, ['system:user:add']);`
@ -96,6 +98,8 @@
- 常用工具:
`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`
## 组件与样式规则
@ -104,6 +108,9 @@
- 树筛选优先复用 `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` 等类。
- 不要为了单页需求修改全局组件样式。
@ -112,22 +119,25 @@
- 树表列表接口通常返回数组,页面通过 `handleTree<T>(res.data || [], 'id', 'parentId')` 组树。
- `ProTable` 使用 `pagination={false}`
- 使用 `expandedRowKeys` 和 `onExpandedRowsChange` 控制展开
- 使用 `useTreeTableExpand` 控制 `expandedRowKeys`、`onExpandedRowsChange`、`syncExpandedRows`、`toggleExpandAll`
- 展开/折叠按钮参考 `demo/tree``workflow/category`,用 `SortAscendingOutlined`
- 表单中上级节点使用 `ProFormTreeSelect`
- 新增子节点时从当前行回填 `parentId`
- 删除确认文案优先使用业务名称,而不是批量 ID 文案。
## 与 Vue 参考项目和生成器模板的关系
## 与 gen 模板、Vue 参考项目和后端生成器的关系
- 当前仓库 `gen/*.vm` 是 React 版内置代码生成模板,是维护生成能力时的第一参考。
- `gen/index.vue.vm`、`gen/index-tree.vue.vm` 文件名为兼容后端 generator 保持不变,但内容必须生成 React TSX 页面。
- 修改公共 hooks/工具后,如果它们能简化标准生成页,要同步评估 `gen/*.vm` 是否需要更新。
- Vue skill 和 `.claude/agents` 提供的是任务分型、优先级、增量修改、自检方式,不是 React 实现模板。
- generator 模板可用于确认字段、权限、导出、状态切换、排序、日期范围等,但不是最终答案。
- boot4 后端 generator 模板可用于确认字段、权限、导出、状态切换、排序、日期范围等,但不是最终答案。
- 当前 React 项目的核心骨架是 `ProTable`、`ModalForm`、`RowActions`、`useTableSelection`、`useTableExport`。
- 修改已有页面时,不要把现有强业务逻辑替换回 generator 的简化逻辑。
## 验证规则
- 只改文档或 skill运行 skill 基础校验
- 只改文档、skill 或 `gen/*.vm` 模板:至少运行 skill 基础校验或 `git diff --check`;如果模板改动依赖项目工具,优先再跑 `pnpm lint`
- 改前端 TS/TSX/API/types优先运行 `pnpm exec tsc --noEmit``pnpm lint`
- 改页面、import、权限或较多文件运行 `pnpm lint`
- 改公共 hooks、组件、构建相关或大范围页面再运行 `pnpm build`

94
gen/api.ts.vm Normal file
View File

@ -0,0 +1,94 @@
import type { #if(!$table.tree)PageResult, #end R } from '@/api/types';
import request from '@/api/request';
import type { ${BusinessName}Form, ${BusinessName}Query, ${BusinessName}VO } from './types';
/**
* 查询${functionName}列表
*/
export function list${BusinessName}(query?: ${BusinessName}Query) {
return request<R<#if($table.tree)${BusinessName}VO[]#else PageResult<${BusinessName}VO>#end>>({
url: '/${moduleName}/${businessName}/list',
method: 'get',
params: query
});
}
/**
* 查询${functionName}详细
*/
export function get${BusinessName}(${pkColumn.javaField}: string | number) {
return request<R<${BusinessName}VO>>({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'get'
});
}
/**
* 新增${functionName}
*/
export function add${BusinessName}(data: ${BusinessName}Form) {
return request<R>({
url: '/${moduleName}/${businessName}',
method: 'post',
data
});
}
/**
* 修改${functionName}
*/
export function update${BusinessName}(data: ${BusinessName}Form) {
return request<R>({
url: '/${moduleName}/${businessName}',
method: 'put',
data
});
}
#if($enableStatus)
/**
* 修改${functionName}状态
*/
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#end
) {
return request<R>({
url: '/${moduleName}/${businessName}/changeStatus',
method: 'put',
data: {
${pkColumn.javaField},
${statusField}
}
});
}
#end
#if($enableSort)
/**
* 调整${functionName}排序
*/
export function update${BusinessName}Sort(
${pkColumn.javaField}: string | number,
${sortField}: #if($sortColumn.javaType == 'LocalDateTime' || $sortColumn.javaType == 'String')string#else number#end
) {
return request<R>({
url: '/${moduleName}/${businessName}/updateSort',
method: 'put',
data: {
${pkColumn.javaField},
${sortField}
}
});
}
#end
/**
* 删除${functionName}
*/
export function del${BusinessName}(${pkColumn.javaField}: string | number | Array<string | number>) {
return request<R>({
url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'delete'
});
}

472
gen/index-tree.vue.vm Normal file
View File

@ -0,0 +1,472 @@
#set($needDict = $dicts != '')
#set($needImagePreview = false)
#set($needImageUpload = false)
#set($needFileUpload = false)
#set($needEditor = false)
#set($needCheckbox = false)
#set($needSelect = false)
#set($needTextArea = false)
#set($needDigit = false)
#set($needDateField = false)
#set($needSwitchField = false)
#foreach($column in $columns)
#if($column.list && $column.htmlType == "imageUpload")
#set($needImagePreview = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "imageUpload")
#set($needImageUpload = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "fileUpload")
#set($needFileUpload = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "editor")
#set($needEditor = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "checkbox")
#set($needCheckbox = true)
#end
#if(($column.insert || $column.edit || $column.query) && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "switch"))
#set($needSelect = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "textarea")
#set($needTextArea = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "inputNumber")
#set($needDigit = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "datetime")
#set($needDateField = true)
#end
#if(($column.insert || $column.edit || $column.list) && $column.htmlType == "switch")
#set($needSwitchField = true)
#end
#end
#macro(comment $column)#set($idx=$column.columnComment.indexOf(""))#if($idx != -1)$column.columnComment.substring(0, $idx)#else$column.columnComment#end#end
import { DeleteOutlined#if($enableExport), DownloadOutlined#end, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
#if($needCheckbox)
ProFormCheckbox,
#end
#if($needDateField)
ProFormDateTimePicker,
#end
#if($needDigit)
ProFormDigit,
#end
#if($needSelect)
ProFormSelect,
#end
#if($needTextArea)
ProFormTextArea,
#end
ProFormText,
ProFormTreeSelect,
ProTable,
type ActionType,
type ProColumns
} from '@ant-design/pro-components';
import { useBoolean } from 'ahooks';
import { Button, Form, message#if($enableStatus || $needSwitchField), Switch#end#if($enableSort), InputNumber#end } 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,
#end
del${BusinessName},
get${BusinessName},
list${BusinessName},
#if($enableSort)
update${BusinessName}Sort,
#end
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
#if($needDict)
import DictTag from '@/components/common/DictTag';
#end
#if($needFileUpload)
import FileUpload from '@/components/common/FileUpload';
#end
#if($needImagePreview)
import ImagePreview from '@/components/common/ImagePreview';
#end
#if($needImageUpload)
import ImageUpload from '@/components/common/ImageUpload';
#end
#if($needEditor)
import RichTextEditor from '@/components/common/RichTextEditor';
#end
import RowActions from '@/components/common/RowActions';
#if($needDict)
import { useDict } from '@/hooks/useDict';
#end
#if($enableExport)
import { useTableExport } from '@/hooks/useTableExport';
#end
import { useTreeTableExpand } from '@/hooks/useTreeTableExpand';
import { useUserStore } from '@/stores/userStore';
#if($needDict)
import { dictOptions } from '@/utils/dict';
#end
#if($enableStatus)
import { confirmAction } from '@/utils/modal';
#end
import { hasPermi } from '@/utils/permission';
import { filterTree#if($needDateField), formatDateTimeFields#end, handleTree#if($needDateField), toDayjsFields#end } from '@/utils/ruoyi';
const default${BusinessName}Form: ${BusinessName}Form = {
${treeParentCode}: ${treeRootValueTsLiteral},
#foreach($column in $columns)
#if(($column.insert || $column.edit) && !$column.pk && $column.htmlType == "checkbox")
${column.javaField}: [],
#end
#end
};
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
}));
}
#macro(switchActiveValue $column)#if($column.javaType == "Boolean")true#elseif($column.javaType == "Integer" || $column.javaType == "Long")0#else'0'#end#end
#macro(switchInactiveValue $column)#if($column.javaType == "Boolean")false#elseif($column.javaType == "Integer" || $column.javaType == "Long")1#else'1'#end#end
#if($enableStatus)
const ${statusField}ActiveValue = #if($statusColumn.javaType == "Boolean")true#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")0#else'0'#end;
const ${statusField}InactiveValue = #if($statusColumn.javaType == "Boolean")false#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")1#else'1'#end;
#end
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});
#end
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();
#end
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']);
#end
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 }, [
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "datetime")
'${column.javaField}',
#end
#end
])#else{ ...res.data }#end;
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "checkbox")
if (typeof data.${column.javaField} === 'string') {
data.${column.javaField} = data.${column.javaField}.split(',');
}
#end
#end
form.resetFields();
form.setFieldsValue(data);
setModalTitle('修改${functionName}');
openModal();
};
const submitForm = async (values: ${BusinessName}Form) => {
const submitValues = #if($needDateField)formatDateTimeFields({ ...values }, [
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "datetime")
'${column.javaField}',
#end
#end
])#else{ ...values }#end;
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "checkbox")
if (Array.isArray(submitValues.${column.javaField})) {
submitValues.${column.javaField} = submitValues.${column.javaField}.join(',');
}
#end
#end
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(`确认要"${text}"吗?`);
await change${BusinessName}Status(row.${pkColumn.javaField}, status);
message.success(`${text}成功`);
actionRef.current?.reload();
} catch {
row.${statusField} = previousStatus;
actionRef.current?.reload();
}
};
#end
#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();
};
#end
const columns: ProColumns<${BusinessName}VO>[] = [
#foreach($column in $columns)
#set($javaField=$column.javaField)
#set($dictType=$column.dictType)
#if($column.list && !$column.pk)
#if($enableStatus && $statusField == $javaField)
{
title: '#comment($column)',
dataIndex: '${javaField}',
valueType: 'select',
width: 100,
#if("" != $dictType)
fieldProps: { options: dictOptions(dicts.${dictType}) },
#end
render: (_, row) => (
<Switch
checked={row.${javaField} === ${statusField}ActiveValue}
disabled={!canEdit}
onChange={checked => handleStatusChange(row, checked)}
/>
)
},
#elseif($enableSort && $sortField == $javaField)
{
title: '#comment($column)',
dataIndex: '${javaField}',
search: false,
width: 130,
render: (_, row) => <InputNumber min={0} value={row.${javaField} as number} onChange={value => handleSortChange(row, value)} />
},
#elseif($column.htmlType == "datetime")
{ title: '#comment($column)', dataIndex: '${javaField}', valueType: 'dateTime', search: #if($column.query)true#else false#end, width: 170 },
#elseif($column.htmlType == "imageUpload")
{
title: '#comment($column)',
dataIndex: '${javaField}Url',
search: false,
width: 100,
render: (_, row) => <ImagePreview src={row.${javaField}Url || String(row.${javaField} || '')} width={50} height={50} />
},
#elseif($column.dictColumn)
{
title: '#comment($column)',
dataIndex: '${javaField}',
valueType: 'select',
fieldProps: { options: dictOptions(dicts.${dictType}) },
render: (_, row) => <DictTag options={dicts.${dictType}} value={row.${javaField}} />
},
#elseif($column.htmlType == "switch")
{
title: '#comment($column)',
dataIndex: '${javaField}',
valueType: 'select',
width: 100,
render: (_, row) => <Switch checked={row.${javaField} === #switchActiveValue($column)} disabled />
},
#else
{ title: '#comment($column)', dataIndex: '${javaField}'#if(!$column.query), search: false#end },
#end
#end
#end
{
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}编号为"#[[${row.]]#${pkColumn.javaField}#[[}]]#"的数据项?`,
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);
#end
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}_#[[${Date.now()}]]#.xlsx`)}
>
导出
</Button>
)#end
]}
/>
<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 />
#foreach($column in $columns)
#if(($column.insert || $column.edit) && !$column.pk)
#set($field=$column.javaField)
#set($dictType=$column.dictType)
#if($field == $treeParentCode)
<ProFormTreeSelect
name="${field}"
label="#comment($column)"
#if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end
fieldProps={{
allowClear: true,
treeDefaultExpandAll: true,
treeData: treeSelectData,
placeholder: '请选择#comment($column)'
}}
/>
#elseif($column.htmlType == "input")
<ProFormText name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "textarea")
<ProFormTextArea name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "inputNumber")
<ProFormDigit name="${field}" label="#comment($column)" min={0} #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif(($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "switch") && "" != $dictType)
<ProFormSelect name="${field}" label="#comment($column)" options={dictOptions(dicts.${dictType})} #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "checkbox" && "" != $dictType)
<ProFormCheckbox.Group name="${field}" label="#comment($column)" options={dictOptions(dicts.${dictType})} #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "switch")
<Form.Item
name="${field}"
label="#comment($column)"
valuePropName="checked"
getValueProps={value => ({ checked: value === #switchActiveValue($column) })}
normalize={checked => (checked ? #switchActiveValue($column) : #switchInactiveValue($column))}
>
<Switch />
</Form.Item>
#elseif($column.htmlType == "datetime")
<ProFormDateTimePicker name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "imageUpload")
<Form.Item name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end>
<ImageUpload />
</Form.Item>
#elseif($column.htmlType == "fileUpload")
<Form.Item name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end>
<FileUpload />
</Form.Item>
#elseif($column.htmlType == "editor")
<Form.Item name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end>
<RichTextEditor />
</Form.Item>
#else
<ProFormText name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#end
#end
#end
</ModalForm>
</PageContainer>
);
}

460
gen/index.vue.vm Normal file
View File

@ -0,0 +1,460 @@
#set($needDict = $dicts != '')
#set($needDateRange = false)
#set($needImagePreview = false)
#set($needImageUpload = false)
#set($needFileUpload = false)
#set($needEditor = false)
#set($needCheckbox = false)
#set($needSelect = false)
#set($needTextArea = false)
#set($needDigit = false)
#set($needDateField = false)
#set($needSwitchField = false)
#foreach($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($needDateRange = true)
#end
#if($column.list && $column.htmlType == "imageUpload")
#set($needImagePreview = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "imageUpload")
#set($needImageUpload = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "fileUpload")
#set($needFileUpload = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "editor")
#set($needEditor = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "checkbox")
#set($needCheckbox = true)
#end
#if(($column.insert || $column.edit || $column.query) && ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "switch"))
#set($needSelect = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "textarea")
#set($needTextArea = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "inputNumber")
#set($needDigit = true)
#end
#if(($column.insert || $column.edit) && $column.htmlType == "datetime")
#set($needDateField = true)
#end
#if(($column.insert || $column.edit || $column.list) && $column.htmlType == "switch")
#set($needSwitchField = true)
#end
#end
#macro(comment $column)#set($idx=$column.columnComment.indexOf(""))#if($idx != -1)$column.columnComment.substring(0, $idx)#else$column.columnComment#end#end
import { DeleteOutlined#if($enableExport), DownloadOutlined#end, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
#if($needCheckbox)
ProFormCheckbox,
#end
#if($needDateField)
ProFormDateTimePicker,
#end
#if($needDigit)
ProFormDigit,
#end
#if($needSelect)
ProFormSelect,
#end
#if($needTextArea)
ProFormTextArea,
#end
ProFormText,
ProTable,
type ActionType,
type ProColumns
} from '@ant-design/pro-components';
import { useBoolean } from 'ahooks';
import { Button, Form, message, Popconfirm#if($enableStatus || $needSwitchField), Switch#end#if($enableSort), InputNumber#end } 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,
#end
del${BusinessName},
get${BusinessName},
list${BusinessName},
#if($enableSort)
update${BusinessName}Sort,
#end
update${BusinessName}
} from '@/api/${moduleName}/${businessName}';
#if($needDict)
import DictTag from '@/components/common/DictTag';
#end
#if($needFileUpload)
import FileUpload from '@/components/common/FileUpload';
#end
#if($needImagePreview)
import ImagePreview from '@/components/common/ImagePreview';
#end
#if($needImageUpload)
import ImageUpload from '@/components/common/ImageUpload';
#end
#if($needEditor)
import RichTextEditor from '@/components/common/RichTextEditor';
#end
import RowActions from '@/components/common/RowActions';
#if($needDict)
import { useDict } from '@/hooks/useDict';
#end
#if($needDateRange)
import { useDateRangeQuery } from '@/hooks/useDateRangeQuery';
#end
#if($enableExport)
import { useTableExport } from '@/hooks/useTableExport';
#end
import { useTableSelection } from '@/hooks/useTableSelection';
import { useUserStore } from '@/stores/userStore';
#if($needDict)
import { dictOptions } from '@/utils/dict';
#end
#if($enableStatus)
import { confirmAction } from '@/utils/modal';
#end
import { hasPermi } from '@/utils/permission';
import { #if($needDateField)formatDateTimeFields, toDayjsFields, #end toPageQuery, toTableData } from '@/utils/ruoyi';
const default${BusinessName}Form: ${BusinessName}Form = {
#foreach($column in $columns)
#if(($column.insert || $column.edit) && !$column.pk && $column.htmlType == "checkbox")
${column.javaField}: [],
#end
#end
};
#macro(switchActiveValue $column)#if($column.javaType == "Boolean")true#elseif($column.javaType == "Integer" || $column.javaType == "Long")0#else'0'#end#end
#macro(switchInactiveValue $column)#if($column.javaType == "Boolean")false#elseif($column.javaType == "Integer" || $column.javaType == "Long")1#else'1'#end#end
#if($enableStatus)
const ${statusField}ActiveValue = #if($statusColumn.javaType == "Boolean")true#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")0#else'0'#end;
const ${statusField}InactiveValue = #if($statusColumn.javaType == "Boolean")false#elseif($statusColumn.javaType == "Integer" || $statusColumn.javaType == "Long")1#else'1'#end;
#end
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});
#end
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();
#end
#foreach($column in $columns)
#if($column.query && $column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
const { applyDateRange: apply${AttrName}DateRange } = useDateRangeQuery('${AttrName}');
#end
#end
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']);
#end
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 }, [
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "datetime")
'${column.javaField}',
#end
#end
])#else{ ...res.data }#end;
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "checkbox")
if (typeof data.${column.javaField} === 'string') {
data.${column.javaField} = data.${column.javaField}.split(',');
}
#end
#end
form.resetFields();
form.setFieldsValue(data);
setModalTitle('修改${functionName}');
openModal();
};
const submitForm = async (values: ${BusinessName}Form) => {
const submitValues = #if($needDateField)formatDateTimeFields({ ...values }, [
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "datetime")
'${column.javaField}',
#end
#end
])#else{ ...values }#end;
#foreach($column in $columns)
#if(($column.insert || $column.edit) && $column.htmlType == "checkbox")
if (Array.isArray(submitValues.${column.javaField})) {
submitValues.${column.javaField} = submitValues.${column.javaField}.join(',');
}
#end
#end
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(`确认要"${text}"吗?`);
await change${BusinessName}Status(row.${pkColumn.javaField}, status);
message.success(`${text}成功`);
actionRef.current?.reload();
} catch {
row.${statusField} = previousStatus;
actionRef.current?.reload();
}
};
#end
#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();
};
#end
const columns: ProColumns<${BusinessName}VO>[] = [
#foreach($column in $columns)
#set($javaField=$column.javaField)
#set($dictType=$column.dictType)
#if($column.list)
#if($enableStatus && $statusField == $javaField)
{
title: '#comment($column)',
dataIndex: '${javaField}',
valueType: 'select',
width: 100,
#if("" != $dictType)
fieldProps: { options: dictOptions(dicts.${dictType}) },
#end
render: (_, row) => (
<Switch
checked={row.${javaField} === ${statusField}ActiveValue}
disabled={!canEdit}
onChange={checked => handleStatusChange(row, checked)}
/>
)
},
#elseif($enableSort && $sortField == $javaField)
{
title: '#comment($column)',
dataIndex: '${javaField}',
search: false,
width: 130,
render: (_, row) => <InputNumber min={0} value={row.${javaField} as number} onChange={value => handleSortChange(row, value)} />
},
#elseif($column.htmlType == "datetime")
{ title: '#comment($column)', dataIndex: '${javaField}', valueType: 'dateTime', search: #if($column.query && $column.queryType != "BETWEEN")true#else false#end, width: 170 },
#elseif($column.htmlType == "imageUpload")
{
title: '#comment($column)',
dataIndex: '${javaField}Url',
search: false,
width: 100,
render: (_, row) => <ImagePreview src={row.${javaField}Url || String(row.${javaField} || '')} width={50} height={50} />
},
#elseif($column.dictColumn)
{
title: '#comment($column)',
dataIndex: '${javaField}',
valueType: 'select',
fieldProps: { options: dictOptions(dicts.${dictType}) },
render: (_, row) => <DictTag options={dicts.${dictType}} value={row.${javaField}} />
},
#elseif($column.htmlType == "switch")
{
title: '#comment($column)',
dataIndex: '${javaField}',
valueType: 'select',
width: 100,
render: (_, row) => <Switch checked={row.${javaField} === #switchActiveValue($column)} disabled />
},
#else
{ title: '#comment($column)', dataIndex: '${javaField}'#if(!$column.query), search: false#end },
#end
#end
#if($column.query && $column.htmlType == "datetime" && $column.queryType == "BETWEEN")
{ title: '#comment($column)', dataIndex: '${javaField}Range', valueType: 'dateTimeRange', hideInTable: true },
#end
#end
{
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}编号为"#[[${row.]]#${pkColumn.javaField}#[[}]]#"的数据项?`,
onClick: () => remove(row)
}
]}
/>
)
}
];
return (
<PageContainer title="${functionName}">
<ProTable<${BusinessName}VO, ${BusinessName}Query#if($needDateRange) & Record<string, unknown>#end>
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);
#foreach($column in $columns)
#if($column.query && $column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
query = apply${AttrName}DateRange(query, params.${column.javaField}Range as [string, string] | undefined);
delete query.${column.javaField}Range;
#end
#end
#else
const query = toPageQuery(params);
#end
#if($enableExport)
updateExportParams(query);
#end
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}编号为"#[[${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}_#[[${Date.now()}]]#.xlsx`)}
>
导出
</Button>
)#end
]}
/>
<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 />
#foreach($column in $columns)
#if(($column.insert || $column.edit) && !$column.pk)
#set($field=$column.javaField)
#set($dictType=$column.dictType)
#if($column.htmlType == "input")
<ProFormText name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "textarea")
<ProFormTextArea name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "inputNumber")
<ProFormDigit name="${field}" label="#comment($column)" min={0} #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif(($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "switch") && "" != $dictType)
<ProFormSelect name="${field}" label="#comment($column)" options={dictOptions(dicts.${dictType})} #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "checkbox" && "" != $dictType)
<ProFormCheckbox.Group name="${field}" label="#comment($column)" options={dictOptions(dicts.${dictType})} #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "switch")
<Form.Item
name="${field}"
label="#comment($column)"
valuePropName="checked"
getValueProps={value => ({ checked: value === #switchActiveValue($column) })}
normalize={checked => (checked ? #switchActiveValue($column) : #switchInactiveValue($column))}
>
<Switch />
</Form.Item>
#elseif($column.htmlType == "datetime")
<ProFormDateTimePicker name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#elseif($column.htmlType == "imageUpload")
<Form.Item name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end>
<ImageUpload />
</Form.Item>
#elseif($column.htmlType == "fileUpload")
<Form.Item name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end>
<FileUpload />
</Form.Item>
#elseif($column.htmlType == "editor")
<Form.Item name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end>
<RichTextEditor />
</Form.Item>
#else
<ProFormText name="${field}" label="#comment($column)" #if($column.required)rules={[{ required: true, message: '#comment($column)不能为空' }]}#end />
#end
#end
#end
</ModalForm>
</PageContainer>
);
}

56
gen/types.ts.vm Normal file
View File

@ -0,0 +1,56 @@
import type { BaseEntity#if(!$table.tree), PageQuery#end } from '@/api/types';
#macro(tsType $column)#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1)string | number#elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal')number#elseif($column.javaType == 'Boolean')boolean#else string#end#end
export interface ${BusinessName}VO {
#foreach ($column in $columns)
#if($column.list || $column.pk)
/**
* $column.columnComment
*/
$column.javaField: #tsType($column);
#if($column.htmlType == "imageUpload")
/**
* ${column.columnComment}Url
*/
${column.javaField}Url?: string;
#end
#end
#end
#if($table.tree)
/**
* 子对象
*/
children?: ${BusinessName}VO[];
#end
}
export interface ${BusinessName}Form extends BaseEntity {
#foreach ($column in $columns)
#if($column.insert || $column.edit || $column.pk)
/**
* $column.columnComment
*/
#if($column.htmlType == "checkbox")
$column.javaField?: string | string[];
#else
$column.javaField?: #tsType($column);
#end
#end
#end
}
export interface ${BusinessName}Query#if(!$table.tree) extends PageQuery#end {
#foreach ($column in $columns)
#if($column.query)
/**
* $column.columnComment
*/
$column.javaField?: #tsType($column);
#end
#end
/**
* 日期范围参数
*/
params?: Record<string, unknown>;
}