小登口算(基础篇)

  • ~74.35K 字
  • 次阅读
  • 条评论
  1. 1. 题目
    1. 1.1. 题目描述
    2. 1.2. 基本要求
    3. 1.3. 进阶要求
  2. 2. 实现过程【基础篇】
    1. 2.1. 随机数的生成
    2. 2.2. 算术题的生成
    3. 2.3. 制作一个答题页面
    4. 2.4. 计时器的实现
    5. 2.5. 两种模式的切换
    6. 2.6. 固定题目数量计算用时
      1. 2.6.1. 计数
      2. 2.6.2. 计时
      3. 2.6.3. 计分
    7. 2.7. 固定时间计算做题数量
    8. 2.8. 相关数据的保存
  3. 3. 程序相关
    1. 3.1. Github
    2. 3.2. DEMO
    3. 3.3. 其他说明

最近在做的一个小练习有点意思,就把完成过程记录下来了,也算是自己的一份笔记吧~
由于使用了 Vue.jsVite 作为前端框架,故本文中的代码多使用 TypeScript 书写
程序的完整代码在 Github 使用 MIT 协议开源,相关信息可以在文末找到,供一起学习的小伙伴们参考~
文中的代码还有很多很多的不足,如果屏幕前的你有更好的建议,欢迎在评论区提出,或是去 Github 提一个 pr ~

本文为基本要求的实现过程,进阶要求请转到【进阶篇】

题目

题目描述

老王家里的小登玩口算被外挂虐爆,于是老王想自研一款口算软件,满足他家小登的好学心。请你帮帮老王,为他写一款口算程序。

基本要求

  1. 题目范围包括100以内的加减乘除,除法必须能整除,题目随机生成。
  2. 有两种模式,固定题目数量计算用时,以及固定时间计算做题数量。
  3. 固定题目数量计算用时为:固定30道题目,计算用时,超过15分钟强行中止作答,最后以正确率的百分数加剩余时间为得分。
  4. 固定时间计算做题数量为:固定3分钟,以做对的题目数量乘以正确率为得分。
  5. 需要保存每次测试的日期时间、模式、题目和分数。

进阶要求

  1. 新增多用户功能,程序可以通过用户名和密码注册及登录,每个用户的数据分开存储。
  2. 新增错题本功能,程序支持查看本人错题本中的题目。做错的题目自动加入错题本,在进行任何练习时,优先从错题本中抽取题目,如果答对则从错题本中移除。
  3. 新增热座PK功能,两种模式都支持两名用户的热座PK。热座PK指的是:出相同的题目,两个小登依次上机测试,然后对得分进行排名。需要保存用户的PK场数和胜率。
  4. 小登精通电脑,请你想办法让他没法乱改你保存的数据,特别是场数和胜率。

实现过程【基础篇】

随机数的生成

在 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; // 除数不为0
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; // 除数不为 0
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> 部分创建一个 userAnswerref

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 函数,用于将 startedref 状态修改为布尔值 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 提交或跳转到下一题

按下 Start 按钮前不显示题目和答题区

输入内容前 Submit 按钮不可用

提交后禁用输入框并显示 Next Question 按钮

到这里,一个有着基础功能的简易答题页面就完成啦~

基本要求 1 达成!

计时器的实现

在 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 清除计时任务

1
clearInterval(timer);

实现了基本的计时功能,该怎么在页面显示出来呢?
在 Vue 中,可以通过 {{ (elapsedTime / 1000).toFixed(2) }} 来读取并显示 elapsedTime 变量仅保留 2 位小数的值,由于 elapsedTimesetInterval 任务下每 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);

那结合先前设置的状态值可以有以下逻辑:
startedfalsestopped 同样为 false 时,程序还未开始运行,计时器停止;
startedtruestoppedfalse 时,程序正在运行,计时器运行;
startedtruestopped 同样为 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); // 每 0.01 秒更新一次
};

加上样式,就有了这样的效果:

效果图~

完整代码
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>

基本要求 2, 3, 4 达成!

相关数据的保存

可以通过将数据暂存在数组中实现数据的临时保存

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' }); // 创建 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
};

这里将数组 questionsDetails 中保存的各题数据放入 JSON 的 questions 部分,同时也在 JSON 中加入了 correctCountquestionCountstartTimeelapsedTimescoremode,分别用于记录回答正确的题数、回答的总题数、开始时间、用时、得分、模式
随后将 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 页面查看具体内容