前言:该我的项目是仿照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.jsexport 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 常量中数据半角全角符号扭转而无奈匹配。

这里应用事件委托形式,给页面 classbtns 的元素增加点击事件 <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,增加辅助变量 cacheLastSymbolutils.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.vueimport { 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.vueimport { 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