HealthDimensionsSection.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <template>
  2. <view class="hds-section">
  3. <!-- 健康指数 -->
  4. <view class="hds-health-index" v-if="healthData && healthData.healthIndex">
  5. <view class="index-circle">
  6. <text class="index-value">{{ healthData.healthIndex }}</text>
  7. <text class="index-label">健康指数</text>
  8. </view>
  9. </view>
  10. <!-- 七维健康雷达 -->
  11. <view class="hds-radar" v-if="healthData && healthData.dimensions">
  12. <view class="radar-title">七维健康雷达</view>
  13. <view class="radar-wrap">
  14. <view class="radar-center">
  15. <view class="radar-grid">
  16. <view class="grid-ring ring-3"></view>
  17. <view class="grid-ring ring-2"></view>
  18. <view class="grid-ring ring-1"></view>
  19. </view>
  20. <view class="radar-spokes">
  21. <view
  22. class="spoke-item"
  23. v-for="(dim, idx) in healthData.dimensions"
  24. :key="dim.dimension"
  25. :style="spokeStyles[idx]"
  26. >
  27. <view class="spoke-dot" :class="'level-' + dim.level"></view>
  28. <view class="spoke-label">{{ dim.label }}</view>
  29. <view class="spoke-score">{{ dim.score || '--' }}</view>
  30. </view>
  31. </view>
  32. </view>
  33. </view>
  34. </view>
  35. <!-- 维度列表 -->
  36. <view class="hds-dim-list" v-if="healthData && healthData.dimensions">
  37. <view
  38. class="dim-card"
  39. v-for="dim in healthData.dimensions"
  40. :key="dim.dimension"
  41. @click="goDimDetail(dim)"
  42. >
  43. <view class="dim-header">
  44. <text class="dim-label">{{ dim.label }}</text>
  45. <text class="dim-score" :class="'score-' + dim.level">{{ dim.score || '--' }}分</text>
  46. </view>
  47. <view class="dim-bar-wrap">
  48. <view class="dim-bar-bg">
  49. <view
  50. class="dim-bar-fill"
  51. :class="'fill-' + dim.level"
  52. :style="{ width: (dim.score || 0) + '%' }"
  53. ></view>
  54. </view>
  55. <text class="dim-norms" v-if="dim.normMin && dim.normMax">
  56. 正常范围 {{ dim.normMin }}-{{ dim.normMax }}
  57. </text>
  58. </view>
  59. <view class="dim-footer">
  60. <text class="dim-level-tag" :class="'tag-' + dim.level">{{ getLevelText(dim.level) }}</text>
  61. <text class="dim-source" v-if="dim.trend">{{ dim.trend === 'up' ? '↑ 上升' : dim.trend === 'down' ? '↓ 下降' : '→ 稳定' }}</text>
  62. </view>
  63. </view>
  64. </view>
  65. <!-- 空数据 -->
  66. <view class="hds-empty" v-if="!healthData">
  67. <text class="empty-tip">暂无维度数据</text>
  68. <text class="empty-sub">上传健康报告或填写问卷后可查看</text>
  69. </view>
  70. </view>
  71. </template>
  72. <script>
  73. export default {
  74. props: {
  75. healthData: {
  76. type: Object,
  77. default: null
  78. },
  79. activeChildId: {
  80. type: [Number, String],
  81. default: null
  82. }
  83. },
  84. computed: {
  85. spokeStyles: function() {
  86. if (!this.healthData || !this.healthData.dimensions) return []
  87. var cache = []
  88. var dims = this.healthData.dimensions
  89. for (var i = 0; i < dims.length; i++) {
  90. var angle = i * 51.4 - 90
  91. var radius = 85
  92. var normalizedScore = (dims[i].score || 0) / 100
  93. var translateY = -radius * normalizedScore
  94. cache.push({
  95. transform: 'rotate(' + angle + 'deg) translateY(' + translateY + 'px)',
  96. '-webkit-transform': 'rotate(' + angle + 'deg) translateY(' + translateY + 'px)'
  97. })
  98. }
  99. return cache
  100. }
  101. },
  102. methods: {
  103. getLevelText: function(level) {
  104. var map = {
  105. 'excellent': '优秀',
  106. 'good': '良好',
  107. 'fair': '一般',
  108. 'poor': '较差',
  109. 'none': '暂无数据'
  110. }
  111. return map[level] || ''
  112. },
  113. goDimDetail: function(dim) {
  114. if (!dim || !dim.dimension) return
  115. var childId = this.activeChildId || ''
  116. uni.navigateTo({
  117. url: '/pages/body-detail/dimension-detail?dimension=' + dim.dimension + '&childId=' + childId
  118. })
  119. }
  120. }
  121. }
  122. </script>
  123. <style scoped>
  124. .hds-section {
  125. margin: 0 30rpx 20rpx;
  126. }
  127. .hds-health-index {
  128. display: flex;
  129. justify-content: center;
  130. padding: 30rpx;
  131. background: #fff;
  132. border-radius: 20rpx;
  133. margin-bottom: 20rpx;
  134. }
  135. .index-circle {
  136. width: 160rpx;
  137. height: 160rpx;
  138. border-radius: 50%;
  139. background: linear-gradient(135deg, #FF8C42, #FF6B35);
  140. display: flex;
  141. flex-direction: column;
  142. align-items: center;
  143. justify-content: center;
  144. color: #fff;
  145. }
  146. .index-value {
  147. font-size: 48rpx;
  148. font-weight: bold;
  149. line-height: 1;
  150. }
  151. .index-label {
  152. font-size: 22rpx;
  153. opacity: 0.9;
  154. margin-top: 8rpx;
  155. }
  156. .hds-radar {
  157. background: #fff;
  158. border-radius: 20rpx;
  159. padding: 30rpx;
  160. margin-bottom: 20rpx;
  161. }
  162. .radar-title {
  163. font-size: 32rpx;
  164. font-weight: bold;
  165. color: #333;
  166. margin-bottom: 30rpx;
  167. text-align: center;
  168. }
  169. .radar-wrap {
  170. display: flex;
  171. justify-content: center;
  172. }
  173. .radar-center {
  174. width: 440rpx;
  175. height: 440rpx;
  176. position: relative;
  177. display: flex;
  178. align-items: center;
  179. justify-content: center;
  180. }
  181. .radar-grid {
  182. position: absolute;
  183. width: 100%;
  184. height: 100%;
  185. }
  186. .grid-ring {
  187. position: absolute;
  188. border-radius: 50%;
  189. border: 1px solid #ddd;
  190. left: 50%;
  191. top: 50%;
  192. transform: translate(-50%, -50%);
  193. }
  194. .ring-1 { width: 33%; height: 33%; }
  195. .ring-2 { width: 66%; height: 66%; }
  196. .ring-3 { width: 100%; height: 100%; }
  197. .radar-spokes {
  198. position: absolute;
  199. width: 100%;
  200. height: 100%;
  201. display: flex;
  202. align-items: center;
  203. justify-content: center;
  204. }
  205. .spoke-item {
  206. position: absolute;
  207. display: flex;
  208. flex-direction: column;
  209. align-items: center;
  210. transform-origin: center center;
  211. width: 80rpx;
  212. margin-left: -40rpx;
  213. top: 10rpx;
  214. }
  215. .spoke-dot {
  216. width: 20rpx;
  217. height: 20rpx;
  218. border-radius: 50%;
  219. background: #FF8C42;
  220. margin-bottom: 6rpx;
  221. }
  222. .spoke-dot.level-excellent { background: #10B981; }
  223. .spoke-dot.level-good { background: #3B82F6; }
  224. .spoke-dot.level-fair { background: #F59E0B; }
  225. .spoke-dot.level-poor { background: #EF4444; }
  226. .spoke-dot.level-none { background: #ddd; }
  227. .spoke-label {
  228. font-size: 22rpx;
  229. color: #333;
  230. white-space: nowrap;
  231. }
  232. .spoke-score {
  233. font-size: 20rpx;
  234. color: #666;
  235. margin-top: 2rpx;
  236. }
  237. .hds-dim-list {
  238. padding: 0;
  239. }
  240. .dim-card {
  241. background: #fff;
  242. border-radius: 16rpx;
  243. padding: 24rpx;
  244. margin-bottom: 20rpx;
  245. }
  246. .dim-header {
  247. display: flex;
  248. justify-content: space-between;
  249. align-items: center;
  250. margin-bottom: 16rpx;
  251. }
  252. .dim-label {
  253. font-size: 30rpx;
  254. font-weight: bold;
  255. color: #333;
  256. }
  257. .dim-score {
  258. font-size: 36rpx;
  259. font-weight: bold;
  260. color: #FF8C42;
  261. }
  262. .dim-score.score-excellent { color: #10B981; }
  263. .dim-score.score-good { color: #3B82F6; }
  264. .dim-score.score-fair { color: #F59E0B; }
  265. .dim-score.score-poor { color: #EF4444; }
  266. .dim-bar-wrap {
  267. margin-bottom: 12rpx;
  268. }
  269. .dim-bar-bg {
  270. height: 16rpx;
  271. background: #f0f0f0;
  272. border-radius: 8rpx;
  273. overflow: hidden;
  274. margin-bottom: 6rpx;
  275. }
  276. .dim-bar-fill {
  277. height: 100%;
  278. border-radius: 8rpx;
  279. background: #FF8C42;
  280. transition: width 0.3s;
  281. }
  282. .dim-bar-fill.fill-excellent { background: #10B981; }
  283. .dim-bar-fill.fill-good { background: #3B82F6; }
  284. .dim-bar-fill.fill-fair { background: #F59E0B; }
  285. .dim-bar-fill.fill-poor { background: #EF4444; }
  286. .dim-norms {
  287. font-size: 22rpx;
  288. color: #999;
  289. }
  290. .dim-footer {
  291. display: flex;
  292. justify-content: space-between;
  293. align-items: center;
  294. }
  295. .dim-level-tag {
  296. font-size: 22rpx;
  297. padding: 4rpx 16rpx;
  298. border-radius: 20rpx;
  299. background: #f0f0f0;
  300. color: #666;
  301. }
  302. .dim-level-tag.tag-excellent { background: #D1FAE5; color: #10B981; }
  303. .dim-level-tag.tag-good { background: #DBEAFE; color: #3B82F6; }
  304. .dim-level-tag.tag-fair { background: #FEF3C7; color: #F59E0B; }
  305. .dim-level-tag.tag-poor { background: #FEE2E2; color: #EF4444; }
  306. .dim-source {
  307. font-size: 22rpx;
  308. color: #999;
  309. }
  310. .hds-empty {
  311. padding: 80rpx 30rpx;
  312. text-align: center;
  313. }
  314. .hds-empty .empty-tip {
  315. font-size: 30rpx;
  316. color: #999;
  317. display: block;
  318. margin-bottom: 16rpx;
  319. }
  320. .hds-empty .empty-sub {
  321. font-size: 24rpx;
  322. color: #ccc;
  323. display: block;
  324. }
  325. </style>