364 lines
7.8 KiB
Vue
364 lines
7.8 KiB
Vue
<template>
|
|
<view
|
|
class="custom-tree-select-content"
|
|
:class="{
|
|
border:
|
|
border &&
|
|
node[dataChildren] &&
|
|
node[dataChildren].length &&
|
|
node.showChildren
|
|
}"
|
|
:style="{ marginLeft: `${level ? 14 : 0}px` }"
|
|
>
|
|
<view v-if="node.visible" class="custom-tree-select-item">
|
|
<view class="item-content">
|
|
<view class="left">
|
|
<view
|
|
v-if="node[dataChildren] && node[dataChildren].length"
|
|
:class="['right-icon', { active: node.showChildren }]"
|
|
@click.stop="nameClick(node)"
|
|
>
|
|
<uni-icons type="forward" size="14" color="#333"></uni-icons>
|
|
</view>
|
|
<view v-else class="smallcircle-filled">
|
|
<uni-icons
|
|
class="smallcircle-filled-icon"
|
|
type="smallcircle-filled"
|
|
size="10"
|
|
color="#333"
|
|
></uni-icons>
|
|
</view>
|
|
<view
|
|
v-if="loadingArr.includes(node[dataValue])"
|
|
class="loading-icon-box"
|
|
>
|
|
<uni-icons
|
|
class="loading-icon"
|
|
type="spinner-cycle"
|
|
size="14"
|
|
color="#333"
|
|
></uni-icons>
|
|
</view>
|
|
<view
|
|
class="name"
|
|
:style="node.disabled ? 'color: #999' : ''"
|
|
@click.stop="nameClick(node)"
|
|
>
|
|
<text>{{ node[dataLabel] }}</text>
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-if="
|
|
choseParent ||
|
|
(!choseParent && !node[dataChildren]) ||
|
|
(!choseParent && node[dataChildren] && !node[dataChildren].length)
|
|
"
|
|
:class="['check-box', { disabled: node.disabled }]"
|
|
@click.stop="nodeClick(node)"
|
|
>
|
|
<view
|
|
v-if="!node.checked && node.partChecked && linkage"
|
|
class="part-checked"
|
|
></view>
|
|
<uni-icons
|
|
v-if="node.checked"
|
|
type="checkmarkempty"
|
|
size="18"
|
|
:color="node.disabled ? '#333' : '#007aff'"
|
|
></uni-icons>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-if="
|
|
node.showChildren && node[dataChildren] && node[dataChildren].length
|
|
"
|
|
>
|
|
<data-select-item
|
|
v-for="item in listData"
|
|
:key="item[dataValue]"
|
|
:node="item"
|
|
:dataLabel="dataLabel"
|
|
:dataValue="dataValue"
|
|
:dataChildren="dataChildren"
|
|
:choseParent="choseParent"
|
|
:border="border"
|
|
:linkage="linkage"
|
|
:level="level + 1"
|
|
:load="load"
|
|
:lazyLoadChildren="lazyLoadChildren"
|
|
></data-select-item>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import dataSelectItem from './data-select-item.vue'
|
|
import { paging } from './utils'
|
|
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'
|
|
},
|
|
border: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
linkage: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
level: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
load: {
|
|
type: Function,
|
|
default: function () {}
|
|
},
|
|
lazyLoadChildren: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
listData: [],
|
|
clearTimerList: [],
|
|
loadingArr: []
|
|
}
|
|
},
|
|
computed: {
|
|
watchData() {
|
|
const { node, dataChildren } = this
|
|
|
|
return {
|
|
node,
|
|
dataChildren
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
watchData: {
|
|
immediate: true,
|
|
handler(newVal) {
|
|
const { node, dataChildren } = newVal
|
|
if (
|
|
node.showChildren &&
|
|
node[dataChildren] &&
|
|
node[dataChildren].length
|
|
) {
|
|
this.resetClearTimerList()
|
|
this.renderTree(node[dataChildren])
|
|
}
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
// 懒加载
|
|
renderTree(arr) {
|
|
const pagingArr = paging(arr)
|
|
this.listData.splice(0, this.listData.length, ...(pagingArr?.[0] || []))
|
|
this.lazyRenderList(pagingArr, 1)
|
|
},
|
|
// 懒加载具体逻辑
|
|
lazyRenderList(arr, startIndex) {
|
|
for (let i = startIndex; i < arr.length; i++) {
|
|
let timer = null
|
|
timer = setTimeout(() => {
|
|
this.listData.push(...arr[i])
|
|
}, i * 500)
|
|
this.clearTimerList.push(() => clearTimeout(timer))
|
|
}
|
|
},
|
|
// 中断懒加载
|
|
resetClearTimerList() {
|
|
const list = [...this.clearTimerList]
|
|
this.clearTimerList.splice(0, this.clearTimerList.length)
|
|
list.forEach((item) => item())
|
|
},
|
|
async nameClick(node) {
|
|
if (!node[this.dataChildren]?.length && this.lazyLoadChildren) {
|
|
this.loadingArr.push(node[this.dataValue])
|
|
try {
|
|
const res = await this.load(node)
|
|
if (Array.isArray(res)) {
|
|
uni.$emit('custom-tree-select-load', {
|
|
source: node,
|
|
target: res
|
|
})
|
|
}
|
|
} finally {
|
|
this.loadingArr = []
|
|
}
|
|
} else {
|
|
if (
|
|
!node.showChildren &&
|
|
node[this.dataChildren] &&
|
|
node[this.dataChildren].length
|
|
) {
|
|
// 打开
|
|
this.renderTree(node[this.dataChildren])
|
|
} else {
|
|
// 关闭
|
|
this.resetClearTimerList()
|
|
this.listData.splice(0, this.listData.length)
|
|
}
|
|
uni.$emit('custom-tree-select-name-click', node)
|
|
}
|
|
},
|
|
nodeClick(node) {
|
|
if (!node.disabled) {
|
|
uni.$emit('custom-tree-select-node-click', node)
|
|
}
|
|
}
|
|
},
|
|
options: {
|
|
styleIsolation: 'shared'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
$primary-color: #007aff;
|
|
$col-sm: 4px;
|
|
$col-base: 8px;
|
|
$col-lg: 12px;
|
|
$row-sm: 5px;
|
|
$row-base: 10px;
|
|
$row-lg: 15px;
|
|
$radius-sm: 3px;
|
|
$radius-base: 6px;
|
|
$border-color: #c8c7cc;
|
|
|
|
.custom-tree-select-content {
|
|
&.border {
|
|
border-left: 1px solid $border-color;
|
|
}
|
|
|
|
/deep/ .uni-checkbox-input {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
.item-content {
|
|
margin: 0 0 $col-lg;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
position: relative;
|
|
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background-color: #fff;
|
|
transform: translateX(-2px);
|
|
z-index: 1;
|
|
}
|
|
|
|
.left {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.right-icon {
|
|
transition: 0.15s ease;
|
|
|
|
&.active {
|
|
transform: rotate(90deg);
|
|
}
|
|
}
|
|
|
|
.smallcircle-filled {
|
|
width: 14px;
|
|
height: 13.6px;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.smallcircle-filled-icon {
|
|
transform-origin: center;
|
|
transform: scale(0.55);
|
|
}
|
|
}
|
|
|
|
.loading-icon-box {
|
|
margin-right: $row-sm;
|
|
width: 14px;
|
|
height: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
.loading-icon {
|
|
transform-origin: center;
|
|
animation: rotating infinite 0.2s ease;
|
|
}
|
|
}
|
|
|
|
.name {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.check-box {
|
|
width: 23.6px;
|
|
height: 23.6px;
|
|
border: 1px solid $border-color;
|
|
border-radius: $radius-sm;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
&.disabled {
|
|
background-color: rgb(225, 225, 225);
|
|
}
|
|
|
|
.part-checked {
|
|
width: 60%;
|
|
height: 2px;
|
|
background-color: $primary-color;
|
|
}
|
|
}
|
|
|
|
@keyframes rotating {
|
|
from {
|
|
transform: rotate(0);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|