Hướng dẫn từng bước xây dựng cây phân cấp động với nút thao tác tùy chỉnh trong Ant Design Vue: Thêm, Xóa, Sửa, Xem và Biểu tượng tùy chỉnh

Xây dựng cây phân cấp doanh nghiệp tùy chỉnh: Tích hợp nâng cao và tương tác thực chiến với Ant Design Vue Tree

Trong việc xây dựng các hệ thống quản trị admin hiện đại, thành phần cây phân cấp (tree) gần như là một yếu tố nền tảng không thể thiếu. Dù là quản lý cơ cấu tổ chức, điều hướng danh mục phân loại, hay cấu hình quyền truy cập phức tạp, một thành phần tree có đầy đủ tính năng và tương tác linh hoạt sẽ nâng cao đáng kể hiệu quả quản lý và trải nghiệm người dùng. Ant Design Vue, với tư cách là một bộ thư viện UI cho doanh nghiệp, thành phần a-tree của nó cung cấp những khả năng nền tảng mạnh mẽ. Tuy nhiên, khi đối mặt với các yêu cầu tùy chỉnh sâu như "thêm nút thao tác động cho mỗi nút", nhiều nhà phát triển thường gặp phải những khó khăn trong việc phối hợp scopedSlotsreplaceFields, hoặc những "sự kiện lạ" khiến nội dung slot tùy chỉnh không được hiển thị.

Hôm nay, chúng ta không chỉ dừng lại ở việc liệt kê API một cách hời hợt, mà sẽ đi sâu vào源码 (nguồn code) và các tình huống thực chiến, từ dữ liệu động, nguyên lý slot đến vòng lặp tương tác, để phân tích toàn diện cách xây dựng một cây có thể thao tác động, hỗ trợ thêm, xóa, sửa, xem và biểu tượng tùy chỉnh. Dù bạn đang đau đầu vì thành phần tree trong dự án, hay muốn trang bị trước những kỹ năng tùy chỉnh thành phần nâng cao, bài viết này sẽ cung cấp một giải pháp rõ ràng, có thể triển khai.

1. Hiểu rõ cốt lõi: Dữ liệu, ánh xạ trường và ưu tiên của slot

Trước khi viết dòng code đầu tiên, chúng ta phải làm rõ logic cốt lõi để a-tree render một nút. Nhiều nhà phát triển bị mắc kẹt ở bước đầu tiên – tại sao nội dung slot tùy chỉnh không được render? Nguyên nhân gốc rễ thường nằm ở việc hiểu không thấu đáo về luồng dữ liệu và thứ tự ưu tiên cấu hình.

Thành phần a-tree render một nút, phụ thuộc chủ yếu vào hai thông tin quan trọng:

  1. Dữ liệu đối tượng nút: Mảng bạn truyền vào qua thuộc tính tree-data, mỗi đối tượng đại diện cho một nút.
  2. Quy tắc ánh xạ trường: Thông qua thuộc tính replaceFields, bạn chỉ cho component biết trong đối tượng dữ liệu của mình, các trường khóa như children (nút con), title (văn bản hiển thị), key (định danh duy nhất) tương ứng với tên thuộc tính nào.

Khi component chuẩn bị render title (nội dung hiển thị) của một nút, nó sẽ tuân theo một thứ tự tìm kiếm rõ ràng:

  1. Đầu tiên: Kiểm tra đối tượng dữ liệu nút đó có chỉ định tên slot tùy chỉnh thông qua thuộc tính scopedSlots hay không.
  2. Thứ hai: Nếu không tìm thấy scopedSlots, nó sẽ xem xét xem component bên ngoài có định nghĩa slot tiêu đề mặc định thông qua <template #title> hoặc v-slot:title hay không.
  3. Cuối cùng: Nếu cả hai đều không có, nó sẽ quay lại sử dụng trường title được chỉ định trong replaceFields để render giá trị chuỗi của trường đó.

Tại đây tồn tại một "cái bẫy" phổ biến: nếu bạn không đính kèm đúng cách cấu hình scopedSlots cho mỗi nút trong quá trình khởi tạo dữ liệu, thì ngay cả khi bạn định nghĩa một template slot đẹp mắt trong template của component, component cũng sẽ bỏ qua nội dung slot của bạn vì không tìm thấy chỉ dẫn, và thay vào đó sử dụng giá trị trường được ánh xạ bởi replaceFields để render, dẫn đến việc nội dung slot của bạn bị bỏ qua hoàn toàn.

Hãy dùng một bảng để so sánh rõ ràng ba cách này về điều kiện và ưu tiên:

Phương thức render Vị trí cấu hình Điều kiện hoạt động Ứng dụng điển hình Ưu tiên
Slot ở cấp nút (scopedSlots) Trong đối tượng dữ liệu nút Đối tượng nút chứa scopedSlots: { title: 'tenSlotTuongUng' } Các nút khác loại cần template render hoàn toàn khác nhau (ví dụ: nút phòng ban hiển thị người phụ trách, nút tệp hiển thị biểu tượng) Cao nhất
Slot mặc định ở cấp component Trong <template> của component Chưa thiết lập slot ở cấp nút Tất cả các nút sử dụng template tiêu đề tùy chỉnh thống nhất, phức tạp (ví dụ: đều có nút thao tác) Trung bình
Ánh xạ trường (replaceFields) Thuộc tính replaceFields của component Cả hai trên đều không hoạt động Cấu trúc dữ liệu chuẩn, chỉ cần hiển thị văn bản đơn giản, ưu tiên cấu hình tối giản Thấp nhất

Lưu ý: Trong phiên bản Ant Design Vue 3.x, cách cấu hình scopedSlots đã tiến hóa thành cú pháp v-slot phù hợp hơn với phong cách API tổ hợp của Vue 3, nhưng ý tưởng cốt lõi "dữ liệu nút điều khiển slot" vẫn không thay đổi. Ví dụ trong bài viết này dựa trên cú pháp Vue 2, người dùng Vue 3 cần lưu ý chuyển đổi cú pháp.

Hiểu được điều này, chúng ta biết rằng để thực hiện nút thao tác động, chìa khóa nằm ở việc sử dụng "slot ở cấp nút" hoặc "slot mặc định ở cấp component" và đảm bảo cấu hình chính xác, từ đó bỏ qua ánh xạ trường đơn giản. Tiếp theo, chúng ta sẽ bắt đầu từ nguồn dữ liệu, từng bước xây dựng.

2. Bắt đầu từ nguồn dữ liệu: Xây dựng cấu trúc dữ liệu hỗ trợ thao tác động

Mọi tương tác đều dựa trên dữ liệu. Mục tiêu của chúng ta là thêm "thông tin siêu dữ liệu" mang tính "thao tác được" vào mô hình dữ liệu của nút tree.

Giả sử chúng ta đang phát triển một module "Quản lý cơ cấu tổ chức", dữ liệu gốc trả về từ backend có thể như sau:

[
  {
    "maDonVi": 1,
    "tenDonVi": "Ban Giám đốc",
    "maDonViCha": 0,
    "thuTu": 1,
    "loai": "donvi",
    "nhanVien": [
      {
        "maNhanVien": 101,
        "hoTen": "Nguyen Van A",
        "chucVu": "Tổng Giám đốc"
      }
    ],
    "con": [
      {
        "maDonVi": 2,
        "tenDonVi": "Phòng Kinh doanh",
        "maDonViCha": 1,
        "thuTu": 1,
        "loai": "donvi",
        "nhanVien": [
          {
            "maNhanVien": 102,
            "hoTen": "Tran Thi B",
            "chucVu": "Trưởng phòng"
          }
        ],
        "con": []
      }
    ]
  }
]

Để sử dụng trong component tree ở frontend và thêm khả năng thao tác, chúng ta cần chuyển đổi dữ liệu. Quá trình chuyển đổi này thường diễn ra sau khi yêu cầu dữ liệu thành công.

// Giả sử đây là phương thức yêu cầu API của bạn
async function layCayPhanCapDonVi() {
  const phanHoi = await axios.get('/api/donvi/cay-phan-cap');
  if (phanHoi.data.maTrangThai === 0) {
    const duLieuNguon = phanHoi.data.duLieu;
    // Bước quan trọng: Chuyển đổi dữ liệu, tiêm cấu hình slot và siêu dữ liệu cần thiết cho thao tác
    this.duLieuCay = this.chuyenDoiDuLieuCay(duLieuNguon);
  }
}

// Hàm chuyển đổi dữ liệu
function chuyenDoiDuLieuCay(nut) {
  if (!Array.isArray(nut)) return [];
  
  return nut.map(nutDonVi => {
    // 1. Xây dựng cấu trúc dữ liệu cơ bản phù hợp với a-tree
    const nutDaChuyenDoi = {
      // key là bắt buộc và phải duy nhất, thường dùng id trực tiếp
      key: nutDonVi.maDonVi.toString(),
      // Trường title gốc, dùng để fallback hiển thị hoặc tìm kiếm
      title: nutDonVi.tenDonVi,
      // Giữ lại dữ liệu gốc, tiện cho việc lấy trong scope của slot
      duLieuGoc: nutDonVi,
      // 2. Tiêm siêu dữ liệu tùy chỉnh, dùng để điều khiển logic UI
      // Ví dụ: Quyết định xem có hiển thị nút "Thêm cấp dưới" dựa trên loại nút
      coTheThemCon: nutDonVi.loai === 'donvi',
      // ... các thuộc tính khác cần thiết cho UI
    };
    
    // Xử lý đệ quy cho các nút con
    if (nutDonVi.con && nutDonVi.con.length > 0) {
      nutDaChuyenDoi.children = chuyenDoiDuLieuCay(nutDonVi.con);
    }
    
    return nutDaChuyenDoi;
  });
}

Thẻ: AntDesignVue cây phân cấp scopedSlots Vue.js CRUD

Đăng vào ngày 1 tháng 6 lúc 01:10