perception-test.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <view class="test-container">
  3. <!-- 步骤一:测试说明 -->
  4. <view class="step-panel" v-if="step === 'instruction'">
  5. <view class="title-icon">👁️</view>
  6. <text class="test-title">感知觉测试</text>
  7. <text class="test-desc">屏幕上会出现 6 个相似的图案,其中有一个与众不同。</text>
  8. <text class="test-desc">请快速找出"不一样"的那个,点击它即可!</text>
  9. <text class="test-meta">共 8 题 · 每题限时 8 秒 · 超时自动进入下一题</text>
  10. <button class="btn-primary" @click="startTest">开始测试</button>
  11. </view>
  12. <!-- 步骤二:答题中 -->
  13. <view class="step-panel" v-else-if="step === 'playing'">
  14. <view class="progress-row">
  15. <text class="progress-text">第 {{ currentIndex + 1 }}/8 题</text>
  16. <text class="progress-text">答对 {{ correctCount }} 题</text>
  17. </view>
  18. <view class="timer-bar">
  19. <view class="timer-fill" :style="timerBarStyle"></view>
  20. </view>
  21. <text class="timer-text">{{ timerLeft }}秒</text>
  22. <text class="question-hint" v-if="currentQuestion">{{ currentQuestion.hint }}</text>
  23. <view class="grid-wrap">
  24. <view
  25. class="grid-cell"
  26. v-for="(item, index) in currentQuestion.items"
  27. :key="getItemKey(item, index)"
  28. :class="{
  29. 'cell-correct': lockInput && index === currentQuestion.oddIndex,
  30. 'cell-wrong': tappedIndex === index && index !== currentQuestion.oddIndex
  31. }"
  32. @click="handleTap(index)"
  33. >
  34. <text class="cell-emoji">{{ item }}</text>
  35. </view>
  36. </view>
  37. </view>
  38. <!-- 步骤三:结果 -->
  39. <view class="step-panel" v-else>
  40. <text class="result-title">测试完成</text>
  41. <view class="score-circle">
  42. <text class="score-num">{{ score }}</text>
  43. </view>
  44. <view class="score-row">
  45. <text class="score-label">答对 {{ correctCount }}/8 题</text>
  46. <text class="dimension-tag">感知觉</text>
  47. </view>
  48. <view class="result-tip" v-if="score >= 75">
  49. <text class="result-tip-text">感官敏锐,找不同又快又准!</text>
  50. </view>
  51. <view class="result-tip" v-else-if="score >= 50">
  52. <text class="result-tip-text">表现不错,再细心一点会更好!</text>
  53. </view>
  54. <view class="result-tip" v-else>
  55. <text class="result-tip-text">别灰心,多练几次会越来越快!</text>
  56. </view>
  57. <button class="btn-primary" @click="submitResult">提交成绩</button>
  58. <button class="btn-secondary" @click="retryTest">再测一次</button>
  59. </view>
  60. </view>
  61. </template>
  62. <script>
  63. import { submitCognitiveSelfTest } from '../../utils/api.js'
  64. // 题库:6 个相似图案,其中一个是"不同"的(颜色/形状变体)
  65. var QUESTION_BANK = [
  66. { items: ['🟦','🟦','🟦','🟦','🟦','🟥'], oddIndex: 5, hint: '找出不同的颜色' },
  67. { items: ['🟨','🟨','🟨','🟨','🟨','🟩'], oddIndex: 3, hint: '找出不同的颜色' },
  68. { items: ['🟪','🟪','🟪','🟪','🟪','🟦'], oddIndex: 4, hint: '找出不同的颜色' },
  69. { items: ['🟩','🟩','🟩','🟩','🟩','🟨'], oddIndex: 1, hint: '找出不同的颜色' },
  70. { items: ['🟥','🟥','🟥','🟥','🟥','🟪'], oddIndex: 2, hint: '找出不同的颜色' },
  71. { items: ['🟡','🟡','🟡','🟡','🟡','🟢'], oddIndex: 0, hint: '找出不同的颜色' },
  72. { items: ['⬛','⬛','⬛','⬛','⬛','⬜'], oddIndex: 5, hint: '找出不同的颜色' },
  73. { items: ['🔴','🔴','🔴','🔴','🔴','🟡'], oddIndex: 2, hint: '找出不同的颜色' },
  74. { items: ['🟨','🟨','🟨','🟨','🟨','🟥'], oddIndex: 0, hint: '找出不同的颜色' },
  75. { items: ['🟩','🟩','🟩','🟩','🟩','🟦'], oddIndex: 4, hint: '找出不同的颜色' },
  76. { items: ['🟪','🟪','🟪','🟪','🟪','⬛'], oddIndex: 1, hint: '找出不同的图案' },
  77. { items: ['⬜','⬜','⬜','⬜','⬜','⬛'], oddIndex: 3, hint: '找出不同的图案' },
  78. { items: ['🔴','🔴','🔴','🔴','🔴','🟥'], oddIndex: 5, hint: '找出不同的图案' },
  79. { items: ['🟥','🟥','🟥','🟥','🟥','🔴'], oddIndex: 2, hint: '找出不同的图案' },
  80. { items: ['🟡','🟡','🟡','🟡','🟡','🟢'], oddIndex: 1, hint: '找出不同的颜色' },
  81. { items: ['🟦','🟦','🟦','🟦','🟦','⬜'], oddIndex: 4, hint: '找出不同的图案' },
  82. { items: ['⬡','⬡','⬡','⬡','⬡','⬢'], oddIndex: 5, hint: '找出不同的形状' },
  83. { items: ['◈','◈','◈','◈','◈','◆'], oddIndex: 1, hint: '找出不同的形状' },
  84. { items: ['○','○','○','○','○','◇'], oddIndex: 3, hint: '找出不同的形状' },
  85. { items: ['◆','◆','◆','◆','◆','◈'], oddIndex: 0, hint: '找出不同的形状' },
  86. { items: ['⬢','⬢','⬢','⬢','⬢','⬡'], oddIndex: 2, hint: '找出不同的形状' },
  87. { items: ['◇','◇','◇','◇','◇','○'], oddIndex: 5, hint: '找出不同的形状' }
  88. ]
  89. export default {
  90. data() {
  91. return {
  92. step: 'instruction',
  93. memberId: '',
  94. roundQuestions: [],
  95. currentIndex: 0,
  96. currentQuestion: null,
  97. correctCount: 0,
  98. timerLeft: 8,
  99. timer: null,
  100. nextTimer: null,
  101. lockInput: false,
  102. tappedIndex: -1,
  103. score: 0
  104. }
  105. },
  106. computed: {
  107. timerPercent: function() {
  108. return Math.round(this.timerLeft / 8 * 100)
  109. },
  110. timerBarStyle: function() {
  111. return 'width:' + this.timerPercent + '%'
  112. }
  113. },
  114. onLoad(options) {
  115. if (options && options.memberId) {
  116. this.memberId = options.memberId
  117. }
  118. },
  119. onUnload() {
  120. this.stopTimer()
  121. if (this.nextTimer) {
  122. clearTimeout(this.nextTimer)
  123. this.nextTimer = null
  124. }
  125. },
  126. methods: {
  127. getItemKey: function(item, index) {
  128. return item + '-' + index
  129. },
  130. startTest: function() {
  131. this.correctCount = 0
  132. this.currentIndex = 0
  133. this.score = 0
  134. this.lockInput = false
  135. this.tappedIndex = -1
  136. // 洗牌后取前 8 题
  137. var bank = QUESTION_BANK.slice()
  138. for (var i = bank.length - 1; i > 0; i--) {
  139. var j = Math.floor(Math.random() * (i + 1))
  140. var tmp = bank[i]
  141. bank[i] = bank[j]
  142. bank[j] = tmp
  143. }
  144. this.roundQuestions = bank.slice(0, 8)
  145. this.step = 'playing'
  146. this.loadQuestion(0)
  147. },
  148. loadQuestion: function(index) {
  149. this.currentIndex = index
  150. this.currentQuestion = this.roundQuestions[index]
  151. this.tappedIndex = -1
  152. this.lockInput = false
  153. this.timerLeft = 8
  154. this.startTimer()
  155. },
  156. startTimer: function() {
  157. this.stopTimer()
  158. this.timer = setInterval(() => {
  159. this.timerLeft--
  160. if (this.timerLeft <= 0) {
  161. this.timerLeft = 0
  162. this.onTimeout()
  163. }
  164. }, 1000)
  165. },
  166. stopTimer: function() {
  167. if (this.timer) {
  168. clearInterval(this.timer)
  169. this.timer = null
  170. }
  171. },
  172. handleTap: function(index) {
  173. if (this.lockInput || !this.currentQuestion) return
  174. this.lockInput = true
  175. this.stopTimer()
  176. this.tappedIndex = index
  177. if (index === this.currentQuestion.oddIndex) {
  178. this.correctCount++
  179. }
  180. this.scheduleNext()
  181. },
  182. onTimeout: function() {
  183. if (this.lockInput) return
  184. this.lockInput = true
  185. this.stopTimer()
  186. this.tappedIndex = -1
  187. // 超时未作答,短暂高亮正确答案后进入下一题
  188. this.scheduleNext()
  189. },
  190. scheduleNext: function() {
  191. if (this.nextTimer) clearTimeout(this.nextTimer)
  192. this.nextTimer = setTimeout(() => {
  193. this.nextTimer = null
  194. this.advance()
  195. }, 700)
  196. },
  197. advance: function() {
  198. if (this.currentIndex + 1 >= this.roundQuestions.length) {
  199. this.finishTest()
  200. } else {
  201. this.loadQuestion(this.currentIndex + 1)
  202. }
  203. },
  204. finishTest: function() {
  205. this.stopTimer()
  206. this.score = Math.round(this.correctCount / this.roundQuestions.length * 100)
  207. this.step = 'result'
  208. },
  209. submitResult: function() {
  210. var memberId = this.memberId || uni.getStorageSync('currentChildId') || ''
  211. if (!memberId) {
  212. uni.showToast({ title: '请先选择孩子', icon: 'none' })
  213. return
  214. }
  215. submitCognitiveSelfTest(memberId, 'perceptionScore', this.score)
  216. .then((res) => {
  217. uni.showToast({ title: '提交成功', icon: 'success' })
  218. setTimeout(() => {
  219. uni.navigateBack()
  220. }, 1500)
  221. })
  222. .catch((err) => {
  223. console.error('提交感知觉测试成绩失败', err)
  224. uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  225. })
  226. },
  227. retryTest: function() {
  228. this.startTest()
  229. }
  230. }
  231. }
  232. </script>
  233. <style scoped>
  234. .test-container {
  235. min-height: 100vh;
  236. background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  237. padding: 60rpx 40rpx;
  238. }
  239. .step-panel {
  240. display: flex;
  241. flex-direction: column;
  242. align-items: center;
  243. }
  244. /* ===== 说明页 ===== */
  245. .title-icon {
  246. font-size: 88rpx;
  247. margin-bottom: 24rpx;
  248. }
  249. .test-title {
  250. font-size: 48rpx;
  251. font-weight: bold;
  252. color: #fff;
  253. margin-bottom: 32rpx;
  254. }
  255. .test-desc {
  256. font-size: 30rpx;
  257. color: rgba(255,255,255,0.7);
  258. line-height: 1.7;
  259. text-align: center;
  260. }
  261. .test-meta {
  262. font-size: 24rpx;
  263. color: rgba(255,255,255,0.45);
  264. margin-top: 24rpx;
  265. margin-bottom: 80rpx;
  266. text-align: center;
  267. }
  268. .btn-primary {
  269. background: linear-gradient(135deg, #4ECDC4, #45B7AA);
  270. color: #fff;
  271. font-size: 34rpx;
  272. font-weight: bold;
  273. width: 480rpx;
  274. height: 92rpx;
  275. line-height: 92rpx;
  276. border-radius: 46rpx;
  277. border: none;
  278. margin-top: 60rpx;
  279. padding: 0;
  280. }
  281. .btn-primary::after {
  282. border: none;
  283. }
  284. .btn-secondary {
  285. background: rgba(255,255,255,0.1);
  286. color: rgba(255,255,255,0.85);
  287. font-size: 32rpx;
  288. width: 480rpx;
  289. height: 92rpx;
  290. line-height: 92rpx;
  291. border-radius: 46rpx;
  292. border: 2rpx solid rgba(255,255,255,0.25);
  293. margin-top: 32rpx;
  294. padding: 0;
  295. }
  296. .btn-secondary::after {
  297. border: none;
  298. }
  299. /* ===== 答题页 ===== */
  300. .progress-row {
  301. display: flex;
  302. justify-content: space-between;
  303. width: 100%;
  304. margin-bottom: 24rpx;
  305. }
  306. .progress-text {
  307. font-size: 28rpx;
  308. color: rgba(255,255,255,0.7);
  309. }
  310. .timer-bar {
  311. width: 100%;
  312. height: 16rpx;
  313. background: rgba(255,255,255,0.12);
  314. border-radius: 8rpx;
  315. overflow: hidden;
  316. }
  317. .timer-fill {
  318. height: 100%;
  319. background: linear-gradient(90deg, #4ECDC4, #45B7AA);
  320. border-radius: 8rpx;
  321. transition: width 0.9s linear;
  322. }
  323. .timer-text {
  324. display: block;
  325. font-size: 26rpx;
  326. color: #4ECDC4;
  327. margin-top: 12rpx;
  328. text-align: center;
  329. }
  330. .question-hint {
  331. display: block;
  332. font-size: 32rpx;
  333. font-weight: bold;
  334. color: #fff;
  335. margin: 40rpx 0 32rpx;
  336. text-align: center;
  337. }
  338. .grid-wrap {
  339. display: flex;
  340. flex-wrap: wrap;
  341. justify-content: space-between;
  342. width: 100%;
  343. }
  344. .grid-cell {
  345. display: flex;
  346. align-items: center;
  347. justify-content: center;
  348. width: 30%;
  349. height: 190rpx;
  350. background: #2a2a4a;
  351. border-radius: 24rpx;
  352. border: 4rpx solid transparent;
  353. margin-bottom: 24rpx;
  354. box-sizing: border-box;
  355. }
  356. .grid-cell:active {
  357. background: #35406b;
  358. }
  359. .cell-emoji {
  360. font-size: 50rpx;
  361. }
  362. .cell-correct {
  363. background: #2f4a4a;
  364. border-color: #4ECDC4;
  365. }
  366. .cell-wrong {
  367. background: #4a2f35;
  368. border-color: #ff6b6b;
  369. }
  370. /* ===== 结果页 ===== */
  371. .result-title {
  372. font-size: 40rpx;
  373. font-weight: bold;
  374. color: #fff;
  375. margin-bottom: 48rpx;
  376. }
  377. .score-circle {
  378. display: flex;
  379. align-items: center;
  380. justify-content: center;
  381. width: 260rpx;
  382. height: 260rpx;
  383. border-radius: 50%;
  384. border: 10rpx solid #4ECDC4;
  385. background: rgba(78,205,196,0.1);
  386. margin-bottom: 36rpx;
  387. }
  388. .score-num {
  389. font-size: 88rpx;
  390. font-weight: bold;
  391. color: #4ECDC4;
  392. }
  393. .score-row {
  394. display: flex;
  395. align-items: center;
  396. margin-bottom: 24rpx;
  397. }
  398. .score-label {
  399. font-size: 30rpx;
  400. color: rgba(255,255,255,0.7);
  401. margin-right: 20rpx;
  402. }
  403. .dimension-tag {
  404. font-size: 24rpx;
  405. color: #4ECDC4;
  406. border: 2rpx solid #4ECDC4;
  407. border-radius: 24rpx;
  408. padding: 6rpx 24rpx;
  409. }
  410. .result-tip {
  411. margin-bottom: 20rpx;
  412. }
  413. .result-tip-text {
  414. font-size: 28rpx;
  415. color: rgba(255,255,255,0.6);
  416. }
  417. </style>