ly-tree.vue 16 KB

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