dimension-detail.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <view class="dd-container">
  3. <!-- Loading -->
  4. <view class="dd-loading" v-if="loading">
  5. <text class="loading-text">加载中...</text>
  6. </view>
  7. <template v-else>
  8. <!-- Dimension Header -->
  9. <view class="dd-header">
  10. <view class="dd-score-ring">
  11. <text class="dd-score-value">{{ currentScore || '--' }}</text>
  12. <text class="dd-score-label">分</text>
  13. </view>
  14. <text class="dd-dim-name">{{ dimLabel }}</text>
  15. <text class="dd-dim-level" :class="'level-' + (dimLevel || 'fair')">{{ levelText }}</text>
  16. </view>
  17. <!-- History Trend Chart (Canvas) -->
  18. <view class="dd-section">
  19. <text class="dd-section-title">历史趋势</text>
  20. <view class="dd-chart-wrap" v-if="history.length > 0">
  21. <canvas canvas-id="trendCanvas" class="dd-canvas"></canvas>
  22. </view>
  23. <view class="dd-empty" v-else>
  24. <text class="empty-text">暂无历史数据</text>
  25. <text class="empty-sub">完成评估或上传报告后可查看趋势</text>
  26. </view>
  27. </view>
  28. <!-- Percentile -->
  29. <view class="dd-section" v-if="percentile !== null">
  30. <text class="dd-section-title">常模对比</text>
  31. <view class="dd-percentile-wrap">
  32. <text class="dd-percentile-value">{{ percentile }}%</text>
  33. <text class="dd-percentile-label">超过同龄同性别 {{ percentile }}% 的人群</text>
  34. </view>
  35. </view>
  36. <!-- Suggestions -->
  37. <view class="dd-section" v-if="suggestions.length > 0">
  38. <text class="dd-section-title">改善建议</text>
  39. <view
  40. class="dd-suggestion-item"
  41. v-for="(s, idx) in suggestions"
  42. :key="idx"
  43. >
  44. <text class="dd-sug-num">{{ idx + 1 }}</text>
  45. <text class="dd-sug-text">{{ s }}</text>
  46. </view>
  47. </view>
  48. <!-- Related Products -->
  49. <view class="dd-section" v-if="products.length > 0">
  50. <text class="dd-section-title">相关商品</text>
  51. <view
  52. class="dd-product-item"
  53. v-for="p in products"
  54. :key="p.id"
  55. @click="goProduct(p)"
  56. >
  57. <image class="dd-prod-img" :src="p.coverImage || '/static/default-product.png'" mode="aspectFill"></image>
  58. <view class="dd-prod-info">
  59. <text class="dd-prod-name">{{ p.name }}</text>
  60. <text class="dd-prod-price">¥{{ p.price }}</text>
  61. </view>
  62. </view>
  63. </view>
  64. <!-- Action Buttons -->
  65. <view class="dd-actions">
  66. <button class="dd-btn" @click="goQuestionnaire">填写问卷</button>
  67. <button class="dd-btn dd-btn-outline" @click="goDimensions">返回全景</button>
  68. </view>
  69. </template>
  70. </view>
  71. </template>
  72. <script>
  73. import { getDimensionHistory, getDimensionOverview, getProductsByDomain } from '@/utils/api'
  74. var SUGGESTIONS = {
  75. growth: [
  76. '保证每日充足的蛋白质和钙摄入,牛奶、鸡蛋、豆制品是优质选择',
  77. '每周至少3次户外活动,跳绳、篮球等纵向运动有助于骨骼发育',
  78. '定期记录身高体重,关注生长曲线是否在正常范围内',
  79. '保证充足睡眠,生长激素在夜间深睡期分泌最旺盛'
  80. ],
  81. sleep: [
  82. '建立固定的睡前程序:洗热水澡、读故事、调暗灯光',
  83. '卧室保持安静、黑暗、凉爽,温度控制在18-22°C',
  84. '睡前1小时避免使用电子设备,蓝光会抑制褪黑素分泌',
  85. '保持规律的作息时间,周末与平日差异不超过1小时'
  86. ],
  87. vision: [
  88. '遵循"20-20-20"法则:每20分钟看20英尺外20秒',
  89. '每日保证至少2小时户外活动,自然光有助于预防近视',
  90. '保持正确读写姿势:眼离书本一尺、胸离桌沿一拳',
  91. '每半年进行一次视力检查,建立视力发育档案'
  92. ],
  93. immunity: [
  94. '均衡饮食,确保蛋白质、维生素C、锌等免疫营养素摄入',
  95. '适度运动增强体质,但避免过度训练导致免疫力下降',
  96. '保证充足睡眠,睡眠不足会影响免疫细胞活性',
  97. '注意手部卫生,但不过度消毒,适度接触微生物有助免疫系统发育'
  98. ],
  99. nutrition: [
  100. '每天摄入12种以上食材,每周25种以上,确保营养多样性',
  101. '减少加工食品和高糖饮料,增加全谷物、蔬菜、水果比例',
  102. '关注铁、钙、维生素D等易缺乏营养素的摄入',
  103. '培养自主进食能力,营造愉快的就餐氛围'
  104. ],
  105. gut: [
  106. '增加膳食纤维摄入,全谷物、蔬菜、水果是优质来源',
  107. '适量补充益生菌食物:酸奶、发酵食品等',
  108. '保持规律排便习惯,及时响应便意',
  109. '减少油炸、辛辣等刺激性食物,减轻肠胃负担'
  110. ],
  111. exercise: [
  112. '每天至少60分钟中高强度运动,可分多次完成',
  113. '运动类型多样化:有氧+力量+柔韧性训练结合',
  114. '让运动变得有趣:球类游戏、游泳、骑自行车都是好选择',
  115. '家长以身作则,全家一起运动效果更好'
  116. ]
  117. }
  118. export default {
  119. data() {
  120. return {
  121. loading: true,
  122. dimension: '',
  123. childId: '',
  124. dimLabel: '',
  125. currentScore: null,
  126. dimLevel: 'fair',
  127. percentile: null,
  128. history: [],
  129. products: [],
  130. suggestions: []
  131. }
  132. },
  133. computed: {
  134. levelText: function() {
  135. var map = { excellent: '优秀', good: '良好', fair: '一般', poor: '较差' }
  136. return map[this.dimLevel] || '一般'
  137. }
  138. },
  139. onLoad: function(options) {
  140. this.dimension = options.dimension || ''
  141. this.childId = options.childId || uni.getStorageSync('currentChildId') || ''
  142. this.setDimLabel()
  143. this.suggestions = SUGGESTIONS[this.dimension] || []
  144. this.loadData()
  145. },
  146. onReady: function() {
  147. if (this.history.length > 0) {
  148. this.drawChart()
  149. }
  150. },
  151. methods: {
  152. setDimLabel: function() {
  153. var map = {
  154. growth: '生长发育',
  155. sleep: '睡眠质量',
  156. vision: '视力健康',
  157. immunity: '免疫力',
  158. nutrition: '营养均衡',
  159. gut: '肠胃健康',
  160. exercise: '运动活力'
  161. }
  162. this.dimLabel = map[this.dimension] || this.dimension
  163. },
  164. loadData: function() {
  165. var self = this
  166. self.loading = true
  167. // Load dimension overview for current score and percentile
  168. getDimensionOverview({ memberId: this.childId }).then(function(res) {
  169. if (res.code === 200 && res.data && res.data.dimensions) {
  170. for (var i = 0; i < res.data.dimensions.length; i++) {
  171. var dim = res.data.dimensions[i]
  172. if (dim.dimension === self.dimension) {
  173. self.currentScore = dim.score
  174. self.dimLevel = dim.level || 'fair'
  175. self.percentile = dim.percentile || null
  176. break
  177. }
  178. }
  179. }
  180. }).catch(function(e) {
  181. console.log('获取维度概览失败', e)
  182. })
  183. // Load history
  184. getDimensionHistory({ memberId: this.childId, dimension: this.dimension, limit: 10 }).then(function(res) {
  185. self.loading = false
  186. if (res.code === 200 && res.data) {
  187. self.history = Array.isArray(res.data) ? res.data : (res.data.history || [])
  188. // Try to draw chart if canvas is ready
  189. setTimeout(function() {
  190. self.drawChart()
  191. }, 100)
  192. }
  193. }).catch(function(e) {
  194. self.loading = false
  195. console.log('获取历史数据失败', e)
  196. })
  197. // Load related products from recommendation
  198. this.loadProducts()
  199. },
  200. loadProducts: function() {
  201. var self = this
  202. getProductsByDomain('body', 1, 5).then(function(res) {
  203. if (res.code === 200 && res.data) {
  204. var list = res.data.rows || res.data
  205. self.products = Array.isArray(list) ? list.slice(0, 5) : []
  206. }
  207. }).catch(function() {})
  208. },
  209. drawChart: function() {
  210. if (this.history.length === 0) return
  211. var ctx = uni.createCanvasContext('trendCanvas', this)
  212. var w = 660
  213. var h = 280
  214. var padding = { top: 30, right: 30, bottom: 40, left: 50 }
  215. var chartW = w - padding.left - padding.right
  216. var chartH = h - padding.top - padding.bottom
  217. // Clear
  218. ctx.setFillStyle('#fff')
  219. ctx.fillRect(0, 0, w, h)
  220. // Grid lines
  221. for (var i = 0; i <= 4; i++) {
  222. var y = padding.top + (chartH / 4) * i
  223. ctx.setStrokeStyle('#f0f0f0')
  224. ctx.setLineWidth(1)
  225. ctx.beginPath()
  226. ctx.moveTo(padding.left, y)
  227. ctx.lineTo(w - padding.right, y)
  228. ctx.stroke()
  229. // Label
  230. var labelVal = 100 - i * 25
  231. ctx.setFillStyle('#999')
  232. ctx.setFontSize(20)
  233. ctx.fillText(labelVal + '', 10, y + 6)
  234. }
  235. // Draw data points and line
  236. var points = []
  237. var count = this.history.length
  238. for (var j = 0; j < count; j++) {
  239. var item = this.history[j]
  240. var score = item.score || 0
  241. var x = padding.left + (chartW / (count - 1 || 1)) * j
  242. var y = padding.top + chartH - (score / 100) * chartH
  243. points.push({ x: x, y: y, score: score, date: item.date || item.createTime || '' })
  244. }
  245. // Line
  246. if (points.length > 1) {
  247. ctx.setStrokeStyle('#4A9BD7')
  248. ctx.setLineWidth(3)
  249. ctx.beginPath()
  250. ctx.moveTo(points[0].x, points[0].y)
  251. for (var k = 1; k < points.length; k++) {
  252. ctx.lineTo(points[k].x, points[k].y)
  253. }
  254. ctx.stroke()
  255. // Area fill
  256. ctx.setFillStyle('rgba(74, 155, 215, 0.1)')
  257. ctx.beginPath()
  258. ctx.moveTo(points[0].x, padding.top + chartH)
  259. ctx.lineTo(points[0].x, points[0].y)
  260. for (var l = 1; l < points.length; l++) {
  261. ctx.lineTo(points[l].x, points[l].y)
  262. }
  263. ctx.lineTo(points[points.length - 1].x, padding.top + chartH)
  264. ctx.closePath()
  265. ctx.fill()
  266. }
  267. // Dots
  268. for (var m = 0; m < points.length; m++) {
  269. ctx.setFillStyle('#4A9BD7')
  270. ctx.beginPath()
  271. ctx.arc(points[m].x, points[m].y, 5, 0, 2 * Math.PI)
  272. ctx.fill()
  273. ctx.setFillStyle('#fff')
  274. ctx.beginPath()
  275. ctx.arc(points[m].x, points[m].y, 2.5, 0, 2 * Math.PI)
  276. ctx.fill()
  277. }
  278. // X-axis labels
  279. if (points.length > 0) {
  280. for (var n = 0; n < points.length; n++) {
  281. var dateStr = this.formatShortDate(points[n].date)
  282. ctx.setFillStyle('#999')
  283. ctx.setFontSize(18)
  284. ctx.setTextAlign('center')
  285. ctx.fillText(dateStr, points[n].x, h - 8)
  286. }
  287. }
  288. ctx.draw()
  289. },
  290. formatShortDate: function(dateStr) {
  291. if (!dateStr) return ''
  292. var d = dateStr.toString()
  293. return d.length >= 10 ? d.substring(5, 10) : d
  294. },
  295. goQuestionnaire: function() {
  296. uni.navigateTo({
  297. url: '/pages/body/dimension-questionnaire?childId=' + this.childId
  298. })
  299. },
  300. goDimensions: function() {
  301. uni.navigateTo({
  302. url: '/pages/body/health-dimensions?childId=' + this.childId
  303. })
  304. },
  305. goProduct: function(product) {
  306. if (product && product.id) {
  307. uni.navigateTo({ url: '/pages/shop/detail?id=' + product.id })
  308. }
  309. }
  310. }
  311. }
  312. </script>
  313. <style scoped>
  314. .dd-container {
  315. min-height: 100vh;
  316. background: #F5FAFE;
  317. padding-bottom: 100rpx;
  318. }
  319. .dd-loading {
  320. display: flex;
  321. justify-content: center;
  322. align-items: center;
  323. height: 60vh;
  324. }
  325. .loading-text {
  326. font-size: 28rpx;
  327. color: #999;
  328. }
  329. .dd-header {
  330. background: linear-gradient(135deg, #4A9BD7, #63B3ED);
  331. padding: 60rpx 0;
  332. text-align: center;
  333. color: #fff;
  334. }
  335. .dd-score-ring {
  336. width: 160rpx;
  337. height: 160rpx;
  338. border: 6rpx solid rgba(255,255,255,0.5);
  339. border-radius: 50%;
  340. display: flex;
  341. flex-direction: column;
  342. align-items: center;
  343. justify-content: center;
  344. margin: 0 auto 20rpx;
  345. }
  346. .dd-score-value {
  347. font-size: 56rpx;
  348. font-weight: 700;
  349. line-height: 1;
  350. }
  351. .dd-score-label {
  352. font-size: 24rpx;
  353. opacity: 0.8;
  354. }
  355. .dd-dim-name {
  356. font-size: 40rpx;
  357. font-weight: 600;
  358. display: block;
  359. }
  360. .dd-dim-level {
  361. font-size: 26rpx;
  362. padding: 6rpx 24rpx;
  363. border-radius: 20rpx;
  364. display: inline-block;
  365. margin-top: 16rpx;
  366. background: rgba(255,255,255,0.2);
  367. }
  368. .dd-section {
  369. background: #fff;
  370. margin: 20rpx 30rpx;
  371. border-radius: 20rpx;
  372. padding: 30rpx;
  373. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
  374. }
  375. .dd-section-title {
  376. font-size: 32rpx;
  377. font-weight: 600;
  378. color: #333;
  379. margin-bottom: 20rpx;
  380. display: block;
  381. }
  382. .dd-chart-wrap {
  383. width: 100%;
  384. height: 300rpx;
  385. }
  386. .dd-canvas {
  387. width: 700rpx;
  388. height: 300rpx;
  389. }
  390. .dd-empty {
  391. text-align: center;
  392. padding: 40rpx 0;
  393. }
  394. .empty-text {
  395. font-size: 28rpx;
  396. color: #999;
  397. }
  398. .empty-sub {
  399. font-size: 24rpx;
  400. color: #ccc;
  401. margin-top: 10rpx;
  402. }
  403. .dd-percentile-wrap {
  404. text-align: center;
  405. padding: 20rpx 0;
  406. }
  407. .dd-percentile-value {
  408. font-size: 48rpx;
  409. font-weight: 700;
  410. color: #4A9BD7;
  411. }
  412. .dd-percentile-label {
  413. font-size: 26rpx;
  414. color: #888;
  415. margin-top: 10rpx;
  416. display: block;
  417. }
  418. .dd-suggestion-item {
  419. display: flex;
  420. align-items: flex-start;
  421. padding: 16rpx 0;
  422. border-bottom: 1rpx solid #f5f5f5;
  423. }
  424. .dd-suggestion-item:last-child {
  425. border-bottom: none;
  426. }
  427. .dd-sug-num {
  428. width: 40rpx;
  429. height: 40rpx;
  430. background: #4A9BD7;
  431. color: #fff;
  432. border-radius: 50%;
  433. text-align: center;
  434. line-height: 40rpx;
  435. font-size: 22rpx;
  436. flex-shrink: 0;
  437. margin-right: 16rpx;
  438. margin-top: 4rpx;
  439. }
  440. .dd-sug-text {
  441. font-size: 28rpx;
  442. color: #555;
  443. line-height: 1.6;
  444. flex: 1;
  445. }
  446. .dd-product-item {
  447. display: flex;
  448. padding: 20rpx 0;
  449. border-bottom: 1rpx solid #f5f5f5;
  450. }
  451. .dd-product-item:last-child {
  452. border-bottom: none;
  453. }
  454. .dd-prod-img {
  455. width: 120rpx;
  456. height: 120rpx;
  457. border-radius: 12rpx;
  458. margin-right: 20rpx;
  459. flex-shrink: 0;
  460. background: #f0f0f0;
  461. }
  462. .dd-prod-info {
  463. flex: 1;
  464. display: flex;
  465. flex-direction: column;
  466. justify-content: center;
  467. }
  468. .dd-prod-name {
  469. font-size: 28rpx;
  470. color: #333;
  471. font-weight: 500;
  472. }
  473. .dd-prod-price {
  474. font-size: 30rpx;
  475. color: #FF6B35;
  476. font-weight: 600;
  477. margin-top: 8rpx;
  478. }
  479. .dd-actions {
  480. display: flex;
  481. gap: 20rpx;
  482. margin: 40rpx 30rpx;
  483. }
  484. .dd-btn {
  485. flex: 1;
  486. height: 88rpx;
  487. line-height: 88rpx;
  488. background: linear-gradient(135deg, #4A9BD7, #63B3ED);
  489. color: #fff;
  490. font-size: 32rpx;
  491. border-radius: 44rpx;
  492. text-align: center;
  493. }
  494. .dd-btn-outline {
  495. background: #fff;
  496. color: #4A9BD7;
  497. border: 2rpx solid #4A9BD7;
  498. }
  499. </style>