ly-tree.vue 16 KB

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