springboot+mysql+Vue3+elementplus,实现左侧树结构开发

0 阅读4分钟

将查询到的学员信息以所在部门的层级显示出来,树的结构

后端返回数组格式

参考代码如下:

[ { id: 1, label: '总部', type: 'dept' children: [ { id: 2, label: '一局', type: 'dept' children: [ { id: 4, label: '一处', children: [ { id: 5, label: '学员2', type: 'student', children:null }, { id: 6, label: '学员', type: 'student', children:null },

        ],
      },
    ],
  },
  {
    id: 3,
    label: '二局',
	type: 'dept'
	children:null
  },
]}]

controller层

/**
 * 查询学员的部门,以及个人信息,组装程树形树结构,通过courseId查询
 */
@GetMapping("/student/deptAndInfo/{courseId}")
public R<List<SudentDeptTreeVo>> studentDeptAndInfo(@PathVariable String courseId) {
    return R.ok(teacherStatisticsService.selectStudentDeptAndInfo(Long.valueOf(courseId)));
}

serviceimpl层

@Override
public List<SudentDeptTreeVo> selectStudentDeptAndInfo(Long courseId) {

    // 1. 根据 courseId 查询该课程下的所有学员信息(包含学员ID、姓名、部门ID)
    List<SysUserVo> studentList = teacherStatisticsMapper.selectStudentListByUserIdList(String.valueOf(courseId));
    if (CollectionUtils.isEmpty(studentList)) {
        return Collections.emptyList();
    }
    // 2. 收集所有涉及的部门ID(去重)
    Set<Long> deptIds = studentList.stream()
        .map(SysUserVo::getDeptId)
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());

    // 3. 查询这些部门及其所有上级部门(一直递归到根部门),保证树结构完整
    List<SysDeptVo> allRelatedDepts = externalPlanMapper.selectDeptAndParentDept(deptIds);
    // 4. 将部门列表转换为树形结构(SudentDeptTreeVo)
    //    部门节点 type = "dept",学员节点 type = "student"
    List<SudentDeptTreeVo> deptTree = buildDeptTree(allRelatedDepts);

    // 5. 构建部门ID -> 部门节点映射,便于后续挂载学员
    Map<Long, SudentDeptTreeVo> deptNodeMap = new HashMap<>();
    flattenDeptTree(deptTree, deptNodeMap);
    // 6. 将学员转换为 SudentDeptTreeVo 节点,并挂载到对应部门的 children 中
    for (SysUserVo student : studentList) {
        Long deptId = student.getDeptId();
        SudentDeptTreeVo deptNode = deptNodeMap.get(deptId);
        if (deptNode != null) {
            SudentDeptTreeVo studentNode = new SudentDeptTreeVo();
            studentNode.setId(String.valueOf(student.getUserId()));
            studentNode.setLabel(student.getNickName());
            studentNode.setType("student");
            studentNode.setParentId(String.valueOf(deptId));
            // 学员节点无子节点
            studentNode.setChildren(null);

            if (deptNode.getChildren() == null) {
                deptNode.setChildren(new ArrayList<>());
            }
            deptNode.getChildren().add(studentNode);
        }
    }

    // 7. 返回树结构(仅根部门节点列表)
    return deptTree;


}

private List<SudentDeptTreeVo> buildDeptTree(List<SysDeptVo> allRelatedDepts) {
    // 创建 ID -> 节点映射
    Map<Long, SudentDeptTreeVo> nodeMap = new HashMap<>();
    for (SysDeptVo dept : allRelatedDepts) {
        SudentDeptTreeVo node = new SudentDeptTreeVo();
        node.setId(String.valueOf(dept.getDeptId()));
        node.setLabel(dept.getDeptName());
        node.setType("dept");
        node.setParentId(dept.getParentId() != null ? String.valueOf(dept.getParentId()) : null);
        node.setChildren(new ArrayList<>());
        nodeMap.put(dept.getDeptId(), node);
    }

    List<SudentDeptTreeVo> roots = new ArrayList<>();
    for (SudentDeptTreeVo node : nodeMap.values()) {
        String parentId = node.getParentId();
        if (parentId == null || parentId.isEmpty() || "0".equals(parentId)) {
            roots.add(node);
        } else {
            SudentDeptTreeVo parent = nodeMap.get(Long.valueOf(parentId));
            if (parent != null) {
                if (parent.getChildren() == null) {
                    parent.setChildren(new ArrayList<>());
                }
                parent.getChildren().add(node);
            } else {
                // 若父节点不在本次查询结果中(理论上不应发生),可作为根处理
                roots.add(node);
            }
        }
    }

    // 清理空的 children 列表(前端可能期望 null 而非空数组,根据实际需要调整)
    cleanEmptyChildren(roots);
    return roots;
}

/**
 * 递归清理空的 children 列表(设为 null)
 */
private void cleanEmptyChildren(List<SudentDeptTreeVo> nodes) {
    if (nodes == null) {
        return;
    }
    for (SudentDeptTreeVo node : nodes) {
        if (node.getChildren() != null && node.getChildren().isEmpty()) {
            node.setChildren(null);
        } else {
            cleanEmptyChildren(node.getChildren());
        }
    }
}

/**
 * 将树形结构扁平化,填充到 Map 中(ID -> 节点)
 */
private void flattenDeptTree(List<SudentDeptTreeVo> nodes, Map<Long, SudentDeptTreeVo> map) {
    if (nodes == null) {
        return;
    }
    for (SudentDeptTreeVo node : nodes) {
        map.put(Long.valueOf(node.getId()), node);
        flattenDeptTree(node.getChildren(), map);
    }
}

mapper层

<select id="selectDeptAndParentDept" resultType="org.kvmp.system.domain.vo.SysDeptVo">
    WITH RECURSIVE dept_tree AS (
    -- 初始查询:根据传入的部门ID查询部门信息
    SELECT
    dept_id,
    dept_name,
    parent_id
    FROM sys_dept
    WHERE dept_id IN
    <foreach collection="deptIds" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
    UNION ALL
    -- 递归部分:向上查找父部门
    SELECT
    d.dept_id,
    d.dept_name,
    d.parent_id
    FROM sys_dept d
    INNER JOIN dept_tree dt ON d.dept_id = dt.parent_id
    )
    SELECT DISTINCT * FROM dept_tree
</select>

前端代码

    <div class="student-selector">
      <div class="selector-header">
        <h3>学员选择</h3>
                <el-input
        v-model="filterText"
        class="w-60 mb-2"
        placeholder="搜索学员姓名"
      ><template #prefix>
            <el-icon><Search /></el-icon>
          </template>
    </el-input>
      </div>
        <el-empty v-if="studentDeptOptions.length === 0"  description="暂无学员数据" :image-size="80" />
      
        <el-tree
           v-else
          ref="treeRef"
          :data="studentDeptOptions"
          :props="defaultProps"
          :filter-node-method="filterNode"
          :highlight-current="true"
          :expand-on-click-node="false"
          :default-expand-all="defaultExpandAll"
          @node-click="handleDeptNodeClick"
        />
    </div>
    

js方法

// 方法 拿到后端返回的数据 const loadStudentList = async () => { try { const responseDept = await getStudentDeptOptions(String(props.courseId)); studentDeptOptions.value = responseDept.data || []; // 默认选中树形结构中第一个学员 if (studentDeptOptions.value.length > 0) { selectFirstStudentIfExists(studentDeptOptions.value); }else{ resetExpandState(); } } catch (error) { proxy?.$modal.msgError('加载学员列表失败'); } };

开发思路

1根据courseid 获得学员信息 ; 2根据学员信息获得部门信息; 3组装数据:使用set(不可重复)集合获得部门id ,根据id查询这些部门及其所有上级部门,将部门数据转换为树状结构;使用hashmap

// 创建 ID -> 节点映射
Map<Long, SudentDeptTreeVo> nodeMap = new HashMap<>();

将学员转换为 SudentDeptTreeVo 节点,并挂载到对应部门的 children 中;

数据对象字段

public class SudentDeptTreeVo {

    private String id;

    /**
     * 名字
     */
    private String label;
    
    /**
     * 类型
     */
    private String type;

    /**
     * 父节点ID
     */
    private String parentId;


    private List<SudentDeptTreeVo> children;


}

4前端接受数据 定义接口 // 定义节点类型(根据后端返回结构)

interface StudentDeptTreeNode { id: string | number; label: string; type: 'dept' | 'student'; parentId?: string | null; children?: StudentDeptTreeNode[] | null; } // 学员部门数据结构数据 const studentDeptOptions = ref<StudentDeptTreeNode[]>([]); 使用element组件处理

<el-tree v-else ref="treeRef" :data="studentDeptOptions" :props="defaultProps" :filter-node-method="filterNode" :highlight-current="true" :expand-on-click-node="false" :default-expand-all="defaultExpandAll" @node-click="handleDeptNodeClick" />

遇到难点:

我开始是没有确定好思路,主要问题是将后端传来的数据,进行处理为树结构,还是直接传已经格式好的树形数据没有想清楚;根据自己的情况来定,如果前端好,就在前端处理,相反则在后端处理好;没有太大的难点;还是代码经验少;多练习,多看;