TreeNode.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <template>
  2. <view class="tree-node" :style="{ paddingLeft: paddingLeft }">
  3. <!-- 节点主体:头像、昵称、层级、消费额、我得CF,点击展开/折叠 -->
  4. <view class="node-main" @click="toggle">
  5. <view class="avatar-wrap">
  6. <image v-if="node.avatar" class="node-avatar" :src="node.avatar" mode="aspectFill" lazy-load />
  7. <text v-else class="node-avatar-text">{{ avatarInitial }}</text>
  8. </view>
  9. <view class="node-body">
  10. <view class="node-header-row">
  11. <text class="node-name">{{ node.nickname || '微信用户' }}</text>
  12. <text class="node-tag" :class="'node-tag-L' + nodeLevel">L{{ nodeLevel }}</text>
  13. </view>
  14. <view class="node-stats">
  15. <text class="node-stat consumption">消费 ¥{{ consumptionText }}</text>
  16. <text class="node-stat my-cf">我得CF ¥{{ cfText }}</text>
  17. </view>
  18. </view>
  19. <view class="node-toggle" v-if="hasChildren">
  20. <text class="toggle-icon">{{ toggleIcon }}</text>
  21. </view>
  22. </view>
  23. <!-- 子节点:父容器为 column-reverse,子节点在上方渲染,形成“从下往上生长”的层级树 -->
  24. <view class="node-children" v-if="expanded && hasChildren">
  25. <TreeNode
  26. v-for="child in node.children"
  27. :key="child.userId"
  28. :node="child"
  29. :level="childLevel"
  30. :maxLevel="maxLevel"
  31. />
  32. </view>
  33. </view>
  34. </template>
  35. <script>
  36. import TreeNode from './TreeNode'
  37. export default {
  38. name: 'TreeNode',
  39. components: { TreeNode },
  40. props: {
  41. node: {
  42. type: Object,
  43. default: function() {
  44. return {}
  45. }
  46. },
  47. level: {
  48. type: Number,
  49. default: 1
  50. },
  51. maxLevel: {
  52. type: Number,
  53. default: 3
  54. }
  55. },
  56. data: function() {
  57. return {
  58. expanded: false
  59. }
  60. },
  61. computed: {
  62. childLevel: function() {
  63. return this.level + 1
  64. },
  65. nodeLevel: function() {
  66. return this.node && this.node.level ? this.node.level : this.level
  67. },
  68. avatarInitial: function() {
  69. var name = this.node && this.node.nickname ? String(this.node.nickname) : ''
  70. return name.charAt(0) || '?'
  71. },
  72. hasChildren: function() {
  73. var children = this.node && this.node.children
  74. return Array.isArray(children) && children.length > 0
  75. },
  76. toggleIcon: function() {
  77. return this.expanded ? '▲' : '▼'
  78. },
  79. consumptionText: function() {
  80. var fen = this.node && this.node.memberConsumption != null ? this.node.memberConsumption : 0
  81. return (fen / 100).toFixed(2)
  82. },
  83. cfText: function() {
  84. var fen = this.node && this.node.myCfEarnedFromMember != null ? this.node.myCfEarnedFromMember : 0
  85. return (fen / 100).toFixed(2)
  86. },
  87. paddingLeft: function() {
  88. return this.level > 1 ? (this.level - 1) * 24 + 'rpx' : '0rpx'
  89. }
  90. },
  91. methods: {
  92. toggle: function() {
  93. this.expanded = !this.expanded
  94. }
  95. }
  96. }
  97. </script>
  98. <style scoped lang="scss">
  99. .tree-node {
  100. margin-bottom: 16rpx;
  101. }
  102. .node-main {
  103. display: flex;
  104. align-items: center;
  105. gap: 16rpx;
  106. background: $app-surface;
  107. border-radius: 16rpx;
  108. padding: 20rpx;
  109. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
  110. }
  111. .avatar-wrap {
  112. width: 60rpx;
  113. height: 60rpx;
  114. border-radius: 50%;
  115. background: $status-warning-soft;
  116. display: flex;
  117. align-items: center;
  118. justify-content: center;
  119. margin-right: 0;
  120. overflow: hidden;
  121. flex-shrink: 0;
  122. }
  123. .node-avatar {
  124. width: 60rpx;
  125. height: 60rpx;
  126. border-radius: 50%;
  127. }
  128. .node-avatar-text {
  129. width: 60rpx;
  130. height: 60rpx;
  131. border-radius: 50%;
  132. background: $status-warning-soft;
  133. display: flex;
  134. align-items: center;
  135. justify-content: center;
  136. font-size: 24rpx;
  137. color: $status-warning-deep;
  138. flex-shrink: 0;
  139. }
  140. .node-body {
  141. flex: 1;
  142. min-width: 0;
  143. }
  144. .node-header-row {
  145. display: flex;
  146. align-items: center;
  147. gap: 10rpx;
  148. margin-bottom: 6rpx;
  149. }
  150. .node-name {
  151. flex: 1;
  152. min-width: 0;
  153. font-size: 26rpx;
  154. color: $app-text;
  155. overflow: hidden;
  156. text-overflow: ellipsis;
  157. white-space: nowrap;
  158. }
  159. .node-tag {
  160. font-size: 20rpx;
  161. padding: 2rpx 8rpx;
  162. border-radius: 8rpx;
  163. flex-shrink: 0;
  164. }
  165. .node-tag-L1 {
  166. color: $brand-primary-deep;
  167. background: $status-warning-soft;
  168. }
  169. .node-tag-L2 {
  170. color: $status-success-deep;
  171. background: $status-success-tint;
  172. }
  173. .node-tag-L3 {
  174. color: $brand-primary-deep;
  175. background: $brand-primary-tint-2;
  176. }
  177. .node-stats {
  178. flex-shrink: 0;
  179. text-align: right;
  180. margin-right: 8rpx;
  181. line-height: 1.5;
  182. }
  183. .node-stat {
  184. font-size: 20rpx;
  185. display: block;
  186. }
  187. .node-stat.consumption {
  188. color: $status-warning-deep;
  189. }
  190. .node-stat.my-cf {
  191. color: $status-success-strong;
  192. }
  193. .node-toggle {
  194. font-size: 20rpx;
  195. color: $app-text-subtle;
  196. flex-shrink: 0;
  197. }
  198. .node-children {
  199. margin-top: 12rpx;
  200. padding-left: 30rpx;
  201. border-left: 1rpx dashed $brand-primary-light-2;
  202. }
  203. </style>