Trong hệ thống quản lý tổ chức, dữ liệu thường được lưu trữ dạng bảng phẳng với mã tổ chức và mã cấp trên. Để hiển thị trên giao diện Vue dưới dạng cây phân cấp, cần chuyển đổi dữ liệu này thành cấu trúc đệ quy gồm các trường label và children.
Entity định nghĩa nút cây
Class Java đại diện cho mỗi nút trong cây:
@Data
@Accessors(chain = true)
public class OrgTreeNode implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long parentId;
private String orgCode;
private String label;
private List<OrgTreeNode> children;
}
Xây dựng cây từ danh sách phẳng
Phương pháp tối ưu sử dụng Map để tra cứu nhanh, độ phức tạp O(n):
@Service
public class OrgStructureService {
public ResponseData<List<OrgTreeNode>> buildOrgHierarchy() {
try {
QueryWrapper<Department> query = new QueryWrapper<>();
query.orderByAsc("parent_code");
List<Department> flatDepts = deptMapper.selectList(query);
List<OrgTreeNode> rootNodes = new ArrayList<>();
Map<String, OrgTreeNode> codeToNodeMap = new HashMap<>();
for (Department dept : flatDepts) {
OrgTreeNode node = convertToNode(dept);
codeToNodeMap.put(dept.getDeptCode(), node);
if ("0".equals(dept.getParentCode())) {
node.setParentId(0L);
rootNodes.add(node);
}
}
for (Department dept : flatDepts) {
if (!"0".equals(dept.getParentCode())) {
OrgTreeNode current = codeToNodeMap.get(dept.getDeptCode());
OrgTreeNode parent = codeToNodeMap.get(dept.getParentCode());
if (parent != null) {
current.setParentId(parent.getId());
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(current);
}
}
}
return ResponseUtil.success(rootNodes);
} catch (Exception ex) {
return exceptionHandler.handleException(ex);
}
}
private OrgTreeNode convertToNode(Department source) {
OrgTreeNode target = new OrgTreeNode();
target.setId(source.getId());
target.setOrgCode(source.getDeptCode());
target.setLabel(source.getDeptName());
target.setChildren(new ArrayList<>());
return targetNode;
}
}
Thành phần Vue hiển thị cây tổ chức
Component Vue với chức năng tìm kiếm và lọc:
<template>
<div class="org-tree-container">
<el-input
v-model="searchKeyword"
class="search-box"
placeholder="Nhập từ khóa để tìm kiếm"
clearable
>
<i slot="prefix" class="el-icon-search" />
</el-input>
<el-tree
ref="orgTree"
:data="hierarchyData"
node-key="id"
:props="displayConfig"
:filter-node-method="matchNode"
default-expand-all
highlight-current
@node-click="onNodeSelect"
/>
</div>
</template>
<script>
import { fetchOrgTree } from "@/api/organization"
export default {
name: "OrgHierarchyTree",
data() {
return {
searchKeyword: "",
hierarchyData: [],
displayConfig: {
label: "label",
children: "children"
}
}
},
watch: {
searchKeyword(val) {
this.$refs.orgTree.filter(val.trim())
}
},
created() {
this.loadHierarchy()
},
methods: {
async loadHierarchy() {
try {
const response = await fetchOrgTree()
if (response.code === 200) {
this.hierarchyData = response.data
} else {
this.$notify.error(response.message)
}
} catch (err) {
console.error("Lỗi tải dữ liệu:", err)
}
},
onNodeSelect(nodeData) {
this.$emit("org-selected", nodeData)
},
matchNode(query, node) {
if (!query) return true
return node.label?.toLowerCase().includes(query.toLowerCase())
}
}
}
</script>
Ưu điểm của phương pháp Map-based
So với đệ quy duyệt cây trực tiếp, cách tiếp cận sử dụng HashMap có những li thế:
- Độ phức tạp giảm từ O(n²) xuống O(n)
- Tránh lặp lại nhiều lần qua các nhánh đã xử lý
- Dễ dàng xử lý các trường hợp dữ liệu không theo thứ tự
- Thuận tiện cho việc mở rộng thêm metadata vào các nút