vue3树形下拉框组件
vue3树形下拉框组件
·
1.组件封装
<!-- 树状选择器 -->
<script lang="ts">
import type { TreeNode } from 'element-plus/es/components/tree-v2/src/types'
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
import type { PropType } from 'vue'
import { defineComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue'
interface PropsIter {
value: string
label: string
children: string
disabled?: boolean
}
const TreeProps: PropsIter = {
value: 'id',
label: 'name',
children: 'children',
}
interface TreeIter {
id: string
label: string
children?: TreeIter[]
}
export default defineComponent({
props: {
// 组件绑定的options
options: {
type: Array as PropType<TreeIter[]>,
required: true,
},
// 配置选项
keyProps: Object as PropType<PropsIter>,
// 双向绑定值
modelValue: [String, Number],
// 组件样式宽
width: {
type: String,
default: '240px',
},
// 空占位字符
placeholder: String,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
// 解决 props道具变异
const { modelValue } = toRefs(props)
const select: { value: string | number | undefined; currentNodeLabel: string | number | undefined; currentNodeKey: string | number | undefined } = reactive({
value: modelValue.value,
currentNodeKey: '',
currentNodeLabel: '',
})
const treeSelect = ref<HTMLElement | null>(null)
const blur = ref<HTMLElement | null>()
const nodeClick = (data: TreeNodeData, node: TreeNode) => {
select.currentNodeKey = data.id
select.currentNodeLabel = data.label || data.name
select.value = data.id
emit('update:modelValue', select.value);
// 关闭下拉框
(treeSelect.value as any).handleClose()
nextTick(() => {
(treeSelect.value as any).handleClose()
})
}
// 筛选方法
const treeV2: any = ref<HTMLElement | null>(null)
const selectFilter = (query: string) => {
treeV2.value.filter(query)
}
// ztree-v2 筛选方法
const treeFilter = (query: string, node: TreeNode) => {
return node.label?.indexOf(query) !== -1
}
// 直接清空选择数据
const clearSelected = () => {
select.currentNodeKey = ''
select.currentNodeLabel = ''
select.value = ''
emit('update:modelValue', undefined)
}
// setCurrent通过select.value 设置下拉选择tree 显示绑定的v-model值
// 可能存在问题:当动态v-model赋值时 options的数据还没有加载完成就会失效,下拉选择时会警告 placeholder
const setCurrent = () => {
select.currentNodeKey = select.value
treeV2.value.setCurrentKey(select.value)
const data: TreeNodeData | undefined = treeV2.value.getCurrentNode(select.value)
select.currentNodeLabel = data?.label || data?.name
}
// 监听外部清空数据源 清空组件数据
watch(modelValue, (v) => {
if (v === undefined && select.currentNodeKey !== '') {
clearSelected()
}
// 动态赋值
if (v) {
select.value = v
setCurrent()
}
})
// 回显数据
onMounted(async () => {
await nextTick()
if (select.value) {
setCurrent()
}
})
return {
treeSelect,
treeV2,
TreeProps,
...toRefs(select),
nodeClick,
selectFilter,
treeFilter,
clearSelected,
}
},
})
</script>
<template>
<div class="tree_box" :style="width && { width: width.includes('px') ? width : width }">
<el-select
ref="treeSelect" v-model="value" clearable filterable :placeholder="placeholder || '请选择'"
:filter-method="selectFilter" @clear="clearSelected"
>
<el-option :value="currentNodeKey" :label="currentNodeLabel">
<el-tree-v2
id="tree_v2" ref="treeV2" :data="options" :props="keyProps || TreeProps"
:current-node-key="currentNodeKey" default-expand-all :expand-on-click-node="false"
:filter-method="treeFilter" @node-click="nodeClick"
/>
</el-option>
</el-select>
</div>
</template>
<style lang="scss" scoped>
.tree_box {
width: 214px;
}
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
height: auto;
max-height: 274px;
padding: 0;
overflow: hidden;
overflow-y: auto;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
ul li :deep(.el-tree .el-tree-node__content) {
height: auto;
padding: 0 20px;
}
.el-tree-node__label {
font-weight: normal;
}
.el-tree :deep(.is-current .el-tree-node__label) {
color: #409eff;
font-weight: 700;
}
.el-tree :deep(.is-current .el-tree-node__children .el-tree-node__label) {
color: #606266;
font-weight: normal;
}
.selectInput {
padding: 0 5px;
box-sizing: border-box;
}
.el-select {
width: 100% !important;
}
</style>
2.组件使用
<com-tree-select
v-model="ruleForm.mesureDept" :options="useDeptList as any" placeholder="使用部门"
:tree-props="deptProps"
/>
3.数据定义
const deptProps = reactive({
parent: 'pid', value: 'id', label: 'name', children: 'children',
})
const useDeptList = ref<deptType[]>([]) // 使用部门列表
// 获取部门列表
getDeptTreeList().then((res) => {
// 转成树结构
useDeptList.value = toTreeList(res.data, '0', true)
})
4.转树结构方法
// 数据结构转换工具
// 定义数组项的数据类型,包含id、name、parentId基本属性
interface ArrayItem {
pid: string
id: string
name?: string
}
// 定义树节点的数据类型,包含id、name、可能存在的子节点
interface TreeNode {
id: string
name?: string
pid: string
children?: TreeNode[] // 叶子节点没有子节点
}
/**
* 判断是否有转树的必要
* @param plainList 平行数据列表
* @param id 祖宗id
* @returns {boolean} 有返回true,无返回false
*/
export function judgeTree(plainList: ArrayItem[], id?: '0') {
if (plainList && plainList.length > 0) {
let flag = false // 是否需要转成树结构
const pid = id
for (const item of plainList) {
if (item.pid !== pid) { // 只要有一个元素的pid没有指向第一个元素的父id,认为有必要转换成树,否则认为无必要
flag = true
break
}
}
return flag
}
else { return false }
}
/**
* 平面数据数据转树结构
* @param plainList 平行数据列表
* @param id 祖宗id
* @param isSelect 是否是下拉需要顶级的树
* @returns {*}
*/
export function toTreeList<T extends ArrayItem>(plainList: T[], rootId = '0', isSelect = false): T[] {
const pid = findPid(plainList)
if (pid.length > 1) { // 如果有多个pid,直接返回列表, 不去构造树
return plainList
}
else {
const tree = cleanChildren(buildTree<T>(plainList, rootId, isSelect))
return tree
}
}
// 构建树
/**
*
* @param plainList 待转换数组
* @param id 父节点
* @param isSelect 是否是下拉框所使用的树
* @returns 树节点列表
*/
function buildTree<T extends TreeNode>(plainList: T[], id = '0', isSelect = false): T[] | [] {
// 递归函数
const fa = (parentId: string): Array<T> | [] => {
const temp = []
for (let i = 0; i < plainList.length; i++) {
const n: TreeNode = { ...plainList[i] }
const id = `${n.id}`
const pid = `${n.pid}`
if (pid === parentId) {
n.children = fa(id)
temp.push(n)
}
}
return temp as T[]
}
// 如果是下拉框需要使用的树,首先寻找顶级,将顶级也放入列表
if (isSelect) {
let flag = 1
const list = []
if (Array.isArray(plainList)) {
for (const item of plainList) {
const n: T = { ...item }
const nid = `${n.id}`
if (nid === id) {
n.children = fa(id)
flag = 0
list.push(n)
return list
}
else {
continue
}
}
}
if (flag === 1) { // 没有找到父级,按原流程走
return fa(id)
}
else {
return []
}
}
else {
return fa(id)
}
}
// 清除children为空列表的children项
function cleanChildren<T extends TreeNode>(data: T[]): T[] {
const fa = (list: TreeNode[]): T[] => {
list.map((e) => {
if (e && e.children && e.children.length) {
fa(e.children)
}
else {
delete e.children
}
return e
})
return list as T[]
}
return fa(data)
}
/**
*
* @param plainList 寻找列表中的父id
* @returns 父id列表
*/
function findPid(plainList: Array<ArrayItem>): Array<string> {
const pidList = new Set<string>()
if (plainList) {
for (const item of plainList) { // 1.添加所有的父id
pidList.add(item.pid)
}
for (const item of plainList) { // 2.删除所有的子id
if (pidList.has(item.id)) {
pidList.delete(item.id)
}
}
const arr = Array.from(pidList) // 剩下的就是最终的父节点
return arr
}
else {
return []
}
}
// 从树列表中删除指定元素
export function deleteItem(list: Array<TreeNode>, des: TreeNode) {
const del = (list: Array<TreeNode>, item: TreeNode) => {
for (let i = 0; i < list.length; i++) {
if (list[i].id === item.id) {
list.splice(i, 1)
return
}
else { // 遍历子孙,继续递归寻找
if (list[i].children && list[i].children!.length > 0) {
del(list[i].children!, item)
}
}
}
}
del(list, des)
}
interface treeItem {
id: string
open: string | boolean
checked: string | boolean
}
/**
*获取列表中的展开项和选中项
* @param plainList
* @param id
* @returns{展开项, 选中项}
*/
export function getShowItem(plainList: treeItem[]): { expandList: string[]; openedList: string[] } {
const expandList = []
const openedList = []
for (let i = 0; i < plainList.length; i++) {
if (plainList[i].open === 'true' || plainList[i].open === true) {
expandList.push(plainList[i].id)
}
if (plainList[i].checked === 'true' || plainList[i].checked === true) {
openedList.push(plainList[i].id)
}
}
return { expandList, openedList }
}
更多推荐
已为社区贡献2条内容
所有评论(0)