Skip to content

实现 算术题生成器 工具

分类:education | 标签:数学、教育、练习 自动生成数学练习题

2.1 功能说明

自动生成数学练习题

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-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 效果截图

暂未生成该工具的效果截图。

工具访问地址:https://www.i91tools.com/tools/math-generator