Răsfoiți Sursa

新增前端代码生成功能

Wayne 4 luni în urmă
părinte
comite
01256d2fb6

+ 0 - 5
ruoyi-admin/pom.xml

@@ -110,11 +110,6 @@
             <scope>test</scope>
         </dependency>
 
-        <dependency>
-            <groupId>org.dromara</groupId>
-            <artifactId>ruoyi-subject</artifactId>
-        </dependency>
-
         <!-- skywalking 整合 logback -->
 <!--        <dependency>-->
 <!--            <groupId>org.apache.skywalking</groupId>-->

+ 60 - 0
ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/util/VelocityUtils.java

@@ -3,6 +3,7 @@ package org.dromara.generator.util;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.lang.Dict;
+import cn.hutool.core.util.ObjectUtil;
 import org.dromara.generator.constant.GenConstants;
 import org.dromara.common.core.utils.DateUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -74,6 +75,18 @@ public class VelocityUtils {
         if (GenConstants.TPL_TREE.equals(tplCategory)) {
             setTreeVelocityContext(velocityContext, genTable);
         }
+        // 判断是modal还是drawer
+        Dict paramsObj = JsonUtils.parseMap(genTable.getOptions());
+        if (ObjectUtil.isNotNull(paramsObj)) {
+            String popupComponent = Optional
+                .ofNullable(paramsObj.getStr("popupComponent"))
+                .orElse("modal");
+            velocityContext.put("popupComponent", popupComponent);
+            velocityContext.put("PopupComponent", StringUtils.capitalize(popupComponent));
+        } else {
+            velocityContext.put("popupComponent", "modal");
+            velocityContext.put("PopupComponent", "Modal");
+        }
         return velocityContext;
     }
 
@@ -134,6 +147,21 @@ public class VelocityUtils {
         } else if (GenConstants.TPL_TREE.equals(tplCategory)) {
             templates.add("vm/vue/index-tree.vue.vm");
         }
+
+        /**
+         * 添加vben5
+         */
+        templates.add("vm/vben5/api/index.ts.vm");
+        templates.add("vm/vben5/api/model.d.ts.vm");
+        templates.add("vm/vben5/views/data.ts.vm");
+        if (GenConstants.TPL_CRUD.equals(tplCategory)) {
+            templates.add("vm/vben5/views/index_vben.vue.vm");
+            templates.add("vm/vben5/views/popup.vue.vm");
+        } else if (GenConstants.TPL_TREE.equals(tplCategory)) {
+            templates.add("vm/vben5/views/index_vben_tree.vue.vm");
+            templates.add("vm/vben5/views/popup_tree.vue.vm");
+        }
+
         return templates;
     }
 
@@ -186,6 +214,38 @@ public class VelocityUtils {
         } else if (template.contains("index-tree.vue.vm")) {
             fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName);
         }
+
+        // 判断是modal还是drawer
+        Dict paramsObj = JsonUtils.parseMap(genTable.getOptions());
+        String popupComponent = "modal";
+        if (ObjectUtil.isNotNull(paramsObj)) {
+            popupComponent = Optional
+                .ofNullable(paramsObj.getStr("popupComponent"))
+                .orElse("modal");
+        }
+        String vben5Path = "vben5";
+        if (template.contains("vm/vben5/api/index.ts.vm")) {
+            fileName = StringUtils.format("{}/api/{}/{}/index.ts", vben5Path, moduleName, businessName);
+        }
+        if (template.contains("vm/vben5/api/model.d.ts.vm")) {
+            fileName = StringUtils.format("{}/api/{}/{}/model.d.ts", vben5Path, moduleName, businessName);
+        }
+        if (template.contains("vm/vben5/views/index_vben.vue.vm")) {
+            fileName = StringUtils.format("{}/views/{}/{}/index.vue", vben5Path, moduleName, businessName);
+        }
+        if (template.contains("vm/vben5/views/index_vben_tree.vue.vm")) {
+            fileName = StringUtils.format("{}/views/{}/{}/index.vue", vben5Path, moduleName, businessName);
+        }
+        if (template.contains("vm/vben5/views/data.ts.vm")) {
+            fileName = StringUtils.format("{}/views/{}/{}/data.ts", vben5Path, moduleName, businessName);
+        }
+        if (template.contains("vm/vben5/views/popup.vue.vm")) {
+            fileName = StringUtils.format("{}/views/{}/{}/{}-{}.vue", vben5Path, moduleName, businessName, businessName, popupComponent);
+        }
+        if (template.contains("vm/vben5/views/popup_tree.vue.vm")) {
+            fileName = StringUtils.format("{}/views/{}/{}/{}-{}.vue", vben5Path, moduleName, businessName, businessName, popupComponent);
+        }
+
         return fileName;
     }
 

+ 69 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/api/index.ts.vm

@@ -0,0 +1,69 @@
+import type { ${BusinessName}VO, ${BusinessName}Form, ${BusinessName}Query } from './model';
+
+import type { ID, IDS } from '#/api/common';
+#if($tplCategory != 'tree')
+import type { PageResult } from '#/api/common';
+#end
+
+import { commonExport } from '#/api/helper';
+import { requestClient } from '#/api/request';
+
+/**
+* 查询${functionName}列表
+* @param params
+* @returns ${functionName}列表
+*/
+export function ${businessName}List(params?: ${BusinessName}Query) {
+  #if($tplCategory != 'tree')
+  return requestClient.get<PageResult<${BusinessName}VO>>('/${moduleName}/${businessName}/list', { params });
+  #else
+  return requestClient.get<${BusinessName}VO[]>(`/${moduleName}/${businessName}/list`, { params });
+  #end
+}
+
+#if($tplCategory != 'tree')
+/**
+ * 导出${functionName}列表
+ * @param params
+ * @returns ${functionName}列表
+ */
+export function ${businessName}Export(params?: ${BusinessName}Query) {
+  return commonExport('/${moduleName}/${businessName}/export', params ?? {});
+}
+#end
+
+/**
+ * 查询${functionName}详情
+ * @param ${pkColumn.javaField} id
+ * @returns ${functionName}详情
+ */
+export function ${businessName}Info(${pkColumn.javaField}: ID) {
+  return requestClient.get<${BusinessName}VO>(`/${moduleName}/${businessName}/${${pkColumn.javaField}}`);
+}
+
+/**
+ * 新增${functionName}
+ * @param data
+ * @returns void
+ */
+export function ${businessName}Add(data: ${BusinessName}Form) {
+  return requestClient.postWithMsg<void>('/${moduleName}/${businessName}', data);
+}
+
+/**
+ * 更新${functionName}
+ * @param data
+ * @returns void
+ */
+export function ${businessName}Update(data: ${BusinessName}Form) {
+  return requestClient.putWithMsg<void>('/${moduleName}/${businessName}', data);
+}
+
+/**
+ * 删除${functionName}
+ * @param ${pkColumn.javaField} id
+ * @returns void
+ */
+export function ${businessName}Remove(${pkColumn.javaField}: ID | IDS) {
+  return requestClient.deleteWithMsg<void>(`/${moduleName}/${businessName}/${${pkColumn.javaField}}`);
+}

+ 56 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/api/model.d.ts.vm

@@ -0,0 +1,56 @@
+import type { PageQuery, BaseEntity } from '#/api/common';
+
+export interface ${BusinessName}VO {
+#foreach ($column in $columns)
+#if($column.list)
+  /**
+   * $column.columnComment
+   */
+  $column.javaField:#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
+#end
+#if ($table.tree)
+  /**
+    * 子对象
+    */
+  children: ${BusinessName}VO[];
+#end
+}
+
+export interface ${BusinessName}Form extends BaseEntity {
+#foreach ($column in $columns)
+#if($column.insert || $column.edit)
+  /**
+   * $column.columnComment
+   */
+  $column.javaField?:#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
+#end
+}
+
+export interface ${BusinessName}Query #if(!${treeCode})extends PageQuery #end{
+#foreach ($column in $columns)
+#if($column.query)
+  /**
+   * $column.columnComment
+   */
+  $column.javaField?:#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
+#end
+  /**
+    * 日期范围参数
+    */
+  params?: any;
+}

+ 217 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/views/data.ts.vm

@@ -0,0 +1,217 @@
+import type { FormSchemaGetter } from '#/adapter/form';
+import type { VxeGridProps } from '#/adapter/vxe-table';
+
+#if(${dicts} != '')
+import { getDictOptions } from '#/utils/dict';
+import { renderDict } from '#/utils/render';
+#end
+
+export const querySchema: FormSchemaGetter = () => [
+  #foreach($column in $columns)
+    #if($column.query)
+      #if($column.dictType)
+        #set($dictType=$column.dictType)
+      #else
+        #set($dictType="")
+      #end
+      #set($parentheseIndex=$column.columnComment.indexOf("("))
+      #if($parentheseIndex != -1)
+        #set($comment=$column.columnComment.substring(0, $parentheseIndex))
+      #else
+        #set($comment=$column.columnComment)
+      #end
+      #if($column.htmlType == "input")
+        #set($component="Input")
+      #elseif($column.htmlType == "textarea")
+        #set($component="Textarea")
+      #elseif($column.htmlType == "select")
+        #set($component="Select")
+      #elseif($column.htmlType == "radio")
+        #set($component="RadioGroup")
+      #elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
+        #set($component="DatePicker")
+      #elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+        #set($component="RangePicker")
+      #else
+        #set($component="Input")
+      #end
+  {
+    component: '${component}',
+    #if($component == "Select" || $component == "RadioGroup")
+    componentProps: {
+      #if($dictType != "")
+      // 可选从DictEnum中获取 DictEnum.${dictType.toUpperCase()} 便于维护
+      options: getDictOptions('$dictType'),
+      #end
+      #if($component == "RadioGroup")
+      buttonStyle: 'solid',
+      optionType: 'button',
+      #end
+    },
+    #elseif($component == "DatePicker" || $component == "RangePicker")
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    #end
+    fieldName: '${column.javaField}',
+    label: '${comment}',
+  },
+    #end
+  #end
+];
+
+export const columns: VxeGridProps['columns'] = [
+  #if($tplCategory != 'tree')
+  { type: 'checkbox', width: 60 },
+  #end
+  #foreach($column in $columns)
+    #if($column.list)
+      #if($column.dictType)
+        #set($dictType=$column.dictType)
+      #else
+        #set($dictType="")
+      #end
+      #set($parentheseIndex=$column.columnComment.indexOf("("))
+      #if($parentheseIndex != -1)
+        #set($comment=$column.columnComment.substring(0, $parentheseIndex))
+      #else
+        #set($comment=$column.columnComment)
+      #end
+  {
+    title: '${comment}',
+    field: '${column.javaField}',
+    #if( $foreach.count == 1 && $tplCategory == 'tree')
+    treeNode: true,
+    #end
+    #if($dictType != "")
+    slots: {
+      default: ({ row }) => {
+        // 可选从DictEnum中获取 DictEnum.${dictType.toUpperCase()} 便于维护
+        return renderDict(row.${column.javaField}, '$dictType');
+      },
+    },
+    #end
+  },
+    #end
+  #end
+  {
+    field: 'action',
+    fixed: 'right',
+    slots: { default: 'action' },
+    title: '操作',
+    width: 180,
+  },
+];
+
+export const ${popupComponent}Schema: FormSchemaGetter = () => [
+  #foreach($column in $columns)
+    #if($column.edit)
+      #if($column.dictType)
+        #set($dictType=$column.dictType)
+      #else
+        #set($dictType="")
+      #end
+      #set($parentheseIndex=$column.columnComment.indexOf("("))
+      #if($parentheseIndex != -1)
+        #set($comment=$column.columnComment.substring(0, $parentheseIndex))
+      #else
+        #set($comment=$column.columnComment)
+      #end
+      #if($column.htmlType == "input")
+        #set($component="Input")
+      #elseif($column.htmlType == "textarea")
+        #set($component="Textarea")
+      #elseif($column.htmlType == "select")
+        #set($component="Select")
+      #elseif($column.htmlType == "radio")
+        #set($component="RadioGroup")
+      #elseif($column.htmlType == "checkbox")
+        #set($component="Checkbox")
+      #elseif($column.htmlType == "imageUpload")
+        #set($component="ImageUpload")
+      #elseif($column.htmlType == "fileUpload")
+        #set($component="FileUpload")
+      #elseif($column.htmlType == "editor")
+        #set($component="RichTextarea")
+      #elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
+        #set($component="DatePicker")
+      #elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
+        #set($component="RangePicker")
+      #else
+        #set($component="Input")
+      #end
+      #if($column.required && $column.pk == false)
+        #set($required='true')
+      #else
+        #set($required='false')
+      #end
+  {
+    label: '${comment}',
+    fieldName: '${column.javaField}',
+    #if("" != $treeParentCode && $column.javaField == $treeParentCode)
+    component: 'TreeSelect',
+    #else
+    component: '${component}',
+    #end
+    #if($component == "DatePicker" || $component == "RangePicker")
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    #elseif($component == "ImageUpload")
+    componentProps: {
+      // accept: ['jpg'], // 不支持type/*的写法 支持拓展名(不带.) 文件头(image/png这种)
+      // maxNumber: 1, // 最大上传文件数 默认为1 为1会绑定为string而非string[]类型
+      // resultField: 'url', // 上传成功后返回的字段名 默认url 可选['ossId', 'url', 'fileName']
+    },
+    #elseif($component == "FileUpload")
+    /**
+    * 注意这里获取为数组 需要自行定义回显/提交
+    * 文件上传还在demo阶段 可能有重大改动!
+    */
+    componentProps: {
+      // accept: ['xlsx'], // 不支持type/*的写法 建议使用拓展名(不带.)
+      // maxNumber: 1, // 最大上传文件数
+      // resultField: 'url', // 上传成功后返回的字段名 默认url 可选['ossId', 'url', 'fileName']
+    },
+    #elseif($component == "RichTextarea")
+    componentProps: {
+      // options: {
+      //  readyonly: false, // 是否只读
+      // }
+      // width: '100%', // 宽度
+      // showImageUpload: true, // 是否显示图片上传
+      // height: 400 // 高度 默认400
+    },
+    #elseif($component == "Select" || $component == "RadioGroup" || $component == "Checkbox")
+    componentProps: {
+      #if($dictType != "")
+      // 可选从DictEnum中获取 DictEnum.${dictType.toUpperCase()} 便于维护
+      options: getDictOptions('$dictType'),
+      #end
+      #if($component == "RadioGroup")
+      buttonStyle: 'solid',
+      optionType: 'button',
+      #end
+    },
+    #end
+    #if(${column.pk})
+    dependencies: {
+      show: () => false,
+      triggerFields: [''],
+    },
+    #end
+    #if(${required} && $column.pk == false)
+    #if($component == "Select" || $component == "RadioGroup" || $component == "Checkbox")
+    rules: 'selectRequired',
+    #else
+    rules: 'required',
+    #end
+    #end
+  },
+    #end
+  #end
+];

+ 180 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/views/index_vben.vue.vm

@@ -0,0 +1,180 @@
+<script setup lang="ts">
+import type { Recordable } from '@vben/types';
+
+import { ref } from 'vue';
+
+import { Page, useVben${PopupComponent}, type VbenFormProps } from '@vben/common-ui';
+import { getVxePopupContainer } from '@vben/utils';
+
+import { Modal, Popconfirm, Space } from 'ant-design-vue';
+import dayjs from 'dayjs';
+
+import {   
+  useVbenVxeGrid,
+  vxeCheckboxChecked,
+  type VxeGridProps 
+} from '#/adapter/vxe-table';
+
+import {
+  ${businessName}Export,
+  ${businessName}List,
+  ${businessName}Remove,
+} from '#/api/${moduleName}/${businessName}';
+import type { ${BusinessName}Form } from '#/api/${moduleName}/${businessName}/model';
+import { commonDownloadExcel } from '#/utils/file/download';
+
+import ${businessName}${PopupComponent} from './${businessName}-${popupComponent}.vue';
+import { columns, querySchema } from './data';
+
+const formOptions: VbenFormProps = {
+  commonConfig: {
+    labelWidth: 80,
+    componentProps: {
+      allowClear: true,
+    },
+  },
+  schema: querySchema(),
+  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
+  // 处理区间选择器RangePicker时间格式 将一个字段映射为两个字段 搜索/导出会用到
+  // 不需要直接删除
+  // fieldMappingTime: [
+  //  [
+  //    'createTime',
+  //    ['params[beginTime]', 'params[endTime]'],
+  //    ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
+  //  ],
+  // ],
+};
+
+const gridOptions: VxeGridProps = {
+  checkboxConfig: {
+    // 高亮
+    highlight: true,
+    // 翻页时保留选中状态
+    reserve: true,
+    // 点击行选中
+    // trigger: 'row',
+  },
+  columns,
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {},
+  proxyConfig: {
+    ajax: {
+      query: async ({ page }, formValues = {}) => {
+        return await ${businessName}List({
+          pageNum: page.currentPage,
+          pageSize: page.pageSize,
+          ...formValues,
+        });
+      },
+    },
+  },
+  rowConfig: {
+    keyField: '${pkColumn.javaField}',
+  },
+  // 表格全局唯一表示 保存列配置需要用到
+  id: '${moduleName}-${businessName}-index'
+};
+
+const [BasicTable, tableApi] = useVbenVxeGrid({
+  formOptions,
+  gridOptions,
+});
+
+const [${BusinessName}${PopupComponent}, ${popupComponent}Api] = useVben${PopupComponent}({
+  connectedComponent: ${businessName}${PopupComponent},
+});
+
+function handleAdd() {
+  ${popupComponent}Api.setData({});
+  ${popupComponent}Api.open();
+}
+
+async function handleEdit(row: Required<${BusinessName}Form>) {
+  ${popupComponent}Api.setData({ id: row.${pkColumn.javaField} });
+  ${popupComponent}Api.open();
+}
+
+async function handleDelete(row: Required<${BusinessName}Form>) {
+  await ${businessName}Remove(row.${pkColumn.javaField});
+  await tableApi.query();
+}
+
+function handleMultiDelete() {
+  const rows = tableApi.grid.getCheckboxRecords();
+  const ids = rows.map((row: Required<${BusinessName}Form>) => row.${pkColumn.javaField});
+  Modal.confirm({
+    title: '提示',
+    okType: 'danger',
+    content: `确认删除选中的${ids.length}条记录吗?`,
+    onOk: async () => {
+      await ${businessName}Remove(ids);
+      await tableApi.query();
+    },
+  });
+}
+
+function handleDownloadExcel() {
+  commonDownloadExcel(${businessName}Export, '${functionName}数据', tableApi.formApi.form.values, {
+    fieldMappingTime: formOptions.fieldMappingTime,
+  });
+}
+</script>
+
+<template>
+  <Page :auto-content-height="true">
+    <BasicTable table-title="${functionName}列表">
+      <template #toolbar-tools>
+        <Space>
+          <a-button
+            v-access:code="['${permissionPrefix}:export']"
+            @click="handleDownloadExcel"
+          >
+            {{ $t('pages.common.export') }}
+          </a-button>
+          <a-button
+            :disabled="!vxeCheckboxChecked(tableApi)"
+            danger
+            type="primary" 
+            v-access:code="['${permissionPrefix}:remove']" 
+            @click="handleMultiDelete">
+            {{ $t('pages.common.delete') }}
+          </a-button>
+          <a-button
+            type="primary"
+            v-access:code="['${permissionPrefix}:add']"
+            @click="handleAdd"
+          >
+            {{ $t('pages.common.add') }}
+          </a-button>
+        </Space>
+      </template>
+      <template #action="{ row }">
+        <Space>
+          <ghost-button
+            v-access:code="['${permissionPrefix}:edit']"
+            @click.stop="handleEdit(row)"
+          >
+            {{ $t('pages.common.edit') }}
+          </ghost-button>
+          <Popconfirm
+            :get-popup-container="getVxePopupContainer"
+            placement="left"
+            title="确认删除?"
+            @confirm="handleDelete(row)"
+          >
+            <ghost-button
+              danger
+              v-access:code="['${permissionPrefix}:remove']"
+              @click.stop=""
+            >
+              {{ $t('pages.common.delete') }}
+            </ghost-button>
+          </Popconfirm>
+        </Space>
+      </template>
+    </BasicTable>
+    <${BusinessName}${PopupComponent} @reload="tableApi.query()" />
+  </Page>
+</template>

+ 150 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/views/index_vben_tree.vue.vm

@@ -0,0 +1,150 @@
+<script setup lang="ts">
+import type { Recordable } from '@vben/types';
+
+import { nextTick } from 'vue';
+
+import { Page, useVben${PopupComponent}, type VbenFormProps } from '@vben/common-ui';
+import { getVxePopupContainer } from '@vben/utils';
+
+import { Popconfirm, Space } from 'ant-design-vue';
+
+import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
+import { ${businessName}List, ${businessName}Remove } from '#/api/${moduleName}/${businessName}';
+import type { ${BusinessName}Form } from '#/api/${moduleName}/${businessName}/model';
+
+import { columns, querySchema } from './data';
+import ${businessName}${PopupComponent} from './${businessName}-${popupComponent}.vue';
+
+const formOptions: VbenFormProps = {
+  commonConfig: {
+    labelWidth: 80,
+    componentProps: {
+      allowClear: true,
+    },
+  },
+  schema: querySchema(),
+  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
+};
+
+const gridOptions: VxeGridProps = {
+  columns,
+  height: 'auto',
+  keepSource: true,
+  pagerConfig: {
+    enabled: false,
+  },
+  proxyConfig: {
+    ajax: {
+      query: async (_, formValues = {}) => {
+        const resp = await ${businessName}List({
+          ...formValues,
+        });
+        return { rows: resp };
+      },
+      // 默认请求接口后展开全部 不需要可以删除这段
+      querySuccess: () => {
+        nextTick(() => {
+          expandAll();
+        });
+      },
+    },
+  },
+  rowConfig: {
+    keyField: '${pkColumn.javaField}',
+  },
+  /**
+  * 虚拟滚动开关 默认关闭
+  * 数据量小可以选择关闭
+  * 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
+  */
+  scrollY: {
+    enabled: false,
+    gt: 0,
+  },
+  treeConfig: {
+    parentField: '${treeParentCode}',
+    rowField: '${treeCode}',
+    // 自动转换为tree 由vxe处理 无需手动转换
+    transform: true,
+  },
+  // 表格全局唯一表示 保存列配置需要用到
+  id: '${moduleName}-${businessName}-index'
+};
+
+const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
+const [${BusinessName}${PopupComponent}, ${popupComponent}Api] = useVben${PopupComponent}({
+  connectedComponent: ${businessName}${PopupComponent},
+});
+
+function handleAdd() {
+  ${popupComponent}Api.setData({});
+  ${popupComponent}Api.open();
+}
+
+async function handleEdit(row: Required<${BusinessName}Form>) {
+  ${popupComponent}Api.setData({ id: row.${pkColumn.javaField} });
+  ${popupComponent}Api.open();
+}
+
+async function handleDelete(row: Required<${BusinessName}Form>) {
+  await ${businessName}Remove(row.${pkColumn.javaField});
+  await tableApi.query();
+}
+
+function expandAll() {
+  tableApi.grid?.setAllTreeExpand(true);
+}
+
+function collapseAll() {
+  tableApi.grid?.setAllTreeExpand(false);
+}
+</script>
+
+<template>
+  <Page :auto-content-height="true">
+    <BasicTable table-title="${functionName}列表">
+      <template #toolbar-tools>
+        <Space>
+          <a-button @click="collapseAll">
+            {{ $t('pages.common.collapse') }}
+          </a-button>
+          <a-button @click="expandAll">
+            {{ $t('pages.common.expand') }}
+          </a-button>
+          <a-button
+            type="primary"
+            v-access:code="['${permissionPrefix}:add']"
+            @click="handleAdd"
+          >
+            {{ $t('pages.common.add') }}
+          </a-button>
+        </Space>
+      </template>
+      <template #action="{ row }">
+        <Space>
+          <ghost-button
+            v-access:code="['${permissionPrefix}:edit']"
+            @click.stop="handleEdit(row)"
+          >
+            {{ $t('pages.common.edit') }}
+          </ghost-button>
+          <Popconfirm
+            :get-popup-container="getVxePopupContainer"
+            placement="left"
+            title="确认删除?"
+            @confirm="handleDelete(row)"
+          >
+            <ghost-button
+              danger
+              v-access:code="['${permissionPrefix}:remove']"
+              @click.stop=""
+            >
+              {{ $t('pages.common.delete') }}
+            </ghost-button>
+          </Popconfirm>
+        </Space>
+      </template>
+    </BasicTable>
+    <${BusinessName}${PopupComponent} @reload="tableApi.query()" />
+  </Page>
+</template>

+ 87 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/views/popup.vue.vm

@@ -0,0 +1,87 @@
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+
+import { useVben${PopupComponent} } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+import { cloneDeep } from '@vben/utils';
+
+import { useVbenForm } from '#/adapter/form';
+import { ${businessName}Add, ${businessName}Info, ${businessName}Update } from '#/api/${moduleName}/${businessName}';
+
+import { ${popupComponent}Schema } from './data';
+
+const emit = defineEmits<{ reload: [] }>();
+
+const isUpdate = ref(false);
+const title = computed(() => {
+  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
+});
+
+const [BasicForm, formApi] = useVbenForm({
+  commonConfig: {
+    // 默认占满两列
+    formItemClass: 'col-span-2',
+    // 默认label宽度 px
+    labelWidth: 80,
+    // 通用配置项 会影响到所有表单项
+    componentProps: {
+      class: 'w-full',
+    }
+  },
+  schema: ${popupComponent}Schema(),
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+const [Basic${PopupComponent}, ${popupComponent}Api] = useVben${PopupComponent}({
+  fullscreenButton: false,
+  onCancel: handleCancel,
+  onConfirm: handleConfirm,
+  onOpenChange: async (isOpen) => {
+    if (!isOpen) {
+      return null;
+    }
+    ${popupComponent}Api.${popupComponent}Loading(true);
+
+    const { id } = ${popupComponent}Api.getData() as { id?: number | string };
+    isUpdate.value = !!id;
+
+    if (isUpdate.value && id) {
+      const record = await ${businessName}Info(id);
+      await formApi.setValues(record);
+    }
+
+    ${popupComponent}Api.${popupComponent}Loading(false);
+  },
+});
+
+async function handleConfirm() {
+  try {
+    ${popupComponent}Api.${popupComponent}Loading(true);
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    // getValues获取为一个readonly的对象 需要修改必须先深拷贝一次
+    const data = cloneDeep(await formApi.getValues());
+    await (isUpdate.value ? ${businessName}Update(data) : ${businessName}Add(data));
+    emit('reload');
+    await handleCancel();
+  } catch (error) {
+    console.error(error);
+  } finally {
+    ${popupComponent}Api.${popupComponent}Loading(false);
+  }
+}
+
+async function handleCancel() {
+  ${popupComponent}Api.close();
+  await formApi.resetForm();
+}
+</script>
+
+<template>
+  <Basic${PopupComponent} :close-on-click-modal="false" :title="title" class="w-[550px]">
+    <BasicForm />
+  </Basic${PopupComponent}>
+</template>

+ 103 - 0
ruoyi-modules/ruoyi-generator/src/main/resources/vm/vben5/views/popup_tree.vue.vm

@@ -0,0 +1,103 @@
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+
+import { useVben${PopupComponent} } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+import { cloneDeep, getPopupContainer, listToTree } from '@vben/utils';
+
+import { useVbenForm } from '#/adapter/form';
+import { ${businessName}Add, ${businessName}Info, ${businessName}List, ${businessName}Update } from '#/api/${moduleName}/${businessName}';
+
+import { ${popupComponent}Schema } from './data';
+
+const emit = defineEmits<{ reload: [] }>();
+
+const isUpdate = ref(false);
+const title = computed(() => {
+  return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
+});
+
+const [BasicForm, formApi] = useVbenForm({
+  commonConfig: {
+    // 默认占满两列
+    formItemClass: 'col-span-2',
+    // 默认label宽度 px
+    labelWidth: 80,
+    // 通用配置项 会影响到所有表单项
+    componentProps: {
+      class: 'w-full',
+    }
+  },
+  schema: ${popupComponent}Schema(),
+  showDefaultActions: false,
+  wrapperClass: 'grid-cols-2',
+});
+
+async function setup${BusinessName}Select() {
+  const listData = await ${businessName}List();
+  const treeData = listToTree(listData, { id: '${treeCode}', pid: '${treeParentCode}' });
+  formApi.updateSchema([{
+    fieldName: '${treeParentCode}',
+    componentProps: {
+      treeData,
+      treeLine: { showLeafIcon: false },
+      fieldNames: { label: '${treeName}', value: '${treeCode}' },
+      treeDefaultExpandAll: true,
+      getPopupContainer,
+    },
+  }]);
+}
+
+const [Basic${PopupComponent}, ${popupComponent}Api] = useVben${PopupComponent}({
+  fullscreenButton: false,
+  onCancel: handleCancel,
+  onConfirm: handleConfirm,
+  onOpenChange: async (isOpen) => {
+    if (!isOpen) {
+      return null;
+    }
+    ${popupComponent}Api.${popupComponent}Loading(true);
+
+    const { id } = ${popupComponent}Api.getData() as { id?: number | string };
+    isUpdate.value = !!id;
+
+    if (isUpdate.value && id) {
+      const record = await ${businessName}Info(id);
+      await formApi.setValues(record);
+    }
+    await setup${BusinessName}Select();
+
+    ${popupComponent}Api.${popupComponent}Loading(false);
+  },
+});
+
+async function handleConfirm() {
+  try {
+    ${popupComponent}Api.${popupComponent}Loading(true);
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    // getValues获取为一个readonly的对象 需要修改必须先深拷贝一次
+    const data = cloneDeep(await formApi.getValues());
+    await (isUpdate.value ? ${businessName}Update(data) : ${businessName}Add(data));
+    emit('reload');
+    await handleCancel();
+  } catch (error) {
+    console.error(error);
+  } finally {
+    ${popupComponent}Api.${popupComponent}Loading(false);
+  }
+}
+
+async function handleCancel() {
+  ${popupComponent}Api.close();
+  await formApi.resetForm();
+}
+</script>
+
+<template>
+  <Basic${PopupComponent} :close-on-click-modal="false" :title="title" class="w-[550px]">
+    <BasicForm />
+  </Basic${PopupComponent}>
+</template>