如題,公司業務需求,數據結構比較復雜,需要在一張表內實現多級樹狀數據展示及同屬性的單元格合并,并在表格內實現增刪改操作。
網上翻閱了很多實例,沒有能解決所有需求的案例,于是自己實現了一套。
時間匆忙,邏輯有優化的地方還請無償指出!
最終效果如下
圖上,編碼有父子層級,每個編碼可包含多個交付階段,每個交付階段可包含多個文件,每個文件可添加不同文檔項次
實現總結如下
一. 結構調整
首先跟后臺確認了數據結構,根據右側最詳細內容為基準,以單層數組返回(以編碼樹級返回更好)。獲取到數據后封裝為樹級數據。保證最詳細處表格每一行都對應一條數據。如圖示,忽略為展開子級數據,則圖上一共對應七條數據。
其中,每個數據對象帶有三個屬性:code_cnt(每條編碼下對應的第三部分行數)、stage_cnt(每個編碼下的交付階段對應的第三部分行數)、file_cnt(每個文件對應的第三部分行數)。后面用于表格合并。
- 封裝完數據或直接獲取到父子層級后,因存在多條數據同一編碼,每條數據下都有相同children數據存在,所以需刪除多余children,保留一條。又因展開時需展示在相同編碼下方,所以需保存相同編碼最后一條數據的children字段。如圖上所示,X-R1.1.4編碼有三條數據,應只保留項次編碼為-D3.2.2的children數據,以保證點擊展開子級時子層級展示在三條數據下方。
// 當同一編碼多條數據且有children時,保留最后一級children
deleteChildren(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].children && data[i].children.length) {
data[i].hasChild = true; // 后續解釋
if ( data.some( (item, index) => index > i && item.code_id === data[i].code_id ) ) {
delete data[i].children;
} else {
data[i].children = this.deleteChildren(data[i].children);
}
}
}
return data;
}
- 因相同編碼、相同階段、相同文件需合并,所以需要遞歸標識出每個相同編碼、階段、文件的首條數據,以滿足后續單元格合并需求。
// 單元格需合并時,標記首條數據
dealDataBefore(data) {
let id = "", stage = "", file = "";
for (let i = 0; i < data.length; i++) {
if (!id || id !== data[i].interface_item_code) {
// 第一條
id = data[i].interface_item_code;
data[i].isFirstLine = true; // 標識編碼首條數據
stage = data[i].stage_keyid;
data[i].isFirstStage = true; // 標識階段首條數據
file = data[i].deliver_file_template_id;
data[i].isFirstFile = true; // 標識文件首條數據
} else {
if (!stage || stage !== data[i].stage_keyid) {
stage = data[i].stage_keyid;
data[i].isFirstStage = true;
file = data[i].deliver_file_template_id;
data[i].isFirstFile = true;
} else {
if (!file || file !== data[i].deliver_file_template_id) {
file = data[i].deliver_file_template_id;
data[i].isFirstFile = true;
}
}
}
if (data[i].children) {
data[i].children = this.dealDataBefore(data[i].children);
}
}
return data;
},
二. 父子層級展開合并
第一步數據處理結束后,會發現交給element-ui渲染,無法展開關閉父子層級。
因為我們第一步對數據的處理,最左側編碼展示的數據已經沒有children數據了,而有children數據的單元格將被上方合并無法點擊。
如上圖所示,4、5兩條數據實則第3條數據的children,而顯示的X-R1.1.4為第1條數據的單元格。
因此,我們需自己做子級的展開合并操作。
- 首先重寫編碼列的渲染模板
<el-table-column
label="編碼"
key="code"
prop="code"
show-overflow-tooltip
>
<template v-slot="{ row }">
<span v-if="row.hasChild" class="arrow-icon" @click="toggleRowExpansion(row)">
<i :class="row.isExpand ? 'el-icon-caret-bottom' : 'el-icon-caret-right'" />
</span>
<span>{{ row.code }}</span>
</template>
</el-table-column>
第一步的hasChild標識意義就出來了,當有多條數據時,末條保留children,首條標記hasChild。
- 遞歸獲取到點擊條目的同層級下所有相同編碼的數據,后將最后一條數據子級做展開/關閉操作。即點擊上圖中X-R1.1.4的按鈕時,需獲取到相同編碼的1、2、3數據,后將3設為展開/關閉狀態。
toggleRowExpansion(row) {
row.isExpand = !row.isExpand;
let rowList = this.getRowList(row, this.tableList);
const expansionRow = rowList[rowList.length - 1];
this.$refs.detailTable &&
this.$refs.detailTable.toggleRowExpansion(expansionRow, row.isExpand);
},
// 獲取點擊層級同編碼所有數據數組
getRowList(row, list) {
for (let i = 0; i < list.length; i++) {
if (list[i].id === row.id)
return list.filter((item) => item.code === row.code );
if (list[i].children && list[i].children.length) {
let res = this.getRowList(row, list[i].children);
if (res) return res;
}
}
return false;
},
三. 單元格合并
第一步已經封裝好了數據,直接綁定table組件的span-method方法如下
//合并單元格
objectSpanMethod({ row, column, rowIndex, columnIndex }) {
if (row.code_cnt > 1 && columnIndex < 3) {
// 同編碼,前三行合并
return {
rowspan: row.code_cnt,
colspan: row.isFirstLine ? 1 : 0,
};
}
if (row.stage_cnt > 1 && columnIndex === 3) {
// 同交付階段多文件,階段合并
return {
rowspan: row.stage_cnt,
colspan: row.isFirstStage ? 1 : 0,
};
}
if (row.file_cnt > 1 && columnIndex === 4) {
// 同文件多項次,文件合并
return {
rowspan: row.file_cnt,
colspan: row.isFirstFile ? 1 : 0,
};
}
},
*四. 表格增刪改操作
截止前三步,表格的展示及交互已全部完成。
本業務流程中,文件為彈框選擇,所以不做介紹。因產品要求,需在表格內直接完成文件后文檔項次等增刪改及操作,所以實現了后續功能(無需求可止步)。
isEdit標識當前行的編輯狀態,據其修改表格列渲染模板。
- 新增
因表格中文件、項次并非一定存在,所以會如第一張圖第二條數據所示,直接出現文件后面為空的情況。此種情況可直接將該行置為編輯狀態。
若是后面幾行,則需處理數據。
矛盾點在于,因交付文件也是合并過的單元格,所以點擊的時候也是同類數據首條,而我們添加的習慣是添加到其最后面。即當我們點擊X-R1.1.4中 測試2 交付文件的+時,我們需要在其兩條后加一條數據,并把前面單元格合并。
async handleAddFileItem(row) {
// 該文件下無項次,則直接修改該項
if (!row.file_item_code) {
this.editMap[row.id] = { ...row }; // 該map用于存儲當前在編輯項的原始狀態,用于取消操作
row.isEdit = true;
} else {
this.tableList = this.addCnt(row, this.tableList);
}
},
addCnt(row, list) {
// code_cnt 相同編碼加一
// stage_cnt 該編碼下相同stage加一
// file_cnt 該文件加一
let hasAdd = false,
addIndex = 0; // 標記加入數據下標
let firstLineIndex = "";
for (let i = 0; i < list.length; i++) {
// 已循環至該添加項次,退出循環并返回修改后數據
if (hasAdd && addIndex === i) return list;
if (list[i].id === row.id) {
firstLineIndex === "" && (firstLineIndex = i);
// 同編碼所有項次cnt加一
list[i].code_cnt++;
if (list[i].stage_keyid === row.stage_keyid) {
// 同交付階段cnt加一
list[i].stage_cnt++;
if (list[i].file_code === row.file_code) {
list[i].file_cnt++;
}
}
// 當前點擊條目
if (list[i].union_id === row.union_id) {
let children =
list[i + list[i].deliver_file_cnt - 2].children || [];
let newLine = {
code_id: list[i].code_id,
code_cnt: list[i].code_cnt,
file_cnt: list[i].file_cnt,
file_code: list[i].file_code,
deliver_file_template_id: list[i].deliver_file_template_id,
isEdit: true,
isAdd: true, // 用于后續刪除時標識刪除條目為新增還是編輯條目
id: new Date().getTime(), // row-key必須字段
parent_id: list[i].parent_id,
stage: list[i].stage,
stage_cnt: list[i].stage_cnt,
stage_keyid: list[i].stage_keyid,
children: children,
isExpand: list[firstLineIndex].isExpand,
};
// children遷移!!!
// 因當前條變為最后一條,需將前面條目children遷移至本條,并同步開閉狀態
list[i + list[i].file_cnt - 2].children = [];
// 在所有相同文件數據最后一條后添加
addIndex = i + list[i].file_cnt - 1;
list.splice(addIndex, 0, newLine);
hasAdd = true;
if (children.length) {
this.$nextTick(() => {
this.$refs.detailTable.toggleRowExpansion(
newLine,
list[firstLineIndex].isExpand
);
});
}
}
} else {
// 未找到編碼則繼續尋找
if (list[i].children && list[i].children.length) {
list[i].children = this.addCnt(row, list[i].children);
}
}
}
return list;
},
- 編輯
編輯操作較為簡單,將isEdit置為true,并在editMap中保存初始狀態即可
this.editMap[row.union_id] = { ...row };
row.isEdit = true; - 新增/編輯條目刪除/取消修改操作
async cancelFileItemDeal(row) {
if (row.isAdd) {
// 新增條目
this.tableList= this.delCnt(row, this.tableList);
} else {
// 編輯項復原
for (let key in this.editMap[row.id]) {
row[key] = this.editMap[row.id][key];
}
delete this.editMap[row.id];
}
},
delCnt(row, list) {
// code_cnt 相同編碼減一
// stage_cnt 該編碼下相同stage減一
// file_cnt 該文件減一
let hasDelete = false;
let firstLineIndex = "";
for (let i = 0; i < list.length; i++) {
// 已刪除并循環至其他項次,退出循環
if (hasDelete && list[i].id !== row.id) return list;
if (list[i].id === row.id) {
firstLineIndex === "" && (firstLineIndex = i);
// 同編碼所有項次cnt加一
list[i].code_cnt--;
if (list[i].stage_keyid === row.stage_keyid) {
// 同交付階段cnt加一
list[i].stage_cnt--;
if (list[i].file_code === row.file_code) {
list[i].file_cnt--;
}
}
// 當前點擊條目
if (list[i].id === row.id) {
let children = list[i].children;
if (children && children.length) {
list[i - 1].children = children;
this.$nextTick(() => {
this.$refs.detailTable.toggleRowExpansion(
list[i - 1],
list[firstLineIndex].isExpand
);
});
}
// 直接刪除
list.splice(i, 1);
hasDelete = true;
}
} else {
// 未找到編碼則繼續尋找
if (list[i].children && list[i].children.length) {
list[i].children = this.delCnt(row, list[i].children);
}
}
}
return list;
},
- 刪除
刪除可直接調用后端接口,后合并數據,無需多余處理
至此,該表格的完整功能實現完成!!!