/**
|
* Tools - 工具集
|
* @author Tevin
|
*/
|
|
import moment from 'moment';
|
import Taro from '@tarojs/taro';
|
|
moment.locale('zh-cn');
|
|
export class Tools {
|
/**
|
* 显示消息
|
* @param msg
|
* @param [duration=2000]
|
* @param [mask=false]
|
*/
|
static toast(msg, duration = 2000, mask = false) {
|
Taro.showToast({
|
title: msg,
|
icon: 'none',
|
mask,
|
duration,
|
});
|
}
|
|
/**
|
* URL参数解析
|
* @param {String} name
|
* @param {String} [search]
|
* @return {String|Null}
|
*/
|
static getUrlParam(name, search) {
|
if (process.env.TARO_ENV === 'weapp') {
|
return null;
|
}
|
search = search || window.location.search;
|
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
|
const r = search.substr(1).match(reg);
|
if (r !== null) {
|
return decodeURIComponent(r[2]);
|
} else {
|
return null;
|
}
|
}
|
|
/**
|
* 生成 GUID
|
* @return {string}
|
*/
|
static createGUID() {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
let r = (Math.random() * 16) | 0,
|
v = c === 'x' ? r : (r & 0x3) | 0x8;
|
return v.toString(16);
|
});
|
}
|
|
/**
|
* 获取随机字符串
|
* @param {number} long
|
* @return {string}
|
*/
|
static getRandomString(long) {
|
long = long || 32;
|
const template = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
|
const count = template.length;
|
let result = '';
|
for (let i = 0; i < long; i++) {
|
result += template.charAt(Math.floor(Math.random() * count));
|
}
|
return result;
|
}
|
|
/**
|
* 判断是否是数字
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isNumber(data) {
|
return Object.prototype.toString.call(data) === '[object Number]';
|
}
|
|
/**
|
* 判断是否是字符串
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isString(data) {
|
return Object.prototype.toString.call(data) === '[object String]';
|
}
|
|
/**
|
* 判断是否为布尔值
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isBoolean(data) {
|
return Object.prototype.toString.call(data) === '[object Boolean]';
|
}
|
|
/**
|
* 判断是否是普通对象
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isObject(data) {
|
return Object.prototype.toString.call(data) === '[object Object]';
|
}
|
|
/**
|
* 判断是否是数组
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isArray(data) {
|
return Object.prototype.toString.call(data) === '[object Array]';
|
}
|
|
/**
|
* 判断是否是函数
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isFunction(data) {
|
return Object.prototype.toString.call(data) === '[object Function]';
|
}
|
|
/**
|
* 判断是否为未定义
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isUndefined(data) {
|
return Object.prototype.toString.call(data) === '[object Undefined]';
|
}
|
|
/**
|
* 判断是否为 null
|
* @param {*} data
|
* @return {Boolean}
|
*/
|
static isNull(data) {
|
return Object.prototype.toString.call(data) === '[object Null]';
|
}
|
|
/**
|
* 是否为空对象
|
* @param {*} obj
|
* @return {Boolean}
|
*/
|
static isEmptyObject(obj) {
|
if (!Tools.isObject(obj)) {
|
return true;
|
}
|
for (let p in obj) {
|
if (obj.hasOwnProperty(p)) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
/**
|
* 判断对象是否在属性上相等
|
* @param {String} a
|
* @param {String} b
|
* @return {Boolean}
|
*/
|
static isObjValEqual(a, b) {
|
const aProps = Object.getOwnPropertyNames(a);
|
const bProps = Object.getOwnPropertyNames(b);
|
if (aProps.length !== bProps.length) {
|
return false;
|
}
|
for (let i = 0, propName; i < aProps.length; i++) {
|
propName = aProps[i];
|
if (a[propName] !== b[propName]) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
/**
|
* 统计字符串占位宽度
|
* @param {String} string
|
* @return {Number}
|
*/
|
static countStringLength(string) {
|
let realLength = 0;
|
const len = string.length;
|
let charCode = -1;
|
for (let i = 0; i < len; i++) {
|
charCode = string.charCodeAt(i);
|
// 128编号内字符占1位宽度
|
if (charCode >= 0 && charCode <= 128) {
|
realLength += 1;
|
}
|
// 128编号后字符(包括中文)占2位宽度
|
else {
|
realLength += 2;
|
}
|
}
|
return realLength;
|
}
|
|
/**
|
* 获取字符串基于字节长度的剪切点
|
* @param {String} string
|
* @param {Number} byteLength
|
* @return {Number}
|
*/
|
static getStringCutIndex(string, byteLength) {
|
if (byteLength === 0) {
|
return 0;
|
}
|
let countLength = 0;
|
let charCode = -1;
|
let cutIndex = 0;
|
const len = string.length;
|
for (let i = 0; i < len; i++) {
|
charCode = string.charCodeAt(i);
|
// 128编号内字符占1位宽度
|
if (charCode >= 0 && charCode <= 128) {
|
countLength += 1;
|
}
|
// 128编号后字符(包括中文)占2位宽度
|
else {
|
countLength += 2;
|
}
|
// 裁剪位置
|
if (countLength > byteLength) {
|
cutIndex = i;
|
break;
|
}
|
}
|
return cutIndex;
|
}
|
|
/**
|
* 限制数值范围,超出边界返回边界值
|
* @param {Number|String} num
|
* @param {[Number,Number]} range
|
* @return {Number}
|
*/
|
static limitNumberRange(num, range) {
|
num = Number(num);
|
num = num < range[0] ? range[0] : num;
|
num = num > range[1] ? range[1] : num;
|
return num;
|
}
|
|
/**
|
* 深拷贝
|
* @param {Object} source
|
* @return {Object|Array}
|
*/
|
static deepCopy(source) {
|
let result = null;
|
if (source instanceof Array) {
|
result = [];
|
} else {
|
result = {};
|
}
|
for (let key in source) {
|
if (source.hasOwnProperty(key)) {
|
if (typeof source[key] === 'object') {
|
if (Tools.isNull(source[key])) {
|
result[key] = source[key];
|
} else {
|
result[key] = this.deepCopy(source[key]);
|
}
|
} else {
|
result[key] = source[key];
|
}
|
}
|
}
|
return result;
|
}
|
|
/**
|
* 深合并
|
* @param target
|
* @param source
|
* @return {Object|Array}
|
*/
|
static deepCombine(target, source) {
|
for (let key in source) {
|
if (source.hasOwnProperty(key)) {
|
if (typeof source[key] === 'object') {
|
if (Tools.isNull(source[key])) {
|
target[key] = source[key];
|
} else {
|
if (Tools.isArray(source[key])) {
|
target[key] = [];
|
} else {
|
target[key] = {};
|
}
|
this.deepCombine(target[key], source[key]);
|
}
|
} else {
|
target[key] = source[key];
|
}
|
}
|
}
|
return target;
|
}
|
|
/**
|
* 数组元素交换位置
|
* @param {array} arr 数组
|
* @param {number} fromIndex 要交换项目的位置
|
* @param {number} toIndex 被交换项目的位置
|
*/
|
static swapArray(arr, fromIndex, toIndex) {
|
// 先在目标位置新增一个和当前元素一样的元素,再把当前元素删除掉
|
arr.splice(toIndex, 0, arr[fromIndex]);
|
// 如果拖动元素上移动,当前元素下标为 fromIndex+1
|
if (fromIndex > toIndex) {
|
arr.splice(fromIndex + 1, 1);
|
}
|
// 如果拖动元素下移,当前元素下标还是 fromIndex
|
else {
|
arr.splice(fromIndex, 1);
|
}
|
return arr;
|
}
|
|
/**
|
* 使用 moment.js 格式化时间戳
|
* @param {Number|String} timestamp
|
* @param {String} [type='date']
|
* @return {String}
|
*/
|
static momentFormat(timestamp, type = 'date') {
|
if (!timestamp || timestamp === '0' || timestamp === 0) {
|
return '';
|
}
|
// 字符串
|
if (Tools.isString(timestamp)) {
|
// 全数值
|
if (/^\d+$/.test(timestamp)) {
|
// 8位视为无连接符日期
|
if (timestamp.length === 8) {
|
const day =
|
timestamp.substr(0, 4) +
|
'-' +
|
timestamp.substr(4, 2) +
|
'-' +
|
timestamp.substr(6, 2);
|
timestamp = new Date(timestamp).getTime();
|
}
|
// 超过8位,视为时间戳
|
else if (timestamp.length > 8) {
|
timestamp = parseInt(timestamp);
|
}
|
}
|
// 日期
|
else {
|
try {
|
timestamp = new Date(timestamp).getTime();
|
} catch (e) {}
|
}
|
}
|
// 数值转换:8位整型日期 转 时间戳
|
if (timestamp < 9 * 10e6) {
|
const timeStr = timestamp + '';
|
const dateStr =
|
timeStr.slice(0, 4) + '-' + timeStr.slice(4, 6) + '-' + timeStr.slice(6);
|
if (type === 'date') {
|
return dateStr;
|
}
|
timestamp = new Date(dateStr).getTime();
|
}
|
// 数值转换:时间戳 秒 转 毫秒
|
else if (timestamp < 9 * 10e8) {
|
timestamp *= 1000;
|
}
|
// 数值转换:14位整型日期 转 时间戳
|
else if (/^2[012]\d{2}[01]\d[0123]\d[012]\d[0-5]\d[0-5]\d/.test(timestamp + '')) {
|
const timeStr = timestamp + '';
|
const dateStr =
|
timeStr.slice(0, 4) +
|
'-' +
|
timeStr.slice(4, 6) +
|
'-' +
|
timeStr.slice(6, 8) +
|
' ' +
|
timeStr.slice(8, 10) +
|
':' +
|
timeStr.slice(10, 12) +
|
':' +
|
timeStr.slice(12, 14);
|
if (type === 'dateTimeFull') {
|
return dateStr;
|
}
|
timestamp = new Date(dateStr).getTime();
|
}
|
const curMoment = moment(timestamp);
|
if (type === 'date') {
|
return curMoment.format('YYYY-MM-DD');
|
} else if (type === 'dateTime') {
|
return curMoment.format('YYYY-MM-DD HH:mm');
|
} else if (type === 'dateTimeShort') {
|
return curMoment.format('MM-DD HH:mm');
|
} else if (type === 'dateTimeFull') {
|
return curMoment.format('YYYY-MM-DD HH:mm:ss');
|
}
|
}
|
|
/**
|
* 数值转换为时长描述
|
* @param {number} timestamp
|
* @return {string}
|
*/
|
static durationFormat(timestamp) {
|
if (typeof timestamp !== 'number') {
|
return '';
|
}
|
let words = '';
|
const duration = moment.duration(timestamp);
|
const seconds = duration.get('seconds');
|
if (seconds) {
|
words = seconds + '秒';
|
}
|
const minutes = duration.get('minutes');
|
if (minutes) {
|
words = minutes + '分' + words;
|
}
|
const hours = parseInt(duration.as('hours'));
|
if (hours) {
|
words = hours + '小时' + words;
|
}
|
return words;
|
}
|
|
/**
|
* 数值转换为金钱格式
|
* @param {Number|String} number
|
* @param {String} [forRead=''] 便于阅读财务金额模式
|
* @return {string}
|
*/
|
static moneyFormat(number, forRead = '') {
|
if (!number && typeof number !== 'number' && typeof number !== 'string') {
|
return '';
|
}
|
if (typeof number === 'string') {
|
number = Number(number) || 0;
|
}
|
if (forRead === 'forRead') {
|
return number.toLocaleString('zh-cn', {
|
minimumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
});
|
} else {
|
return number.toFixed(2);
|
}
|
}
|
|
/**
|
* 数值转换为千分位格式
|
* @param {Number|String} number - 传入的值
|
* @param {String} decimalFormat - 处理小数的方式 - notRetain(不保留小数,有小数时也会去掉小数位),keepTwo(保留两位小数),keepThree(保留三位小数)
|
* @return {string}
|
*/
|
static thousandFormat(number, decimalFormat = 'keepTwo') {
|
if (!number && typeof number !== 'number' && typeof number !== 'string') {
|
return '';
|
}
|
// 原始数据
|
let num = number.toString();
|
// 需要进千分位的数,小数点前的数才需要进行千分位计算,即整数位
|
let needThousand = '';
|
// 小数位
|
let decimals = '';
|
// 没有小数位
|
if (!(num.indexOf('.') > -1)) {
|
needThousand = num.toString();
|
if (decimalFormat === 'keepTwo') {
|
decimals = '00';
|
} else if (decimalFormat === 'keepThree') {
|
decimals = '000';
|
}
|
}
|
// 存在小数位时
|
else if (num.indexOf('.') > -1) {
|
needThousand = num.split('.')[0].toString();
|
if (decimalFormat === 'keepTwo') {
|
decimals = Number(num).toFixed(2).split('.')[1] || '';
|
} else if (decimalFormat === 'keepThree') {
|
decimals = Number(num).toFixed(3).split('.')[1] || '';
|
}
|
}
|
// 计算整数位长度
|
let len = needThousand.length;
|
// 如果整数位小于3,直接返回
|
if (len <= 3) {
|
return decimals ? needThousand + '.' + decimals : needThousand;
|
}
|
// 整数位大于3
|
else {
|
// 小数位后的值
|
let temp = '';
|
// 对整数取余
|
let remainder = len % 3;
|
if (decimals) {
|
temp = '.' + decimals;
|
}
|
if (remainder > 0) {
|
// 不是3的整数倍
|
return (
|
needThousand.slice(0, remainder) +
|
',' +
|
needThousand.slice(remainder, len).match(/\d{3}/g)?.join(',') +
|
temp
|
);
|
} else {
|
// 3的整数倍
|
return needThousand.slice(0, len).match(/\d{3}/g)?.join(',') + temp;
|
}
|
}
|
}
|
|
/**
|
* 加法函数,用来得到精确的加法结果
|
* @param {Number|String} num1
|
* @param {Number|String} num2
|
*/
|
static accAdd(num1, num2) {
|
const r1 = Tools.getDecimalLength(num1);
|
const r2 = Tools.getDecimalLength(num2);
|
const m = Math.pow(10, Math.max(r1, r2));
|
return (Number(num1) * m + Number(num2) * m) / m;
|
}
|
|
/**
|
* 减法函数,用来得到精确的减法结果
|
* @param {Number|String} num1
|
* @param {Number|String} num2
|
*/
|
static accSub(num1, num2) {
|
return Tools.accAdd(num1, -num2);
|
}
|
|
/**
|
* 乘法函数,用来得到精确的乘法结果
|
* @param {Number|String} num1
|
* @param {Number|String} num2
|
*/
|
static accMul(num1, num2) {
|
let m = 0;
|
m += Tools.getDecimalLength(num1);
|
m += Tools.getDecimalLength(num2);
|
const r1 = Number(num1.toString().replace('.', ''));
|
const r2 = Number(num2.toString().replace('.', ''));
|
return (r1 * r2) / Math.pow(10, m);
|
}
|
|
/**
|
* 除法函数,用来得到精确的除法结果
|
* @param {Number|String} num1
|
* @param {Number|String} num2
|
*/
|
static accDiv(num1, num2) {
|
const t1 = Tools.getDecimalLength(num1);
|
const t2 = Tools.getDecimalLength(num2);
|
const r1 = Number(num1.toString().replace('.', ''));
|
const r2 = Number(num2.toString().replace('.', ''));
|
return (r1 / r2) * Math.pow(10, t2 - t1);
|
}
|
|
/**
|
* 求小数点后的数据长度
|
* @param {Number|String} num
|
* @return {Number}
|
*/
|
static getDecimalLength(num) {
|
let t = 0;
|
try {
|
t = num.toString().split('.')[1].length;
|
} catch (e) {}
|
return t;
|
}
|
|
/**
|
* 判断是否为手机号码
|
* @param {Number|String} phone
|
* @returns {boolean}
|
*/
|
static isPhone(phone) {
|
return /^1[3456789]\d{9}$/.test('' + phone);
|
}
|
|
/**
|
* 转换周数到日期
|
* @param {Number} year
|
* @param {Number} week
|
* @param {Number} weekDay 需要输出星期几对应的日期 (1~7)
|
* @return {String}
|
*/
|
static transWeekIndexToDate(year, week, weekDay) {
|
if (weekDay < 1 && weekDay > 7) {
|
return '';
|
}
|
const weekDate = moment(year + '-' + week, 'YYYY-WW');
|
const firstDay = weekDate.startOf('week');
|
return firstDay.add(weekDay - 1, 'day').format('YYYY-MM-DD');
|
}
|
|
/**
|
* 防抖函数(一段时间周期内,仅执行最后一次回调)
|
* 当持续触发事件时,只在事件最后一次触发后的一段时间内执行回调函数
|
* 如果在这个时间内再次触发事件,就重新计算时间
|
* @param {Function} fnc
|
* @param {Number} wait
|
* @returns {Function}
|
* @tutorial Tools.debounce(<fnc>, <wait>)(<DebounceKey>)
|
*/
|
static debounce(fnc, wait = 200) {
|
return key => {
|
if (!key || !Tools.isString(key)) {
|
throw 'Debounce function need a key!';
|
}
|
const timer = Tools.debounce['dKey-' + key];
|
if (timer) {
|
clearTimeout(timer);
|
}
|
Tools.debounce['dKey-' + key] = setTimeout(fnc, wait);
|
};
|
}
|
|
/**
|
* 节流函数(本段时间内,仅执行第一次回调)
|
* 在一定时间内只能触发一次函数执行
|
* 如果这个时间内再次触发,则忽略,直到下一个时间段再次触发
|
* @param {Function} fnc
|
* @param {Number} wait
|
* @returns {function(key):void}
|
* @tutorial Tools.throttle(<fnc>, <wait>)(<ThrottleKey>)
|
*/
|
static throttle(fnc, wait = 200) {
|
return key => {
|
if (!key || !Tools.isString(key)) {
|
throw 'Throttle function need a key!';
|
}
|
const previous = Tools.throttle['tKey-' + key] || 0;
|
const now = Date.now();
|
if (now - previous > wait) {
|
Tools.throttle['tKey-' + key] = now;
|
fnc();
|
}
|
};
|
}
|
/**
|
* 精确保留小数点几位数, 自动补零, 四舍五入
|
* 修复 toFixed 四舍六入五讨论的问题
|
* @param {Number|String} num 数值
|
* @param {Number} [digit=0] 小数点后位数
|
* @returns {String|NaN}
|
*/
|
static accFixed(num, digit = 0) {
|
const number = parseFloat(num);
|
if (isNaN(number)) {
|
return NaN;
|
}
|
if (number >= 0) {
|
return (
|
Math.round((number + Number.EPSILON) * Math.pow(10, digit)) / Math.pow(10, digit)
|
).toFixed(digit);
|
} else {
|
return (
|
'-' +
|
(
|
Math.round((Math.abs(number) + Number.EPSILON) * Math.pow(10, digit)) /
|
Math.pow(10, digit)
|
).toFixed(digit)
|
);
|
}
|
}
|
}
|
|
// h5 中,实现 rem 转换 px
|
if (process.env.NODE_ENV === 'development' && process.env.TARO_ENV === 'h5') {
|
window.rem = val => {
|
const p1 = {
|
x: parseFloat(Taro.pxTransform(10, 750)),
|
y: 10,
|
};
|
const p2 = {
|
x: parseFloat(Taro.pxTransform(1000, 750)),
|
y: 1000,
|
};
|
const value = typeof val === 'number' ? val : parseFloat(val);
|
const px = ((value - p1.x) * (p2.y - p1.y)) / (p2.x - p1.x) + p1.y;
|
console.info(Math.round(px) + 'px');
|
};
|
}
|