移动端树形结构
该组件依据需求来做,当前包含三种选择状态,选中,未选中,半选。由于不需要做树形的收缩展开故没有写相关内容。树形展开与收缩与选中类似,只需要在节点上挂载相关字段即可实现。由于需求需要增加不限的功能,所以改组件内部实现不限的勾选。
效果
组件代码
<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;// 父级肯定有childrenconst 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.5if(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>