<template> <view> <template v-if="showLoading"> <view class="ly-loader ly-flex-center"> <view class="ly-loader-inner">加载中...</view> </view> </template> <template v-else> <view v-if="isEmpty || !visible" class="notData-box u-flex-col"> <view class="u-flex-col notData-inner"> <image :src="icon" mode="" class="iconImg"></image> </view> <text class="notData-inner-text">暂无数据</text> </view> <view :key="updateKey" class="ly-tree" :class="{'is-empty': isEmpty || !visible}" role="tree" name="LyTreeExpand"> <ly-tree-node v-for="nodeId in childNodesId" :nodeId="nodeId" :render-after-expand="renderAfterExpand" :show-checkbox="showCheckbox" :show-radio="showRadio" :check-only-leaf="checkOnlyLeaf" :key="getNodeKey(nodeId)" :indent="indent" :icon-class="iconClass" updateKey="hanldeUpdateKey"> </ly-tree-node> </view> </template> </view> </template> <script> import resources from '@/libs/resources.js' import TreeStore from './model/tree-store.js'; import { getNodeKey } from './tool/util.js'; import LyTreeNode from './ly-tree-node.vue'; export default { name: 'LyTree', componentName: 'LyTree', components: { LyTreeNode }, data() { return { updateKey: new Date().getTime(), // 数据更新的时候,重新渲染树 elId: `ly_${Math.ceil(Math.random() * 10e5).toString(36)}`, visible: true, store: { ready: false }, currentNode: null, childNodesId: [], mathKey: 1, icon: resources.message.nodata, }; }, provide() { return { tree: this } }, props: { // 展示数据 treeData: Array, // 自主控制loading加载,避免数据还没获取到的空档出现“暂无数据”字样 ready: { type: Boolean, default: true }, // 内容为空的时候展示的文本 emptyText: { type: String, default: '暂无数据' }, // 是否在第一次展开某个树节点后才渲染其子节点 renderAfterExpand: { type: Boolean, default: true }, // 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 nodeKey: String, // 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false checkStrictly: Boolean, // 是否默认展开所有节点 defaultExpandAll: { type: Boolean, default: true }, // 切换全部展开、全部折叠 toggleExpendAll: Boolean, // 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点 expandOnClickNode: { type: Boolean, default: true }, // 选中的时候展开节点 expandOnCheckNode: { type: Boolean, default: true }, // 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点 checkOnClickNode: Boolean, checkDescendants: { type: Boolean, default: false }, // 展开子节点的时候是否自动展开父节点 autoExpandParent: { type: Boolean, default: true }, // 默认勾选的节点的 key 的数组 defaultCheckedKeys: Array, // 默认展开的节点的 key 的数组 defaultExpandedKeys: Array, // 是否展开当前节点的父节点 expandCurrentNodeParent: Boolean, // 当前选中的节点 currentNodeKey: [String, Number], // 是否最后一层叶子节点才显示单选/多选框 checkOnlyLeaf: { type: Boolean, default: false }, // 节点是否可被选择 showCheckbox: { type: Boolean, default: false }, // 节点单选 showRadio: { type: Boolean, default: false }, // 配置选项 props: { type: [Object, Function], default () { return { children: 'children', // 指定子树为节点对象的某个属性值 label: 'label', // 指定节点标签为节点对象的某个属性值 disabled: 'disabled' // 指定节点选择框是否禁用为节点对象的某个属性值 }; } }, // 是否懒加载子节点,需与 load 方法结合使用 lazy: { type: Boolean, default: false }, // 是否高亮当前选中节点,默认值是 false highlightCurrent: Boolean, // 加载子树数据的方法,仅当 lazy 属性为true 时生效 load: Function, // 对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏 filterNodeMethod: Function, // 搜索时是否展示匹配项的所有子节点 childVisibleForFilterNode: { type: Boolean, default: false }, // 是否每次只打开一个同级树节点展开 accordion: Boolean, // 相邻级节点间的水平缩进,单位为像素 indent: { type: Number, default: 18 }, // 自定义树节点的展开图标 iconClass: String, // 是否显示节点图标,如果配置为true,需要配置props中对应的图标属性名称 showNodeIcon: { type: Boolean, default: false }, // 当节点图标显示出错时,显示的默认图标 defaultNodeIcon: { type: String, default: '' }, // 如果数据量较大,建议不要在node节点中添加parent属性,会造成性能损耗 isInjectParentInNode: { type: Boolean, default: false } }, computed: { isEmpty() { if (this.store.root) { const childNodes = this.store.root.getChildNodes(this.childNodesId); return !childNodes || childNodes.length === 0 || childNodes.every(({ visible }) => !visible); } return true; }, showLoading() { //不要删除 const a = this.mathKey return !(this.store.getReady() && this.ready); } }, watch: { toggleExpendAll(newVal) { this.store.toggleExpendAll(newVal); }, defaultCheckedKeys(newVal) { this.store.setDefaultCheckedKey(newVal); }, defaultExpandedKeys(newVal) { this.store.defaultExpandedKeys = newVal; this.store.setDefaultExpandedKeys(newVal); }, checkStrictly(newVal) { this.store.checkStrictly = newVal || this.checkOnlyLeaf; }, 'store.root.childNodesId'(newVal) { this.childNodesId = newVal; }, 'store.root.visible'(newVal) { this.visible = newVal; }, childNodesId() { this.$nextTick(() => { this.$emit('ly-tree-render-completed'); }); }, treeData: { handler(newVal) { this.updateKey = new Date().getTime(); this.store.setData(newVal); }, deep: true } }, methods: { /* * @description 对树节点进行筛选操作 * @method filter * @param {all} value 在 filter-node-method 中作为第一个参数 * @param {Object} data 搜索指定节点的节点数据,不传代表搜索所有节点,假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 */ filter(value, data) { if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter'); this.store.filter(value, data); this.handleUpdateKey() }, handleUpdateKey() { this.updateKey = new Date().getTime(); }, /* * @description 获取节点的唯一标识符 * @method getNodeKey * @param {String, Number} nodeId * @return {String, Number} 匹配到的数据中的某一项数据 */ getNodeKey(nodeId) { let node = this.store.root.getChildNodes([nodeId])[0]; return getNodeKey(this.nodeKey, node.data); }, /* * @description 获取节点路径 * @method getNodePath * @param {Object} data 节点数据 * @return {Array} 路径数组 */ getNodePath(data) { return this.store.getNodePath(data); }, /* * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组 * @method getCheckedNodes * @param {Boolean} leafOnly 是否只是叶子节点,默认false * @param {Boolean} includeHalfChecked 是否包含半选节点,默认false * @return {Array} 目前被选中的节点所组成的数组 */ getCheckedNodes(leafOnly, includeHalfChecked) { return this.store.getCheckedNodes(leafOnly, includeHalfChecked); }, /* * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点的 key 所组成的数组 * @method getCheckedKeys * @param {Boolean} leafOnly 是否只是叶子节点,默认false,若为 true 则仅返回被选中的叶子节点的 keys * @param {Boolean} includeHalfChecked 是否返回indeterminate为true的节点,默认false * @return {Array} 目前被选中的节点所组成的数组 */ getCheckedKeys(leafOnly, includeHalfChecked) { return this.store.getCheckedKeys(leafOnly, includeHalfChecked); }, /* * @description 获取当前被选中节点的 data,若没有节点被选中则返回 null * @method getCurrentNode * @return {Object} 当前被选中节点的 data,若没有节点被选中则返回 null */ getCurrentNode() { const currentNode = this.store.getCurrentNode(); return currentNode ? currentNode.data : null; }, /* * @description 获取当前被选中节点的 key,若没有节点被选中则返回 null * @method getCurrentKey * @return {all} 当前被选中节点的 key, 若没有节点被选中则返回 null */ getCurrentKey() { const currentNode = this.getCurrentNode(); return currentNode ? currentNode[this.nodeKey] : null; }, /* * @description 设置全选/取消全选 * @method setCheckAll * @param {Boolean} isCheckAll 选中状态,默认为true */ setCheckAll(isCheckAll = true) { if (this.showRadio) throw new Error( 'You set the "show-radio" property, so you cannot select all nodes'); if (!this.showCheckbox) console.warn( 'You have not set the property "show-checkbox". Please check your settings'); this.store.setCheckAll(isCheckAll); }, /* * @description 设置目前勾选的节点 * @method setCheckedNodes * @param {Array} nodes 接收勾选节点数据的数组 * @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false */ setCheckedNodes(nodes, leafOnly) { this.store.setCheckedNodes(nodes, leafOnly); }, /* * @description 通过 keys 设置目前勾选的节点 * @method setCheckedKeys * @param {Array} keys 勾选节点的 key 的数组 * @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false */ setCheckedKeys(keys, leafOnly) { if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys'); this.store.setCheckedKeys(keys, leafOnly); this.handleUpdateKey() }, /* * @description 通过 key / data 设置某个节点的勾选状态 * @method setChecked * @param {all} data 勾选节点的 key 或者 data * @param {Boolean} checked 节点是否选中 * @param {Boolean} deep 是否设置子节点 ,默认为 false */ setChecked(data, checked, deep) { this.store.setChecked(data, checked, deep); }, /* * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点所组成的数组 * @method getHalfCheckedNodes * @return {Array} 目前半选中的节点所组成的数组 */ getHalfCheckedNodes() { return this.store.getHalfCheckedNodes(); }, /* * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点的 key 所组成的数组 * @method getHalfCheckedKeys * @return {Array} 目前半选中的节点的 key 所组成的数组 */ getHalfCheckedKeys() { return this.store.getHalfCheckedKeys(); }, /* * @description 通过 node 设置某个节点的当前选中状态 * @method setCurrentNode * @param {Object} node 待被选节点的 node */ setCurrentNode(node) { if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode'); this.store.setUserCurrentNode(node); }, /* * @description 通过 key 设置某个节点的当前选中状态 * @method setCurrentKey * @param {all} key 待被选节点的 key,若为 null 则取消当前高亮的节点 */ setCurrentKey(key) { if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey'); this.store.setCurrentNodeKey(key); }, /* * @description 根据 data 或者 key 拿到 Tree 组件中的 node * @method getNode * @param {all} data 要获得 node 的 key 或者 data */ getNode(data) { return this.store.getNode(data); }, /* * @description 删除 Tree 中的一个节点 * @method remove * @param {all} data 要删除的节点的 data 或者 node */ remove(data) { this.store.remove(data); }, /* * @description 为 Tree 中的一个节点追加一个子节点 * @method append * @param {Object} data 要追加的子节点的 data * @param {Object} parentNode 子节点的 parent 的 data、key 或者 node */ append(data, parentNode) { this.store.append(data, parentNode); }, /* * @description 为 Tree 的一个节点的前面增加一个节点 * @method insertBefore * @param {Object} data 要增加的节点的 data * @param {all} refNode 要增加的节点的后一个节点的 data、key 或者 node */ insertBefore(data, refNode) { this.store.insertBefore(data, refNode); }, /* * @description 为 Tree 的一个节点的后面增加一个节点 * @method insertAfter * @param {Object} data 要增加的节点的 data * @param {all} refNode 要增加的节点的前一个节点的 data、key 或者 node */ insertAfter(data, refNode) { this.store.insertAfter(data, refNode); }, /* * @description 通过 keys 设置节点子元素 * @method updateKeyChildren * @param {String, Number} key 节点 key * @param {Object} data 节点数据的数组 */ updateKeyChildren(key, data) { if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild'); this.store.updateChildren(key, data); } }, created() { this.isTree = true; let props = this.props; if (typeof this.props === 'function') props = this.props(); if (typeof props !== 'object') throw new Error('props must be of object type.'); this.store = new TreeStore({ key: this.nodeKey, data: this.treeData, lazy: this.lazy, props: props, load: this.load, showCheckbox: this.showCheckbox, showRadio: this.showRadio, currentNodeKey: this.currentNodeKey, checkStrictly: this.checkStrictly || this.checkOnlyLeaf, checkDescendants: this.checkDescendants, expandOnCheckNode: this.expandOnCheckNode, defaultCheckedKeys: this.defaultCheckedKeys, defaultExpandedKeys: this.defaultExpandedKeys, expandCurrentNodeParent: this.expandCurrentNodeParent, autoExpandParent: this.autoExpandParent, defaultExpandAll: this.defaultExpandAll, filterNodeMethod: this.filterNodeMethod, childVisibleForFilterNode: this.childVisibleForFilterNode, showNodeIcon: this.showNodeIcon, isInjectParentInNode: this.isInjectParentInNode }); this.childNodesId = this.store.root.childNodesId; uni.$on(`updateKey`, () => { this.handleUpdateKey() this.mathKey++ }); }, beforeDestroy() { if (this.accordion) { uni.$off(`${this.elId}-tree-node-expand`) } uni.$off('updateKey') } }; </script> <style lang="scss" scoped> .notData-box { width: 100%; height: 100%; justify-content: center; align-items: center; margin-top: 200rpx; .notData-inner { width: 286rpx; height: 222rpx; align-items: center; .iconImg { width: 100%; height: 100%; } } .notData-inner-text { color: #909399; } } .ly-tree { position: relative; cursor: default; background: #FFF; color: #606266; padding: 30rpx; } .ly-tree.is-empty { background: transparent; } /* lyEmpty-start */ .ly-empty { width: 100%; display: flex; justify-content: center; margin-top: 100rpx; } /* lyEmpty-end */ /* lyLoader-start */ .ly-loader { margin-top: 100rpx; display: flex; align-items: center; justify-content: center; } .ly-loader-inner, .ly-loader-inner:before, .ly-loader-inner:after { background: #efefef; animation: load 1s infinite ease-in-out; width: .5em; height: 1em; } .ly-loader-inner:before, .ly-loader-inner:after { position: absolute; top: 0; content: ''; } .ly-loader-inner:before { left: -1em; } .ly-loader-inner { text-indent: -9999em; position: relative; font-size: 22rpx; animation-delay: 0.16s; } .ly-loader-inner:after { left: 1em; animation-delay: 0.32s; } /* lyLoader-end */ @keyframes load { 0%, 80%, 100% { box-shadow: 0 0 #efefef; height: 1em; } 40% { box-shadow: 0 -1.5em #efefef; height: 1.5em; } } </style>