vue2+vant2 实现带搜索的级联选择组件
级联选择组件
新建组件-子项
- CasadeSelect/CasadeSelectItem.vue
<template>
<div class="container-list-item">
<div class="icon">
<div class="checkbox">
<svg-icon v-if="curChecked" icon-class="radio" @click.stop="clickBox(false, item)" />
<van-icon v-else name="circle" @click.stop="clickBox(true, item)" />
</div>
</div>
<div class="info">
<div class="word text-cut">{{ item[sLabel] }}</div>
<div v-if="hasChildren" class="right blue" @click="clickItem($event, item)">
<van-icon name="cluster-o" />
下级
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CasadeSelectItem',
props: {
// 传入的数值
item: {
type: Object,
default: () => {}
},
// 是否选中状态
checked: {
type: Boolean,
default: false
},
// 是否只能选择叶子结点
nodes: {
type: Boolean,
default: false
},
// 列名称对照表
comparison: {
type: Object,
default: () => {
return {
value: 'value', // 选中值
label: 'label', // 显示名称
children: 'children' // 子级名称
}
}
}
},
data() {
return {
sLabel: this.comparison.label ? this.comparison.label : 'label', // label值名称
sChildren: this.comparison.children ? this.comparison.children : 'children', // children值名称
curChecked: this.checked //是否选中状态
}
},
computed: {
/**
* 是否包含子级
*/
hasChildren: function () {
return this.item ? (this.item[this.sChildren] ? this.item[this.sChildren].length > 0 : false) : false
}
},
watch: {
// 监听列名对照表变化
comparison: {
handler() {
this.sLabel = this.comparison.label ? this.comparison.label : 'label'
this.sChildren = this.comparison.children ? this.comparison.children : 'children'
},
deep: true
},
// 是否选中状态
checked: function (val) {
this.curChecked = val
}
},
methods: {
/** 点击当前项的执行方法
*/
clickItem: function (e, item) {
this.$emit('clickItem', item, this.hasChildren)
// 不包含下一级,修改check值事件
if (!this.hasChildren && this.isCheck) {
this.clickBox(e)
}
},
/**
* 点击单选框或复选框
*/
clickBox: function (checked) {
this.curChecked = !this.curChecked
this.$emit('change', checked)
}
}
}
</script>
<style lang="less" scoped>
@import './index';
</style>
新建组件-面包屑
CasadeSelect/CasadeSelectNavigation.vue
<template>
<div class="title">
<div ref="sea" class="flex nav">
<!-- 全部 -->
<div class="inline-item" @click="clickItem(null, -1)">
<span v-if="!isre && treeStack.length == 0" class="none">全部</span>
<span v-else class="active">全部</span>
</div>
<!-- 全部 -->
<!-- 搜索结果 -->
<div v-if="isre" @click="clickItem(null, -2)" class="inline-item" :class="activeSearch ? 'active' : 'none'">
<van-icon name="arrow" class="gray-6" />
搜索结果
</div>
<!-- 搜索结果 -->
<!-- 当前树的层级值 -->
<div v-for="(item, index) in treeStack" class="inline-item" :key="index">
<div class="inline-item" @click="clickItem(item, index)">
<van-icon name="arrow" class="gray-6" />
<span v-if="index == treeStack.length - 1" class="none inline-item">
{{ item[slabel] }}
</span>
<span v-else class="active">
{{ item[slabel] }}
</span>
</div>
</div>
<!-- 当前树的层级值 -->
</div>
</div>
</template>
<script>
export default {
name: 'CasadeSelectNavigation',
props: {
// 显示的label值
slabel: {
type: String,
default: 'label'
}
},
data() {
return {
isre: false, // 是否进行了搜索(返回是否进行了搜索)
treeStack: [] // 当前搜索值
}
},
computed: {
// 是否可点击搜索结果
activeSearch: function () {
return this.treeStack.length > 0
}
},
created: function () {
// 浅拷贝导航列表的每一个对象(为了不改变item值,也不复制过多的数据)
this.treeStack.forEach(item => {
var tempItem = Object.assign(item)
this.treeStack.push(tempItem)
})
var obj = {
setIsre: this.setIsre,
getIsre: this.getIsre,
setTreeStack: this.setTreeStack,
concatTreeStack: this.concatTreeStack,
pushTreeStack: this.pushTreeStack,
clearTreeStack: this.clearTreeStack,
getTreeStack: this.getTreeStack
}
this.$emit('inF', obj) // 导出的导航栏调用方法
},
methods: {
/** 设置isre值(是否搜索)
*/
setIsre: function (isre) {
this.isre = isre
},
/**
* 获取isr值(获取是否搜索中)
*/
getIsre: function () {
return this.isre
},
/** 设置导航树
*/
setTreeStack: function (treeStack) {
this.treeStack = treeStack
},
/** 拼接导航树
*/
concatTreeStack: function (treeStack) {
this.treeStack = this.treeStack.concat(treeStack)
},
/** 为导航树添加项
*/
pushTreeStack: function (item) {
this.treeStack.push(item)
},
/**
* 获取当前导航条
*/
getTreeStack: function () {
return this.treeStack
},
/**
* 清空导航树
*/
clearTreeStack: function () {
this.treeStack.splice(0)
},
/** 点击导航栏索引
*/
clickItem(item, index) {
if (index == -1) {
// 点击全部
this.isre = false
this.treeStack.splice(0)
} else if (index == -2) {
// 搜索结果
if (this.activeSearch) {
this.isre = true
this.treeStack.splice(0)
}
} else {
// 点击某一层级树
this.isre = false
if (this.treeStack.length - 1 > index) {
this.treeStack.splice(index + 1)
}
}
this.$emit('clickItem', item, index)
}
}
}
</script>
<style lang="less" scoped>
.inline-item {
color: #011b4c;
}
.nav {
width: 100%;
white-space: nowrap;
overflow: auto;
}
.active {
color: #0056fb;
}
</style>
新建组件-搜索
CasadeSelect/CasadeSelectSearch.vue
<template>
<div>
<!-- 输入框内容 -->
<van-search
class="text"
type="text"
v-model="inputVal"
confirm-type="搜索"
placeholder="请输入"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@search="handleFllter"
@clear="clears"
/>
</div>
</template>
<script>
export default {
name: 'CasadeSelectSearch',
data() {
return {
inputVal: '' // 输入内容
}
},
methods: {
/** 输入框变化时方法
*/
handleInput() {
this.$emit('input', this.inputVal)
},
/** 输入框聚焦时触发
*/
handleFocus() {
this.$emit('focus', this.inputVal)
},
/** 输入框失去焦点时触发
*/
handleBlur() {
this.$emit('blur', this.inputVal)
},
/** 提交内容时触发
*/
handleFllter() {
this.$emit('confirm', this.inputVal)
},
/**
* 清空输入框内容
*/
clears: function () {
this.inputVal = ''
this.$emit('clear', this.inputVal)
}
}
}
</script>
<style lang="less" scoped>
.filterBox {
padding: 15px 32px;
.filter-input {
height: 80px;
display: flex;
align-items: center;
padding-left: 40px;
.filterImg {
width: 32px;
height: 32px;
margin-right: 20px;
margin-bottom: 5px;
}
.filterImgs {
width: 32px;
height: 32px;
}
.text {
width: 100%;
font-size: 32px;
color: #000;
}
}
}
// 添加左侧padding(用于扩大图标范围)
.padding-left-sm {
padding-left: 20px;
}
</style>
新建组件-index
- CasadeSelect/index.vue
<template>
<div>
<!-- 搜索框 -->
<div class="header">
<!-- 搜索栏 -->
<casade-select-search @confirm="confirmSearch" />
<!-- 面包屑导航 -->
<casade-select-navigation :slabel="props.label" ref="navigation" @inF="navigationInt" @clickItem="backTree" />
</div>
<!-- 列表 -->
<div>
<div class="container-list">
<casade-select-item
v-for="(item, index) in tree"
:key="index"
:item="item"
:checked="newCheckList.includes(item.id)"
:nodes="props.nodes"
:comparison="comparison"
@clickItem="toChildren"
@change="checked => radioChange(checked, item, index)"
/>
</div>
</div>
<!-- 确定按钮 -->
<div class="padding-lr-sm padding-tb-sm">
<van-button block round class="sureBtn" type="info" @click="backConfirm">确认</van-button>
</div>
</div>
</template>
<script>
import { Toast } from 'vant'
import CasadeSelectSearch from './CasadeSelectSearch'
import CasadeSelectItem from './CasadeSelectItem'
import CasadeSelectNavigation from './CasadeSelectNavigation'
export default {
name: 'CasadeSelect',
components: { CasadeSelectSearch, CasadeSelectItem, CasadeSelectNavigation },
props: {
// 传入的树形结构数据,每个对象必须包含唯一的id值
trees: {
type: Array,
default: () => {
return []
}
},
// 选中列表
checkList: {
type: Array,
default: () => []
},
// 父级列表
parentList: {
type: Array,
default: () => []
},
// 树的属性参数
props: {
type: Object,
default: () => {
return {
id: 'id',
label: 'name',
children: 'children',
checkStrictly: false, //不关联
nodes: false // nodes为false时,可以选择任意一级选项;nodes为true时只能选择叶子节点
}
}
},
/**
* 是否懒加载树的值
*/
stepReload: {
type: Boolean,
default: false
},
// 每次循环加载的item的数据量
pageSize: {
type: Number,
default: 50
}
},
data() {
return {
// 导航条
setIsre: null, // 导航条方法 - 设置是否搜索中方法
getIsre: null, // 获取是否搜索中
setTreeStack: null, // 导航条 - 设置导航
concatTreeStack: null, // 导航条 - 拼接当前导航对象
clearTreeStack: null, // 导航条- 清空导航条
getTreeStack: null, // 导航条 - 获取导航条
itemsLoading: false, // item是否在循环渲染中
itemsStop: false, // 是否终止其他渲染
tree: [], // 默认数组
newNum: 0,
oldNum: 0,
allData: this.trees,
parent_data: this.parentList || [], //选择父辈
searchResult: [],
newCheckList: this.checkList,
nodePathArray: [], // 当前展示的路径
// item名称对照表
comparison: {
value: this.props.id ? this.props.id : 'id', // 选中值名称
label: this.props.label ? this.props.label : 'name', // 显示名称
children: this.props.children ? this.props.children : 'children' // 子集名称
}
}
},
watch: {
checkList() {
this.newCheckList = this.checkList
this.setSelected()
},
// 监听数据值的变化
trees(val, oldval) {
if (val != oldval) {
var tree_stack = this.getTreeStack()
this.allData = val // 重新加载所有树
// 重新加载当前树
if (!Array.isArray(tree_stack)) {
this.loadTree(val)
return
}
var length = tree_stack.length
if (length === 0) {
if (typeof this.getIsre === 'function') {
if (this.getIsre()) {
return
}
}
this.loadTree(val)
} else {
let tempArray = val // 存储当前值
let children = this.props.children
for (var i = 0; i < length; i++) {
var tempObject = tempArray.find(item => {
return tree_stack[i].Value == item.Value
})
if (tempObject) {
tempArray = tempObject[children]
} else {
// 跳转到全部
break
}
if (i == length - 1) {
this.loadTree(tempArray)
}
}
}
}
},
// 树的属性对照参数
props: {
handler: function () {
this.comparison.value = this.props.id ? this.props.id : 'id'
this.comparison.label = this.props.label ? this.props.label : 'name'
this.comparison.children = this.props.children ? this.props.children : []
},
deep: true
}
},
created: function () {
this.loadTree(this.trees)
},
// 实例被挂载后调用
mounted() {},
methods: {
/*
* 默认选中
* */
setSelected() {
// 初始化选中项
this.$nextTick(() => {
let children = this.props.children
if (this.newCheckList.length > 0) {
this.getNodeRoute(this.allData, this.newCheckList[0])
let arr = this.nodePathArray.reverse()
if (arr.length == 0) {
return
}
this.concatTreeStack(arr) // 获取导航条的值
var tree_stack = this.getTreeStack()
var data = tree_stack[tree_stack.length - 1][children] ? tree_stack[tree_stack.length - 1][children] : []
this.loadTree(data)
}
})
},
/** 初始化导航条的方法
*/
navigationInt(e) {
this.setIsre = e.setIsre
this.getIsre = e.getIsre
this.concatTreeStack = e.concatTreeStack
this.pushTreeStack = e.pushTreeStack
this.clearTreeStack = e.clearTreeStack
this.getTreeStack = e.getTreeStack
},
/**单选
*/
radioChange: function (checked, item, index) {
let id = this.props.id
if (checked) {
// 选中当前对象
this.newCheckList = []
this.newCheckList.push(this.tree[index][id])
} else {
// 移除其他对象
var nIndex = this.newCheckList.indexOf(item[id])
this.newCheckList.splice(nIndex, 1)
}
this.$emit('change', this.newCheckList)
},
/**
* @param {Array} tree 目标树
* @param {Object} targetId 为目标节点id
*/
getNodeRoute(tree, targetId) {
let children = this.props.children
let id = this.props.id
for (let index = 0; index < tree.length; index++) {
if (tree[index][children]) {
if (tree[index][children]) {
let endRecursiveLoop = this.getNodeRoute(tree[index][children], targetId)
if (endRecursiveLoop) {
this.nodePathArray.push(tree[index])
return true
}
}
}
if (tree[index][id] === targetId) {
return true
}
}
},
/**跳转到子级
*/
toChildren(item, realHasChildren) {
this.$emit('clickItem', item, realHasChildren) // 点击导航栏事件
// 不包含子级,不执行任何操作
if (!realHasChildren) {
return
}
// 点击跳转下一级
let children = this.props.children // 子级名称
// 将当前item加入到导航列表
if (item[children].length > 0) {
this.loadTree(item[children])
this.pushTreeStack(item) // 添加导航
}
},
/** 返回到其他树层
*/
backTree(item, index) {
this.$emit('backTree', item, index)
let that = this
if (index == -1) {
// 全部
that.loadTree(that.allData)
} else if (index == -2) {
// 搜索
that.loadTree(that.searchResult) // 搜索结果
} else {
// 其他层级
that.loadTree(item[that.props.children]) // tree的其他层级
}
},
/**
/** 搜索提交方法
*/
confirmSearch(val) {
this.searchResult = []
// 查找
this.$tips.loading('搜索中...')
this.search(this.allData, val)
// 返回搜索结果
Toast.clear()
this.setIsre(true) // 设置导航条为搜索状态
this.clearTreeStack() // 清空导航条
this.loadTree(this.searchResult)
},
/**搜索方法
*/
search(data, keyword) {
var that = this
let children = that.props.children
for (var i = 0, len = data.length; i < len; i++) {
// try-catch(try-catch) - 没有label列,跳过继续执行
try {
if (data[i][that.props.label].indexOf(keyword) >= 0) {
that.searchResult.push(data[i])
}
if (data[i][children]) {
if (data[i][children].length > 0) {
that.search(data[i][children], keyword)
}
}
} catch (e) {
console.warn(e)
}
}
},
/**
* 点击确认按钮执行事件
*/
backConfirm() {
this.$emit('sendValue', this.newCheckList, 'back')
},
/**加载Tree值
*/
loadTree(datas, start = 0) {
let that = this
if (!this.stepReload) {
// 不进行多次渲染加载
that.tree = datas
} else {
// datas为null, 不进行渲染
if (!Array.isArray(datas)) {
that.tree = datas
return
} else if (datas.length === 0) {
that.tree = datas
return
}
// 进行多次渲染加载
if (start === 0) {
// 终止其他渲染
if (that.itemsLoading) {
that.itemsStop = true //终止其他Item渲染
}
// 首次加载提醒
this.$tips.loading()
that.tree = []
that.itemsLoading = true
}
var length = datas.length
var end = Math.min(start + that.pageSize, length)
var tempArray = datas.slice(start, end)
that.tree = that.tree.concat(tempArray)
that.$nextTick(function () {
if (start == 0) {
Toast.clear()
that.itemsStop = false
}
if (end < length && !that.itemsStop) {
that.loadTree(datas, end)
} else {
that.itemsLoading = false
}
})
}
}
}
}
</script>
<style lang="less" scoped>
@import 'index.less';
</style>
新建样式-index.less
- CasadeSelect/index.less
.flex_between_center {
display: flex;
justify-content: space-between;
align-items: center;
}
.checkbox {
position: relative;
height: 18px;
margin-left: 5px;
margin-right: 0px;
width: 18px;
.color {
color: #0056fb;
background-color: #0056fb;
}
.txt {
// font-size: 15px;
line-height: 18px;
width: 100%;
height: 100%;
display: flex;
}
}
.checkBorder {
border: 1px solid #ecdee4;
}
.header {
width: 100%;
//position: fixed;
background-color: #fff;
z-index: 9999;
.title {
height: 45px;
padding: 0 16px;
line-height: 45px;
font-size: 15px;
background-color: #f5f5f5;
color: #606064;
.iconclass {
display: inline-block;
margin: 0 6px;
color: #D0D4DB;
font-size: 14px;
}
}
}
.container-list {
overflow-y: scroll;
overflow-x: hidden;
.common {
background-color: #fff;
border-bottom: 1px solid #f4f4f4;
padding-left: 5px;
.content {
display: flex;
align-items: center;
min-height: 30px;
width: 100%;
padding: 8px 0;
position: relative;
font-size: 16px;
.right {
position: absolute;
right: 15px;
color: #babdc3;
font-size: 16px;
}
}
}
}
.active {
color:#0056fb !important;
}
.none {
color: #666666;
}
.icon-selected{
color: #0056fb!important;
font-size: 20px!important;
}
.icons{
color: #0056fb!important;
font-size: 20px!important;
}
.inline-item {
display: inline-block
}
.content-item{
display: flex;
position: relative;
align-items: center;
}
.box_sizing {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.btn {
//position: fixed;
bottom: 0;
padding: 5px;
background-color: #fff;
width: 100%;
.sureBtn {
background-color: #0056fb;
color: #fff;
}
}
.container-list-item {
display: flex;
font-size: 14px;
align-items: center;
width: calc(100% - 20px);
margin: 0 10px ;
padding: 10px 0;
border-bottom: 1px solid #D9DDE4;
.icon {
font-size: 17px;
}
.info {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
white-space: nowrap;
margin-left: 10px;
}
// item的数字样式
.word {
font-size: 15px;
color: #5b5757;
width: 100%;
word-break: break-all;
}
}
.container-list {
height: 40vh;
}
调用组件
<template>
<div>
<div class="value" @click="showPicker = true" :class="fieldValue ? 'black' : 'gray-6'">
{{ fieldValue ? fieldValue : '请选择' }}
</div>
<van-popup get-container="body" v-model="showPicker" round position="bottom" safe-area-inset-bottom>
<van-cascader
v-model="cascaderValue"
title="所属部门"
:options="options"
@close="showPicker = false"
@finish="onFinish"
/>
</van-popup>
</div>
</template>
<script>
export default {
name: 'CascaderPicker',
data() {
return {
showPicker: false,
fieldValue: '',
cascaderValue: '',
// 选项列表,children 代表子选项,支持多级嵌套
options: []
}
},
methods: {
// 全部选项选择完毕后,会触发 finish 事件
onFinish({ selectedOptions }) {
this.showPicker = false
this.fieldValue = selectedOptions.map(option => option.text).join('/')
}
}
}
</script>
本文来自博客园,作者:战立标,转载请注明原文链接:https://www.cnblogs.com/zhanlibiao/p/17391013.html