u-tree.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <template>
  2. <view class="u-tree">
  3. <view class="u-tree__nodes">
  4. <view
  5. v-for="(node, index) in visibleNodes"
  6. :key="index"
  7. class="u-tree__node-wrapper"
  8. :style="[nodeWrapperStyle(node)]"
  9. >
  10. <view
  11. class="u-tree__switcher"
  12. v-if="showSwitcher"
  13. @tap.stop="onToggle(node)"
  14. >
  15. <slot
  16. name="switcher"
  17. :hide="!(hasChildren(node) || (!!loadNode && !isLeaf(node)))"
  18. :loading="isLoading(node)"
  19. :expanded="isExpanded(node)"
  20. >
  21. <u-loading-icon
  22. v-if="isLoading(node)"
  23. mode="circle"
  24. :color="loadingColor"
  25. :size="12"
  26. />
  27. <u-icon
  28. v-else-if="hasChildren(node) || (!!loadNode && !isLeaf(node))"
  29. :name="isExpanded(node) ? collapseIcon : expandIcon"
  30. :size="switcherSize"
  31. :color="switcherColor"
  32. />
  33. </slot>
  34. </view>
  35. <u-checkbox
  36. v-if="checkable"
  37. :size="16"
  38. :checked="isChecked(node)"
  39. :indeterminate="isIndeterminate(node)"
  40. :activeColor="checkedColor"
  41. @change="handleCheck(node)"
  42. ></u-checkbox>
  43. <view
  44. class="u-tree__content"
  45. :style="[contentStyle(node)]"
  46. @tap.stop="onToggle(node)"
  47. >
  48. <slot name="content" :node="node">
  49. <text class="u-tree__label" :style="[labelStyle(node)]">
  50. {{ node[labelField] }}
  51. </text>
  52. </slot>
  53. </view>
  54. </view>
  55. </view>
  56. </view>
  57. </template>
  58. <script>
  59. import props from './props.js';
  60. import mixin from '../../libs/mixin/mixin';
  61. import mpMixin from '../../libs/mixin/mpMixin';
  62. /**
  63. * tree树形组件
  64. * @description 树形结构,支持选择、级联选择、异步加载、搜索过滤、插槽自定义。
  65. * @tutorial https://uview.d3u.cn/components/tree.html
  66. * @property {Array} data 数据源
  67. * @property {String} keyField 节点key
  68. * @property {String} labelField 节点label
  69. * @property {String} childrenField 子节点
  70. * @property {String} isLeafField 是否叶子节点
  71. * @property {String} disabledField 是否禁用
  72. * @property {Array} defaultCheckedKeys 默认选中
  73. * @property {Array} defaultExpandedKeys 默认展开
  74. * @property {Array} checkedKeys 受控的选中项
  75. * @property {Array} expandedKeys 受控的展开项
  76. * @property {Boolean} checkable 是否可选择
  77. * @property {Boolean} selectable 是否可选择
  78. * @property {Boolean} cascade 是否级联
  79. * @property {Boolean} expandOnClick 是否允许点击节点展开/收缩
  80. * @property {Boolean} checkOnClick 是否允许点击节点勾选/取消勾选
  81. * @property {Function} loadNode 异步加载节点数据
  82. * @property {Boolean} allowCheckingNotLoaded 是否允许勾选未加载的节点
  83. * @property {Boolean} showIrrelevantNodes 是否显示搜索无关的节点
  84. * @property {Boolean} showSwitcher 是否显示展开/收缩按钮
  85. * @property {String} expandIcon 展开图标
  86. * @property {String} collapseIcon 收缩图标
  87. * @property {String} loadingColor loading的颜色
  88. * @property {String} checkedColor checkebox选中颜色
  89. * @property {Boolean} rotatableSwitcher 是否可旋转展开/收缩按钮
  90. * @property {String} highlightBgColor 高亮背景颜色
  91. * @property {String} selectedBgColor 选中背景颜色
  92. * @property {Number} switcherSize 展开/收缩按钮大小
  93. * @property {String} switcherColor 展开/收缩按钮颜色
  94. * @property {String} indentWidth 缩进宽度
  95. * @property {String} pattern 搜索词
  96. * @property {String} highlightBgColor 高亮背景颜色
  97. * @property {String} selectedBgColor 选中背景颜色
  98. * @property {Number} switcherSize 展开/收缩按钮大小
  99. * @property {String} switcherColor 展开/收缩按钮颜色
  100. *
  101. * @event {event} click 点击节点
  102. * @event {event} checked 勾选节点
  103. * @event {event} expanded 展开节点
  104. * @event {event} update:checked-keys 更新选中项
  105. * @event {event} update:expanded-keys 更新展开项
  106. *
  107. * @slot {slot} switcher 展开/收缩按钮
  108. * @slot {slot} content 节点内容
  109. *
  110. * @example
  111. * <u-tree :data="data" @click="click" @checked="checked" @expanded="expanded" @update:checked-keys="updateCheckedKeys" @update:expanded-keys="updateExpandedKeys"></u-tree>
  112. */
  113. export default {
  114. name: 'u-tree',
  115. mixins: [mpMixin, mixin, props],
  116. data() {
  117. return {
  118. innerCheckedKeys: this.defaultCheckedKeys
  119. ? [...this.defaultCheckedKeys]
  120. : [],
  121. innerExpandedKeys: this.defaultExpandedKeys
  122. ? [...this.defaultExpandedKeys]
  123. : [],
  124. selectedKey: null,
  125. loadingMap: {},
  126. };
  127. },
  128. computed: {
  129. visibleNodes() {
  130. const result = [];
  131. const filter = (node) => {
  132. if (!this.pattern) return true;
  133. const label = node[this.labelField] + '';
  134. return label
  135. .toLowerCase()
  136. .includes(this.pattern.toLowerCase());
  137. };
  138. const traverse = (nodes, level, parentMatched) => {
  139. if (!nodes || nodes.length === 0) return;
  140. for (const n of nodes) {
  141. const matched = filter(n);
  142. const shouldShow = this.showIrrelevantNodes
  143. ? true
  144. : matched ||
  145. parentMatched ||
  146. this.hasDescendantMatch(n);
  147. if (shouldShow) {
  148. result.push({
  149. ...n,
  150. __level: level,
  151. __matched: matched,
  152. });
  153. if (this.isExpanded(n)) {
  154. traverse(
  155. n[this.childrenField],
  156. level + 1,
  157. parentMatched || matched,
  158. );
  159. }
  160. }
  161. }
  162. };
  163. traverse(this.data, 0, false);
  164. return result;
  165. },
  166. },
  167. watch: {
  168. data: {
  169. handler() {
  170. this.buildNodeIndex();
  171. },
  172. deep: true,
  173. immediate: true,
  174. },
  175. pattern: {
  176. handler() {
  177. this.expandAncestorsForPattern();
  178. },
  179. },
  180. },
  181. // #ifdef VUE3
  182. emits: [
  183. 'click',
  184. 'checked',
  185. 'expanded',
  186. 'update:checked-keys',
  187. 'update:expanded-keys',
  188. ],
  189. // #endif
  190. methods: {
  191. keyOf(node) {
  192. return node[this.keyField];
  193. },
  194. // 受控/非受控展开、勾选 keys 统一获取
  195. getEffectiveExpandedKeys() {
  196. return this.expandedKeys ? this.expandedKeys : this.innerExpandedKeys;
  197. },
  198. getEffectiveCheckedKeys() {
  199. return this.checkedKeys ? this.checkedKeys : this.innerCheckedKeys;
  200. },
  201. ensureNodeIndexesBuilt() {
  202. if (!this._nodeIndex || !this._parentIndex) this.buildNodeIndex();
  203. },
  204. // 通用索引构建器:从任意起点增量构建 key->node 与 key->parentKey
  205. indexNodes(startNodes, parentKey) {
  206. const stack = Array.isArray(startNodes)
  207. ? startNodes.map((n) => ({ node: n, parentKey }))
  208. : [];
  209. while (stack.length) {
  210. const { node: current, parentKey: currentParentKey } = stack.pop();
  211. if (!current) continue;
  212. const currentNodeKey = this.keyOf(current);
  213. if (currentNodeKey) {
  214. this._nodeIndex[currentNodeKey] = current;
  215. if (currentParentKey) this._parentIndex[currentNodeKey] = currentParentKey;
  216. }
  217. const children = current[this.childrenField];
  218. if (Array.isArray(children) && children.length) {
  219. for (let i = 0; i < children.length; i++) {
  220. stack.push({ node: children[i], parentKey: currentNodeKey });
  221. }
  222. }
  223. }
  224. },
  225. buildNodeIndex() {
  226. this._nodeIndex = Object.create(null);
  227. this._parentIndex = Object.create(null);
  228. this.indexNodes(this.data, null);
  229. },
  230. indexSubtree(node) {
  231. if (!node) return;
  232. const children = node[this.childrenField];
  233. if (!Array.isArray(children) || children.length === 0) return;
  234. const parentKey = this.keyOf(node);
  235. this.indexNodes(children, parentKey);
  236. },
  237. // 根据搜索词,自动展开所有命中的子节点的祖先
  238. expandAncestorsForPattern() {
  239. const p = (this.pattern || '').toLowerCase();
  240. if (!p) {
  241. if (this._preSearchExpandedKeys) {
  242. this.setExpandedKeys(this._preSearchExpandedKeys);
  243. this._preSearchExpandedKeys = null;
  244. }
  245. return;
  246. }
  247. this.ensureNodeIndexesBuilt();
  248. if (!this._preSearchExpandedKeys) {
  249. const current = this.getEffectiveExpandedKeys();
  250. this._preSearchExpandedKeys = Array.isArray(current) ? current.slice() : [];
  251. }
  252. const next = new Set(this._preSearchExpandedKeys || []);
  253. for (const nodeKey in this._nodeIndex) {
  254. const nodeRef = this._nodeIndex[nodeKey];
  255. const label = (nodeRef[this.labelField] + '').toLowerCase();
  256. if (label.includes(p)) {
  257. let parentKey = this._parentIndex ? this._parentIndex[nodeKey] : null;
  258. while (parentKey) {
  259. next.add(parentKey);
  260. parentKey = this._parentIndex[parentKey];
  261. }
  262. }
  263. }
  264. this.setExpandedKeys(Array.from(next));
  265. },
  266. childrenOf(node) {
  267. return node[this.childrenField] || [];
  268. },
  269. hasChildren(node) {
  270. const children = node[this.childrenField];
  271. return children && children.length > 0;
  272. },
  273. isLeaf(node) {
  274. const flag = node && node[this.isLeafField];
  275. return flag === true;
  276. },
  277. isExpanded(node) {
  278. const nodeKey = this.keyOf(node);
  279. return new Set(this.getEffectiveExpandedKeys()).has(nodeKey);
  280. },
  281. isChecked(node) {
  282. const nodeKey = this.keyOf(node);
  283. return new Set(this.getEffectiveCheckedKeys()).has(nodeKey);
  284. },
  285. isIndeterminate(node) {
  286. if (!this.cascade) return false;
  287. const children = this.childrenOf(node);
  288. if (!children || children.length === 0) return false;
  289. let checkedCount = 0;
  290. let indeterminateCount = 0;
  291. for (const child of children) {
  292. if (this.isChecked(child)) {
  293. checkedCount++;
  294. } else if (this.isIndeterminate(child)) {
  295. indeterminateCount++;
  296. }
  297. }
  298. // 如果有选中的子节点但不是全部选中,或者有不确定状态的子节点,则当前节点为不确定状态
  299. return (
  300. (checkedCount > 0 && checkedCount < children.length) ||
  301. indeterminateCount > 0
  302. );
  303. },
  304. isSelected(node) {
  305. return this.selectable && this.selectedKey === this.keyOf(node);
  306. },
  307. contentStyle(node) {
  308. const style = {};
  309. if (this.isSelected(node) && this.selectedBgColor) {
  310. style.background = this.selectedBgColor;
  311. }
  312. return style;
  313. },
  314. labelStyle(node) {
  315. const style = {};
  316. if (this.pattern && node.__matched && this.highlightBgColor) {
  317. style.backgroundColor = this.highlightBgColor;
  318. }
  319. return style;
  320. },
  321. nodeWrapperStyle(node) {
  322. return {
  323. paddingLeft: uni.$u.addUnit(
  324. (node.__level || 0) * Number(this.indentWidth),
  325. ),
  326. };
  327. },
  328. onToggle(node) {
  329. const nodeKey = this.keyOf(node);
  330. this.selectedKey = nodeKey;
  331. if (!this.hasChildren(node) && (!this.loadNode || this.isLeaf(node))) return;
  332. const expanded = this.isExpanded(node);
  333. let next = new Set(this.getEffectiveExpandedKeys());
  334. if (expanded){
  335. next.delete(nodeKey);
  336. } else {
  337. next.add(nodeKey);
  338. }
  339. this.setExpandedKeys(Array.from(next));
  340. if (!expanded && this.loadNode && !this.hasChildren(node) && !this.isLeaf(node)) {
  341. this.setLoading(node, true);
  342. this.ensureNodeIndexesBuilt();
  343. const sourceNode = this._nodeIndex[nodeKey];
  344. Promise.resolve(this.loadNode(sourceNode)).finally(() => {
  345. this.indexSubtree(sourceNode);
  346. this.setLoading(node, false);
  347. });
  348. }
  349. },
  350. handleCheck(node) {
  351. if (!!node[this.disabledField]) return;
  352. const nodeKey = this.keyOf(node);
  353. let current = new Set(this.getEffectiveCheckedKeys());
  354. const checked = current.has(nodeKey);
  355. if (checked) {
  356. current.delete(nodeKey);
  357. } else {
  358. current.add(nodeKey);
  359. }
  360. if (this.cascade) {
  361. // 级联对子孙生效
  362. const affectDescendants = (n, value) => {
  363. const descendantKey = this.keyOf(n);
  364. if (value) {
  365. current.add(descendantKey);
  366. } else {
  367. current.delete(descendantKey);
  368. }
  369. for (const childNode of this.childrenOf(n)) {
  370. affectDescendants(childNode, value);
  371. }
  372. };
  373. // 级联对祖先生效
  374. const affectAncestors = (n) => {
  375. const parent = this.findParent(
  376. this.data,
  377. this.keyOf(n),
  378. );
  379. if (!parent) return;
  380. const parentKey = this.keyOf(parent);
  381. const siblings = this.childrenOf(parent);
  382. let allChecked = true;
  383. let anyChecked = false;
  384. for (const sibling of siblings) {
  385. const siblingKey = this.keyOf(sibling);
  386. if (current.has(siblingKey)) {
  387. anyChecked = true;
  388. } else {
  389. allChecked = false;
  390. }
  391. }
  392. if (allChecked) {
  393. current.add(parentKey);
  394. } else {
  395. current.delete(parentKey);
  396. }
  397. // 递归处理上级父节点
  398. affectAncestors(parent);
  399. };
  400. // 先处理子孙节点
  401. affectDescendants(node, !checked);
  402. // 再处理祖先节点
  403. affectAncestors(node);
  404. }
  405. this.setCheckedKeys(Array.from(current));
  406. },
  407. getSiblings(node) {
  408. const nodeKey = this.keyOf(node);
  409. const res = this.findParentAndSiblings(this.data, null, nodeKey);
  410. return res && res.siblings ? res.siblings : [];
  411. },
  412. findParentAndSiblings(nodes, parent, targetKey) {
  413. if (!nodes) return null;
  414. for (const n of nodes) {
  415. const nodeKey = this.keyOf(n);
  416. if (nodeKey === targetKey) {
  417. return {
  418. parent,
  419. siblings: parent
  420. ? (parent[this.childrenField] || []).filter(
  421. (sibling) => this.keyOf(sibling) !== targetKey,
  422. )
  423. : [],
  424. };
  425. }
  426. const found = this.findParentAndSiblings(
  427. n[this.childrenField],
  428. n,
  429. targetKey,
  430. );
  431. if (found) return found;
  432. }
  433. return null;
  434. },
  435. findParent(nodes, targetKey, parent = null) {
  436. if (!nodes) return null;
  437. for (const n of nodes) {
  438. const nodeKey = this.keyOf(n);
  439. if (nodeKey === targetKey) {
  440. return parent;
  441. }
  442. const found = this.findParent(
  443. n[this.childrenField],
  444. targetKey,
  445. n,
  446. );
  447. if (found) return found;
  448. }
  449. return null;
  450. },
  451. hasDescendantMatch(node) {
  452. if (!this.pattern) return false;
  453. const stack = [...(this.childrenOf(node) || [])];
  454. while (stack.length) {
  455. const n = stack.pop();
  456. if (!n) continue;
  457. const label = (n[this.labelField] + '').toLowerCase();
  458. if (label.includes(this.pattern.toLowerCase())) return true;
  459. stack.push(...(this.childrenOf(n) || []));
  460. }
  461. return false;
  462. },
  463. setLoading(node, value) {
  464. const key = this.keyOf(node);
  465. if (!this.loadingMap) this.loadingMap = {};
  466. if (this.$set) {
  467. this.$set(this.loadingMap, key, value);
  468. } else {
  469. this.loadingMap = { ...this.loadingMap, [key]: value };
  470. }
  471. },
  472. isLoading(node) {
  473. const key = this.keyOf(node);
  474. return !!(this.loadingMap && this.loadingMap[key]);
  475. },
  476. setExpandedKeys(next) {
  477. if (this.expandedKeys) {
  478. this.$emit('update:expanded-keys', next);
  479. this.$emit('expanded', next);
  480. } else {
  481. this.innerExpandedKeys = next;
  482. this.$emit('expanded', next);
  483. }
  484. },
  485. setCheckedKeys(next) {
  486. if (this.checkedKeys) {
  487. this.$emit('update:checked-keys', next);
  488. this.$emit('checked', next);
  489. } else {
  490. this.innerCheckedKeys = next;
  491. this.$emit('checked', next);
  492. }
  493. },
  494. },
  495. };
  496. </script>
  497. <style lang="scss" scoped>
  498. @import '../../libs/css/components.scss';
  499. .u-tree {
  500. &__nodes {
  501. @include flex(column);
  502. }
  503. &__node-wrapper {
  504. @include flex(row);
  505. align-items: center;
  506. }
  507. &__switcher {
  508. margin-right: 6px;
  509. }
  510. &__content {
  511. flex: 1;
  512. border-radius: 4px;
  513. padding: 2px 0px;
  514. }
  515. &__label {
  516. //#ifndef APP-NVUE
  517. display: inline-block;
  518. //#endif
  519. font-size: 14px;
  520. border-radius: 4px;
  521. padding: 2px 0px;
  522. }
  523. }
  524. </style>