Xây dựng cây phân cấp tổ chức từ dữ liệu phẳng trong Vue và Java

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 labelchildren.

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

Thẻ: Vue.js Element UI Java Spring Boot Tree Data Structure

Đăng vào ngày 21 tháng 8 lúc 23:33