tree-node.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <template>
  2. <view class="tree-item">
  3. <view
  4. class="head"
  5. @click.stop="changeShow"
  6. style="padding-left: 10rpx"
  7. :class="{ active: isItemActive(item) }"
  8. >
  9. <!-- -->
  10. <text class="txt">{{ item[defaultProps.label] }}</text>
  11. <view
  12. class="left-icon"
  13. :class="show ? 'rt45' : ''"
  14. v-if="hasChildren"
  15. ></view>
  16. </view>
  17. <view class="content" v-if="shouldRecurse">
  18. <view
  19. v-for="(node, index) in nodes"
  20. :key="node._uid"
  21. class="tree-node-item"
  22. >
  23. <view
  24. class="head"
  25. @click.stop="toggleNode(index)"
  26. :style="{ paddingLeft: (node._level + 1) * 10 + 20 + 'rpx' }"
  27. :class="{ active: isItemActive(node) }"
  28. >
  29. <text class="txt">{{ node[defaultProps.label] }}</text>
  30. <view
  31. class="left-icon"
  32. :class="node._isExpanded ? 'rt45' : ''"
  33. v-if="getNodeChildren(node).length > 0"
  34. ></view>
  35. </view>
  36. </view>
  37. </view>
  38. </view>
  39. </template>
  40. <script>
  41. export default {
  42. name: "TreeNode",
  43. componentName: "TreeNode",
  44. props: {
  45. item: {
  46. type: Object,
  47. default: function () {
  48. return {};
  49. },
  50. },
  51. defaultProps: {
  52. type: Object,
  53. default: function () {
  54. return {
  55. id: "id",
  56. children: "children",
  57. label: "label",
  58. };
  59. },
  60. },
  61. selectedNodeId: {
  62. type: [String, Number],
  63. default: null,
  64. },
  65. },
  66. computed: {
  67. hasChildren: function () {
  68. return (
  69. this.item[this.defaultProps.children] &&
  70. this.item[this.defaultProps.children].length > 0
  71. );
  72. },
  73. shouldRecurse: function () {
  74. return this.hasChildren && this.show;
  75. },
  76. },
  77. data: function () {
  78. return {
  79. show: false,
  80. nodes: [], // 扁平化后的节点列表
  81. };
  82. },
  83. methods: {
  84. // 判断节点是否被选中
  85. isItemActive: function (node) {
  86. if (!this.selectedNodeId || !node) return false;
  87. return node[this.defaultProps.id] === this.selectedNodeId;
  88. },
  89. // 获取节点的子节点
  90. getNodeChildren: function (node) {
  91. if (node && node[this.defaultProps.children]) {
  92. return node[this.defaultProps.children];
  93. }
  94. return [];
  95. },
  96. // 切换节点展开/折叠状态
  97. toggleNode: function (index) {
  98. var node = this.nodes[index];
  99. if (node) {
  100. // 触发节点点击事件
  101. this.$emit("node-click", node);
  102. // 如果有子节点,切换展开状态
  103. if (this.getNodeChildren(node).length > 0) {
  104. // 更新节点状态
  105. this.$set(node, "_isExpanded", !node._isExpanded);
  106. // 触发展开/折叠事件
  107. this.$emit(node._isExpanded ? "node-expand" : "node-collapse", node);
  108. // 重新构建扁平节点
  109. this.buildFlatNodes();
  110. }
  111. }
  112. },
  113. // 根节点的展开/折叠
  114. changeShow: function () {
  115. // 触发根节点点击事件
  116. this.$emit("node-click", this.item);
  117. if (this.hasChildren) {
  118. this.show = !this.show;
  119. // 触发展开/折叠事件
  120. this.$emit(this.show ? "node-expand" : "node-collapse", this.item);
  121. // 当展开时,构建扁平节点列表
  122. if (this.show) {
  123. this.buildFlatNodes();
  124. } else {
  125. this.nodes = [];
  126. }
  127. }
  128. },
  129. // 构建扁平的节点列表
  130. buildFlatNodes: function () {
  131. var result = [];
  132. var children = this.item[this.defaultProps.children] || [];
  133. // 添加第一层子节点
  134. for (var i = 0; i < children.length; i++) {
  135. var node = children[i];
  136. if (!node._uid) {
  137. this.$set(node, "_uid", this.generateUID());
  138. }
  139. if (typeof node._level === "undefined") {
  140. this.$set(node, "_level", 0);
  141. }
  142. if (typeof node._isExpanded === "undefined") {
  143. this.$set(node, "_isExpanded", false);
  144. }
  145. result.push(node);
  146. // 如果节点已展开且有子节点,递归添加子节点
  147. if (node._isExpanded) {
  148. this.addChildrenToResult(node, result, 1);
  149. }
  150. }
  151. this.nodes = result;
  152. },
  153. // 递归添加子节点到结果数组
  154. addChildrenToResult: function (parentNode, result, level) {
  155. var children = this.getNodeChildren(parentNode);
  156. for (var i = 0; i < children.length; i++) {
  157. var child = children[i];
  158. if (!child._uid) {
  159. this.$set(child, "_uid", this.generateUID());
  160. }
  161. this.$set(child, "_level", level);
  162. if (typeof child._isExpanded === "undefined") {
  163. this.$set(child, "_isExpanded", false);
  164. }
  165. result.push(child);
  166. // 如果子节点已展开且有子节点,继续递归
  167. if (child._isExpanded && this.getNodeChildren(child).length > 0) {
  168. this.addChildrenToResult(child, result, level + 1);
  169. }
  170. }
  171. },
  172. // 生成唯一ID
  173. generateUID: function () {
  174. return (
  175. Math.random().toString(36).substring(2, 15) +
  176. Math.random().toString(36).substring(2, 15)
  177. );
  178. },
  179. },
  180. };
  181. </script>
  182. <style scoped lang="scss">
  183. @mixin animate2 {
  184. -moz-transition: all 0.2s linear;
  185. -webkit-transition: all 0.2s linear;
  186. -o-transition: all 0.2s linear;
  187. -ms-transition: all 0.2s linear;
  188. transition: all 0.2s linear;
  189. }
  190. .tree-item {
  191. .head {
  192. display: flex;
  193. align-items: center;
  194. line-height: 60rpx;
  195. min-height: 60rpx;
  196. position: relative;
  197. // padding-right: 50rpx;
  198. &.active {
  199. background-color: #fff;
  200. }
  201. .txt {
  202. flex: 1;
  203. white-space: nowrap;
  204. overflow: hidden;
  205. text-overflow: ellipsis;
  206. width: 100%;
  207. height: 84rpx;
  208. line-height: 84rpx;
  209. color: #666;
  210. font-family: "PingFang SC";
  211. font-size: 28rpx;
  212. font-weight: 400;
  213. }
  214. }
  215. .left-icon {
  216. width: 30rpx;
  217. height: 30rpx;
  218. flex-shrink: 0;
  219. position: absolute;
  220. right: 10rpx;
  221. top: 50%;
  222. transform: translateY(-50%) rotate(-90deg);
  223. -ms-transform: translateY(-50%) rotate(-90deg);
  224. -moz-transform: translateY(-50%) rotate(-90deg);
  225. -webkit-transform: translateY(-50%) rotate(-90deg);
  226. -o-transform: translateY(-50%) rotate(-90deg);
  227. @include animate2;
  228. &::before {
  229. content: "";
  230. position: absolute;
  231. top: 50%;
  232. left: 50%;
  233. margin-top: -6rpx;
  234. margin-left: -6rpx;
  235. width: 0;
  236. height: 0;
  237. border-left: 6rpx solid transparent;
  238. border-right: 6rpx solid transparent;
  239. border-top: 12rpx solid #999;
  240. }
  241. &.rt45 {
  242. transform: translateY(-50%) rotate(0deg);
  243. -ms-transform: translateY(-50%) rotate(0deg);
  244. -moz-transform: translateY(-50%) rotate(0deg);
  245. -webkit-transform: translateY(-50%) rotate(0deg);
  246. -o-transform: translateY(-50%) rotate(0deg);
  247. }
  248. }
  249. .content {
  250. width: 100%;
  251. }
  252. .tree-node-item {
  253. width: 100%;
  254. }
  255. }
  256. </style>