dimension-detail.vue 14 KB

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