将查询到的学员信息以所在部门的层级显示出来,树的结构
后端返回数组格式
参考代码如下:
[ { 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" />
遇到难点:
我开始是没有确定好思路,主要问题是将后端传来的数据,进行处理为树结构,还是直接传已经格式好的树形数据没有想清楚;根据自己的情况来定,如果前端好,就在前端处理,相反则在后端处理好;没有太大的难点;还是代码经验少;多练习,多看;