最近在做的一个小练习有点意思,就把完成过程记录下来了,也算是自己的一份笔记吧~
由于使用了 Vue.js
和 Vite
作为前端框架,故本文中的代码多使用 TypeScript
书写
程序的完整代码在 Github 使用 MIT
协议开源,相关信息可以在文末找到,供一起学习的小伙伴们参考~
文中的代码还有很多很多的不足,如果屏幕前的你有更好的建议,欢迎在评论区提出,或是去 Github 提一个 pr ~
本文为基本要求的实现过程,进阶要求请转到【进阶篇】
题目
题目描述
老王家里的小登玩口算被外挂虐爆,于是老王想自研一款口算软件,满足他家小登的好学心。请你帮帮老王,为他写一款口算程序。
基本要求
- 题目范围包括100以内的加减乘除,除法必须能整除,题目随机生成。
- 有两种模式,固定题目数量计算用时,以及固定时间计算做题数量。
- 固定题目数量计算用时为:固定30道题目,计算用时,超过15分钟强行中止作答,最后以正确率的百分数加剩余时间为得分。
- 固定时间计算做题数量为:固定3分钟,以做对的题目数量乘以正确率为得分。
- 需要保存每次测试的日期时间、模式、题目和分数。
进阶要求
- 新增多用户功能,程序可以通过用户名和密码注册及登录,每个用户的数据分开存储。
- 新增错题本功能,程序支持查看本人错题本中的题目。做错的题目自动加入错题本,在进行任何练习时,优先从错题本中抽取题目,如果答对则从错题本中移除。
- 新增热座PK功能,两种模式都支持两名用户的热座PK。热座PK指的是:出相同的题目,两个小登依次上机测试,然后对得分进行排名。需要保存用户的PK场数和胜率。
- 小登精通电脑,请你想办法让他没法乱改你保存的数据,特别是场数和胜率。
实现过程【基础篇】
随机数的生成
在 TypeScript 中,Math.random()
可以生成一个范围为 [0, 1] 的随机数,Math.floor()
可以实现取整,将其结合后便可以生成一定范围内的随机整数,例如生成一个范围在 [0, 100] 的随机整数,便可以使用如下代码:
1
| Math.floor(Math.random() * 100);
|
算术题的生成
为了保证加减乘除四则运算的均匀分布,需要优先在加减乘除内随机选取一种运算
1 2
| const operations = ['+', '-', '*', '/']; const operation = operations[Math.floor(Math.random() * operations.length)];
|
这里采用了一个数组,对数组的下标进行随机,从而返回相应的运算符
既然得到了运算法则,接下来生成两个随机数,将随机数和运算法则相结合,就是一道算术题了
部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const generateQuestion = () => { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100);
switch (operation) { case '+': answer = num1 + num2; if (answer > 100) { return generateQuestion(); } break; case '-': if (num1 < num2) [num1, num2] = [num2, num1]; answer = num1 - num2; break; case '*': answer = num1 * num2; if (answer > 100) { return generateQuestion(); } break; case '/': answer = num1 / num2; if (answer % 1 != 0) { return generateQuestion(); } break; } }
|
这段代码在实际使用中还有很多问题,比如随机生成的 num2
有可能为 0 ,当 0 作为除数时…boom;还有乘法和除法答案不均匀的问题,尤其是除法,经常会出现 0 作为被除数的题(如下图)
那么该如何解决呢?
0 作为除数这个问题比较好解决,给作为除数的 num2
+ 1 即可
1
| num2 = Math.floor(Math.random() * 100) + 1;
|
那乘除法呢?
可以对两个数进行限制,比如上界设为 20 ,这样能缓解答案不均的问题(但似乎并不能从根本上解决)
1 2 3 4 5 6 7
| case '*': do { num1 = Math.floor(Math.random() * 20); num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); answer = num1 * num2; break;
|
欸~除法的逆运算不是乘法嘛,可不可以把它反过来呢?
当然可以!
1 2 3 4 5 6 7
| case '/': do { num2 = Math.floor(Math.random() * 20) + 1; answer = Math.floor(Math.random() * 20); } while (num2 * answer > 100); num1 = answer * num2; break;
|
用乘法来生成除法,解决了 0 经常被作为除数的问题,省去了判断是否整除的步骤,运行的效率也会提高
结合起来,就有了这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; const operation = operations[Math.floor(Math.random() * operations.length)];
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); answer = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; answer = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); answer = num1 * num2; break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; answer = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = answer * num2; break; }
question = `${num1} ${operation} ${num2}`; };
|
制作一个答题页面
在 Vue.js 的一个单文件组件中,封装了 <template>
、<script>
和 <style>
三个块,分别对应页面模板、脚本和样式
可以将写好的用于生成题目的代码放置在 <script>
部分,同时使用 ref
将内容暴露给模板,还可以使用一个按钮与 generateQuestion
相关联,当按钮被按下时执行 generateQuestion
函数,生成一道新的题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <script setup lang="ts"> import { ref } from 'vue';
const question = ref(''); const correctAnswer = ref<number | null>(null);
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; const operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 }; </script>
<template> <div> <p>{{ question }}</p> <button @click="generateQuestion">下一题</button> </div> </template>
|
那该怎么获取用户输入呢?
我们在 <script>
部分创建一个 userAnswer
的 ref
1
| const userAnswer = ref('');
|
然后在 <template>
部分创建一个输入框并与 userAnswer
关联
1
| <input v-model="userAnswer" type="number" placeholder="Your answer" />
|
获取到用户输入之后,就可以和答案做比较,判断用户的答案是否正确
创建一个 checkAnswer
函数并在 <template>
中与一个按钮关联
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script setup lang="ts"> import { ref } from 'vue';
const userAnswer = ref(''); const correctAnswer = ref<number | null>(null); const feedback = ref('');
...
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } };
</script>
<template> <div> <p>{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" /> <button @click="checkAnswer">提交</button> <p>{{ feedback }}</p> <button @click="generateQuestion">下一题</button> </div> </template>
|
判断功能也一切正常~
接下来完善 亿 一点点细节,再添加一个简单的样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
const question = ref(''); const userAnswer = ref(''); const correctAnswer = ref<number | null>(null); const feedback = ref(''); const started = ref(false); const answered = ref(false);
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; let operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; operation = '×'; // 乘号 break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; operation = '÷'; // 除号 break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 userAnswer.value = ''; feedback.value = ''; answered.value = false; };
const startQuiz = () => { started.value = true; generateQuestion(); };
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; };
const handleKeyup = (event: KeyboardEvent) => { if (event.key === 'Enter' && !isNaN(parseFloat(userAnswer.value))) { if (!answered.value) { checkAnswer(); } else { generateQuestion(); } } };
onMounted(() => { document.addEventListener('keyup', handleKeyup); });
onUnmounted(() => { document.removeEventListener('keyup', handleKeyup); }); </script>
<template> <div> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> <p class="question">{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" :disabled="answered" /> <button @click="checkAnswer" :disabled="isNaN(parseFloat(userAnswer))" :class="{ 'disabled': isNaN(parseFloat(userAnswer)) }">提交</button> <p>{{ feedback }}</p> <button v-if="answered" @click="generateQuestion">下一题</button> </div> </div> </template>
<style scoped> input { margin-right: 1rem; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; appearance: textfield; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
button { margin-top: 1rem; margin-right: 1rem; padding: 0.5rem 1rem; font-size: 1rem; border: none; border-radius: 4px; background-color: hsla(160, 100%, 37%, 1); color: white; cursor: pointer; }
button:hover { background-color: hsla(158, 49%, 44%, 1); }
button.disabled { background-color: #ccc; cursor: not-allowed; }
p { margin-top: 1rem; font-size: 1rem; }
p.question { margin-top: 1rem; font-size: 1.8rem; }
|
<style>
部分放置了对应的 CSS 样式,用来对页面进行美化;
对乘除运算的 operation
变量进行了再次赋值,将 *
替换为 ×
,/
替换为 ÷
;
这里创建了一个 startQuiz
函数,用于将 started
的 ref
状态修改为布尔值 true
,并与 Start
按钮相关联,结合 v-if
v-else
实现按下开始按钮后再显示题目和答题区;
同样, answered
用于检测用户是否提交,当用户提交后禁用输入框,并显示转到下一题的按钮;
Submit
按钮中添加了 isNaN(parseFloat(userAnswer))
的判断,用来防止用户在未填写答案时按下按钮,同时在 CSS 中添加了 cursor: not-allowed;
的光标样式,对用户进行提示。由于答案有可能为 0 ,所以这里检测 userAnswer
的值是否为 NaN
,避免了答案为 0 时 Submit
按钮不生效的情况出现;
handleKeyup
用于捕获 Enter
按钮,同时通过 onMounted
函数使组件运行时挂载了一个钩子,通过注册该回调函数,在输入框失焦时仍能获取到 Enter
按下的事件,由此实现用户通过按下 Enter
提交或跳转到下一题
到这里,一个有着基础功能的简易答题页面就完成啦~
计时器的实现
在 TypeScript 中,我们可以通过 Date.now()
来获取当前的时间戳(精度为千分之一秒)
在按下开始按钮时,可以用变量来存储开始时间
1
| const startTime = Date.now();
|
随后可以通过两时间作差来得到用时
1
| let elapsedTime = Date.now() - startTime;
|
上面 elapsedTime
只是一次计算和赋值,要让计时器可以实时更新,该怎么实现呢?
欸~ setInterval
可以使某任务隔一段时间就执行一次,那是不是通过这个函数,每隔 0.01 秒计算一次时间差呢
1 2 3 4 5 6 7
| let timer: number | null = null;
timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; } }, 10)
|
这样就得到了一个以 0.01 秒为单位的计时器
当需要停止计时的时候,只需要使用 clearInterval
清除计时任务
实现了基本的计时功能,该怎么在页面显示出来呢?
在 Vue 中,可以通过 {{ (elapsedTime / 1000).toFixed(2) }}
来读取并显示 elapsedTime
变量仅保留 2 位小数的值,由于 elapsedTime
在 setInterval
任务下每 0.01 秒更新一次,因此页面中显示的时间也是 0.01 秒更新一次
最后确保在组件卸载时能停止计时,别忘记在 onUnmounted
函数中清除计时任务哦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
...
const startTime = ref<number | null>(null); const elapsedTime = ref(0); let timer: number | null = null;
...
const startQuiz = () => { started.value = true; startTime.value = Date.now(); elapsedTime.value = 0; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; } }, 10); // 每 0.01 秒更新一次 }; onUnmounted(() => { if (timer !== null) { clearInterval(timer); } } </script>
<template> <div> <div> <span v-if="started">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started">秒</span> </div> ... </div> </template>
|
现在让我们加点样式吧~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
const question = ref(''); const userAnswer = ref(''); const correctAnswer = ref<number | null>(null); const feedback = ref(''); const started = ref(false); const answered = ref(false); const startTime = ref<number | null>(null); const elapsedTime = ref(0); let timer: number | null = null;
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; let operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; operation = '×'; // 乘号 break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; operation = '÷'; // 除号 break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 userAnswer.value = ''; feedback.value = ''; answered.value = false; };
const startQuiz = () => { started.value = true; startTime.value = Date.now(); elapsedTime.value = 0; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; } }, 10); // 每 0.01 秒更新一次 };
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; };
const handleKeyup = (event: KeyboardEvent) => { if (event.key === 'Enter' && !isNaN(parseFloat(userAnswer.value))) { if (!answered.value) { checkAnswer(); } else { generateQuestion(); } } };
onMounted(() => { document.addEventListener('keyup', handleKeyup); });
onUnmounted(() => { document.removeEventListener('keyup', handleKeyup); if (timer !== null) { clearInterval(timer); // 清除计时器 } }); </script>
<template> <div> <div class="timer-container"> <span v-if="started" class="timer">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started">秒</span> </div> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> <p class="question">{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" :disabled="answered" /> <button @click="checkAnswer" :disabled="isNaN(parseFloat(userAnswer))" :class="{ 'disabled': isNaN(parseFloat(userAnswer)) }">提交</button> <p>{{ feedback }}</p> <button v-if="answered" @click="generateQuestion">下一题</button> </div> </div> </template>
<style scoped> input { margin-right: 1rem; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; appearance: textfield; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
button { margin-top: 1rem; margin-right: 1rem; padding: 0.5rem 1rem; font-size: 1rem; border: none; border-radius: 4px; background-color: hsla(160, 100%, 37%, 1); color: white; cursor: pointer; }
button:hover { background-color: hsla(158, 49%, 44%, 1); }
button.disabled { background-color: #ccc; cursor: not-allowed; }
p { margin-top: 1rem; font-size: 1rem; }
p.question { margin-top: 1rem; font-size: 1.8rem; }
.timer-container { display: flex; align-items: center; }
.timer-container span { margin: 0 3px; font-size: 0.8rem; }
.timer-container .timer { margin-bottom: 5px; font-size: 1.5rem; } </style>
|
两种模式的切换
两种模式下的限时不同,故可以用最大时间来作为不同模式的判断依据
1 2 3 4 5 6 7 8 9 10 11
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
...
const timeLimit = [15 * 60 * 1000, 3 * 60 * 1000]; const selectedTimeLimit = ref(timeLimit[0]);
...
</script>
|
在 <template>
中,我们可以创建一个类似开关的按钮,同时创建 switchTimeLimit
函数并将二者相关联,以进行时间限制的控制,从而改变模式
也应引入相应的 CSS 来绘制开关按钮的样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
...
const timeLimit = [15 * 60 * 1000, 3 * 60 * 1000]; const selectedTimeLimit = ref(timeLimit[0]);
const switchTimeLimit = () => { selectedTimeLimit.value = selectedTimeLimit.value === timeLimit[0] ? timeLimit[1] : timeLimit[0]; };
...
</script>
<template> <div> <div class="switch-container"> <span>模式:</span> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[0] }">按数量</span> <label class="switch"> <input type="checkbox" @change="switchTimeLimit" :checked="selectedTimeLimit === timeLimit[1]" /> <span class="slider"></span> </label> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[1] }">按时间</span> </div> ... </div> </template>
<style scoped>
...
.switch-container { display: flex; align-items: center; }
.switch-container span { margin: 0 3px; font-size: 0.8rem; }
.switch-container .highlight { color: hsla(160, 100%, 37%, 1); }
.switch { position: relative; display: inline-block; width: 50px; height: 25px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 25px; }
.slider:before { position: absolute; content: ""; height: 19px; width: 19px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider:before { transform: translateX(18px); } </style>
|
再加一点点细节吧~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
const question = ref(''); const userAnswer = ref(''); const correctAnswer = ref<number | null>(null); const feedback = ref(''); const started = ref(false); const answered = ref(false); const startTime = ref<number | null>(null); const elapsedTime = ref(0); const timeLimit = [15 * 60 * 1000, 3 * 60 * 1000]; const selectedTimeLimit = ref(timeLimit[0]); let timer: number | null = null;
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; let operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; operation = '×'; // 乘号 break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; operation = '÷'; // 除号 break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 userAnswer.value = ''; feedback.value = ''; answered.value = false; };
const startQuiz = () => { started.value = true; startTime.value = Date.now(); elapsedTime.value = 0; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; } }, 10); // 每 0.01 秒更新一次 };
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; };
const switchTimeLimit = () => { selectedTimeLimit.value = selectedTimeLimit.value === timeLimit[0] ? timeLimit[1] : timeLimit[0]; };
const handleKeyup = (event: KeyboardEvent) => { if (event.key === 'Enter' && !isNaN(parseFloat(userAnswer.value))) { if (!answered.value) { checkAnswer(); } else { generateQuestion(); } } };
onMounted(() => { document.addEventListener('keyup', handleKeyup); });
onUnmounted(() => { document.removeEventListener('keyup', handleKeyup); if (timer !== null) { clearInterval(timer); // 清除计时器 } }); </script>
<template> <div> <div class="header-container"> <div class="switch-container"> <span>模式:</span> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[0] }">按数量</span> <label class="switch"> <input type="checkbox" @change="switchTimeLimit" :checked="selectedTimeLimit === timeLimit[1]" :disabled="started"/> <span class="slider"></span> </label> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[1] }">按时间</span> </div> <div class="timer-container"> <span v-if="started" class="timer">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started">秒</span> </div> </div> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> <p class="question">{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" :disabled="answered" /> <button @click="checkAnswer" :disabled="isNaN(parseFloat(userAnswer))" :class="{ 'disabled': isNaN(parseFloat(userAnswer)) }">提交</button> <p>{{ feedback }}</p> <button v-if="answered" @click="generateQuestion">下一题</button> </div> </div> </template>
<style scoped> input { margin-right: 1rem; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; appearance: textfield; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
button { margin-top: 1rem; margin-right: 1rem; padding: 0.5rem 1rem; font-size: 1rem; border: none; border-radius: 4px; background-color: hsla(160, 100%, 37%, 1); color: white; cursor: pointer; }
button:hover { background-color: hsla(158, 49%, 44%, 1); }
button.disabled { background-color: #ccc; cursor: not-allowed; }
p { margin-top: 1rem; font-size: 1rem; }
p.question { margin-top: 1rem; font-size: 1.8rem; }
.header-container { display: flex; align-items: center; height: 3rem; }
.switch-container { display: flex; align-items: center; }
.switch-container span { margin: 0 3px; font-size: 0.8rem; }
.switch-container .highlight { color: hsla(160, 100%, 37%, 1); }
.timer-container { margin-left: 5rem; display: flex; align-items: center; }
.timer-container span { margin: 0 3px; font-size: 0.8rem; }
.timer-container .timer { margin-bottom: 5px; font-size: 1.5rem; }
.switch { position: relative; display: inline-block; width: 50px; height: 25px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 25px; }
.slider:before { position: absolute; content: ""; height: 19px; width: 19px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider:before { transform: translateX(18px); } </style>
|
固定题目数量计算用时
计数
统计题目的完成数量很好实现,我们在 <script>
部分创建两个 ref
元素,分别用来存放已做题数和正确题数
1 2 3 4
| <script setup lang="ts"> const questionCount = ref(0); const correctCount = ref(0); </script>
|
计时
按照要求,要实现固定 30 道题的计时,可以在按下提交按钮,即检查答案是否正确时,对于已完成的题数进行判断
在 checkAnswer
中,我们可以添加 questionCount
的自增和 correctCount
的自增,当完成一道题时,使完成数 questionCount
+1,若回答正确,使正确数 correctCount
+1
同时,我们也可以在 checkAnswer
中添加对停止提问的条件判断,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; correctCount.value++; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; questionCount.value++; if (selectedTimeLimit.value === timeLimit[0] && (elapsedTime.value >= selectedTimeLimit.value || questionCount.value >= 30)) { stopQuiz(); } if (selectedTimeLimit.value === timeLimit[1] && elapsedTime.value >= selectedTimeLimit.value) { stopQuiz(); } };
|
提问程序现在分为三个主要部分,第一部分是控制是否运行的 start
程序,第二部分是循环执行的题目生成与判断程序,第三部分是计时程序
因此若要使提问停止,同样应进行三种操作:生成对应状态,停止新题目的生成,暂停计时
在这里,我引入了一个新的 ref
状态 stopped
1
| const stopped = ref(false);
|
那结合先前设置的状态值可以有以下逻辑:
当 started
为 false
,stopped
同样为 false
时,程序还未开始运行,计时器停止;
当 started
为 true
,stopped
为 false
时,程序正在运行,计时器运行;
当 started
为 true
,stopped
同样为 true
时,程序已结束,计时器停止,此时应显示答题情况和分数
这样就可以写出 stopQuiz
函数
1 2 3 4 5 6 7
| const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); } };
|
现在把它添加到 Vue 组件中,同时修改页面的 <template>
部分,实现停止后答题区隐藏,同时信息栏保持显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
const started = ref(false); const stopped = ref(false);
...
const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); // 清除计时器 } };
const checkAnswer = () => {
... if (selectedTimeLimit.value === timeLimit[0] && (elapsedTime.value >= selectedTimeLimit.value || questionCount.value >= 30)) { stopQuiz(); } if (selectedTimeLimit.value === timeLimit[1] && elapsedTime.value >= selectedTimeLimit.value) { stopQuiz(); } };
...
</script>
<template> <div> ... <div class="info-container"> <div class="counter-container"> <span v-if="started || stopped">已做</span> <span v-if="started || stopped" class="counter">{{ questionCount }}</span> <span v-if="started || stopped">题</span> <span v-if="started || stopped">正确</span> <span v-if="started || stopped" class="counter">{{ correctCount }}</span> <span v-if="started || stopped">题</span> </div> <div class="timer-container"> <span v-if="started || stopped" class="timer">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started || stopped">秒</span> </div> </div> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> ... </div> </div> </template>
|
计分
题目中的要求是“以正确率的百分数加剩余时间为得分”,针对题目可以稍作延伸,便可以得到一个更合理的计分方式:
按正确率与用时占比加权赋分,满分100,其中正确率权重为75%,用时权重为25%
这样可以避免通过恶意刷时长来获取更高分数
1
| score.value = Math.round((correctCount.value / questionCount.value) * 75) + Math.round((1 - (elapsedTime.value / selectedTimeLimit.value)) * 25);
|
在 Vue 组件中,我们可以将分数的计算部分放入 stopQuiz
函数中,同时也应注意一道题都不做的情况出现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <script setup lang="ts"> import { ref } from 'vue';
...
const score = ref(0);
...
const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); // 清除计时器 } if (selectedTimeLimit.value === timeLimit[0]) { score.value = Math.round((correctCount.value / 30) * 75) + Math.round((1 - (elapsedTime.value / selectedTimeLimit.value)) * 25); } if (isNaN(score.value)) { // 一道题都不做怎么能行呀 score.value = 0; } }; </script>
|
在 <template>
部分,也添加相应的分数显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <div> ... <div v-if="stopped" class="score-container"> <span v-if="stopped">得分:</span> <span v-if="stopped" class="score">{{ score }}</span> <span v-if="stopped">分</span> </div>
... </div> </template>
|
至于15分钟的超时判定,我们可以在 startQuiz
函数中设置的 timer
任务中加一个判断,在每次更新计时器的同时对是否超时做出判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const startQuiz = () => { started.value = true; stopped.value = false; startTime.value = Date.now(); elapsedTime.value = 0; questionCount.value = 0; correctCount.value = 0; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; if (elapsedTime.value >= selectedTimeLimit.value) { stopQuiz(); } } }, 10); };
|
加上样式,就有了这样的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
const question = ref(''); // 拼接后的问题 const userAnswer = ref(''); // 用户输入的答案 const correctAnswer = ref<number | null>(null); // 正确答案 const feedback = ref(''); // 回答正确或错误的反馈信息 const started = ref(false); // [状态]是否开始 const answered = ref(false); // [状态]是否提交作答 const stopped = ref(false); // [状态]是否答题停止 const startTime = ref<number | null>(null); // 开始时的时间 const elapsedTime = ref(0); // 累计用时 const timeLimit = [15 * 60 * 1000, 3 * 60 * 1000]; // 时间限制(模式) const selectedTimeLimit = ref(timeLimit[0]); // 选择的时间限制(模式) const questionCount = ref(0); // 回答的总题数 const correctCount = ref(0); // 回答正确的题数 const score = ref(0); // 得分 let timer: number | null = null; // 计时器
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; let operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; operation = '×'; // 乘号 break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; operation = '÷'; // 除号 break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 userAnswer.value = ''; feedback.value = ''; answered.value = false; };
const startQuiz = () => { started.value = true; stopped.value = false; startTime.value = Date.now(); elapsedTime.value = 0; questionCount.value = 0; correctCount.value = 0; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; if (elapsedTime.value >= selectedTimeLimit.value) { // 超时判断 stopQuiz(); } } }, 10); // 每 0.01 秒更新一次 };
const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); // 清除计时器 } if (selectedTimeLimit.value === timeLimit[0]) { score.value = Math.round((correctCount.value / 30) * 75) + Math.round((1 - (elapsedTime.value / selectedTimeLimit.value)) * 25); } if (isNaN(score.value)) { // 一道题都不做怎么能行呀 score.value = 0; } };
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; correctCount.value++; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; questionCount.value++;
if (selectedTimeLimit.value === timeLimit[0] && (elapsedTime.value >= selectedTimeLimit.value || questionCount.value >= 30)) { stopQuiz(); } };
const switchTimeLimit = () => { selectedTimeLimit.value = selectedTimeLimit.value === timeLimit[0] ? timeLimit[1] : timeLimit[0]; };
const handleKeyup = (event: KeyboardEvent) => { if (event.key === 'Enter' && !isNaN(parseFloat(userAnswer.value))) { if (!answered.value) { checkAnswer(); } else { generateQuestion(); } } };
onMounted(() => { document.addEventListener('keyup', handleKeyup); });
onUnmounted(() => { document.removeEventListener('keyup', handleKeyup); if (timer !== null) { clearInterval(timer); // 清除计时器 } }); </script>
<template> <div> <div class="switch-container"> <span>模式:</span> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[0] }">按数量</span> <label class="switch"> <input type="checkbox" @change="switchTimeLimit" :checked="selectedTimeLimit === timeLimit[1]" :disabled="started"/> <span class="slider"></span> </label> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[1] }">按时间</span> </div> <div class="info-container"> <div class="counter-container"> <span v-if="started || stopped">已做</span> <span v-if="started || stopped" class="counter">{{ questionCount }}</span> <span v-if="started || stopped">题</span> <span v-if="started || stopped">正确</span> <span v-if="started || stopped" class="counter">{{ correctCount }}</span> <span v-if="started || stopped">题</span> </div> <div class="timer-container"> <span v-if="started || stopped" class="timer">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started || stopped">秒</span> </div> </div> <div v-if="stopped" class="score-container"> <span v-if="stopped">得分:</span> <span v-if="stopped" class="score">{{ score }}</span> <span v-if="stopped">分</span> </div> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> <p class="question">{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" :disabled="answered" /> <button @click="checkAnswer" :disabled="isNaN(parseFloat(userAnswer))" :class="{ 'disabled': isNaN(parseFloat(userAnswer)) }">提交</button> <p>{{ feedback }}</p> <button v-if="answered" @click="generateQuestion">下一题</button> </div> </div> </template>
<style scoped> input { margin-right: 1rem; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; appearance: textfield; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
button { margin-top: 1rem; margin-right: 1rem; padding: 0.5rem 1rem; font-size: 1rem; border: none; border-radius: 4px; background-color: hsla(160, 100%, 37%, 1); color: white; cursor: pointer; }
button:hover { background-color: hsla(158, 49%, 44%, 1); }
button.disabled { background-color: #ccc; cursor: not-allowed; }
p { margin-top: 1rem; font-size: 1rem; }
p.question { margin-top: 1rem; font-size: 1.8rem; }
.info-container { display: flex; align-items: center; height: 3rem; max-width: 30rem; justify-content: flex-end; }
.switch-container { display: flex; align-items: center; }
.switch-container span { margin: 0 3px; font-size: 0.8rem; }
.switch-container .highlight { color: hsla(160, 100%, 37%, 1); }
.counter-container { display: flex; align-items: center; }
.counter-container span { margin: 0 3px; font-size: 0.8rem; }
.counter-container .counter { margin-bottom: 5px; font-size: 1.5rem; }
.timer-container { margin-left: 1rem; display: flex; align-items: center; }
.timer-container span { margin: 0 3px; font-size: 0.8rem; }
.timer-container .timer { margin-bottom: 5px; font-size: 1.5rem; }
.score-container { height: 3rem; display: flex; align-items: center; }
.score-container span { margin-right: 6px; font-size: 1.3rem; }
.score-container .score { margin-bottom: 6px; font-size: 2.2rem; }
.switch { position: relative; display: inline-block; width: 50px; height: 25px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 25px; }
.slider:before { position: absolute; content: ""; height: 19px; width: 19px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider:before { transform: translateX(18px); } </style>
|
固定时间计算做题数量
这个模式和“按数量”大同小异,只需要添加另一种模式的分数计算即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); } if (selectedTimeLimit.value === timeLimit[0]) { score.value = Math.round((correctCount.value / 30) * 75) + Math.round((1 - (elapsedTime.value / selectedTimeLimit.value)) * 25); } if (selectedTimeLimit.value === timeLimit[1]) { score.value = Math.round(correctCount.value * 100); } if (isNaN(score.value)) { score.value = 0; } };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
const question = ref(''); // 拼接后的问题 const userAnswer = ref(''); // 用户输入的答案 const correctAnswer = ref<number | null>(null); // 正确答案 const feedback = ref(''); // 回答正确或错误的反馈信息 const started = ref(false); // [状态]是否开始 const answered = ref(false); // [状态]是否提交作答 const stopped = ref(false); // [状态]是否答题停止 const startTime = ref<number | null>(null); // 开始时的时间 const elapsedTime = ref(0); // 累计用时 const timeLimit = [15 * 60 * 1000, 3 * 60 * 1000]; // 时间限制(模式) const selectedTimeLimit = ref(timeLimit[0]); // 选择的时间限制(模式) const questionCount = ref(0); // 回答的总题数 const correctCount = ref(0); // 回答正确的题数 const score = ref(0); // 得分 let timer: number | null = null; // 计时器
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; let operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; operation = '×'; // 乘号 break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; operation = '÷'; // 除号 break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 userAnswer.value = ''; feedback.value = ''; answered.value = false; };
const startQuiz = () => { started.value = true; stopped.value = false; startTime.value = Date.now(); elapsedTime.value = 0; questionCount.value = 0; correctCount.value = 0; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; if (elapsedTime.value >= selectedTimeLimit.value) { // 超时判断 stopQuiz(); } } }, 10); // 每 0.01 秒更新一次 };
const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); // 清除计时器 } if (selectedTimeLimit.value === timeLimit[0]) { score.value = Math.round((correctCount.value / 30) * 75) + Math.round((1 - (elapsedTime.value / selectedTimeLimit.value)) * 25); } if (selectedTimeLimit.value === timeLimit[1]) { score.value = Math.round(correctCount.value * 100); } if (isNaN(score.value)) { // 一道题都不做怎么能行呀 score.value = 0; } };
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; correctCount.value++; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; questionCount.value++;
if (selectedTimeLimit.value === timeLimit[0] && (elapsedTime.value >= selectedTimeLimit.value || questionCount.value >= 30)) { stopQuiz(); } if (selectedTimeLimit.value === timeLimit[1] && elapsedTime.value >= selectedTimeLimit.value) { stopQuiz(); } };
const switchTimeLimit = () => { selectedTimeLimit.value = selectedTimeLimit.value === timeLimit[0] ? timeLimit[1] : timeLimit[0]; };
const handleKeyup = (event: KeyboardEvent) => { if (event.key === 'Enter' && !isNaN(parseFloat(userAnswer.value))) { if (!answered.value) { checkAnswer(); } else { generateQuestion(); } } };
onMounted(() => { document.addEventListener('keyup', handleKeyup); });
onUnmounted(() => { document.removeEventListener('keyup', handleKeyup); if (timer !== null) { clearInterval(timer); // 清除计时器 } }); </script>
<template> <div> <div class="switch-container"> <span>模式:</span> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[0] }">按数量</span> <label class="switch"> <input type="checkbox" @change="switchTimeLimit" :checked="selectedTimeLimit === timeLimit[1]" :disabled="started"/> <span class="slider"></span> </label> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[1] }">按时间</span> </div> <div class="info-container"> <div class="counter-container"> <span v-if="started || stopped">已做</span> <span v-if="started || stopped" class="counter">{{ questionCount }}</span> <span v-if="started || stopped">题</span> <span v-if="started || stopped">正确</span> <span v-if="started || stopped" class="counter">{{ correctCount }}</span> <span v-if="started || stopped">题</span> </div> <div class="timer-container"> <span v-if="started || stopped" class="timer">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started || stopped">秒</span> </div> </div> <div v-if="stopped" class="score-container"> <span v-if="stopped">得分:</span> <span v-if="stopped" class="score">{{ score }}</span> <span v-if="stopped">分</span> </div> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> <p class="question">{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" :disabled="answered" /> <button @click="checkAnswer" :disabled="isNaN(parseFloat(userAnswer))" :class="{ 'disabled': isNaN(parseFloat(userAnswer)) }">提交</button> <p>{{ feedback }}</p> <button v-if="answered" @click="generateQuestion">下一题</button> </div> </div> </template>
<style scoped> input { margin-right: 1rem; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; appearance: textfield; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
button { margin-top: 1rem; margin-right: 1rem; padding: 0.5rem 1rem; font-size: 1rem; border: none; border-radius: 4px; background-color: hsla(160, 100%, 37%, 1); color: white; cursor: pointer; }
button:hover { background-color: hsla(158, 49%, 44%, 1); }
button.disabled { background-color: #ccc; cursor: not-allowed; }
p { margin-top: 1rem; font-size: 1rem; }
p.question { margin-top: 1rem; font-size: 1.8rem; }
.info-container { display: flex; align-items: center; height: 3rem; max-width: 30rem; justify-content: flex-end; }
.switch-container { display: flex; align-items: center; }
.switch-container span { margin: 0 3px; font-size: 0.8rem; }
.switch-container .highlight { color: hsla(160, 100%, 37%, 1); }
.counter-container { display: flex; align-items: center; }
.counter-container span { margin: 0 3px; font-size: 0.8rem; }
.counter-container .counter { margin-bottom: 5px; font-size: 1.5rem; }
.timer-container { margin-left: 1rem; display: flex; align-items: center; }
.timer-container span { margin: 0 3px; font-size: 0.8rem; }
.timer-container .timer { margin-bottom: 5px; font-size: 1.5rem; }
.score-container { height: 3rem; display: flex; align-items: center; }
.score-container span { margin-right: 6px; font-size: 1.3rem; }
.score-container .score { margin-bottom: 6px; font-size: 2.2rem; }
.switch { position: relative; display: inline-block; width: 50px; height: 25px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 25px; }
.slider:before { position: absolute; content: ""; height: 19px; width: 19px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider:before { transform: translateX(18px); } </style>
|
相关数据的保存
可以通过将数据暂存在数组中实现数据的临时保存
1
| const questionsDetails = ref([]);
|
不同于 JavaScript ,在 TypeScript 中,可以通过 interface
定义一个接口,用于初始化数组,使数组能够存储 JSON 格式数据
1 2 3 4 5 6 7 8
| interface QuestionDetail { question: string; userAnswer: string; correctAnswer: number | null; isCorrect: boolean; }
const questionsDetails = ref<QuestionDetail[]>([]);
|
随后可以在 checkAnswer
函数中使用 push
方法将每个问题的详细信息存入 questionsDetails
数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const checkAnswer = () => {
...
questionsDetails.value.push({ question: question.value, userAnswer: userAnswer.value, correctAnswer: correctAnswer.value, isCorrect: parseFloat(userAnswer.value) === correctAnswer.value });
...
};
|
这样一来,每一次的问答就会被记录下来了
那么下一步,该怎么生成并下载数据文件呢?
可以通过这样的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const downloadSummary = () => { let mode;
if (selectedTimeLimit.value === timeLimit[0]){ mode = 'quantity'; } if (selectedTimeLimit.value === timeLimit[1]){ mode = 'time'; }
const quizSummary = { questions: questionsDetails.value, correctCount: correctCount.value, questionCount: questionCount.value, startTime: startTime.value, elapsedTime: elapsedTime.value, score: score.value, mode: mode };
const json = JSON.stringify(quizSummary, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'summary.json'; a.click(); URL.revokeObjectURL(url); };
|
这里将数组 questionsDetails
中保存的各题数据放入 JSON 的 questions
部分,同时也在 JSON 中加入了 correctCount
、 questionCount
、startTime
、 elapsedTime
、score
、mode
,分别用于记录回答正确的题数、回答的总题数、开始时间、用时、得分、模式
随后将 JSON 格式的数据转为 application/json
格式的 Blob 对象
再使用 URL.createObjectURL
方法为 Blob
对象创建一个临时的 URL,同时创建一个 HTML a
元素,并将其 href
属性设置为临时 URL,download
属性设置为文件名 summary.json
一切准备完毕,对 a
元素调用 click()
方法开始下载吧~
在完成下载后也要及时使用 URL.revokeObjectURL
销毁这一临时的 URL
当然,别忘记在 <template>
中创建一个按钮与 downloadSummary
相结合
1 2 3 4 5 6 7 8 9 10 11
| <template> <div> ... <button v-if="stopped" @click="downloadSummary">下载数据</button> ... </div> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
| <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue';
interface QuestionDetail { question: string; userAnswer: string; correctAnswer: number | null; isCorrect: boolean; }
const question = ref(''); // 拼接后的问题 const userAnswer = ref(''); // 用户输入的答案 const correctAnswer = ref<number | null>(null); // 正确答案 const feedback = ref(''); // 回答正确或错误的反馈信息 const started = ref(false); // [状态]是否开始 const answered = ref(false); // [状态]是否提交作答 const stopped = ref(false); // [状态]是否答题停止 const startTime = ref<number | null>(null); // 开始时的时间 const elapsedTime = ref(0); // 累计用时 const timeLimit = [15 * 60 * 1000, 3 * 60 * 1000]; // 时间限制(模式) const selectedTimeLimit = ref(timeLimit[0]); // 选择的时间限制(模式) const questionCount = ref(0); // 回答的总题数 const correctCount = ref(0); // 回答正确的题数 const score = ref(0); // 得分 const questionsDetails = ref<QuestionDetail[]>([]); // 题目存储 let timer: number | null = null; // 计时器
const generateQuestion = () => { let num1; let num2; const operations = ['+', '-', '*', '/']; let operation = operations[Math.floor(Math.random() * operations.length)]; // 随机四则运算
switch (operation) { case '+': do { num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); } while (num1 + num2 > 100); correctAnswer.value = num1 + num2; break; case '-': num1 = Math.floor(Math.random() * 100); num2 = Math.floor(Math.random() * 100); if (num1 < num2) [num1, num2] = [num2, num1]; correctAnswer.value = num1 - num2; break; case '*': do { num1 = Math.floor(Math.random() * 20); // 限制范围 num2 = Math.floor(Math.random() * 20); } while (num1 * num2 > 100); correctAnswer.value = num1 * num2; operation = '×'; // 乘号 break; case '/': do { num2 = Math.floor(Math.random() * 20) + 1; // 除数不为 0 correctAnswer.value = Math.floor(Math.random() * 20); } while (num2 * correctAnswer.value > 100); num1 = correctAnswer.value * num2; operation = '÷'; // 除号 break; }
question.value = `${num1} ${operation} ${num2}`; // 拼接问题 userAnswer.value = ''; feedback.value = ''; answered.value = false; };
const startQuiz = () => { started.value = true; stopped.value = false; startTime.value = Date.now(); elapsedTime.value = 0; questionCount.value = 0; correctCount.value = 0; questionsDetails.value = []; generateQuestion(); timer = setInterval(() => { if (startTime.value !== null) { elapsedTime.value = Date.now() - startTime.value; if (elapsedTime.value >= selectedTimeLimit.value) { // 超时判断 stopQuiz(); } } }, 10); // 每 0.01 秒更新一次 };
const stopQuiz = () => { started.value = false; stopped.value = true; if (timer !== null) { clearInterval(timer); // 清除计时器 } if (selectedTimeLimit.value === timeLimit[0]) { score.value = Math.round((correctCount.value / 30) * 75) + Math.round((1 - (elapsedTime.value / selectedTimeLimit.value)) * 25); } if (selectedTimeLimit.value === timeLimit[1]) { score.value = Math.round(correctCount.value * 100); } if (isNaN(score.value)) { // 一道题都不做怎么能行呀 score.value = 0; } };
const checkAnswer = () => { if (parseFloat(userAnswer.value) === correctAnswer.value) { feedback.value = '做对啦!'; correctCount.value++; } else { feedback.value = `再想想呢...正确答案是 ${correctAnswer.value} 哦`; } answered.value = true; questionCount.value++;
questionsDetails.value.push({ question: question.value, userAnswer: userAnswer.value, correctAnswer: correctAnswer.value, isCorrect: parseFloat(userAnswer.value) === correctAnswer.value });
if (selectedTimeLimit.value === timeLimit[0] && (elapsedTime.value >= selectedTimeLimit.value || questionCount.value >= 30)) { stopQuiz(); } if (selectedTimeLimit.value === timeLimit[1] && elapsedTime.value >= selectedTimeLimit.value) { stopQuiz(); } };
const switchTimeLimit = () => { selectedTimeLimit.value = selectedTimeLimit.value === timeLimit[0] ? timeLimit[1] : timeLimit[0]; };
const downloadSummary = () => { let mode;
if (selectedTimeLimit.value === timeLimit[0]){ mode = 'quantity'; } if (selectedTimeLimit.value === timeLimit[1]){ mode = 'time'; }
const quizSummary = { questions: questionsDetails.value, correctCount: correctCount.value, questionCount: questionCount.value, startTime: startTime.value, elapsedTime: elapsedTime.value, score: score.value, mode: mode };
const json = JSON.stringify(quizSummary, null, 2); const blob = new Blob([json], { type: 'application/json' }); // 创建 Blob const url = URL.createObjectURL(blob); // 创建临时 URL
const a = document.createElement('a'); a.href = url; a.download = 'summary.json'; a.click();
URL.revokeObjectURL(url); // 销毁临时 URL };
const handleKeyup = (event: KeyboardEvent) => { if (event.key === 'Enter' && !isNaN(parseFloat(userAnswer.value))) { if (!answered.value) { checkAnswer(); } else { generateQuestion(); } } };
onMounted(() => { document.addEventListener('keyup', handleKeyup); });
onUnmounted(() => { document.removeEventListener('keyup', handleKeyup); if (timer !== null) { clearInterval(timer); // 清除计时器 } }); </script>
<template> <div> <div class="switch-container"> <span>模式:</span> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[0] }">按数量</span> <label class="switch"> <input type="checkbox" @change="switchTimeLimit" :checked="selectedTimeLimit === timeLimit[1]" :disabled="started"/> <span class="slider"></span> </label> <span :class="{ 'highlight': selectedTimeLimit === timeLimit[1] }">按时间</span> </div> <div class="info-container"> <div class="counter-container"> <span v-if="started || stopped">已做</span> <span v-if="started || stopped" class="counter">{{ questionCount }}</span> <span v-if="started || stopped">题</span> <span v-if="started || stopped">正确</span> <span v-if="started || stopped" class="counter">{{ correctCount }}</span> <span v-if="started || stopped">题</span> </div> <div class="timer-container"> <span v-if="started || stopped" class="timer">{{ (elapsedTime / 1000).toFixed(2) }}</span> <span v-if="started || stopped">秒</span> </div> </div> <div v-if="stopped" class="score-container"> <span v-if="stopped">得分:</span> <span v-if="stopped" class="score">{{ score }}</span> <span v-if="stopped">分</span> </div> <button v-if="stopped" @click="downloadSummary">下载数据</button> <button v-if="!started" @click="startQuiz">开始</button> <div v-else> <p class="question">{{ question }}</p> <input v-model="userAnswer" type="number" placeholder="输入你的答案" :disabled="answered" /> <button @click="checkAnswer" :disabled="isNaN(parseFloat(userAnswer))" :class="{ 'disabled': isNaN(parseFloat(userAnswer)) }">提交</button> <p>{{ feedback }}</p> <button v-if="answered" @click="generateQuestion">下一题</button> </div> </div> </template>
<style scoped> input { margin-right: 1rem; padding: 0.5rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 4px; appearance: textfield; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
button { margin-top: 1rem; margin-right: 1rem; padding: 0.5rem 1rem; font-size: 1rem; border: none; border-radius: 4px; background-color: hsla(160, 100%, 37%, 1); color: white; cursor: pointer; }
button:hover { background-color: hsla(158, 49%, 44%, 1); }
button.disabled { background-color: #ccc; cursor: not-allowed; }
p { margin-top: 1rem; font-size: 1rem; }
p.question { margin-top: 1rem; font-size: 1.8rem; }
.info-container { display: flex; align-items: center; height: 3rem; max-width: 30rem; justify-content: flex-end; }
.switch-container { display: flex; align-items: center; }
.switch-container span { margin: 0 3px; font-size: 0.8rem; }
.switch-container .highlight { color: hsla(160, 100%, 37%, 1); }
.counter-container { display: flex; align-items: center; }
.counter-container span { margin: 0 3px; font-size: 0.8rem; }
.counter-container .counter { margin-bottom: 5px; font-size: 1.5rem; }
.timer-container { margin-left: 1rem; display: flex; align-items: center; }
.timer-container span { margin: 0 3px; font-size: 0.8rem; }
.timer-container .timer { margin-bottom: 5px; font-size: 1.5rem; }
.score-container { height: 3rem; display: flex; align-items: center; }
.score-container span { margin-right: 6px; font-size: 1.3rem; }
.score-container .score { margin-bottom: 6px; font-size: 2.2rem; }
.switch { position: relative; display: inline-block; width: 50px; height: 25px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 25px; }
.slider:before { position: absolute; content: ""; height: 19px; width: 19px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider:before { transform: translateX(18px); } </style>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| { "questions": [ { "question": "98 - 63", "userAnswer": 35, "correctAnswer": 35, "isCorrect": true }, { "question": "2 × 3", "userAnswer": 6, "correctAnswer": 6, "isCorrect": true }, { "question": "9 + 84", "userAnswer": 93, "correctAnswer": 93, "isCorrect": true }, { "question": "10 × 6", "userAnswer": 60, "correctAnswer": 60, "isCorrect": true }, { "question": "14 + 38", "userAnswer": 52, "correctAnswer": 52, "isCorrect": true }, { "question": "87 - 13", "userAnswer": 74, "correctAnswer": 74, "isCorrect": true } ], "correctCount": 6, "questionCount": 6, "startTime": 1731172983196, "elapsedTime": 180187, "score": 600, "mode": "time" }
|
基本要求 5 达成!
至此基本要求全部完成,撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。
程序相关
Github
本程序源代码在 Github 平台完全开源
【链接 Link】: Oral-Arithmetic
【协议 License】: MIT License
可以在 Github 点点右上角那颗小星星嘛?quq
DEMO
https://arith.223322.best
其他说明
后续的完善与更新将在 Github 平台进行,这里不再重复描述,可以前往 Github Commits 页面查看具体内容