Browse Source

Merge remote-tracking branch 'origin/tree' into next-dev

# Conflicts:
#	.github/workflows/mirror-repository.yml
lengleng 1 month ago
parent
commit
ed8e60020e

+ 1 - 0
.github/workflows/mirror-repository.yml

@@ -5,6 +5,7 @@ on:
     branches:
       - master 
       - next
+      - tree
       - next-dev
       - dev
 

+ 52 - 0
config.json

@@ -112,5 +112,57 @@
       "generatorPath": "${backendPath}/src/main/java/${packagePath}/${moduleName}/mapper/${ChildClassName}Mapper.java",
       "templateFile": "multiple/子Mapper.java"
     }
+  ],
+  "树形表格增删改查": [
+    {
+      "templateName": "Controller",
+      "generatorPath": "${backendPath}/src/main/java/${packagePath}/${moduleName}/controller/${ClassName}Controller.java",
+      "templateFile": "tree/树形Controller.java"
+    },
+    {
+      "templateName": "Service",
+      "generatorPath": "${backendPath}/src/main/java/${packagePath}/${moduleName}/service/${ClassName}Service.java",
+      "templateFile": "tree/树形Service.java"
+    },
+    {
+      "templateName": "ServiceImpl",
+      "generatorPath": "${backendPath}/src/main/java/${packagePath}/${moduleName}/service/impl/${ClassName}ServiceImpl.java",
+      "templateFile": "tree/树形ServiceImpl.java"
+    },
+    {
+      "templateName": "实体",
+      "generatorPath": "${backendPath}/src/main/java/${packagePath}/${moduleName}/entity/${ClassName}Entity.java",
+      "templateFile": "tree/树形实体.java"
+    },
+    {
+      "templateName": "Mapper",
+      "generatorPath": "${backendPath}/src/main/java/${packagePath}/${moduleName}/mapper/${ClassName}Mapper.java",
+      "templateFile": "single/Mapper.java"
+    },
+    {
+      "templateName": "Mapper.xml",
+      "generatorPath": "${backendPath}/src/main/resources/mapper/${ClassName}Mapper.xml",
+      "templateFile": "single/Mapper.xml"
+    },
+    {
+      "templateName": "权限菜单",
+      "generatorPath": "${backendPath}/menu/${functionName}_menu.sql",
+      "templateFile": "common/权限菜单.sql"
+    },
+    {
+      "templateName": "api.ts",
+      "generatorPath": "${frontendPath}/src/api/${moduleName}/${functionName}.ts",
+      "templateFile": "tree/树形api.ts"
+    },
+    {
+      "templateName": "树形表格",
+      "generatorPath": "${frontendPath}/src/views/${moduleName}/${functionName}/index.vue",
+      "templateFile": "tree/树形表格.vue"
+    },
+    {
+      "templateName": "树形表单",
+      "generatorPath": "${frontendPath}/src/views/${moduleName}/${functionName}/form.vue",
+      "templateFile": "tree/树形表单.vue"
+    }
   ]
 }

+ 199 - 0
tree/树形Controller.java

@@ -0,0 +1,199 @@
+package ${package}.${moduleName}.controller;
+
+#if($queryList)
+import cn.hutool.core.util.StrUtil;
+#end
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import ${package}.common.core.util.R;
+import ${package}.common.log.annotation.SysLog;
+#if($opensource)
+import com.pig4cloud.plugin.excel.annotation.ResponseExcel;
+import com.pig4cloud.plugin.excel.annotation.RequestExcel;
+#else
+import ${package}.common.excel.annotation.ResponseExcel;
+import ${package}.common.excel.annotation.RequestExcel;
+#end
+import ${package}.${moduleName}.entity.${ClassName}Entity;
+import ${package}.${moduleName}.service.${ClassName}Service;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+#if($isSpringBoot3)
+import ${package}.common.security.annotation.HasPermission;
+import org.springdoc.core.annotations.ParameterObject;
+#else
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springdoc.api.annotations.ParameterObject;
+#end
+import org.springframework.http.HttpHeaders;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * ${tableComment}
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/${functionName}" )
+@Tag(description = "${functionName}" , name = "${tableComment}管理" )
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class ${ClassName}Controller {
+
+    private final  ${ClassName}Service ${className}Service;
+
+    /**
+     * 获取树形列表
+     * @param ${className} ${tableComment}
+     * @return
+     */
+    @Operation(summary = "获取树形列表" , description = "获取树形列表" )
+    @GetMapping("/tree" )
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_view")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_view')" )
+    #end
+    public R get${ClassName}Tree(@ParameterObject ${ClassName}Entity ${className}) {
+        LambdaQueryWrapper<${ClassName}Entity> wrapper = Wrappers.lambdaQuery();
+#foreach ($field in $queryList)
+#set($getAttrName=$str.getProperty($field.attrName))
+#set($var="${className}.$getAttrName()")
+#if($field.attrType == 'String')
+#set($expression="StrUtil.isNotBlank")
+#else
+#set($expression="Objects.nonNull")
+#end
+#if($field.queryType == '=')
+		wrapper.eq($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == 'like' )
+		wrapper.like($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == '!-' )
+		wrapper.ne($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == '>' )
+		wrapper.gt($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == '<' )
+		wrapper.lt($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == '>=' )
+		wrapper.ge($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == '<=' )
+		wrapper.le($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == 'left like' )
+		wrapper.likeLeft($expression($var),${ClassName}Entity::$getAttrName,$var);
+#elseif( $field.queryType == 'right like' )
+		wrapper.likeRight($expression($var),${ClassName}Entity::$getAttrName,$var);
+#end
+#end
+        return R.ok(${className}Service.buildTree(wrapper));
+    }
+
+    /**
+     * 通过条件查询${tableComment}
+     * @param ${className} 查询条件
+     * @return R  对象列表
+     */
+    @Operation(summary = "通过条件查询" , description = "通过条件查询对象" )
+    @GetMapping("/details" )
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_view")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_view')" )
+    #end
+    public R getDetails(@ParameterObject ${ClassName}Entity ${className}) {
+        return R.ok(${className}Service.list(Wrappers.query(${className})));
+    }
+
+    /**
+     * 新增${tableComment}
+     * @param ${className} ${tableComment}
+     * @return R
+     */
+    @Operation(summary = "新增${tableComment}" , description = "新增${tableComment}" )
+    @SysLog("新增${tableComment}" )
+    @PostMapping
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_add")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_add')" )
+    #end
+    public R save(@RequestBody ${ClassName}Entity ${className}) {
+        return R.ok(${className}Service.save(${className}));
+    }
+
+    /**
+     * 修改${tableComment}
+     * @param ${className} ${tableComment}
+     * @return R
+     */
+    @Operation(summary = "修改${tableComment}" , description = "修改${tableComment}" )
+    @SysLog("修改${tableComment}" )
+    @PutMapping
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_edit")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_edit')" )
+    #end
+    public R updateById(@RequestBody ${ClassName}Entity ${className}) {
+        return R.ok(${className}Service.updateById(${className}));
+    }
+
+    /**
+     * 通过id删除${tableComment}
+     * @param ids ${pk.attrName}列表
+     * @return R
+     */
+    @Operation(summary = "通过id删除${tableComment}" , description = "通过id删除${tableComment}" )
+    @SysLog("通过id删除${tableComment}" )
+    @DeleteMapping
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_del")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_del')" )
+    #end
+    public R removeById(@RequestBody ${pk.attrType}[] ids) {
+        return R.ok(${className}Service.removeBatchByIds(CollUtil.toList(ids)));
+    }
+
+    /**
+     * 导出excel 表格
+     * @param ${className} 查询条件
+   	 * @param ids 导出指定ID
+     * @return excel 文件流
+     */
+    @ResponseExcel
+    @GetMapping("/export")
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_export")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_export')" )
+    #end
+    public List<${ClassName}Entity> exportExcel(${ClassName}Entity ${className},${pk.attrType}[] ids) {
+        return ${className}Service.list(Wrappers.lambdaQuery(${className}).in(ArrayUtil.isNotEmpty(ids), ${ClassName}Entity::$str.getProperty($pk.attrName), ids));
+    }
+
+    /**
+     * 导入excel 表
+     * @param ${className}List 对象实体列表
+     * @param bindingResult 错误信息列表
+     * @return ok fail
+     */
+    @PostMapping("/import")
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_export")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_export')" )
+    #end
+    public R importExcel(@RequestExcel List<${ClassName}Entity> ${className}List, BindingResult bindingResult) {
+        return R.ok(${className}Service.saveBatch(${className}List));
+    }
+} 

+ 31 - 0
tree/树形Service.java

@@ -0,0 +1,31 @@
+package ${package}.${moduleName}.service;
+
+import cn.hutool.core.lang.tree.Tree;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.IService;
+import ${package}.${moduleName}.entity.${ClassName}Entity;
+
+import java.util.List;
+
+/**
+ * ${tableComment} Service接口
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+public interface ${ClassName}Service extends IService<${ClassName}Entity> {
+
+    /**
+     * 构建树形结构数据
+     * @param wrapper 查询条件
+     * @return 树形结构数据
+     */
+    List<Tree<${pk.attrType}>> buildTree(LambdaQueryWrapper<${ClassName}Entity> wrapper);
+
+    /**
+     * 递归删除节点及其子节点
+     * @param ids 要删除的节点ID列表
+     * @return 删除结果
+     */
+    boolean removeBatchByIds(List<${pk.attrType}> ids);
+} 

+ 132 - 0
tree/树形ServiceImpl.java

@@ -0,0 +1,132 @@
+package ${package}.${moduleName}.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.tree.Tree;
+import cn.hutool.core.lang.tree.TreeNode;
+import cn.hutool.core.lang.tree.TreeUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import ${package}.${moduleName}.entity.${ClassName}Entity;
+import ${package}.${moduleName}.mapper.${ClassName}Mapper;
+import ${package}.${moduleName}.service.${ClassName}Service;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * ${tableComment} Service实现类
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+@Service
+@RequiredArgsConstructor
+public class ${ClassName}ServiceImpl extends ServiceImpl<${ClassName}Mapper, ${ClassName}Entity> implements ${ClassName}Service {
+
+    /**
+     * 构建树形结构数据
+     * @param wrapper 查询条件
+     * @return 树形结构数据
+     */
+    @Override
+    public List<Tree<${pk.attrType}>> buildTree(LambdaQueryWrapper<${ClassName}Entity> wrapper) {
+        // 查询所有数据
+        List<${ClassName}Entity> allList = list(wrapper);
+        
+        if (CollUtil.isEmpty(allList)) {
+            return new ArrayList<>();
+        }
+
+        // 转换为TreeNode
+        List<TreeNode<${pk.attrType}>> collect = allList.stream().map(getNodeFunction()).toList();
+
+        // 使用TreeUtil构建树形结构,根节点ID为0
+#if($pk.attrType == 'Long')
+        return TreeUtil.build(collect, 0L);
+#else
+        return TreeUtil.build(collect, 0);
+#end
+    }
+
+    /**
+     * 获取TreeNode转换函数
+     * @return TreeNode转换函数
+     */
+    @NotNull
+    private Function<${ClassName}Entity, TreeNode<${pk.attrType}>> getNodeFunction() {
+        return entity -> {
+            TreeNode<${pk.attrType}> node = new TreeNode<>();
+            node.setId(entity.$str.getProperty($pk.attrName)());
+            node.setName(entity.$str.getProperty($nameField)());
+            node.setParentId(entity.$str.getProperty($parentField)() != null ? entity.$str.getProperty($parentField)() : 0L);
+
+            // 扩展属性
+            Map<String, Object> extra = new HashMap<>();
+#foreach($field in $fieldList)
+            extra.put(${ClassName}Entity.Fields.${field.attrName}, entity.$str.getProperty($field.attrName)());
+#end
+            node.setExtra(extra);
+            return node;
+        };
+    }
+
+    /**
+     * 递归删除节点及其子节点
+     * @param ids 要删除的节点ID列表
+     * @return 删除结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean removeBatchByIds(List<${pk.attrType}> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return true;
+        }
+
+        // 获取所有要删除的ID(包括子节点)
+        List<${pk.attrType}> allDeleteIds = new ArrayList<>(ids);
+        
+        for (${pk.attrType} id : ids) {
+            List<${pk.attrType}> childIds = getChildIdsRecursive(id);
+            allDeleteIds.addAll(childIds);
+        }
+
+        // 去重
+        allDeleteIds = allDeleteIds.stream().distinct().collect(Collectors.toList());
+
+        // 批量删除
+        return super.removeBatchByIds(allDeleteIds);
+    }
+
+    /**
+     * 递归获取所有子节点ID
+     * @param parentId 父节点ID
+     * @return 子节点ID列表
+     */
+    private List<${pk.attrType}> getChildIdsRecursive(${pk.attrType} parentId) {
+        List<${pk.attrType}> childIds = new ArrayList<>();
+        
+        LambdaQueryWrapper<${ClassName}Entity> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(${ClassName}Entity::$str.getProperty($parentField), parentId);
+        wrapper.select(${ClassName}Entity::$str.getProperty($pk.attrName));
+        
+        List<${ClassName}Entity> children = list(wrapper);
+        
+        for (${ClassName}Entity child : children) {
+            ${pk.attrType} childId = child.$str.getProperty($pk.attrName)();
+            childIds.add(childId);
+            // 递归获取子节点的子节点
+            childIds.addAll(getChildIdsRecursive(childId));
+        }
+        
+        return childIds;
+    }
+} 

+ 106 - 0
tree/树形api.ts

@@ -0,0 +1,106 @@
+import request from "/@/utils/request"
+
+// ========== 树形表格CRUD接口 ==========
+
+/**
+ * 获取树形列表数据
+ * @param query - 查询参数对象
+ * @returns Promise<树形数据>
+ */
+export function fetchTreeList(query?: Object) {
+  return request({
+    url: '/${moduleName}/${functionName}/tree',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 新增数据
+ * @param obj - 要新增的数据对象
+ * @returns Promise<boolean> - 操作结果
+ */
+export function addObj(obj?: Object) {
+  return request({
+    url: '/${moduleName}/${functionName}',
+    method: 'post',
+    data: obj
+  })
+}
+
+/**
+ * 获取详情数据
+ * @param obj - 查询参数对象(包含ID等)
+ * @returns Promise<数据详情>
+ */
+export function getObj(obj?: Object) {
+  return request({
+    url: '/${moduleName}/${functionName}/details',
+    method: 'get',
+    params: obj
+  })
+}
+
+/**
+ * 批量删除数据(递归删除子节点)
+ * @param ids - 要删除的ID数组
+ * @returns Promise<操作结果>
+ */
+export function delObjs(ids?: Object) {
+  return request({
+    url: '/${moduleName}/${functionName}',
+    method: 'delete',
+    data: ids
+  })
+}
+
+/**
+ * 更新数据
+ * @param obj - 要更新的数据对象
+ * @returns Promise<操作结果>
+ */
+export function putObj(obj?: Object) {
+  return request({
+    url: '/${moduleName}/${functionName}',
+    method: 'put',
+    data: obj
+  })
+}
+
+
+// ========== 工具函数 ==========
+
+/**
+ * 验证字段值唯一性
+ * @param rule - 验证规则对象
+ * @param value - 要验证的值
+ * @param callback - 验证回调函数
+ * @param isEdit - 是否为编辑模式
+ * 
+ * @example
+ * // 在表单验证规则中使用
+ * fieldName: [
+ *   {
+ *     validator: (rule, value, callback) => {
+ *       validateExist(rule, value, callback, form.${pk.attrName} !== '');
+ *     },
+ *     trigger: 'blur',
+ *   },
+ * ]
+ */
+export function validateExist(rule: any, value: any, callback: any, isEdit: boolean) {
+  // 编辑模式下跳过验证
+  if (isEdit) {
+    return callback();
+  }
+
+  // 查询是否存在相同值
+  getObj({ [rule.field]: value }).then((response) => {
+    const result = response.data;
+    if (result !== null && result.length > 0) {
+      callback(new Error('数据已经存在'));
+    } else {
+      callback();
+    }
+  });
+}

+ 59 - 0
tree/树形实体.java

@@ -0,0 +1,59 @@
+package ${package}.${moduleName}.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.FieldNameConstants;
+#if($isTenant)
+import ${package}.common.core.util.TenantTable;
+#end
+#foreach($import in $importList)
+import $import;
+#end
+
+/**
+ * ${tableComment}
+ *
+ * @author ${author}
+ * @date ${datetime}
+ */
+@Data
+#if($isTenant)
+@TenantTable
+#end
+@FieldNameConstants
+@TableName("${tableName}")
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "${tableComment}")
+public class ${ClassName}Entity extends Model<${ClassName}Entity> {
+
+#foreach ($field in $fieldList)
+#if(${field.fieldComment})#set($comment=${field.fieldComment})#else #set($comment=${field.attrName})#end
+
+	/**
+	* $comment
+	*/
+#if($field.primaryPk == '1')
+    @TableId(type = IdType.ASSIGN_ID)
+#end
+#if($field.autoFill == 'INSERT')
+	@TableField(fill = FieldFill.INSERT)
+#elseif($field.autoFill == 'INSERT_UPDATE')
+	@TableField(fill = FieldFill.INSERT_UPDATE)
+#elseif($field.autoFill == 'UPDATE')
+	@TableField(fill = FieldFill.UPDATE)
+#end
+#if($field.fieldName == 'del_flag')
+    @TableLogic
+	@TableField(fill = FieldFill.INSERT)
+#end
+    @Schema(description="$comment"#if($field.hidden),hidden=$field.hidden#end)
+#if($field.formType == 'checkbox')
+    private ${field.attrType}[] $field.attrName;
+#else
+    private $field.attrType $field.attrName;
+#end    
+#end
+}

+ 351 - 0
tree/树形表单.vue

@@ -0,0 +1,351 @@
+<template>
+  <el-dialog :title="form.${pk.attrName} ? '编辑' : '新增'" v-model="visible"
+    :close-on-click-modal="false" draggable>
+    <el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
+      <el-row :gutter="24">
+        <!-- 父级节点选择 -->
+#if($formLayout == 1)
+        <el-col :span="24" class="mb20">
+#elseif($formLayout == 2)
+        <el-col :span="12" class="mb20">
+#end
+          <el-form-item label="父级节点" prop="${parentField}">
+            <el-tree-select
+              v-model="form.${parentField}"
+              :data="parentNodes"
+              :props="treeSelectProps"
+              check-strictly
+              :render-after-expand="false"
+              placeholder="请选择父级节点"
+              style="width: 100%"
+              clearable
+              filterable
+            />
+          </el-form-item>
+        </el-col>
+#foreach($field in $formList)
+#if($field.attrName != ${pk.attrName} && $field.attrName != ${parentField})
+#if($formLayout == 1)
+        <el-col :span="24" class="mb20">
+#elseif($formLayout == 2)
+        <el-col :span="12" class="mb20">
+#end
+#if($field.formType == 'text')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-input v-model="form.${field.attrName}" placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end"/>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'textarea')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-input type="textarea" v-model="form.${field.attrName}" placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end"/>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'select')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-select v-model="form.${field.attrName}" placeholder="请选择#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end">
+#if($field.fieldDict)
+              <el-option :value="item.value" :label="item.label" v-for="(item, index) in ${field.fieldDict}" :key="index"></el-option>
+#else
+              <el-option label="请选择" value="0"></el-option>
+#end
+            </el-select>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'radio')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-radio-group v-model="form.${field.attrName}">
+#if($field.fieldDict)
+              <el-radio :label="item.value" v-for="(item, index) in ${field.fieldDict}" border :key="index">{{ item.label }}</el-radio>
+#else
+              <el-radio label="${field.fieldComment}" border>${field.fieldComment}</el-radio>
+#end
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'checkbox')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-checkbox-group v-model="form.${field.attrName}">
+#if($field.fieldDict)
+              <el-checkbox :label="item.value" v-for="(item, index) in ${field.fieldDict}" :key="index">{{ item.label }}</el-checkbox>
+#else
+              <el-checkbox label="启用" name="type"></el-checkbox>
+              <el-checkbox label="禁用" name="type"></el-checkbox>
+#end
+            </el-checkbox-group>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'date')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-date-picker type="date" placeholder="请选择#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" v-model="form.${field.attrName}" :value-format="dateStr"></el-date-picker>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'datetime')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-date-picker type="datetime" placeholder="请选择#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" v-model="form.${field.attrName}" :value-format="dateTimeStr"></el-date-picker>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'number')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-input-number :min="1" :max="1000" v-model="form.${field.attrName}" placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end"></el-input-number>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'upload-file')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <upload-file v-model="form.${field.attrName}"></upload-file>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'upload-img')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <upload-img v-model:imageUrl="form.${field.attrName}"></upload-img>
+          </el-form-item>
+        </el-col>
+#elseif($field.formType == 'editor')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <editor v-if="visible" v-model:get-html="form.${field.attrName}" placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end"></editor>
+          </el-form-item>
+        </el-col>
+#else
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-input v-model="form.${field.attrName}" placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end"/>
+          </el-form-item>
+        </el-col>
+#end
+#end
+#end
+      </el-row>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="visible = false">取 消</el-button>
+        <el-button type="primary" @click="onSubmit" :disabled="loading">确 认</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts" name="${ClassName}TreeDialog">
+// ========== 导入语句 ==========
+import { useMessage } from "/@/hooks/message";
+import { getObj, addObj, putObj, fetchTreeList } from '/@/api/${moduleName}/${functionName}';
+#if($fieldDict && $fieldDict.size() > 0)
+import { useDict } from '/@/hooks/dict';
+#end
+#foreach($field in $formList)
+#if($field.formValidator && $field.formValidator != 'duplicate')
+import { rule } from '/@/utils/validate';
+#break
+#end
+#end
+
+// ========== 类型定义 ==========
+interface TreeNode {
+  ${pk.attrName}: string | number | null;
+#foreach($field in $formList)
+#if($field.attrName != ${pk.attrName} && $field.attrName != ${parentField})
+#set($nameField = $field)
+#break
+#end
+#end
+#if($nameField)
+  ${nameField.attrName}: string;
+#end
+  children?: TreeNode[];
+}
+
+interface FormData {
+#if(!$formList.contains(${pk.attrName}))
+  ${pk.attrName}?: string;
+#end
+  ${parentField}?: string | number | null;
+#foreach($field in $formList)
+#if($field.attrName != ${parentField})
+#if($field.formType == 'number')
+  ${field.attrName}: number;
+#elseif($field.formType == 'checkbox')
+  ${field.attrName}: any[];
+#else
+  ${field.attrName}: string;
+#end
+#end
+#end
+}
+
+// ========== 组件定义 ==========
+const emit = defineEmits(['refresh']);
+
+// ========== 响应式数据定义 ==========
+const dataFormRef = ref(); // 表单引用
+const visible = ref(false); // 弹窗显示状态
+const loading = ref(false); // 加载状态
+const parentNodes = ref<TreeNode[]>([]); // 父级节点数据
+
+// 树形选择器配置
+const treeSelectProps = {
+  children: 'children',
+#foreach($field in $formList)
+#if($field.attrName != ${pk.attrName} && $field.attrName != ${parentField})
+  label: '${field.attrName}',
+#break
+#end
+#end
+  value: '${pk.attrName}',
+  checkStrictly: true
+};
+
+// 表单数据对象
+const form = reactive<FormData>({
+#if(!$formList.contains(${pk.attrName}))
+  ${pk.attrName}: '', // 主键
+#end
+  ${parentField}: null, // 父级ID
+#foreach($field in $formList)
+#if($field.attrName != ${parentField})
+#if($field.formType == 'number')
+  ${field.attrName}: 0, // ${field.fieldComment}
+#elseif($field.formType == 'checkbox')
+  ${field.attrName}: [], // ${field.fieldComment}
+#else
+  ${field.attrName}: '', // ${field.fieldComment}
+#end
+#end
+#end
+});
+
+// ========== 字典数据处理 ==========
+#set($fieldDict=[])
+#foreach($field in $formList)
+#if($field.fieldDict)
+#set($void=$fieldDict.add($field.fieldDict))
+#end
+#end
+#if($fieldDict && $fieldDict.size() > 0)
+// 加载字典数据
+const { $dict.format($fieldDict) } = useDict($dict.quotation($fieldDict));
+#end
+
+// ========== 表单校验规则 ==========
+const dataRules = ref({
+  ${parentField}: [
+    { 
+      required: true, 
+      validator: (rule: any, value: any, callback: any) => {
+        if (value === null || value === undefined || value === '') {
+          callback(new Error('请选择父级节点'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change' 
+    }
+  ],
+#foreach($field in $formList)
+#if($field.formRequired == '1' && $field.formValidator == 'duplicate')
+  ${field.attrName}: [
+    { required: true, message: '${field.fieldComment}不能为空', trigger: 'blur' },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        // 重复性校验(编辑时跳过)
+        validateExist(rule, value, callback, form.${pk.attrName} !== '');
+      },
+      trigger: 'blur',
+    }
+  ],
+#elseif($field.formRequired == '1' && $field.formValidator)
+  ${field.attrName}: [
+    { required: true, message: '${field.fieldComment}不能为空', trigger: 'blur' },
+    { validator: rule.${field.formValidator}, trigger: 'blur' }
+  ],
+#elseif($field.formRequired == '1')
+  ${field.attrName}: [
+    { required: true, message: '${field.fieldComment}不能为空', trigger: 'blur' }
+  ],
+#elseif($field.formValidator)
+  ${field.attrName}: [
+    { validator: rule.${field.formValidator}, trigger: 'blur' }
+  ],
+#end
+#end
+});
+
+// ========== 方法定义 ==========
+/**
+ * 获取详情数据
+ */
+const get${ClassName}Data = async (id: string) => {
+  try {
+    loading.value = true;
+    const { data } = await getObj({ ${pk.attrName}: id });
+    if (data && data.length > 0) {
+      Object.assign(form, data[0]);
+    }
+  } catch (error) {
+    useMessage().error('获取数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+
+/**
+ * 打开弹窗方法
+ * @param id 编辑时的数据ID
+ * @param parentId 新增时的父级ID
+ */
+const openDialog = async (id?: string, parentId?: string | number) => {
+  visible.value = true;
+  form.${pk.attrName} = '';
+  form.${parentField} = parentId || '0';
+
+  // 初始化父级节点数据
+  const { data } = await fetchTreeList();
+  parentNodes.value = [{ ${pk.attrName}: '0', ${nameField.attrName}: '根节点', children: data }];
+
+  // 重置表单验证
+  nextTick(() => {
+    dataFormRef.value?.resetFields();
+  });
+
+  if (id) {
+    form.${pk.attrName} = id;
+    await get${ClassName}Data(id);
+  }
+};
+
+/**
+ * 提交表单方法
+ */
+const onSubmit = async () => {
+  // 防止重复提交
+  if (loading.value) return;
+  loading.value = true;
+  
+  try {
+    const valid = await dataFormRef.value.validate().catch(() => {});
+		if (!valid) {
+			loading.value = false;
+			return false;
+    }
+
+    // 处理父级ID,如果选择根节点(null)则设为null,其他情况保持原值
+    if (form.${pk.attrName}) {
+      await putObj(form);
+      useMessage().success('修改成功');
+    } else {
+      await addObj(form);
+      useMessage().success('添加成功');
+    }
+    
+    visible.value = false;
+    emit('refresh'); // 通知父组件刷新列表
+  } catch (err: any) {
+    useMessage().error(err.msg || '操作失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+// ========== 对外暴露 ==========
+defineExpose({
+  openDialog
+});
+</script> 

+ 342 - 0
tree/树形表格.vue

@@ -0,0 +1,342 @@
+<template>
+  <div class="layout-padding">
+    <div class="layout-padding-auto layout-padding-view">
+#if($queryList)
+      <!-- 查询表单区域 -->
+      <el-row v-show="showSearch">
+        <el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
+#foreach($field in $queryList)
+#if($field.queryFormType == 'select')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-select v-model="state.queryForm.${field.attrName}" placeholder="请选择#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end">
+#if($field.fieldDict)
+              <el-option 
+                :label="item.label" 
+                :value="item.value" 
+                v-for="(item, index) in ${field.fieldDict}" 
+                :key="index"
+              />
+#else
+              <el-option label="请选择" value="0" />
+#end
+            </el-select>
+          </el-form-item>
+#elseif($field.queryFormType == 'date')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-date-picker 
+              type="date" 
+              placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" 
+              v-model="state.queryForm.${field.attrName}"
+              :value-format="dateStr"
+            />
+          </el-form-item>
+#elseif($field.queryFormType == 'datetime')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-date-picker 
+              type="datetime" 
+              placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" 
+              v-model="state.queryForm.${field.attrName}"
+              :value-format="dateTimeStr"
+            />
+          </el-form-item>
+#elseif($field.formType == 'radio')
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-radio-group v-model="state.queryForm.${field.attrName}">
+#if($field.fieldDict)
+              <el-radio 
+                :label="item.value" 
+                v-for="(item, index) in ${field.fieldDict}" 
+                border 
+                :key="index"
+              >
+                {{ item.label }}
+              </el-radio>
+#else
+              <el-radio label="${field.fieldComment}" border>
+                ${field.fieldComment}
+              </el-radio>
+#end
+            </el-radio-group>
+          </el-form-item>
+#else
+          <el-form-item label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" prop="${field.attrName}">
+            <el-input 
+              placeholder="请输入#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" 
+              v-model="state.queryForm.${field.attrName}" 
+            />
+          </el-form-item>
+#end
+#end
+          <el-form-item>
+            <el-button icon="search" type="primary" @click="getDataList">
+              查询
+            </el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-row>
+#end
+
+      <!-- 操作按钮区域 -->
+      <el-row>
+        <div class="mb8" style="width: 100%">
+          <el-button 
+            icon="folder-add" 
+            type="primary" 
+            class="ml10" 
+            @click="formDialogRef.openDialog()"
+            v-auth="'${moduleName}_${functionName}_add'"
+          >
+            新增
+          </el-button>
+
+          <el-button 
+            plain 
+            :disabled="multiple" 
+            icon="Delete" 
+            type="primary"
+            v-auth="'${moduleName}_${functionName}_del'" 
+            @click="handleDelete(selectObjs)"
+          >
+            删除
+          </el-button>
+
+          <el-button 
+            plain 
+            :icon="isExpand ? 'FolderOpened' : 'Folder'" 
+            type="primary"
+            @click="handleExpand"
+          >
+            {{ isExpand ? '全部折叠' : '全部展开' }}
+          </el-button>
+
+          <right-toolbar 
+            v-model:showSearch="showSearch" 
+            class="ml10 mr20" 
+            style="float: right;"
+            @queryTable="getDataList"
+          />
+        </div>
+      </el-row>
+
+      <!-- 树形数据表格区域 -->
+      <el-table 
+        ref="tableRef"
+        :data="state.dataList" 
+        v-loading="state.loading" 
+        border 
+        row-key="${pk.attrName}"
+        default-expand-all
+        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+        :cell-style="tableStyle.cellStyle" 
+        :header-cell-style="tableStyle.headerCellStyle"
+        @selection-change="selectionChangHandle"
+        @expand-change="onExpandChange"
+      >
+        <el-table-column type="selection" width="40" align="center" />
+        <el-table-column type="index" label="#" width="40" />
+#foreach($field in $gridList)
+#if($field.fieldDict)
+        <el-table-column prop="${field.attrName}" label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" show-overflow-tooltip>
+          <template #default="scope">
+            <dict-tag :options="${field.fieldDict}" :value="scope.row.${field.attrName}" />
+          </template>
+        </el-table-column>
+#else
+        <el-table-column 
+          prop="${field.attrName}" 
+          label="#if(${field.fieldComment})${field.fieldComment}#else${field.attrName}#end" 
+          show-overflow-tooltip
+#if($field == $gridList[0])
+          width="200"
+#end
+        />
+#end
+#end
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="scope">
+            <el-button 
+              icon="plus" 
+              text 
+              type="primary" 
+              v-auth="'${moduleName}_${functionName}_add'"
+              @click="formDialogRef.openDialog(undefined, scope.row.${pk.attrName})"
+            >
+              新增
+            </el-button>
+            <el-button 
+              icon="edit-pen" 
+              text 
+              type="primary" 
+              v-auth="'${moduleName}_${functionName}_edit'"
+              @click="formDialogRef.openDialog(scope.row.${pk.attrName})"
+            >
+              编辑
+            </el-button>
+            <el-button 
+              icon="delete" 
+              text 
+              type="primary" 
+              v-auth="'${moduleName}_${functionName}_del'" 
+              @click="handleDelete([scope.row.${pk.attrName}])"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 编辑、新增弹窗 -->
+    <form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
+  </div>
+</template>
+
+<script setup lang="ts" name="system${ClassName}Tree">
+// ========== 导入声明 ==========
+import { BasicTableProps, useTable } from "/@/hooks/table";
+import { fetchTreeList, delObjs } from "/@/api/${moduleName}/${functionName}";
+import { useMessage, useMessageBox } from "/@/hooks/message";
+import { useDict } from '/@/hooks/dict';
+
+// ========== 组件声明 ==========
+// 异步加载表单弹窗组件
+const FormDialog = defineAsyncComponent(() => import('./form.vue'));
+
+// ========== 字典数据 ==========
+#set($fieldDict=[])
+#foreach($field in $queryList)
+#if($field.fieldDict)
+#set($void=$fieldDict.add($field.fieldDict))
+#end
+#end
+#foreach($field in $gridList)
+#if($field.fieldDict)
+#set($void=$fieldDict.add($field.fieldDict))
+#end
+#end
+#if($fieldDict && $fieldDict.size() > 0)
+// 加载字典数据
+const { $dict.format($fieldDict) } = useDict($dict.quotation($fieldDict));
+#end
+
+// ========== 组件引用 ==========
+const formDialogRef = ref();          // 表单弹窗引用
+const queryRef = ref();               // 查询表单引用
+const tableRef = ref();               // 表格引用
+
+// ========== 响应式数据 ==========
+const showSearch = ref(true);         // 是否显示搜索区域
+const selectObjs = ref([]) as any;    // 表格多选数据
+const multiple = ref(true);           // 是否多选
+const isExpand = ref(true);           // 是否展开状态,默认展开
+
+// ========== 表格状态 ==========
+const state: BasicTableProps = reactive<BasicTableProps>({
+  isPage: false,  // 是否分页
+  queryForm: {},      // 查询参数
+  pageList: fetchTreeList, // 树形数据查询方法(不分页)
+  loading: false,     // 加载状态
+  dataList: []        // 数据列表
+});
+
+// ========== Hook引用 ==========
+// 表格相关Hook (树形表格不使用分页)
+const {
+  getDataList,
+  tableStyle
+} = useTable(state);
+
+// ========== 方法定义 ==========
+/**
+ * 重置查询条件
+ */
+const resetQuery = () => {
+  // 清空搜索条件
+  queryRef.value?.resetFields();
+  // 清空多选
+  selectObjs.value = [];
+  // 重新查询
+  getDataList();
+};
+
+/**
+ * 表格多选事件处理
+ * @param objs 选中的数据行
+ */
+const selectionChangHandle = (objs: { ${pk.attrName}: string }[]) => {
+  selectObjs.value = objs.map(({ ${pk.attrName} }) => ${pk.attrName});
+  multiple.value = !objs.length;
+};
+
+/**
+ * 树形表格展开状态变化事件
+ * 当用户手动点击展开/折叠按钮时触发
+ * @param row 当前行数据
+ * @param expanded 是否展开
+ */
+const onExpandChange = (row: any, expanded: boolean) => {
+  // 可以在这里添加额外的逻辑,比如记录展开状态等
+  // console.log('Row expand changed:', row, expanded);
+};
+
+/**
+ * 删除数据处理
+ * @param ids 要删除的数据ID数组
+ */
+const handleDelete = async (ids: string[]) => {
+  try {
+    await useMessageBox().confirm('此操作将永久删除选中数据及其子数据,是否继续?');
+  } catch {
+    return;
+  }
+
+  try {
+    await delObjs(ids);
+    getDataList();
+    useMessage().success('删除成功');
+  } catch (err: any) {
+    useMessage().error(err.msg);
+  }
+};
+
+/**
+ * 展开/折叠树形表格
+ * 基于Element Plus官方文档的toggleRowExpansion方法实现
+ */
+const handleExpand = () => {
+  isExpand.value = !isExpand.value;
+  
+  // 等待DOM更新后再进行展开/折叠操作
+  nextTick(() => {
+    if (state.dataList && state.dataList.length > 0 && tableRef.value) {
+      toggleExpand(state.dataList, isExpand.value);
+    }
+  });
+};
+
+/**
+ * 递归展开/折叠所有节点
+ * 使用Element Plus Table组件的toggleRowExpansion方法
+ * @param children 子节点数组
+ * @param unfold 是否展开
+ */
+const toggleExpand = (children: any[], unfold = true) => {
+  for (const item of children) {
+    // 使用Element Plus官方的toggleRowExpansion方法
+    // 第二个参数为可选的布尔值,直接设置展开状态
+    tableRef.value?.toggleRowExpansion(item, unfold);
+    
+    // 递归处理子节点
+    if (item.children && item.children.length > 0) {
+      toggleExpand(item.children, unfold);
+    }
+  }
+};
+
+// ========== 生命周期 ==========
+// 组件挂载时获取数据
+onMounted(() => {
+  getDataList();
+});
+</script>