如题,uni-app应增加一个类似element plus Cascader 一样的级联选择组件,这样后端采用element-ui的项目移动端迁移会比较方便,当然其他的组件也需进一步丰富,但cascader有着特殊的数据结构,也较为复杂,对于普通开发者有着比较大的挑战,希望官方能够考虑,谢谢。
下面的代码是基于插件商城ba-tree-picker修改的一个简单替代,不过问题也很多,同时要求treenode id必须唯一,也不支持标签查询,对于大数据量的场景体验也很差,但可以作为参考 ` <!-- 树形层级选择器--> <!-- 1、支持单选、多选 --> <template> <view> <view> <uni-easyinput :clearable="true" v-model="text" @clear="_clearTreeList" placeholder="title" prefix-icon="search" @iconClick="_click" :disabled="readonly"></uni-easyinput> </view><uni-popup type="bottom" ref="xTreepop" :is-mask-click="false">
<!-- <view class="tree-cover" :class="{'show':showDialog}" @tap="_cancel"></view>
<view class="tree-dialog" :class="{'show':showDialog}"> -->
<view class="tree_pop">
<!-- <view class="tree-bar">
<view class="tree-bar-title" :style="{'color':titleColor}">{{title}}</view>
<view class="tree-bar-cancel" :style="{'color':cancelColor}" hover-class="hover-c" @tap="_cancel">取消
</view>
<view class="tree-bar-confirm" :style="{'color':confirmColor}" hover-class="hover-c" @tap="_confirm">
{{multiple?'确定':''}}
</view>
</view> -->
<view>
<uni-section :title="title" titleFontSize="11pt" :color="titleColor" type="line">
<template v-slot:right>
<uni-tag text="确定" :type="readonly?'default':'success'" :circle="true" @tap="_confirm" v-show="multiple" class="uni-pr-4"></uni-tag>
<uni-tag text="关闭" type="warning" :circle="true" @tap="_cancel" class="uni-mr-3"></uni-tag>
</template>
</uni-section>
</view>
<view class="tree-view">
<scroll-view class="tree-list" :scroll-y="true">
<block v-for="(item, index) in treeList" :key="index">
<view class="tree-item" :style="[{
paddingLeft: item.level*30 + 'rpx'
}]" :class="{
itemBorder: border === true,
show: item.isShow
}">
<view class="item-label">
<view class="item-icon uni-inline-item" @tap.stop="_onItemSwitch(item, index)">
<view v-if="!item.isLastLevel&&item.isShowChild" class="switch-on" style="{'border-left-color':switchColor}"> </view>
<view v-else-if="!item.isLastLevel&&!item.isShowChild" class="switch-off" style="{'border-top-color':switchColor}"> </view>
<view v-else class="item-last-dot" :style="{'border-top-color':switchColor}">
</view>
</view>
<view class="uni-flex-item uni-inline-item" @tap.stop="_onItemSelect(item, index)">
<view class="item-name"> {{item.name+(item.childCount?"("+item.childCount+")":'')}}
</view>
<view class="item-check" v-if="selectParent?true:(item.isLastLevel)">
<view class="item-check-yes" v-if="item.checkStatus==1" class="{'radio':!multiple}" :style="{'border-color':confirmColor}"> <view class="item-check-yes-part" style="{'background-color':confirmColor}"> </view>
</view>
<view class="item-check-yes" v-else-if="item.checkStatus==2" class="{'radio':!multiple}" :style="{'border-color':confirmColor}"> <view class="item-check-yes-all" :style="{'background-color':confirmColor}">
</view>
</view>
<view class="item-check-no" v-else-if="item.checkStatus==0" :class="{'radio':!multiple}" style="{'border-color':confirmColor}"></view> <view class="item-check-disabled" v-else :class="{'radio':!multiple}"
style="{'border-color':confirmColor}"></view>
</view>
</view>
</view>
</view>
</block>
</scroll-view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import _ from "lodash"
export default {
emits: ['update:modelValue','change','clear'],
name: "ba-tree-picker",
props: {
readonly:{
type:Boolean,
default:false
},
modelValue:{
type: Array,
default: function() {
return []
}
},
valueKey: {
type: String,
default: 'id'
},
textKey: {
type: String,
default: 'name'
},
disabledKey: {
type: String,
default: 'disabled'
},
disabled: {
type: Boolean,
default: false
},
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: '' // #33c313
},
cancelColor: { // 取消按钮颜色
type: String,
default: '' // #344a99
},
titleColor: { // 标题颜色
type: String,
default: '#F4F6FF' //
},
switchColor: { // 节点切换图标颜色
type: String,
default: '' // #666
},
border: { // 是否有分割线
type: Boolean,
default: false
},
},
data() {
return {
text:"",
showDialog: false,
treeList: [],
datacopy: [] //当前treeList副本
}
},
computed: {},
methods: {
_click() {
this._show()
},
_show() {
//this.showDialog = true
if(this.multiple&&!this.readonly){
this.datacopy=_.cloneDeep(this.treeList)
}
this.$refs.xTreepop.open()
//console.log("tree",'open')
},
_hide() {
//this.showDialog = false
this.$refs.xTreepop.close()
},
_cancel() {
if(this.multiple&&!this.readonly){
this.treeList=this.datacopy
}
this._hide()
//this.$emit("cancel", '');
},
// _isNull(item){
// if(item==null)||item==undefined){
// return true
// }
// return false
// },
_getSelectedValues(curnode,items) {
let values=[]
let name=[]
values.push(curnode.id)
name.push(curnode.name)
let idx=items.findIndex(item => item.id == curnode.parentId)
while(idx!=-1&&curnode.parentId!=0){
values.push(items[idx].id)
name.push(items[idx].name)
idx=items.findIndex(item => item.id == items[idx].parentId)
}
//console.log("curnode",curnode)
name=name.reverse()
let strName=name.join("/")
return {keys:values.reverse(),name:strName}
},
_confirm() { //多选
if(this.readonly){
return
}
let selectedList = []; //如果子集全部选中,只返回父级 id
let selectedValues = [];
let selectedLabel =[];
let selectedNames;
let currentLevel = -1;
this.treeList.forEach((item, index) => {
//if (currentLevel >= 0 && item.level > currentLevel) {
//
//} else {
if (item.checkStatus === 2&&(this.selectParent?true:item.isLastLevel)) {
currentLevel = item.level;
selectedList.push(item.id);
selectedNames = selectedNames ? selectedNames + ' / ' + item.name : item.name;
let obj=this._getSelectedValues(item,this.treeList)
selectedValues.push(obj.keys)
selectedLabel.push(obj.name)
} else {
currentLevel = -1;
}
//}
})
//console.log('_confirm', selectedList);
this.text=selectedLabel.join(" ");
this._hide();
//this.$emit("select-change", selectedList, selectedNames);
this.$emit("update:modelValue", selectedValues);
this.$emit("change", selectedValues);
},
//格式化原数据(原数据为tree结构)
_formatTreeData(list = [], level = 0, parentItem, isShowChild = true) {
let nextIndex = 0;
let parentId = -1;
let initCheckStatus = 0;
let str="";
if (parentItem) {
nextIndex = this.treeList.findIndex(item => item.id === parentItem.id) + 1;
parentId = parentItem.id;
if (!this.multiple) { //单选
initCheckStatus = 0;
} else
initCheckStatus = parentItem.checkStatus == 2 ? 2 : 0;
}
list.forEach(item => {
//console.log(item[this.textKey],item && item[this.childrenKey])
let isLastLevel = true;
//数据源无disabled属性处理
if(item[this.disabledKey]==undefined||item[this.disabledKey]==null){
item[this.disabledKey]=true
}
if (item && item[this.childrenKey]!=null && item[this.childrenKey]!=undefined) {
let children = item[this.childrenKey];
if (Array.isArray(children) && children.length > 0) {
isLastLevel = false;
}
}
let itemT = {
id: item[this.valueKey],
name: item[this.textKey],
//disabled:可直接修改为item[this.disabledKey],此处为我项目数据结构原因
disabled:!item[this.disabledKey]&&isLastLevel?true:false,
level,
isLastLevel,
isShow: isShowChild,
isShowChild: false,
checkStatus: !item[this.disabledKey]&&isLastLevel?-1:initCheckStatus,
orCheckStatus: !item[this.disabledKey]&&isLastLevel?-1:0,
parentId,
children: item[this.childrenKey],
childCount: item[this.childrenKey]!=null ? item[this.childrenKey].length : 0,
childCheckCount: 0,
childCheckPCount: 0
};
this.treeList.splice(nextIndex, 0, itemT);
nextIndex++;
//this.treeList.push(itemT)
// if(!itemT.isLatstLevel&& itemT.children!=null&&itemT.children!=undefined){
// this._formatTreeData(item[this.childrenKey], itemT.level + 1, itemT,false);
// }
})
},
_expandSelectedItems() {
//console.log("modeValue:",this.modelValue)
if(!Array.isArray(this.modelValue)){
return
}
let tmp=[]
if(this.multiple){
tmp=this.modelValue
}
else{
//单选转为二维数组
tmp.push(this.modelValue)
}
tmp.forEach(v=>{
v.forEach(x=>{
let idx1=this.treeList.findIndex(node => node.id == x)
if(idx1!==-1&&this.treeList[idx1].children){
//this._formatTreeData(this.treeList[idx1].children, this.treeList[idx1].level + 1, this.treeList[idx1]);
this._onItemSwitch(this.treeList[idx1],idx1)
}
})
})
},
_setDisplayInfo() {
if(!Array.isArray(this.modelValue)){
return
}
let names = []
let tmp=[]
if(this.multiple){
tmp=this.modelValue
}
else{
//单选转为二维数组
tmp.push(this.modelValue)
}
tmp.forEach(v=>{
try{
let idx=v.length - 1
let idx1=this.treeList.findIndex(node => node.id == v[idx])
if(idx1!==-1){
this.treeList[idx1].checkStatus = 2;
this.treeList[idx1].orCheckStatus = 2;
this.treeList[idx1].childCheckCount = !this.treeList[idx1].isLastLevel ? this.treeList[idx1].children.length : 0;
this._onItemParentSelect(this.treeList[idx1], idx1);
names.push(this._getSelectedValues(this.treeList[idx1],this.treeList).name)
}
}
catch(err){
}
})
//设置显示文本
this.text=names.join(" ");
//console.log("treeList",this.treeList);
},
// 节点打开、关闭切换
_onItemSwitch(item, index) {
if (item.isLastLevel === true) {
return;
}
item.isShowChild = !item.isShowChild;
if (item.children) {
this._formatTreeData(item.children, item.level + 1, item);
item.children = undefined;
} else {
this._onItemChildSwitch(item, index);
}
//console.log("treeList",this.treeList);
//this._onItemChildSwitch(item, index);
},
_onItemChildSwitch(item, index) {
//console.log('_onItemChildSwitch')
const firstChildIndex = index + 1;
if (firstChildIndex > 0)
for (var i = firstChildIndex; i < this.treeList.length; i++) {
let itemChild = this.treeList[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;
}
}
},
// 节点选中、取消选中
_onItemSelect(item, index) {
//console.log('_onItemSelect',item)
if(this.readonly) return
if(item.disabled) return
if (!this.multiple) { //单选
item.checkStatus = item.checkStatus == 0 ? 2 : 0;
////判断是否是否允许选择父节点
if(!this.selectParent){
if(!item.isLastLevel&&!item.isShowChild){
this._onItemSwitch(item,index)
return
}
}
this.treeList.forEach((v, i) => {
if (i != index) {
this.treeList[i].checkStatus = 0
} else {
this.treeList[i].checkStatus = 2
}
})
let selectedList = [];
let selectedNames;
let obj=this._getSelectedValues(item,this.treeList)
//selectedList.push(obj.keys);
selectedList=obj.keys
selectedNames = obj.name;
this.text=selectedNames
this._hide()
//this.$emit("select-change", selectedList, selectedNames);
this.$emit("update:modelValue", selectedList);
this.$emit("change", selectedList);
//console.log("node",selectedList)
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;
}
if(!item.isLastLevel&&!item.isShowChild){
this._onItemSwitch(item,index)
}
//子节点 全部选中
this._onItemChildSelect(item, index);
//父节点 选中状态变化
this._onItemParentSelect(item, index, oldCheckStatus);
},
_onItemChildSelect(item, index) {
//console.log('_onItemChildSelect')
let allChildCount = 0;
if (item.childCount && item.childCount > 0) {
index++;
while (index < this.treeList.length && this.treeList[index].level > item.level) {
let itemChild = this.treeList[index];
if(itemChild.disabled){
index++;
continue;
}
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;
}
// console.log('>>>>index:', index, 'item:', itemChild.name, ' status:', itemChild
// .checkStatus)
index++;
}
}
},
_onItemParentSelect(item, index, oldCheckStatus) {
//console.log('_onItemParentSelect')
//console.log(item)
const parentIndex = this.treeList.findIndex(itemP => itemP.id == item.parentId);
//console.log('parentIndex:' + parentIndex)
if (parentIndex >= 0) {
let itemParent = this.treeList[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;
}
if(itemParent.disabled) itemParent.checkStatus = -1;
//console.log('itemParent:', itemParent)
this._onItemParentSelect(itemParent, parentIndex, oldCheckStatusParent);
}
},
// 重置数据
_reTreeList() {
this.treeList.forEach((v, i) => {
this.treeList[i].checkStatus = v.orCheckStatus
})
},
_clearTreeList() {
this.treeList.forEach((v, i) => {
if(!this.treeList[i].disabled)
this.treeList[i].checkStatus = 0
})
this.$emit("clear", []);
},
_initTree() {
this.treeList = [];
if(this.localdata.length>0){
this._formatTreeData(this.localdata);
this._expandSelectedItems()
this._setDisplayInfo()
}
else{
this._clearTreeList()
}
},
},
watch: {
localdata() {
this._initTree();
},
localTreeList() {
this.treeList = this.localTreeList;
},
modelValue() {
//this._initTree();
this._expandSelectedItems()
this._setDisplayInfo()
}
},
mounted() {
//console.log("localdata",this.localdata)
//console.log("modelValue",this.modelValue)
this._initTree();
//console.log("treeList",this.treeList);
//console.log("datacopy",this.datacopy);
}
}
</script>
<style scoped>
.tree-cover {
position: fixed;
top: 0rpx;
right: 0rpx;
bottom: 0rpx;
left: 0rpx;
z-index: 100;
background-color: rgba(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: 0rpx;
right: 0rpx;
bottom: 0rpx;
left: 0rpx;
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_pop{
width: 100%;
height:calc(100vh * 0.8);
background-color: white;
overflow: auto;
padding-left: 5px;
padding-right: 5px;
padding-bottom: 5px;
/ align-items: center;
justify-content: center;
align-content: center; /
position: fixed;
bottom:0;
/ #ifndef APP-NVUE /
display: flex;
/ #endif /
flex-direction: column;
z-index: 300;
}
.tree-bar {
/* background-color: #fff; */
height: 90rpx;
padding-left: 25rpx;
padding-right: 25rpx;
display: flex;
align-items: center;
box-sizing: border-box;
border-bottom-width: 1rpx !important;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
font-size: 32rpx;
color: #757575;
background: #F4F6FF;
line-height: 1;
width: 100%
}
.tree-bar-confirm {
color: #0055ff;
padding-left: 25rpx;
padding-right:25rpx;
padding-top:15rpx;
padding-bottom:15rpx;
}
.tree-bar-title {}
.tree-bar-cancel {
color: dargrey;
padding: 15rpx;
margin-left: auto;
}
.tree-view {
flex: 1;
padding: 20rpx;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
overflow: hidden;
height: 100%;
}
.tree-list {
flex: 1;
height: 100%;
overflow: hidden;
}
.tree-item {
display: flex;
justify-content: space-between;
align-items: center;
line-height: 1;
height: 0;
opacity: 0;
transition: 0.2s;
overflow: hidden;
}
.tree-item.show {
height: 90rpx;
opacity: 1;
}
.tree-item.showchild:before {
transform: rotate(90deg);
}
.tree-item.last:before {
opacity: 0;
}
.switch-on {
width: 0;
height: 0;
border-left: 16rpx solid transparent;
border-right: 16rpx solid transparent;
border-top: 24rpx solid #666;
}
.switch-off {
width: 0;
height: 0;
border-bottom: 16rpx solid transparent;
border-top: 16rpx solid transparent;
border-left: 24rpx solid #666;
}
.item-last-dot {
position: absolute;
width: 16rpx;
height: 16rpx;
border-radius: 100%;
background: #666;
}
.item-icon {
width: 26rpx;
height: 26rpx;
margin-right: 8rpx;
padding-right: 20rpx;
padding-left: 20rpx;
}
.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: 450rpx;
}
.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: 1rpx;
border-left-width: 1rpx;
border-bottom-width: 1rpx;
border-right-width: 1rpx;
border-style: solid;
border-color: #0055ff;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.item-check-disabled {
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: 1rpx;
border-left-width: 1rpx;
border-bottom-width: 1rpx;
border-right-width: 1rpx;
border-style: solid;
border-color: darkgray;
background-color: lightgray;
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;
}
.uni-flex-item{ display: flex; align-items: center; width: 100%; }
</style>
`