如題,公司業(yè)務需求庸论,數(shù)據(jù)結構比較復雜职辅,需要在一張表內(nèi)實現(xiàn)多級樹狀數(shù)據(jù)展示及同屬性的單元格合并,并在表格內(nèi)實現(xiàn)增刪改操作聂示。
網(wǎng)上翻閱了很多實例域携,沒有能解決所有需求的案例,于是自己實現(xiàn)了一套鱼喉。
時間匆忙秀鞭,邏輯有優(yōu)化的地方還請無償指出!
最終效果如下
圖上扛禽,編碼有父子層級锋边,每個編碼可包含多個交付階段,每個交付階段可包含多個文件编曼,每個文件可添加不同文檔項次
實現(xiàn)總結如下
一. 結構調(diào)整
首先跟后臺確認了數(shù)據(jù)結構豆巨,根據(jù)右側最詳細內(nèi)容為基準,以單層數(shù)組返回(以編碼樹級返回更好)掐场。獲取到數(shù)據(jù)后封裝為樹級數(shù)據(jù)往扔。保證最詳細處表格每一行都對應一條數(shù)據(jù)。如圖示熊户,忽略為展開子級數(shù)據(jù)萍膛,則圖上一共對應七條數(shù)據(jù)。
其中敏弃,每個數(shù)據(jù)對象帶有三個屬性:code_cnt(每條編碼下對應的第三部分行數(shù))卦羡、stage_cnt(每個編碼下的交付階段對應的第三部分行數(shù))、file_cnt(每個文件對應的第三部分行數(shù))麦到。后面用于表格合并绿饵。
- 封裝完數(shù)據(jù)或直接獲取到父子層級后,因存在多條數(shù)據(jù)同一編碼瓶颠,每條數(shù)據(jù)下都有相同children數(shù)據(jù)存在拟赊,所以需刪除多余children,保留一條粹淋。又因展開時需展示在相同編碼下方吸祟,所以需保存相同編碼最后一條數(shù)據(jù)的children字段。如圖上所示桃移,X-R1.1.4編碼有三條數(shù)據(jù)屋匕,應只保留項次編碼為-D3.2.2的children數(shù)據(jù),以保證點擊展開子級時子層級展示在三條數(shù)據(jù)下方借杰。
// 當同一編碼多條數(shù)據(jù)且有children時过吻,保留最后一級children
deleteChildren(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].children && data[i].children.length) {
data[i].hasChild = true; // 后續(xù)解釋
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;
}
- 因相同編碼、相同階段、相同文件需合并纤虽,所以需要遞歸標識出每個相同編碼乳绕、階段、文件的首條數(shù)據(jù)逼纸,以滿足后續(xù)單元格合并需求洋措。
// 單元格需合并時,標記首條數(shù)據(jù)
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; // 標識編碼首條數(shù)據(jù)
stage = data[i].stage_keyid;
data[i].isFirstStage = true; // 標識階段首條數(shù)據(jù)
file = data[i].deliver_file_template_id;
data[i].isFirstFile = true; // 標識文件首條數(shù)據(jù)
} 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;
},
二. 父子層級展開合并
第一步數(shù)據(jù)處理結束后杰刽,會發(fā)現(xiàn)交給element-ui渲染菠发,無法展開關閉父子層級。
因為我們第一步對數(shù)據(jù)的處理专缠,最左側編碼展示的數(shù)據(jù)已經(jīng)沒有children數(shù)據(jù)了雷酪,而有children數(shù)據(jù)的單元格將被上方合并無法點擊。
如上圖所示涝婉,4哥力、5兩條數(shù)據(jù)實則第3條數(shù)據(jù)的children,而顯示的X-R1.1.4為第1條數(shù)據(jù)的單元格墩弯。
因此吩跋,我們需自己做子級的展開合并操作。
- 首先重寫編碼列的渲染模板
<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標識意義就出來了渔工,當有多條數(shù)據(jù)時锌钮,末條保留children,首條標記hasChild引矩。
- 遞歸獲取到點擊條目的同層級下所有相同編碼的數(shù)據(jù)梁丘,后將最后一條數(shù)據(jù)子級做展開/關閉操作。即點擊上圖中X-R1.1.4的按鈕時旺韭,需獲取到相同編碼的1氛谜、2、3數(shù)據(jù)区端,后將3設為展開/關閉狀態(tài)值漫。
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);
},
// 獲取點擊層級同編碼所有數(shù)據(jù)數(shù)組
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;
},
三. 單元格合并
第一步已經(jīng)封裝好了數(shù)據(jù),直接綁定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,
};
}
},
*四. 表格增刪改操作
截止前三步沥邻,表格的展示及交互已全部完成危虱。
本業(yè)務流程中,文件為彈框選擇唐全,所以不做介紹槽地。因產(chǎn)品要求,需在表格內(nèi)直接完成文件后文檔項次等增刪改及操作,所以實現(xiàn)了后續(xù)功能(無需求可止步)捌蚊。
isEdit標識當前行的編輯狀態(tài),據(jù)其修改表格列渲染模板近弟。
- 新增
因表格中文件缅糟、項次并非一定存在,所以會如第一張圖第二條數(shù)據(jù)所示祷愉,直接出現(xiàn)文件后面為空的情況窗宦。此種情況可直接將該行置為編輯狀態(tài)。
若是后面幾行二鳄,則需處理數(shù)據(jù)赴涵。
矛盾點在于,因交付文件也是合并過的單元格订讼,所以點擊的時候也是同類數(shù)據(jù)首條髓窜,而我們添加的習慣是添加到其最后面。即當我們點擊X-R1.1.4中 測試2 交付文件的+時欺殿,我們需要在其兩條后加一條數(shù)據(jù)寄纵,并把前面單元格合并。
async handleAddFileItem(row) {
// 該文件下無項次脖苏,則直接修改該項
if (!row.file_item_code) {
this.editMap[row.id] = { ...row }; // 該map用于存儲當前在編輯項的原始狀態(tài)程拭,用于取消操作
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; // 標記加入數(shù)據(jù)下標
let firstLineIndex = "";
for (let i = 0; i < list.length; i++) {
// 已循環(huán)至該添加項次,退出循環(huán)并返回修改后數(shù)據(jù)
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, // 用于后續(xù)刪除時標識刪除條目為新增還是編輯條目
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遷移!!!
// 因當前條變?yōu)樽詈笠粭l棍潘,需將前面條目children遷移至本條恃鞋,并同步開閉狀態(tài)
list[i + list[i].file_cnt - 2].children = [];
// 在所有相同文件數(shù)據(jù)最后一條后添加
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 {
// 未找到編碼則繼續(xù)尋找
if (list[i].children && list[i].children.length) {
list[i].children = this.addCnt(row, list[i].children);
}
}
}
return list;
},
- 編輯
編輯操作較為簡單,將isEdit置為true亦歉,并在editMap中保存初始狀態(tài)即可
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++) {
// 已刪除并循環(huán)至其他項次恤浪,退出循環(huán)
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 {
// 未找到編碼則繼續(xù)尋找
if (list[i].children && list[i].children.length) {
list[i].children = this.delCnt(row, list[i].children);
}
}
}
return list;
},
- 刪除
刪除可直接調(diào)用后端接口,后合并數(shù)據(jù)鳍徽,無需多余處理
至此资锰,該表格的完整功能實現(xiàn)完成!=准馈绷杜!