Trong các ứng dụng quản trị, việc hiển thị dữ liệu dạng bảng với các ô bị gộp lại (merge cells) khi có giá trị giống nhau ở các hàng liền kề giúp giao diện gọn gàng và dễ theo dõi hơn. Ant Design Vue cung cấp thuộc tính customCell để tùy biến ô, kết hợp với Vue 3 Composition API, chúng ta có thể xử lý logic gộp dòng (rowSpan) một cách linh hoạt và tối ưu.
1. Cấu hình cột (Column Configuration)
Để thực hiện gộp dòng, chúng ta cần sử dụng hàm customCell trong định nghĩa cột. Hàm này sẽ trả về một object chứa thuộc tính rowSpan. Giá trị của rowSpan sẽ được tính toán động và gắn trực tiếp vào từng bản ghi (record) trong dữ liệu bảng.
Lưu ý: Trong Ant Design Vue, nếu rowSpan bằng 0, ô sẽ bị ẩn đi. Nếu bằng 1, ô hiển thị bình thường. Nếu lớn hơn 1, ô sẽ được mở rộng xuống các hàng bên dưới.
export const tableColumns = [
{
title: 'Phân nhóm cấp 1',
dataIndex: 'groupLevel1',
align: 'center',
width: 120,
customCell: (record) => {
return { rowSpan: record.groupLevel1RowSpan ?? 1 };
},
},
{
title: 'Phân nhóm cấp 2',
dataIndex: 'groupLevel2',
align: 'center',
width: 120,
customCell: (record) => {
return { rowSpan: record.groupLevel2RowSpan ?? 1 };
},
},
{
title: 'Phân nhóm cấp 3',
dataIndex: 'groupLevel3',
align: 'center',
width: 120,
customCell: (record) => {
return { rowSpan: record.groupLevel3RowSpan ?? 1 };
},
},
{
title: 'Loại hình dịch vụ',
dataIndex: 'serviceType',
align: 'center',
width: 150,
}
];
2. Xử lý dữ liệu và tính toán Rowspan
Thay vì tính toán trực tiếp trong template hoặc sử dụng các vòng lặp lồng ghép phức tạp, chúng ta nên tách logic tính toán thành một hàm tiện ích (utility function). Hàm này sẽ duyệt qua danh sách dữ liệu, so sánh các giá trị liền kề của các trường cần gộp, và gán giá trị rowSpan tương ứng.
Đoạn mã dưới đây sử dụng Vue 3 Composition API với watch để tự động cập nhật lại bảng khi dữ liệu nguồn thay đổi, đồng thời tránh việc tính toán trùng lặp.
import { ref, watch } from 'vue';
import { tableColumns } from './tableConfig';
// Dữ liệu bảng gốc nhận từ API
const rawTableData = ref([]);
// Dữ liệu đã qua xử lý để render lên bảng
const processedTableData = ref([]);
// Danh sách các trường (dataIndex) cần thực hiện gộp dòng
const mergeableFields = ['groupLevel1', 'groupLevel2', 'groupLevel3'];
/**
* Hàm tính toán rowspan cho các trường chỉ định
* @param {Array} dataList - Dữ liệu bảng gốc
* @param {Array} fields - Các trường cần gộp
* @returns {Array} - Dữ liệu đã được gắn thuộc tính rowspan
*/
const calculateRowSpans = (dataList, fields) => {
if (!dataList || dataList.length === 0) return [];
// Clone dữ liệu để tránh mutate trực tiếp vào dữ liệu gốc
const clonedData = JSON.parse(JSON.stringify(dataList));
fields.forEach(field => {
const rowSpanKey = `${field}RowSpan`;
let i = 0;
while (i < clonedData.length) {
let j = i + 1;
// Đếm số lượng phần tử liên tiếp có cùng giá trị ở trường hiện tại
while (j < clonedData.length && clonedData[j][field] === clonedData[i][field]) {
j++;
}
const spanCount = j - i;
// Gán giá trị rowspan cho nhóm các phần tử vừa tìm được
for (let k = i; k < j; k++) {
// Phần tử đầu tiên của nhóm nhận tổng số dòng cần gộp
// Các phần tử còn lại nhận giá trị 0 để ẩn ô
clonedData[k][rowSpanKey] = (k === i) ? spanCount : 0;
}
// Di chuyển con trỏ sang nhóm tiếp theo
i = j;
}
});
return clonedData;
};
// Theo dõi sự thay đổi của dữ liệu gốc để trigger tính toán lại
watch(rawTableData, (newData) => {
if (newData && newData.length > 0) {
processedTableData.value = calculateRowSpans(newData, mergeableFields);
} else {
processedTableData.value = [];
}
}, { immediate: true, deep: true });
Khi áp dụng cấu hình này, component <a-table> của Ant Design Vue sẽ tự động đọc thuộc tính rowSpan từ customCell và render giao diện bảng với các ô được gộp chính xác theo nhóm dữ liệu liền kề.