flat:所属社区
This commit is contained in:
187
uni_modules/custom-tree-select/changelog.md
Normal file
187
uni_modules/custom-tree-select/changelog.md
Normal file
@@ -0,0 +1,187 @@
|
||||
## 4.1.0(2023-10-22)
|
||||
- 修复小程序端无法移除已选项问题
|
||||
- 修复小程序端异步加载节点不可用问题
|
||||
- 增加点击左侧箭头同样可以展开元素
|
||||
## 4.0.0(2023-09-17)
|
||||
- 调整 v-model 传参类型,为适应数值和字符串同时存在的树形数据
|
||||
## 3.8.4(2023-09-17)
|
||||
- 支持动态加载节点
|
||||
## 3.8.3(2023-09-17)
|
||||
- 文档更新
|
||||
## 3.8.2(2023-09-17)
|
||||
- 新增路径模式吗,解决重名选项无法辨认问题
|
||||
## 3.8.1(2023-08-06)
|
||||
- 修复加载选择器数据后,选择框没有数据问题
|
||||
## 3.8.0(2023-07-29)
|
||||
- 修复父子联动状态下,数据回显父级半选状态不展示问题
|
||||
- selectChange 返回数据调整
|
||||
- 新增 contentHeight 、disabledList 属性
|
||||
- 文档更新
|
||||
## 3.7.9(2023-07-11)
|
||||
- 文档更新
|
||||
## 3.7.8(2023-07-10)
|
||||
- 修复父元素未完全选中状态显示异常问题
|
||||
## 3.7.7(2023-07-07)
|
||||
- 文档更新
|
||||
## 3.7.6(2023-07-07)
|
||||
- 重写checkbox,解决与colorui样式冲突问题
|
||||
- 父级添加子级未全选状态,更直观了解选中数据情况
|
||||
- 样式优化,提取文件内样式变量
|
||||
- 代码体积优化
|
||||
## 3.7.5(2023-05-15)
|
||||
- 修复visible在数据初始化时配置有时失效问题
|
||||
## 3.7.4(2023-05-11)
|
||||
- 修复父元素下某个子元素设置隐藏时,父子联动状态异常问题
|
||||
- 对性能以及代码体积做了进一步优化
|
||||
## 3.7.3(2023-05-08)
|
||||
- 修复子元素展开懒加载功能被覆盖问题
|
||||
- 优化数据初始化速度
|
||||
## 3.7.2(2023-05-04)
|
||||
- uni.$emit 替换 bus事件
|
||||
## 3.7.1(2023-04-10)
|
||||
- 解决小程序无法折叠或展开问题
|
||||
## 3.7.0(2023-04-09)
|
||||
- 全选默认不开启,并且只在多选模式下生效
|
||||
## 3.6.91(2023-04-09)
|
||||
- 文档更新
|
||||
## 3.6.9(2023-04-09)
|
||||
- 代码整体优化
|
||||
- 新增弹窗安全区配置选项
|
||||
- 新增全选功能配置选项
|
||||
- 新增搜索框清除按钮是否直接重置搜索结果选项
|
||||
- 遗留问题:小程序端搜索后无法折叠或打开
|
||||
## 3.6.8(2023-04-07)
|
||||
- 优化deepClone
|
||||
## 3.6.7(2023-04-03)
|
||||
- 取消对微信小程序的支持
|
||||
## 3.6.6(2023-04-03)
|
||||
- 修复listData刷新后,不能正确渲染勾选状态问题
|
||||
## 3.6.5(2023-03-30)
|
||||
- 修复小程序节点展开或折叠时指示箭头不变换状态
|
||||
- 修复回显数据有时不展示问题
|
||||
## 3.6.3(2023-03-27)
|
||||
- [紧急] 修复单选状态仍可以多选bug
|
||||
## 3.6.2(2023-03-27)
|
||||
- 修复偶尔出现节点无法根据绑定数据改变选中状态问题
|
||||
## 3.6.1(2023-03-24)
|
||||
- 修复引入问题
|
||||
## 3.6.0(2023-03-24)
|
||||
- 重构去除冗余代码,功能不变
|
||||
## 3.5.0(2023-03-23)
|
||||
- 修复没有子元素时点击报错问题
|
||||
## 3.3.9(2023-03-22)
|
||||
- 解决搜索时 toLowerCase 报错问题
|
||||
## 3.3.8(2023-03-22)
|
||||
- 懒加载优化
|
||||
## 3.3.7(2023-03-20)
|
||||
- 样式更新
|
||||
## 3.3.6(2023-03-20)
|
||||
- 修复子元素折叠或展开不触发问题
|
||||
## 3.3.5(2023-03-15)
|
||||
- isArray 替换 instanceof
|
||||
## 3.3.4(2023-03-11)
|
||||
- 修复搜索后无法展开或折叠问题
|
||||
- 搜索后直接展开搜索项
|
||||
## 3.3.3(2023-03-06)
|
||||
- 优化展示效果
|
||||
- 添加引导线选项
|
||||
## 3.3.2(2023-02-22)
|
||||
- 修复搜索内容包含隐藏数据的问题
|
||||
- 优化搜索触发方式
|
||||
## 3.3.1(2023-02-16)
|
||||
- 修复异常
|
||||
## 3.2.9(2023-02-16)
|
||||
- 修复编译到小程序报错问题
|
||||
## 3.2.8(2023-02-16)
|
||||
- 修复多个组件key重复问题
|
||||
## 3.2.7(2023-02-16)
|
||||
- 全新底层架构设计
|
||||
- 修改默认配色,更显成熟稳重
|
||||
## 3.2.6(2023-02-15)
|
||||
- 修改文档
|
||||
## 3.2.5(2023-02-15)
|
||||
- 文档修改
|
||||
## 3.2.3(2023-02-15)
|
||||
- 修复小程序组件无法折叠问题
|
||||
## 3.2.2(2023-02-10)
|
||||
- 文档更新
|
||||
## 3.2.1(2023-02-09)
|
||||
- 增加节点展开和关闭全局配置
|
||||
- 修复搜索后数据无法展开和关闭问题
|
||||
- 修复搜索后不能自动滚动到顶部问题
|
||||
## 3.2.0(2023-01-16)
|
||||
- 修复事件冲突
|
||||
## 3.1.5(2023-01-13)
|
||||
- 样式调整
|
||||
## 3.1.4(2023-01-13)
|
||||
- 样式修复
|
||||
## 3.1.3(2023-01-13)
|
||||
- 文档更新
|
||||
## 3.1.2(2023-01-13)
|
||||
- 修复一些已知问题
|
||||
## 3.1.1(2023-01-12)
|
||||
- 解决微信小程序bus事件冲突问题
|
||||
## 3.1.0(2023-01-11)
|
||||
- 支持微信小程序
|
||||
## 3.0.3(2023-01-10)
|
||||
- 更新文档
|
||||
## 3.0.2(2023-01-09)
|
||||
- 修复了一些已知问题
|
||||
## 3.0.1(2023-01-09)
|
||||
- 修复了一些已知问题
|
||||
## 3.0.0(2023-01-09)
|
||||
- 全面支持懒加载,大量数据也能快速打开
|
||||
## 2.0.01(2022-12-29)
|
||||
- 文档修改
|
||||
## 2.0.0(2022-12-28)
|
||||
- 添加搜索功能
|
||||
- 完善文档
|
||||
## 1.9.2(2022-12-14)
|
||||
- 修复了一些已知问题
|
||||
## 1.9.1(2022-12-14)
|
||||
- 修复了一些已知问题
|
||||
## 1.9.0(2022-12-14)
|
||||
- 增加选项禁用、隐藏功能
|
||||
- 文档添加示例
|
||||
## 1.8.4(2022-12-13)
|
||||
- 文档修复
|
||||
## 1.8.3(2022-12-09)
|
||||
- 样式调整
|
||||
## 1.8.2(2022-12-07)
|
||||
- 修复弹窗不同设备显示高度问题
|
||||
## 1.8.1(2022-12-06)
|
||||
- 修复了一些已知问题
|
||||
## 1.8.0(2022-12-05)
|
||||
- 修复了一些已知问题
|
||||
- 图标替换图片
|
||||
## 1.7.0(2022-12-05)
|
||||
- 增加 disabled 属性,适用于查看页面
|
||||
- 修复了一些已知问题
|
||||
## 1.6.0(2022-12-05)
|
||||
- 修复了一些已知问题
|
||||
## 1.5.0(2022-12-05)
|
||||
- 根据绑定数据类型自动判断返回值类型
|
||||
- 完善文档
|
||||
## 1.4.0(2022-12-02)
|
||||
- 修复了一些已知问题
|
||||
## 1.3.1(2022-12-02)
|
||||
- 修复了一些已知问题
|
||||
## 1.3.0(2022-12-02)
|
||||
- 修复了一些已知问题
|
||||
## 1.2.01(2022-12-02)
|
||||
- 文档更新
|
||||
## 1.2.0(2022-12-02)
|
||||
- 添加一键清除功能
|
||||
- 添加多选、单选功能
|
||||
## 1.1.0(2022-12-02)
|
||||
- 支持数据回显、已选数据移除
|
||||
- 对标 uni-easyinput 默认样式
|
||||
- 支持更多属性和事件
|
||||
## 1.0.3(2022-12-01)
|
||||
- 修改信息
|
||||
## 1.0.2(2022-12-01)
|
||||
- 更新文档
|
||||
## 1.0.1(2022-12-01)
|
||||
- 更新预览截图
|
||||
## 1.0.0(2022-12-01)
|
||||
- 插件更新
|
||||
@@ -0,0 +1,983 @@
|
||||
<template>
|
||||
<view class="custom-tree-select-content">
|
||||
<view
|
||||
:class="['select-list', { disabled }, { active: selectList.length }]"
|
||||
:style="boxStyle"
|
||||
@click.stop="open"
|
||||
>
|
||||
<view class="left">
|
||||
<view v-if="selectList.length" class="select-items">
|
||||
<view
|
||||
class="select-item"
|
||||
v-for="item in selectedListBaseinfo"
|
||||
:key="item[dataValue]"
|
||||
>
|
||||
<view class="name">
|
||||
<text>{{ pathMode ? item.path : item[dataLabel] }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="!disabled && !item.disabled"
|
||||
class="close"
|
||||
@click.stop="removeSelectedItem(item)"
|
||||
>
|
||||
<uni-icons type="closeempty" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else style="color: #6a6a6a" class="no-data">
|
||||
<text style="#C0C4CB">{{ placeholder }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="right">
|
||||
<uni-icons
|
||||
v-if="!selectList.length || !clearable"
|
||||
type="bottom"
|
||||
size="14"
|
||||
color="#999"
|
||||
></uni-icons>
|
||||
<uni-icons
|
||||
v-if="selectList.length && clearable"
|
||||
type="clear"
|
||||
size="24"
|
||||
color="#c0c4cc"
|
||||
@click.native.stop="clear"
|
||||
></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 || defaultContentHeight }"
|
||||
>
|
||||
<view class="title">
|
||||
<view
|
||||
v-if="mutiple && canSelectAll"
|
||||
class="left"
|
||||
@click.stop="handleSelectAll"
|
||||
>
|
||||
<text>{{ isSelectedAll ? '取消全选' : '全选' }}</text>
|
||||
</view>
|
||||
<view class="center">
|
||||
<text>{{ placeholder }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="right"
|
||||
:style="{ color: confirmTextColor }"
|
||||
@click.stop="close"
|
||||
>
|
||||
<text>{{ confirmText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="search" class="search-box">
|
||||
<uni-easyinput
|
||||
:maxlength="-1"
|
||||
prefixIcon="search"
|
||||
placeholder="搜索"
|
||||
v-model="searchStr"
|
||||
confirm-type="search"
|
||||
@confirm="handleSearch(false)"
|
||||
@clear="handleSearch(true)"
|
||||
>
|
||||
</uni-easyinput>
|
||||
<button
|
||||
type="primary"
|
||||
size="mini"
|
||||
class="search-btn"
|
||||
@click.stop="handleSearch(false)"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="treeData.length" class="select-content">
|
||||
<scroll-view
|
||||
class="scroll-view-box"
|
||||
:scroll-top="scrollTop"
|
||||
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"
|
||||
:border="border"
|
||||
:linkage="linkage"
|
||||
:load="load"
|
||||
:lazyLoadChildren="lazyLoadChildren"
|
||||
></data-select-item>
|
||||
<view class="sentry" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view v-else class="no-data center">
|
||||
<text>暂无数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const partCheckedSet = new Set()
|
||||
import { paging } from './utils'
|
||||
import dataSelectItem from './data-select-item.vue'
|
||||
export default {
|
||||
name: 'custom-tree-select',
|
||||
components: {
|
||||
dataSelectItem
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
boxStyle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
canSelectAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
safeArea: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearResetSearch: {
|
||||
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'
|
||||
},
|
||||
contentHeight: {
|
||||
type: String
|
||||
},
|
||||
disabledList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
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
|
||||
},
|
||||
showChildren: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
pathMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
pathHyphen: {
|
||||
type: String,
|
||||
default: '/'
|
||||
},
|
||||
load: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
},
|
||||
lazyLoadChildren: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultContentHeight: '500px',
|
||||
treeData: [],
|
||||
filterTreeData: [],
|
||||
clearTimerList: [],
|
||||
selectedListBaseinfo: [],
|
||||
showPopup: false,
|
||||
clickOpenTimer: null,
|
||||
isSelectedAll: false,
|
||||
scrollTop: 0,
|
||||
searchStr: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectList() {
|
||||
return this.value || []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
listData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
partCheckedSet.clear()
|
||||
this.treeData = this.initData(newVal)
|
||||
|
||||
if (this.value) {
|
||||
this.changeStatus(this.treeData, this.value, true)
|
||||
this.filterTreeData.length &&
|
||||
this.changeStatus(this.filterTreeData, this.value)
|
||||
}
|
||||
if (this.showPopup) {
|
||||
this.resetClearTimerList()
|
||||
this.renderTree(this.treeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.changeStatus(this.treeData, this.value, true)
|
||||
this.filterTreeData.length &&
|
||||
this.changeStatus(this.filterTreeData, this.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getContentHeight(uni.getSystemInfoSync())
|
||||
},
|
||||
methods: {
|
||||
// 搜索完成返回顶部
|
||||
goTop() {
|
||||
this.scrollTop = 10
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = 0
|
||||
})
|
||||
},
|
||||
// 获取对应数据
|
||||
getReflectNode(node, arr) {
|
||||
const array = [...arr]
|
||||
while (array.length) {
|
||||
const item = array.shift()
|
||||
if (item[this.dataValue] === node[this.dataValue]) {
|
||||
return item
|
||||
}
|
||||
if (item[this.dataChildren]?.length) {
|
||||
array.push(...item[this.dataChildren])
|
||||
}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getContentHeight({ screenHeight }) {
|
||||
this.defaultContentHeight = `${Math.floor(screenHeight * 0.7)}px`
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch(isClear = false) {
|
||||
this.resetClearTimerList()
|
||||
if (isClear) {
|
||||
// 点击清空按钮并且设置清空按钮会重置搜索
|
||||
if (this.clearResetSearch) {
|
||||
this.renderTree(this.treeData)
|
||||
}
|
||||
} else {
|
||||
this.renderTree(this.searchValue(this.searchStr, this.treeData))
|
||||
}
|
||||
this.goTop()
|
||||
uni.hideKeyboard()
|
||||
},
|
||||
// 具体搜索方法
|
||||
searchValue(str, arr) {
|
||||
const res = []
|
||||
arr.forEach((item) => {
|
||||
if (item.visible) {
|
||||
if (
|
||||
item[this.dataLabel]
|
||||
.toString()
|
||||
.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) {
|
||||
if (
|
||||
str &&
|
||||
!item.showChildren &&
|
||||
item[this.dataChildren]?.length
|
||||
) {
|
||||
item.showChildren = true
|
||||
}
|
||||
res.push({
|
||||
...item,
|
||||
[this.dataChildren]: data
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return res
|
||||
},
|
||||
// 懒加载
|
||||
renderTree(arr) {
|
||||
const pagingArr = paging(arr)
|
||||
this.filterTreeData.splice(
|
||||
0,
|
||||
this.filterTreeData.length,
|
||||
...(pagingArr?.[0] || [])
|
||||
)
|
||||
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))
|
||||
}
|
||||
},
|
||||
// 中断懒加载
|
||||
resetClearTimerList() {
|
||||
const list = [...this.clearTimerList]
|
||||
this.clearTimerList = []
|
||||
list.forEach((fn) => fn())
|
||||
},
|
||||
// 打开弹窗
|
||||
open() {
|
||||
// disaled模式下禁止打开弹窗
|
||||
if (this.disabled) return
|
||||
this.showPopup = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.popup.open()
|
||||
this.renderTree(this.treeData)
|
||||
})
|
||||
},
|
||||
// 关闭弹窗
|
||||
close() {
|
||||
this.$refs.popup.close()
|
||||
},
|
||||
// 弹窗状态变化 包括点击回显框和遮罩
|
||||
change(data) {
|
||||
if (data.show) {
|
||||
uni.$on('custom-tree-select-node-click', this.handleNodeClick)
|
||||
uni.$on('custom-tree-select-name-click', this.handleHideChildren)
|
||||
uni.$on('custom-tree-select-load', this.handleLoadNode)
|
||||
} else {
|
||||
uni.$off('custom-tree-select-node-click', this.handleNodeClick)
|
||||
uni.$off('custom-tree-select-name-click', this.handleHideChildren)
|
||||
uni.$off('custom-tree-select-load', this.handleLoadNode)
|
||||
this.resetClearTimerList()
|
||||
this.searchStr = ''
|
||||
if (this.animation) {
|
||||
setTimeout(() => {
|
||||
this.showPopup = false
|
||||
}, 200)
|
||||
} else {
|
||||
this.showPopup = false
|
||||
}
|
||||
}
|
||||
this.$emit('change', data)
|
||||
},
|
||||
// 点击遮罩
|
||||
maskClick() {
|
||||
this.$emit('maskClick')
|
||||
},
|
||||
// 初始化数据
|
||||
initData(arr, parentVisible = undefined, pathArr = []) {
|
||||
if (!Array.isArray(arr)) return []
|
||||
const res = []
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const curPathArr = [...pathArr, arr[i][this.dataLabel]]
|
||||
const obj = {
|
||||
...arr[i],
|
||||
[this.dataLabel]: arr[i][this.dataLabel],
|
||||
[this.dataValue]: arr[i][this.dataValue],
|
||||
path: curPathArr.join(this.pathHyphen)
|
||||
}
|
||||
|
||||
obj.checked = this.selectList.includes(arr[i][this.dataValue])
|
||||
|
||||
obj.disabled = false
|
||||
if (
|
||||
Boolean(arr[i].disabled) ||
|
||||
this.disabledList.includes(obj[this.dataValue])
|
||||
) {
|
||||
obj.disabled = true
|
||||
}
|
||||
|
||||
//半选
|
||||
obj.partChecked = Boolean(
|
||||
arr[i].partChecked === undefined ? false : arr[i].partChecked
|
||||
)
|
||||
obj.partChecked && obj.partCheckedSet.add(obj[this.dataValue])
|
||||
!obj.partChecked && (this.isSelectedAll = false)
|
||||
|
||||
const parentVisibleState =
|
||||
parentVisible === undefined ? true : parentVisible
|
||||
const curVisibleState =
|
||||
arr[i].visible === undefined ? true : Boolean(arr[i].visible)
|
||||
if (parentVisibleState === curVisibleState) {
|
||||
obj.visible = parentVisibleState
|
||||
} else if (!parentVisibleState || !curVisibleState) {
|
||||
obj.visible = false
|
||||
} else {
|
||||
obj.visible = true
|
||||
}
|
||||
|
||||
obj.showChildren =
|
||||
'showChildren' in arr[i] && arr[i].showChildren != undefined
|
||||
? arr[i].showChildren
|
||||
: this.showChildren
|
||||
|
||||
if (arr[i][this.dataChildren]?.length) {
|
||||
const childrenVal = this.initData(
|
||||
arr[i][this.dataChildren],
|
||||
obj.visible,
|
||||
curPathArr
|
||||
)
|
||||
obj[this.dataChildren] = childrenVal
|
||||
if (
|
||||
!obj.checked &&
|
||||
childrenVal.some((item) => item.checked || item.partChecked)
|
||||
) {
|
||||
obj.partChecked = true
|
||||
partCheckedSet.add(obj[this.dataValue])
|
||||
}
|
||||
}
|
||||
|
||||
res.push(obj)
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
// 获取某个节点后面所有元素
|
||||
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 true
|
||||
}
|
||||
|
||||
if (arr[i][this.dataChildren]?.length) {
|
||||
const childRes = this.getParentNode(target, arr[i][this.dataChildren])
|
||||
if (typeof childRes === 'boolean' && childRes) {
|
||||
res = [arr[i]]
|
||||
} else if (Array.isArray(childRes) && childRes.length) {
|
||||
res = [...childRes, arr[i]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
// 点击checkbox
|
||||
handleNodeClick(data, status) {
|
||||
const node = this.getReflectNode(data, this.treeData)
|
||||
node.checked = typeof status === 'boolean' ? status : !node.checked
|
||||
node.partChecked = false
|
||||
partCheckedSet.delete(node[this.dataValue])
|
||||
// 如果是单选不考虑其他情况
|
||||
if (!this.mutiple) {
|
||||
let emitData = []
|
||||
if (node.checked) {
|
||||
emitData = [node[this.dataValue]]
|
||||
}
|
||||
this.$emit('input', emitData)
|
||||
} else {
|
||||
// 多选情况
|
||||
if (!this.linkage) {
|
||||
// 不需要联动
|
||||
let emitData = null
|
||||
if (node.checked) {
|
||||
emitData = Array.from(
|
||||
new Set([...this.selectList, node[this.dataValue]])
|
||||
)
|
||||
} else {
|
||||
emitData = this.selectList.filter(
|
||||
(id) => id !== node[this.dataValue]
|
||||
)
|
||||
}
|
||||
this.$emit('input', emitData)
|
||||
} else {
|
||||
// 需要联动
|
||||
let emitData = [...this.selectList]
|
||||
const parentNodes = this.getParentNode(node, this.treeData)
|
||||
const childrenVal = this.getChildren(node).filter(
|
||||
(item) => !item.disabled
|
||||
)
|
||||
if (node.checked) {
|
||||
// 选中
|
||||
emitData = Array.from(new Set([...emitData, node[this.dataValue]]))
|
||||
if (childrenVal.length) {
|
||||
emitData = Array.from(
|
||||
new Set([
|
||||
...emitData,
|
||||
...childrenVal.map((item) => item[this.dataValue])
|
||||
])
|
||||
)
|
||||
// 孩子节点全部选中并且清除半选状态
|
||||
childrenVal.forEach((childNode) => {
|
||||
childNode.partChecked = false
|
||||
partCheckedSet.delete(childNode[this.dataValue])
|
||||
})
|
||||
}
|
||||
if (parentNodes.length) {
|
||||
let flag = false
|
||||
// 有父元素 如果父元素下所有子元素全部选中,选中父元素
|
||||
while (parentNodes.length) {
|
||||
const item = parentNodes.shift()
|
||||
if (!item.disabled) {
|
||||
if (flag) {
|
||||
// 前一个没选中并且为半选那么之后的全为半选
|
||||
item.partChecked = true
|
||||
partCheckedSet.add(item[this.dataValue])
|
||||
} else {
|
||||
const allChecked = item[this.dataChildren]
|
||||
.filter((node) => node.visible && !node.disabled)
|
||||
.every((node) => node.checked)
|
||||
if (allChecked) {
|
||||
item.checked = true
|
||||
item.partChecked = false
|
||||
partCheckedSet.delete(item[this.dataValue])
|
||||
emitData = Array.from(
|
||||
new Set([...emitData, item[this.dataValue]])
|
||||
)
|
||||
} else {
|
||||
item.partChecked = true
|
||||
partCheckedSet.add(item[this.dataValue])
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 取消选中
|
||||
emitData = emitData.filter((id) => id !== node[this.dataValue])
|
||||
if (childrenVal.length) {
|
||||
// 取消选中全部子节点
|
||||
childrenVal.forEach((childNode) => {
|
||||
emitData = emitData.filter(
|
||||
(id) => id !== childNode[this.dataValue]
|
||||
)
|
||||
})
|
||||
}
|
||||
if (parentNodes.length) {
|
||||
parentNodes.forEach((parentNode) => {
|
||||
if (emitData.includes(parentNode[this.dataValue])) {
|
||||
parentNode.checked = false
|
||||
}
|
||||
emitData = emitData.filter(
|
||||
(id) => id !== parentNode[this.dataValue]
|
||||
)
|
||||
const hasChecked = parentNode[this.dataChildren]
|
||||
.filter((node) => node.visible && !node.disabled)
|
||||
.some((node) => node.checked || node.partChecked)
|
||||
|
||||
parentNode.partChecked = hasChecked
|
||||
if (hasChecked) {
|
||||
partCheckedSet.add(parentNode[this.dataValue])
|
||||
} else {
|
||||
partCheckedSet.delete(parentNode[this.dataValue])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this.$emit('input', emitData)
|
||||
}
|
||||
}
|
||||
},
|
||||
// 点击名称折叠或展开
|
||||
handleHideChildren(node) {
|
||||
const status = !node.showChildren
|
||||
this.getReflectNode(node, this.treeData).showChildren = status
|
||||
this.getReflectNode(node, this.filterTreeData).showChildren = status
|
||||
},
|
||||
// 根据 dataValue 找节点
|
||||
changeStatus(list, ids, needEmit = false) {
|
||||
const arr = [...list]
|
||||
let flag = true
|
||||
needEmit && (this.selectedListBaseinfo = [])
|
||||
|
||||
while (arr.length) {
|
||||
const item = arr.shift()
|
||||
|
||||
if (ids.includes(item[this.dataValue])) {
|
||||
this.$set(item, 'checked', true)
|
||||
needEmit && this.selectedListBaseinfo.push(item)
|
||||
// 数据被选中清除半选状态
|
||||
item.partChecked = false
|
||||
partCheckedSet.delete(item[this.dataValue])
|
||||
} else {
|
||||
this.$set(item, 'checked', false)
|
||||
if (item.visible && !item.disabled) {
|
||||
flag = false
|
||||
}
|
||||
if (partCheckedSet.has(item[this.dataValue])) {
|
||||
this.$set(item, 'partChecked', true)
|
||||
} else {
|
||||
this.$set(item, 'partChecked', false)
|
||||
}
|
||||
}
|
||||
|
||||
if (item[this.dataChildren]?.length) {
|
||||
arr.push(...item[this.dataChildren])
|
||||
}
|
||||
}
|
||||
this.isSelectedAll = flag
|
||||
needEmit && this.$emit('selectChange', [...this.selectedListBaseinfo])
|
||||
},
|
||||
// 移除选项
|
||||
removeSelectedItem(node) {
|
||||
this.isSelectedAll = false
|
||||
if (this.linkage) {
|
||||
this.handleNodeClick(node, false)
|
||||
this.$emit('removeSelect', node)
|
||||
} else {
|
||||
const emitData = this.selectList.filter(
|
||||
(item) => item !== node[this.dataValue]
|
||||
)
|
||||
this.$emit('removeSelect', node)
|
||||
this.$emit('input', emitData)
|
||||
}
|
||||
},
|
||||
// 全部选中
|
||||
handleSelectAll() {
|
||||
this.isSelectedAll = !this.isSelectedAll
|
||||
if (this.isSelectedAll) {
|
||||
if (!this.mutiple) {
|
||||
uni.showToast({
|
||||
title: '单选模式下不能全选',
|
||||
icon: 'none',
|
||||
duration: 1000
|
||||
})
|
||||
return
|
||||
}
|
||||
let emitData = []
|
||||
this.treeData.forEach((item) => {
|
||||
if (item.visible || (item.disabled && item.checked)) {
|
||||
emitData = Array.from(new Set([...emitData, item[this.dataValue]]))
|
||||
if (item[this.dataChildren]?.length) {
|
||||
emitData = Array.from(
|
||||
new Set([
|
||||
...emitData,
|
||||
...this.getChildren(item)
|
||||
.filter(
|
||||
(item) =>
|
||||
!item.disabled || (item.disabled && item.checked)
|
||||
)
|
||||
.map((item) => item[this.dataValue])
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.$emit('input', emitData)
|
||||
} else {
|
||||
this.clear()
|
||||
}
|
||||
},
|
||||
// 清空选项
|
||||
clear() {
|
||||
if (this.disabled) return
|
||||
const emitData = []
|
||||
partCheckedSet.clear()
|
||||
this.selectedListBaseinfo.forEach((node) => {
|
||||
if (node.visible && node.checked && node.disabled) {
|
||||
emitData.push(node[this.dataValue])
|
||||
}
|
||||
})
|
||||
this.$emit('input', emitData)
|
||||
},
|
||||
// 异步加载节点
|
||||
handleLoadNode({ source, target }) {
|
||||
// #ifdef MP-WEIXIN
|
||||
const node = this.getReflectNode(source, this.treeData)
|
||||
this.$set(
|
||||
node,
|
||||
this.dataChildren,
|
||||
this.initData(
|
||||
target,
|
||||
source.visible,
|
||||
source.path.split(this.pathHyphen)
|
||||
)
|
||||
)
|
||||
this.$nextTick(() => {
|
||||
this.handleHideChildren(node)
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
this.$set(
|
||||
source,
|
||||
this.dataChildren,
|
||||
this.initData(
|
||||
target,
|
||||
source.visible,
|
||||
source.path.split(this.pathHyphen)
|
||||
)
|
||||
)
|
||||
this.handleHideChildren(source)
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$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;
|
||||
|
||||
.custom-tree-select-content {
|
||||
.select-list {
|
||||
padding-left: $row-base;
|
||||
min-height: 35px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: $radius-sm;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&.active {
|
||||
padding: calc(#{$col-sm} / 2) 0 calc(#{$col-sm} / 2) $row-base;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
|
||||
.select-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select-item {
|
||||
margin: $col-sm $row-base $col-sm 0;
|
||||
padding: $col-sm $row-sm;
|
||||
max-width: auto;
|
||||
height: auto;
|
||||
background-color: #eaeaea;
|
||||
border-radius: $radius-sm;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
padding-right: $row-base;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.close {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-right: $row-sm;
|
||||
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: $col-base 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: $col-base $row-base 0;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-btn {
|
||||
margin-left: $row-base;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-content {
|
||||
margin: $col-base $row-base;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-view-box {
|
||||
touch-action: none;
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sentry {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
width: auto;
|
||||
color: #C0C4CB !important;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.no-data.center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,363 @@
|
||||
<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>
|
||||
@@ -0,0 +1,17 @@
|
||||
export function isString(data) {
|
||||
return typeof data === 'string'
|
||||
}
|
||||
|
||||
// 分页
|
||||
export function paging(data, PAGENUM = 50) {
|
||||
if (!Array.isArray(data) || !data.length) return data
|
||||
const pages = []
|
||||
data.forEach((item, index) => {
|
||||
const i = Math.floor(index / PAGENUM)
|
||||
if (!pages[i]) {
|
||||
pages[i] = []
|
||||
}
|
||||
pages[i].push(item)
|
||||
})
|
||||
return pages
|
||||
}
|
||||
83
uni_modules/custom-tree-select/package.json
Normal file
83
uni_modules/custom-tree-select/package.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"id": "custom-tree-select",
|
||||
"displayName": "custom-tree-select树形选择器支持v-model",
|
||||
"version": "4.1.0",
|
||||
"description": "树形选择器基于uni-ui,v-model绑定数据,父级可选、数据回显、移除选项",
|
||||
"keywords": [
|
||||
"custom",
|
||||
"tree-select",
|
||||
"选择器",
|
||||
"树形选择器"
|
||||
],
|
||||
"repository": "https://github.com/qzlthxp/custom-tree-select",
|
||||
"engines": {
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "u"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "u",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
228
uni_modules/custom-tree-select/readme.md
Normal file
228
uni_modules/custom-tree-select/readme.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# custom-tree-select 使用指南
|
||||
|
||||
找工作中…,大佬们有内推加群联系我
|
||||
|
||||
**提示:** 使用该插件前确保你已经导入 `uni-popup` `uni-icons` `uni-easyinput` 插件。
|
||||
|
||||
当前插件主要作为表单中选择器来使用,如果只是作为弹窗会出现数据状态无法重置,此时推荐使用 [`custom-tree-popup`](https://ext.dcloud.net.cn/plugin?name=custom-tree-popup) 组件。
|
||||
|
||||
**如果在微信小程序中使用,在 `main.js` 文件中添加以下代码,2023/05/04 之前安装插件或者本地插件版本 `<=3.7.1` 需要添加**
|
||||
|
||||
```js
|
||||
// #ifdef MP-WEIXIN
|
||||
Vue.prototype.$bus = new Vue()
|
||||
// #endif
|
||||
```
|
||||
|
||||
**有问题可以加 QQ 群:297080738**
|
||||
|
||||
## 优势
|
||||
|
||||
💪:基于 `uni-popup`、 `uni-icons`、 `uni-easyinput` 插件进行开发,默认样式与 `uni-easyinput` 样式对标。
|
||||
|
||||
⚡:全面支持懒加载应对大量数据。
|
||||
|
||||
🚀:v-model 绑定数据、数据回显、移除选项。
|
||||
|
||||
⚙ :提供更多配置项。
|
||||
|
||||
📦:开箱即用。
|
||||
|
||||
## Props
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 | 版本要求 |
|
||||
| :-------------------: | :---------: | :-----------------------------: | :----------------------------------------------------------: | --------- |
|
||||
| canSelectAll | Boolean | false | 开启一键全选功能 | |
|
||||
| contentHeight | String | '500px' \| 视窗高度的 75%的像素 | 弹出内选择器容器高度,为解决搜索时搜索框被顶出屏幕,监听输入框 focus 事件 动态修改弹窗内选择器容器高度 | |
|
||||
| clearResetSearch | Boolean | false | 设置为 `true` 并且搜索之后,点击输入框清除按钮,会清空搜索内容并且会直接重置整个弹窗内树形选择器内容,默认情况下只有清除之后再次进行查询才会重置选择器 | |
|
||||
| animation | Boolean | ture | 是否开启弹窗动画 | |
|
||||
| is-mask-click | Boolean | true | 点击遮罩关闭弹窗 | |
|
||||
| mask-background-color | String | rgba(0,0,0,0.4) | 蒙版颜色,建议使用 rgba 颜色值 | |
|
||||
| background-color | String | none | 主窗口背景色 | |
|
||||
| safe-area | Boolean | true | 是否适配底部安全区 | |
|
||||
| **choseParent** | **Boolean** | **true** | **父节点是否可选** | |
|
||||
| **linkage** | **Boolean** | **false** | **父子节点是否联动** | |
|
||||
| placeholder | String | 请选择 | 空状态信息提示、弹窗标题 | |
|
||||
| confirmText | String | 完成 | 确定按钮文字 | |
|
||||
| confirmTextColor | String | #007aff | 确定按钮文字颜色 | |
|
||||
| listData | Array | - | 展示的数据 | |
|
||||
| **dataLabel** | **String** | **name** | **listData 中对应数据的 label** | |
|
||||
| **dataValue** | **String** | **id** | **listData 中对应数据的 value** | |
|
||||
| **dataChildren** | **String** | **children** | **listData 中对应数据的 children** | |
|
||||
| clearable | Boolean | false | 是否显示清除按钮,点击清除所有已选项 | |
|
||||
| **mutiple** | **Boolean** | **false** | **是否可以多选** | |
|
||||
| **disabled** | **Boolean** | **false** | **是否允许修改** | |
|
||||
| disabledList | Array | [] | 设置某些选项为不可选,数组元素为数据 dataValue 对应的值,也可以操作数据 disabled 属性实现 | |
|
||||
| search | Boolean | false | 是否可以搜索(常用于数据较多的情况) | |
|
||||
| showChildren | Boolean | false | 默认展开(数据内部 showChildren 属性优先级更高,可以设置全局收起,单独展开某一条数据) | |
|
||||
| border | Boolean | false | 显示引导线 | |
|
||||
| pathMode | Boolean | false | 路径模式,开启后选择框内展示选项所在层级的完整信息如:`城市/街道/小区` | >= 3.8.2 |
|
||||
| pathHyphen | String | / | 路径模式下连字符 | >= 3.8.2 |
|
||||
| lazyLoadChildren | Boolean | false | 是否允许动态加载获取子节点 | >= 3.8.4 |
|
||||
| load | Function | (node) => Promise<void> | 动态加载函数,具体用法见下方示例 | >= 3.8.4 |
|
||||
| **v-model/value** | **Array** | **[ ]** | **已选择的值,通过 v-model 进行绑定,例如:v-model="formData.selectedList"** (根据你绑定数据的类型自动返回相同类型的数据,String 类型通过 `,` 进行分隔。>=4.0.0 版本不在支持字符串类型传参,修改为数组类型) | >= 4.0.0 |
|
||||
|
||||
## listdata 特有属性
|
||||
|
||||
| 名称 | 类型 | 默认值 | 说明 |
|
||||
| ------------ | ------- | ------ | ------------------ |
|
||||
| disabled | Boolean | false | 选项是否可选 |
|
||||
| visible | Boolean | true | 选项是否展示 |
|
||||
| showChildren | Boolean | true | 选项是否展示子节点 |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名称 | 说明 | 返回值 |
|
||||
| ------------ | ------------------------ | -------------------------------------- |
|
||||
| change | 弹窗组件状态发生变化触发 | e={show: true | false, type:当前模式} |
|
||||
| maskClick | 点击遮罩层触发 | |
|
||||
| input | 选中数据或取消选中时触发 | 以数组形式返回已选择数据 |
|
||||
| selectChange | 选中数据或取消选中时触发 | 以数组形式返回已选择数据完整信息 |
|
||||
| removeSelect | 选择框移除选项时触发 | 返回对应数据的完整信息 |
|
||||
|
||||
## 基础使用示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!--/pages/index/index-->
|
||||
<custom-tree-select :listData="listData" v-model="formData.selectedArr" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
selectedArr: [],
|
||||
},
|
||||
listData: [
|
||||
{
|
||||
id: 1,
|
||||
name: '城市1',
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
name: '街道1',
|
||||
children: [
|
||||
{
|
||||
id: 4,
|
||||
name: '小区1'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '城市2',
|
||||
children: [
|
||||
{
|
||||
id: 6,
|
||||
name: '街道2'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 禁用某些选项,或隐藏某些选项
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!--/pages/index/index-->
|
||||
<custom-tree-select
|
||||
mutiple
|
||||
linkage
|
||||
clearable
|
||||
search
|
||||
dataLabel="text"
|
||||
dataValue="value"
|
||||
:listData="listData"
|
||||
:disabledList="[6]"
|
||||
v-model="formData.selected"
|
||||
></custom-tree-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
selected: []
|
||||
},
|
||||
listData: [
|
||||
{
|
||||
value: 1,
|
||||
text: '城市1',
|
||||
children: [
|
||||
{
|
||||
value: 3,
|
||||
text: '街道1',
|
||||
disabled: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: '城市2',
|
||||
children: [
|
||||
{
|
||||
value: 6,
|
||||
text: '街道2'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 7,
|
||||
text: '城市3',
|
||||
visible: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 动态加载节点
|
||||
|
||||
```vue
|
||||
<custom-tree-select
|
||||
:listData="listData"
|
||||
:load="load"
|
||||
lazyLoadChildren
|
||||
pathMode
|
||||
v-model="selectedArr"
|
||||
/>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedArr: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load(node) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (node) {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{
|
||||
value: '128047129041',
|
||||
text: '测试异步加载'
|
||||
}
|
||||
])
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user