在此作者基础上封装:https://blog.csdn.net/m0_68428581/article/details/130641982
1.页面结构
<div>
<div
class="tree-cover"
:class="{ show: showDialog }"
@click="_cancel"
></div>
<div class="tree-dialog" :class="{ show: showDialog }">
<div class="tree-bar">
<div
class="tree-bar-cancel"
:style="{ color: cancelColor }"
hover-class="hover-c"
@click="_cancel"
>
取消
</div>
<div class="tree-bar-title" :style="{ color: titleColor }">
{{ title }}
</div>
<div
class="tree-bar-confirm"
:style="{ color: confirmColor }"
hover-class="hover-c"
>
{{ multiple ? '确定' : '' }}
</div>
</div>
<div class="tree-div">
<scrollView class="tree-list" :scroll-y="true">
<div v-for="(item, index) in treeList" :key="index" class="tree-box">
<div
class="tree-item"
:style="[{ paddingLeft: item.level * 30 + 'px' }]"
:class="{ itemBorder: border === true, show: item.isShow }"
>
<div class="item-label">
<div
class="item-icon uni-inline-item"
@click="_onItemSwitch(item, index)"
ref="switchRef"
>
<div
v-if="!item.isLastLevel && item.isShowChild"
class="switch-on"
:style="{ 'border-left-color': switchColor }"
></div>
<div
v-else-if="!item.isLastLevel && !item.isShowChild"
class="switch-off"
:style="{ 'border-top-color': switchColor }"
></div>
<div
v-else
class="item-last-dot"
:style="{ 'border-top-color': switchColor }"
></div>
</div>
<div class="item-name" @click="_onItemSelect(item, index)">
{{
item.name +
(item.childCount ? '(' + item.childCount + ')' : '')
}}
</div>
</div>
</div>
</div>
</scrollView>
</div>
</div>
</div>
2.逻辑结构
<script setup lang="ts">
import {
ref,
onMounted,
watch,
defineProps,
defineEmits,
defineExpose,
} from 'vue'
import scrollView from '@/components/tree-picker/scrollView.vue'
let showDialog = ref<boolean>(false)
let treeList = ref<any>([])
let $emit = defineEmits(['select-change'])
let switchRef = ref<any>()
let props = defineProps({
valueKey: {
type: String,
default: 'id',
},
textKey: {
type: String,
default: 'name',
},
childrenKey: {
type: String,
default: 'children',
},
localdata: {
type: Array,
default: function () {
return []
},
},
localTreeList: {
//在已经格式化好的数据
type: Array,
default: function () {
return []
},
},
selectedData: {
type: Array,
default: function () {
return []
},
},
title: {
type: String,
default: '',
},
multiple: {
// 是否可以多选
type: Boolean,
default: true,
},
selectParent: {
//是否可以选父级
type: Boolean,
default: true,
},
confirmColor: {
// 确定按钮颜色
type: String,
default: '', // #0055ff
},
cancelColor: {
// 取消按钮颜色
type: String,
default: '', // #757575
},
titleColor: {
// 标题颜色
type: String,
default: '', //
},
switchColor: {
// 节点切换图标颜色
type: String,
default: '', // #666
},
border: {
// 是否有分割线
type: Boolean,
default: false,
},
})
const _hide = () => {
showDialog.value = false
}
const _cancel = () => {
_hide()
// this.$emit('cancel', '')
}
const _show = () => {
showDialog.value = true
}
//格式化原数据(原数据为tree结构)
const _formatTreeData = (
list: any = [],
level = 0,
parentItem: any,
isShowChild = true,
) => {
let nextIndex = 0
let parentId = -1
let initCheckStatus = 0
if (parentItem) {
nextIndex =
treeList.value.findIndex((item: any) => item.id === parentItem.id) + 1
parentId = parentItem.id
if (!props.multiple) {
//单选
initCheckStatus = 0
} else initCheckStatus = parentItem.checkStatus == 2 ? 2 : 0
}
list.forEach((item: any) => {
let isLastLevel = true
if (item && item[props.childrenKey]) {
let children = item[props.childrenKey]
if (Array.isArray(children) && children.length > 0) {
isLastLevel = false
}
}
let itemT = {
id: item.groupId,
name: item.groupName,
level,
isLastLevel,
isShow: isShowChild,
isShowChild: false,
checkStatus: initCheckStatus,
orCheckStatus: 0,
parentId,
children: item[props.childrenKey],
childCount: item[props.childrenKey] ? item[props.childrenKey].length : 0,
childCheckCount: 0,
childCheckPCount: 0,
}
if (props.selectedData.indexOf(itemT.id) >= 0) {
itemT.checkStatus = 2
itemT.orCheckStatus = 2
itemT.childCheckCount = itemT.children ? itemT.children.length : 0
_onItemParentSelect(itemT, nextIndex, '')
}
treeList.value.splice(nextIndex, 0, itemT)
nextIndex++
})
console.log(treeList.value)
}
// 节点打开、关闭切换
const _onItemSwitch = (item: any, index: number) => {
if (item.isLastLevel === true) {
return
}
item.isShowChild = !item.isShowChild
if (item.children) {
_formatTreeData(item.children, item.level + 1, item)
// item.children = undefined
} else {
_onItemChildSwitch(item, index)
}
}
const _onItemChildSwitch = (item: any, index: any) => {
// console.log('_onItemChildSwitch')
const firstChildIndex = index + 1
if (firstChildIndex > 0)
for (var i = firstChildIndex; i < treeList.value.length; i++) {
let itemChild = treeList.value[i]
if (itemChild.level > item.level) {
if (item.isShowChild) {
if (itemChild.parentId === item.id) {
itemChild.isShow = item.isShowChild
if (!itemChild.isShow) {
itemChild.isShowChild = false
}
}
} else {
itemChild.isShow = item.isShowChild
itemChild.isShowChild = false
}
} else {
return
}
}
}
// 节点选中、取消选中
const _onItemSelect = (item: any, index: any) => {
//console.log('_onItemSelect')
//console.log(item)
if (!props.multiple) {
//单选
item.checkStatus = item.checkStatus == 0 ? 2 : 0
treeList.value.forEach((itemv: any, i: number) => {
console.log(itemv)
if (i != index) {
treeList.value[i].checkStatus = 0
} else {
treeList.value[i].checkStatus = 2
}
})
let selectedList = []
let selectedNames
selectedList.push(item.id)
selectedNames = item.name
_hide()
$emit('select-change', selectedList, selectedNames)
return
}
let oldCheckStatus = item.checkStatus
switch (oldCheckStatus) {
case 0:
item.checkStatus = 2
item.childCheckCount = item.childCount
item.childCheckPCount = 0
break
case 1:
case 2:
item.checkStatus = 0
item.childCheckCount = 0
item.childCheckPCount = 0
break
default:
break
}
//子节点 全部选中
_onItemChildSelect(item, index)
//父节点 选中状态变化
_onItemParentSelect(item, index, oldCheckStatus)
}
const _onItemChildSelect = (item: any, index: any) => {
//console.log('_onItemChildSelect')
// let allChildCount = 0
if (item.childCount && item.childCount > 0) {
index++
while (
index < treeList.value.length &&
treeList.value[index].level > item.level
) {
let itemChild = treeList.value[index]
itemChild.checkStatus = item.checkStatus
if (itemChild.checkStatus == 2) {
itemChild.childCheckCount = itemChild.childCount
itemChild.childCheckPCount = 0
} else if (itemChild.checkStatus == 0) {
itemChild.childCheckCount = 0
itemChild.childCheckPCount = 0
}
index++
}
}
}
const _onItemParentSelect = (item: any, index: any, oldCheckStatus: any) => {
//console.log('_onItemParentSelect')
//console.log(item)
console.log(index)
const parentIndex = treeList.value.findIndex(
(itemP: any) => itemP.id == item.parentId,
)
//console.log('parentIndex:' + parentIndex)
if (parentIndex >= 0) {
let itemParent = treeList.value[parentIndex]
// let count = itemParent.childCheckCount
let oldCheckStatusParent = itemParent.checkStatus
if (oldCheckStatus == 1) {
itemParent.childCheckPCount -= 1
} else if (oldCheckStatus == 2) {
itemParent.childCheckCount -= 1
}
if (item.checkStatus == 1) {
itemParent.childCheckPCount += 1
} else if (item.checkStatus == 2) {
itemParent.childCheckCount += 1
}
if (itemParent.childCheckCount <= 0 && itemParent.childCheckPCount <= 0) {
itemParent.childCheckCount = 0
itemParent.childCheckPCount = 0
itemParent.checkStatus = 0
} else if (itemParent.childCheckCount >= itemParent.childCount) {
itemParent.childCheckCount = itemParent.childCount
itemParent.childCheckPCount = 0
itemParent.checkStatus = 2
} else {
itemParent.checkStatus = 1
}
//console.log('itemParent:', itemParent)
_onItemParentSelect(itemParent, parentIndex, oldCheckStatusParent)
}
}
// 重置数据
const _reTreeList = () => {
treeList.value.forEach((v: any, i: any) => {
treeList.value[i].checkStatus = v.orCheckStatus
})
}
const treeToArray = (tree: any) => {
tree.forEach((item: any, index: number) => {
if (item.children) {
_onItemSwitch(item, index)
item.children = undefined
treeToArray(treeList.value)
}
})
}
const _initTree = () => {
treeList.value = []
_formatTreeData(props.localdata, 0, '', true)
if (treeList.value.length > 0) {
treeToArray(treeList.value)
}
}
defineExpose({
showDialog,
_show,
_formatTreeData,
treeList,
_hide,
_cancel,
_onItemSwitch,
_onItemChildSwitch,
_onItemSelect,
_onItemChildSelect,
_onItemParentSelect,
_reTreeList,
_initTree,
})
onMounted(() => {
_initTree()
})
</script>
<style lang="scss" scoped>
.tree-cover {
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
z-index: 100;
background-color: rgba(0, 0, 0, 0.4);
opacity: 0;
transition: all 0.3s ease;
visibility: hidden;
}
.tree-cover.show {
visibility: visible;
opacity: 1;
}
.tree-dialog {
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background-color: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
z-index: 102;
top: 20%;
transition: all 0.3s ease;
transform: translateY(100%);
}
.tree-dialog.show {
transform: translateY(0);
}
.tree-bar {
/* background-color: #fff; */
height: 40px;
padding-left: 15px;
padding-right: 25px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-bottom-width: 1px !important;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
font-size: 15px;
color: #757575;
line-height: 1;
}
.tree-bar-confirm {
color: #0055ff;
padding: 15px;
}
.tree-bar-title {
color: #007aff;
}
.tree-bar-cancel {
color: #757575;
padding: 10px;
}
.tree-div {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
overflow: hidden;
height: 100%;
}
.tree-list {
flex: 1;
height: 100%;
overflow: hidden;
}
.tree-box {
:active {
background: #eeeeee;
}
}
.tree-item {
display: flex;
justify-content: space-between;
align-items: center;
line-height: 1;
height: 0;
opacity: 0;
transition: 0.2s;
overflow: hidden;
:active {
background: none;
}
}
.tree-item.show {
height: 5vh;
opacity: 1;
padding: 0 15px 0 0;
border: 1px solid #f0f8f8;
margin-bottom: -1px;
}
.tree-item.showchild:before {
transform: rotate(90deg);
}
.tree-item.last:before {
opacity: 0;
}
.switch-on {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 10px solid #666;
}
.switch-off {
width: 0;
height: 0;
border-bottom: 6px solid transparent;
border-top: 6px solid transparent;
border-left: 10px solid #666;
}
.item-last-dot {
position: absolute;
width: 0px;
height: 0px;
border-radius: 100%;
background: #666;
}
.item-icon {
width: 0px;
height: 8px;
/* margin-right: 8px; */
padding-right: 20px;
padding-left: 20px;
}
.item-label {
flex: 1;
display: flex;
align-items: center;
height: 100%;
line-height: 1.2;
}
.item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
text-align: left;
font-size: 16px;
}
.item-check {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.item-check-yes,
.item-check-no {
width: 20px;
height: 20px;
border-top-left-radius: 20%;
border-top-right-radius: 20%;
border-bottom-right-radius: 20%;
border-bottom-left-radius: 20%;
border-top-width: 1px;
border-left-width: 1px;
border-bottom-width: 1px;
border-right-width: 1px;
border-style: solid;
border-color: #0055ff;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.item-check-yes-part {
width: 12px;
height: 12px;
border-top-left-radius: 20%;
border-top-right-radius: 20%;
border-bottom-right-radius: 20%;
border-bottom-left-radius: 20%;
background-color: #0055ff;
}
.item-check-yes-all {
margin-bottom: 5px;
border: 2px solid #007aff;
border-left: 0;
border-top: 0;
height: 12px;
width: 6px;
transform-origin: center;
/* #ifndef APP-NVUE */
transition: all 0.3s;
/* #endif */
transform: rotate(45deg);
}
.item-check .radio {
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
border-bottom-left-radius: 50%;
}
.item-check .radio .item-check-yes-b {
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-right-radius: 50%;
border-bottom-left-radius: 50%;
}
.hover-c {
opacity: 0.6;
}
.itemBorder {
border-bottom: 1px solid #e5e5e5;
}
</style>
3.控制样式的组件
<template>
<div
v-bind:class="{ my_scroll_container: true }"
@scroll="getScroll"
:style="`${
scrollX === 'true' || scrollX === true
? 'overflow-x:scroll;overflow-y:hidden'
: ''
};${
scrollY === 'true' || scrollY === true
? 'overflow-x:hidden;overflow-y:scroll'
: ''
};`"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue'
// const scrollX = ref<string | boolean>(false)
// const scrollY = ref<string | boolean>(false)
const scrollTop = ref<number>(0)
let $emit = defineEmits(['reachBottom'])
defineProps({
scrollX: {
type: [String, Boolean],
value: false,
},
scrollY: {
type: [String, Boolean],
value: false,
},
})
// 滚动事件
const getScroll = (e: any) => {
let wScrollY = e.target.scrollTop // 当前滚动条位置
scrollTop.value = wScrollY
let wInnerH = e.target.clientHeight // 设备窗口的高度(不会变)
let bScrollH = e.target.scrollHeight // 元素总高度
if (wScrollY + wInnerH >= bScrollH) {
$emit('reachBottom')
}
}
defineExpose({
scrollTop,
getScroll,
})
// export default {
// props: {
// scrollX: {
// type: [String, Boolean],
// value: false,
// },
// scrollY: {
// type: [String, Boolean],
// value: false,
// },
// },
// data() {
// return {
// scrollTop: 0,
// }
// },
// methods: {
// //滚动事件
// // getScroll(e) {
// // let wScrollY = e.target.scrollTop // 当前滚动条位置
// // this.scrollTop = wScrollY
// // let wInnerH = e.target.clientHeight // 设备窗口的高度(不会变)
// // let bScrollH = e.target.scrollHeight // 元素总高度
// // if (wScrollY + wInnerH >= bScrollH) {
// // this.$emit('reachBottom')
// // }
// // },
// },
// activated() {
// this.$el.scrollTop = this.scrollTop
// this.$emit('getScrollTop', { scrollTop: this.scrollTop })
// },
// updated() {},
// mounted() {},
// }
</script>
<style scoped>
.my_scroll_container {
width: 100%;
height: 250px;
}
</style>