screening.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <template>
  2. <view class="screen-container">
  3. <view class="screen-header">
  4. <text class="screen-title">心理状态自评</text>
  5. <text class="screen-subtitle">定期评估,了解自己的心理状态</text>
  6. </view>
  7. <view class="section" v-if="!currentScreen">
  8. <view class="scale-card" @click="startScreen('PHQ9')">
  9. <view class="scale-icon-wrap" style="background:#FFF0F5">
  10. <text class="scale-icon">&#x1F9E0;</text>
  11. </view>
  12. <view class="scale-info">
  13. <text class="scale-name">PHQ-9 抑郁筛查</text>
  14. <text class="scale-desc">过去两周,您是否被以下问题困扰?</text>
  15. </view>
  16. <text class="scale-arrow">&gt;</text>
  17. </view>
  18. <view class="scale-card" @click="startScreen('GAD7')">
  19. <view class="scale-icon-wrap" style="background:#F0F4FF">
  20. <text class="scale-icon">&#x1F4A8;</text>
  21. </view>
  22. <view class="scale-info">
  23. <text class="scale-name">GAD-7 焦虑筛查</text>
  24. <text class="scale-desc">过去两周,您是否被以下问题困扰?</text>
  25. </view>
  26. <text class="scale-arrow">&gt;</text>
  27. </view>
  28. </view>
  29. <view class="section" v-if="currentScreen">
  30. <view class="quiz-header">
  31. <text class="quiz-title">{{ currentScreen === 'PHQ9' ? 'PHQ-9 抑郁筛查' : 'GAD-7 焦虑筛查' }}</text>
  32. <text class="quiz-progress">{{ currentStep + 1 }}/{{ questions.length }}</text>
  33. </view>
  34. <view class="progress-track">
  35. <view class="progress-fill" :style="{ width: ((currentStep + 1) / questions.length * 100) + '%' }"></view>
  36. </view>
  37. <view class="question-card">
  38. <text class="question-num">Q{{ currentStep + 1 }}</text>
  39. <text class="question-text">{{ questions[currentStep] }}</text>
  40. </view>
  41. <view class="options-list">
  42. <view
  43. v-for="(opt, oi) in answerOptions"
  44. :key="oi"
  45. :class="['option-btn', answers[currentStep] === oi ? 'option-active' : '']"
  46. @click="selectAnswer(oi)">
  47. <view :class="['option-radio', answers[currentStep] === oi ? 'radio-checked' : '']">
  48. <view v-if="answers[currentStep] === oi" class="radio-dot"></view>
  49. </view>
  50. <text class="option-text">{{ opt }}</text>
  51. </view>
  52. </view>
  53. <view class="quiz-nav">
  54. <button class="nav-btn prev-btn" v-if="currentStep > 0" @click="prevStep">上一题</button>
  55. <button class="nav-btn next-btn" v-if="currentStep < questions.length - 1" @click="nextStep">下一题</button>
  56. <button class="nav-btn submit-btn" v-if="currentStep === questions.length - 1" @click="submitScreen">提交问卷</button>
  57. </view>
  58. </view>
  59. <view class="section" v-if="result">
  60. <view class="result-header">
  61. <text class="result-type">{{ currentScreen === 'PHQ9' ? 'PHQ-9 抑郁筛查' : 'GAD-7 焦虑筛查' }}</text>
  62. <view :class="['result-score-circle', severityClass]">
  63. <text class="result-score">{{ result.totalScore }}</text>
  64. <text class="result-score-label">分</text>
  65. </view>
  66. <text :class="['result-severity', severityClass]">{{ severityLabel }}</text>
  67. </view>
  68. <view class="suggestion-card">
  69. <text class="suggestion-title">评估建议</text>
  70. <text class="suggestion-text">{{ result.suggestions }}</text>
  71. </view>
  72. <view class="risk-section" v-if="result.riskFlags && result.riskFlags.length > 0">
  73. <view class="risk-item" v-for="flag in result.riskFlags" :key="flag">
  74. <text class="risk-icon">&#x26A0;&#xFE0F;</text>
  75. <text class="risk-text">{{ riskFlagLabel(flag) }}</text>
  76. </view>
  77. </view>
  78. <view class="crisis-banner" v-if="showCrisisBanner">
  79. <text class="crisis-title">&#x1F6A8; 需要帮助?</text>
  80. <text class="crisis-phone">全国心理危机干预热线</text>
  81. <text class="crisis-number">400-161-9995</text>
  82. <text class="crisis-hint">24小时 · 免费 · 专业</text>
  83. </view>
  84. <view class="result-actions">
  85. <button class="action-btn primary-btn" @click="goTrend">查看情绪趋势</button>
  86. <button class="action-btn secondary-btn" @click="resetScreen">再测一次</button>
  87. </view>
  88. </view>
  89. <view class="loading-mask" v-if="submitting">
  90. <view class="loading-box">
  91. <text class="loading-text">提交中...</text>
  92. </view>
  93. </view>
  94. </view>
  95. </template>
  96. <script>
  97. import config from '../../config'
  98. export default {
  99. data() {
  100. return {
  101. currentScreen: null,
  102. currentStep: 0,
  103. answers: [],
  104. submitting: false,
  105. result: null,
  106. phq9Questions: [
  107. '\u505A\u4E8B\u65F6\u63D0\u4E0D\u8D77\u52B2\u6216\u6CA1\u6709\u5174\u8DA3',
  108. '\u611F\u5230\u5FC3\u60C5\u4F4E\u843D\u3001\u6CAE\u4E27\u6216\u7EDD\u671B',
  109. '\u5165\u7761\u56F0\u96BE\u3001\u7761\u4E0D\u5B89\u7A33\u6216\u7761\u7720\u8FC7\u591A',
  110. '\u611F\u89C9\u75B2\u4E4F\u6216\u6CA1\u6709\u6D3B\u529B',
  111. '\u98DF\u6B32\u4E0D\u632F\u6216\u5403\u592A\u591A',
  112. '\u89C9\u5F97\u81EA\u5DF1\u5F88\u7CDF\u2014\u2014\u6216\u89C9\u5F97\u81EA\u5DF1\u5F88\u5931\u8D25',
  113. '\u5BF9\u4E8B\u7269\u4E13\u6CE8\u6709\u56F0\u96BE',
  114. '\u884C\u52A8\u6216\u8BF4\u8BDD\u901F\u5EA6\u7F13\u6162',
  115. '\u6709\u4E0D\u5982\u6B7B\u6389\u6216\u4F24\u5BB3\u81EA\u5DF1\u7684\u5FF5\u5934'
  116. ],
  117. gad7Questions: [
  118. '\u611F\u5230\u7D27\u5F20\u3001\u7126\u8651\u6216\u70E6\u8E81',
  119. '\u65E0\u6CD5\u505C\u6B62\u6216\u63A7\u5236\u62C5\u5FE7',
  120. '\u5BF9\u5404\u79CD\u5404\u6837\u7684\u4E8B\u60C5\u62C5\u5FE7\u8FC7\u591A',
  121. '\u5F88\u96BE\u653E\u677E\u4E0B\u6765',
  122. '\u7531\u4E8E\u4E0D\u5B89\u800C\u65E0\u6CD5\u9759\u5750',
  123. '\u53D8\u5F97\u5BB9\u6613\u70E6\u607C\u6216\u6025\u8E81',
  124. '\u611F\u5230\u5BB3\u6015\uFF0C\u597D\u50CF\u53EF\u80FD\u53D1\u751F\u53EF\u6015\u7684\u4E8B\u60C5'
  125. ],
  126. answerOptions: ['\u5B8C\u5168\u4E0D\u4F1A', '\u597D\u51E0\u5929', '\u4E00\u534A\u4EE5\u4E0A\u7684\u5929\u6570', '\u51E0\u4E4E\u6BCF\u5929'],
  127. riskFlagLabels: {
  128. 'suicide_ideation': '存在自杀/自伤相关念头,请立即寻求帮助',
  129. 'core_depression_symptoms': '核心抑郁症状明显,建议关注',
  130. 'core_anxiety_symptoms': '核心焦虑症状明显,建议关注'
  131. }
  132. }
  133. },
  134. computed: {
  135. questions() {
  136. if (this.currentScreen === 'PHQ9') return this.phq9Questions
  137. if (this.currentScreen === 'GAD7') return this.gad7Questions
  138. return []
  139. },
  140. severityLabel() {
  141. if (!this.result) return ''
  142. var map = { 'none': '状态良好', 'mild': '轻度', 'moderate': '中度', 'moderately_severe': '中重度', 'severe': '重度' }
  143. return map[this.result.severityLevel] || this.result.severityLevel
  144. },
  145. severityClass() {
  146. if (!this.result) return ''
  147. var map = { 'none': 'severity-good', 'mild': 'severity-mild', 'moderate': 'severity-moderate', 'moderately_severe': 'severity-moderate', 'severe': 'severity-severe' }
  148. return map[this.result.severityLevel] || ''
  149. },
  150. showCrisisBanner() {
  151. return this.result && this.result.riskFlags && this.result.riskFlags.indexOf('suicide_ideation') !== -1
  152. }
  153. },
  154. methods: {
  155. startScreen(type) {
  156. this.currentScreen = type
  157. this.currentStep = 0
  158. this.answers = []
  159. this.result = null
  160. var count = type === 'PHQ9' ? 9 : 7
  161. for (var i = 0; i < count; i++) { this.answers.push(-1) }
  162. },
  163. selectAnswer(oi) {
  164. var newAnswers = this.answers.slice()
  165. newAnswers[this.currentStep] = oi
  166. this.answers = newAnswers
  167. },
  168. nextStep() {
  169. if (this.answers[this.currentStep] === -1) { uni.showToast({ title: '请先选择一项', icon: 'none' }); return }
  170. this.currentStep++
  171. },
  172. prevStep() {
  173. if (this.currentStep > 0) this.currentStep--
  174. },
  175. submitScreen() {
  176. for (var i = 0; i < this.answers.length; i++) {
  177. if (this.answers[i] === -1) { uni.showToast({ title: '请回答所有问题', icon: 'none' }); this.currentStep = i; return }
  178. }
  179. this.submitting = true
  180. const memberId = uni.getStorageSync('currentChildId')
  181. uni.request({
  182. url: config.API_BASE_URL + '/api/mind/screening/submit',
  183. method: 'POST',
  184. data: { memberId, screenType: this.currentScreen, answers: this.answers },
  185. success: (res) => {
  186. this.submitting = false
  187. if (res.data && res.data.code === 200) { this.result = res.data.data }
  188. else { uni.showToast({ title: (res.data && res.data.message) || '提交失败', icon: 'none' }) }
  189. },
  190. fail: () => { this.submitting = false; uni.showToast({ title: '网络错误', icon: 'none' }) }
  191. })
  192. },
  193. resetScreen() { this.currentScreen = null; this.currentStep = 0; this.answers = []; this.result = null },
  194. goTrend() { uni.navigateTo({ url: '/pages/mind-detail/emotion-trend' }) },
  195. riskFlagLabel(flag) { return this.riskFlagLabels[flag] || flag }
  196. }
  197. }
  198. </script>
  199. <style scoped>
  200. .screen-container { min-height: 100vh; background: linear-gradient(180deg, #F0F4FF 0%, #FFFFFF 100%); padding-bottom: 40rpx; }
  201. .screen-header { padding: 40rpx 32rpx 20rpx; }
  202. .screen-title { font-size: 36rpx; font-weight: 700; color: #333; display: block; }
  203. .screen-subtitle { font-size: 26rpx; color: #999; margin-top: 8rpx; display: block; }
  204. .section { margin: 20rpx 32rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
  205. .scale-card { display: flex; align-items: center; padding: 24rpx; background: #FAFAFA; border-radius: 16rpx; margin-bottom: 16rpx; gap: 20rpx; }
  206. .scale-icon-wrap { width: 72rpx; height: 72rpx; border-radius: 36rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  207. .scale-icon { font-size: 36rpx; }
  208. .scale-info { flex: 1; }
  209. .scale-name { font-size: 28rpx; font-weight: 600; color: #333; display: block; }
  210. .scale-desc { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
  211. .scale-arrow { font-size: 28rpx; color: #ccc; }
  212. .quiz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
  213. .quiz-title { font-size: 28rpx; font-weight: 600; color: #333; }
  214. .quiz-progress { font-size: 24rpx; color: #999; }
  215. .progress-track { height: 6rpx; background: #F0F0F0; border-radius: 3rpx; margin-bottom: 24rpx; overflow: hidden; }
  216. .progress-fill { height: 100%; background: linear-gradient(90deg, #6366F1, #8B5CF6); border-radius: 3rpx; transition: width 0.3s; }
  217. .question-card { background: #F8F6FF; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; }
  218. .question-num { font-size: 22rpx; color: #8B5CF6; font-weight: 600; display: block; margin-bottom: 8rpx; }
  219. .question-text { font-size: 30rpx; color: #333; line-height: 1.5; }
  220. .options-list { display: flex; flex-direction: column; gap: 12rpx; }
  221. .option-btn { display: flex; align-items: center; padding: 20rpx 24rpx; border-radius: 14rpx; border: 2rpx solid #F0F0F0; gap: 16rpx; }
  222. .option-active { border-color: #8B5CF6; background: #F8F6FF; }
  223. .option-radio { width: 40rpx; height: 40rpx; border-radius: 20rpx; border: 3rpx solid #D0D0D0; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  224. .radio-checked { border-color: #8B5CF6; }
  225. .radio-dot { width: 22rpx; height: 22rpx; border-radius: 11rpx; background: #8B5CF6; }
  226. .option-text { font-size: 28rpx; color: #444; }
  227. .option-active .option-text { color: #333; font-weight: 500; }
  228. .quiz-nav { display: flex; gap: 16rpx; margin-top: 32rpx; }
  229. .nav-btn { flex: 1; height: 80rpx; line-height: 80rpx; border-radius: 40rpx; font-size: 28rpx; text-align: center; }
  230. .prev-btn { background: #F0F0F0; color: #666; }
  231. .next-btn { background: #6366F1; color: #fff; }
  232. .submit-btn { background: linear-gradient(135deg, #6366F1, #8B5CF6); color: #fff; }
  233. .result-header { display: flex; flex-direction: column; align-items: center; padding: 20rpx 0; }
  234. .result-type { font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
  235. .result-score-circle { width: 120rpx; height: 120rpx; border-radius: 60rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; margin-bottom: 12rpx; }
  236. .result-score { font-size: 48rpx; font-weight: 700; color: #fff; line-height: 1; }
  237. .result-score-label { font-size: 20rpx; color: rgba(255,255,255,0.8); margin-top: 2rpx; }
  238. .result-severity { font-size: 30rpx; font-weight: 600; }
  239. .severity-good { background: #10B981; color: #10B981; }
  240. .severity-mild { background: #F59E0B; color: #F59E0B; }
  241. .severity-moderate { background: #FF8C42; color: #FF8C42; }
  242. .severity-severe { background: #EF4444; color: #EF4444; }
  243. .result-score-circle.severity-good { background: #10B981; }
  244. .result-score-circle.severity-mild { background: #F59E0B; }
  245. .result-score-circle.severity-moderate { background: #FF8C42; }
  246. .result-score-circle.severity-severe { background: #EF4444; }
  247. .suggestion-card { background: #F0FFF4; border-radius: 14rpx; padding: 20rpx; margin-top: 16rpx; }
  248. .suggestion-title { font-size: 24rpx; color: #10B981; font-weight: 600; display: block; margin-bottom: 8rpx; }
  249. .suggestion-text { font-size: 26rpx; color: #555; line-height: 1.6; }
  250. .risk-section { margin-top: 16rpx; }
  251. .risk-item { display: flex; align-items: flex-start; gap: 8rpx; padding: 12rpx 16rpx; background: #FFF5F5; border-radius: 12rpx; margin-bottom: 8rpx; }
  252. .risk-icon { font-size: 24rpx; flex-shrink: 0; }
  253. .risk-text { font-size: 24rpx; color: #E53E3E; line-height: 1.5; }
  254. .crisis-banner { margin-top: 16rpx; padding: 24rpx; background: linear-gradient(135deg, #FF6B6B, #E53E3E); border-radius: 16rpx; text-align: center; }
  255. .crisis-title { font-size: 28rpx; color: #fff; font-weight: 700; display: block; }
  256. .crisis-phone { font-size: 22rpx; color: rgba(255,255,255,0.8); margin-top: 12rpx; display: block; }
  257. .crisis-number { font-size: 40rpx; color: #fff; font-weight: 700; margin-top: 4rpx; display: block; letter-spacing: 2rpx; }
  258. .crisis-hint { font-size: 20rpx; color: rgba(255,255,255,0.7); margin-top: 4rpx; display: block; }
  259. .result-actions { display: flex; gap: 16rpx; margin-top: 28rpx; }
  260. .action-btn { flex: 1; height: 76rpx; line-height: 76rpx; border-radius: 38rpx; font-size: 26rpx; text-align: center; }
  261. .primary-btn { background: #FF6B9D; color: #fff; }
  262. .secondary-btn { background: #F0F0F0; color: #666; }
  263. .loading-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; z-index: 999; }
  264. .loading-box { background: #fff; padding: 40rpx 60rpx; border-radius: 20rpx; }
  265. .loading-text { font-size: 28rpx; color: #666; }
  266. </style>