ly-tree.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <template>
  2. <view>
  3. <template v-if="showLoading">
  4. <view class="ly-loader ly-flex-center">
  5. <view class="ly-loader-inner">加载中...</view>
  6. </view>
  7. </template>
  8. <template v-else>
  9. <view v-if="isEmpty || !visible"
  10. class="ly-empty">
  11. {{emptyText}}
  12. </view>
  13. <view :key="updateKey"
  14. class="ly-tree"
  15. :class="{'is-empty': isEmpty || !visible}"
  16. role="tree"
  17. name="LyTreeExpand">
  18. <ly-tree-node v-for="nodeId in childNodesId"
  19. :nodeId="nodeId"
  20. :render-after-expand="renderAfterExpand"
  21. :show-checkbox="showCheckbox"
  22. :show-radio="showRadio"
  23. :check-only-leaf="checkOnlyLeaf"
  24. :key="getNodeKey(nodeId)"
  25. :indent="indent"
  26. :icon-class="iconClass">
  27. </ly-tree-node>
  28. </view>
  29. </template>
  30. </view>
  31. </template>
  32. <script>
  33. import Vue from 'vue'
  34. import TreeStore from './model/tree-store.js';
  35. import {getNodeKey} from './tool/util.js';
  36. import LyTreeNode from './ly-tree-node.vue';
  37. export default {
  38. name: 'LyTree',
  39. componentName: 'LyTree',
  40. components: {
  41. LyTreeNode
  42. },
  43. data() {
  44. return {
  45. updateKey: new Date().getTime(), // 数据更新的时候,重新渲染树
  46. elId: `ly_${Math.ceil(Math.random() * 10e5).toString(36)}`,
  47. visible: true,
  48. store: {
  49. ready: false
  50. },
  51. currentNode: null,
  52. childNodesId: []
  53. };
  54. },
  55. provide() {
  56. return {
  57. tree: this
  58. }
  59. },
  60. props: {
  61. // 展示数据
  62. treeData: Array,
  63. // 自主控制loading加载,避免数据还没获取到的空档出现“暂无数据”字样
  64. ready: {
  65. type: Boolean,
  66. default: true
  67. },
  68. // 内容为空的时候展示的文本
  69. emptyText: {
  70. type: String,
  71. default: '暂无数据'
  72. },
  73. // 是否在第一次展开某个树节点后才渲染其子节点
  74. renderAfterExpand: {
  75. type: Boolean,
  76. default: true
  77. },
  78. // 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的
  79. nodeKey: String,
  80. // 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false
  81. checkStrictly: Boolean,
  82. // 是否默认展开所有节点
  83. defaultExpandAll: Boolean,
  84. // 切换全部展开、全部折叠
  85. toggleExpendAll: Boolean,
  86. // 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点
  87. expandOnClickNode: {
  88. type: Boolean,
  89. default: true
  90. },
  91. // 选中的时候展开节点
  92. expandOnCheckNode: {
  93. type: Boolean,
  94. default: true
  95. },
  96. // 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点
  97. checkOnClickNode: Boolean,
  98. checkDescendants: {
  99. type: Boolean,
  100. default: false
  101. },
  102. // 展开子节点的时候是否自动展开父节点
  103. autoExpandParent: {
  104. type: Boolean,
  105. default: true
  106. },
  107. // 默认勾选的节点的 key 的数组
  108. defaultCheckedKeys: Array,
  109. // 默认展开的节点的 key 的数组
  110. defaultExpandedKeys: Array,
  111. // 是否展开当前节点的父节点
  112. expandCurrentNodeParent: Boolean,
  113. // 当前选中的节点
  114. currentNodeKey: [String, Number],
  115. // 是否最后一层叶子节点才显示单选/多选框
  116. checkOnlyLeaf: {
  117. type: Boolean,
  118. default: false
  119. },
  120. // 节点是否可被选择
  121. showCheckbox: {
  122. type: Boolean,
  123. default: false
  124. },
  125. // 节点单选
  126. showRadio: {
  127. type: Boolean,
  128. default: false
  129. },
  130. // 配置选项
  131. props: {
  132. type: [Object, Function],
  133. default () {
  134. return {
  135. children: 'children', // 指定子树为节点对象的某个属性值
  136. label: 'label', // 指定节点标签为节点对象的某个属性值
  137. disabled: 'disabled' // 指定节点选择框是否禁用为节点对象的某个属性值
  138. };
  139. }
  140. },
  141. // 是否懒加载子节点,需与 load 方法结合使用
  142. lazy: {
  143. type: Boolean,
  144. default: false
  145. },
  146. // 是否高亮当前选中节点,默认值是 false
  147. highlightCurrent: Boolean,
  148. // 加载子树数据的方法,仅当 lazy 属性为true 时生效
  149. load: Function,
  150. // 对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏
  151. filterNodeMethod: Function,
  152. // 搜索时是否展示匹配项的所有子节点
  153. childVisibleForFilterNode: {
  154. type: Boolean,
  155. default: false
  156. },
  157. // 是否每次只打开一个同级树节点展开
  158. accordion: Boolean,
  159. // 相邻级节点间的水平缩进,单位为像素
  160. indent: {
  161. type: Number,
  162. default: 18
  163. },
  164. // 自定义树节点的展开图标
  165. iconClass: String,
  166. // 是否显示节点图标,如果配置为true,需要配置props中对应的图标属性名称
  167. showNodeIcon: {
  168. type: Boolean,
  169. default: false
  170. },
  171. // 当节点图标显示出错时,显示的默认图标
  172. defaultNodeIcon: {
  173. type: String,
  174. default: 'https://img-cdn-qiniu.dcloud.net.cn/uniapp/doc/github.svg'
  175. },
  176. // 如果数据量较大,建议不要在node节点中添加parent属性,会造成性能损耗
  177. isInjectParentInNode: {
  178. type: Boolean,
  179. default: false
  180. }
  181. },
  182. computed: {
  183. isEmpty() {
  184. if (this.store.root) {
  185. const childNodes = this.store.root.getChildNodes(this.childNodesId);
  186. return !childNodes || childNodes.length === 0 || childNodes.every(({visible}) => !visible);
  187. }
  188. return true;
  189. },
  190. showLoading() {
  191. return !(this.store.ready && this.ready);
  192. }
  193. },
  194. watch: {
  195. toggleExpendAll(newVal) {
  196. this.store.toggleExpendAll(newVal);
  197. },
  198. defaultCheckedKeys(newVal) {
  199. this.store.setDefaultCheckedKey(newVal);
  200. },
  201. defaultExpandedKeys(newVal) {
  202. this.store.defaultExpandedKeys = newVal;
  203. this.store.setDefaultExpandedKeys(newVal);
  204. },
  205. checkStrictly(newVal) {
  206. this.store.checkStrictly = newVal || this.checkOnlyLeaf;
  207. },
  208. 'store.root.childNodesId'(newVal) {
  209. this.childNodesId = newVal;
  210. },
  211. 'store.root.visible'(newVal) {
  212. this.visible = newVal;
  213. },
  214. childNodesId(){
  215. this.$nextTick(() => {
  216. this.$emit('ly-tree-render-completed');
  217. });
  218. },
  219. treeData: {
  220. handler(newVal) {
  221. this.updateKey = new Date().getTime();
  222. this.store.setData(newVal);
  223. },
  224. deep: true
  225. }
  226. },
  227. methods: {
  228. /*
  229. * @description 对树节点进行筛选操作
  230. * @method filter
  231. * @param {all} value 在 filter-node-method 中作为第一个参数
  232. * @param {Object} data 搜索指定节点的节点数据,不传代表搜索所有节点,假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据
  233. */
  234. filter(value, data) {
  235. if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
  236. this.store.filter(value, data);
  237. },
  238. /*
  239. * @description 获取节点的唯一标识符
  240. * @method getNodeKey
  241. * @param {String, Number} nodeId
  242. * @return {String, Number} 匹配到的数据中的某一项数据
  243. */
  244. getNodeKey(nodeId) {
  245. let node = this.store.root.getChildNodes([nodeId])[0];
  246. return getNodeKey(this.nodeKey, node.data);
  247. },
  248. /*
  249. * @description 获取节点路径
  250. * @method getNodePath
  251. * @param {Object} data 节点数据
  252. * @return {Array} 路径数组
  253. */
  254. getNodePath(data) {
  255. return this.store.getNodePath(data);
  256. },
  257. /*
  258. * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组
  259. * @method getCheckedNodes
  260. * @param {Boolean} leafOnly 是否只是叶子节点,默认false
  261. * @param {Boolean} includeHalfChecked 是否包含半选节点,默认false
  262. * @return {Array} 目前被选中的节点所组成的数组
  263. */
  264. getCheckedNodes(leafOnly, includeHalfChecked) {
  265. return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
  266. },
  267. /*
  268. * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点的 key 所组成的数组
  269. * @method getCheckedKeys
  270. * @param {Boolean} leafOnly 是否只是叶子节点,默认false,若为 true 则仅返回被选中的叶子节点的 keys
  271. * @param {Boolean} includeHalfChecked 是否返回indeterminate为true的节点,默认false
  272. * @return {Array} 目前被选中的节点所组成的数组
  273. */
  274. getCheckedKeys(leafOnly, includeHalfChecked) {
  275. return this.store.getCheckedKeys(leafOnly, includeHalfChecked);
  276. },
  277. /*
  278. * @description 获取当前被选中节点的 data,若没有节点被选中则返回 null
  279. * @method getCurrentNode
  280. * @return {Object} 当前被选中节点的 data,若没有节点被选中则返回 null
  281. */
  282. getCurrentNode() {
  283. const currentNode = this.store.getCurrentNode();
  284. return currentNode ? currentNode.data : null;
  285. },
  286. /*
  287. * @description 获取当前被选中节点的 key,若没有节点被选中则返回 null
  288. * @method getCurrentKey
  289. * @return {all} 当前被选中节点的 key, 若没有节点被选中则返回 null
  290. */
  291. getCurrentKey() {
  292. const currentNode = this.getCurrentNode();
  293. return currentNode ? currentNode[this.nodeKey] : null;
  294. },
  295. /*
  296. * @description 设置全选/取消全选
  297. * @method setCheckAll
  298. * @param {Boolean} isCheckAll 选中状态,默认为true
  299. */
  300. setCheckAll(isCheckAll = true) {
  301. if (this.showRadio) throw new Error('You set the "show-radio" property, so you cannot select all nodes');
  302. if (!this.showCheckbox) console.warn('You have not set the property "show-checkbox". Please check your settings');
  303. this.store.setCheckAll(isCheckAll);
  304. },
  305. /*
  306. * @description 设置目前勾选的节点
  307. * @method setCheckedNodes
  308. * @param {Array} nodes 接收勾选节点数据的数组
  309. * @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false
  310. */
  311. setCheckedNodes(nodes, leafOnly) {
  312. this.store.setCheckedNodes(nodes, leafOnly);
  313. },
  314. /*
  315. * @description 通过 keys 设置目前勾选的节点
  316. * @method setCheckedKeys
  317. * @param {Array} keys 勾选节点的 key 的数组
  318. * @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false
  319. */
  320. setCheckedKeys(keys, leafOnly) {
  321. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys');
  322. this.store.setCheckedKeys(keys, leafOnly);
  323. },
  324. /*
  325. * @description 通过 key / data 设置某个节点的勾选状态
  326. * @method setChecked
  327. * @param {all} data 勾选节点的 key 或者 data
  328. * @param {Boolean} checked 节点是否选中
  329. * @param {Boolean} deep 是否设置子节点 ,默认为 false
  330. */
  331. setChecked(data, checked, deep) {
  332. this.store.setChecked(data, checked, deep);
  333. },
  334. /*
  335. * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点所组成的数组
  336. * @method getHalfCheckedNodes
  337. * @return {Array} 目前半选中的节点所组成的数组
  338. */
  339. getHalfCheckedNodes() {
  340. return this.store.getHalfCheckedNodes();
  341. },
  342. /*
  343. * @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点的 key 所组成的数组
  344. * @method getHalfCheckedKeys
  345. * @return {Array} 目前半选中的节点的 key 所组成的数组
  346. */
  347. getHalfCheckedKeys() {
  348. return this.store.getHalfCheckedKeys();
  349. },
  350. /*
  351. * @description 通过 node 设置某个节点的当前选中状态
  352. * @method setCurrentNode
  353. * @param {Object} node 待被选节点的 node
  354. */
  355. setCurrentNode(node) {
  356. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode');
  357. this.store.setUserCurrentNode(node);
  358. },
  359. /*
  360. * @description 通过 key 设置某个节点的当前选中状态
  361. * @method setCurrentKey
  362. * @param {all} key 待被选节点的 key,若为 null 则取消当前高亮的节点
  363. */
  364. setCurrentKey(key) {
  365. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey');
  366. this.store.setCurrentNodeKey(key);
  367. },
  368. /*
  369. * @description 根据 data 或者 key 拿到 Tree 组件中的 node
  370. * @method getNode
  371. * @param {all} data 要获得 node 的 key 或者 data
  372. */
  373. getNode(data) {
  374. return this.store.getNode(data);
  375. },
  376. /*
  377. * @description 删除 Tree 中的一个节点
  378. * @method remove
  379. * @param {all} data 要删除的节点的 data 或者 node
  380. */
  381. remove(data) {
  382. this.store.remove(data);
  383. },
  384. /*
  385. * @description 为 Tree 中的一个节点追加一个子节点
  386. * @method append
  387. * @param {Object} data 要追加的子节点的 data
  388. * @param {Object} parentNode 子节点的 parent 的 data、key 或者 node
  389. */
  390. append(data, parentNode) {
  391. this.store.append(data, parentNode);
  392. },
  393. /*
  394. * @description 为 Tree 的一个节点的前面增加一个节点
  395. * @method insertBefore
  396. * @param {Object} data 要增加的节点的 data
  397. * @param {all} refNode 要增加的节点的后一个节点的 data、key 或者 node
  398. */
  399. insertBefore(data, refNode) {
  400. this.store.insertBefore(data, refNode);
  401. },
  402. /*
  403. * @description 为 Tree 的一个节点的后面增加一个节点
  404. * @method insertAfter
  405. * @param {Object} data 要增加的节点的 data
  406. * @param {all} refNode 要增加的节点的前一个节点的 data、key 或者 node
  407. */
  408. insertAfter(data, refNode) {
  409. this.store.insertAfter(data, refNode);
  410. },
  411. /*
  412. * @description 通过 keys 设置节点子元素
  413. * @method updateKeyChildren
  414. * @param {String, Number} key 节点 key
  415. * @param {Object} data 节点数据的数组
  416. */
  417. updateKeyChildren(key, data) {
  418. if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
  419. this.store.updateChildren(key, data);
  420. }
  421. },
  422. created() {
  423. this.isTree = true;
  424. let props = this.props;
  425. if (typeof this.props === 'function') props = this.props();
  426. if (typeof props !== 'object') throw new Error('props must be of object type.');
  427. this.store = new TreeStore({
  428. key: this.nodeKey,
  429. data: this.treeData,
  430. lazy: this.lazy,
  431. props: props,
  432. load: this.load,
  433. showCheckbox: this.showCheckbox,
  434. showRadio: this.showRadio,
  435. currentNodeKey: this.currentNodeKey,
  436. checkStrictly: this.checkStrictly || this.checkOnlyLeaf,
  437. checkDescendants: this.checkDescendants,
  438. expandOnCheckNode: this.expandOnCheckNode,
  439. defaultCheckedKeys: this.defaultCheckedKeys,
  440. defaultExpandedKeys: this.defaultExpandedKeys,
  441. expandCurrentNodeParent: this.expandCurrentNodeParent,
  442. autoExpandParent: this.autoExpandParent,
  443. defaultExpandAll: this.defaultExpandAll,
  444. filterNodeMethod: this.filterNodeMethod,
  445. childVisibleForFilterNode: this.childVisibleForFilterNode,
  446. showNodeIcon: this.showNodeIcon,
  447. isInjectParentInNode: this.isInjectParentInNode
  448. });
  449. this.childNodesId = this.store.root.childNodesId;
  450. },
  451. beforeDestroy() {
  452. if (this.accordion) {
  453. uni.$off(`${this.elId}-tree-node-expand`)
  454. }
  455. },
  456. edit(node){
  457. this.$emit('edit',node)
  458. },
  459. deldata(node){
  460. this.$emit('deldata',node)
  461. }
  462. };
  463. </script>
  464. <style>
  465. .ly-tree {
  466. position: relative;
  467. cursor: default;
  468. background: #FFF;
  469. color: #606266;
  470. padding: 30rpx;
  471. }
  472. .ly-tree.is-empty {
  473. background: transparent;
  474. }
  475. /* lyEmpty-start */
  476. .ly-empty {
  477. width: 100%;
  478. display: flex;
  479. justify-content: center;
  480. margin-top: 100rpx;
  481. }
  482. /* lyEmpty-end */
  483. /* lyLoader-start */
  484. .ly-loader {
  485. margin-top: 100rpx;
  486. display: flex;
  487. align-items: center;
  488. justify-content: center;
  489. }
  490. .ly-loader-inner,
  491. .ly-loader-inner:before,
  492. .ly-loader-inner:after {
  493. background: #efefef;
  494. animation: load 1s infinite ease-in-out;
  495. width: .5em;
  496. height: 1em;
  497. }
  498. .ly-loader-inner:before,
  499. .ly-loader-inner:after {
  500. position: absolute;
  501. top: 0;
  502. content: '';
  503. }
  504. .ly-loader-inner:before {
  505. left: -1em;
  506. }
  507. .ly-loader-inner {
  508. text-indent: -9999em;
  509. position: relative;
  510. font-size: 22rpx;
  511. animation-delay: 0.16s;
  512. }
  513. .ly-loader-inner:after {
  514. left: 1em;
  515. animation-delay: 0.32s;
  516. }
  517. /* lyLoader-end */
  518. @keyframes load {
  519. 0%,
  520. 80%,
  521. 100% {
  522. box-shadow: 0 0 #efefef;
  523. height: 1em;
  524. }
  525. 40% {
  526. box-shadow: 0 -1.5em #efefef;
  527. height: 1.5em;
  528. }
  529. }
  530. </style>