ly-tree.vue 17 KB

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