深嵌套树的选择
主要思想:
- 拿到数据,先对数据进行处理,给数据添加expend 控制打开 关闭 ,给叶子节点添加isLeaf字段
- 先整理出所有非叶子节点的所有叶子节点 faLeaf ,用于控制非叶子节点的选中效果
- 通过遍历faLeaf,整理出一份所有非叶子节点的选中情况的对象 faIsChecked,后续如果需要提交父节点,可以通过这个对象获取
- 如果需要知道code对应的name值可以顺便整理一份对象 allNames
- 代码中使用了eventBus
以下是实现代码,文件嵌套关系如下
- labelSelectTree
- index
- treeItem
- mCheckbox
- labelFilter
- mock.json
labelSelectTree > index 组件
<template>
<div class="labelSelectTree">
<div v-for="item in list" :key="item.code">
<div class="label-header" @click="togglePanel(item)">
<span class="label-title">{{ item.name }}</span>
<i v-if="item.expanded" class="label-icon el-icon-caret-top" />
<i v-else class="label-icon el-icon-caret-bottom" />
</div>
<treeItem
v-show="item.expanded"
:list="item"
:faLeaf="faLeaf"
:checkedItems="checkedItems"
:faIsChecked="faIsChecked"
></treeItem>
</div>
</div>
</template>
<script>
import { EventBus } from "@/utils/eventBus";
import treeItem from "./treeItem";
export default {
name: "labelSelectTree",
components: { treeItem },
props: {
list: Array,
faLeaf: Object,
value: Array,
faIsChecked: Object,
},
data() {
return {
checkedItems: [],
};
},
watch: {
value: {
handler(v) {
if (!v.length) {
this.checkedItems = [];
}
},
},
},
mounted() {
EventBus.$on("changeSelect", (v) => {
this.checkedItems = [...new Set(v)];
this.$emit("input", this.checkedItems);
});
},
methods: {
togglePanel(item) {
item.expanded = !item.expanded;
},
},
};
</script>
<style lang="scss" scoped>
.labelSelectTree {
.label-header {
background: #1a939380;
box-sizing: border-box;
width: 94% !important;
margin: 0 auto 10px;
cursor: pointer;
border-radius: 10px;
color: #fff;
justify-content: space-between;
align-items: center;
display: flex;
height: 36px;
padding: 0 16px;
}
.label-title {
font-weight: bold;
font-size: 16px;
}
}
</style>
labelSelectTree > treeItem组件
<template>
<div class="treeItem label">
<div v-for="item in list.items" :key="item.id" class="label-content">
<div v-if="!(item.items && item.items.length)" class="selfBox">
<mCheckbox :check="checkedItems.includes(item.code)" @handleCheck="handleCheckAllChange(item)" /><span class="label-name" @click.self="handleCheckAllChange(item)">{{
item.name
}}</span>
</div>
<div v-else class="selfBox">
<mCheckbox :check="faIsChecked[item.code]" @handleCheck="handleCheckAllChange(item)" /><span class="label-name" @click.self="togglePanel(item)">{{
item.name
}}</span>
<i :class="[
'label-icon',
item.expanded ? 'el-icon-caret-top' : 'el-icon-caret-bottom',
]" @click.self="togglePanel(item)" />
</div>
<div class="son" v-if="item.items && item.items.length" v-show="item.expanded">
<tree-item :list="item" :faLeaf="faLeaf" :faIsChecked="faIsChecked" :checkedItems="checkedItems"></tree-item>
</div>
</div>
</div>
</template>
<script>
import mCheckbox from "./mCheckbox";
import { EventBus } from "@/utils/eventBus";
export default {
name: "treeItem",
components: { mCheckbox },
props: {
list: Object,
checkedItems: Array, //选中项
faLeaf: Object, //统计父节点的所有叶子节点code
faIsChecked: Object, //统计父节点是否选中的对象
},
data() {
return {};
},
computed: {},
watch: {},
methods: {
changeCheckedItems(code, flag) {
code = typeof code === "string" ? [code] : code;
let checked = [...this.checkedItems];
if (flag) {
checked.push(...code);
} else {
checked = checked.filter((item) => !code.includes(item));
}
EventBus.$emit("changeSelect", [...checked]);
},
handleCheckAllChange(self) {
let isChecked = null;
if (self.isLeaf) {
// 叶子节点
isChecked = this.checkedItems.includes(self.code);
this.changeCheckedItems(self.code, !isChecked);
} else {
let selfLeaf = this.faLeaf[self.code];
isChecked =
selfLeaf &&
selfLeaf.some((item) => {
return !this.checkedItems.includes(item);
});
this.changeCheckedItems(selfLeaf, isChecked);
}
},
togglePanel(self) {
this.$set(self, "expanded", !self.expanded);
},
},
};
</script>
<style lang="scss" scoped>
.treeItem.label {
margin-bottom: 10px;
width: 100%;
color: #333;
.label-name {
font-size: 18px;
padding-left: 10px;
}
.label-icon {
font-weight: bold;
color: #333;
font-size: 16px;
}
.selfBox {
display: grid;
grid-template-columns: 20px 1fr 20px;
align-items: center;
line-height: 32px;
padding: 4px 10px 4px 15px;
cursor: pointer;
&:hover {
background-color: #00000040;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.label-content {
padding-left: 13px;
}
}
</style>
labelSelectTree > mCheckbox 组件
<template>
<div :class="{ checkbox: true, disabled }" @click.stop="handleCheck">
<div class="checked el-icon-check" v-if="check"></div>
</div>
</template>
<script>
export default {
name: "mCheckbox",
props: {
check: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
},
data() {
return {};
},
methods: {
handleCheck() {
if (this.disabled) return;
this.$emit("handleCheck");
},
},
};
</script>
<style lang="scss" scoped>
.checkbox {
width: 22px;
height: 22px;
border-width: 2px;
background-color: #1d455240;
border: 2px solid #00f6ff88;
border-radius: 3px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
.checked {
color: #fff;
background-color: #00f6ff88;
}
&.disabled {
border: 0.5px solid #e8ebee;
}
}
</style>
labelFilter 数据处理 及 展示 页面
<template>
<div class="inner_container">
<div class="treeContainer">
<mTree v-model="industryList" :list="labelTree" :faLeaf="faLeaf" :faIsChecked="faIsChecked"></mTree>
</div>
<ul class="labelDisplay">
<li v-for="(item,i) in checkedNames" :key="i">{{ item }}</li>
</ul>
<div class="footer">
<el-button type="primary" size="mini" @click="handleSubmit">确定</el-button>
<el-button size="mini" @click="reset">重置</el-button>
</div>
</div>
</template>
<script>
import mTree from "./labelFilter/index";
import { originData } from "./mock.json";
export default {
name: "toggleRole",
components: { mTree },
props: {},
data() {
return {
originData: originData,
labelTree: [],
industryList: [],
allNames: {},
checkedNames: [],
};
},
computed: {
// 所有非叶子节点的叶子节点code
faLeaf() {
let FaLeaf = {};
function loop(list) {
list.map((item) => {
if (item.items && item.items.length) {
let codes = item.items.map((a) => a.code);
item.code in FaLeaf
? FaLeaf[item.code].push(...codes)
: (FaLeaf[item.code] = codes);
loop(item.items);
}
});
}
loop(this.originData);
for (const key in FaLeaf) {
let element = FaLeaf[key];
element.map((item, i) => {
if (item in FaLeaf) {
element[i] = FaLeaf[item];
}
});
}
for (const key in FaLeaf) {
let element = FaLeaf[key];
FaLeaf[key] = element.flat(4);
}
return FaLeaf;
},
faIsChecked() {
let obj = {};
for (const key in this.faLeaf) {
const element = this.faLeaf[key];
let flag = element.some((item) => {
return !this.industryList.includes(item);
});
obj[key] = !flag;
}
return obj;
},
},
watch: {
},
methods: {
loop(arr) {
return arr.map((item) => {
if (item.items && item.items.length) {
this.loop(item.items);
} else {
this.$set(item, "isLeaf", true);
}
this.$set(item, "expanded", false);
this.allNames[item.code] = item.name
return item;
});
},
async getAllTreePaIndustry() {
// const res = await getAllTreePaIndustry();
// let data = []
// this.originData = data;
let D = [{ name: "商户行业", code: "wree", items: this.originData }];
D = this.loop(D);
this.labelTree = D;
},
handleSubmit() {
let AllCheckedFa = [] //所有选中的父节点
for (const key in this.faIsChecked) {
const element = this.faIsChecked[key];
element && AllCheckedFa.push(key)
}
this.checkedNames = [...AllCheckedFa, ...this.industryList].map(item => {
return this.allNames[item]
})
console.log(AllCheckedFa);
console.log(this.industryList);//所有选中的叶子节点
},
reset() {
this.industryList = []
},
findByCode(nestedArray, code) {
for (let i = 0; i < nestedArray.length; i++) {
const item = nestedArray[i];
if (item.code === code) {
return item;
}
if (Array.isArray(item.children)) {
const foundItem = this.findByCode(item.children, code);
if (foundItem) { return foundItem }
}
}
return null;
},
},
mounted() {
this.getAllTreePaIndustry();
},
};
</script>
<style scoped lang="scss">
.inner_container {
height: 100%;
padding: 0.075075rem /* 10/133.2 */;
display: grid;
grid-template-rows: 1fr 25% 0.225225rem /* 30/133.2 */;
grid-gap: 0.112613rem /* 15/133.2 */;
.treeContainer {
overflow-y: auto;
background-color: #fff;
border-radius: 4px;
box-shadow: 0px 0px 3px 1px #ddd;
padding: 0.075075rem /* 10/133.2 */;
}
.labelDisplay {
box-shadow: 0px 0px 3px 1px #cfcfcf;
// border: 1px solid #ccc;
border-radius: 4px;
padding: 0.075075rem /* 10/133.2 */;
text-align: left;
overflow-y: auto;
li {
display: inline-block;
color: #1677dc;
background: #e8ecf2;
border-radius: 0.022523rem /* 3/133.2 */;
padding: 2px 6px;
margin: 0 0.037538rem /* 5/133.2 */ 0.037538rem /* 5/133.2 */ 0;
font-size: 14px;
}
}
.mTree {
overflow: auto;
}
}
</style>
mock 数据
{
"originData": [
{
"parentCode": null,
"code": "A0000",
"name": "童年",
"indLevel": "1",
"items": [
{
"parentCode": "A0000",
"code": "A0400",
"name": "灰太狼",
"indLevel": "2",
"items": [
{
"parentCode": "A0400",
"code": "A0410",
"name": "喜洋洋",
"indLevel": "3",
"items": [
{
"parentCode": "A0410",
"code": "A0412",
"name": "红太狼",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0410",
"code": "A0411",
"name": "小灰灰",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0400",
"code": "A0420",
"name": "沸羊羊",
"indLevel": "3",
"items": [
{
"parentCode": "A0420",
"code": "A0421",
"name": "美羊羊",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0420",
"code": "A0422",
"name": "暖洋洋",
"indLevel": "4",
"items": null
}
]
}
]
},
{
"parentCode": "A0000",
"code": "A0100",
"name": "懒洋洋",
"indLevel": "2",
"items": [
{
"parentCode": "A0100",
"code": "A0110",
"name": "慢羊羊",
"indLevel": "3",
"items": [
{
"parentCode": "A0110",
"code": "A0111",
"name": "蕉太狼",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0110",
"code": "A0112",
"name": "小猪猪",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0110",
"code": "A0119",
"name": "伊之助",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0110",
"code": "A0113",
"name": "炭治郎",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0140",
"name": "善逸",
"indLevel": "3",
"items": [
{
"parentCode": "A0140",
"code": "A0141",
"name": "炎柱",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0140",
"code": "A0142",
"name": "水柱",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0140",
"code": "A0143",
"name": "音柱",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0140",
"code": "A0149",
"name": "恋柱",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0120",
"name": "豆浆",
"indLevel": "3",
"items": [
{
"parentCode": "A0120",
"code": "A0122",
"name": "油条",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0120",
"code": "A0123",
"name": "拌粉",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0120",
"code": "A0121",
"name": "凉粉",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0130",
"name": "水饺",
"indLevel": "3",
"items": [
{
"parentCode": "A0130",
"code": "A0131",
"name": "馄饨",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0130",
"code": "A0132",
"name": "手抓饼",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0130",
"code": "A0133",
"name": "煎饼果子",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0130",
"code": "A0134",
"name": "烤冷面",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0190",
"name": "其他农业",
"indLevel": "3",
"items": null
},
{
"parentCode": "A0100",
"code": "A0160",
"name": "坚果",
"indLevel": "3",
"items": [
{
"parentCode": "A0160",
"code": "A0162",
"name": "牛油果",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0160",
"code": "A0163",
"name": "香蕉",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0160",
"code": "A0161",
"name": "草莓",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0160",
"code": "A0164",
"name": "西瓜",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0160",
"code": "A0169",
"name": "苹果",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0150",
"name": "葡萄",
"indLevel": "3",
"items": [
{
"parentCode": "A0150",
"code": "A0151",
"name": "柚子",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0150",
"code": "A0152",
"name": "橘子",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0150",
"code": "A0153",
"name": "橙子",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0150",
"code": "A0154",
"name": "沃柑",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0150",
"code": "A0159",
"name": "柠檬",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0180",
"name": "草种植及割草",
"indLevel": "3",
"items": [
{
"parentCode": "A0180",
"code": "A0181",
"name": "狗尾巴草",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0180",
"code": "A0182",
"name": "薰衣草",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "A0100",
"code": "A0170",
"name": "中药材种植",
"indLevel": "3",
"items": [
{
"parentCode": "A0170",
"code": "A0179",
"name": "当归",
"indLevel": "4",
"items": null
},
{
"parentCode": "A0170",
"code": "A0171",
"name": "鱼腥草",
"indLevel": "4",
"items": null
}
]
}
]
}
]
},
{
"parentCode": null,
"code": "B0000",
"name": "采矿业",
"indLevel": "1",
"items": [
{
"parentCode": "B0000",
"code": "B0600",
"name": "金矿",
"indLevel": "2",
"items": [
{
"parentCode": "B0600",
"code": "B0610",
"name": "银矿",
"indLevel": "3",
"items": null
},
{
"parentCode": "B0600",
"code": "B0620",
"name": "铜矿",
"indLevel": "3",
"items": null
}
]
},
{
"parentCode": "B0000",
"code": "B0700",
"name": "石油和天然气开采业",
"indLevel": "2",
"items": [
{
"parentCode": "B0700",
"code": "B0710",
"name": "石油",
"indLevel": "3",
"items": [
{
"parentCode": "B0710",
"code": "B0711",
"name": "陆地石油",
"indLevel": "4",
"items": null
},
{
"parentCode": "B0710",
"code": "B0712",
"name": "海洋石油",
"indLevel": "4",
"items": null
}
]
},
{
"parentCode": "B0700",
"code": "B0720",
"name": "社畜",
"indLevel": "3",
"items": [
{
"parentCode": "B0720",
"code": "B0721",
"name": "文员",
"indLevel": "4",
"items": null
},
{
"parentCode": "B0720",
"code": "B0722",
"name": "程序猿",
"indLevel": "4",
"items": null
}
]
}
]
},
{
"parentCode": "B0000",
"code": "B1100",
"name": "十上无难事",
"indLevel": "2",
"items": [
{
"parentCode": "B1100",
"code": "B1110",
"name": "呱呱乐",
"indLevel": "3",
"items": null
},
{
"parentCode": "B1100",
"code": "B1120",
"name": "逆揍凯",
"indLevel": "3",
"items": null
},
{
"parentCode": "B1100",
"code": "B1190",
"name": "四世同堂",
"indLevel": "3",
"items": null
}
]
}
]
}
]
}