Skip to content

实现 科学计算器 工具

分类:retirement | 标签:计算器、科学、函数 支持函数、阶乘、进制转换的科学计算器

2.1 功能说明

支持函数、阶乘、进制转换的科学计算器

2.1.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> 中定义的主要函数/方法:

  • trackCaret()
  • currentPos()
  • spliceAt()
  • onExprInput()
  • ins()
  • clear()
  • back()
  • factorial()
  • callFunc()
  • tokenize()
  • evaluate()
  • peek()
  • next()
  • exprP()
  • term()
  • factor()
  • unary()
  • postfix()
  • primary()
  • equals()
  • formatNum()
  • loadHist()
  • clearHist()
  • doBase()
  • onKey()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref, nextTick, onMounted, onBeforeUnmount } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'

const angle = ref('deg')
const expr = ref('')              // 完整的输入表达式(始终可见、可直接编辑)
const exprInput = ref(null)
const caret = ref(0)             // 输入框光标位置(失焦时仍保留最后光标,供按钮插入使用)
const err = ref('')
const result = ref('')            // 计算结果展示(= ...)
const history = ref([])
const baseVal = ref(''), baseFrom = ref(10), baseOut = ref(null)

// 记录光标位置:点击/键盘/鼠标抬起/选中/聚焦都同步
function trackCaret(e) {
  const el = e && e.target
  if (el && typeof el.selectionStart === 'number') caret.value = el.selectionStart
}
// 当前应插入的位置:焦点在输入框用真实光标,否则用最后记录的光标
function currentPos() {
  const el = exprInput.value
  if (el && document.activeElement === el && typeof el.selectionStart === 'number') return el.selectionStart
  return Math.min(caret.value, expr.value.length)
}
// 在指定位置插入并把光标移到插入内容之后,再重新聚焦输入框
function spliceAt(pos, text) {
  const before = expr.value.slice(0, pos)
  const after = expr.value.slice(pos)
  expr.value = before + text + after
  const newPos = pos + text.length
  nextTick(() => {
    const el = exprInput.value
    if (el) { el.focus(); el.setSelectionRange(newPos, newPos); caret.value = newPos }
  })
}

// 任意修改表达式都让旧结果失效;按钮追加与键盘原生输入都走这里
function onExprInput() { result.value = ''; err.value = '' }
function ins(tok) {
  err.value = ''
  result.value = ''
  spliceAt(currentPos(), tok)
}
function clear() { expr.value = ''; result.value = ''; err.value = ''; caret.value = 0 }
function back() {
  err.value = ''; result.value = ''
  const pos = currentPos()
  if (pos <= 0) return
  // 删除光标前的一个字符:保留 [0,pos-1) [pos,末尾)
  expr.value = expr.value.slice(0, pos - 1) + expr.value.slice(pos)
  const newPos = pos - 1
  nextTick(() => {
    const el = exprInput.value
    if (el) { el.focus(); el.setSelectionRange(newPos, newPos); caret.value = newPos }
  })
}

function factorial(n) {
  if (n < 0 || !Number.isInteger(n)) throw new Error('阶乘仅支持非负整数')
  if (n > 170) throw new Error('数值过大')
  let r = 1; for (let i = 2; i <= n; i++) r *= i; return r
}
function callFunc(name, x) {
  const deg = angle.value === 'deg'
  const toRad = v => deg ? v * Math.PI / 180 : v
  const fromRad = v => deg ? v * 180 / Math.PI : v
  const map = {
    sin: v => Math.sin(toRad(v)), cos: v => Math.cos(toRad(v)), tan: v => Math.tan(toRad(v)),
    asin: v => fromRad(Math.asin(v)), acos: v => fromRad(Math.acos(v)), atan: v => fromRad(Math.atan(v)),
    sinh: Math.sinh, cosh: Math.cosh, tanh: Math.tanh,
    ln: Math.log, log: v => Math.log10(v), lg: v => Math.log10(v),
    sqrt: Math.sqrt, cbrt: Math.cbrt, exp: Math.exp, abs: Math.abs,
    floor: Math.floor, ceil: Math.ceil, round: Math.round, sign: Math.sign
  }
  if (!(name in map)) throw new Error('未知函数: ' + name)
  return map[name](x)
}

function tokenize(s) {
  const toks = []; let i = 0
  const isDig = c => c >= '0' && c <= '9'
  while (i < s.length) {
    const c = s[i]
    if (c === ' ' || c === '\t') { i++; continue }
    if (isDig(c) || c === '.') {
      let j = i + 1
      while (j < s.length && (isDig(s[j]) || s[j] === '.')) j++
      toks.push({ t: 'num', v: s.slice(i, j) }); i = j; continue
    }
    if (/[a-zA-Zπ]/.test(c)) {
      let j = i + 1
      while (j < s.length && /[a-zA-Z]/.test(s[j])) j++
      toks.push({ t: 'id', v: s.slice(i, j) }); i = j; continue
    }
    if ('+-*/%^()!'.includes(c)) { toks.push({ t: 'op', v: c }); i++; continue }
    if (c === '×') { toks.push({ t: 'op', v: '*' }); i++; continue }
    if (c === '÷') { toks.push({ t: 'op', v: '/' }); i++; continue }
    throw new Error('非法字符: ' + c)
  }
  return toks
}

function evaluate(s) {
  const tokens = tokenize(s)
  if (!tokens.length) throw new Error('表达式为空')
  let pos = 0
  const peek = () => tokens[pos]
  const next = () => tokens[pos++]
  function exprP() {
    let v = term()
    while (peek() && peek().t === 'op' && (peek().v === '+' || peek().v === '-')) {
      const op = next().v; const r = term(); v = op === '+' ? v + r : v - r
    }
    return v
  }
  function term() {
    let v = factor()
    while (peek() && peek().t === 'op' && (peek().v === '*' || peek().v === '/' || peek().v === '%')) {
      const op = next().v; const r = factor()
      v = op === '*' ? v * r : op === '/' ? v / r : v % r
    }
    return v
  }
  function factor() {
    let v = unary()
    if (peek() && peek().t === 'op' && peek().v === '^') { next(); v = Math.pow(v, factor()) }
    return v
  }
  function unary() {
    if (peek() && peek().t === 'op' && (peek().v === '+' || peek().v === '-')) {
      const op = next().v; const r = unary(); return op === '-' ? -r : r
    }
    return postfix()
  }
  function postfix() {
    let v = primary()
    while (peek() && peek().t === 'op' && peek().v === '!') { next(); v = factorial(v) }
    return v
  }
  function primary() {
    const tk = peek()
    if (!tk) throw new Error('表达式不完整')
    if (tk.t === 'num') { next(); const n = parseFloat(tk.v); if (Number.isNaN(n)) throw new Error('数字无效'); return n }
    if (tk.t === 'op' && tk.v === '(') {
      next(); const v = exprP()
      if (!peek() || peek().v !== ')') throw new Error('缺少右括号')
      next(); return v
    }
    if (tk.t === 'id') {
      next()
      const low = tk.v.toLowerCase()
      if (low === 'pi') return Math.PI
      if (low === 'e') return Math.E
      if (peek() && peek().t === 'op' && peek().v === '(') {
        next(); const arg = exprP()
        if (!peek() || peek().v !== ')') throw new Error('函数缺少右括号')
        next(); return callFunc(low, arg)
      }
      throw new Error('未知标识符: ' + tk.v)
    }
    throw new Error('无法解析: ' + JSON.stringify(tk.v))
  }
  const v = exprP()
  if (pos < tokens.length) throw new Error('表达式存在多余符号')
  return v
}

function equals() {
  err.value = ''
  const s = expr.value.trim()
  if (!s) { err.value = '请输入表达式'; return }
  try {
    const val = evaluate(s)
    if (!Number.isFinite(val)) throw new Error('计算结果非有限值')
    const txt = formatNum(val)
    // 保留完整表达式可见,结果单独成行;不覆盖输入框
    result.value = '= ' + txt
    history.value.unshift({ expr: s, val: txt })
    if (history.value.length > 50) history.value.pop()
  } catch (e) { err.value = e.message; result.value = '' }
}
function formatNum(v) {
  if (Number.isInteger(v)) return String(v)
  return String(parseFloat(v.toPrecision(12)))
}

function loadHist(h) {
  // 点击历史:载入当时的表达式,用户可继续编辑后重算
  expr.value = h.expr
  result.value = ''
  err.value = ''
  caret.value = h.expr.length
  nextTick(() => {
    const el = exprInput.value
    if (el) { el.focus(); el.setSelectionRange(el.value.length, el.value.length) }
  })
}

function clearHist() { history.value = [] }

function doBase() {
  err.value = ''
  const base = baseFrom.value
  const valid = base === 16 ? /^[0-9a-fA-F]+$/ : base === 8 ? /^[0-7]+$/ : base === 2 ? /^[01]+$/ : /^-?\d+$/
  if (!baseVal.value.trim()) { err.value = '请输入数值'; return }
  if (!valid.test(baseVal.value.trim())) { err.value = `输入含非 ${base} 进制字符`; return }
  const dec = parseInt(baseVal.value.trim(), base)
  if (Number.isNaN(dec)) { err.value = '进制解析失败'; return }
  baseOut.value = {
    dec: String(dec),
    hex: '0x' + dec.toString(16).toUpperCase(),
    oct: '0o' + dec.toString(8),
    bin: dec.toString(2)
  }
}

function onKey(e) {
  const tag = (e.target.tagName || '').toLowerCase()
  // 焦点在输入框(含表达式框/进制数值框)时,交给原生输入处理
  if (tag === 'input' || tag === 'textarea') return
  const k = e.key
  if (/[0-9]/.test(k)) ins(k)
  else if (k === '+' || k === '-' || k === '*' || k === '/' || k === '%' || k === '(' || k === ')' || k === '.' || k === '^') ins(k)
  else if (k === 'Enter') equals()
  else if (k === 'Backspace') back()
  else if (k === 'Escape') clear()
}
onMounted(() => window.addEventListener('keydown', onKey))
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))

2.3 效果截图

科学计算器 效果截图

工具访问地址:https://www.i91tools.com/tools/scientific-calculator