该组件依据需求来做,当前包含三种选择状态,选中,未选中,半选。由于不需要做树形的收缩展开故没有写相关内容。树形展开与收缩与选中类似,只需要在节点上挂载相关字段即可实现。由于需求需要增加不限的功能,所以改组件内部实现不限的勾选。
效果
组件代码
<template>
<div class="tree-container">
<div v-for="item in selfTreeData" :key="item.code + item.name">
<div class="tree-node">
<!-- 第一级不需要缩进 -->
<span v-if="level > 0" :style="{ width: (level - 1) * 24 + 'rpx' }" class="space"></span>
<span v-if="level > 0" class="relevance"></span>
<span :class="item.checked == 1 ? 'is-active' : ''">{{ item.name }} </span>
<span class="check-box" @click="changeChecked(item)">
<img v-show="!item.checked" src="@/static/images/check-normal.svg" />
<img v-show="item.checked == 0.5" src="@/static/images/check-indeterminate.svg" />
<img v-show="item.checked == 1" src="@/static/images/check-active.svg" />
</span>
</div>
<my-tree v-if="item.children && item.children.length" :tree-data="item.children" :level="level + 1"
:ref="`chTree${item.code}`" @changeParent="changeParent" @clearUnlimited="clearUnlimited"></my-tree>
</div>
</div>
</template>
<script>
import myTree from './myTree';
export default {
name: 'myTree',
data() {
return {}
},
props: {
treeData: {
required: true,
type: Array
},
level: {
type: Number,
default: 0
}
},
data(){
return {
selfTreeData:[]
}
},
watch:{
treeData:{
handler(val){
this.selfTreeData = val;
},
immediate:true,
deep:true
}
},
created() {
},
methods: {
changeChecked(treeNode) {
if (!treeNode.code) {
// 选中不限,需要清除下面的所有
if(!treeNode.checked)
return this.clearChecked();
} else if(this.level){
// 清除code为null的
this.$emit('clearUnlimited')
}else{
// 清除code为null的
this.clearUnlimited()
}
// checked 被改变
if (!treeNode.checked) return this.handleChangeNodeChecked(treeNode, 1);
if (treeNode.checked == 0.5) return this.handleChangeNodeChecked(treeNode, 1);
if (treeNode.checked == 1) return this.handleChangeNodeChecked(treeNode, 0);
},
handleChangeNodeChecked(treeNode, state , isClear = false) {
this.$set(treeNode, 'checked', state);
// 通知父级,改变父级选中状态。
if(!isClear)
this.$emit('changeParent', treeNode.pcode);
// 将子级全部选中。
if (treeNode.code && this.$refs[`chTree${treeNode.code}`] && this.$refs[`chTree${treeNode.code}`][0])
this.$refs[`chTree${treeNode.code}`][0].changeChChecked(treeNode, state);
},
changeParent(pcode) {
// 子级发生改变,更新父级选中状态。
const pNode = this.selfTreeData.find(node => node.code === pcode);
if (!pcode) return;
// 父级肯定有children
const len = pNode.children.length;
let state = 0 , hasChecked = false , hasNoChecked = false;
// 此处不统计个数是假设数组很长,我前两个就能判断的话减少性能消耗。
pNode.children.some((node,index) => {
if(node.checked == 0.5){
// 直接将当前设置为半选
state = 0.5;
return true;
}
// 只有存在又有选中,又有未选中则直接设置为0.5
if(node.checked == 1){
hasChecked = true;
}
if(!node.checked){
hasNoChecked = true;
}
if(hasChecked && hasNoChecked){
state = 0.5;
return true;
}
})
// 此处只要判断state是否还是0,如果还是0且hasChecked为true证明需要为1。
state = state === 0 && hasChecked ? 1 : state;
this.$set(pNode, 'checked', state);
this.$emit('changeParent', pNode.pcode);
},
changeChChecked(treeNode, state) {
// 改变子级的选中状态
if (treeNode.children && treeNode.children.length) {
treeNode.children.forEach(node => {
this.$set(node, 'checked', state);
if(this.$refs[`chTree${node.code}`] && this.$refs[`chTree${node.code}`][0])
this.$refs[`chTree${node.code}`][0].changeChChecked(node, state);
})
}
},
clearUnlimited() {
// 清除不限
if (this.level) {
this.$emit('clearUnlimited')
} else {
const unlimitedNode = this.selfTreeData.find(node => !node.code);
if (unlimitedNode && unlimitedNode.checked) {
unlimitedNode.checked = 0;
}
}
},
getChecked(){
const checkedCodes = [];
this.selfTreeData.forEach(node=>{
const checked = node.checked;
if(checked == 1){
checkedCodes.push(node.code);
}
if(checked && this.$refs[`chTree${node.code}`] && this.$refs[`chTree${node.code}`][0])
checkedCodes.push(...this.$refs[`chTree${node.code}`][0].getChecked());
})
return checkedCodes;
},
clearChecked(){
// 业务需求,重置的时候勾选不限。
this.selfTreeData.forEach(node => {
if (node.code){
this.handleChangeNodeChecked(node,0,true)
}
else
this.$set(node,'checked',1)
})
}
}
}
</script>
<style lang="scss" scoped>
.tree-node {
height: 96rpx;
padding: 26rpx 24rpx 26rpx 57rpx;
color: #262626;
font-size: 28rpx;
box-shadow: inset 0px -1px 0px 0px #E6E6E6;
box-sizing: border-box;
position: relative;
.space {
height: 1px;
display: inline-block;
}
.relevance {
height: 14rpx;
width: 14rpx;
display: inline-block;
border-left: 1px solid #B2B2B2;
border-bottom: 1px solid #B2B2B2;
position: relative;
top: -10rpx;
margin-right: 14rpx;
}
.check-box {
position: absolute;
right: 24rpx;
overflow: auto;
img {
height: 40rpx;
width: 40rpx;
}
}
.is-active {
color: $primaryColor;
}
}</style>
使用
<template>
<div class="filter-panel-content">
<div class="tab-conrainer">
<div class="left-tabs">
<div
v-for="tab in pageData"
:key="tab.code"
:class="tab.code === activeTab ? 'tab-item active' : 'tab-item'"
@click="changeTabActive(tab)"
>{{ tab.name }}</div>
</div>
<div class="tree-container" v-for="tab in pageData" :key="tab.code" v-show="tab.code === activeTab">
<my-tree :tree-data="tab.treeData" :ref="`tree${tab.code}`" />
</div>
</div>
<div class="operate-container">
<div class="btn" @click="reset">重置</div>
<div class="btn primary" @click="search">确定</div>
</div>
</div>
</template>
<script>
import myTree from './components/myTree.vue';
export default {
props:{
showFilterPopup:{
type:Boolean,
default:true
}
},
data(){
return {
pageData:[],
activeTab:'',
searchData:{}
}
},
components:{
myTree
},
created(){
this.getData()
},
methods:{
getData(){
this.pageData = [
{ name:'湖南' , code:'01',treeData:[
{name:'不限',code:null,checked:1},
{name:'长沙',code:'011',children:[
{name:'天心区',code:'0111',pcode:'011'},
{name:'开福区',code:'0112',pcode:'011'},
{name:'芙蓉区',code:'0113',pcode:'011'},
{name:'岳麓区',code:'0114',pcode:'011'},
{name:'望城区',code:'0115',pcode:'011'},
{name:'长沙县',code:'0116',pcode:'011',children:[
{name:'星沙',code:'01161',pcode:'0116'},
{name:'安沙',code:'01162',pcode:'0116'},
]},
]},
{name:'株洲',code:'012'},
{name:'湘潭',code:'013'}
]},
{ name:'广东' , code:'02',treeData:[
{name:'不限',code:null,checked:1},
{name:'广州',code:'021'},
{name:'深圳',code:'022'},
{name:'东莞',code:'023'}
]},
{ name:'湖北' , code:'03',treeData:[
{name:'不限',code:null,checked:1},
{name:'武汉',code:'031'},
{name:'恩施',code:'032'},
{name:'孝感',code:'033'}
]},
{ name:'江西' , code:'04',treeData:[
{name:'不限',code:null,checked:1},
{name:'武昌',code:'041'},
{name:'赣州',code:'042'},
{name:'九江',code:'043'}
]},
];
this.activeTab = this.pageData[0].code;
},
changeTabActive(tab){
this.activeTab = tab.code;
},
reset(){
this.pageData.forEach(tab=>{
this.$refs[`tree${tab.code}`][0].clearChecked()
})
},
search(){
this.pageData.forEach(tab=>{
this.searchData[tab.code] = this.$refs[`tree${tab.code}`][0].getChecked().filter(item=>item)
})
this.$emit('search',this.searchData)
}
}
}
</script>
<style scoped lang="scss">
uni-view{
height: 100%;
}
.filter-panel-content{
background:#FFF;
max-height: 100%;
overflow: auto;
.tab-conrainer{
display: flex;
.left-tabs{
background: #F0F0F0;
width: 160rpx;
min-height: 804rpx;
.tab-item{
height: 96rpx;
line-height: 96rpx;
color: $text-normal-color;
font-size: 28rpx;
font-weight: 400;
text-align: center;
}
.tab-item.active{
color: $primaryColor;
background: #FFF;
}
}
.tree-container{
flex: 1;
}
}
.operate-container{
padding: 22rpx 40rpx;
background: #F9F9F9;
box-shadow: inset 0px 1rpx 0px 0px #D9D9D9;
display: flex;
.btn{
cursor: pointer;
color: $text-normal-color;
line-height: 96rpx;
box-sizing: border-box;
flex: 1;
text-align: center;
font-size: 36rpx;
background: #FFF;
border: 2rpx solid rgba(0,0,0,0.15);
border-radius: 8rpx;
}
.btn.primary{
color: #FFF;
background: $primaryColor;
border: 0px;
margin-left: 30rpx;
}
}
}
</style>