FamilyEnergyBar.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. <template>
  2. <view class="family-energy-bar">
  3. <view class="bar-header" @click="toggleMembers">
  4. <text class="bar-icon">{{ dimensionIcon }}</text>
  5. <text class="bar-title">{{ dimensionName }}能量</text>
  6. <text class="bar-total-score">{{ familyScore }}</text>
  7. <text class="bar-expand-icon">{{ showMembers ? '▼' : '▶' }}</text>
  8. </view>
  9. <view class="bar-list" v-if="showMembers">
  10. <!-- 非堆叠模式:单色条 -->
  11. <template v-if="!isStacked">
  12. <view class="bar-row bar-row-family">
  13. <text class="bar-label">全家</text>
  14. <view class="bar-track">
  15. <view class="bar-fill" :style="{ width: familyScore + '%', background: dimensionColor }"></view>
  16. </view>
  17. <text class="bar-value">{{ familyScore }}</text>
  18. </view>
  19. <view
  20. class="bar-row bar-row-member"
  21. v-for="member in members"
  22. :key="member.memberId"
  23. @click="onMemberClick(member)"
  24. >
  25. <text class="bar-label">{{ member.name }}</text>
  26. <view class="bar-track">
  27. <view class="bar-fill" :style="{ width: getMemberScore(member) + '%', background: memberColor(member) }"></view>
  28. </view>
  29. <text class="bar-value">{{ getMemberScore(member) }}</text>
  30. <text class="bar-arrow">›</text>
  31. </view>
  32. </template>
  33. <!-- 堆叠模式:主维度 + 副维度两段色 -->
  34. <template v-else>
  35. <view class="bar-row bar-row-family">
  36. <text class="bar-label">全家</text>
  37. <view class="bar-track stacked-track">
  38. <view class="bar-fill bar-fill-primary" :style="{ width: (sandboxData[dualDimension.primary.code + 'Score'] || 0) + '%', background: dualDimension.primary.color || '#8B5CF6' }"></view>
  39. <view class="bar-fill bar-fill-secondary" :style="{ width: (sandboxData[dualDimension.secondary.code + 'Score'] || 0) + '%', background: dualDimension.secondary.color || '#3B82F6' }"></view>
  40. </view>
  41. <text class="bar-value">{{ familyScore }}</text>
  42. </view>
  43. <view
  44. class="bar-row bar-row-member"
  45. v-for="member in members"
  46. :key="member.memberId"
  47. @click="onMemberClick(member)"
  48. >
  49. <text class="bar-label">{{ member.name }}</text>
  50. <view class="bar-track stacked-track">
  51. <view class="bar-fill bar-fill-primary" :style="{ width: getMemberPrimaryScore(member) + '%', background: dualDimension.primary.color || '#8B5CF6' }"></view>
  52. <view class="bar-fill bar-fill-secondary" :style="{ width: getMemberSecondaryScore(member) + '%', background: dualDimension.secondary.color || '#3B82F6' }"></view>
  53. </view>
  54. <text class="bar-value">{{ getMemberScore(member) }}</text>
  55. <text class="bar-arrow">›</text>
  56. </view>
  57. <!-- 图例 -->
  58. <view class="stacked-legend">
  59. <view class="legend-item">
  60. <view class="legend-dot" :style="{ background: dualDimension.primary.color || '#8B5CF6' }"></view>
  61. <text class="legend-label">{{ dualDimension.primary.label || '心' }}</text>
  62. </view>
  63. <view class="legend-item">
  64. <view class="legend-dot" :style="{ background: dualDimension.secondary.color || '#3B82F6' }"></view>
  65. <text class="legend-label">{{ dualDimension.secondary.label || '智' }}</text>
  66. </view>
  67. </view>
  68. </template>
  69. </view>
  70. </view>
  71. </template>
  72. <script>
  73. export default {
  74. props: {
  75. dimensionCode: { type: String, required: true },
  76. sandboxData: { type: Object, default: null },
  77. dualDimension: { type: [String, Object], default: '' }
  78. },
  79. data: function() {
  80. return {
  81. showMembers: false
  82. }
  83. },
  84. computed: {
  85. isStacked: function() {
  86. return typeof this.dualDimension === 'object' && this.dualDimension !== null && this.dualDimension.primary && this.dualDimension.secondary
  87. },
  88. dimensionConfig: function() {
  89. var maps = {
  90. body: { name: '身体', icon: '\u{1F3C3}', color: '#10B981' },
  91. mind: { name: '心', icon: '\u2764', color: '#8B5CF6' },
  92. wisdom: { name: '智', icon: '\u{1F9E0}', color: '#3B82F6' },
  93. action: { name: '行动', icon: '\u{1F3CB}', color: '#F97316' },
  94. wealth: { name: '富', icon: '\u{1F4B0}', color: '#F59E0B' }
  95. }
  96. return maps[this.dimensionCode] || { name: '', icon: '', color: '#999' }
  97. },
  98. dimensionIcon: function() { return this.dimensionConfig.icon },
  99. dimensionName: function() { return this.dimensionConfig.name },
  100. dimensionColor: function() { return this.dimensionConfig.color },
  101. familyScore: function() {
  102. if (!this.sandboxData) return 0
  103. if (this.isStacked) {
  104. var primary = this.sandboxData[this.dualDimension.primary.code + 'Score'] || 0
  105. var secondary = this.sandboxData[this.dualDimension.secondary.code + 'Score'] || 0
  106. return primary + secondary
  107. }
  108. return this.sandboxData[this.dimensionCode + 'Score'] || 0
  109. },
  110. members: function() {
  111. return this.sandboxData && this.sandboxData.members ? this.sandboxData.members : []
  112. }
  113. },
  114. methods: {
  115. toggleMembers: function() {
  116. this.showMembers = !this.showMembers
  117. },
  118. getMemberScore: function(member) {
  119. if (this.isStacked) {
  120. var primary = member[this.dualDimension.primary.code + 'Score'] || 0
  121. var secondary = member[this.dualDimension.secondary.code + 'Score'] || 0
  122. return primary + secondary
  123. }
  124. return member[this.dimensionCode + 'Score'] || 0
  125. },
  126. getMemberPrimaryScore: function(member) {
  127. if (!this.isStacked) return 0
  128. return member[this.dualDimension.primary.code + 'Score'] || 0
  129. },
  130. getMemberSecondaryScore: function(member) {
  131. if (!this.isStacked) return 0
  132. return member[this.dualDimension.secondary.code + 'Score'] || 0
  133. },
  134. memberColor: function(member) {
  135. var score = this.getMemberScore(member)
  136. if (score >= 80) return '#10B981'
  137. if (score >= 60) return '#3B82F6'
  138. if (score >= 40) return '#F97316'
  139. return '#EF4444'
  140. },
  141. onMemberClick: function(member) {
  142. var self = this
  143. uni.navigateTo({
  144. url: '/pages/member-detail/member-detail?memberId=' + member.memberId +
  145. '&memberType=' + (member.memberType || 'child') +
  146. '&memberName=' + encodeURIComponent(member.name) +
  147. '&dimensionCode=' + self.dimensionCode +
  148. '&entrySource=body-card'
  149. })
  150. }
  151. }
  152. }
  153. </script>
  154. <style scoped>
  155. .family-energy-bar {
  156. margin: 20rpx 30rpx;
  157. background: #fff;
  158. border-radius: 24rpx;
  159. padding: 28rpx 24rpx;
  160. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
  161. }
  162. .bar-header {
  163. display: flex;
  164. flex-direction: row;
  165. align-items: center;
  166. margin-bottom: 20rpx;
  167. }
  168. .bar-icon { font-size: 32rpx; margin-right: 8rpx; }
  169. .bar-title { font-size: 28rpx; font-weight: bold; color: #333; flex: 1; }
  170. .bar-total-score { font-size: 40rpx; font-weight: bold; color: #F97316; margin-right: 12rpx; }
  171. .bar-expand-icon {
  172. font-size: 20rpx;
  173. color: #bbb;
  174. transition: transform 0.3s ease;
  175. }
  176. .bar-row {
  177. display: flex;
  178. flex-direction: row;
  179. align-items: center;
  180. margin-bottom: 16rpx;
  181. padding: 8rpx 0;
  182. }
  183. .bar-row:last-child { margin-bottom: 0; }
  184. .bar-row-member:active { opacity: 0.6; }
  185. .bar-label {
  186. font-size: 24rpx;
  187. color: #666;
  188. width: 80rpx;
  189. flex-shrink: 0;
  190. }
  191. .bar-track {
  192. flex: 1;
  193. height: 24rpx;
  194. background: #f0f0f0;
  195. border-radius: 12rpx;
  196. overflow: hidden;
  197. margin: 0 12rpx;
  198. }
  199. .bar-fill {
  200. height: 100%;
  201. border-radius: 12rpx;
  202. transition: width 0.5s ease;
  203. }
  204. .bar-value {
  205. font-size: 26rpx;
  206. font-weight: bold;
  207. color: #333;
  208. width: 60rpx;
  209. text-align: right;
  210. flex-shrink: 0;
  211. }
  212. .bar-arrow {
  213. font-size: 28rpx;
  214. color: #ccc;
  215. margin-left: 8rpx;
  216. }
  217. .bar-row-family .bar-fill {
  218. background: linear-gradient(90deg, #F97316, #FB923C);
  219. }
  220. /* ===== 堆叠模式 ===== */
  221. .stacked-track {
  222. display: flex;
  223. flex-direction: row;
  224. overflow: hidden;
  225. }
  226. .bar-fill-primary {
  227. border-radius: 12rpx 0 0 12rpx;
  228. }
  229. .bar-fill-secondary {
  230. border-radius: 0 12rpx 12rpx 0;
  231. margin-left: 2rpx;
  232. }
  233. .stacked-legend {
  234. display: flex;
  235. flex-direction: row;
  236. justify-content: center;
  237. margin-top: 12rpx;
  238. gap: 24rpx;
  239. }
  240. .legend-item {
  241. display: flex;
  242. flex-direction: row;
  243. align-items: center;
  244. }
  245. .legend-dot {
  246. width: 16rpx;
  247. height: 16rpx;
  248. border-radius: 8rpx;
  249. margin-right: 6rpx;
  250. }
  251. .legend-label {
  252. font-size: 22rpx;
  253. color: #999;
  254. }
  255. </style>