使用vue,手写一个计算器(科学计算器/标准计算器)

前言

最近产品经理又又加了一个需求,想在开单页面加一个计算器,用户就可以在录单时可以使用计算器了。

需求内容就一句话:支持加减乘除四则运算,点击计算器图标,当前页面弹出计算器弹窗。我一问,答案就是:你参考别人家的计算器做就行。

我的思路:记录用户每次按下的按键,然后拼接成一个运算表达式,然后再解析运算表达式不久okk了!!

解析运算表达式

封装一个函数,接收运算表达式,返回表达式的结果

按照产品的要求,解析运算表达式需要有这些功能:

  • 运算表达式需要支持 +,-,*,/ 四则运算,并且按照运算顺序,先乘除,后加减
  • 需要支持小数位数的计算,需要解决js计算的精度问题

思路与难点:

  1. 如何收集数字

    • 声明一个栈(数组),用于存储数字
    • 遍历字符串,如果是点或数字,就拼接起来,如果是运算符号,就将数字推入到栈中
  2. 解决运算顺序,先乘除后加减

    • 遍历字符串时,如果预存符号是+,就直接将数字推入栈,如果是-,就*-1再推入到栈,如果是* /,就先计算栈顶元素与当前数字的结果,再将结果推入到栈顶
    • 最后将栈中的数字相+,就是运算结果了
  3. 遍历到符号的时候,怎么将后面的数字收集起来推入到栈中

    • 先预先存一个+的符号,判断预存的符号,这样就可以将第一个数字推入栈中
    • 然后再将预存符号赋值为当前的符号。
  4. 解决js精度问题,可以采用外部库,或参考我之前封装的代码:解决js计算精度问题

举例:

  1. 假设有运算表达式:3.1 - 2 + 3 * 4 - 4 / 2
  2. 先乘除后加减,加入到栈中,栈:[ 3.1, -2, 12, -2 ]
  3. 再将栈中的元素相加:3.1 - 2 + 12 - 2 = 11.1

实现代码:

src/utils/parseExpression.js

function parseExpression(s) {
  s = s.replace(/\s/g, '') + 'e' // 去除所有空格,末尾加上一个结束符号,确保最后一个数加入到栈中
  let stack = []
  let preSign = '+' // 初始置为+,目的:让第一个数字入栈
  let curNum = 0 // 当前数字
  let reg = /[0-9]|./
  for (let i = 0; i < s.length; i++) {
    if (reg.test(s[i])) {
      // 当前字符为数字或点,拼接数字
      curNum = curNum + s[i]
    } else {
      // 当前字符是操作符,则判断运算上一符号
      if (preSign === '+') {
        stack.push(curNum)
      } else if (preSign === '-') {
        let last = stack[stack.length - 1]
        if (last === 0 && 1 / last < 0) {
          // 判断上一个数字是不是 -0,解决类似:2--7这种问题
          stack.push(curNum)
        } else {
          stack.push(-1 * curNum)
        }
      } else if (['+', '-'].includes(s[i]) && !curNum) {
        // 对数字的正负号做特殊处理 比如:2*-2
        if (['+', '-'].includes(curNum)) return 0 // 2*--2 有两个减号,直接返回 0
        curNum = s[i]
        continue
      } else if (preSign === '*') {
        stack.push(calc(stack.pop(), preSign, curNum)) // calc方法是用于解决js计算精度的方法,可参考我的另外一篇文章
      } else if (preSign === '/') {
        stack.push(calc(stack.pop(), preSign, curNum))
      } else {
        return 0 // 既不是数字也不是符号也不是点,直接返回0
      }
      curNum = 0 // 运算后将curNum置为0
      preSign = s[i] // 记录当前符号
    }
  }
  let sum = stack.reduce((pre, num) => calc(pre, '+', num), 0) // 计算栈的和
  if (isNaN(sum) || !isFinite(sum)) sum = 0 // 如果结果是NaN,或者无穷,就置为0
  return sum
}

export default parseExpression

// test
console.log(parseExpression('3.1 - 2 + 3 * 4 - 4 / 2'))   // 11.1

科学计算器

完成了解析表达式的步骤,就可以做一个简易科学计算器

在写代码的时候,产品增加了两个要求:

  1. 需要根据鼠标点击的位置不同,在不同位置显示计算器
  2. 要与键盘上的数字与运算键绑定,用键盘也能使用计算器
    (两个要求不难办,就不细讲,可以直接看代码)

思路:

  1. 页面上需要显示两行,一行是表达式,一行显示当前数字。

    • data中用两个数据表示
  2. 每次按下运算符号后,当前的数字都要重置

    • data中使用一个值来记录是否需要重置当前数字

处理异常:

  1. 连续点击运算符号,需要覆盖上一次的运算符号
  2. 一个数字中不能出现多个小数点
  3. 按下等号后继续输入,需要将之前的数据清空

实现后效果:

实现代码:

<template>

  <div>

    <!-- 遮罩层,点击遮罩层隐藏计算器 -->

    <div class="mask" @click="$emit('hideCalc')"></div>

    <div :style="{ top: top + 'px', left: left + 'px' }" class="calculator">

      <div class="showPanel">

        <span class="exp">{{ exp }}</span>

        <span class="number">{{ number }}</span>

      </div>

      <div class="caculator-button">

        <el-button @click="getResult('c')">c</el-button>

        <el-button @click="getResult('/')">/</el-button>

        <el-button @click="getResult('*')">*</el-button>

        <el-button @click="getResult('del')" icon="delete"> </el-button>



        <el-button @click="getResult('7')">7</el-button>

        <el-button @click="getResult('8')">8</el-button>

        <el-button @click="getResult('9')">9</el-button>

        <el-button @click="getResult('-')">-</el-button>



        <el-button @click="getResult('4')">4</el-button>

        <el-button @click="getResult('5')">5</el-button>

        <el-button @click="getResult('6')">6</el-button>

        <el-button @click="getResult('+')">+</el-button>

        <el-button @click="getResult('1')">1</el-button>
        <el-button @click="getResult('2')">2</el-button>
        <el-button @click="getResult('3')">3</el-button>
        <el-button @click="getResult('=')" type="primary" class="equal">=</el-button>

        <el-button @click="getResult('+/-')">+/-</el-button>
        <el-button @click="getResult('0')">0</el-button>
        <el-button @click="getResult('.')">.</el-button>
      </div>
    </div>
  </div>
</template>

<script>
import parseExpression from '@/utils/parseExpression'
export default {
  name: 'calculator',
  props: {
    calcTop: {
      // calcTop,calcLeft计算器的位置
      type: Number,
      default: 100
    },
    calcLeft: {
      type: Number,
      default: 100
    }
  },
  data() {
    return {
      number: '0', // 当前显示在输入框的数值,这里要注意number时刻是个字符串格式,否则indexOf方法会报错
      exp: '', // 用于计算和显示的表达式
      rewrite: false, // 是否要清空输入框重写,比如按下5后,再按+,下次再按一个数字是,就需要清空输入框重写
      isInit: false // 是否需要初始化,比如按下=号后,继续按键
    }

  },

  computed: {

    top() {

      // 计算器定位,距离边距的处理

      if (document.documentElement.clientHeight - this.calcTop < 270) {

        return this.calcTop - 300

      } else {

        return this.calcTop

      }

    },

    left() {

      // 计算器定位

      return this.calcLeft < 0 ? 30 : this.calcLeft

    },

    // 键盘按键与事件之间的对应关系

    keyMap() {

      return new Map([

        ['0', this.getResult.bind(this, '0')],

        ['1', this.getResult.bind(this, '1')],

        ['2', this.getResult.bind(this, '2')],

        ['3', this.getResult.bind(this, '3')],

        ['4', this.getResult.bind(this, '4')],

        ['5', this.getResult.bind(this, '5')],

        ['6', this.getResult.bind(this, '6')],

        ['7', this.getResult.bind(this, '7')],

        ['8', this.getResult.bind(this, '8')],

        ['9', this.getResult.bind(this, '9')],

        ['Backspace', this.getResult.bind(this, 'del')],

        ['/', this.getResult.bind(this, '/')],

        ['*', this.getResult.bind(this, '*')],

        ['+', this.getResult.bind(this, '+')],

        ['-', this.getResult.bind(this, '-')],

        ['.', this.getResult.bind(this, '.')],

        ['c', this.getResult.bind(this, 'c')],

        ['Enter', this.getResult.bind(this, '=')]

      ])

    }

  },

  mounted() {

    document.addEventListener('keyup', this.keyEvent)

  },

  destroyed() {

    document.removeEventListener('keyup', this.keyEvent)

  },

  methods: {

    // 按下按键
    getResult(e) {
      // 如果之前按下了等号,重置数据
      if (this.isInit) {
        this.isInit = false
        Object.assign(this.$data, this.$options.data())
      }
      // 不可以连续点击多个小数点
      if (this.number.indexOf('.') != -1 && e === '.') return
      if (/[0-9]|./.test(e)) {
        // 如果是小数点或者数字,就给this.number赋值
        if (this.rewrite) {
          this.number = e
          this.rewrite = false
        } else {
          if (this.number === '0' && e !== '.') {
            this.number = e // 避免输入多个0的情况,00005
          } else {
            // 可以输入:0.5
            this.number += e
          }
        }
      } else if (['+', '-', '*', '/'].includes(e)) {
        // 上一次按下的也是运算符号,则需要覆盖最后一个运算符号
        let last = this.exp.charAt(this.exp.length - 1)
        if (this.rewrite && ['+', '-', '*', '/'].includes(last)) {
          this.exp = this.exp.slice(0, this.exp.length - 1) + e
        } else {
          this.rewrite = true  // 下次输入数字时需要重置this.number
          this.exp += this.number + e
        }
      } else if (e === 'del') {
        if (this.number === '0') return
        if (this.rewrite) return // 上一次点的是运算符号,不可以删除
        this.number = this.number.slice(0, this.number.length - 1)
        if (this.number === '') this.number = '0'
      } else if (e === '+/-') {
        // 取反
        this.number = (-1 * this.number).toString()
      } else if (e === '=') {
        this.exp += this.number
        // 算出结果
        this.number = parseExpression(this.exp).toString()
        this.exp += e
        this.isInit = true
        // 相父组件传出结果
        this.$emit('getCalcResult', this.number)
      } else if (e === 'c') {
        Object.assign(this.$data, this.$options.data())
      }
    },
    /**
     * @description  监听键盘事件
     */
    keyEvent(e) {
      this.keyMap.get(e.key) && this.keyMap.get(e.key)()
    }
  }
}
</script>

<style lang="scss" scoped>

.mask {

  position: absolute;

  z-index: 80;

  top: 0;

  left: 0;

  width: 100vw;

  height: 100vh;

}

.calculator {

  position: fixed;

  z-index: 99;

  border: solid 1px #dcdfe6;

  padding: 5px;

  background-color: #fffffff1;

  border-radius: 4px;

  box-shadow: 0 0 2px #dcdfe6;



  .showPanel {

    display: flex;

    flex-direction: column;

    align-items: flex-end;

    padding: 2px 20px;

    height: 42px;

    border: 1px #f0f0f0 solid;

    width: 203px;

    border-radius: 4px;

    box-sizing: border-box;

    margin-bottom: 3px;

    justify-content: space-evenly;

    .exp {

      color: #aaa;

      font-size: 10px;

      height: 12px;

    }

    .number {

      font-size: 16px;

      font-weight: 900;

    }

  }

  // 删除的icon图标

  ::v-deep.delete {

    display: inline-block;

    width: 20px;

    height: 12px;

    background: url('../../icon/calcDetele.png') no-repeat center center;

    background-size: 90% 90%;

  }

}

.el-button {

  margin: 0 !important;

  padding: 10px;

  font-weight: 600;

  width: 100%;

}

.caculator-button {

  margin: 0 auto;

  width: 190px;

  display: grid;

  border: solid 1px #eee;

  padding: 6px;

  grid-template-columns: 1fr 1fr 1fr 1fr;

  gap: 3px;

  border-radius: 4px;

  background-color: #fffffff1;

}

#result {

  margin-bottom: 6px;

}

.equal {

  grid-column: 4/5;

  grid-row: 4/6;

}

</style>

  • 产品看后,沉思了片刻,说:和我电脑上的计算器不一样啊,点运算符号都会把上一次的结果算出来,是这种:

  • 我:你这是标准计算器,你点一点左上角,选择科学,就一样了

  • 产品:我就要这种标准计算器

  • 我:????

标准计算器

  • 既然产品都发话了,那就做吧,把科学计算器改一改,就是一个标准计算器了
  • 重点就是改一改按下字符如果是运算符号时,先将之前按下的数据算出来

实现后的效果:

实现代码:

<template>

  <div>

    <!-- 遮罩层,点击遮罩层隐藏计算器 -->

    <div class="mask" @click="$emit('hideCalc')"></div>

    <div :style="{ top: top + 'px', left: left + 'px' }" class="calculator">

      <div class="showPanel">

        <span class="exp">{{ exp }}</span>

        <span class="number">{{ number }}</span>

      </div>

      <div class="caculator-button">

        <el-button @click="getResult('c')">c</el-button>

        <el-button @click="getResult('/')">/</el-button>

        <el-button @click="getResult('*')">*</el-button>

        <el-button @click="getResult('del')" icon="delete"> </el-button>



        <el-button @click="getResult('7')">7</el-button>

        <el-button @click="getResult('8')">8</el-button>

        <el-button @click="getResult('9')">9</el-button>

        <el-button @click="getResult('-')">-</el-button>



        <el-button @click="getResult('4')">4</el-button>

        <el-button @click="getResult('5')">5</el-button>

        <el-button @click="getResult('6')">6</el-button>


        <el-button @click="getResult('+')">+</el-button>

        <el-button @click="getResult('1')">1</el-button>
        <el-button @click="getResult('2')">2</el-button>
        <el-button @click="getResult('3')">3</el-button>
        <el-button @click="getResult('=')" type="primary" class="equal">=</el-button>

        <el-button @click="getResult('+/-')">+/-</el-button>
        <el-button @click="getResult('0')">0</el-button>
        <el-button @click="getResult('.')">.</el-button>
      </div>
    </div>
  </div>
</template>

<script>
import parseExpression from '@/utils/parseExpression'
export default {
  name: 'Calculator',
  props: {
    calcTop: {
      // calcTop,calcLeft计算器的位置
      type: Number,
      default: 100
    },
    calcLeft: {
      type: Number,
      default: 100
    }
  },
  data() {
    return {
      number: '0', // 当前显示在输入框的数值,这里要注意number时刻是个字符串格式,否则indexOf方法会报错
      exp: '', // 用于计算和显示的表达式
      rewrite: false // 是否要清空输入框重写,比如按下5后,再按+,下次再按一个数字是,就需要清空输入框重写
    }

  },

  computed: {

    top() {

      // 计算器定位,距离边距的处理

      if (document.documentElement.clientHeight - this.calcTop < 270) {

        return this.calcTop - 300

      } else {

        return this.calcTop

      }

    },

    left() {

      // 计算器定位

      return this.calcLeft < 0 ? 30 : this.calcLeft

    },

    // 键盘按键与事件之间的对应关系

    keyMap() {

      return new Map([

        ['0', this.getResult.bind(this, '0')],

        ['1', this.getResult.bind(this, '1')],

        ['2', this.getResult.bind(this, '2')],

        ['3', this.getResult.bind(this, '3')],

        ['4', this.getResult.bind(this, '4')],

        ['5', this.getResult.bind(this, '5')],

        ['6', this.getResult.bind(this, '6')],

        ['7', this.getResult.bind(this, '7')],

        ['8', this.getResult.bind(this, '8')],

        ['9', this.getResult.bind(this, '9')],

        ['Backspace', this.getResult.bind(this, 'del')],

        ['/', this.getResult.bind(this, '/')],

        ['*', this.getResult.bind(this, '*')],

        ['+', this.getResult.bind(this, '+')],

        ['-', this.getResult.bind(this, '-')],

        ['.', this.getResult.bind(this, '.')],

        ['c', this.getResult.bind(this, 'c')],

        ['Enter', this.getResult.bind(this, '=')]

      ])

    }

  },

  mounted() {

    document.addEventListener('keyup', this.keyEvent)

  },

  destroyed() {

    document.removeEventListener('keyup', this.keyEvent)

  },

  methods: {

    getResult(e) {
      // 如果之前按下了等号
      if (this.isInit) {
        this.isInit = false
        Object.assign(this.$data, this.$options.data())
      }
      // 不可以连续点击多个小数点
      if (this.number.indexOf('.') != -1 && e === '.') return
      if (/[0-9]|./.test(e)) {
        // 如果是小数点或者数字,更改this.number的值
        if (this.rewrite) {
          this.number = e
          this.rewrite = false
        } else {
          if (this.number === '0' && e !== '.') {
            this.number = e
          } else {
            // 可以输入:0.5
            this.number += e
          }
        }
      } else if (['+', '-', '*', '/'].includes(e)) {
        // 上一次点击的按键也是运算符号,则需要覆盖最后一个运算符号
        let last = this.exp.charAt(this.exp.length - 1)
        if (this.rewrite && ['+', '-', '*', '/'].includes(last)) {
          this.exp = this.exp.slice(0, this.exp.length - 1) + e
        } else {
          this.rewrite = true   // 下次输入数字时需要重置this.number
          this.exp += this.number
          // 每次按下运算符都需要将上次的结果算出来给输入框
          this.number = parseExpression(this.exp).toString()
          this.exp = this.number + e
        }
      } else if (e === 'del') {
        if (this.number === '0') return
        if (this.rewrite) return // 刚刚点完符号,不可以删除
        this.number = this.number.slice(0, this.number.length - 1)
        if (this.number === '') this.number = '0'
      } else if (e === '+/-') {
        // 取反,并给表达式最后一项乘-1,注:-1需要在最后一项前面乘
        this.number = (-1 * this.number).toString()
      } else if (e === '=') {
        this.exp += this.number
        // 算出结果
        this.number = parseExpression(this.exp).toString()
        this.exp += e
        this.isInit = true // 下次点击按键时,重置数据
        this.$emit('getCalcResult', this.number)
      } else if (e === 'c') {
        Object.assign(this.$data, this.$options.data())
      }
    },
    /**
     * @description  监听键盘事件
     */
    keyEvent(e) {
      this.keyMap.get(e.key) && this.keyMap.get(e.key)()
    }
  }
}
</script>
<style lang="scss" scoped>

.mask {

  position: absolute;

  z-index: 80;

  top: 0;

  left: 0;

  width: 100vw;

  height: 100vh;

}

.calculator {

  position: fixed;

  z-index: 99;

  border: solid 1px #dcdfe6;

  padding: 5px;

  background-color: #fffffff1;

  border-radius: 4px;

  box-shadow: 0 0 2px #dcdfe6;



  .showPanel {

    display: flex;

    flex-direction: column;

    align-items: flex-end;

    padding: 2px 20px;

    height: 42px;

    border: 1px #f0f0f0 solid;

    width: 203px;

    border-radius: 4px;

    box-sizing: border-box;

    margin-bottom: 3px;

    justify-content: space-evenly;

    .exp {

      color: #aaa;

      font-size: 10px;

      height: 12px;

    }

    .number {

      font-size: 16px;

      font-weight: 900;

    }

  }

  // 删除的icon图标

  ::v-deep.delete {

    display: inline-block;

    width: 20px;

    height: 12px;

    background: url('../../icon/calcDetele.png') no-repeat center center;

    background-size: 90% 90%;

  }

}

.el-button {

  margin: 0 !important;

  padding: 10px;

  font-weight: 600;

  width: 100%;

}

.caculator-button {

  margin: 0 auto;

  width: 190px;

  display: grid;

  border: solid 1px #eee;

  padding: 6px;

  grid-template-columns: 1fr 1fr 1fr 1fr;

  gap: 3px;

  border-radius: 4px;

  background-color: #fffffff1;

}

#result {

  margin-bottom: 6px;

}

.equal {

  grid-column: 4/5;

  grid-row: 4/6;

}

</style>

这下产品没话说了吧?

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYVPk4tU' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片