FamilyEnergyBar.vue 8.2 KB

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