Browse Source

feat: implement tree structure CRUD functionality with corresponding API, service, and UI components

PIG AI 1 week ago
parent
commit
74506c8b55
7 changed files with 1320 additions and 0 deletions
  1. 52 0
      config.json
  2. 214 0
      tree/树形Controller.java
  3. 43 0
      tree/树形Service.java
  4. 152 0
      tree/树形ServiceImpl.java
  5. 176 0
      tree/树形api.ts
  6. 326 0
      tree/树形表单.vue
  7. 357 0
      tree/树形表格.vue

+ 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": "single/实体.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"
+    }
   ]
 }

+ 214 - 0
tree/树形Controller.java

@@ -0,0 +1,214 @@
+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));
+    }
+
+    /**
+     * 获取所有父级节点
+     * @return R
+     */
+    @Operation(summary = "获取所有父级节点" , description = "获取所有父级节点" )
+    @GetMapping("/parent" )
+    #if($isSpringBoot3)
+    @HasPermission("${moduleName}_${functionName}_view")
+    #else
+    @PreAuthorize("@pms.hasPermission('${moduleName}_${functionName}_view')" )
+    #end
+    public R getParentNodes() {
+        return R.ok(${className}Service.getParentNodes());
+    }
+} 

+ 43 - 0
tree/树形Service.java

@@ -0,0 +1,43 @@
+package ${package}.${moduleName}.service;
+
+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<${ClassName}Entity> buildTree(LambdaQueryWrapper<${ClassName}Entity> wrapper);
+
+    /**
+     * 获取所有父级节点
+     * @return 父级节点列表
+     */
+    List<${ClassName}Entity> getParentNodes();
+
+    /**
+     * 根据父ID获取子节点
+     * @param parentId 父级ID
+     * @return 子节点列表
+     */
+    List<${ClassName}Entity> getChildrenByParentId(${pk.attrType} parentId);
+
+    /**
+     * 递归删除节点及其子节点
+     * @param ids 要删除的节点ID列表
+     * @return 删除结果
+     */
+    boolean removeBatchByIds(List<${pk.attrType}> ids);
+} 

+ 152 - 0
tree/树形ServiceImpl.java

@@ -0,0 +1,152 @@
+package ${package}.${moduleName}.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+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<${ClassName}Entity> buildTree(LambdaQueryWrapper<${ClassName}Entity> wrapper) {
+        // 查询所有数据
+        List<${ClassName}Entity> allList = list(wrapper);
+        
+        if (CollUtil.isEmpty(allList)) {
+            return new ArrayList<>();
+        }
+
+        // 按父ID分组
+        Map<${pk.attrType}, List<${ClassName}Entity>> groupByParent = allList.stream()
+                .collect(Collectors.groupingBy(item -> {
+                    // 假设有parentId字段,如果没有请根据实际情况调整
+                    return item.getParentId() != null ? item.getParentId() : 0L;
+                }));
+
+        // 构建树形结构
+        return buildTreeRecursive(groupByParent, 0L);
+    }
+
+    /**
+     * 递归构建树形结构
+     * @param groupByParent 按父ID分组的数据
+     * @param parentId 父ID
+     * @return 树形结构数据
+     */
+    private List<${ClassName}Entity> buildTreeRecursive(Map<${pk.attrType}, List<${ClassName}Entity>> groupByParent, ${pk.attrType} parentId) {
+        List<${ClassName}Entity> children = groupByParent.get(parentId);
+        if (CollUtil.isEmpty(children)) {
+            return new ArrayList<>();
+        }
+
+        children.forEach(child -> {
+            List<${ClassName}Entity> subChildren = buildTreeRecursive(groupByParent, child.get${str.capitalizeFirst($pk.attrName)}());
+            child.setChildren(subChildren);
+            child.setHasChildren(!CollUtil.isEmpty(subChildren));
+        });
+
+        return children;
+    }
+
+    /**
+     * 获取所有父级节点
+     * @return 父级节点列表
+     */
+    @Override
+    public List<${ClassName}Entity> getParentNodes() {
+        LambdaQueryWrapper<${ClassName}Entity> wrapper = Wrappers.lambdaQuery();
+        // 查询所有父级节点(parentId为null或0的节点)
+        wrapper.and(w -> w.isNull(${ClassName}Entity::getParentId).or().eq(${ClassName}Entity::getParentId, 0));
+        wrapper.orderByAsc(${ClassName}Entity::getSort); // 假设有sort字段用于排序
+        return list(wrapper);
+    }
+
+    /**
+     * 根据父ID获取子节点
+     * @param parentId 父级ID
+     * @return 子节点列表
+     */
+    @Override
+    public List<${ClassName}Entity> getChildrenByParentId(${pk.attrType} parentId) {
+        LambdaQueryWrapper<${ClassName}Entity> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(${ClassName}Entity::getParentId, parentId);
+        wrapper.orderByAsc(${ClassName}Entity::getSort); // 假设有sort字段用于排序
+        return list(wrapper);
+    }
+
+    /**
+     * 递归删除节点及其子节点
+     * @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::getParentId, parentId);
+        wrapper.select(${ClassName}Entity::get${str.capitalizeFirst($pk.attrName)});
+        
+        List<${ClassName}Entity> children = list(wrapper);
+        
+        for (${ClassName}Entity child : children) {
+            ${pk.attrType} childId = child.get${str.capitalizeFirst($pk.attrName)}();
+            childIds.add(childId);
+            // 递归获取子节点的子节点
+            childIds.addAll(getChildIdsRecursive(childId));
+        }
+        
+        return childIds;
+    }
+} 

+ 176 - 0
tree/树形api.ts

@@ -0,0 +1,176 @@
+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
+  })
+}
+
+/**
+ * 获取所有父级节点
+ * @returns Promise<父级节点列表>
+ */
+export function getParentNodes() {
+  return request({
+    url: '/${moduleName}/${functionName}/parent',
+    method: 'get'
+  })
+}
+
+/**
+ * 根据父ID获取子节点
+ * @param parentId - 父级ID
+ * @returns Promise<子节点列表>
+ */
+export function getChildrenByParentId(parentId: string | number) {
+  return request({
+    url: '/${moduleName}/${functionName}/children',
+    method: 'get',
+    params: { parentId }
+  })
+}
+
+// ========== 工具函数 ==========
+
+/**
+ * 验证字段值唯一性
+ * @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();
+    }
+  });
+}
+
+/**
+ * 构建前端树形结构数据
+ * @param data - 平铺数据数组
+ * @param parentId - 父级ID,默认为0或null
+ * @returns 树形结构数据
+ */
+export function buildTreeData(data: any[], parentId: string | number | null = null): any[] {
+  const result: any[] = [];
+  
+  data.forEach(item => {
+    if (item.parentId === parentId || (parentId === null && (item.parentId === null || item.parentId === 0 || item.parentId === '0'))) {
+      const children = buildTreeData(data, item.${pk.attrName});
+      const node = {
+        ...item,
+        children: children.length > 0 ? children : undefined,
+        hasChildren: children.length > 0
+      };
+      result.push(node);
+    }
+  });
+  
+  return result;
+}
+
+/**
+ * 扁平化树形数据
+ * @param treeData - 树形数据
+ * @returns 扁平化后的数据数组
+ */
+export function flattenTreeData(treeData: any[]): any[] {
+  const result: any[] = [];
+  
+  function traverse(nodes: any[]) {
+    nodes.forEach(node => {
+      const { children, ...nodeData } = node;
+      result.push(nodeData);
+      
+      if (children && children.length > 0) {
+        traverse(children);
+      }
+    });
+  }
+  
+  traverse(treeData);
+  return result;
+} 

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

@@ -0,0 +1,326 @@
+<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">
+        <!-- 父级节点选择 -->
+        <el-col :span="24" class="mb20">
+          <el-form-item label="父级节点" prop="parentId">
+            <el-tree-select
+              v-model="form.parentId"
+              :data="parentNodes"
+              :props="treeSelectProps"
+              check-strictly
+              :render-after-expand="false"
+              placeholder="请选择父级节点"
+              style="width: 100%"
+              clearable
+            />
+          </el-form-item>
+        </el-col>
+#foreach($field in $formList)
+#if($field.attrName != ${pk.attrName} && $field.attrName != 'parentId')
+#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">
+// ========== 1. 导入语句 ==========
+import { useDict } from '/@/hooks/dict';
+import { rule } from '/@/utils/validate';
+import { useMessage } from "/@/hooks/message";
+import { getObj, addObj, putObj, validateExist, getParentNodes } from '/@/api/${moduleName}/${functionName}';
+
+// ========== 2. 组件定义 ==========
+// 定义组件事件
+const emit = defineEmits(['refresh']);
+
+// ========== 3. 响应式数据定义 ==========
+// 基础响应式变量
+const dataFormRef = ref(); // 表单引用
+const visible = ref(false); // 弹窗显示状态
+const loading = ref(false); // 加载状态
+const parentNodes = ref([]); // 父级节点数据
+
+// 树形选择器配置
+const treeSelectProps = {
+  children: 'children',
+  label: 'name', // 假设显示字段为name,请根据实际情况调整
+  value: '${pk.attrName}',
+  checkStrictly: true
+};
+
+// 表单数据对象
+const form = reactive({
+#if(!$formList.contains(${pk.attrName}))
+  ${pk.attrName}: '', // 主键
+#end
+  parentId: null, // 父级ID
+#foreach($field in $formList)
+#if($field.attrName != 'parentId')
+#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
+});
+
+// ========== 4. 字典数据处理 ==========
+#set($fieldDict=[])
+#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
+
+// ========== 5. 表单校验规则 ==========
+const dataRules = ref({
+#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
+});
+
+// ========== 6. 方法定义 ==========
+// 获取详情数据
+const get${ClassName}Data = async (id: string) => {
+  try {
+    loading.value = true;
+    const { data } = await getObj({ ${pk.attrName}: id });
+    // 直接将第一条数据赋值给表单
+    Object.assign(form, data[0]);
+  } catch (error) {
+    useMessage().error('获取数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取父级节点数据
+const getParentNodesList = async () => {
+  try {
+    const { data } = await getParentNodes();
+    // 添加根节点选项
+    parentNodes.value = [
+      { ${pk.attrName}: null, name: '根节点', children: [] },
+      ...buildParentTree(data)
+    ];
+  } catch (error) {
+    console.error('获取父级节点失败:', error);
+    parentNodes.value = [{ ${pk.attrName}: null, name: '根节点', children: [] }];
+  }
+};
+
+// 构建父级节点树形结构
+const buildParentTree = (data: any[]) => {
+  // 如果后端已经返回树形结构,直接返回
+  if (data && data.length > 0 && data[0].children !== undefined) {
+    return data;
+  }
+  
+  // 如果是平铺数据,需要构建树形结构
+  const map = new Map();
+  const roots: any[] = [];
+  
+  data.forEach(item => {
+    map.set(item.${pk.attrName}, { ...item, children: [] });
+  });
+  
+  data.forEach(item => {
+    const node = map.get(item.${pk.attrName});
+    if (item.parentId) {
+      const parent = map.get(item.parentId);
+      if (parent) {
+        parent.children.push(node);
+      } else {
+        roots.push(node);
+      }
+    } else {
+      roots.push(node);
+    }
+  });
+  
+  return roots;
+};
+
+// 打开弹窗方法
+const openDialog = (id: string, parentId?: string) => {
+  visible.value = true;
+  form.${pk.attrName} = '';
+  form.parentId = parentId || null;
+
+  // 获取父级节点数据
+  getParentNodesList();
+
+  // 重置表单数据
+  nextTick(() => {
+    dataFormRef.value?.resetFields();
+  });
+
+  // 获取${ClassName}信息
+  if (id) {
+    form.${pk.attrName} = id;
+    get${ClassName}Data(id);
+  }
+};
+
+// 提交表单方法
+const onSubmit = async () => {
+  loading.value = true; // 防止重复提交
+  
+  // 表单校验
+  const valid = await dataFormRef.value.validate().catch(() => {});
+  if (!valid) {
+    loading.value = false;
+    return false;
+  }
+
+  try {
+    // 处理父级ID为空的情况
+    if (!form.parentId) {
+      form.parentId = null;
+    }
+    
+    // 根据是否有ID判断是新增还是修改
+    form.${pk.attrName} ? await putObj(form) : await addObj(form);
+    useMessage().success(form.${pk.attrName} ? '修改成功' : '添加成功');
+    visible.value = false;
+    emit('refresh'); // 通知父组件刷新列表
+  } catch (err: any) {
+    useMessage().error(err.msg);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// ========== 7. 对外暴露 ==========
+// 暴露方法给父组件
+defineExpose({
+  openDialog
+});
+</script> 

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

@@ -0,0 +1,357 @@
+<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 
+            icon="sort" 
+            type="primary" 
+            class="ml10" 
+            @click="expandAll"
+          >
+            展开/折叠
+          </el-button>
+          <el-button 
+            plain 
+            icon="upload-filled" 
+            type="primary" 
+            class="ml10" 
+            @click="excelUploadRef.show()" 
+            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>
+          <right-toolbar 
+            v-model:showSearch="showSearch" 
+            :export="'${moduleName}_${functionName}_export'"
+            @exportExcel="exportExcel" 
+            class="ml10 mr20" 
+            style="float: right;"
+            @queryTable="getDataList"
+          />
+        </div>
+      </el-row>
+
+      <!-- 树形数据表格区域 -->
+      <el-table 
+        :data="state.dataList" 
+        v-loading="state.loading" 
+        border 
+        row-key="${pk.attrName}"
+        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+        :cell-style="tableStyle.cellStyle" 
+        :header-cell-style="tableStyle.headerCellStyle"
+        @selection-change="selectionChangHandle"
+        :default-expand-all="isExpandAll"
+      >
+        <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('', 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)" />
+
+    <!-- 导入excel弹窗 (需要在 upms-biz/resources/file 下维护模板) -->
+    <upload-excel
+      ref="excelUploadRef"
+      title="导入"
+      url="/${moduleName}/${functionName}/import"
+      temp-url="/admin/sys-file/local/file/${functionName}.xlsx"
+      @refreshDataList="getDataList"
+    />
+  </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)
+// 加载字典数据
+const { $dict.format($fieldDict) } = useDict($dict.quotation($fieldDict));
+#end
+
+// ========== 组件引用 ==========
+const formDialogRef = ref();          // 表单弹窗引用
+const excelUploadRef = ref();         // Excel上传弹窗引用
+const queryRef = ref();               // 查询表单引用
+
+// ========== 响应式数据 ==========
+const showSearch = ref(true);         // 是否显示搜索区域
+const selectObjs = ref([]) as any;    // 表格多选数据
+const multiple = ref(true);           // 是否多选
+const isExpandAll = ref(false);       // 是否展开所有节点
+
+// ========== 表格状态 ==========
+const state: BasicTableProps = reactive<BasicTableProps>({
+  queryForm: {},      // 查询参数
+  pageList: fetchTreeList, // 树形数据查询方法(不分页)
+  loading: false,     // 加载状态
+  dataList: []        // 数据列表
+});
+
+// ========== Hook引用 ==========
+// 表格相关Hook (树形表格不使用分页)
+const {
+  getDataList,
+  downBlobFile,
+  tableStyle
+} = useTable(state);
+
+// ========== 方法定义 ==========
+/**
+ * 重置查询条件
+ */
+const resetQuery = () => {
+  // 清空搜索条件
+  queryRef.value?.resetFields();
+  // 清空多选
+  selectObjs.value = [];
+  // 重新查询
+  getDataList();
+};
+
+/**
+ * 展开/折叠所有节点
+ */
+const expandAll = () => {
+  isExpandAll.value = !isExpandAll.value;
+};
+
+/**
+ * 导出Excel文件
+ */
+const exportExcel = () => {
+  downBlobFile(
+    '/${moduleName}/${functionName}/export',
+    Object.assign(state.queryForm, { ids: selectObjs }),
+    '${functionName}.xlsx'
+  );
+};
+
+/**
+ * 表格多选事件处理
+ * @param objs 选中的数据行
+ */
+const selectionChangHandle = (objs: { $pk.attrName: string }[]) => {
+  selectObjs.value = objs.map(({ $pk.attrName }) => $pk.attrName);
+  multiple.value = !objs.length;
+};
+
+/**
+ * 删除数据处理
+ * @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);
+  }
+};
+
+// ========== 生命周期 ==========
+// 组件挂载时获取数据
+onMounted(() => {
+  getDataList();
+});
+</script>
+
+<style scoped>
+.layout-padding {
+  padding: 15px;
+}
+
+.layout-padding-auto {
+  margin: 0 auto;
+}
+
+.layout-padding-view {
+  min-height: calc(100vh - 50px);
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+.ml10 {
+  margin-left: 10px;
+}
+
+.mr20 {
+  margin-right: 20px;
+}
+</style>