language-test.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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">观察每组词语,找出与其它三个不同类的那一个。</text>
  8. <text class="test-desc">考验你的词汇理解和分类归纳能力!</text>
  9. <text class="test-meta">共 6 题 · 每题限时 12 秒 · 超时自动进入下一题</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 }}/6 题</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.prompt }}</text>
  23. <view class="option-wrap">
  24. <view
  25. class="option-cell"
  26. v-for="(opt, index) in currentQuestionOptions"
  27. :key="index"
  28. :class="{
  29. 'cell-correct': lockInput && index === currentQuestion.answerIndex,
  30. 'cell-wrong': tappedIndex === index && index !== currentQuestion.answerIndex
  31. }"
  32. @click="handleTap(index)"
  33. >
  34. <text class="option-text">{{ opt }}</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 }}/6 题</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. // 题库:4 个词语,其中一个是"不同类"的
  65. var QUESTION_BANK = [
  66. { prompt: '找出不同类的一项', options: ['苹果', '香蕉', '胡萝卜', '橘子'], answerIndex: 2 },
  67. { prompt: '找出不同类的一项', options: ['狗', '猫', '马', '鱼'], answerIndex: 3 },
  68. { prompt: '找出不同类的一项', options: ['红色', '蓝色', '绿色', '桌子'], answerIndex: 3 },
  69. { prompt: '找出不同类的一项', options: ['钢琴', '小提琴', '吉他', '足球'], answerIndex: 3 },
  70. { prompt: '找出不同类的一项', options: ['春天', '夏天', '秋天', '月亮'], answerIndex: 3 },
  71. { prompt: '找出不同类的一项', options: ['跑步', '游泳', '跳跃', '思考'], answerIndex: 3 },
  72. { prompt: '找出不同类的一项', options: ['父亲', '母亲', '姐姐', '老师'], answerIndex: 3 },
  73. { prompt: '找出不同类的一项', options: ['火车', '飞机', '轮船', '自行车'], answerIndex: 2 },
  74. { prompt: '找出不同类的一项', options: ['黄金', '白银', '铜', '铁'], answerIndex: 0 },
  75. { prompt: '找出不同类的一项', options: ['广州', '深圳', '香港', '北京'], answerIndex: 2 },
  76. { prompt: '找出不同类的一项', options: ['三角形', '圆形', '方形', '菱形'], answerIndex: 1 },
  77. { prompt: '找出不同类的一项', options: ['莎士比亚', '李白', '杜甫', '爱因斯坦'], answerIndex: 3 },
  78. { prompt: '找出不同类的一项', options: ['幸福', '快乐', '悲伤', '满足'], answerIndex: 2 },
  79. { prompt: '找出不同类的一项', options: ['书本', '报纸', '杂志', '电脑'], answerIndex: 3 },
  80. { prompt: '找出不同类的一项', options: ['太阳', '月亮', '星星', '云朵'], answerIndex: 3 },
  81. { prompt: '找出不同类的一项', options: ['钢笔', '铅笔', '毛笔', '画笔'], answerIndex: 0 },
  82. { prompt: '找出不同类的一项', options: ['苹果', '芒果', '榴莲', '土豆'], answerIndex: 3 },
  83. { prompt: '找出不同类的一项', options: ['大海', '河流', '湖泊', '雨水'], answerIndex: 3 },
  84. { prompt: '找出不同类的一项', options: ['红色', '橙色', '黄色', '白色'], answerIndex: 3 },
  85. { prompt: '找出不同类的一项', options: ['钢琴', '鼓', '喇叭', '篮球'], answerIndex: 3 }
  86. ]
  87. var ROUND_COUNT = 6
  88. var TIMER_SECONDS = 12
  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: TIMER_SECONDS,
  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 / TIMER_SECONDS * 100)
  109. },
  110. timerBarStyle: function() {
  111. return 'width:' + this.timerPercent + '%'
  112. },
  113. currentQuestionOptions: function() {
  114. if (!this.currentQuestion) return []
  115. return this.currentQuestion.options
  116. }
  117. },
  118. onLoad: function(options) {
  119. if (options && options.memberId) {
  120. this.memberId = options.memberId
  121. }
  122. },
  123. onUnload: function() {
  124. this.stopTimer()
  125. if (this.nextTimer) {
  126. clearTimeout(this.nextTimer)
  127. this.nextTimer = null
  128. }
  129. },
  130. methods: {
  131. startTest: function() {
  132. this.correctCount = 0
  133. this.currentIndex = 0
  134. this.score = 0
  135. this.lockInput = false
  136. this.tappedIndex = -1
  137. // 洗牌后取前 6 题
  138. var bank = QUESTION_BANK.slice()
  139. for (var i = bank.length - 1; i > 0; i--) {
  140. var j = Math.floor(Math.random() * (i + 1))
  141. var tmp = bank[i]
  142. bank[i] = bank[j]
  143. bank[j] = tmp
  144. }
  145. this.roundQuestions = bank.slice(0, ROUND_COUNT)
  146. this.step = 'playing'
  147. this.loadQuestion(0)
  148. },
  149. loadQuestion: function(index) {
  150. this.currentIndex = index
  151. this.currentQuestion = this.roundQuestions[index]
  152. this.tappedIndex = -1
  153. this.lockInput = false
  154. this.timerLeft = TIMER_SECONDS
  155. this.startTimer()
  156. },
  157. startTimer: function() {
  158. this.stopTimer()
  159. this.timer = setInterval(() => {
  160. this.timerLeft--
  161. if (this.timerLeft <= 0) {
  162. this.timerLeft = 0
  163. this.onTimeout()
  164. }
  165. }, 1000)
  166. },
  167. stopTimer: function() {
  168. if (this.timer) {
  169. clearInterval(this.timer)
  170. this.timer = null
  171. }
  172. },
  173. handleTap: function(index) {
  174. if (this.lockInput || !this.currentQuestion) return
  175. this.lockInput = true
  176. this.stopTimer()
  177. this.tappedIndex = index
  178. if (index === this.currentQuestion.answerIndex) {
  179. this.correctCount++
  180. }
  181. this.scheduleNext()
  182. },
  183. onTimeout: function() {
  184. if (this.lockInput) return
  185. this.lockInput = true
  186. this.stopTimer()
  187. this.tappedIndex = -1
  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, 'languageScore', 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. box-sizing: border-box;
  239. }
  240. .step-panel {
  241. display: flex;
  242. flex-direction: column;
  243. align-items: center;
  244. }
  245. /* ===== 说明页 ===== */
  246. .title-icon {
  247. font-size: 88rpx;
  248. margin-bottom: 24rpx;
  249. }
  250. .test-title {
  251. font-size: 48rpx;
  252. font-weight: bold;
  253. color: #fff;
  254. margin-bottom: 32rpx;
  255. }
  256. .test-desc {
  257. font-size: 30rpx;
  258. color: rgba(255,255,255,0.7);
  259. line-height: 1.7;
  260. text-align: center;
  261. }
  262. .test-meta {
  263. font-size: 24rpx;
  264. color: rgba(255,255,255,0.45);
  265. margin-top: 24rpx;
  266. margin-bottom: 80rpx;
  267. text-align: center;
  268. }
  269. .btn-primary {
  270. background: linear-gradient(135deg, #F59E0B, #FBBF24);
  271. color: #1a1a2e;
  272. font-size: 34rpx;
  273. font-weight: bold;
  274. width: 480rpx;
  275. height: 92rpx;
  276. line-height: 92rpx;
  277. border-radius: 46rpx;
  278. border: none;
  279. margin-top: 60rpx;
  280. padding: 0;
  281. }
  282. .btn-primary::after {
  283. border: none;
  284. }
  285. .btn-secondary {
  286. background: rgba(255,255,255,0.1);
  287. color: rgba(255,255,255,0.85);
  288. font-size: 32rpx;
  289. width: 480rpx;
  290. height: 92rpx;
  291. line-height: 92rpx;
  292. border-radius: 46rpx;
  293. border: 2rpx solid rgba(255,255,255,0.25);
  294. margin-top: 32rpx;
  295. padding: 0;
  296. }
  297. .btn-secondary::after {
  298. border: none;
  299. }
  300. /* ===== 答题页 ===== */
  301. .progress-row {
  302. display: flex;
  303. justify-content: space-between;
  304. width: 100%;
  305. margin-bottom: 24rpx;
  306. }
  307. .progress-text {
  308. font-size: 28rpx;
  309. color: rgba(255,255,255,0.7);
  310. }
  311. .timer-bar {
  312. width: 100%;
  313. height: 16rpx;
  314. background: rgba(255,255,255,0.12);
  315. border-radius: 8rpx;
  316. overflow: hidden;
  317. }
  318. .timer-fill {
  319. height: 100%;
  320. background: linear-gradient(90deg, #F59E0B, #FBBF24);
  321. border-radius: 8rpx;
  322. transition: width 0.9s linear;
  323. }
  324. .timer-text {
  325. display: block;
  326. font-size: 26rpx;
  327. color: #F59E0B;
  328. margin-top: 12rpx;
  329. text-align: center;
  330. }
  331. .question-hint {
  332. display: block;
  333. font-size: 32rpx;
  334. font-weight: bold;
  335. color: #fff;
  336. margin: 40rpx 0 32rpx;
  337. text-align: center;
  338. }
  339. .option-wrap {
  340. display: flex;
  341. flex-wrap: wrap;
  342. justify-content: space-between;
  343. width: 100%;
  344. }
  345. .option-cell {
  346. display: flex;
  347. align-items: center;
  348. justify-content: center;
  349. width: 47%;
  350. min-height: 120rpx;
  351. background: #2a2a4a;
  352. border-radius: 24rpx;
  353. border: 4rpx solid transparent;
  354. margin-bottom: 24rpx;
  355. padding: 24rpx;
  356. box-sizing: border-box;
  357. }
  358. .option-cell:active {
  359. background: #35406b;
  360. }
  361. .option-text {
  362. font-size: 34rpx;
  363. color: #fff;
  364. font-weight: 500;
  365. }
  366. .cell-correct {
  367. background: #3d4a2f;
  368. border-color: #4ECDC4;
  369. }
  370. .cell-wrong {
  371. background: #4a2f35;
  372. border-color: #ff6b6b;
  373. }
  374. /* ===== 结果页 ===== */
  375. .result-title {
  376. font-size: 40rpx;
  377. font-weight: bold;
  378. color: #fff;
  379. margin-bottom: 48rpx;
  380. }
  381. .score-circle {
  382. display: flex;
  383. align-items: center;
  384. justify-content: center;
  385. width: 260rpx;
  386. height: 260rpx;
  387. border-radius: 50%;
  388. border: 10rpx solid #F59E0B;
  389. background: rgba(245,158,11,0.1);
  390. margin-bottom: 36rpx;
  391. }
  392. .score-num {
  393. font-size: 88rpx;
  394. font-weight: bold;
  395. color: #F59E0B;
  396. }
  397. .score-row {
  398. display: flex;
  399. align-items: center;
  400. margin-bottom: 24rpx;
  401. }
  402. .score-label {
  403. font-size: 30rpx;
  404. color: rgba(255,255,255,0.7);
  405. margin-right: 20rpx;
  406. }
  407. .dimension-tag {
  408. font-size: 24rpx;
  409. color: #F59E0B;
  410. border: 2rpx solid #F59E0B;
  411. border-radius: 24rpx;
  412. padding: 6rpx 24rpx;
  413. }
  414. .result-tip {
  415. margin-bottom: 20rpx;
  416. }
  417. .result-tip-text {
  418. font-size: 28rpx;
  419. color: rgba(255,255,255,0.6);
  420. }
  421. </style>