Appearance
实现 算术题生成器 工具
分类:education | 标签:数学、教育、练习 自动生成数学练习题
2.1 功能说明
自动生成数学练习题
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
loadHistory()trimHistory()saveToHistory()pad()deleteHistoryRecord()clearAllHistory()goBack()generateProblem()openPrintPreview()toggleAllAnswers()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import {computed, onMounted, reactive, ref} from 'vue'
import { useRouter } from 'vue-router'
import { ElButton, ElIcon, ElMessageBox, ElMessage } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import FeedbackForm from '@/components/FeedbackForm.vue'
import { useAuthStore } from '@/stores/auth'
import { isCustomCodeAllowed } from '@/utils/permission.js'
import { safeArithmetic } from '@/utils/safeArithmetic.js'
interface ProblemConfig {
questionCount: number
maxNumber: number
divideInteger: boolean
multiplySingleDigit: boolean
}
interface MathProblem {
expression: string
answer: number
difficulty: string
hasBrackets: boolean
showAnswer?: boolean
}
interface HistoryRecord {
id: number
createdAt: string
problems: MathProblem[]
}
const authStore = useAuthStore()
// 非受信任角色使用安全模式时的「权限提示」只弹一次
const customCodeWarned = ref(false)
const DB_NAME = 'MathGeneratorDB'
const DB_VERSION = 1
const STORE_NAME = 'history'
const MAX_RECORDS = 5
const openDB = (): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION)
request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true })
store.createIndex('createdAt', 'createdAt', { unique: false })
}
}
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
const dbGetAll = async (): Promise<HistoryRecord[]> => {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly')
const store = tx.objectStore(STORE_NAME)
const request = store.getAll()
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
const dbAdd = async (record: Omit<HistoryRecord, 'id'>): Promise<HistoryRecord> => {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite')
const store = tx.objectStore(STORE_NAME)
const request = store.add(record)
request.onsuccess = () => {
resolve({ ...record, id: request.result as number })
}
request.onerror = () => reject(request.error)
})
}
const dbDelete = async (id: number): Promise<void> => {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite')
const store = tx.objectStore(STORE_NAME)
const request = store.delete(id)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
const dbClear = async (): Promise<void> => {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite')
const store = tx.objectStore(STORE_NAME)
const request = store.clear()
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
const config = reactive<ProblemConfig>({
questionCount: 10,
maxNumber: 999,
divideInteger: true,
multiplySingleDigit: true
})
const problems = ref<MathProblem[]>([])
const historyList = ref<HistoryRecord[]>([])
const loadHistory = async () => {
try {
const records = await dbGetAll()
records.sort((a, b) => b.id - a.id)
historyList.value = records
} catch (e) {
console.error('加载历史记录失败:', e)
}
}
const trimHistory = async () => {
const all = await dbGetAll()
all.sort((a, b) => a.id - b.id)
while (all.length > MAX_RECORDS) {
const oldest = all.shift()!
await dbDelete(oldest.id)
}
}
const saveToHistory = async (newProblems: MathProblem[]) => {
try {
const now = new Date()
const pad = (n: number) => n.toString().padStart(2, '0')
const createdAt = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
const problemsToSave = newProblems.map(p => ({
expression: p.expression,
answer: p.answer,
difficulty: p.difficulty,
hasBrackets: p.hasBrackets
}))
await dbAdd({ createdAt, problems: problemsToSave })
await trimHistory()
await loadHistory()
} catch (e) {
console.error('保存历史记录失败:', e)
}
}
const deleteHistoryRecord = async (index: number) => {
try {
await ElMessageBox.confirm('确定删除该条历史记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await dbDelete(historyList.value[index].id)
await loadHistory()
} catch {
// 用户取消
}
}
const clearAllHistory = async () => {
try {
await ElMessageBox.confirm('确定清空所有历史记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await dbClear()
await loadHistory()
} catch {
// 用户取消
}
}
// 当前激活的Tab
const activeTab = ref('generator')
const router = useRouter()
onMounted(() => {
loadHistory()
})
// 返回板块列表
const goBack = () => {
router.push('/education')
}
// 是否所有答案都可见
const allAnswersVisible = computed(() => {
return problems.value.every(p => p.showAnswer)
})
// 生成随机数
const randomInt = (min: number, max: number): number => {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// 表达式模板接口
interface ExpressionTemplate {
pattern: string // 表达式模板
generate: () => Record<string, number> // 生成数字的方法
hasBrackets: boolean
}
// 模板库
const expressionTemplates: ExpressionTemplate[] = [
// ========== 一步运算 ==========
{
pattern: 'a + b',
generate: () => ({
a: randomInt(10, config.maxNumber),
b: randomInt(10, config.maxNumber)
}),
hasBrackets: false
},
{
pattern: 'a - b',
generate: () => {
const a = randomInt(20, config.maxNumber)
const b = randomInt(10, a) // 确保结果非负
return {a, b}
},
hasBrackets: false
},
{
pattern: 'a × b',
generate: () => {
if (config.multiplySingleDigit) {
return {
a: randomInt(10, config.maxNumber),
b: randomInt(2, 9) // 乘数是个位数
}
}
return {
a: randomInt(1, Math.min(config.maxNumber, 99)),
b: randomInt(2, Math.min(config.maxNumber, 99))
}
},
hasBrackets: false
},
{
pattern: 'a ÷ b',
generate: () => {
const b = randomInt(2, 9) // 除数是个位数
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / b))
const quotient = randomInt(1, maxQuotient)
const a = b * quotient // 确保整除
return {a, b}
},
hasBrackets: false
},
// ========== 两步运算(带括号) ==========
{
pattern: '(a + b) + c',
generate: () => ({
a: randomInt(10, config.maxNumber),
b: randomInt(10, config.maxNumber),
c: randomInt(10, config.maxNumber)
}),
hasBrackets: true
},
{
pattern: '(a + b) - c',
generate: () => {
const a = randomInt(10, config.maxNumber)
const b = randomInt(10, config.maxNumber)
const c = randomInt(10, a + b) // 确保结果非负
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a + b) × c',
generate: () => {
const a = randomInt(10, config.maxNumber)
const b = randomInt(10, config.maxNumber)
const c = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 9)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a + b) ÷ c',
generate: () => {
const c = randomInt(2, 9) // 除数是个位数
const quotient = randomInt(10, Math.floor(config.maxNumber / c))
const sum = c * quotient // 确保 (a+b) 能被 c 整除
const a = randomInt(5, sum - 1)
const b = sum - a
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a - b) + c',
generate: () => {
const a = randomInt(10, config.maxNumber)
const b = randomInt(10, a)
const c = randomInt(10, config.maxNumber)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a - b) - c',
generate: () => {
const a = randomInt(10, config.maxNumber)
const b = randomInt(10, a - 5)
const c = randomInt(0, a - b) // 确保结果非负
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a - b) × c',
generate: () => {
const a = randomInt(10, config.maxNumber)
const b = randomInt(1, a-5)
const c = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 9)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a - b) ÷ c',
generate: () => {
const c = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / c))
const quotient = randomInt(1, maxQuotient)
const diff = c * quotient
const maxA = Math.min(config.maxNumber, diff + 100)
const a = randomInt(diff, Math.max(diff, maxA))
const b = a - diff
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a × b) + c',
generate: () => {
const a = randomInt(1, config.multiplySingleDigit ? 99 : config.maxNumber)
const b = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 99)
const c = randomInt(1, config.maxNumber)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a × b) - c',
generate: () => {
const a = randomInt(1, config.multiplySingleDigit ? 99 : config.maxNumber)
const b = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 99)
const product = a * b
const c = randomInt(0, product)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a × b) × c',
generate: () => {
const a = randomInt(1, config.multiplySingleDigit ? 99 : config.maxNumber)
const b = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 99)
const c = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 99)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a × b) ÷ c',
generate: () => {
const c = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / c))
const quotient = randomInt(1, maxQuotient)
const product = c * quotient
// 分解 product 为 a × b
const factors = getFactors(product)
if (factors.length === 0) {
return {a: product, b: 1, c}
}
// 过滤掉 1 和 c,避免出现 × 1 和 (a×b)/b 的情况
const validFactors = factors.filter(f => f > 1 && f <= 99 && f !== c)
if (validFactors.length === 0) {
// 如果无法找到合适的因子,重新生成
return generate()
}
const factorIndex = randomInt(0, validFactors.length - 1)
let b = validFactors[factorIndex]
let a = product / b
// 如果要求乘数是个位数,检查 b
if (config.multiplySingleDigit && b > 9) {
// 尝试找个位数的因子(同时排除 c)
const singleDigitFactors = validFactors.filter(f => f >= 2 && f <= 9 && f !== c)
if (singleDigitFactors.length > 0) {
b = singleDigitFactors[randomInt(0, singleDigitFactors.length - 1)]
a = product / b
} else {
// 无法找到个位数因子,重新生成
return generate()
}
}
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a ÷ b) + c',
generate: () => {
const b = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / b))
const quotient = randomInt(1, maxQuotient)
const a = b * quotient
const c = randomInt(1, config.maxNumber)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a ÷ b) - c',
generate: () => {
const b = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / b))
const quotient = randomInt(1, maxQuotient)
const a = b * quotient
const c = randomInt(0, quotient)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a ÷ b) × c',
generate: () => {
const b = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / b))
const quotient = randomInt(1, maxQuotient)
const a = b * quotient
const c = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 9)
return {a, b, c}
},
hasBrackets: true
},
{
pattern: '(a ÷ b) ÷ c',
generate: () => {
const b = randomInt(2, 9)
const c = randomInt(2, 9)
const divisor = b * c
const maxFinalQuotient = Math.max(1, Math.floor(config.maxNumber / divisor))
const finalQuotient = randomInt(1, maxFinalQuotient)
const quotient = finalQuotient * c
const a = b * quotient
return {a, b, c}
},
hasBrackets: true
},
// ========== 两步运算(不带括号,先乘除后加减) ==========
{
pattern: 'a + b × c',
generate: () => {
const a = randomInt(1, config.maxNumber)
const b = randomInt(1, config.multiplySingleDigit ? 99 : config.maxNumber)
const c = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 99)
return {a, b, c}
},
hasBrackets: false
},
{
pattern: 'a - b × c',
generate: () => {
const b = randomInt(1, config.multiplySingleDigit ? 99 : config.maxNumber)
const c = config.multiplySingleDigit ? randomInt(2, 9) : randomInt(2, 99)
const product = b * c
const a = randomInt(product, config.maxNumber)
return {a, b, c}
},
hasBrackets: false
},
{
pattern: 'a + b ÷ c',
generate: () => {
const a = randomInt(1, config.maxNumber)
const c = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / c))
const quotient = randomInt(1, maxQuotient)
const b = c * quotient
return {a, b, c}
},
hasBrackets: false
},
{
pattern: 'a - b ÷ c',
generate: () => {
const c = randomInt(2, 9)
const maxQuotient = Math.max(1, Math.floor(config.maxNumber / c))
const quotient = randomInt(1, maxQuotient)
const b = c * quotient
const a = randomInt(quotient, config.maxNumber)
return {a, b, c}
},
hasBrackets: false
}
]
// 获取一个数的所有因子
const getFactors = (num: number): number[] => {
const factors: number[] = []
for (let i = 1; i <= num; i++) {
if (num % i === 0) {
factors.push(i)
}
}
return factors
}
// 替换模板中的变量为实际数字
const replaceTemplate = (pattern: string, values: Record<string, number>): string => {
let expression = pattern
Object.keys(values).forEach(key => {
const regex = new RegExp(`${key}(?!\\w)`, 'g')
expression = expression.replace(regex, values[key].toString())
})
return expression
}
// 计算表达式结果
const calculateExpression = (expression: string): number => {
try {
const jsExpression = expression.replace(/×/g, '*').replace(/÷/g, '/')
// 受信任角色(如管理员)保留原有执行路径;其余角色改用安全算术解析器,杜绝任意 JS 执行
const allowed = isCustomCodeAllowed(authStore.userInfo)
let result: number
if (allowed) {
result = new Function('return ' + jsExpression)()
} else {
if (!customCodeWarned.value) {
customCodeWarned.value = true
console.warn('当前账号无自定义代码执行权限,已使用安全算术模式计算(仅支持 + - * / 与括号)')
}
result = safeArithmetic(jsExpression)
}
// 处理浮点数精度问题,如果是整数则返回整数
if (Number.isInteger(result)) {
return result
}
// 保留小数点后 6 位,然后转回数字(避免显示 -0.00000001 这种)
return parseFloat(result.toFixed(6))
} catch (error) {
console.error('表达式计算失败:', expression, error)
return 0
}
}
// 生成表达式
const generateExpression = (): { expression: string; answer: number; hasBrackets: boolean } => {
try {
// 随机选择一个模板
const templateIndex = randomInt(0, expressionTemplates.length - 1)
const template = expressionTemplates[templateIndex]
// 使用模板生成数字
const values = template.generate()
// 替换模板得到表达式
const expression = replaceTemplate(template.pattern, values)
// 计算答案
const answer = calculateExpression(expression)
return {
expression,
answer,
hasBrackets: template.hasBrackets
}
} catch (error) {
console.error('生成表达式失败:', error)
// 返回一个默认的安全表达式
return {
expression: '1 + 1',
answer: 2,
hasBrackets: false
}
}
}
// 评估难度
const evaluateDifficulty = (expression: string): string => {
const operatorCount = (expression.match(/[+\-×÷]/g) || []).length
const hasBrackets = expression.includes('(')
if (hasBrackets || operatorCount > 1) {
return '中等'
} else if (expression.includes('×') || expression.includes('÷')) {
return '简单'
}
return '基础'
}
// 生成题目
const generateProblem = () => {
const newProblems: MathProblem[] = []
for (let i = 0; i < config.questionCount; i++) {
const {expression, answer, hasBrackets} = generateExpression()
newProblems.push({
expression,
answer,
difficulty: evaluateDifficulty(expression),
hasBrackets,
showAnswer: false // 默认隐藏答案
})
}
problems.value = newProblems
saveToHistory(newProblems)
}
const openPrintPreview = () => {
if (problems.value.length === 0) return
const problemItems = problems.value.map((p) => {
return `<div class="problem-cell">${p.expression} =</div>`
}).join('\n')
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>数学题目打印预览</title>
<style>
@page {
size: A4;
margin: 15mm 15mm 15mm 15mm;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #e8e8e8;
display: flex;
justify-content: center;
padding: 20px 0;
font-family: "Microsoft YaHei", "SimSun", sans-serif;
-webkit-print-color-adjust: exact;
}
.page {
width: 210mm;
min-height: 297mm;
padding: 15mm;
background: #fff;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
.problem-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
row-gap: 11em;
column-gap: 8mm;
}
.problem-cell {
font-size: 16px;
line-height: 1.6;
color: #333;
white-space: nowrap;
}
.print-btn-wrapper {
position: fixed;
top: 10px;
right: 10px;
z-index: 9999;
}
.print-btn-wrapper button {
padding: 10px 24px;
font-size: 16px;
background: #409eff;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
.print-btn-wrapper button:hover {
background: #66b1ff;
}
@media print {
body {
background: #fff;
padding: 0;
box-shadow: none;
}
.page {
box-shadow: none;
padding: 0;
}
.print-btn-wrapper {
display: none !important;
}
}
</style>
</head>
<body>
<div class="print-btn-wrapper">
<button onclick="window.print()">打 印</button>
</div>
<div class="page">
<div class="problem-grid">
${problemItems}
</div>
</div>
</body>
</html>`
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(html)
printWindow.document.close()
}
}
// 切换所有答案显示
const toggleAllAnswers = () => {
const newState = !allAnswersVisible.value
problems.value.forEach(problem => {
problem.showAnswer = newState
})
}2.3 效果截图
暂未生成该工具的效果截图。