Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

558 řádky
14 KiB

  1. <template>
  2. <view class="u-tree">
  3. <view
  4. v-for="item in visibleNodes"
  5. :key="item.key"
  6. class="u-tree-node"
  7. :class="getNodeClass(item)"
  8. >
  9. <view
  10. class="u-tree-node__content"
  11. :style="getNodeContentStyle(item)"
  12. @tap="handleNodeClick(item)"
  13. >
  14. <view class="u-tree-node__switcher" @tap.stop="handleExpandClick(item)">
  15. <up-icon
  16. v-if="item.hasChildren"
  17. :name="item.expanded ? collapseIcon : expandIcon"
  18. :size="iconSize"
  19. :color="switcherColor"
  20. />
  21. </view>
  22. <up-checkbox
  23. v-if="showCheckbox"
  24. class="u-tree-node__checkbox"
  25. usedAlone
  26. :size="checkboxSize"
  27. :checked="item.checked"
  28. :disabled="item.disabled"
  29. @change="handleCheckboxChange(item, $event)"
  30. />
  31. <view class="u-tree-node__label">
  32. <!-- #ifndef MP-WEIXIN -->
  33. <slot
  34. :node="item.node"
  35. :data="item.node"
  36. :level="item.level + 1"
  37. :expanded="item.expanded"
  38. :checked="item.checked"
  39. :indeterminate="item.indeterminate"
  40. :disabled="item.disabled"
  41. >
  42. <text class="u-tree-node__text">{{ getNodeLabel(item.node) }}</text>
  43. </slot>
  44. <!-- #endif -->
  45. <!-- #ifdef MP-WEIXIN -->
  46. <text class="u-tree-node__text">{{ getNodeLabel(item.node) }}</text>
  47. <!-- #endif -->
  48. </view>
  49. </view>
  50. </view>
  51. </view>
  52. </template>
  53. <script>
  54. import { mpMixin } from '../../libs/mixin/mpMixin';
  55. import { mixin } from '../../libs/mixin/mixin';
  56. export default {
  57. name: 'u-tree',
  58. mixins: [mpMixin, mixin],
  59. props: {
  60. data: {
  61. type: Array,
  62. default: () => []
  63. },
  64. props: {
  65. type: Object,
  66. default: () => ({
  67. label: 'label',
  68. children: 'children',
  69. nodeKey: 'id',
  70. disabled: 'disabled'
  71. })
  72. },
  73. nodeKey: {
  74. type: String,
  75. default: ''
  76. },
  77. showCheckbox: {
  78. type: Boolean,
  79. default: false
  80. },
  81. defaultExpandAll: {
  82. type: Boolean,
  83. default: false
  84. },
  85. defaultExpandedKeys: {
  86. type: Array,
  87. default: () => []
  88. },
  89. defaultCheckedKeys: {
  90. type: Array,
  91. default: () => []
  92. },
  93. expandOnClickNode: {
  94. type: Boolean,
  95. default: true
  96. },
  97. checkOnClickNode: {
  98. type: Boolean,
  99. default: false
  100. },
  101. checkStrictly: {
  102. type: Boolean,
  103. default: false
  104. },
  105. accordion: {
  106. type: Boolean,
  107. default: false
  108. },
  109. highlightCurrent: {
  110. type: Boolean,
  111. default: false
  112. },
  113. currentNodeKey: {
  114. type: [String, Number],
  115. default: ''
  116. },
  117. indent: {
  118. type: [String, Number],
  119. default: 32
  120. },
  121. iconSize: {
  122. type: [String, Number],
  123. default: 14
  124. },
  125. checkboxSize: {
  126. type: [String, Number],
  127. default: 16
  128. },
  129. expandIcon: {
  130. type: String,
  131. default: 'play-right-fill'
  132. },
  133. collapseIcon: {
  134. type: String,
  135. default: 'arrow-down-fill'
  136. }
  137. },
  138. data() {
  139. return {
  140. treeData: [],
  141. currentKey: '',
  142. nodeMap: {},
  143. privateKeySeed: 0
  144. };
  145. },
  146. computed: {
  147. treeProps() {
  148. return Object.assign({
  149. label: 'label',
  150. children: 'children',
  151. nodeKey: 'id',
  152. disabled: 'disabled'
  153. }, this.props || {});
  154. },
  155. labelKey() {
  156. return this.treeProps.label;
  157. },
  158. childrenKey() {
  159. return this.treeProps.children;
  160. },
  161. disabledKey() {
  162. return this.treeProps.disabled;
  163. },
  164. keyField() {
  165. return this.nodeKey || this.treeProps.nodeKey || 'id';
  166. },
  167. visibleNodes() {
  168. const result = [];
  169. this.collectVisibleNodes(this.treeData, 0, result);
  170. return result;
  171. },
  172. switcherColor() {
  173. return this.upThemeVar('--up-content-color', '#606266');
  174. }
  175. },
  176. watch: {
  177. data: {
  178. handler() {
  179. this.initTree();
  180. },
  181. deep: true,
  182. immediate: true
  183. },
  184. props: {
  185. handler() {
  186. this.initTree();
  187. },
  188. deep: true
  189. },
  190. nodeKey() {
  191. this.initTree();
  192. },
  193. defaultExpandAll() {
  194. this.initTree();
  195. },
  196. defaultExpandedKeys() {
  197. this.initTree();
  198. },
  199. defaultCheckedKeys() {
  200. this.initTree();
  201. },
  202. checkStrictly() {
  203. this.initTree();
  204. },
  205. currentNodeKey: {
  206. handler(value) {
  207. this.currentKey = value;
  208. },
  209. immediate: true
  210. }
  211. },
  212. emits: [
  213. 'node-click',
  214. 'check-change',
  215. 'check',
  216. 'node-expand',
  217. 'node-collapse',
  218. 'current-change'
  219. ],
  220. methods: {
  221. initTree() {
  222. this.privateKeySeed = 0;
  223. this.nodeMap = {};
  224. this.treeData = this.cloneNodes(this.data || [], null, 0);
  225. if (!this.checkStrictly) {
  226. this.syncParentChecked(this.treeData);
  227. }
  228. },
  229. cloneNodes(nodes, parent, level) {
  230. const list = [];
  231. nodes.forEach((node, index) => {
  232. const clone = Object.assign({}, node);
  233. const key = this.resolveNodeKey(clone, parent, index);
  234. const children = Array.isArray(node[this.childrenKey]) ? node[this.childrenKey] : [];
  235. const expanded = this.defaultExpandAll || this.includesKey(this.defaultExpandedKeys, key) || clone.expanded === true;
  236. const checked = this.includesKey(this.defaultCheckedKeys, key) || clone.checked === true;
  237. clone.__uTreeKey = key;
  238. clone.expanded = expanded;
  239. clone.checked = checked;
  240. clone.indeterminate = false;
  241. clone[this.childrenKey] = this.cloneNodes(children, clone, level + 1);
  242. if (checked && !this.checkStrictly) {
  243. this.setChildrenChecked(clone, true);
  244. }
  245. this.nodeMap[key] = {
  246. node: clone,
  247. parent,
  248. level
  249. };
  250. list.push(clone);
  251. });
  252. return list;
  253. },
  254. resolveNodeKey(node, parent, index) {
  255. const rawKey = node[this.keyField];
  256. if (rawKey !== undefined && rawKey !== null && rawKey !== '') {
  257. return rawKey;
  258. }
  259. const parentKey = parent ? parent.__uTreeKey : 'root';
  260. this.privateKeySeed += 1;
  261. return parentKey + '-' + index + '-' + this.privateKeySeed;
  262. },
  263. collectVisibleNodes(nodes, level, result) {
  264. nodes.forEach(node => {
  265. const children = this.getChildren(node);
  266. const key = this.getNodeKey(node);
  267. result.push({
  268. key,
  269. node,
  270. level,
  271. hasChildren: children.length > 0,
  272. expanded: node.expanded === true,
  273. checked: node.checked === true,
  274. indeterminate: node.indeterminate === true,
  275. disabled: this.isNodeDisabled(node)
  276. });
  277. if (children.length > 0 && node.expanded === true) {
  278. this.collectVisibleNodes(children, level + 1, result);
  279. }
  280. });
  281. },
  282. getChildren(node) {
  283. const children = node ? node[this.childrenKey] : [];
  284. return Array.isArray(children) ? children : [];
  285. },
  286. getNodeKey(node) {
  287. return node ? node.__uTreeKey : '';
  288. },
  289. getNodeLabel(node) {
  290. const label = node ? node[this.labelKey] : '';
  291. return label === undefined || label === null ? '' : label;
  292. },
  293. isNodeDisabled(node) {
  294. return !!(node && node[this.disabledKey]);
  295. },
  296. includesKey(keys, key) {
  297. return Array.isArray(keys) && keys.indexOf(key) !== -1;
  298. },
  299. getNodeClass(item) {
  300. const classes = [];
  301. if (this.highlightCurrent && item.key === this.currentKey) {
  302. classes.push('u-tree-node--current');
  303. }
  304. if (item.disabled) {
  305. classes.push('u-tree-node--disabled');
  306. }
  307. return classes.join(' ');
  308. },
  309. getNodeContentStyle(item) {
  310. return {
  311. paddingLeft: this.getIndentValue(item.level)
  312. };
  313. },
  314. getIndentValue(level) {
  315. const indent = String(this.indent);
  316. const matched = indent.match(/^(\d+(?:\.\d+)?)(.*)$/);
  317. if (!matched) {
  318. return indent;
  319. }
  320. const unit = matched[2] || 'rpx';
  321. return (Number(matched[1]) * level) + unit;
  322. },
  323. handleNodeClick(item) {
  324. const node = item.node;
  325. const oldCurrentNode = this.getCurrentNode();
  326. this.currentKey = item.key;
  327. if (this.expandOnClickNode && item.hasChildren) {
  328. this.toggleExpand(item);
  329. }
  330. if (this.checkOnClickNode && this.showCheckbox && !item.disabled) {
  331. this.setNodeChecked(node, !node.checked, true);
  332. this.emitCheck(node);
  333. }
  334. this.$emit('node-click', node);
  335. if (oldCurrentNode !== node) {
  336. this.$emit('current-change', node, oldCurrentNode);
  337. }
  338. },
  339. handleExpandClick(item) {
  340. if (!item.hasChildren) {
  341. return;
  342. }
  343. this.toggleExpand(item);
  344. },
  345. toggleExpand(item) {
  346. const node = item.node;
  347. const nextExpanded = node.expanded !== true;
  348. if (this.accordion && nextExpanded) {
  349. this.collapseSiblingNodes(node);
  350. }
  351. node.expanded = nextExpanded;
  352. this.$emit(nextExpanded ? 'node-expand' : 'node-collapse', node);
  353. },
  354. collapseSiblingNodes(node) {
  355. const parent = this.getParentNode(node);
  356. const siblings = parent ? this.getChildren(parent) : this.treeData;
  357. siblings.forEach(sibling => {
  358. if (sibling !== node) {
  359. sibling.expanded = false;
  360. }
  361. });
  362. },
  363. handleCheckboxChange(item, checked) {
  364. if (item.disabled) {
  365. return;
  366. }
  367. this.setNodeChecked(item.node, checked, true);
  368. this.$emit('check-change', item.node, checked);
  369. this.emitCheck(item.node);
  370. },
  371. setNodeChecked(node, checked, deep) {
  372. node.checked = checked;
  373. node.indeterminate = false;
  374. if (!this.checkStrictly && deep !== false) {
  375. this.setChildrenChecked(node, checked);
  376. }
  377. if (!this.checkStrictly) {
  378. this.updateParentChecked(node);
  379. }
  380. },
  381. setChildrenChecked(node, checked) {
  382. this.getChildren(node).forEach(child => {
  383. if (!this.isNodeDisabled(child)) {
  384. child.checked = checked;
  385. child.indeterminate = false;
  386. this.setChildrenChecked(child, checked);
  387. }
  388. });
  389. },
  390. updateParentChecked(node) {
  391. const parent = this.getParentNode(node);
  392. if (!parent) {
  393. return;
  394. }
  395. const children = this.getChildren(parent).filter(child => !this.isNodeDisabled(child));
  396. const allChecked = children.length > 0 && children.every(child => child.checked === true);
  397. const someChecked = children.some(child => child.checked === true || child.indeterminate === true);
  398. parent.checked = allChecked;
  399. parent.indeterminate = !allChecked && someChecked;
  400. this.updateParentChecked(parent);
  401. },
  402. syncParentChecked(nodes) {
  403. nodes.forEach(node => {
  404. const children = this.getChildren(node);
  405. if (children.length > 0) {
  406. this.syncParentChecked(children);
  407. const enabledChildren = children.filter(child => !this.isNodeDisabled(child));
  408. const allChecked = enabledChildren.length > 0 && enabledChildren.every(child => child.checked === true);
  409. const someChecked = enabledChildren.some(child => child.checked === true || child.indeterminate === true);
  410. node.checked = allChecked;
  411. node.indeterminate = !allChecked && someChecked;
  412. }
  413. });
  414. },
  415. getParentNode(node) {
  416. const mapItem = this.nodeMap[this.getNodeKey(node)];
  417. return mapItem ? mapItem.parent : null;
  418. },
  419. walkNodes(nodes, callback) {
  420. nodes.forEach(node => {
  421. callback(node);
  422. this.walkNodes(this.getChildren(node), callback);
  423. });
  424. },
  425. getNodeByKey(key) {
  426. const mapItem = this.nodeMap[key];
  427. return mapItem ? mapItem.node : null;
  428. },
  429. getCheckedNodes(leafOnly) {
  430. const result = [];
  431. this.walkNodes(this.treeData, node => {
  432. const isLeaf = this.getChildren(node).length === 0;
  433. if (node.checked === true && (!leafOnly || isLeaf)) {
  434. result.push(node);
  435. }
  436. });
  437. return result;
  438. },
  439. getCheckedKeys(leafOnly) {
  440. return this.getCheckedNodes(leafOnly).map(node => this.getNodeKey(node));
  441. },
  442. getHalfCheckedNodes() {
  443. const result = [];
  444. this.walkNodes(this.treeData, node => {
  445. if (node.indeterminate === true) {
  446. result.push(node);
  447. }
  448. });
  449. return result;
  450. },
  451. getHalfCheckedKeys() {
  452. return this.getHalfCheckedNodes().map(node => this.getNodeKey(node));
  453. },
  454. setCheckedKeys(keys, leafOnly) {
  455. const checkedKeys = Array.isArray(keys) ? keys : [];
  456. this.walkNodes(this.treeData, node => {
  457. node.checked = false;
  458. node.indeterminate = false;
  459. });
  460. checkedKeys.forEach(key => {
  461. const node = this.getNodeByKey(key);
  462. if (node) {
  463. const isLeaf = this.getChildren(node).length === 0;
  464. if (!leafOnly || isLeaf) {
  465. this.setNodeChecked(node, true, !this.checkStrictly);
  466. }
  467. }
  468. });
  469. if (!this.checkStrictly) {
  470. this.syncParentChecked(this.treeData);
  471. }
  472. },
  473. setChecked(key, checked, deep) {
  474. const node = typeof key === 'object' ? key : this.getNodeByKey(key);
  475. if (node) {
  476. this.setNodeChecked(node, checked, deep);
  477. }
  478. },
  479. setCurrentKey(key) {
  480. this.currentKey = key;
  481. },
  482. getCurrentKey() {
  483. return this.currentKey;
  484. },
  485. getCurrentNode() {
  486. return this.getNodeByKey(this.currentKey);
  487. },
  488. emitCheck(node) {
  489. this.$emit('check', node, {
  490. checkedNodes: this.getCheckedNodes(false),
  491. checkedKeys: this.getCheckedKeys(false),
  492. halfCheckedNodes: this.getHalfCheckedNodes(),
  493. halfCheckedKeys: this.getHalfCheckedKeys()
  494. });
  495. }
  496. }
  497. };
  498. </script>
  499. <style lang="scss" scoped>
  500. .u-tree {
  501. font-size: 28rpx;
  502. color: var(--up-main-color, #303133);
  503. }
  504. .u-tree-node__content {
  505. display: flex;
  506. flex-direction: row;
  507. align-items: center;
  508. min-height: 72rpx;
  509. box-sizing: border-box;
  510. border-radius: 8rpx;
  511. }
  512. .u-tree-node--current .u-tree-node__content {
  513. background-color: var(--up-primary-light, #ecf5ff);
  514. }
  515. .u-tree-node--disabled {
  516. opacity: 0.55;
  517. }
  518. .u-tree-node__switcher {
  519. width: 36rpx;
  520. height: 36rpx;
  521. display: flex;
  522. flex-direction: row;
  523. align-items: center;
  524. justify-content: center;
  525. flex-shrink: 0;
  526. }
  527. .u-tree-node__checkbox {
  528. margin-left: 8rpx;
  529. margin-right: 8rpx;
  530. }
  531. .u-tree-node__label {
  532. flex: 1;
  533. min-width: 0;
  534. display: flex;
  535. flex-direction: row;
  536. align-items: center;
  537. }
  538. .u-tree-node__text {
  539. color: var(--up-main-color, #303133);
  540. font-size: 28rpx;
  541. line-height: 40rpx;
  542. }
  543. </style>