Appearance
实现 数学表达式计算 工具
分类:general | 标签:数学、计算、公式 安全求值数学表达式
2.1 功能说明
安全求值数学表达式
2.1.1 使用指南
功能说明
安全计算数学表达式,支持 + - * / ^ %、括号、常量 pi/e,函数 sqrt/sin/cos/tan/log/ln/abs/pow/fact。
使用场景
公式求值、作业核对(不使用 eval,防注入)。
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> 中定义的主要函数/方法:
tokenize()toRPN()evalRPN()run()clear()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'
const expr = ref('')
const result = ref('')
const error = ref('')
const FUNCS = {
sqrt: Math.sqrt, sin: Math.sin, cos: Math.cos, tan: Math.tan,
log: (x) => Math.log10(x), ln: Math.log, abs: Math.abs,
pow: (a, b) => Math.pow(a, b), fact: (x) => { let r = 1; for (let i = 2; i <= x; i++) r *= i; return r }
}
const CONST = { pi: Math.PI, e: Math.E }
function tokenize(s) {
const re = /\d+\.?\d*|[a-zA-Z_]+|\*\*|[%+\-*/^(),!]/g
return s.match(re) || []
}
function toRPN(tokens) {
const out = [], op = []
const prec = { '+': 1, '-': 1, '*': 2, '/': 2, '%': 2, '^': 3, 'u-': 4, '!': 5 }
const right = { '^': true, 'u-': true, '!': true }
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i]
if (/^\d+\.?\d*$/.test(t)) out.push({ n: parseFloat(t) })
else if (CONST[t] !== undefined) out.push({ n: CONST[t] })
else if (FUNCS[t]) { op.push(t) }
else if (t === '(') op.push(t)
else if (t === ')') {
while (op.length && op[op.length - 1] !== '(') out.push({ o: op.pop() })
if (!op.length) throw new Error('括号不匹配')
op.pop()
} else if (t === '!') out.push({ o: '!' })
else if (t === '-' && (i === 0 || ['+', '-', '*', '/', '%', '^', '('].includes(tokens[i - 1]) || FUNCS[tokens[i - 1]])) {
op.push('u-')
} else if (t === '^' || t === '*' || t === '/' || t === '%' || t === '+' || t === '-') {
while (op.length && op[op.length - 1] !== '(' &&
(prec[op[op.length - 1]] > prec[t] || (prec[op[op.length - 1]] === prec[t] && !right[t]))) {
out.push({ o: op.pop() })
}
op.push(t)
} else throw new Error('无法识别:' + t)
}
while (op.length) {
const o = op.pop()
if (o === '(') throw new Error('括号不匹配')
out.push({ o })
}
return out
}
function evalRPN(rpn) {
const st = []
for (const t of rpn) {
if (t.n !== undefined) st.push(t.n)
else if (t.o === 'u-') st.push(-st.pop())
else if (t.o === '!') { const x = st.pop(); st.push(FUNCS.fact(x)) }
else if (FUNCS[t.o]) {
if (t.o === 'fact') continue
const b = st.pop(), a = st.pop(); st.push(FUNCS[t.o](a, b))
} else {
const b = st.pop(), a = st.pop()
st.push({ '+': a + b, '-': a - b, '*': a * b, '/': a / b, '%': a % b, '^': Math.pow(a, b) }[t.o])
}
}
if (st.length !== 1) throw new Error('表达式无效')
return st[0]
}
function run() {
error.value = ''; result.value = ''
const s = expr.value.trim()
if (!s) { error.value = '请输入表达式'; return }
try {
const rpn = toRPN(tokenize(s.replace(/\*\*/g, '^')))
const v = evalRPN(rpn)
if (!Number.isFinite(v)) throw new Error('结果非有限数')
result.value = String(v)
} catch (e) { error.value = e.message }
}
function clear() { expr.value = result.value = error.value = '' }2.3 效果截图
