前言
最近产品经理又又加了一个需求,想在开单页面加一个计算器,用户就可以在录单时可以使用计算器了。需求内容就一句话:支持加减乘除四则运算,点击计算器图标,当前页面弹出计算器弹窗。我一问,答案就是:你参考别人家的计算器做就行。
我的思路:记录用户每次按下的按键,然后拼接成一个运算表达式,然后再解析运算表达式不久okk了!!
解析运算表达式
按照产品的要求,解析运算表达式需要有这些功能:
- 如何收集数字
- 声明一个栈(数组),用于存储数字
- 遍历字符串,如果是点或数字,就拼接起来,如果是运算符号,就将数字推入到栈中
- 解决运算顺序,先乘除后加减
- 遍历字符串时,如果预存符号是加,就直接将数字推入栈,如果是减,就乘 -1再推入到栈,如果是乘除,就先计算栈顶元素与当前数字的结果,再将结果推入到栈顶
- 最后将栈中的数字相加,就是运算结果了
- 遍历到符号的时候,怎么将后面的数字收集起来推入到栈中
- 先预先存一个+的符号,不判断当前的符号,而是判断预存的符号,这样就可以将第一个数字推入栈中
- 然后再将预存符号赋值为当前的符号。
-
举例:
假设有运算表达式:3.1 - 2 + 3 * 4 - 4 / 2
- 先乘除后加减,加入到栈中,栈:[ 3.1, -2, 12, -2 ]
- 再将栈中的元素相加:3.1 - 2 + 12 - 2 = 11.1
实现代码:
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*-2if (['+', '-'].includes(curNum)) return 0 // 2*--2 有两个减号,直接返回 0curNum = 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置为0preSign = s[i] // 记录当前符号}}let sum = stack.reduce((pre, num) => calc(pre, '+', num), 0) // 计算栈的和if (isNaN(sum) || !isFinite(sum)) sum = 0 // 如果结果是NaN,或者无穷,就置为0return sum}// 测试console.log(parseExpression('3.1 - 2 + 3 * 4 - 4 / 2')) // 11.1
科学计算器
完成了解析表达式的步骤,就可以做一个简易科学计算器
在完成科学计算器的时候,产品增加了两个要求:
- 需要根据鼠标点击的位置不同,在不同位置显示计算器
- 要与键盘上的数字与运算键绑定,用键盘也能使用计算器
两个要求不难办,就不细讲,可以直接看代码
思路:
- 页面上需要显示两行,一行是表达式,一行显示当前数字。
- data中用两个数据表示
每次按下运算符号后,当前的数字都要重置
连续点击运算符号,需要将最后一次的运算符号覆盖
- 一个数字中不能出现多个小数点
-
实现后效果:
实现代码:
```html
{{ exp }} {{ number }}
> 产品看后,沉思了片刻,说:和我电脑上的计算器不一样啊,点运算符号都会把上一次的结果算出来,这种:> > 我:你这是标准计算器,你点一点左上角,选择科学,就一样了> 产品:我就要这种标准计算器> 我:????<a name="TBY0e"></a>## 标准计算器> 既然产品都发话了,那就做吧,把科学计算器改一改,就是一个标准计算器了> 重点就是改一改按下字符如果是运算符号时,先将之前按下的数据算出来<a name="t2QIr"></a>### 实现后的效果:<a name="XszBR"></a>### 实现代码:```html<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 = falseObject.assign(this.$data, this.$options.data())}// 不可以连续点击多个小数点if (this.number.indexOf('.') != -1 && e === '.') returnif (/[0-9]|\./.test(e)) {// 如果是小数点或者数字,更改this.number的值if (this.rewrite) {this.number = ethis.rewrite = false} else {if (this.number === '0' && e !== '.') {this.number = e} else {// 可以输入:0.5this.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.numberthis.exp += this.number// 每次按下运算符都需要将上次的结果算出来给输入框this.number = parseExpression(this.exp).toString()this.exp = this.number + e}} else if (e === 'del') {if (this.number === '0') returnif (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 += ethis.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>
这下产品经理没话说了吧?
