前提需求
PM复述客户需求:需要一个能够提供使用数学公式计算内部数据的功能(要求灵活功能强大一点)。
和后端商量了一下,由前端控制公式输入,后端用js引擎计算(具体啥js引擎 咱不懂)
1、不墨迹看效果

2、组件代码
FormulaEditor.vue
vue
<template>
<div>
<div style="margin: 8px 0; display: flex; gap: 12px;">
<!-- 函数选择 -->
<el-select
v-model="selectedFunction"
placeholder="插入函数"
clearable
@change="handleFunctionSelect"
style="width: 220px"
>
<el-option
v-for="fn in functionList"
:key="fn.name"
:label="`${fn.template}(${fn.label})`"
:value="fn.name"
/>
</el-select>
<!-- 参数选择 -->
<el-select
v-model="selectedParam"
placeholder="插入参数"
filterable
clearable
ref="selectRef"
@change="insertParam"
style="width: 150px"
>
<el-option
v-for="param in paramList"
:key="param.id"
:label="param.srcName + ' - ' + param.nameCn"
:value="param.expression??param.srcName"
/>
</el-select>
<!-- <el-link type="primary" href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math" target="_blank">查看更多JS数学函数</el-link> -->
</div>
<!-- 表达式输入框 -->
<textarea
v-model="currentFormula"
ref="textareaRef"
rows="4"
cols="60"
@input="validateFormula"
style="font-family: monospace; font-size: 14px; width: 100%;"
></textarea>
<!-- 解析提示 -->
<div style="margin-top: 6px;" v-if="currentFormula">
<span v-if="parseError" style="color: red;">❌ {{ parseError }}</span>
<span v-else style="color: green;">✅ 表达式合法</span>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed, nextTick } from 'vue'
import { parse as mathParse } from 'mathjs'
// Element Plus 选择框绑定值
const selectedFunction = ref('')
const selectedParam = ref('')
// 默认函数列表
const defaultFunctions = [
{ name: 'Math.max', template: 'Math.max(x, y)', label: '最大值' },
{ name: 'Math.min', template: 'Math.min(x, y)', label: '最小值' },
{ name: 'Math.pow', template: 'Math.pow(base, exponent)', label: '乘幂' },
{ name: 'Math.sqrt', template: 'Math.sqrt(x)', label: '平方根' },
{ name: 'Math.abs', template: 'Math.abs(x)', label: '绝对值' },
{ name: 'Math.floor', template: 'Math.floor(x)', label: '向下取整' },
{ name: 'Math.ceil', template: 'Math.ceil(x)', label: '向上取整' },
{ name: 'Math.round', template: 'Math.round(x)', label: '四舍五入' },
{ name: 'Math.log', template: 'Math.log(x)', label: '自然对数' },
// { name: 'Math.sign', template: 'Math.sign(x)', label: '符号函数' },
// { name: 'Math.cbrt', template: 'Math.cbrt(x)', label: '立方根' },
]
const props = defineProps({
modelValue: {
type: String,
default: '',
},
params: {
type: Array,
default: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
},
variables: {
type: Object,
default: () => ({ a: 10, b: 5, c: 3 })
}
})
const emit = defineEmits(['update:modelValue'])
const currentFormula = ref('')
const parseError = ref(null)
const result = ref(null)
const textareaRef = ref(null)
const functionList = computed(() => defaultFunctions)
const paramList = computed(() => props.params)
// 插入光标位置文本
const insertAtCursor = (text) => {
const textarea = textareaRef.value
if (!textarea) return
const start = textarea.selectionStart
const end = textarea.selectionEnd
const oldVal = currentFormula.value
currentFormula.value = oldVal.slice(0, start) + text + oldVal.slice(end)
nextTick(() => {
textarea.focus()
textarea.selectionStart = textarea.selectionEnd = start + text.length
})
}
// 选择函数插入模板
const handleFunctionSelect = (fnName) => {
const fn = functionList.value.find(f => f.name === fnName)
if (!fn) return
insertAtCursor(fn.template)
selectedFunction.value = ''
}
const selectRef = ref(null)
// 插入参数
const insertParam = (param) => {
if (!param) return
insertAtCursor(param)
selectedParam.value = ''
setTimeout(() => {
selectRef.value?.blur()
})
}
// 表达式校验 + 运行
const validateFormula = () => {
let expr = currentFormula.value.trim()
if (expr.startsWith('=')) {
expr = expr.slice(1)
}
try {
const node = mathParse(expr)
const usedSymbols = new Set()
node.traverse(function (node, path, parent) {
if (node.isSymbolNode) {
usedSymbols.add(node.name)
}
})
const allowedVars = paramList.value.map(p => p.srcName)
const allowedFuncs = [
'abs', 'ceil', 'floor', 'max', 'min', 'pow',
'round', 'sign', 'sqrt', 'log', 'cbrt'
]
allowedVars.push('Math')
// 检查未定义变量
const invalid = Array.from(usedSymbols).filter(
name => !allowedVars.includes(name) && !allowedFuncs.includes(name)
)
if (invalid.length) {
parseError.value = `未定义的变量:${invalid.join(', ')}`
result.value = null
} else {
parseError.value = null
result.value = '合法公式'
}
} catch (err) {
parseError.value = `语法错误:${err.message}`
result.value = null
}
}
watch(() => props.modelValue, (val) => {
currentFormula.value = val
validateFormula()
}, { immediate: true })
watch(currentFormula, (val) => {
emit('update:modelValue', val)
})
</script>
<style scoped>
</style>
Tips:
内部参数逻辑其实还是有点复杂的,没有贴出来,提供的代码拓展性也是很强的,可以根据需求来调整。