mathjs简单实现一个数学计算公式及校验组件

前提需求

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:

内部参数逻辑其实还是有点复杂的,没有贴出来,提供的代码拓展性也是很强的,可以根据需求来调整。

相关推荐
全栈前端老曹2 小时前
【包管理】npm init 项目名后底层发生了什么的完整逻辑
前端·javascript·npm·node.js·json·包管理·底层原理
boooooooom2 小时前
Vue3 provide/inject 跨层级通信:最佳实践与避坑指南
前端·vue.js
一颗烂土豆2 小时前
Vue 3 + Three.js 打造轻量级 3D 图表库 —— chart3
前端·vue.js·数据可视化
青莲8432 小时前
Android 动画机制完整详解
android·前端·面试
iReachers2 小时前
HTML打包APK(安卓APP)中下载功能常见问题和详细介绍
前端·javascript·html·html打包apk·网页打包app·下载功能
颜酱2 小时前
前端算法必备:双指针从入门到很熟练(快慢指针+相向指针+滑动窗口)
前端·后端·算法
lichenyang4532 小时前
从零开始:使用 Docker 部署 React 前端项目完整实战
前端
沉静的思考者2 小时前
vue优雅的适配无障碍
vue.js
明月_清风2 小时前
【开源项目推荐】Biome:让前端代码质量工具链快到飞起来
前端