【uniapp学习树形图】
树形图 uniapp
·
效果图
代码块
引用
<template>
<!-- https://ext.dcloud.net.cn/plugin?id=10295 -->
<!--/pages/index/index-->
<custom-tree-select
:choseParent='true'
placeholder="请选择"
dataLabel="text"
dataValue="value"
:mutiple="true"
:listData="listData"
:linkage="true"
:clearable="true"
:search="true"
v-model="formData.selected"
@done="done">
</custom-tree-select>
</template>
<script>
export default {
data() {
return {
formData: {
selected: ''
},
listData: [{
value: 1,
text: '城市1',
children: [{
value: 3,
text: '街道1',
children: [{
value: 4,
text: '小区1',
disabled: true
},
{
value: 5,
text: '小区2'
}
]
}]
},
{
value: 2,
text: '城市2',
children: [{
value: 6,
text: '街道2'
}]
},
{
value: 7,
text: '城市3',
visible: false,
children: [{
value: 8,
text: '街道1'
},
{
value: 9,
text: '街道2'
},
{
value: 10,
text: '街道10'
}
]
}
]
}
},
methods: {
done(data) {
console.log(data)
},
}
}
</script>
组件
custom-tree-select.vue
<template>
<view class="custom-tree-select-content">
<view :class="['select-list', { disabled }, { active: selectList.length }]" @click="open">
<view class="left">
<view v-if="selectList.length">
<view class="select-item" v-for="item in selectList" :key="item">
<view class="name">
<text>{{ getName(item) }}</text>
</view>
<view v-if="!disabled" class="close" @click.stop="removeSelectedItem(item)">
<uni-icons type="closeempty" size="18" color="#fff"></uni-icons>
</view>
</view>
</view>
<view v-if="!selectList.length" style="color: #6a6a6a" class="no-data">
<text>{{ placeholder }}</text>
</view>
</view>
<view class="right">
<uni-icons
v-if="(!selectList.length || !clearable) && !clickOpen"
type="bottom"
size="14"
color="#999"
></uni-icons>
<uni-icons
v-if="selectList.length && clearable && !clickOpen"
type="clear"
size="24"
color="#c0c4cc"
@click.native.stop="clear"
></uni-icons>
<uni-icons v-if="clickOpen" class="rotating" type="spinner-cycle" size="16"></uni-icons>
</view>
</view>
<uni-popup
v-if="showPopup"
ref="popup"
:animation="animation"
:is-mask-click="isMaskClick"
:mask-background-color="maskBackgroundColor"
:background-color="backgroundColor"
:safe-area="safeArea"
type="bottom"
@change="change"
@maskClick="maskClick"
>
<view class="popup-content" :style="{ height: contentHeight }">
<view class="title">
<view class="left" :style="{ color: cancelTextColor }" @click="close()">
<text>{{ cancelText }}</text>
</view>
<view class="center">
<text>{{ placeholder }}</text>
</view>
<view class="right" :style="{ color: confirmTextColor }" @click="done()">
<text>{{ confirmText }}</text>
</view>
</view>
<view v-if="search" class="search-box">
<uni-easyinput :maxlength="-1" prefixIcon="search" placeholder="搜索" @input="handleSearch"></uni-easyinput>
</view>
<view v-if="treeData.length" class="select-content">
<scroll-view class="scroll-view-box" scroll-y="true" @touchmove.stop>
<view v-if="!filterTreeData.length" class="no-data center">
<text>暂无数据</text>
</view>
<data-select-item
v-for="item in filterTreeData"
:key="item[dataValue]"
:node="item"
:dataLabel="dataLabel"
:dataValue="dataValue"
:dataChildren="dataChildren"
:choseParent="choseParent"
></data-select-item>
</scroll-view>
</view>
<view v-else class="no-data center">
<text>暂无数据</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import dataSelectItem from './data-select-item.vue'
export default {
name: 'custom-tree-select',
components: {
dataSelectItem
},
model: {
prop: 'value',
event: 'input'
},
props: {
search: {
type: Boolean,
default: false
},
animation: {
type: Boolean,
default: true
},
'is-mask-click': {
type: Boolean,
default: true
},
'mask-background-color': {
type: String,
default: 'rgba(0,0,0,0.4)'
},
'background-color': {
type: String,
default: 'none'
},
'safe-area': {
type: Boolean,
default: true
},
choseParent: {
type: Boolean,
default: true
},
placeholder: {
type: String,
default: '请选择'
},
confirmText: {
type: String,
default: '完成'
},
confirmTextColor: {
type: String,
default: '#007aff'
},
cancelText: {
type: String,
default: '取消'
},
cancelTextColor: {
type: String,
default: '#333'
},
listData: {
type: Array,
default: () => []
},
dataLabel: {
type: String,
default: 'name'
},
dataValue: {
type: String,
default: 'id'
},
dataChildren: {
type: String,
default: 'children'
},
linkage: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
},
mutiple: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
value: {
type: [Array, String],
default: () => []
},
},
data() {
return {
contentHeight: '500px',
treeData: [],
filterTreeData: [],
clearTimerList: [],
showPopup: false,
clickOpen: false,
clickOpenTimer: null,
timer: null
}
},
computed: {
selectList() {
return typeof this.value === 'string'
? this.value.length
? this.value.split(',')
: []
: this.value.map((item) => item.toString())
}
},
watch: {
listData: {
deep: true,
immediate: true,
handler(newVal) {
this.treeData = this.deepCopy(newVal)
this.initData(this.treeData)
if (this.clickOpen) {
this.clickOpen = false
this.open()
}
}
},
value: {
deep: true,
handler() {
this.initData(this.treeData)
this.updateTreeData(this.filterTreeData)
}
}
},
mounted() {
this.getContentHeight(uni.getSystemInfoSync())
this.$bus.$on('custom-tree-select-node-click', (node) => {
this.handleNodeClick(node)
})
this.$bus.$on('custom-tree-select-name-click', (node) => {
this.handleHideChildren(node)
})
},
methods: {
paging(data, PAGENUM = 50) {
if (!data instanceof Array || !data.length) return data
const pages = []
data.forEach((item, index) => {
const i = Math.floor(index / PAGENUM)
if (!pages[i]) {
pages[i] = []
}
pages[i].push(Object.freeze(item))
})
return pages
},
handleSearch(str) {
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.resetClearTimerList()
const pagingArr = this.paging(this.searchValue(str, this.treeData))
this.filterTreeData.splice(0, this.filterTreeData.length, ...(pagingArr?.[0] || []))
this.lazyRenderList(pagingArr, 1)
// uni.hideKeyboard()
}, 300)
},
searchValue(str, arr) {
const res = []
arr.forEach((item) => {
if (item[this.dataLabel].toLowerCase().indexOf(str.toLowerCase()) > -1) {
res.push(item)
} else {
if (item[this.dataChildren]?.length) {
const data = this.searchValue(str, item[this.dataChildren])
if (data?.length) {
res.push({
...item,
[this.dataChildren]: data
})
}
}
}
})
return res
},
updateTreeData(arr) {
if (arr.length) {
for (let i = 0; i < arr.length; i++) {
arr[i].checked = this.getTruthNode(arr[i]).checked
if (arr[i][this.dataChildren]?.length) {
this.updateTreeData(arr[i][this.dataChildren])
}
}
}
},
getContentHeight({ screenHeight }) {
this.contentHeight = `${Math.floor(screenHeight * 0.7)}px`
},
open() {
if (this.disabled || this.clickOpen) return
if (!this.treeData.length) {
if (!this.clickOpen) {
this.clickOpen = true
this.clickOpenTimer = setTimeout(() => {
if (this.clickOpen) {
this.clickOpen = false
uni.showToast({
title: '暂无数据项',
icon: 'none',
duration: 1000
})
}
}, 2000)
}
return
}
const pagingArr = this.paging(this.treeData)
this.filterTreeData.push(...(pagingArr?.[0] || []))
this.showPopup = true
this.$nextTick(() => {
this.$refs.popup.open()
this.lazyRenderList(pagingArr, 1)
})
},
lazyRenderList(arr, startIndex) {
for (let i = startIndex; i < arr.length; i++) {
let timer = null
timer = setTimeout(() => {
this.filterTreeData.push(...arr[i])
}, i * 500)
this.clearTimerList.push(() => clearTimeout(timer))
}
},
close() {
this.$refs.popup.close()
this.showPopup = false
},
done(){
this.$emit('done', this.selectList)
this.close()
},
change(data) {
if (!data.show) {
this.resetClearTimerList()
this.filterTreeData.splice(0, this.filterTreeData.length)
}
this.$emit('change', data)
},
resetClearTimerList() {
const list = [...this.clearTimerList]
this.clearTimerList.splice(0, this.clearTimerList.length)
list.forEach((item) => item())
},
maskClick() {
this.$emit('maskClick')
},
deepCopy(target) {
let copyed_objs = [] //此数组解决了循环引用和相同引用的问题,它存放已经递归到的目标对象
function _deepCopy(target) {
if (typeof target !== 'object' || !target) {
return target
}
for (let i = 0; i < copyed_objs.length; i++) {
if (copyed_objs[i].target === target) {
return copyed_objs[i].copyTarget
}
}
let obj = {}
if (Array.isArray(target)) {
obj = [] //处理target是数组的情况
}
copyed_objs.push({ target: target, copyTarget: obj })
Object.keys(target).forEach((key) => {
if (obj[key]) {
return
}
obj[key] = _deepCopy(target[key])
})
return obj
}
return _deepCopy(target)
},
initData(arr) {
for (let i = 0; i < arr.length; i++) {
if (this.selectList.includes(arr[i][this.dataValue].toString())) {
this.$set(arr[i], 'checked', true)
} else {
this.$set(arr[i], 'checked', false)
}
if (arr[i].disabled) {
this.$set(arr[i], 'disabled', true)
} else {
this.$set(arr[i], 'disabled', false)
}
if (JSON.stringify(arr[i].visible) === 'false') {
this.$set(arr[i], 'visible', false)
if (arr[i][this.dataChildren]?.length) {
for (let j = 0; j < arr[i][this.dataChildren].length; j++) {
arr[i][this.dataChildren][j].visible = false
}
}
} else {
this.$set(arr[i], 'visible', true)
}
this.$set(arr[i], 'showChildren', arr[i].showChildren ?? true)
if (!arr[i].handleNodeClick) {
this.$set(arr[i], 'handleNodeClick', this.handleNodeClick)
}
if (!arr[i].handleHideChildren) {
this.$set(arr[i], 'handleHideChildren', this.handleHideChildren)
}
if (arr[i][this.dataChildren]?.length) {
this.initData(arr[i][this.dataChildren])
}
}
},
isString(data) {
return typeof data === 'string'
},
getChildren(node) {
if (!node[this.dataChildren]?.length) return []
const res = node[this.dataChildren].reduce((pre, val) => {
if (val.visible) {
return [...pre, val]
}
return pre
}, [])
for (let i = 0; i < node[this.dataChildren].length; i++) {
res.push(...this.getChildren(node[this.dataChildren][i]))
}
return res
},
getParentNode(target, arr) {
let res = []
for (let i = 0; i < arr.length; i++) {
if (arr[i][this.dataValue] === target[this.dataValue]) {
return [arr[i]]
}
if (arr[i][this.dataChildren]?.length) {
const result = this.getParentNode(target, arr[i][this.dataChildren])
if (result.length && arr[i].visible) {
res = [...result, arr[i]]
}
}
}
return res
},
getContiguousNodes(target, arr) {
for (let i = 0; i < arr.length; i++) {
if (arr[i][this.dataValue] === target[this.dataValue]) {
return arr.reduce((pre, val) => {
if (val.visible) {
return [...pre, val]
}
return pre
}, [])
}
if (arr[i][this.dataChildren]?.length) {
const res = this.getContiguousNodes(target, arr[i][this.dataChildren])
if (res.length) return res
}
}
return []
},
allChecked(value, arr) {
for (let i = 0; i < arr.length; i++) {
if (!value.includes(arr[i][this.dataValue].toString())) {
return false
}
}
return true
},
getTruthNode(node) {
const arr = [...this.treeData]
while (arr.length) {
const item = arr.shift()
if (item[this.dataValue] === node[this.dataValue]) {
return item
}
if (item[this.dataChildren]?.length) {
arr.push(...item[this.dataChildren])
}
}
return null
},
handleNodeClick(node) {
node = this.getTruthNode(node)
node.checked = !node.checked
// 如果是单选不考虑其他情况
if (!this.mutiple) {
if (node.checked) {
this.$emit(
'input',
this.isString(this.value) ? node[this.dataValue].toString() : [node[this.dataValue].toString()]
)
} else {
this.$emit('input', this.isString(this.value) ? '' : [])
}
this.close()
} else {
// 多选情况
if (!this.linkage) {
// 不需要联动
let emitData = null
if (node.checked) {
emitData = Array.from(new Set([...this.selectList, node[this.dataValue].toString()]))
} else {
emitData = this.selectList.filter((id) => id !== node[this.dataValue].toString())
}
this.$emit('input', this.isString(this.value) ? emitData.join(',') : emitData)
} else {
// 需要联动
let emitData = [...this.selectList]
let childrenVal = []
if (node[this.dataChildren]?.length) {
childrenVal = this.getChildren(node)
.filter((item) => !item.disabled)
.map((item) => item[this.dataValue].toString())
}
const contiguousNodes = this.getContiguousNodes(node, this.treeData).filter((item) => !item.disabled)
const [_, ...parentNodes] = this.getParentNode(node, this.treeData)
if (node.checked) {
// 选中
emitData = Array.from(new Set([...emitData, node[this.dataValue].toString()]))
if (childrenVal.length) {
// 选中全部子节点
emitData = Array.from(new Set([...emitData, ...childrenVal]))
}
if (parentNodes.length && this.allChecked(emitData, contiguousNodes)) {
// 有父元素 如果父元素下所有子元素全部选中,选中父元素
while (parentNodes.length) {
const item = parentNodes.shift()
if (!item.disabled) {
const children = this.getChildren(item).filter((child) => !child.disabled)
if (this.allChecked(emitData, children)) {
emitData = Array.from(new Set([...emitData, item[this.dataValue].toString()]))
} else {
break
}
}
}
}
} else {
// 取消选中
emitData = emitData.filter((id) => id !== node[this.dataValue].toString())
if (childrenVal.length) {
// 取消选中全部子节点
childrenVal.forEach((childVal) => {
emitData = emitData.filter((id) => id !== childVal)
})
}
}
this.$emit('input', this.isString(this.value) ? emitData.join(',') : emitData)
}
}
},
handleHideChildren(node) {
if (!node[this.dataChildren]?.length) return
this.$set(node, 'showChildren', !node.showChildren)
},
getName(id) {
const arr = [...this.treeData]
while (arr.length) {
const item = arr.shift()
if (item[this.dataValue].toString() === id) {
return item[this.dataLabel]
}
if (item[this.dataChildren]?.length) {
arr.push(...item[this.dataChildren])
}
}
return ''
},
removeSelectedItem(id) {
const emitData = this.selectList.filter((item) => item !== id)
this.$emit('input', this.isString(this.value) ? emitData.join(',') : emitData)
},
clear() {
if (this.disabled) return
this.$emit('input', this.isString(this.value) ? '' : [])
}
}
}
</script>
<style lang="scss" scoped>
.custom-tree-select-content {
.select-list {
padding-left: 10px;
min-height: 35px;
border: 1px solid #e5e5e5;
border-radius: 4px;
display: flex;
align-items: center;
&.active {
padding: 4px 0 4px 10px;
}
.left {
flex: 1;
display: flex;
flex-wrap: wrap;
.select-item {
margin: 4px 10px 4px 0;
padding: 4px 5px;
max-width: auto;
height: auto;
background-color: #007aff;
border-radius: 4px;
color: #fff;
display: flex;
align-items: center;
.name {
flex: 1;
padding-right: 10px;
font-size: 14px;
}
.close {
width: 18px;
height: 18px;
overflow: hidden;
}
}
}
.right {
margin-right: 5px;
display: flex;
justify-content: flex-end;
align-items: center;
}
&.disabled {
background-color: #f5f7fa;
.left {
.select-item {
.name {
padding: 0;
}
}
}
}
}
.popup-content {
flex: 1;
background-color: #fff;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
display: flex;
flex-direction: column;
.title {
padding: 8px 3rem;
border-bottom: 1px solid $uni-border-color;
font-size: 14px;
display: flex;
justify-content: space-between;
position: relative;
.left {
position: absolute;
left: 10px;
}
.center {
flex: 1;
text-align: center;
}
.right {
position: absolute;
right: 10px;
}
}
.search-box {
margin: 8px 10px 0;
background-color: #fff;
}
.select-content {
margin: 8px 10px;
flex: 1;
overflow: hidden;
position: relative;
}
.scroll-view-box {
touch-action: none;
flex: 1;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
.no-data {
width: auto;
color: #999;
font-size: 12px;
}
.no-data.center {
text-align: center;
}
.rotating {
animation: ROTATING 1s infinite linear;
}
@keyframes ROTATING {
form {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
}
</style>
data-select-item.vue
<template>
<view class="custom-tree-select-content">
<view v-if="node.visible" class="custom-tree-select-item">
<view class="item-content" :style="{ paddingLeft: `${level * 10}px` }">
<view class="left" @click="nameClick(node)">
<view
:class="[
'icon',
{
active: node.showChildren
},
{ 'last-level': !node[dataChildren] || (node[dataChildren] && !node[dataChildren].length) }
]"
></view>
<view class="name" :style="node.disabled ? 'color: #999' : ''">
<text>{{ node[dataLabel] }}</text>
</view>
</view>
<checkbox
v-if="
choseParent ||
(!choseParent && !node[dataChildren]) ||
(!choseParent && node[dataChildren] && !node[dataChildren].length)
"
:disabled="node.disabled"
:value="node[dataValue].toString()"
:checked="node.checked"
@click="nodeClick(node)"
/>
</view>
</view>
<view v-if="node.showChildren && node[dataChildren] && node[dataChildren].length">
<data-select-item
v-for="item in node[dataChildren]"
:key="item[dataValue]"
:node="item"
:dataLabel="dataLabel"
:dataValue="dataValue"
:dataChildren="dataChildren"
:choseParent="choseParent"
:level="level + 1"
></data-select-item>
</view>
</view>
</template>
<script>
import dataSelectItem from './data-select-item.vue'
export default {
name: 'data-select-item',
components: {
'data-select-item': dataSelectItem
},
props: {
node: {
type: Object,
default: () => ({})
},
choseParent: {
type: Boolean,
default: true
},
dataLabel: {
type: String,
default: 'name'
},
dataValue: {
type: String,
default: 'value'
},
dataChildren: {
type: String,
default: 'children'
},
level: {
type: Number,
default: 0
}
},
methods: {
nameClick(node) {
// #ifdef MP-WEIXIN
this.$bus.$emit('custom-tree-select-name-click', node)
// #endif
// #ifndef MP-WEIXIN
node.handleHideChildren(node)
// #endif
},
nodeClick(node) {
if (!node.disabled) {
// #ifdef MP-WEIXIN
this.$bus.$emit('custom-tree-select-node-click', node)
// #endif
// #ifndef MP-WEIXIN
node.handleNodeClick(node)
// #endif
}
}
},
options: {
styleIsolation: 'shared'
}
}
</script>
<style lang="scss" scoped>
.custom-tree-select-content {
/deep/ .uni-checkbox-input {
margin: 0 !important;
}
.item-content {
margin: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
&:first-child {
margin-top: 0;
}
.left {
padding-right: 10px;
flex: 1;
display: flex;
align-items: center;
}
.icon {
margin: 0 5px;
border-style: solid;
border-color: transparent;
border-width: 5px 0 5px 5px;
border-left-color: #000;
transition: transform 0.2s ease;
&.active {
transform: rotate(90deg);
}
&.last-level {
width: 5px;
height: 5px;
border-radius: 5px;
background-color: #000;
border: none;
}
}
.name {
flex: 1;
height: auto;
word-break: break-all;
}
}
}
</style>
更多推荐
已为社区贡献20条内容
所有评论(0)