FormulaInput 公式输入组件

一个支持简单数学公式计算的输入组件,基于 Vue 3 + TypeScript + Ant Design Vue 开发。

功能特点

  • 支持直接输入数字
  • 支持输入数学公式(使用等号触发计算)
  • 支持下拉选项选择
  • 支持自定义小数位数
  • 实时显示计算结果
  • 失焦时自动格式化结果
  • 支持 v-model 双向绑定
  • 支持禁用状态

使用示例

<template>
  <formula-input
    v-model="value"
    :options="options"
    :decimal-places="2"
    placeholder="请输入数字或计算公式"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';

const value = ref<number>(0);
const options = ref([
  { label: '选项1', value: 100 },
  { label: '选项2', value: 200 },
]);
</script>

属性说明

属性名 类型 默认值 说明
modelValue string | number ‘’ 组件的值,支持 v-model 双向绑定
placeholder string ‘请输入数字或计算公式’ 输入框占位文本
disabled boolean false 是否禁用
options Option[] [] 下拉选项列表
decimalPlaces number 2 小数位数,小于 0 表示不保留小数

Option 接口

interface Option {
  label: string;    // 选项显示文本
  value: string | number;  // 选项值
}

事件

事件名 说明 回调参数
update:modelValue 值更新时触发 (value: number)
change 值变化时触发 (value: number)

数学公式支持

组件支持以下数学运算符:

  • 加法:+
  • 减法:-
  • 乘法:*
  • 除法:/
  • 括号:()
  • 小数点:.

示例:

1 + 1 = 2
2 * 3 = 6
(1 + 2) * 3 = 9
10 / 2 = 5

注意事项

  1. 输入公式时需要使用等号(=)触发计算
  2. 失焦时会自动格式化结果
  3. 所有返回给父组件的值都是数字类型
  4. 当输入值无法转换为数字时,会返回 0
  5. 在 vxe-table 的可编辑单元格中使用时,组件会自动处理编辑态切换时的计算

依赖项

  • Vue 3
  • TypeScript
  • Ant Design Vue
  • mathjs(用于数学公式计算)
  • es2020

组件代码

<template>
  <div class="formula-input-wrapper">
    <a-auto-complete
      v-model:value="inputValue"
      :placeholder="placeholder"
      :disabled="disabled"
      :options="options"
      :filter-option="false"
      @search="handleSearch"
      @blur="handleBlur"
      @focus="handleFocus"
      @select="handleSelect"
    >
      <template #suffix>
        <span v-if="result !== null && !isFocused" class="result">= {{ result }}</span>
      </template>
    </a-auto-complete>
  </div>
</template>

<script lang="ts" setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import { evaluate } from 'mathjs';

// 定义选项接口
interface Option {
  label: string;
  value: string | number;
}

// 定义组件属性接口
interface Props {
  modelValue?: string | number;
  placeholder?: string;
  disabled?: boolean;
  options?: Option[];
  decimalPlaces?: number; // 小数位数配置
}

// 设置组件属性默认值
const props = withDefaults(defineProps<Props>(), {
  modelValue: '',
  placeholder: '请输入数字或计算公式',
  disabled: false,
  options: () => [],
  decimalPlaces: 2, // 默认保留两位小数
});

const emit = defineEmits(['update:modelValue', 'change']);

// 核心状态变量
const inputValue = ref<string>(''); // 输入框的值
const result = ref<number | null>(null); // 计算结果
const isFocused = ref<boolean>(false); // 输入框是否聚焦

/**
 * 格式化数字,根据配置保留小数位数并四舍五入
 * @param num 需要格式化的数字
 * @returns 格式化后的数字字符串
 */
const formatNumber = (num: number) => {
  // 如果 decimalPlaces 小于 0,表示不需要保留小数位数
  if (props.decimalPlaces < 0) {
    return Math.round(num).toString();
  }
  
  // 如果需要保留小数位数,使用配置的位数
  return Number(Math.round(Number(num + `e${props.decimalPlaces}`)) + `e-${props.decimalPlaces}`).toFixed(props.decimalPlaces);
};

/**
 * 计算公式结果
 * @param formula 计算公式
 */
const calculateResult = (formula: string) => {
  try {
    formula = formula.replace(/\s/g, ''); // 移除所有空格
    
    // 验证公式有效性
    if (!formula || !/^[\d+\-*/(). ]+$/.test(formula)) {
      result.value = null;
      return;
    }

    // 计算结果
    result.value = evaluate(formula);
  } catch (error) {
    result.value = null;
  }
};

/**
 * 初始化输入值并处理公式
 * @param value 初始值
 */
const initValue = (value: string | number) => {
  if (value) {
    inputValue.value = String(value);
    if (inputValue.value.includes('=')) {
      const formula = inputValue.value.split('=')[1].trim();
      calculateResult(formula);
    }
  }
};

// 事件处理函数
const handleSearch = (value: string) => {
  // 尝试将输入值转换为数字
  const numValue = Number(value);
  emit('update:modelValue', isNaN(numValue) ? 0 : numValue);
  emit('change', isNaN(numValue) ? 0 : numValue);
};

const handleSelect = (value: string | number) => {
  inputValue.value = String(value);
  result.value = null;
  // 确保返回数字类型
  const numValue = Number(value);
  emit('update:modelValue', isNaN(numValue) ? 0 : numValue);
  emit('change', isNaN(numValue) ? 0 : numValue);
};

const handleBlur = () => {
  isFocused.value = false;
  if (result.value !== null) {
    const formattedResult = formatNumber(result.value);
    inputValue.value = formattedResult;
    // 使用计算结果(已经是数字类型)
    emit('update:modelValue', result.value);
    emit('change', result.value);
  } else {
    const numValue = Number(inputValue.value);
    emit('update:modelValue', isNaN(numValue) ? 0 : numValue);
    emit('change', isNaN(numValue) ? 0 : numValue);
  }
};

const handleFocus = () => {
  isFocused.value = true;
};

// 生命周期钩子
onMounted(() => {
  initValue(props.modelValue);
});

// 监听外部值变化
watch(
  () => props.modelValue,
  (newVal) => {
    if (newVal !== inputValue.value) {
      initValue(newVal);
    }
  }
);

// 监听输入值变化,自动计算公式
watch(
  () => inputValue.value,
  (newVal) => {
    if (newVal && newVal.includes('=')) {
      const formula = newVal.split('=')[1].trim();
      calculateResult(formula);
      // 如果计算结果存在,立即格式化
      if (result.value !== null) {
        result.value = Number(formatNumber(result.value));
      }
    } else {
      result.value = null;
    }
  }
);

// 组件卸载前确保公式计算完成
onBeforeUnmount(() => {
  if (inputValue.value && inputValue.value.includes('=')) {
    const formula = inputValue.value.split('=')[1].trim();
    calculateResult(formula);
    if (result.value !== null) {
      result.value = Number(formatNumber(result.value));
      emit('update:modelValue', result.value);
      emit('change', result.value);
    } else {
      const numValue = Number(inputValue.value);
      emit('update:modelValue', isNaN(numValue) ? 0 : numValue);
      emit('change', isNaN(numValue) ? 0 : numValue);
    }
  }
});

</script>

<style lang="less" scoped>
.formula-input-wrapper {
  width: 100%;
  
  // 确保输入框和自动完成组件占满容器宽度
  :deep(.ant-select),
  :deep(.ant-input-affix-wrapper) {
    width: 100%;
  }
  
  // 计算结果样式
  .result {
    color: #1890ff;
    margin-left: 8px;
  }
}
</style> 
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐