前言:该我的项目是仿照 ios 计算器 app 实现,局部交互细节未实现。主体逻辑是依据本人教训实现,有问题之处欢送指出。
Github 仓库地址
应用 uni-app
搭建我的项目后在 index.vue
编写 UI
动态页代码,同级创立 utils.js
文件,寄存一些常量和工具函数。
// index.vue
<template>
<view class="content">
<view class="view">
<span :class="{lessen: viewValue.length >= 7}">
{{viewValue}}
</span>
</view>
<!-- 按钮 -->
<view class="btns">
<view v-for="item in btns" :key='item.text' :style="item.style" :class="item.className" :data-text="item.text"></view>
</view>
</view>
</template>
<script>
import {KEYS, symbolReg1, symbolReg2, symbolReg3} from './utils';
export default {data() {
return {
// 渲染值
viewValue: 0,
btns: KEYS && KEYS.map(item => {let style = {}
let className = 'default'
// 雷同类型的增加 class
if (symbolReg3.test(item) || item === KEYS[KEYS.length - 1]) {className = 'origin'}
if (new RegExp(`${KEYS[0]}|(?=${KEYS[1]})|${KEYS[2]}`).test(item)) {className = 'gray'}
// 独自辨别款式
if (item === KEYS[KEYS.length - 3]) {style.width = 'calc(144rpx * 2 + 100% / 4 / 4)'
style.justifyContent = 'flex-start'
style.padding = '0 50rpx'
}
if (item === KEYS[1]) {style.fontSize = '50rpx'}
if (item === KEYS[0]) {style.fontSize = '50rpx'}
return {
text: item,
style,
className
}
})
}
}
}
</script>
<style scoped lang="scss">
.content {
display: flex;
flex-wrap: wrap;
align-content: flex-end;
background: #000000;
min-height: 100vh;
box-sizing: border-box;
padding: 30rpx;
font-family: PingFangSC-Regular, sans-serif;
> view {
width: 100%;
color: #ffffff;
}
.view {
text-align: right;
font-size: 168rpx;
line-height: 100px;
> .lessen {font-size: 138rpx;}
> span {
display: inline-block;
box-sizing: border-box;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
overflow: hidden;
border-radius: 20rpx;
&::selection {
background-color: #333;
color: #fff;
}
}
}
.btns {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
height: 870rpx;
> view {
width: 144rpx;
height: 144rpx;
border-radius: 144rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
color: #ffffff;
transition: all .3s;
background: #333333;
&.origin {
background: #fea00c;
&:active {
background: #ffffff;
color: #fea00c;
}
}
&.gray {
background: #a5a5a5;
color: #000000;
&:active {background: #eeeeee;}
}
&.default {
&:active {background: #bbbbbb;}
}
&::after {content: attr(data-text);
}
}
}
}
</style>
// utils.js
export const KEYS = ['AC', '+/-', '%', '÷', '7', '8', '9', '×', '4', '5', '6', '-', '1', '2', '3', '+', '0', '.', '=']
// 乘除正则
export const symbolReg1 = new RegExp(`^[${KEYS[4 - 1]}${KEYS[2 * 4 - 1]}]$`)
// 加减正则
export const symbolReg2 = new RegExp(`^[\\${KEYS[3 * 4 - 1]}${KEYS[4 * 4 - 1]}]$`)
// 加减乘除正则
export const symbolReg3 = new RegExp(`^[${KEYS[4 - 1]}${KEYS[2 * 4 - 1]}\\${KEYS[3 * 4 - 1]}${KEYS[4 * 4 - 1]}]$`)
到这一步页面成果曾经进去了然而还没有点击事件。这里正则中是应用动静正则形式,防止 KEYS
常量中数据半角全角符号扭转而无奈匹配。
这里应用事件委托形式,给页面 class
为 btns
的元素增加点击事件 <view class="btns" @click="handleClick">
,而不是在 v-for 循环元素上增加事件。在 methods
属性中增加 handleClick
函数。
handleClick({target}) {
// 判断是否点击元素
if (target.dataset.text == undefined) return
switch (target.dataset.text) {
// AC 点击
case this.btns[0].text:
break;
// 加减乘除点击
case KEYS[4 - 1]:
case KEYS[2 * 4 - 1]:
case KEYS[3 * 4 - 1]:
case KEYS[4 * 4 - 1]:
break;
// 等于
case KEYS[KEYS.length - 1]:
break;
// 取反
case KEYS[1]:
break;
// 百分号
case KEYS[2]:
break;
// 数字
default:
}
},
编写好主体逻辑之后一步步欠缺,先补充辅助变量和数字点击逻辑
import {KEYS, symbolReg1, symbolReg2, symbolReg3} from './utils';
// 计算逻辑
let computes = []
// 判断最初一个是否为计算符号
const isSymbol = () => {return symbolReg3.test(computes[computes.length - 1])
}
//...
default:
// `viewValue` 的长度在 `ios` 中只有 `9` 位数,存在 `.` 的时候有 `10` 位数,所以在输出数字时也须要加判断。判断是否存在小数点
if (this.viewValue.length > (String(this.viewValue).indexOf('.') != -1 ? 9 : 8)) {return}
// 判断是否最初一个是否是字符
if (isSymbol()) {
this.viewValue = target.dataset.text
computes.push(this.viewValue)
} else {
// 判断是否存在 .
if (String(this.viewValue).indexOf('.') != -1 && target.dataset.text === '.') {return}
// 三元判断是否为点, 防止 Number(.) 会为 NaN
this.viewValue += this.viewValue === 0
? target.dataset.text !== '.' ? Number(target.dataset.text) : target.dataset.text
: target.dataset.text
// 替换栈最初一个数字
computes[computes.length > 0 ? computes.length - 1 : 0] = this.viewValue
}
// 批改页面 AC 文字
this.btns[0].text = computes.length > 0 || this.viewValue > 0 ? 'C' : KEYS[0]
//...
补充取反和百分号逻辑
// 取反
case KEYS[1]:
// 兼容 - 0 的状况
if (this.viewValue === '-0') {this.viewValue = 0} else {this.viewValue = this.viewValue >= 0 ? '-' + this.viewValue : Math.abs(this.viewValue)
}
// 这里用 if 判断的话就不须要走 lastIndexOf 循环查找了
if (computes.length == 1) {computes[0] = this.viewValue
} else {const lastIndex = computes.lastIndexOf(this.viewValue)
computes.splice(lastIndex, 1, this.viewValue)
}
break;
// 百分号
case KEYS[2]:
this.viewValue = this.viewValue * 0.01
if (computes.length == 1) {computes[0] = this.viewValue
} else {const lastIndex = computes.lastIndexOf(this.viewValue)
computes.splice(lastIndex, 1, this.viewValue)
}
break;
加减乘除符号点击,在 import
下退出辅助变量 countSymbol1、countSymbol2
import {KEYS, symbolReg1, symbolReg2, symbolReg3} 'utils.js'
// ...
// 乘除运算符次数
let countSymbol1 = 0
// 加减
let countSymbol2 = 0
// ...
// 加减乘除点击
case KEYS[4 - 1]:
case KEYS[2 * 4 - 1]:
case KEYS[3 * 4 - 1]:
case KEYS[4 * 4 - 1]:
// 不能一开始就是符号
if (computes.length == 0) return
if (isSymbol()) {
// 扭转符号
computes[computes.length - 1] = target.dataset.text
} else {computes.push(target.dataset.text)
// 退出统计字符数量
if (symbolReg1.test(target.dataset.text)) countSymbol1++
if (symbolReg2.test(target.dataset.text)) countSymbol2++
}
break;
等于符号点击,增加计算逻辑函数 numFun
,增加辅助变量 cacheLastSymbol
,utils.js
文件增加运算函数 operation
,在 index.vue
文件中导入 operation
。
在 ios
计算器中,当如输出 3 + 3
点击屡次 =
时,会在 第一次等于运算的后果 上始终 + 3
,而当输出 3 + 3 +
时会将 3 + 3
的值进行计算出来再进行最初一个加,也就是变成了 6 +
,当最初一个是运算符时会始终对运算符前一个数字进行运算,后果会变成 12
。
再举一个例子,2 + 5 + 5 +
后果是 27
,因为它会先将 2 + 5 + 5
的后果先进行计算,再将的到的后果相加 也就变成了 12 +
。2 + 5 * 5 *
后果是 627
,它是对 *
先进行计算也就是 2 + 25 *
,25 * 25 = 625
再 + 2
。
cacheLastSymbol
的作用为存储最初一个字符和最初一个数字,例如:3 + 5 +
会存储 8
和 +
,2 + 5 * 5 *
则会存储 25
和 *
,3 + 5
则会贮存 5
和 +
。当你点击第二次 =
的时候会拿到第一次的值在拿到 cacheLastSymbol
中贮存的值和运算符进行运算。
// utils.js
/// ...
// 计算结果
export const operation = (num1, num2, symbol) => {
let num = undefined
// 这里能够应用 eval 将字符串当做 js 执行, 如果是 × 须要去判断改为 *
switch (symbol) {case KEYS[4 - 1]:
num = num1 / num2
break;
case KEYS[2 * 4 - 1]:
num = num1 * num2
break;
case KEYS[3 * 4 - 1]:
num = num1 - num2
break;
case KEYS[4 * 4 - 1]:
num = Number(num1) + Number(num2)
break;
}
// 为 8 次要是 ios 最大为 8 为小数
return String(parseFloat(Number(num).toPrecision(8)))
}
// index.vue
import {KEYS, symbolReg1, symbolReg2, symbolReg3, operation} from './utils';
// ...
// 存储最初操作符 0 为操作符,1 为值
let cacheLastSymbol = []
/**
* 计算逻辑
* @param {string} type 0 加减 1 乘除
* @param {number} count 次数
* @returns string
* */
const numFun = (type, count) => {
let num = undefined
for(let i = 1; i <= count; i++) {const index = computes.findIndex(symbol => type === '0' ? symbolReg2.test(symbol) : symbolReg1.test(symbol))
// 这里做容错判断 防止不存在运算符号时进行运算
if (index == -1) return
// 进行计算
// 这里能够应用 eval 将字符串当做 js 执行, 须要去判断 *
if (index === computes.length - 1) { // 如果最初一个是字符而非数字的状况
computes.splice(index - 1, 2, operation(computes[index - 1], computes[index - 1], computes[index]))
} else {computes.splice(index - 1, 3, operation(computes[index - 1], computes[index + 1], computes[index]))
}
/**
* 最初一个是操作符时增加栈中计算的值
* 假如 2 + 5 * 5 * 4 *
* 第一次进入时存储 5 * 5 的值,此时 computes 栈中为 [2, '+', '25', '*', '4', '*']
* 第二次进入时存储 25 * 4 的值,此时 computes 栈中为 [2, '+', '100', '*']
*/
if (isSymbol()) {cacheLastSymbol[1] = computes[computes.length - 2]
}
}
}
// ...
// 等于
case KEYS[KEYS.length - 1]:
if (cacheLastSymbol.length == 2) { // 第二次点击会进入
computes[0] = operation(computes[0], cacheLastSymbol[1], cacheLastSymbol[0])
}
if (countSymbol1 || countSymbol2) { // 存在操作符时,将操作符退出缓存变量
if (isSymbol()) { // 判断最初一个是操作符则取操作符前一个
cacheLastSymbol[0] = computes[computes.length - 1]
cacheLastSymbol[1] = computes[computes.length - 2]
} else {cacheLastSymbol[0] = computes[computes.length - 2]
cacheLastSymbol[1] = computes[computes.length - 1]
}
}
if (countSymbol1) numFun('1', countSymbol1)
if (countSymbol2) numFun('0', countSymbol2)
// 革除操作符统计
countSymbol1 = 0
countSymbol2 = 0
if (computes.length == 1) {this.viewValue = computes[0]
}
break;
再将渲染数据格式化,methods
增加 formatt
函数,因为咱们只在页面写了一个 formatt
,所以在 methods
写也能够,不会存在性能问题,当页面中写了多个 formatt(viewValue)
时倡议改成 computed
计算属性模式,利用计算属性的缓存机制优化性能。
<span :class="{lessen: viewValue.length >= 7}">
{{formatt(viewValue) }}
</span>
// ...
methods: {
// ...
formatt(val) {if (/\.$/.test(val)) {return val} else {
// 最多保留 8 位小数
return Number(val).toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 8
})
}
}
}
到这一步根本实现了整体性能。
然而有些细节还是须要欠缺,2 + 5 * 5 *
输出实现时 viewValue
的值应该为 25
,当 *
改为 +
时 2 + 5 * 5 +
值应该是 27
增加辅助变量 editStatus
,在 utils.js
文件中增加 computeCount
工具函数,加减乘除逻辑中增加代码
// utils.js
// 计算总和值,如果最初一个是字符时不进行计算
export const computeCount = (computes) => {
// 增加缓存变量
let cacheIndex = undefined
let index = undefined
for (let i in computes) {
// 如果以后是乘除则跳出循环,并且给 index 赋值下标
if (symbolReg1.test(computes[i])) {
index = i
break
} else if (!cacheIndex && symbolReg2.test(computes[i])) {cacheIndex = i}
}
// 如果 computes 中没有乘除符号时,index 会为空,此时将 缓存下的下标赋值给 index
index = Number(index || cacheIndex)
// 替换数组内容
computes.splice(index - 1, 3, operation(computes[index - 1], computes[index + 1], computes[index]))
// 长度大于 2 时阐明还须要进行递归计算
if (computes.length > 2) {return computeCount(computes)
} else {return computes[0]
}
}
// index.vue
import {KEYS, symbolReg1, symbolReg2, symbolReg3, operation, computeCount} from './utils';
// 批改状态 0 不须要操作栈,1 删除栈中倒数第二个符号本身、上一个以及下一个下标,2 删除倒数第一个符号之前的栈,应用 viewValue 代替
let editStatus = 0
// ...
// 加减乘除点击
case KEYS[4 - 1]:
case KEYS[2 * 4 - 1]:
case KEYS[3 * 4 - 1]:
case KEYS[4 * 4 - 1]:
// ...
if (computes.length > 3) {
const previousSymbolIndex = computes.length - 3
// 判断上一个符号是否是乘除 && 以后符号为加减
if (symbolReg1.test(computes[previousSymbolIndex]) && symbolReg2.test(target.dataset.text)) {
// 这里应用 ... 扩大运算符浅克隆 computes 数组,如果是援用类型则须要深克隆
this.viewValue = computeCount([...computes])
// console.log('上个乘除以后符号加减', this.viewValue);
editStatus = 2
}
// 上个符号和以后符号 一样的类型
if ((symbolReg1.test(computes[previousSymbolIndex]) && symbolReg1.test(target.dataset.text)) ||
(symbolReg2.test(computes[previousSymbolIndex]) && symbolReg2.test(target.dataset.text))
) {// console.log('符号雷同');
this.viewValue = operation(computes[previousSymbolIndex - 1], computes[previousSymbolIndex + 1], computes[previousSymbolIndex])
editStatus = 1
}
if (!editStatus) {this.viewValue = computes[computes.length - 2]
editStatus = 0
}
}
break;
此时成果曾经实现了,点击等于也没什么问题,然而代码还需优化,因为在下面代码中咱们的值其实曾经计算出来了,当点击等于的时候,相当于又从新计算了一遍栈中的值,所以在点击数字的时候咱们须要革除栈中相应数据,之所以在点击数字时,是因为点击数字后,视图 viewValue
会进行扭转
default:
if (this.viewValue.length > (String(this.viewValue).indexOf('.') != -1 ? 9 : 8)) {return}
if (editStatus) {// 等于 1 时阐明上个符号和以后符号雷同,以后栈中数据为 [1, '+', '1', '+'] 格局时进入该 if
// 将栈中值替换为 ['2', '+']
if (editStatus === 1) {
const previousSymbolIndex = computes.length - 3
// 将后果替换栈中数据
computes.splice(previousSymbolIndex - 1, 3, operation(computes[previousSymbolIndex - 1], computes[previousSymbolIndex + 1], computes[previousSymbolIndex]))
}
// 等于 2 时阐明以后栈格局为 [1, '*', '1', '+'] 或者 [1, '+', '2', '*', '3', '+']
// 将栈中值替换为 ['7', '+']
if (editStatus === 2) computes.splice(0, computes.length - 1, this.viewValue)
editStatus = 0
}
if (isSymbol()) {
this.viewValue = target.dataset.text
computes.push(this.viewValue)
} else {// ...
到这一步计算逻辑都实现了,不懂的有什么提议或者意见能够在评论区回复或者去 github
上提 issue
。