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: stringlabel: stringchildren: stringdisabled?: boolean}const TreeProps: PropsIter = {value: 'id',label: 'name',children: 'children',}interface TreeIter {id: stringlabel: stringchildren?: TreeIter[]}export default defineComponent({props: {// 组件绑定的optionsoptions: {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.idselect.currentNodeLabel = data.label || data.nameselect.value = data.idemit('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的数据还没有加载完成就会失效,下拉选择时会警告 placeholderconst setCurrent = () => {select.currentNodeKey = select.valuetreeV2.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 = vsetCurrent()}})// 回显数据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-selectref="treeSelect" v-model="value" clearable filterable :placeholder="placeholder || '请选择'":filter-method="selectFilter" @clear="clearSelected"><el-option :value="currentNodeKey" :label="currentNodeLabel"><el-tree-v2id="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-selectv-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: stringid: stringname?: string}// 定义树节点的数据类型,包含id、name、可能存在的子节点interface TreeNode {id: stringname?: stringpid: stringchildren?: 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 = idfor (const item of plainList) {if (item.pid !== pid) { // 只要有一个元素的pid没有指向第一个元素的父id,认为有必要转换成树,否则认为无必要flag = truebreak}}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 = 1const 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 = 0list.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.添加所有的父idpidList.add(item.pid)}for (const item of plainList) { // 2.删除所有的子idif (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: stringopen: string | booleanchecked: 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 }}