dimension-questionnaire.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <template>
  2. <view class="qd-container">
  3. <!-- Loading -->
  4. <view class="qd-loading" v-if="loading">
  5. <text class="loading-text">加载中...</text>
  6. </view>
  7. <template v-else>
  8. <!-- Step 1: 选择维度 -->
  9. <view v-if="step === 1" class="qd-step">
  10. <view class="qd-header">
  11. <text class="qd-title">选择健康维度</text>
  12. <text class="qd-desc">请选择一个维度进行评估</text>
  13. </view>
  14. <view class="qd-dim-list">
  15. <view
  16. class="qd-dim-item"
  17. :class="{ active: selectedDimension === dim.code }"
  18. v-for="dim in dimensions"
  19. :key="dim.code"
  20. @click="selectedDimension = dim.code"
  21. >
  22. <text class="qd-dim-icon">{{ dim.icon }}</text>
  23. <text class="qd-dim-label">{{ dim.label }}</text>
  24. <text class="qd-dim-check" v-if="selectedDimension === dim.code">✓</text>
  25. </view>
  26. </view>
  27. <view class="qd-footer">
  28. <button class="qd-btn" :disabled="!selectedDimension" @click="startQuestions">开始答题</button>
  29. </view>
  30. </view>
  31. <!-- Step 2: 回答问题 -->
  32. <view v-if="step === 2" class="qd-step">
  33. <view class="qd-header">
  34. <text class="qd-title">{{ currentDimLabel }}</text>
  35. <text class="qd-progress">{{ currentQIndex + 1 }} / {{ currentQuestions.length }}</text>
  36. </view>
  37. <view class="qd-progress-bar">
  38. <view class="qd-progress-fill" :style="{ width: ((currentQIndex + 1) / currentQuestions.length * 100) + '%' }"></view>
  39. </view>
  40. <view class="qd-question-card">
  41. <text class="qd-q-text">{{ currentQuestion.question }}</text>
  42. <view class="qd-options">
  43. <view
  44. class="qd-option"
  45. :class="{ selected: currentAnswer === opt.value }"
  46. v-for="opt in currentQuestion.options"
  47. :key="opt.value"
  48. @click="currentAnswer = opt.value"
  49. >
  50. <text class="qd-opt-label">{{ opt.label }}</text>
  51. <text class="qd-opt-check" v-if="currentAnswer === opt.value">✓</text>
  52. </view>
  53. </view>
  54. </view>
  55. <view class="qd-footer">
  56. <button class="qd-btn qd-btn-prev" @click="prevQuestion" v-if="currentQIndex > 0">上一题</button>
  57. <button class="qd-btn" @click="nextQuestion" :disabled="!currentAnswer">
  58. {{ currentQIndex < currentQuestions.length - 1 ? '下一题' : '提交评估' }}
  59. </button>
  60. </view>
  61. </view>
  62. <!-- Step 3: 提交结果 -->
  63. <view v-if="step === 3" class="qd-step">
  64. <view class="qd-result" v-if="result">
  65. <text class="qd-result-icon">{{ resultIcon }}</text>
  66. <text class="qd-result-title">评估完成!</text>
  67. <text class="qd-result-score">{{ currentDimLabel }}:{{ result.score }}分</text>
  68. <view class="qd-result-level" :class="'level-' + (result.level || 'fair')">
  69. {{ levelText(result.level) }}
  70. </view>
  71. <text class="qd-result-tip" v-if="result.suggestion">{{ result.suggestion }}</text>
  72. </view>
  73. <view class="qd-footer">
  74. <button class="qd-btn" @click="goToDimensions">查看维度全景</button>
  75. <button class="qd-btn qd-btn-outline" @click="resetQuestionnaire">继续评估其他维度</button>
  76. </view>
  77. </view>
  78. </template>
  79. </view>
  80. </template>
  81. <script>
  82. import { submitDimensionQuestionnaire } from '@/utils/api'
  83. export default {
  84. data() {
  85. return {
  86. loading: false,
  87. step: 1,
  88. dimensions: [
  89. { code: 'growth', label: '生长发育', icon: '📈' },
  90. { code: 'sleep', label: '睡眠质量', icon: '🛌' },
  91. { code: 'vision', label: '视力健康', icon: '👁️' },
  92. { code: 'immunity', label: '免疫力', icon: '🛡️' },
  93. { code: 'nutrition', label: '营养均衡', icon: '🥗' },
  94. { code: 'gut', label: '肠胃健康', icon: '🌿' },
  95. { code: 'exercise', label: '运动活力', icon: '💪' }
  96. ],
  97. selectedDimension: '',
  98. questions: {
  99. growth: [
  100. { question: '孩子的身高体重在同龄人中属于?', options: [
  101. { label: '偏高/偏壮', value: 'tall' },
  102. { label: '正常范围', value: 'normal' },
  103. { label: '偏矮/偏瘦', value: 'short' },
  104. { label: '不清楚', value: 'unknown' }
  105. ]},
  106. { question: '过去半年身高增长情况?', options: [
  107. { label: '持续增长,速度正常', value: 'steady' },
  108. { label: '增长缓慢', value: 'slow' },
  109. { label: '没有明显变化', value: 'none' },
  110. { label: '没留意', value: 'unknown' }
  111. ]}
  112. ],
  113. sleep: [
  114. { question: '孩子平均每天睡眠时长?', options: [
  115. { label: '不足6小时', value: 'lt6' },
  116. { label: '6-8小时', value: '6to8' },
  117. { label: '8-10小时', value: '8to10' },
  118. { label: '超过10小时', value: 'gt10' }
  119. ]},
  120. { question: '孩子睡眠质量如何?', options: [
  121. { label: '很好,倒头就睡', value: 'good' },
  122. { label: '一般,偶尔翻来覆去', value: 'fair' },
  123. { label: '较差,频繁夜醒/难入睡', value: 'poor' }
  124. ]},
  125. { question: '孩子是否有规律的作息时间?', options: [
  126. { label: '作息很规律', value: 'regular' },
  127. { label: '基本规律', value: 'mostly' },
  128. { label: '不太规律', value: 'irregular' }
  129. ]}
  130. ],
  131. vision: [
  132. { question: '孩子目前是否佩戴眼镜?', options: [
  133. { label: '不戴眼镜', value: 'no' },
  134. { label: '已戴眼镜', value: 'yes' }
  135. ]},
  136. { question: '孩子每天的屏幕使用时间(手机/平板/电视)?', options: [
  137. { label: '少于1小时', value: 'lt1' },
  138. { label: '1-2小时', value: '1to2' },
  139. { label: '2-4小时', value: '2to4' },
  140. { label: '超过4小时', value: 'gt4' }
  141. ]}
  142. ],
  143. immunity: [
  144. { question: '过去半年孩子生病次数?', options: [
  145. { label: '0次,基本不生病', value: 'zero' },
  146. { label: '1-2次,偶尔感冒', value: 'few' },
  147. { label: '3-5次,经常生病', value: 'some' },
  148. { label: '6次以上,频繁生病', value: 'many' }
  149. ]},
  150. { question: '生病严重程度如何?', options: [
  151. { label: '从未住院,吃药就好', value: 'mild' },
  152. { label: '偶尔需要输液/雾化', value: 'moderate' },
  153. { label: '曾住院治疗', value: 'severe' }
  154. ]},
  155. { question: '孩子是否有过敏史?', options: [
  156. { label: '没有过敏', value: 'none' },
  157. { label: '有轻微过敏(花粉/尘螨等)', value: 'mild' },
  158. { label: '有食物/药物过敏', value: 'severe' }
  159. ]}
  160. ],
  161. nutrition: [
  162. { question: '孩子挑食/偏食程度?', options: [
  163. { label: '不挑食,什么都吃', value: 'none' },
  164. { label: '挑几样特定的食物', value: 'mild' },
  165. { label: '严重挑食,种类很少', value: 'severe' }
  166. ]},
  167. { question: '每周大概吃多少种不同食材?', options: [
  168. { label: '超过20种,饮食多样', value: 'diverse' },
  169. { label: '10-20种,比较丰富', value: 'moderate' },
  170. { label: '少于10种,比较单一', value: 'limited' }
  171. ]},
  172. { question: '孩子是否在使用营养补充剂?', options: [
  173. { label: '按医嘱使用多种补充剂', value: 'multiple' },
  174. { label: '使用维生素D/钙等基础补充剂', value: 'basic' },
  175. { label: '不使用任何补充剂', value: 'none' }
  176. ]}
  177. ],
  178. gut: [
  179. { question: '孩子最近消化舒适度如何?', options: [
  180. { label: '很好,消化顺畅', value: 'good' },
  181. { label: '一般,偶尔肚子不舒服', value: 'fair' },
  182. { label: '较差,经常腹痛/腹胀/腹泻', value: 'poor' }
  183. ]},
  184. { question: '孩子排便频率?', options: [
  185. { label: '每天1-2次,规律', value: 'regular' },
  186. { label: '隔天一次,基本规律', value: 'mostly' },
  187. { label: '不规律,经常便秘或腹泻', value: 'irregular' }
  188. ]},
  189. { question: '孩子是否有肠胃相关诊断?', options: [
  190. { label: '没有肠胃问题', value: 'none' },
  191. { label: '有偶发性消化不良', value: 'mild' },
  192. { label: '有确诊肠胃问题', value: 'diagnosed' }
  193. ]}
  194. ],
  195. exercise: [
  196. { question: '孩子每周运动频率?', options: [
  197. { label: '每天运动', value: 'daily' },
  198. { label: '每周3-5次', value: 'often' },
  199. { label: '每周1-2次', value: 'sometimes' },
  200. { label: '基本不运动', value: 'rarely' }
  201. ]},
  202. { question: '孩子每次运动时长?', options: [
  203. { label: '超过1小时', value: 'gt1h' },
  204. { label: '30分钟到1小时', value: '30to1h' },
  205. { label: '少于30分钟', value: 'lt30m' }
  206. ]},
  207. { question: '孩子喜欢的运动类型?', options: [
  208. { label: '球类/田径等竞技运动', value: 'competitive' },
  209. { label: '游泳/骑行等有氧运动', value: 'aerobic' },
  210. { label: '跳绳/体操等灵活性运动', value: 'flexibility' },
  211. { label: '不太喜欢运动', value: 'sedentary' }
  212. ]}
  213. ]
  214. },
  215. currentQIndex: 0,
  216. answers: {},
  217. currentAnswer: '',
  218. result: null
  219. }
  220. },
  221. computed: {
  222. currentDimLabel: function() {
  223. var dim = this.dimensions.find(function(d) { return d.code === this.selectedDimension }.bind(this))
  224. return dim ? dim.label : ''
  225. },
  226. currentQuestions: function() {
  227. return this.questions[this.selectedDimension] || []
  228. },
  229. currentQuestion: function() {
  230. return this.currentQuestions[this.currentQIndex] || {}
  231. },
  232. resultIcon: function() {
  233. if (!this.result) return '✅'
  234. var level = this.result.level
  235. if (level === 'excellent') return '🌟'
  236. if (level === 'good') return '👍'
  237. if (level === 'fair') return '💪'
  238. return '📋'
  239. }
  240. },
  241. methods: {
  242. startQuestions: function() {
  243. if (!this.selectedDimension) return
  244. this.currentQIndex = 0
  245. this.answers = {}
  246. this.currentAnswer = ''
  247. this.step = 2
  248. },
  249. nextQuestion: function() {
  250. if (!this.currentAnswer) return
  251. var key = 'q' + this.currentQIndex
  252. this.answers[key] = this.currentAnswer
  253. if (this.currentQIndex < this.currentQuestions.length - 1) {
  254. this.currentQIndex++
  255. this.currentAnswer = this.answers['q' + this.currentQIndex] || ''
  256. } else {
  257. this.submitAnswers()
  258. }
  259. },
  260. prevQuestion: function() {
  261. if (this.currentQIndex > 0) {
  262. var key = 'q' + this.currentQIndex
  263. this.answers[key] = this.currentAnswer
  264. this.currentQIndex--
  265. this.currentAnswer = this.answers['q' + this.currentQIndex] || ''
  266. }
  267. },
  268. submitAnswers: function() {
  269. var _this = this
  270. var answersList = []
  271. for (var i = 0; i < this.currentQuestions.length; i++) {
  272. var ansKey = 'q' + i
  273. answersList.push({
  274. question: this.currentQuestions[i].question,
  275. answer: _this.answers[ansKey] || ''
  276. })
  277. }
  278. var data = {
  279. memberId: this.activeChildId,
  280. dimension: this.selectedDimension,
  281. answers: answersList
  282. }
  283. _this.loading = true
  284. submitDimensionQuestionnaire(data).then(function(res) {
  285. _this.loading = false
  286. if (res.code === 200) {
  287. _this.result = {
  288. score: res.data && res.data.score ? res.data.score : 75,
  289. level: res.data && res.data.level ? res.data.level : 'good',
  290. suggestion: res.data && res.data.suggestion ? res.data.suggestion : ''
  291. }
  292. _this.step = 3
  293. } else {
  294. // Fallback: compute estimated score locally
  295. _this.result = {
  296. score: 70,
  297. level: 'good',
  298. suggestion: '可根据评估结果关注相关健康维度。'
  299. }
  300. _this.step = 3
  301. }
  302. }).catch(function() {
  303. _this.loading = false
  304. _this.result = { score: 70, level: 'good', suggestion: '提交成功,可根据评估结果关注相关健康维度。' }
  305. _this.step = 3
  306. })
  307. },
  308. levelText: function(level) {
  309. var map = { excellent: '优秀', good: '良好', fair: '一般', poor: '较差' }
  310. return map[level] || '一般'
  311. },
  312. goToDimensions: function() {
  313. uni.navigateTo({ url: '/pages/body/health-dimensions?childId=' + this.activeChildId })
  314. },
  315. resetQuestionnaire: function() {
  316. this.step = 1
  317. this.selectedDimension = ''
  318. this.currentQIndex = 0
  319. this.answers = {}
  320. this.currentAnswer = ''
  321. this.result = null
  322. }
  323. },
  324. onLoad: function(options) {
  325. this.activeChildId = options.childId || ''
  326. }
  327. }
  328. </script>
  329. <style scoped>
  330. .qd-container {
  331. min-height: 100vh;
  332. background: #F5FAFE;
  333. padding: 20rpx 30rpx 100rpx;
  334. }
  335. .qd-loading {
  336. display: flex;
  337. justify-content: center;
  338. align-items: center;
  339. height: 60vh;
  340. }
  341. .loading-text {
  342. font-size: 28rpx;
  343. color: #999;
  344. }
  345. .qd-step {
  346. animation: fadeIn 0.3s ease;
  347. }
  348. @keyframes fadeIn {
  349. from { opacity: 0; transform: translateY(10rpx); }
  350. to { opacity: 1; transform: translateY(0); }
  351. }
  352. .qd-header {
  353. text-align: center;
  354. padding: 30rpx 0;
  355. }
  356. .qd-title {
  357. font-size: 36rpx;
  358. font-weight: 600;
  359. color: #333;
  360. }
  361. .qd-desc {
  362. font-size: 26rpx;
  363. color: #888;
  364. margin-top: 10rpx;
  365. }
  366. .qd-dim-list {
  367. display: flex;
  368. flex-wrap: wrap;
  369. gap: 20rpx;
  370. margin: 20rpx 0;
  371. }
  372. .qd-dim-item {
  373. width: 45%;
  374. background: #fff;
  375. border-radius: 16rpx;
  376. padding: 30rpx 20rpx;
  377. display: flex;
  378. flex-direction: column;
  379. align-items: center;
  380. border: 2rpx solid #eee;
  381. position: relative;
  382. box-sizing: border-box;
  383. }
  384. .qd-dim-item.active {
  385. border-color: #4A9BD7;
  386. background: #F0F8FF;
  387. }
  388. .qd-dim-icon {
  389. font-size: 48rpx;
  390. margin-bottom: 10rpx;
  391. }
  392. .qd-dim-label {
  393. font-size: 28rpx;
  394. color: #333;
  395. font-weight: 500;
  396. }
  397. .qd-dim-check {
  398. position: absolute;
  399. top: 10rpx;
  400. right: 10rpx;
  401. width: 36rpx;
  402. height: 36rpx;
  403. background: #4A9BD7;
  404. color: #fff;
  405. border-radius: 50%;
  406. text-align: center;
  407. line-height: 36rpx;
  408. font-size: 22rpx;
  409. }
  410. .qd-progress {
  411. font-size: 26rpx;
  412. color: #4A9BD7;
  413. margin-top: 10rpx;
  414. }
  415. .qd-progress-bar {
  416. height: 6rpx;
  417. background: #E8E8E8;
  418. border-radius: 3rpx;
  419. margin: 20rpx 0;
  420. }
  421. .qd-progress-fill {
  422. height: 100%;
  423. background: linear-gradient(90deg, #4A9BD7, #63B3ED);
  424. border-radius: 3rpx;
  425. transition: width 0.3s ease;
  426. }
  427. .qd-question-card {
  428. background: #fff;
  429. border-radius: 20rpx;
  430. padding: 40rpx 30rpx;
  431. margin: 20rpx 0;
  432. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  433. }
  434. .qd-q-text {
  435. font-size: 32rpx;
  436. font-weight: 500;
  437. color: #333;
  438. line-height: 1.5;
  439. }
  440. .qd-options {
  441. margin-top: 30rpx;
  442. }
  443. .qd-option {
  444. display: flex;
  445. align-items: center;
  446. justify-content: space-between;
  447. padding: 24rpx 20rpx;
  448. border: 2rpx solid #eee;
  449. border-radius: 12rpx;
  450. margin-bottom: 16rpx;
  451. transition: all 0.2s;
  452. }
  453. .qd-option.selected {
  454. border-color: #4A9BD7;
  455. background: #F0F8FF;
  456. }
  457. .qd-opt-label {
  458. font-size: 28rpx;
  459. color: #333;
  460. flex: 1;
  461. }
  462. .qd-opt-check {
  463. width: 36rpx;
  464. height: 36rpx;
  465. background: #4A9BD7;
  466. color: #fff;
  467. border-radius: 50%;
  468. text-align: center;
  469. line-height: 36rpx;
  470. font-size: 22rpx;
  471. }
  472. .qd-footer {
  473. display: flex;
  474. gap: 20rpx;
  475. margin-top: 40rpx;
  476. justify-content: center;
  477. }
  478. .qd-btn {
  479. flex: 1;
  480. height: 88rpx;
  481. line-height: 88rpx;
  482. background: linear-gradient(135deg, #4A9BD7, #63B3ED);
  483. color: #fff;
  484. font-size: 32rpx;
  485. border-radius: 44rpx;
  486. border: none;
  487. text-align: center;
  488. }
  489. .qd-btn[disabled] {
  490. opacity: 0.5;
  491. }
  492. .qd-btn-prev {
  493. flex: 0.4;
  494. background: #f0f0f0;
  495. color: #666;
  496. }
  497. .qd-btn-outline {
  498. background: #fff;
  499. color: #4A9BD7;
  500. border: 2rpx solid #4A9BD7;
  501. }
  502. .qd-result {
  503. text-align: center;
  504. padding: 60rpx 0;
  505. }
  506. .qd-result-icon {
  507. font-size: 80rpx;
  508. }
  509. .qd-result-title {
  510. font-size: 40rpx;
  511. font-weight: 600;
  512. color: #333;
  513. margin: 20rpx 0;
  514. }
  515. .qd-result-score {
  516. font-size: 32rpx;
  517. color: #666;
  518. margin-bottom: 20rpx;
  519. }
  520. .qd-result-level {
  521. font-size: 28rpx;
  522. padding: 8rpx 24rpx;
  523. border-radius: 20rpx;
  524. display: inline-block;
  525. margin-bottom: 20rpx;
  526. }
  527. .qd-result-level.level-excellent { background: #E8F5E9; color: #2E7D32; }
  528. .qd-result-level.level-good { background: #E3F2FD; color: #1565C0; }
  529. .qd-result-level.level-fair { background: #FFF8E1; color: #F57F17; }
  530. .qd-result-level.level-poor { background: #FFEBEE; color: #C62828; }
  531. .qd-result-tip {
  532. font-size: 28rpx;
  533. color: #888;
  534. margin-top: 20rpx;
  535. line-height: 1.6;
  536. padding: 0 30rpx;
  537. }
  538. </style>