flat:来源标红
This commit is contained in:
@@ -1,363 +1,332 @@
|
||||
<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>
|
||||
<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;
|
||||
}
|
||||
|
||||
::v-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>
|
||||
|
||||
Reference in New Issue
Block a user