FamilyEnergyBar.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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 || fallbackPrimaryColor }"></view>
  39. <view class="bar-fill bar-fill-secondary" :style="{ width: (sandboxData[dualDimension.secondary.code + 'Score'] || 0) + '%', background: dualDimension.secondary.color || fallbackSecondaryColor }"></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 || fallbackPrimaryColor }"></view>
  52. <view class="bar-fill bar-fill-secondary" :style="{ width: getMemberSecondaryScore(member) + '%', background: dualDimension.secondary.color || fallbackSecondaryColor }"></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 || fallbackPrimaryColor }"></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 || fallbackSecondaryColor }"></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. import { BrandColors } from '../utils/brand-colors.js'
  74. // 维度色板(JS 侧单一事实源;SCSS 侧等值令牌见 uni.scss $dim-*/$facade-violet/$status-info)
  75. var DIM_COLORS = {
  76. body: BrandColors.action, // 身体
  77. mind: BrandColors.violet, // 心
  78. wisdom: BrandColors.infoBlue, // 智
  79. action: BrandColors.primary, // 行动
  80. wealth: BrandColors.wealth // 富
  81. }
  82. export default {
  83. props: {
  84. dimensionCode: { type: String, required: true },
  85. sandboxData: { type: Object, default: null },
  86. dualDimension: { type: [String, Object], default: '' }
  87. },
  88. data: function() {
  89. return {
  90. showMembers: false,
  91. fallbackPrimaryColor: BrandColors.violet,
  92. fallbackSecondaryColor: BrandColors.infoBlue
  93. }
  94. },
  95. computed: {
  96. isStacked: function() {
  97. return typeof this.dualDimension === 'object' && this.dualDimension !== null && this.dualDimension.primary && this.dualDimension.secondary
  98. },
  99. dimensionConfig: function() {
  100. var maps = {
  101. body: { name: '身体', icon: '\u{1F3C3}', color: DIM_COLORS.body },
  102. mind: { name: '心', icon: '\u2764', color: DIM_COLORS.mind },
  103. wisdom: { name: '智', icon: '\u{1F9E0}', color: DIM_COLORS.wisdom },
  104. action: { name: '行动', icon: '\u{1F3CB}', color: DIM_COLORS.action },
  105. wealth: { name: '富', icon: '\u{1F4B0}', color: DIM_COLORS.wealth }
  106. }
  107. return maps[this.dimensionCode] || { name: '', icon: '', color: BrandColors.textMuted }
  108. },
  109. dimensionIcon: function() { return this.dimensionConfig.icon },
  110. dimensionName: function() { return this.dimensionConfig.name },
  111. dimensionColor: function() { return this.dimensionConfig.color },
  112. familyScore: function() {
  113. if (!this.sandboxData) return 0
  114. if (this.isStacked) {
  115. var primary = this.sandboxData[this.dualDimension.primary.code + 'Score'] || 0
  116. var secondary = this.sandboxData[this.dualDimension.secondary.code + 'Score'] || 0
  117. return primary + secondary
  118. }
  119. return this.sandboxData[this.dimensionCode + 'Score'] || 0
  120. },
  121. members: function() {
  122. return this.sandboxData && this.sandboxData.members ? this.sandboxData.members : []
  123. }
  124. },
  125. methods: {
  126. toggleMembers: function() {
  127. this.showMembers = !this.showMembers
  128. },
  129. getMemberScore: function(member) {
  130. if (this.isStacked) {
  131. var primary = member[this.dualDimension.primary.code + 'Score'] || 0
  132. var secondary = member[this.dualDimension.secondary.code + 'Score'] || 0
  133. return primary + secondary
  134. }
  135. return member[this.dimensionCode + 'Score'] || 0
  136. },
  137. getMemberPrimaryScore: function(member) {
  138. if (!this.isStacked) return 0
  139. return member[this.dualDimension.primary.code + 'Score'] || 0
  140. },
  141. getMemberSecondaryScore: function(member) {
  142. if (!this.isStacked) return 0
  143. return member[this.dualDimension.secondary.code + 'Score'] || 0
  144. },
  145. memberColor: function(member) {
  146. var score = this.getMemberScore(member)
  147. if (score >= 80) return BrandColors.action
  148. if (score >= 60) return BrandColors.infoBlue
  149. if (score >= 40) return BrandColors.primary
  150. return BrandColors.danger
  151. },
  152. onMemberClick: function(member) {
  153. var self = this
  154. uni.navigateTo({
  155. url: '/pages/member-detail/member-detail?memberId=' + member.memberId +
  156. '&memberType=' + (member.memberType || 'child') +
  157. '&memberName=' + encodeURIComponent(member.name) +
  158. '&dimensionCode=' + self.dimensionCode +
  159. '&entrySource=body-card'
  160. })
  161. }
  162. }
  163. }
  164. </script>
  165. <style scoped lang="scss">
  166. .family-energy-bar {
  167. margin: 20rpx 30rpx;
  168. background: $app-surface;
  169. border-radius: 24rpx;
  170. padding: 28rpx 24rpx;
  171. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
  172. }
  173. .bar-header {
  174. display: flex;
  175. flex-direction: row;
  176. align-items: center;
  177. margin-bottom: 20rpx;
  178. }
  179. .bar-icon { font-size: 32rpx; margin-right: 8rpx; }
  180. .bar-title { font-size: 28rpx; font-weight: bold; color: $facade-text-strong; flex: 1; }
  181. .bar-total-score { font-size: 40rpx; font-weight: bold; color: $brand-primary; margin-right: 12rpx; }
  182. .bar-expand-icon {
  183. font-size: 20rpx;
  184. color: $facade-text-ghost-light;
  185. transition: transform 0.3s ease;
  186. }
  187. .bar-row {
  188. display: flex;
  189. flex-direction: row;
  190. align-items: center;
  191. margin-bottom: 16rpx;
  192. padding: 8rpx 0;
  193. }
  194. .bar-row:last-child { margin-bottom: 0; }
  195. .bar-row-member:active { opacity: 0.6; }
  196. .bar-label {
  197. font-size: 24rpx;
  198. color: $facade-text-body;
  199. width: 80rpx;
  200. flex-shrink: 0;
  201. }
  202. .bar-track {
  203. flex: 1;
  204. height: 24rpx;
  205. background: $facade-fill;
  206. border-radius: 12rpx;
  207. overflow: hidden;
  208. margin: 0 12rpx;
  209. }
  210. .bar-fill {
  211. height: 100%;
  212. border-radius: 12rpx;
  213. transition: width 0.5s ease;
  214. }
  215. .bar-value {
  216. font-size: 26rpx;
  217. font-weight: bold;
  218. color: $facade-text-strong;
  219. width: 60rpx;
  220. text-align: right;
  221. flex-shrink: 0;
  222. }
  223. .bar-arrow {
  224. font-size: 28rpx;
  225. color: $facade-text-ghost;
  226. margin-left: 8rpx;
  227. }
  228. .bar-row-family .bar-fill {
  229. background: linear-gradient(90deg, $brand-primary, $brand-primary-light);
  230. }
  231. /* ===== 堆叠模式 ===== */
  232. .stacked-track {
  233. display: flex;
  234. flex-direction: row;
  235. overflow: hidden;
  236. }
  237. .bar-fill-primary {
  238. border-radius: 12rpx 0 0 12rpx;
  239. }
  240. .bar-fill-secondary {
  241. border-radius: 0 12rpx 12rpx 0;
  242. margin-left: 2rpx;
  243. }
  244. .stacked-legend {
  245. display: flex;
  246. flex-direction: row;
  247. justify-content: center;
  248. margin-top: 12rpx;
  249. gap: 24rpx;
  250. }
  251. .legend-item {
  252. display: flex;
  253. flex-direction: row;
  254. align-items: center;
  255. }
  256. .legend-dot {
  257. width: 16rpx;
  258. height: 16rpx;
  259. border-radius: 8rpx;
  260. margin-right: 6rpx;
  261. }
  262. .legend-label {
  263. font-size: 22rpx;
  264. color: $facade-text-muted;
  265. }
  266. </style>