Vue.js 中的实用工具方法

收集日常开发中常用到的一些工具方法, 包含 vue 的公用过滤器、公用指令等 (PS: 懒人养成记)

公用自定义过滤器

import Vue from 'vue'
import moment from 'moment'

/**
 * @filter dateFormat 时间格式化
 * @param {String, Date} value 可被 new Date 解析的字符串
 * @param {String} formatStr moment 的 format 字符串
 * 使用方法 {{ 2019-1-1 | dateFormat() }}
 */
Vue.filter('dateFormat', (value, formatStr) => {
    return moment(value).format(formatStr || 'YYYY年MM月DD日 hh:mm:ss')
})

/**
 * @filter digitUppercase 人民币金额转成汉字大写
 * @param {Number} value 金额数字
 * 使用方法 {{ 1111 | digitUppercase }}
 */
Vue.filter('digitUppercase', (value) => {
    if (Number(value)) {
        let fraction = ['角', '分']
        let digit = [
            '零', '壹', '贰', '叁', '肆',
            '伍', '陆', '柒', '捌', '玖'
        ]
        let unit = [
            ['元', '万', '亿'],
            ['', '拾', '佰', '仟']
        ]

        let head = value < 0 ? '欠' : ''
        value = Math.abs(value)
        let s = ''
        for (let i = 0; i < fraction.length; i++) {
            s += (digit[Math.floor(value * 10 * Math.pow(10, i)) % 10] + fraction[i]).replace(/零./, '')
        }
        s = s || '整'
        value = Math.floor(value)
        for (let i = 0; i < unit[0].length && value > 0; i++) {
            let p = ''
            for (let j = 0; j < unit[1].length && value > 0; j++) {
                p = digit[value % 10] + unit[1][j] + p
                value = Math.floor(value / 10)
            }
            s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s
        }
        return head + s.replace(/(零.)*零元/, '元')
            .replace(/(零.)+/g, '零')
            .replace(/^整$/, '零元整')
    } else {
        return '零元整'
    }
})

公用自定义指令

import Vue from 'vue'

/**
 * @directive preventReClick 防止按钮在短时间内多次点击造成的多次请求(一般用于提交按钮)
 * @param {Element} el 绑定的元素
 * @param {Number} binding 绑定的时间
 * 使用方式 <el-button v-prevent-replace-click></el-button>
 */
Vue.directive('preventReplaceClick', {
    inserted(el, binding) {
        el.addEventListener('click', () => {
            if (!el.disabled) {
                el.classList.add('is-disabled')
                const i = document.createElement('i')
                i.classList.add('el-icon-loading')
                el.prepend(i)
                el.classList.add('is-loading')
                el.disabled = true

                setTimeout(() => {
                    el.disabled = false
                    el.classList.remove('is-disabled')
                    el.classList.remove('is-loading')
                    el.removeChild(i)
                }, binding.value || 1000)
            }
        })
    }
})

实用方法

节流和防抖

/**
 * 应用场景
 * debounce(抖动)
 * search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
 * window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
 *
 * throttle(节流)
 * 鼠标不断点击触发,mousedown(单位时间内只触发一次)
 * 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
 */

// 防抖
export function debounce(fn, delay = 200) {
    let timer
    return function() {
        let th = this
        let args = arguments
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(function() {
            timer = null
            fn.apply(th, args)
        }, delay)
    }
}
// 节流
export function throttle(fn, interval = 200) {
    let last
    let timer
    return function() {
        let th = this
        let args = arguments
        let now = +new Date()
        if (last && now - last < interval) {
            clearTimeout(timer)
            timer = setTimeout(function() {
                last = now
                fn.apply(th, args)
            }, interval)
        } else {
            last = now
            fn.apply(th, args)
        }
    }
}

时间格式化处理

// 时间格式化成 startDate 和 endDate
import moment from 'moment'
import _ from 'lodash'

/**
 * @method timerByAdd 计算相对当前时间后N个单位时间的日期(加法)
 * @param num {Number} 相对于几个时间点
 * @param timer {String} 时间单位 'days' 'months' 'years‘ 更多时间单位参考moment官方文档
 * @param formatStr {String} moment 的 format 字符串
 * @return {Object} {startDate,endDate}
 */
export function timerByAdd({
    num,
    timer = 'days'
} = {}, formatStr = 'YYYY-MM-DD') {
    let startDate = moment().format(formatStr)
    let endDate

    num ? endDate = moment().add(num, timer).format(formatStr) : startDate = endDate

    endDate = moment(endDate).subtract(1, 'days').format(formatStr)

    let result = {
        startDate,
        endDate
    }
    return result
}

/**
 * @method timerBySubtract 计算相对当前时间前N个单位时间的日期(减法)
 * @param num {Number} 相对于几个时间点
 * @param timer {String} 时间单位 'days' 'months' 'years‘ 更多时间单位参考moment官方文档
 * @param formatStr {String} moment 的 format 字符串
 * @return {Object} {startDate,endDate}
 */
export function timerBySubtract({
    num,
    timer = 'days'
} = {}, formatStr = 'YYYY-MM-DD') {
    let startDate
    let endDate = moment().format(formatStr)

    num ? startDate = moment().subtract(num, timer).format(formatStr) : startDate = endDate

    let result = {
        startDate,
        endDate
    }
    return result
}

/**
 * @method timerFormat 将对象时间转成数组形式
 * @param {Object} timer {startDate, endDate}
 */
export function timerFormat(timer) {
    if (_.isObject(timer)) {
        return _.values(timer)
    }
}

/**
 * @method timerByDiff 计算两个时间段的相差天数,精确到 N 年 N 天 精确到小数后两位
 * @param {Date} startDate 开始时间
 * @param {Date} endDate 结束时间
 */
export function timerByDiff(startDate, endDate) {
    const start = moment(startDate)
    const end = moment(endDate)
    const diffDays = end.diff(start, 'days', true).toFixed(2)
    const diffYears = Math.floor(diffDays / 365)

    let str = ''
    if (diffYears) {
        str = `${diffYears} 年 ${(diffDays - diffYears * 365) + 1} 天` 
    } else {
        str = `${Math.floor(diffDays) + 1} 天` 
    }
    return str
}

常量

HTTP 状态信息

export const SUCCESS = {
    status: 200,
    message: '成功'
}
export const UNAUTHORIZED = {
    sattus: 401,
    message: '无效或者过期的 token'
}
export const FORBIDDEN = {
    sattus: 403,
    message: '没有访问权限'
}
export const NOT_FOUNED = {
    sattus: 404,
    message: '页面不存在或者已经被移除了'
}
export const UNPROCESSABLE_ENTITY = {
    status: 422,
    message: '无效的表单数据或者未填写完整的表单'
}
export const INTERNAL_SERVER_ERROR = {
    status: 500,
    message: '服务器内部发生不可抗拒的错误'
}

兼容处理

当前使用的浏览器

const userAgent = navigator.userAgent // 取得浏览器的userAgent字符串
const compare = s => {
    return userAgent.indexOf(s) > -1
}
if (compare('Chrome')) {
    if (compare('Edge')) {
        return 'Edge'
    }
    return 'Chrome'
}
if (compare('Opera')) {
    return 'Opera'
}
if (compare('Safari')) {
    return 'Safari'
}
if (compare('Firefox')) {
    return 'Firefox'
}
if (compare('compatible') || compare('MSIE') || compare('Trident')) {
    return 'IE'
}

prepend

MDN ParentNode.prepend

(function(arr) {
    arr.forEach(function(item) {
        if (item.hasOwnProperty('prepend')) {
            return
        }
        Object.defineProperty(item, 'prepend', {
            configurable: true,
            enumerable: true,
            writable: true,
            value: function prepend() {
                var argArr = Array.prototype.slice.call(arguments)
                var docFrag = document.createDocumentFragment()

                argArr.forEach(function(argItem) {
                    var isNode = argItem instanceof Node
                    docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)))
                })

                this.insertBefore(docFrag, this.firstChild)
            }
        })
    })
})([Element.prototype, Document.prototype, DocumentFragment.prototype])

添加至购物车效果

参考传送门: 利用animation和transform制作加入购物车动画

const ballFly = function(opt) {
    return (function() {
        const Parabola = function(opt) {
            this.init(opt)
        }

        Parabola.prototype = {
            init: function(opt) {
                // console.log(opt)
                let flyO = this.calculatedValue(opt)
                let flyDom = this.creatHtml(flyO.site, flyO.callback)
                let flyRule = this.creatRule(flyO.coord)

                document.getElementsByTagName('head')[0].appendChild(flyRule)
                document.body.appendChild(flyDom)
            },
            creatRule: function(coord) {
                var cssAnimation = document.createElement('style')
                cssAnimation.type = 'text/css'
                var rules = `
          .parabola-animation {
            width: 100%;
          }

          .parabola-box-hor {
            position: fixed;
            z-index: 99;
            top: 0;
            left: 0;
            -webkit-animation: parabola-hor-animation 1s ease-out 1;
            animation: parabola-hor-animation 1s ease-out 1;
          }

          .parabola-box-hor.top {
            -webkit-animation-timing-function: ease-in;
            animation-timing-function: ease-in;
          }

          .parabola-box-ver {
            position: fixed;
            top: 50px;
            left: 20px;
            overflow: hidden;
            // width: 35px;
            // height: 35px;
            // background-color: red;
            -webkit-animation: parabola-ver-animation 1s ease-in 1;
            animation: parabola-ver-animation 1s ease-in 1;
            border-radius: 50%;
          }

          .parabola-box-ver.top {
            -webkit-animation-timing-function: ease-out;
            animation-timing-function: ease-out;
          }

          @-webkit-keyframes parabola-hor-animation{
              0%{
                  -webkit-transform: translate(0px, 0px);
                          transform: translate(0px, 0px);
              }
              10%{
                  -webkit-transform: translate(0px, 0px);
                          transform: translate(0px, 0px);
              }
              100%{
                  -webkit-transform: translate(${coord.x}px, 0px);
                          transform: translate(${coord.x}px, 0px);
              }
          }
          @keyframes parabola-hor-animation{
              0%{
                  -webkit-transform: translate(0px, 0px);
                          transform: translate(0px, 0px);
              }
              10%{
                  -webkit-transform: translate(0px, 0px);
                          transform: translate(0px, 0px);
              }
              100%{
                  -webkit-transform: translate(${coord.x}px, 0px);
                          transform: translate(${coord.x}px, 0px);
              }
          }
            @-webkit-keyframes parabola-ver-animation{
                0%{
                    -webkit-transform: translate(0px, 0px);
                            transform: translate(0px, 0px);
                }
                10%{
                    -webkit-transform: translate(0px, ${coord.os}px);
                            transform: translate(0px, ${coord.os}px);
                }
                100%{
                    -webkit-transform: translate(0px, ${coord.y}px);
                            transform: translate(0px, ${coord.y}px);
                }
            }
            @keyframes parabola-ver-animation{
                0%{
                    -webkit-transform: translate(0px, 0px);
                            transform: translate(0px, 0px);
                }
                10%{
                    -webkit-transform: translate(0px, ${coord.os}px);
                            transform: translate(0px, ${coord.os}px);
                }
                100%{
                    -webkit-transform: translate(0px, ${coord.y}px);
                            transform: translate(0px, ${coord.y}px);
                }
            }
        `
                cssAnimation.innerHTML = rules
                return cssAnimation
            },
            creatHtml: function(site, callback) {
                let html = `
          <div class="parabola-box-hor">
            <div class="parabola-box-ver">
            </div>
          </div>
        `
                let parentBox = document.createElement('div')
                parentBox.innerHTML = html
                parentBox.setAttribute('class', 'parabola-animation')

                const event = ['webkitAnimationEnd', 'mozAnimationEnd', 'MSAnimationEnd', 'oanimationend', 'animationend']
                event.forEach(item => {
                    parentBox.addEventListener(item, function() {
                        parentBox.remove()
                        callback()
                    })
                })

                let frag = document.createDocumentFragment()
                frag.appendChild(parentBox)

                let verBox = frag.querySelector('.parabola-box-ver')
                let horBpx = frag.querySelector('.parabola-box-hor')
                verBox.style.left = site.left + 'px'
                verBox.style.top = site.top + 'px'
                verBox.style.width = opt.size + 'px'
                verBox.style.height = opt.size + 'px'
                verBox.style.backgroundColor = opt.color

                if (site.cubic) {
                    verBox.setAttribute('class', 'parabola-box-ver top')
                    horBpx.setAttribute('class', 'parabola-box-hor top')
                }

                return frag
            },
            calculatedValue: function(opt) {
                let fly = {
                    begin: '',
                    end: '',
                    size: 12,
                    color: '#f9666b',
                    callback: function() {
                        console.log('动画完成')
                    }
                }
                let vData = {
                    site: {
                        left: 0,
                        top: 0,
                        cubic: false
                    },
                    coord: {
                        x: 0,
                        y: 0,
                        os: 0
                    },
                    callback: function() {}
                }

                if (typeof opt === 'object') {
                    fly = {
                        ...fly,
                        ...opt
                    }
                }

                if (!fly.begin || !fly.end) return vData

                /**
                 * beginCrood 获取开始元素的位置
                 * endCrood   获取结束元素的位置
                 */
                let beginCrood = fly.begin.getBoundingClientRect()
                let endCrood = fly.end.getBoundingClientRect()

                /*!
                 *  购物车动画出现的位置
                 *  left: 开始元素的left+width/2
                 *  top: 开始元素的top
                 *  购物车动画结束的位置
                 *  x: 结束元素的left+width/2 再减去购物车动画出现的位置的left
                 *  y: 结束元素的top+height/2 再减去购物车动画出现的位置的top
                 */
                /**
                 * 全部减去 18是因为购物车宽度和高度都是35px;一半难得算(-_-),就填18
                 */
                vData.site.left = beginCrood.left + parseInt(beginCrood.width / 2, 10) - 18
                vData.site.top = beginCrood.top - 18
                vData.coord.x = endCrood.left + parseInt(endCrood.width / 2, 10) - vData.site.left - 18
                vData.coord.y = endCrood.top + parseInt(endCrood.height / 2, 10) - vData.site.top - 18
                vData.coord.os = -50
                vData.callback = fly.callback
                if (beginCrood.top > endCrood.top) vData.site.cubic = true

                return vData
            }
        }

        return new Parabola(opt)
    })()
}

export default ballFly

使用栗子:

<template>
    <div>
        <button class="plus-btn" @click="handlerPlus">+</button>

        <br>
        <br>
        <br>
        <br>
        <br>

        <div class="shopping-cart">购物车</div>
    </div>
</template>

<script>
    import ballFly from '@/utils/ballFly'

    export default {
        methods: {
            handlerPlus(el) {
                ballFly({
                    begin: el.querySelector('.van-stepper__plus'), // 起始位置
                    end: document.querySelector('.picking-num-info'), // 终止位置
                    size: 12, // 小球的直径,默认 12
                    color: '#f9666b' //小球的颜色,默认 #f9666b
                })
            }
        }
    }
</script>