我的微码-不积跬步无以至千里

150 阅读5分钟

[TOC]

我的微码

自己积累的一些常用代码。基于多地存储的原则,线上保存一份。

持续更新……

场景篇

基于策略模式实现的表单验证类

封装了常用验证规则、弹窗逻辑,易扩展、复用性强。

  • 封装表单验证类
// validate 策略类 (设计模式之策略模式)
class Validate {
  constructor() {
    this.validateFuncs = [];
    this._strategies = {
      isNotEmpty(value, errorMsg) {
        const trimedValue = typeof value === 'string' ? value.trim() : value;
        if (!trimedValue) {
          return errorMsg;
        }
      },
      isNotLessThan(value, errorMsg) {
        const [num1, num2] = value;
        if (Number(num1) < Number(num2)) {
          return errorMsg;
        }
      },
      isNotMoreThan(value, errorMsg) {
        const [num1, num2] = value;
        if (Number(num1) > Number(num2)) {
          return errorMsg;
        }
      },
      isShouldIdentilyCard(value, errorMsg) {
        const idPattern =
          /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
        if (!idPattern.test(value)) {
          return errorMsg;
        }
      },
      isShouldMobileNo(value, errorMsg) {
        const mobilePattern = /^1[1-9]\d{9}$/;
        if (!mobilePattern.test(value)) {
          return errorMsg;
        }
      },
    };
  }

  add(value, rule, errorMsg) {
    this.validateFuncs.push(() => {
      return this._strategies[rule](value, errorMsg);
    });
  }

  check() {
    const { validateFuncs } = this;
    let result = true;
    for (let i = 0, len = validateFuncs.length; i < len; i++) {
      const errorMsg = (() => validateFuncs[i]())();
      if (errorMsg) {
        MessageBox({
          title: '温馨提示',
          message: errorMsg,
        });
        result = false;
        break;
      }
    }
    return result;
  }
}
export default Validate;
  • 使用
const validate = new Validate();
const drawMoney = Number(this.drawMoney);
const canDrawMaxAmount = Number(this.darwData.principalAmt);
validate.add(drawMoney, "isNotEmpty", "请输入支取金额");
validate.add(
  [drawMoney, canDrawMaxAmount],
  "isNotMoreThan",
  "支取金额不能大于最大可支取金额"
);
if (!validate.check()) {
  return;
}

基于CSS3 var()实现的色值级主题定制

  • 配置项(通过接口获取)说明
{
  themeColor: '#B48F4B', // 字体、边框、背景
  lightBgColor: 'rgba(236, 209, 157, 0.2)', // 半透明背景
  gradientStartColor: '#C29B5F', // 渐变色开始
  gradientEndColor: '#B48F4B', // 渐变色结束
  successImg: 'http://10.102.201.171:32720/uploads/-/system/user/avatar/621/avatar.png'
}
  • 实现。逻辑和样式集中放置在app.vue中,做了默认值、惰性加载、兼容手机银行类名等处理。
// src/app.vue
<template>
  <div id="app" :style="styleVar" :class="{'custom':isNeedCustom}">
    <router-view></router-view>
  </div>
</template>
<script>
export default {
  data () {
    return {
      isNeedCustom: false,
      // 默认值
      styleVar: {
        '--themeColor': '#176de6', // 字体、边框、背景
        '--lightBgColor': '#ecf9ff',
        '--gradientStartColor': '#0173e6',
        '--gradientEndColor': '#1452cc'
      }
    }
  },
  methods: {
    // 根据渠道配置,设置自定义样式
    setCustomStyle (config = {}) {
      let customStyles = {}
      const styleVar = this.styleVar
      for (const key in config) {
        const value = config[key]
        const styleName = `--${key}`
        // 如果有值,且是约定中的样式名
        if (value && styleVar[styleName]) {
          customStyles[styleName] = value
        }
      }
      if (JSON.stringify(customStyles) === '{}') {
        // 只有配置了自定义样式,才会应用var变量的这套方案
        // 否则就是全局定义的默认样式
        return
      }
      this.styleVar = { ...styleVar, ...customStyles }
      this.isNeedCustom = true
    }
  }
}
</script>
<style lang="less">
// 兼容框架原有换肤方案,保持与其一致的类名
#app.custom .dynamic-font-color {
  color: var(--themeColor);
}
#app.custom .dynamic-border-color {
  border-color: var(--themeColor);
}
#app.custom .dynamic-bg-color {
  background-color: var(--themeColor);
}
#app.custom .dynamic-bg-opacitycolor {
  background-color: var(--lightBgColor);
}
#app.custom .btn-single {
  background: linear-gradient(
    to right,
    var(--gradientStartColor) 0%,
    var(--gradientEndColor) 100%
  );
}
#app.custom .btn-half {
  background: linear-gradient(
    to right,
    var(--gradientStartColor) 0%,
    var(--gradientEndColor) 100%
  );
}
#app.custom .btn-box-half {
  border: 1px solid var(--themeColor);
  color: var(--themeColor);
}
#app.custom .gradient-block {
  background: linear-gradient(
    to right,
    var(--gradientStartColor) 0%,
    var(--gradientEndColor) 100%
  );
}
</style>
  • 使用。获取到渠道配置后,执行app实例中的setCustomStyle方法。
// main.js
const app = new Vue({...})
// 获取config并设置主题色           
app.$root.$children[0].setCustomStyle(config['wx'])

曝光埋点指令

  • 指令封装
// plugin.js
import './intersection-observer.min.js';

const intersectionObserver = new IntersectionObserver(
  function (entries) {
    let uuId = sessionStorage.getItem('uuid');
    if (!uuId) {
      sessionStorage.setItem('uuid', uuidv4());
      uuId = sessionStorage.getItem('uuid');
    }
    entries.forEach((entry) => {
      // 当节点进入视窗
      if (entry.isIntersecting) {
        const data = entry.target.attributes['exposure-data'].value;
        const dataObJ = JSON.parse(data); 
        const sendsaData = {
          page_name: dataObJ.currentPagename || '', // 所在页面
          ...
        };
        sensors.track('shared_op_impression', sendsaData);
      }
    });
  },
  {
    root: null,
    rootMargin: '0px',
    threshold: 0.5, // 不一定非得全部露出来  这个阈值可以小一点点
  }
);

// 指令
const MyPlugin = {
  install: (Vue) => {
    // 添加全局方法。会直接挂载在构造函数上
    Vue.$post = post;

    // 添加实例方法。new实例化后通过this.$xxx访问
    Vue.prototype.$post = post;
      
    // 全局指令-曝光埋点
    Vue.directive('exposure', {
      // 当被绑定的元素插入到 DOM 中时……
      bind(el) {
        // 监听元素
        intersectionObserver.observe(el);
      },
    }); 
  }
}
  • 使用
<div v-for="(item, index) in productList"
     v-exposure
     :exposure-data="JSON.stringify(item)"
     :key="index"
></div>

获取验证码时的倒计时

<template>
  <span v-if="countDownNumber" class="main__row__btn main__row__btn--waiting">重新获取({{ countDownNumber }})</span>
  <span v-else class="main__row__btn" @click="onSendCode()">{{
    countDownNumber === '' ? '获取验证码' : '重新获取'
    }}</span>
</template>
   <script>
export default {
  data() {
    return {
      countDownNumber: ""
    };
  },
  methods: {
    initCountDown() {
      clearInterval(this.timer);
      this.countDownNumber = 60;
      this.timer = setInterval(() => {
        let count = this.countDownNumber;
        count--;
        this.countDownNumber = count;
        if (count === 0) {
          clearInterval(this.timer);
        }
      }, 1000);
    }
  },
  destroyed() {
    clearInterval(this.timer);
  }
};
</script>

Vue二次封装组件时,对双向绑定和属性继承的处理

纯备忘

<template>
  <van-popup class="warpper" v-model="isShowPopup" position="bottom" v-bind="$attrs">
  </van-popup>
</template>
<script>
export default {
  props: {
    visible: Boolean // 显隐
  },
  computed: {
    isShowPopup: {
      get() {
        return this.visible;
      },
      set(newVal) {
        this.$emit("update:visible", newVal);
      }
    }
  }
};
</script>

解决VueRouter在3.0以上版本跳转与当前相同路由时报错的问题

import VueRouter from 'vue-router';
// 解决vue-router在3.0版本以上跳转与当前路由相同路由时报错问题
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location, onResolve, onReject) {
  if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject);
  return originalPush.call(this, location).catch((err) => {
    if (VueRouter.isNavigationFailure(err)) {
      // resolve err
      return err;
    }
    // rethrow error
    return Promise.reject(err);
  });
};
Vue.use(VueRouter);

VueRouter路由切换时自动滚动到顶部

// router.js
const router = new VueRouter({
  routes: [],
  scrollBehavior() {
    return { x: 0, y: 0 };
  }
});
export default router

重载Vue页面

  • 方案1(推荐)

    如果是在js中使用,可以把reload挂载到window;如果是在vue组件内使用,可以使用依赖注入

<template>
  <div id="app">
    <router-view v-if="isRouterAlive"></router-view>
  </div>
</template>
<script>
export default {
  data() {
    // 加挂全局的vue对象,用于手机银行重新登陆后的页面重载
    window.$appVue = this;
    return {
      isRouterAlive: true
    };
  },
  methods: {
    reload() {
      this.isRouterAlive = false;
      this.$nextTick(() => {
        this.isRouterAlive = true;
      });
    }
  }
};
</script>
  • 方案2
  function reloadPage() {
    // window.$appVue.$router.go(0) // location.reload方案在APP无效
    const path = window.$appVue.$route.fullPath;
    window.$appVue.$router.replace('/');
    setTimeout(() => {
      window.$appVue.$router.replace(path);
    }, 0);
  }

axios二次封装,保证并行请求仅有1个loading

// http.js
let requestNum = 0; // 带loading的请求数量,保证并行请求仅有1个loading
let isLoading = false;

function showLoading() {
  if (!isLoading) {
    sirius.showLoading();
    isLoading = true;
  }
  requestNum++;
}

function hideLoading() {
  requestNum--;
  if (requestNum <= 0) {
    sirius.hideLoading();
    isLoading = false;
  }
}

const post = (needLoading = true) => {
  needLoading && showLoading();
  return new Promise((resolve, reject) => {
    axios().then(()=>{
      resolve()
      needLoading && hideLoading();
    }).catch(()=>{
      reject()
      needLoading && hideLoading();
    })
  });
};

export default post;

判断首次进入页面

router.beforeEach((to, from, next) => {
  // 初次进入清除tokne的判断:有accesstoken,且不是单页应用内后退,且不是刷新
  // window.performance.navigation.type判断页面加载来源  0跳转 1刷新 2后退
  if (accesstoken && from.name === null && window.performance.navigation.type === 0) {
    window.localStorage.removeItem('sessions');
  }
});

监听页面显隐事件

export default {
  created() {
    document.addEventListener("visibilitychange", this.onVisibilityChange);
  },
  methods: {
    onVisibilityChange() {
      if (document.visibilityState === "visible") {
        console.log("***页面可见***");
      }
    }
  },
  destroyed() {
    document.removeEventListener("visibilitychange", this.onVisibilityChange);
  }
};

解决滚动穿透

// 解决滚动穿透
$('.box').on('touchmove', '.box-bg', (e) => {
	e.preventDefault();
});

工具篇

扩展toFixed,以解决四舍五入不准确的问题

window.Number.prototype.toFixed = function (d) {
  let s = `${this}`;
  if (!d) d = 0;
  if (s.indexOf('.') === -1) s += '.';
  s += new Array(d + 1).join('0');
  if (new RegExp(`^(-|\\+)?(\\d+(\\.\\d{0,${d + 1}})?)\\d*$`).test(s)) {
    let s2 = `0${RegExp.$2}`;
    const pm = RegExp.$1;
    let a = RegExp.$3.length;
    let b = true;
    if (a === d + 2) {
      a = s2.match(/\d/g);
      if (parseInt(a[a.length - 1]) > 4) {
        for (let i = a.length - 2; i >= 0; i--) {
          a[i] = parseInt(a[i]) + 1;
          if (a[i] === 10) {
            a[i] = 0;
            b = i !== 1;
          } else break;
        }
      }
      s2 = a.join('').replace(new RegExp(`(\\d+)(\\d{${d}})\\d$`), '$1.$2');
    }
    if (b) s2 = s2.substr(1);
    return (pm + s2).replace(/\.$/, '');
  }
  return `${this}`;
};

封装sessionStroage,使支持对json的存取

const saveJsonToSession = (key, data) => {
  const str = typeof data === 'string' ? data : JSON.stringify(data);
  window.sessionStorage.setItem(key, str);
};

const getJsonFromSession = (key) => {
  const stringData = window.sessionStorage.getItem(key);
  let result = null;
  if (stringData) {
    result = stringData.startsWith('{') || stringData.startsWith('[') ? JSON.parse(stringData) : stringData;
  }
  return result;
};

浮点数四则运算

解决js浮点数运算不准确的问题

// 浮点数计算相加兼容
const floatAdd = (num1, num2) => {
  let r1, r2;
  try {
    r1 = num1.toString().split('.')[1].length;
  } catch (e) {
    r1 = 0;
  }
  try {
    r2 = num2.toString().split('.')[1].length;
  } catch (e) {
    r2 = 0;
  }
  const m = 10 ** Math.max(r1, r2);
  const n = r1 >= r2 ? r1 : r2;
  return ((num1 * m + num2 * m) / m).toFixed(n);
};
// 浮点相减bug兼容
const floatSub = (num1, num2) => {
  let r1, r2;
  try {
    r1 = num1.toString().split('.')[1].length;
  } catch (e) {
    r1 = 0;
  }
  try {
    r2 = num2.toString().split('.')[1].length;
  } catch (e) {
    r2 = 0;
  }
  const m = 10 ** Math.max(r1, r2);
  const n = r1 >= r2 ? r1 : r2;
  return ((num1 * m - num2 * m) / m).toFixed(n);
};
// js 浮点数计算 乘
const floatMul = function (a, b) {
  let c = 0;
  const d = a.toString();
  const e = b.toString();
  const dDecimals = d.split('.')[1];
  const eDecimals = e.split('.')[1];
  if (dDecimals) {
    c += dDecimals.length;
  }
  if (eDecimals) {
    c += eDecimals.length;
  }
  return (Number(d.replace('.', '')) * Number(e.replace('.', ''))) / 10 ** c;
};
// 浮点数相除问题
const floatDiv = function (a, b) {
  const c = Number(a.toString().replace('.', ''));
  const d = Number(b.toString().replace('.', ''));
  const aDecimals = a.toString().split('.')[1];
  const bDecimals = b.toString().split('.')[1];
  const e = aDecimals ? aDecimals.length : 0;
  const f = bDecimals ? bDecimals.length : 0;
  return floatMul(c / d, 10 ** (f - e));
};

防抖/节流函数

// 防抖功能函数
function debounce(fn, timestamp = 1000) {
  // 1、创建一个标记用来存放定时器的返回值
  let timeout = null;
  return function (...args) {
    // 2、每次当用户点击/输入的时候,把前一个定时器清除
    clearTimeout(timeout);
    // 3、然后创建一个新的 setTimeout,
    // 这样就能保证点击按钮后的 interval 间隔内
    // 如果用户还点击了的话,就不会执行 fn 函数
    timeout = setTimeout(() => {
      fn.call(this, args);
    }, timestamp);
  };
}

// 节流函数,立即执行版
function throttle(fn, timestamp = 1000) {
  // 1、通过闭包保存一个标记
  let canRun = true;
  return function (...args) {
    // 2、在函数开头判断标志是否为 true,不为 true 则中断函数
    if (!canRun) {
      return;
    }
    // 3、将 canRun 设置为 false,防止执行之前再被执行
    canRun = false;
    // 4、定时器
    fn.call(this, args);
    setTimeout(() => {
      // fn.call(this, arguments);
      // 5、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
      canRun = true;
    }, timestamp);
  };
}

金额转汉字

// 金额大写
Vue.filter('amount', function (input) {
  if (input !== undefined) {
    const intPosTwo = input.indexOf(',');
    if (intPosTwo) {
      input = input.replace(/,/g, '');
    }
    if (parseFloat(input) < 0 || isNaN(input) || input === '') {
      return '零元整';
    }
    input = String(parseFloat(input));
    let strOutput = '';
    let strUnit = '仟佰拾万仟佰拾亿仟佰拾万仟佰拾元角分';
    input += '00';
    const intPos = input.indexOf('.');
    if (intPos >= 0) {
      input = input.substring(0, intPos) + input.substr(intPos + 1, 2);
    }
    strUnit = strUnit.substr(strUnit.length - input.length);
    for (let i = 0; i < input.length; i++) {
      strOutput += '零壹贰叁肆伍陆柒捌玖'.substr(input.substr(i, 1), 1) + strUnit.substr(i, 1);
    }
    strOutput = strOutput
      .replace(/零角零分$/, '整')
      .replace(/零[仟佰拾]/g, '零')
      .replace(/零{2,}/g, '零')
      .replace(/零([亿|万])/g, '$1')
      .replace(/零+元/, '元')
      .replace(/亿零{0,3}万/, '亿')
      .replace(/^元/, '零元')
      .replace(/零角/, '零')
      .replace(/零分/, '');
    if (parseFloat(input) === 0) {
      return '零元整';
    }
    if (parseFloat(input) > 0 && parseFloat(input) < 100) {
      return strOutput.replace(/零元/, '').replace(/零/, '');
    }
    return strOutput;
  }
  return '零元';
});