最近在做的一个小练习有点意思,就把完成过程记录下来了,也算是自己的一份笔记吧~
由于使用了 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);
 | 
算术题的生成
为了保证加减乘除四则运算的均匀分布,需要优先在加减乘除内随机选取一种运算
| 12
 
 | const operations = ['+', '-', '*', '/'];const operation = operations[Math.floor(Math.random() * operations.length)];
 
 | 
这里采用了一个数组,对数组的下标进行随机,从而返回相应的运算符
既然得到了运算法则,接下来生成两个随机数,将随机数和运算法则相结合,就是一道算术题了
部分代码如下:
| 12
 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 ,这样能缓解答案不均的问题(但似乎并不能从根本上解决)
| 12
 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;
 
 | 
欸~除法的逆运算不是乘法嘛,可不可以把它反过来呢?
当然可以!
| 12
 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 经常被作为除数的问题,省去了判断是否整除的步骤,运行的效率也会提高
结合起来,就有了这样的代码:
| 12
 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 函数,生成一道新的题目
| 12
 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> 中与一个按钮关联
| 12
 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>
 
 | 

判断功能也一切正常~

接下来完善 亿 一点点细节,再添加一个简单的样式
| 12
 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 秒计算一次时间差呢
| 12
 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 函数中清除计时任务哦
| 12
 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>
 
 | 
现在让我们加点样式吧~

    
    
        | 12
 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>
 
 | 
  
两种模式的切换
两种模式下的限时不同,故可以用最大时间来作为不同模式的判断依据
| 12
 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 来绘制开关按钮的样式
| 12
 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>
 
 | 
再加一点点细节吧~


    
    
        | 12
 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 元素,分别用来存放已做题数和正确题数
| 12
 3
 4
 
 | <script setup lang="ts">const questionCount = ref(0);
 const correctCount = ref(0);
 </script>
 
 | 
计时
按照要求,要实现固定 30 道题的计时,可以在按下提交按钮,即检查答案是否正确时,对于已完成的题数进行判断
在 checkAnswer 中,我们可以添加 questionCount 的自增和 correctCount 的自增,当完成一道题时,使完成数 questionCount +1,若回答正确,使正确数 correctCount +1
同时,我们也可以在 checkAnswer 中添加对停止提问的条件判断,代码如下:
| 12
 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 函数
| 12
 3
 4
 5
 6
 7
 
 | const stopQuiz = () => {started.value = false;
 stopped.value = true;
 if (timer !== null) {
 clearInterval(timer);
 }
 };
 
 | 
现在把它添加到 Vue 组件中,同时修改页面的 <template> 部分,实现停止后答题区隐藏,同时信息栏保持显示
| 12
 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 函数中,同时也应注意一道题都不做的情况出现
| 12
 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> 部分,也添加相应的分数显示
| 12
 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 任务中加一个判断,在每次更新计时器的同时对是否超时做出判断
| 12
 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);
 };
 
 | 
加上样式,就有了这样的效果:

    
    
        | 12
 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>
 
 | 
  
固定时间计算做题数量
这个模式和“按数量”大同小异,只需要添加另一种模式的分数计算即可
| 12
 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;
 }
 };
 
 | 

    
    
        | 12
 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 格式数据
| 12
 3
 4
 5
 6
 7
 8
 
 | interface QuestionDetail {question: string;
 userAnswer: string;
 correctAnswer: number | null;
 isCorrect: boolean;
 }
 
 const questionsDetails = ref<QuestionDetail[]>([]);
 
 | 
随后可以在 checkAnswer 函数中使用 push 方法将每个问题的详细信息存入 questionsDetails 数组
| 12
 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
 });
 
 ...
 
 };
 
 | 
这样一来,每一次的问答就会被记录下来了
那么下一步,该怎么生成并下载数据文件呢?
可以通过这样的代码实现:
| 12
 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 相结合
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | <template><div>
 
 ...
 
 <button v-if="stopped" @click="downloadSummary">下载数据</button>
 
 ...
 
 </div>
 </template>
 
 | 

    
    
        | 12
 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>
 
 | 
  
    
    
        | 12
 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 页面查看具体内容