tree-node.vue 6.9 KB

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