feat: add web application
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
//获取相关CSS属性
|
||||
const getCss = function (o, key) {
|
||||
return o.currentStyle
|
||||
? o.currentStyle[key]
|
||||
: document.defaultView?.getComputedStyle(o, null)[key];
|
||||
};
|
||||
|
||||
const params = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
currentX: 0,
|
||||
currentY: 0,
|
||||
flag: false,
|
||||
};
|
||||
|
||||
const startDrag = function (bar, target, callback?) {
|
||||
const screenWidth = document.body.clientWidth; // body当前宽度
|
||||
const screenHeight = document.documentElement.clientHeight; // 可见区域高度
|
||||
|
||||
const dragDomW = target.offsetWidth; // 对话框宽度
|
||||
const dragDomH = target.offsetHeight; // 对话框高度
|
||||
|
||||
const minDomLeft = target.offsetLeft;
|
||||
const minDomTop = target.offsetTop;
|
||||
|
||||
const maxDragDomLeft = screenWidth - minDomLeft - dragDomW;
|
||||
const maxDragDomTop = screenHeight - minDomTop - dragDomH;
|
||||
|
||||
if (getCss(target, 'left') !== 'auto') {
|
||||
params.left = getCss(target, 'left');
|
||||
}
|
||||
if (getCss(target, 'top') !== 'auto') {
|
||||
params.top = getCss(target, 'top');
|
||||
}
|
||||
|
||||
//o是移动对象
|
||||
bar.onmousedown = function (event) {
|
||||
params.flag = true;
|
||||
if (!event) {
|
||||
event = window.event;
|
||||
//防止IE文字选中
|
||||
bar.onselectstart = function () {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
const e = event;
|
||||
params.currentX = e.clientX;
|
||||
params.currentY = e.clientY;
|
||||
};
|
||||
document.onmouseup = function () {
|
||||
params.flag = false;
|
||||
if (getCss(target, 'left') !== 'auto') {
|
||||
params.left = getCss(target, 'left');
|
||||
}
|
||||
if (getCss(target, 'top') !== 'auto') {
|
||||
params.top = getCss(target, 'top');
|
||||
}
|
||||
};
|
||||
document.onmousemove = function (event) {
|
||||
const e: any = event ? event : window.event;
|
||||
if (params.flag) {
|
||||
const nowX = e.clientX,
|
||||
nowY = e.clientY;
|
||||
const disX = nowX - params.currentX,
|
||||
disY = nowY - params.currentY;
|
||||
|
||||
let left = parseInt(params.left) + disX;
|
||||
let top = parseInt(params.top) + disY;
|
||||
|
||||
// 拖出屏幕边缘
|
||||
if (-left > minDomLeft) {
|
||||
left = -minDomLeft;
|
||||
} else if (left > maxDragDomLeft) {
|
||||
left = maxDragDomLeft;
|
||||
}
|
||||
|
||||
if (-top > minDomTop) {
|
||||
top = -minDomTop;
|
||||
} else if (top > maxDragDomTop) {
|
||||
top = maxDragDomTop;
|
||||
}
|
||||
|
||||
target.style.left = left + 'px';
|
||||
target.style.top = top + 'px';
|
||||
|
||||
if (typeof callback == 'function') {
|
||||
callback((parseInt(params.left) || 0) + disX, (parseInt(params.top) || 0) + disY);
|
||||
}
|
||||
|
||||
if (event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default startDrag;
|
||||
@@ -0,0 +1,127 @@
|
||||
// 默认缓存期限为7天
|
||||
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
|
||||
|
||||
/**
|
||||
* 创建本地缓存对象
|
||||
* @param {string=} prefixKey -
|
||||
* @param {Object} [storage=localStorage] - sessionStorage | localStorage
|
||||
*/
|
||||
export const createStorage = ({ prefixKey = '', storage = localStorage } = {}) => {
|
||||
/**
|
||||
* 本地缓存类
|
||||
* @class Storage
|
||||
*/
|
||||
const Storage = class {
|
||||
private storage = storage;
|
||||
private prefixKey?: string = prefixKey;
|
||||
|
||||
private getKey(key: string) {
|
||||
return `${this.prefixKey}${key}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 设置缓存
|
||||
* @param {string} key 缓存键
|
||||
* @param {*} value 缓存值
|
||||
* @param expire
|
||||
*/
|
||||
set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
|
||||
});
|
||||
this.storage.setItem(this.getKey(key), stringData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取缓存
|
||||
* @param {string} key 缓存键
|
||||
* @param {*=} def 默认值
|
||||
*/
|
||||
get(key: string, def: any = null) {
|
||||
const item = this.storage.getItem(this.getKey(key));
|
||||
if (item) {
|
||||
try {
|
||||
const data = JSON.parse(item);
|
||||
const { value, expire } = data;
|
||||
// 在有效期内直接返回
|
||||
if (expire === null || expire >= Date.now()) {
|
||||
return value;
|
||||
}
|
||||
this.remove(key);
|
||||
} catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存删除某项
|
||||
* @param {string} key
|
||||
*/
|
||||
remove(key: string) {
|
||||
this.storage.removeItem(this.getKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
* @memberOf Cache
|
||||
*/
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置cookie
|
||||
* @param {string} name cookie 名称
|
||||
* @param {*} value cookie 值
|
||||
* @param {number=} expire 过期时间
|
||||
* 如果过期时间为设置,默认关闭浏览器自动删除
|
||||
* @example
|
||||
*/
|
||||
setCookie(name: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
|
||||
document.cookie = `${this.getKey(name)}=${value}; Max-Age=${expire}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名字获取cookie值
|
||||
* @param name
|
||||
*/
|
||||
getCookie(name: string): string {
|
||||
const cookieArr = document.cookie.split('; ');
|
||||
for (let i = 0, length = cookieArr.length; i < length; i++) {
|
||||
const kv = cookieArr[i].split('=');
|
||||
if (kv[0] === this.getKey(name)) {
|
||||
return kv[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名字删除指定的cookie
|
||||
* @param {string} key
|
||||
*/
|
||||
removeCookie(key: string) {
|
||||
this.setCookie(key, 1, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空cookie,使所有cookie失效
|
||||
*/
|
||||
clearCookie(): void {
|
||||
const keys = document.cookie.match(/[^ =;]+(?==)/g);
|
||||
if (keys) {
|
||||
for (let i = keys.length; i--; ) {
|
||||
document.cookie = keys[i] + '=0;expire=' + new Date(0).toUTCString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return new Storage();
|
||||
};
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
export default Storage;
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
export function delNullProperty(obj) {
|
||||
for (const i in obj) {
|
||||
if (obj[i] === undefined || obj[i] === null || obj[i] === '') {
|
||||
delete obj[i];
|
||||
} else if (obj[i].constructor === Object) {
|
||||
if (Object.keys(obj[i]).length === 0) delete obj[i];
|
||||
delNullProperty(obj[i]);
|
||||
} else if (obj[i].constructor === Array) {
|
||||
if (Array.prototype.isPrototypeOf(obj[i]) && obj[i].length === 0) {
|
||||
delete obj[i];
|
||||
} else {
|
||||
for (let index = 0; index < obj[i].length; index++) {
|
||||
if (
|
||||
obj[i][index] === undefined ||
|
||||
obj[i][index] === null ||
|
||||
obj[i][index] === '' ||
|
||||
JSON.stringify(obj[i][index]) === '{}'
|
||||
) {
|
||||
obj[i].splice(index, 1);
|
||||
index--;
|
||||
}
|
||||
if (obj[i][index] === undefined || obj[i][index].constructor !== undefined) {
|
||||
continue;
|
||||
}
|
||||
if (obj[i][index].constructor === Object || obj[i][index].constructor === Array) {
|
||||
delNullProperty(obj[i][index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function reverse(array) {
|
||||
if (array !== undefined && array !== null && array.length > 0) {
|
||||
return array.reverse();
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
export function encodeParams(obj) {
|
||||
const arr = [];
|
||||
for (const p in obj) {
|
||||
// @ts-ignore
|
||||
arr.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
|
||||
}
|
||||
return arr.join('&');
|
||||
}
|
||||
|
||||
export function copyObj(obj2: any, obj1: any) {
|
||||
for (const key in obj1) {
|
||||
if (obj2[key] !== undefined) {
|
||||
obj2[key] = obj1[key];
|
||||
}
|
||||
}
|
||||
return obj2;
|
||||
}
|
||||
|
||||
// 返回两个数组的差集
|
||||
export function findArrayDifference(arr1: number[], arr2: number[]): number[] {
|
||||
return arr1.filter((num) => !arr2.includes(num));
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @description 获取用户浏览器版本及系统信息
|
||||
* @param {string='zh-cn' | 'en'} lang 返回中文的信息还是英文的
|
||||
* @constructor
|
||||
*/
|
||||
export default function BrowserType(lang: 'zh-cn' | 'en' = 'en') {
|
||||
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const testUa = (regexp) => regexp.test(ua);
|
||||
const testVs = (regexp) =>
|
||||
ua
|
||||
.match(regexp)
|
||||
?.toString()
|
||||
.replace(/[^0-9|_.]/g, '')
|
||||
.replace(/_/g, '.');
|
||||
// 系统
|
||||
const system =
|
||||
new Map([
|
||||
[testUa(/windows|win32|win64|wow32|wow64/g), 'windows'], // windows系统
|
||||
[testUa(/macintosh|macintel/g), 'macos'], // macos系统
|
||||
[testUa(/x11/g), 'linux'], // linux系统
|
||||
[testUa(/android|adr/g), 'android'], // android系统
|
||||
[testUa(/ios|iphone|ipad|ipod|iwatch/g), 'ios'], // ios系统
|
||||
]).get(true) || 'unknow';
|
||||
|
||||
// 系统版本
|
||||
const systemVs =
|
||||
new Map([
|
||||
[
|
||||
'windows',
|
||||
new Map([
|
||||
[testUa(/windows nt 5.0|windows 2000/g), '2000'],
|
||||
[testUa(/windows nt 5.1|windows xp/g), 'xp'],
|
||||
[testUa(/windows nt 5.2|windows 2003/g), '2003'],
|
||||
[testUa(/windows nt 6.0|windows vista/g), 'vista'],
|
||||
[testUa(/windows nt 6.1|windows 7/g), '7'],
|
||||
[testUa(/windows nt 6.2|windows 8/g), '8'],
|
||||
[testUa(/windows nt 6.3|windows 8.1/g), '8.1'],
|
||||
[testUa(/windows nt 10.0|windows 10/g), '10'],
|
||||
]).get(true),
|
||||
],
|
||||
['macos', testVs(/os x [\d._]+/g)],
|
||||
['android', testVs(/android [\d._]+/g)],
|
||||
['ios', testVs(/os [\d._]+/g)],
|
||||
]).get(system) || 'unknow';
|
||||
|
||||
// 平台
|
||||
let platform = 'unknow';
|
||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||
platform = 'desktop'; // 桌面端
|
||||
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||
platform = 'mobile'; // 移动端
|
||||
}
|
||||
// 内核和载体
|
||||
const [engine = 'unknow', supporter = 'unknow'] = new Map([
|
||||
[
|
||||
testUa(/applewebkit/g),
|
||||
[
|
||||
'webkit',
|
||||
new Map([
|
||||
// webkit内核
|
||||
[testUa(/safari/g), 'safari'], // safari浏览器
|
||||
[testUa(/chrome/g), 'chrome'], // chrome浏览器
|
||||
[testUa(/opr/g), 'opera'], // opera浏览器
|
||||
[testUa(/edge/g), 'edge'], // edge浏览器
|
||||
]).get(true),
|
||||
] || 'unknow',
|
||||
], // [webkit内核, xxx浏览器]
|
||||
[testUa(/gecko/g) && testUa(/firefox/g), ['gecko', 'firefox']], // [gecko内核,firefox浏览器]
|
||||
[testUa(/presto/g), ['presto', 'opera']], // [presto内核,opera浏览器]
|
||||
[testUa(/trident|compatible|msie/g), ['trident', 'iexplore']], // [trident内核,iexplore浏览器]
|
||||
]).get(true) || ['unknow', 'unknow'];
|
||||
|
||||
// 内核版本
|
||||
const engineVs =
|
||||
new Map([
|
||||
['webkit', testVs(/applewebkit\/[\d._]+/g)],
|
||||
['gecko', testVs(/gecko\/[\d._]+/g)],
|
||||
['presto', testVs(/presto\/[\d._]+/g)],
|
||||
['trident', testVs(/trident\/[\d._]+/g)],
|
||||
]).get(engine) || 'unknow';
|
||||
|
||||
// 载体版本
|
||||
const supporterVs =
|
||||
new Map([
|
||||
['firefox', testVs(/firefox\/[\d._]+/g)],
|
||||
['opera', testVs(/opr\/[\d._]+/g)],
|
||||
['iexplore', testVs(/(msie [\d._]+)|(rv:[\d._]+)/g)],
|
||||
['edge', testVs(/edge\/[\d._]+/g)],
|
||||
['safari', testVs(/version\/[\d._]+/g)],
|
||||
['chrome', testVs(/chrome\/[\d._]+/g)],
|
||||
]).get(supporter) || 'unknow';
|
||||
|
||||
// 外壳和外壳版本
|
||||
const [shell = 'none', shellVs = 'unknow'] = new Map([
|
||||
[testUa(/micromessenger/g), ['wechat', testVs(/micromessenger\/[\d._]+/g)]], // [微信浏览器,]
|
||||
[testUa(/qqbrowser/g), ['qq', testVs(/qqbrowser\/[\d._]+/g)]], // [QQ浏览器,]
|
||||
[testUa(/ucbrowser/g), ['uc', testVs(/ucbrowser\/[\d._]+/g)]], // [UC浏览器,]
|
||||
[testUa(/qihu 360se/g), ['360', 'unknow']], // [360浏览器(无版本),]
|
||||
[testUa(/2345explorer/g), ['2345', testVs(/2345explorer\/[\d._]+/g)]], // [2345浏览器,]
|
||||
[testUa(/metasr/g), ['sougou', 'unknow']], // [搜狗浏览器(无版本),]
|
||||
[testUa(/lbbrowser/g), ['liebao', 'unknow']], // [猎豹浏览器(无版本),]
|
||||
[testUa(/maxthon/g), ['maxthon', testVs(/maxthon\/[\d._]+/g)]], // [遨游浏览器,]
|
||||
]).get(true) || ['none', 'unknow'];
|
||||
|
||||
return {
|
||||
'zh-cn': Object.assign(
|
||||
{
|
||||
内核: engine, // 内核: webkit gecko presto trident
|
||||
内核版本: engineVs, // 内核版本
|
||||
平台: platform, // 平台: desktop mobile
|
||||
载体: supporter, // 载体: chrome safari firefox opera iexplore edge
|
||||
载体版本: supporterVs, // 载体版本
|
||||
系统: system, // 系统: windows macos linux android ios
|
||||
系统版本: systemVs, // 系统版本
|
||||
},
|
||||
shell === 'none'
|
||||
? {}
|
||||
: {
|
||||
外壳: shell, // 外壳: wechat qq uc 360 2345 sougou liebao maxthon
|
||||
外壳版本: shellVs, // 外壳版本
|
||||
}
|
||||
),
|
||||
en: Object.assign(
|
||||
{
|
||||
engine, // 内核: webkit gecko presto trident
|
||||
engineVs, // 内核版本
|
||||
platform, // 平台: desktop mobile
|
||||
supporter, // 载体: chrome safari firefox opera iexplore edge
|
||||
supporterVs, // 载体版本
|
||||
system, // 系统: windows macos linux android ios
|
||||
systemVs, // 系统版本
|
||||
},
|
||||
shell === 'none'
|
||||
? {}
|
||||
: {
|
||||
shell, // 外壳: wechat qq uc 360 2345 sougou liebao maxthon
|
||||
shellVs, // 外壳版本
|
||||
}
|
||||
),
|
||||
}[lang];
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 随机生成字符串
|
||||
*/
|
||||
export function getRandomString(len = 12, isSmall = false) {
|
||||
const _charStr = 'abacdefghjklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const _smallCharStr = 'abacdefghjklmnopqrstuvwxyz0123456789';
|
||||
const charStr = isSmall ? _smallCharStr : _charStr;
|
||||
const min = 0;
|
||||
const max = charStr.length - 1;
|
||||
let _str = '';
|
||||
//循环生成字符串
|
||||
for (let i = 0, index; i < len; i++) {
|
||||
index = (function (randomIndexFunc, i) {
|
||||
return randomIndexFunc(min, max, i, randomIndexFunc);
|
||||
})(function (min, max, i, _self) {
|
||||
const numStart = _charStr.length - 10;
|
||||
let indexTemp = Math.floor(Math.random() * (max - min + 1) + min);
|
||||
if (i == 0 && indexTemp >= numStart) {
|
||||
indexTemp = _self(min, max, i, _self);
|
||||
}
|
||||
return indexTemp;
|
||||
}, i);
|
||||
_str += _charStr[index];
|
||||
}
|
||||
return _str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏中间几位字符
|
||||
*/
|
||||
export function structure(array) {
|
||||
// 将字符串转化成数组
|
||||
const arrBox = [...array];
|
||||
const count = arrBox.length;
|
||||
if (count == 1) {
|
||||
return '*';
|
||||
}
|
||||
let min = 1;
|
||||
let max = count;
|
||||
// 两位姓名
|
||||
if (count == 2) {
|
||||
min = 0;
|
||||
max = count;
|
||||
}
|
||||
// 三位姓名
|
||||
if (count == 3) {
|
||||
min = 0;
|
||||
max = count - 1;
|
||||
}
|
||||
// if (count >= 2 && count <= 8) {
|
||||
// min = 1;
|
||||
// }
|
||||
|
||||
// 手机号
|
||||
if (count == 11) {
|
||||
min = 3;
|
||||
max = 7;
|
||||
}
|
||||
// 身份证号码
|
||||
if (count >= 15) {
|
||||
min = 9;
|
||||
max = count - 4;
|
||||
}
|
||||
// 2.将数组中的4-7位变成*
|
||||
let str = '';
|
||||
arrBox.map((res, index) => {
|
||||
if (index > min && index < max) {
|
||||
str += '*';
|
||||
} else {
|
||||
str += res;
|
||||
}
|
||||
});
|
||||
return str;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
endOfMonth,
|
||||
endOfToday,
|
||||
endOfWeek,
|
||||
endOfYesterday,
|
||||
format,
|
||||
startOfMonth,
|
||||
startOfToday,
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
startOfYesterday,
|
||||
subMonths,
|
||||
} from 'date-fns';
|
||||
|
||||
const DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm:ss';
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export function formatToDateTime(date: string, formatStr = DATE_TIME_FORMAT): string {
|
||||
if (date === null || date === undefined || date === '') {
|
||||
return ``;
|
||||
}
|
||||
return format(new Date(Date.parse(date)), formatStr);
|
||||
}
|
||||
|
||||
export function formatToDate(date: string, formatStr = DATE_FORMAT): string {
|
||||
if (date === null || date === undefined || date === '') {
|
||||
return ``;
|
||||
}
|
||||
return format(new Date(Date.parse(date)), formatStr);
|
||||
}
|
||||
|
||||
export function timestampToTime(timestamp) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const Y = date.getFullYear() + '-';
|
||||
const M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
|
||||
const D = (date.getDate() + 1 <= 10 ? '0' + date.getDate() : date.getDate()) + ' ';
|
||||
const h = (date.getHours() + 1 <= 10 ? '0' + date.getHours() : date.getHours()) + ':';
|
||||
const m = (date.getMinutes() + 1 <= 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':';
|
||||
const s = date.getSeconds() + 1 <= 10 ? '0' + date.getSeconds() : date.getSeconds();
|
||||
return Y + M + D + h + m + s;
|
||||
}
|
||||
|
||||
export function timestampToTimeNF(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const Y = date.getFullYear();
|
||||
const M = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
|
||||
const D = date.getDate() + 1 <= 10 ? '0' + date.getDate() : date.getDate();
|
||||
const h = date.getHours() + 1 <= 10 ? '0' + date.getHours() : date.getHours();
|
||||
const m = date.getMinutes() + 1 <= 10 ? '0' + date.getMinutes() : date.getMinutes();
|
||||
const s = date.getSeconds() + 1 <= 10 ? '0' + date.getSeconds() : date.getSeconds();
|
||||
return Y.toString() + M.toString() + D.toString() + h.toString() + m.toString() + s.toString();
|
||||
}
|
||||
|
||||
export function dateToTimestamp(date: string) {
|
||||
if (date === null || date === undefined || date === '') {
|
||||
return 0;
|
||||
}
|
||||
return new Date(date).getTime();
|
||||
}
|
||||
|
||||
export function timestampToDate(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const Y = date.getFullYear() + '-';
|
||||
const M = (date.getMonth() + 1 <= 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
|
||||
const D = date.getDate() + 1 <= 10 ? '0' + date.getDate() : date.getDate();
|
||||
return Y + M + D;
|
||||
}
|
||||
|
||||
export function getTime() {
|
||||
const myDate = new Date();
|
||||
const hour = myDate.getHours().toString().padStart(2, '0');
|
||||
const minutes = myDate.getMinutes().toString().padStart(2, '0');
|
||||
const seconed = myDate.getSeconds().toString().padStart(2, '0');
|
||||
return hour + ':' + minutes + ':' + seconed;
|
||||
}
|
||||
|
||||
export function getDate() {
|
||||
const myDate = new Date();
|
||||
const month = (myDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = myDate.getDate().toString().padStart(2, '0');
|
||||
return myDate.getFullYear() + '-' + month + '-' + day;
|
||||
}
|
||||
|
||||
export function defaultStatisticsTimeOptions() {
|
||||
return new Date().getTime() - 86400 * 1000;
|
||||
}
|
||||
|
||||
export function formatBefore(oldDate) {
|
||||
//当前时间
|
||||
const newDate = new Date();
|
||||
const newDateTime1 = newDate.getTime(); //含有时分秒
|
||||
newDate.setHours(0);
|
||||
newDate.setMinutes(0);
|
||||
newDate.setSeconds(0);
|
||||
newDate.setMilliseconds(0);
|
||||
const newDateTime2 = newDate.getTime(); //当前时间,不含有时分秒
|
||||
|
||||
//传递时间
|
||||
const oldDateTime1 = oldDate.getTime(); //含有时分秒
|
||||
oldDate.setHours(0);
|
||||
oldDate.setMinutes(0);
|
||||
oldDate.setSeconds(0);
|
||||
oldDate.setMilliseconds(0);
|
||||
const oldDateTime2 = oldDate.getTime(); //不含有时分秒
|
||||
|
||||
const d1 = (newDateTime1 - oldDateTime1) / 1000;
|
||||
const d2 = (newDateTime2 - oldDateTime2) / 1000;
|
||||
|
||||
let res = '';
|
||||
if (d2 > 0) {
|
||||
//是几天前
|
||||
const days = parseInt(d2 / 86400);
|
||||
if (days === 1) {
|
||||
res = '昨天';
|
||||
} else if (days === 2) {
|
||||
res = '前天';
|
||||
} else {
|
||||
res = days + '天前';
|
||||
}
|
||||
} else {
|
||||
//是今天
|
||||
const hours = parseInt(d1 / 3600);
|
||||
if (hours > 0) {
|
||||
res = hours + '小时前';
|
||||
} else {
|
||||
const minutes = parseInt(d1 / 60);
|
||||
if (minutes > 0) {
|
||||
res = minutes + '分钟前';
|
||||
} else {
|
||||
const seconds = parseInt(d1);
|
||||
if (seconds > 10) {
|
||||
res = seconds + '秒前';
|
||||
} else {
|
||||
res = '刚刚';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export function formatAfter(end): string {
|
||||
const start = new Date();
|
||||
let sjc = start.getTime() - end.getTime(); //时间差的毫秒数
|
||||
if (end.getTime() - start.getTime() > 0) {
|
||||
sjc = end.getTime() - start.getTime(); //时间差的毫秒数
|
||||
}
|
||||
const days = Math.floor(sjc / (24 * 3600 * 1000)); //计算出相差天数
|
||||
const leave1 = sjc % (24 * 3600 * 1000); //计算天数后剩余的毫秒数
|
||||
const hours = Math.floor(leave1 / (3600 * 1000)); //计算出小时数
|
||||
const leave2 = leave1 % (3600 * 1000); //计算小时数后剩余的毫秒数
|
||||
const minutes = Math.floor(leave2 / (60 * 1000)); //计算相差分钟数
|
||||
const leave3 = leave2 % (60 * 1000); //计算分钟数后剩余的毫秒数
|
||||
const seconds = Math.round(leave3 / 1000); //计算相差秒数
|
||||
if (days > 0) {
|
||||
return days + '天后';
|
||||
}
|
||||
if (hours > 0) {
|
||||
return hours + '小时后';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return minutes + '分钟后';
|
||||
}
|
||||
if (seconds > 0) {
|
||||
return seconds + '秒后';
|
||||
}
|
||||
return '刚刚';
|
||||
}
|
||||
|
||||
export function defShortcuts() {
|
||||
return {
|
||||
今天: startOfToday().getTime(),
|
||||
昨天: startOfYesterday().getTime(),
|
||||
明天: startOfTomorrow().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function defRangeShortcuts() {
|
||||
const nowDate = new Date();
|
||||
const dayBase = 86400 * 1000;
|
||||
return {
|
||||
今天: [startOfToday().getTime(), endOfToday().getTime()] as const,
|
||||
昨天: () => {
|
||||
return [startOfYesterday().getTime(), endOfYesterday().getTime()] as const;
|
||||
},
|
||||
近7天: [startOfToday().getTime() - dayBase * 6, endOfToday().getTime()] as const,
|
||||
近30天: [startOfToday().getTime() - dayBase * 29, endOfToday().getTime()] as const,
|
||||
近90天: [startOfToday().getTime() - dayBase * 89, endOfToday().getTime()] as const,
|
||||
本周: () => {
|
||||
return [
|
||||
startOfWeek(nowDate, { weekStartsOn: 1 }).getTime(),
|
||||
endOfWeek(nowDate, { weekStartsOn: 1 }).getTime(),
|
||||
] as const;
|
||||
},
|
||||
本月: () => {
|
||||
return [startOfMonth(nowDate).getTime(), endOfMonth(nowDate).getTime()] as const;
|
||||
},
|
||||
上月: () => {
|
||||
return [
|
||||
startOfMonth(subMonths(nowDate, 1)).getTime(),
|
||||
endOfMonth(subMonths(nowDate, 1)).getTime(),
|
||||
] as const;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { upperFirst } from 'lodash-es';
|
||||
|
||||
export interface ViewportOffsetResult {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
rightIncludeBody: number;
|
||||
bottomIncludeBody: number;
|
||||
}
|
||||
|
||||
export function getBoundingClientRect(element: Element): DOMRect | number {
|
||||
if (!element || !element.getBoundingClientRect) {
|
||||
return 0;
|
||||
}
|
||||
return element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function trim(string: string) {
|
||||
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '');
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function hasClass(el: Element, cls: string) {
|
||||
if (!el || !cls) return false;
|
||||
if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.');
|
||||
if (el.classList) {
|
||||
return el.classList.contains(cls);
|
||||
} else {
|
||||
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function addClass(el: Element, cls: string) {
|
||||
if (!el) return;
|
||||
let curClass = el.className;
|
||||
const classes = (cls || '').split(' ');
|
||||
|
||||
for (let i = 0, j = classes.length; i < j; i++) {
|
||||
const clsName = classes[i];
|
||||
if (!clsName) continue;
|
||||
|
||||
if (el.classList) {
|
||||
el.classList.add(clsName);
|
||||
} else if (!hasClass(el, clsName)) {
|
||||
curClass += ' ' + clsName;
|
||||
}
|
||||
}
|
||||
if (!el.classList) {
|
||||
el.className = curClass;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function removeClass(el: Element, cls: string) {
|
||||
if (!el || !cls) return;
|
||||
const classes = cls.split(' ');
|
||||
let curClass = ' ' + el.className + ' ';
|
||||
|
||||
for (let i = 0, j = classes.length; i < j; i++) {
|
||||
const clsName = classes[i];
|
||||
if (!clsName) continue;
|
||||
|
||||
if (el.classList) {
|
||||
el.classList.remove(clsName);
|
||||
} else if (hasClass(el, clsName)) {
|
||||
curClass = curClass.replace(' ' + clsName + ' ', ' ');
|
||||
}
|
||||
}
|
||||
if (!el.classList) {
|
||||
el.className = trim(curClass);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the left and top offset of the current element
|
||||
* left: the distance between the leftmost element and the left side of the document
|
||||
* top: the distance from the top of the element to the top of the document
|
||||
* right: the distance from the far right of the element to the right of the document
|
||||
* bottom: the distance from the bottom of the element to the bottom of the document
|
||||
* rightIncludeBody: the distance between the leftmost element and the right side of the document
|
||||
* bottomIncludeBody: the distance from the bottom of the element to the bottom of the document
|
||||
*
|
||||
* @description:
|
||||
*/
|
||||
export function getViewportOffset(element: Element): ViewportOffsetResult {
|
||||
const doc = document.documentElement;
|
||||
|
||||
const docScrollLeft = doc.scrollLeft;
|
||||
const docScrollTop = doc.scrollTop;
|
||||
const docClientLeft = doc.clientLeft;
|
||||
const docClientTop = doc.clientTop;
|
||||
|
||||
const pageXOffset = window.pageXOffset;
|
||||
const pageYOffset = window.pageYOffset;
|
||||
|
||||
const box = getBoundingClientRect(element);
|
||||
|
||||
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect;
|
||||
|
||||
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0);
|
||||
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0);
|
||||
const offsetLeft = retLeft + pageXOffset;
|
||||
const offsetTop = rectTop + pageYOffset;
|
||||
|
||||
const left = offsetLeft - scrollLeft;
|
||||
const top = offsetTop - scrollTop;
|
||||
|
||||
const clientWidth = window.document.documentElement.clientWidth;
|
||||
const clientHeight = window.document.documentElement.clientHeight;
|
||||
return {
|
||||
left: left,
|
||||
top: top,
|
||||
right: clientWidth - rectWidth - left,
|
||||
bottom: clientHeight - rectHeight - top,
|
||||
rightIncludeBody: clientWidth - left,
|
||||
bottomIncludeBody: clientHeight - top,
|
||||
};
|
||||
}
|
||||
|
||||
export function hackCss(attr: string, value: string) {
|
||||
const prefix: string[] = ['webkit', 'Moz', 'ms', 'OT'];
|
||||
|
||||
const styleObj: any = {};
|
||||
prefix.forEach((item) => {
|
||||
styleObj[`${item}${upperFirst(attr)}`] = value;
|
||||
});
|
||||
return {
|
||||
...styleObj,
|
||||
[attr]: value,
|
||||
};
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function on(
|
||||
element: Element | HTMLElement | Document | Window,
|
||||
event: string,
|
||||
handler: EventListenerOrEventListenerObject
|
||||
): void {
|
||||
if (element && event && handler) {
|
||||
element.addEventListener(event, handler, false);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function off(
|
||||
element: Element | HTMLElement | Document | Window,
|
||||
event: string,
|
||||
handler: Fn
|
||||
): void {
|
||||
if (element && event && handler) {
|
||||
element.removeEventListener(event, handler, false);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function once(el: HTMLElement, event: string, fn: EventListener): void {
|
||||
const listener = function (this: any, ...args: unknown[]) {
|
||||
if (fn) {
|
||||
fn.apply(this, args);
|
||||
}
|
||||
off(el, event, listener);
|
||||
};
|
||||
on(el, event, listener);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 根据文件url获取文件名
|
||||
* @param url 文件url
|
||||
*/
|
||||
function getFileName(url) {
|
||||
const num = url.lastIndexOf('/') + 1;
|
||||
let fileName = url.substring(num);
|
||||
//把参数和文件名分割开
|
||||
fileName = decodeURI(fileName.split('?')[0]);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件地址下载文件
|
||||
* @param {*} sUrl
|
||||
*/
|
||||
export function downloadByUrl({
|
||||
url,
|
||||
target = '_blank',
|
||||
fileName,
|
||||
}: {
|
||||
url: string;
|
||||
target?: '_self' | '_blank';
|
||||
fileName?: string;
|
||||
}): Promise<boolean> {
|
||||
// 是否同源
|
||||
const isSameHost = new URL(url).host == location.host;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
if (isSameHost) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.target = target;
|
||||
|
||||
if (link.download !== undefined) {
|
||||
link.download = fileName || getFileName(url);
|
||||
}
|
||||
|
||||
if (document.createEvent) {
|
||||
const e = document.createEvent('MouseEvents');
|
||||
e.initEvent('click', true, true);
|
||||
link.dispatchEvent(e);
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
if (url.indexOf('?') === -1) {
|
||||
url += '?download';
|
||||
}
|
||||
|
||||
window.open(url, target);
|
||||
return resolve(true);
|
||||
} else {
|
||||
const canvas = document.createElement('canvas');
|
||||
const img = document.createElement('img');
|
||||
img.setAttribute('crossOrigin', 'Anonymous');
|
||||
img.src = url;
|
||||
img.onload = () => {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.drawImage(img, 0, 0, img.width, img.height);
|
||||
// window.navigator.msSaveBlob(canvas.msToBlob(),'image.jpg');
|
||||
// saveAs(imageDataUrl, '附件');
|
||||
canvas.toBlob((blob) => {
|
||||
const link = document.createElement('a');
|
||||
// @ts-ignore
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = getFileName(url);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
resolve(true);
|
||||
}, 'image/jpeg');
|
||||
};
|
||||
img.onerror = (e) => reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
const defaultKey = 'f080a463654b2279';
|
||||
|
||||
export const aesEcb = {
|
||||
// 加密
|
||||
encrypt(word: string, keyStr: string = defaultKey): string {
|
||||
const key = CryptoJS.enc.Utf8.parse(keyStr);
|
||||
const src = CryptoJS.enc.Utf8.parse(word);
|
||||
const encrypted = CryptoJS.AES.encrypt(src, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
},
|
||||
// 解密
|
||||
decrypt(word: string, keyStr: string = defaultKey): string {
|
||||
const key = CryptoJS.enc.Utf8.parse(keyStr);
|
||||
const decrypt = CryptoJS.AES.decrypt(word, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { GlobEnvConfig } from '/#/config';
|
||||
|
||||
import { warn } from '@/utils/log';
|
||||
import pkg from '../../package.json';
|
||||
import { getConfigFileName } from '../../build/getConfigFileName';
|
||||
|
||||
export function getCommonStoragePrefix() {
|
||||
const { VITE_GLOB_APP_SHORT_NAME } = getAppEnvConfig();
|
||||
return `${VITE_GLOB_APP_SHORT_NAME}__${getEnv()}`.toUpperCase();
|
||||
}
|
||||
|
||||
// Generate cache key according to version
|
||||
export function getStorageShortName() {
|
||||
return `${getCommonStoragePrefix()}${`__${pkg.version}`}__`.toUpperCase();
|
||||
}
|
||||
|
||||
export function getAppEnvConfig() {
|
||||
const ENV_NAME = getConfigFileName(import.meta.env);
|
||||
|
||||
const ENV = (import.meta.env.DEV
|
||||
? // Get the global configuration (the configuration will be extracted independently when packaging)
|
||||
(import.meta.env as unknown as GlobEnvConfig)
|
||||
: window[ENV_NAME as any]) as unknown as GlobEnvConfig;
|
||||
|
||||
const {
|
||||
VITE_GLOB_APP_TITLE,
|
||||
VITE_GLOB_API_URL,
|
||||
VITE_GLOB_APP_SHORT_NAME,
|
||||
VITE_GLOB_API_URL_PREFIX,
|
||||
VITE_GLOB_UPLOAD_URL,
|
||||
VITE_GLOB_IMG_URL,
|
||||
} = ENV;
|
||||
|
||||
if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
|
||||
warn(
|
||||
`VITE_GLOB_APP_SHORT_NAME Variables can only be characters/underscores, please modify in the environment variables and re-running.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
VITE_GLOB_APP_TITLE,
|
||||
VITE_GLOB_API_URL,
|
||||
VITE_GLOB_APP_SHORT_NAME,
|
||||
VITE_GLOB_API_URL_PREFIX,
|
||||
VITE_GLOB_UPLOAD_URL,
|
||||
VITE_GLOB_IMG_URL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Development model
|
||||
*/
|
||||
export const devMode = 'development';
|
||||
|
||||
/**
|
||||
* @description: Production mode
|
||||
*/
|
||||
export const prodMode = 'production';
|
||||
|
||||
/**
|
||||
* @description: Get environment variables
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function getEnv(): string {
|
||||
return import.meta.env.MODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Is it a development mode
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function isDevMode(): boolean {
|
||||
return import.meta.env.DEV;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Is it a production mode
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function isProdMode(): boolean {
|
||||
return import.meta.env.PROD;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// 关键词配置
|
||||
interface IKeywordOption {
|
||||
keyword: string | RegExp;
|
||||
color?: string;
|
||||
bgColor?: string;
|
||||
style?: Record<string, any>;
|
||||
// 高亮标签名
|
||||
tagName?: string;
|
||||
// 忽略大小写
|
||||
caseSensitive?: boolean;
|
||||
// 自定义渲染高亮html
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
renderHighlightKeyword?: (content: string) => any;
|
||||
}
|
||||
|
||||
type IKeyword = string | IKeywordOption;
|
||||
|
||||
export interface IMatchIndex {
|
||||
index: number;
|
||||
subString: string;
|
||||
}
|
||||
|
||||
// 关键词索引
|
||||
export interface IKeywordParseIndex {
|
||||
keyword: string | RegExp;
|
||||
indexList: IMatchIndex[];
|
||||
option?: IKeywordOption;
|
||||
}
|
||||
|
||||
// 关键词
|
||||
export interface IKeywordParseResult {
|
||||
start: number;
|
||||
end: number;
|
||||
subString?: string;
|
||||
option?: IKeywordOption;
|
||||
}
|
||||
|
||||
// 计算
|
||||
const getKeywordIndexList = (content: string, keyword: string | RegExp, flags = 'ig') => {
|
||||
const reg = new RegExp(keyword, flags);
|
||||
const res = (content as any).matchAll(reg);
|
||||
const arr = [...res];
|
||||
const allIndexArr: IMatchIndex[] = arr.map((e) => ({
|
||||
index: e.index,
|
||||
subString: e['0'],
|
||||
}));
|
||||
return allIndexArr;
|
||||
};
|
||||
|
||||
// 驼峰转换横线
|
||||
function humpToLine(name: string) {
|
||||
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
}
|
||||
|
||||
const renderNodeTag = (subStr: string, option: IKeywordOption) => {
|
||||
const s = subStr;
|
||||
if (!option) {
|
||||
return s;
|
||||
}
|
||||
const { tagName = 'mark', bgColor, color, style = {}, renderHighlightKeyword } = option;
|
||||
if (typeof renderHighlightKeyword === 'function') {
|
||||
return renderHighlightKeyword(subStr);
|
||||
}
|
||||
style.backgroundColor = bgColor;
|
||||
style.color = color;
|
||||
|
||||
const styleContent = Object.keys(style)
|
||||
.map((k) => `${humpToLine(k)}:${style[k]}`)
|
||||
.join(';');
|
||||
const styleStr = `style="${styleContent}"`;
|
||||
return `<${tagName} ${styleStr}>${s}</${tagName}>`;
|
||||
};
|
||||
|
||||
const renderHighlightHtml = (content: string, list: any[]) => {
|
||||
let str = '';
|
||||
list.forEach((item) => {
|
||||
const { start, end, option } = item;
|
||||
const s = content.slice(start, end);
|
||||
const subStr = renderNodeTag(s, option);
|
||||
str += subStr;
|
||||
item.subString = subStr;
|
||||
});
|
||||
return str;
|
||||
};
|
||||
|
||||
// 解析关键词为索引
|
||||
const parseHighlightIndex = (content: string, keywords: IKeyword[]) => {
|
||||
const result: IKeywordParseIndex[] = [];
|
||||
keywords.forEach((keywordOption: IKeyword) => {
|
||||
let option: IKeywordOption = { keyword: '' };
|
||||
if (typeof keywordOption === 'string') {
|
||||
option = { keyword: keywordOption };
|
||||
} else {
|
||||
option = keywordOption;
|
||||
}
|
||||
const { keyword, caseSensitive = true } = option;
|
||||
const indexList = getKeywordIndexList(content, keyword, caseSensitive ? 'g' : 'gi');
|
||||
const res = {
|
||||
keyword,
|
||||
indexList,
|
||||
option,
|
||||
};
|
||||
result.push(res);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const parseHighlightString = (content: string, keywords: IKeyword[]) => {
|
||||
const result = parseHighlightIndex(content, keywords);
|
||||
const splitList: IKeywordParseResult[] = [];
|
||||
const findSplitIndex = (index: number, len: number) => {
|
||||
for (let i = 0; i < splitList.length; i++) {
|
||||
const cur = splitList[i];
|
||||
// 有交集
|
||||
if (
|
||||
(index > cur.start && index < cur.end) ||
|
||||
(index + len > cur.start && index + len < cur.end) ||
|
||||
(cur.start > index && cur.start < index + len) ||
|
||||
(cur.end > index && cur.end < index + len) ||
|
||||
(index === cur.start && index + len === cur.end)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
// 没有交集,且在当前的前面
|
||||
if (index + len <= cur.start) {
|
||||
return i;
|
||||
}
|
||||
// 没有交集,且在当前的后面的,放在下个迭代处理
|
||||
}
|
||||
return splitList.length;
|
||||
};
|
||||
result.forEach(({ indexList, option }: IKeywordParseIndex) => {
|
||||
indexList.forEach((e) => {
|
||||
const { index, subString } = e;
|
||||
const item = {
|
||||
start: index,
|
||||
end: index + subString.length,
|
||||
option,
|
||||
};
|
||||
const splitIndex = findSplitIndex(index, subString.length);
|
||||
if (splitIndex !== -1) {
|
||||
splitList.splice(splitIndex, 0, item);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 补上没有匹配关键词的部分
|
||||
const list: IKeywordParseResult[] = [];
|
||||
splitList.forEach((cur, i) => {
|
||||
const { start, end } = cur;
|
||||
const next = splitList[i + 1];
|
||||
// 第一个前面补一个
|
||||
if (i === 0 && start > 0) {
|
||||
list.push({ start: 0, end: start, subString: content.slice(0, start) });
|
||||
}
|
||||
list.push({ ...cur, subString: content.slice(start, end) });
|
||||
// 当前和下一个中间补一个
|
||||
if (next?.start > end) {
|
||||
list.push({
|
||||
start: end,
|
||||
end: next.start,
|
||||
subString: content.slice(end, next.start),
|
||||
});
|
||||
}
|
||||
// 最后一个后面补一个
|
||||
if (i === splitList.length - 1 && end < content.length - 1) {
|
||||
list.push({
|
||||
start: end,
|
||||
end: content.length - 1,
|
||||
subString: content.slice(end, content.length),
|
||||
});
|
||||
}
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
// 生成关键词高亮的html字符串
|
||||
const highHtml = (content: string, keywords: IKeyword[]) => {
|
||||
const splitList = parseHighlightString(content, keywords);
|
||||
return {
|
||||
highText: renderHighlightHtml(content, splitList),
|
||||
highList: splitList,
|
||||
};
|
||||
};
|
||||
|
||||
export default highHtml;
|
||||
@@ -0,0 +1,186 @@
|
||||
// @ts-ignore
|
||||
import onerrorImg from '@/assets/images/onerror.png';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { ActionItem } from '@/components/Table';
|
||||
import { isBoolean, isFunction } from '@/utils/is';
|
||||
import { PermissionsEnum } from '@/enums/permissionsEnum';
|
||||
|
||||
/**
|
||||
* @deprecated 这个方法将来未来版本完全弃用,请统一使用useDictStore()来维护字典选项
|
||||
*/
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: string | number;
|
||||
key: string | number;
|
||||
// type: string;
|
||||
listClass: 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning';
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 这个方法将来未来版本完全弃用,请统一使用useDictStore()来维护字典选项
|
||||
*/
|
||||
export interface Options {
|
||||
[name: string]: Option[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选项名称
|
||||
* @deprecated 这个方法将来未来版本完全弃用,请统一使用useDictStore()来维护字典选项
|
||||
*/
|
||||
export function getOptionLabel(options: Option[], value) {
|
||||
if (options === undefined || options?.length === 0) {
|
||||
return `unknown`;
|
||||
}
|
||||
for (const item of options) {
|
||||
if (item.value == value) {
|
||||
return item.label;
|
||||
}
|
||||
}
|
||||
|
||||
return `unknown`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选项标签
|
||||
* @deprecated 这个方法将来未来版本完全弃用,请统一使用useDictStore()来维护字典选项
|
||||
*/
|
||||
export function getOptionTag(options: Option[], value) {
|
||||
if (options === undefined || options?.length === 0) {
|
||||
return 'default';
|
||||
}
|
||||
for (const item of options) {
|
||||
if (item.value == value) {
|
||||
return item.listClass;
|
||||
}
|
||||
}
|
||||
|
||||
return 'default';
|
||||
}
|
||||
|
||||
// 自适应模板宽度
|
||||
export function adaModalWidth(def = 840) {
|
||||
const val = document.body.clientWidth;
|
||||
if (val <= def) {
|
||||
return '100%';
|
||||
} else {
|
||||
return def + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
export interface TableColumn {
|
||||
width?: number | string;
|
||||
auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
|
||||
ifShow?: boolean | ((action: ActionItem) => boolean);
|
||||
}
|
||||
|
||||
// 自适应表格组件横向滑动可见宽度
|
||||
export function adaTableScrollX(columns: TableColumn[] = [], actionWidth: number) {
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
let x = 50; // 勾选列宽度
|
||||
columns = columns.filter((column) => {
|
||||
return hasPermission(column.auth as string[]) && isIfShow(column);
|
||||
});
|
||||
for (const column of columns) {
|
||||
if (column.width && Number(column.width) >= 1) {
|
||||
x += Number(column.width);
|
||||
} else {
|
||||
x += 100; // 默认列宽度
|
||||
}
|
||||
}
|
||||
x += actionWidth;
|
||||
return x;
|
||||
}
|
||||
|
||||
export function isIfShow(action: ActionItem): boolean {
|
||||
let isIfShow = true;
|
||||
const ifShow = action.ifShow;
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
// 图片加载失败显示自定义默认图片(缺省图) img标签使用
|
||||
export function errorImg(e: any): void {
|
||||
e.target.src = onerrorImg;
|
||||
e.target.onerror = null;
|
||||
}
|
||||
|
||||
// 图片加载失败显示自定义默认图片(缺省图) NImage组件使用
|
||||
export function fallbackSrc(): string {
|
||||
return onerrorImg;
|
||||
}
|
||||
|
||||
export function timeFix() {
|
||||
const time = new Date();
|
||||
const hour = time.getHours();
|
||||
return hour < 9
|
||||
? '早上好'
|
||||
: hour <= 11
|
||||
? '上午好'
|
||||
: hour <= 13
|
||||
? '中午好'
|
||||
: hour < 20
|
||||
? '下午好'
|
||||
: '晚上好';
|
||||
}
|
||||
|
||||
// 随机浅色
|
||||
export function rdmLightRgbColor(): string {
|
||||
const letters = '456789ABCDEF';
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (i === 0) {
|
||||
color += 'F'; // 确保第一个字符较亮
|
||||
} else {
|
||||
color += letters[Math.floor(Math.random() * letters.length)];
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// 将列表数据转为树形数据
|
||||
export function convertListToTree(list: any[], idField = 'id', pidField = 'pid') {
|
||||
if (!list || list.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const min = list.reduce((prev, current) => (prev[pidField] < current[pidField] ? prev : current));
|
||||
|
||||
const map = list.reduce((acc, item) => {
|
||||
acc[item[idField]] = { ...item, children: [] };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
list.forEach((item) => {
|
||||
if (item[pidField] !== min[pidField]) {
|
||||
if (!map[item[pidField]]) {
|
||||
map[item[pidField]] = {};
|
||||
}
|
||||
|
||||
if (!map[item[pidField]].children) {
|
||||
map[item[pidField]].children = [];
|
||||
}
|
||||
map[item[pidField]].children.push(map[item[idField]]);
|
||||
}
|
||||
});
|
||||
return list.filter((item) => item[pidField] === min[pidField]).map((item) => map[item[idField]]);
|
||||
}
|
||||
|
||||
// 从树选项中获取所有key
|
||||
export function getTreeKeys(data: any[], idField = 'id') {
|
||||
const keys: any = [];
|
||||
if (!data || data.length === 0) {
|
||||
return keys;
|
||||
}
|
||||
data.map((item: any) => {
|
||||
keys.push(item[idField]);
|
||||
if (item.children && item.children.length) {
|
||||
keys.push(...getTreeKeys(item.children));
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { AxiosCanceler } from './axiosCancel';
|
||||
import { isFunction, isString, isUrl } from '@/utils/is';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
|
||||
import { ContentTypeEnum } from '@/enums/httpEnum';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
|
||||
export * from './axiosTransform';
|
||||
|
||||
/**
|
||||
* @description: axios模块
|
||||
*/
|
||||
export class VAxios {
|
||||
private axiosInstance: AxiosInstance;
|
||||
private options: CreateAxiosOptions;
|
||||
|
||||
constructor(options: CreateAxiosOptions) {
|
||||
this.options = options;
|
||||
this.axiosInstance = axios.create(options);
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
getAxios(): AxiosInstance {
|
||||
return this.axiosInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 重新配置axios
|
||||
*/
|
||||
configAxios(config: CreateAxiosOptions) {
|
||||
if (!this.axiosInstance) {
|
||||
return;
|
||||
}
|
||||
this.createAxios(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 设置通用header
|
||||
*/
|
||||
setHeader(headers: any): void {
|
||||
if (!this.axiosInstance) {
|
||||
return;
|
||||
}
|
||||
Object.assign(this.axiosInstance.defaults.headers, headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求方法
|
||||
*/
|
||||
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
let conf: AxiosRequestConfig = cloneDeep(config);
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt: RequestOptions = Object.assign({}, requestOptions, options);
|
||||
|
||||
const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||
conf = beforeRequestHook(conf, opt);
|
||||
}
|
||||
|
||||
//这里重新 赋值成最新的配置
|
||||
// @ts-ignore
|
||||
conf.requestOptions = opt;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<any, AxiosResponse<Result>>(conf)
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
// 请求是否被取消
|
||||
const isCancel = axios.isCancel(res);
|
||||
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
|
||||
try {
|
||||
const ret = transformRequestData(res, opt);
|
||||
resolve(ret);
|
||||
} catch (err) {
|
||||
reject(err || new Error('request error!'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (requestCatch && isFunction(requestCatch)) {
|
||||
reject(requestCatch(e));
|
||||
return;
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 创建axios实例
|
||||
*/
|
||||
private createAxios(config: CreateAxiosOptions): void {
|
||||
this.axiosInstance = axios.create(config);
|
||||
}
|
||||
|
||||
private getTransform() {
|
||||
const { transform } = this.options;
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 文件上传
|
||||
*/
|
||||
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams, options?: RequestOptions) {
|
||||
const transform = this.getTransform();
|
||||
const { requestCatch, transformRequestData } = transform || {};
|
||||
const { requestOptions } = this.options;
|
||||
const opt: RequestOptions = Object.assign({}, requestOptions, options);
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix || '';
|
||||
const apiUrl = globSetting.apiUrl || '';
|
||||
const isUrlStr = isUrl(config.url as string);
|
||||
|
||||
if (!isUrlStr) {
|
||||
config.url = `${urlPrefix}${config.url}`;
|
||||
}
|
||||
|
||||
if (!isUrlStr && apiUrl && isString(apiUrl)) {
|
||||
config.url = `${apiUrl}${config.url}`;
|
||||
}
|
||||
|
||||
const formData = new window.FormData();
|
||||
if (params) {
|
||||
Object.keys(params).forEach((key) => {
|
||||
const value = params![key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
formData.append(`${key}[]`, item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(key, params![key]);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<T>({
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||
ignoreCancelToken: true,
|
||||
},
|
||||
...config,
|
||||
})
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
// 请求是否被取消
|
||||
const isCancel = axios.isCancel(res);
|
||||
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
|
||||
try {
|
||||
const ret = transformRequestData(res, opt);
|
||||
resolve(ret);
|
||||
} catch (err) {
|
||||
reject(err || new Error('request error!'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (requestCatch && isFunction(requestCatch)) {
|
||||
reject(requestCatch(e));
|
||||
return;
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 拦截器配置
|
||||
*/
|
||||
private setupInterceptors() {
|
||||
const transform = this.getTransform();
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
requestInterceptors,
|
||||
requestInterceptorsCatch,
|
||||
responseInterceptors,
|
||||
responseInterceptorsCatch,
|
||||
} = transform;
|
||||
|
||||
const axiosCanceler = new AxiosCanceler();
|
||||
|
||||
// 请求拦截器配置处理
|
||||
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
|
||||
const {
|
||||
headers: { ignoreCancelToken },
|
||||
} = config;
|
||||
const ignoreCancel =
|
||||
ignoreCancelToken !== undefined
|
||||
? ignoreCancelToken
|
||||
: this.options.requestOptions?.ignoreCancelToken;
|
||||
|
||||
!ignoreCancel && axiosCanceler.addPending(config);
|
||||
if (requestInterceptors && isFunction(requestInterceptors)) {
|
||||
config = requestInterceptors(config, this.options);
|
||||
}
|
||||
return config;
|
||||
}, undefined);
|
||||
|
||||
// 请求拦截器错误捕获
|
||||
requestInterceptorsCatch &&
|
||||
isFunction(requestInterceptorsCatch) &&
|
||||
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
|
||||
|
||||
// 响应结果拦截器处理
|
||||
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
|
||||
res && axiosCanceler.removePending(res.config);
|
||||
if (responseInterceptors && isFunction(responseInterceptors)) {
|
||||
res = responseInterceptors(res);
|
||||
}
|
||||
return res;
|
||||
}, undefined);
|
||||
|
||||
// 响应结果拦截器错误捕获
|
||||
responseInterceptorsCatch &&
|
||||
isFunction(responseInterceptorsCatch) &&
|
||||
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios, { AxiosRequestConfig, Canceler } from 'axios';
|
||||
import qs from 'qs';
|
||||
|
||||
import { isFunction } from '@/utils/is/index';
|
||||
|
||||
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
|
||||
let pendingMap = new Map<string, Canceler>();
|
||||
|
||||
export const getPendingUrl = (config: AxiosRequestConfig) =>
|
||||
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join('&');
|
||||
|
||||
export class AxiosCanceler {
|
||||
/**
|
||||
* 添加请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
addPending(config: AxiosRequestConfig) {
|
||||
this.removePending(config);
|
||||
const url = getPendingUrl(config);
|
||||
config.cancelToken =
|
||||
config.cancelToken ||
|
||||
new axios.CancelToken((cancel) => {
|
||||
if (!pendingMap.has(url)) {
|
||||
// 如果 pending 中不存在当前请求,则添加进去
|
||||
pendingMap.set(url, cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 清空所有pending
|
||||
*/
|
||||
removeAllPending() {
|
||||
pendingMap.forEach((cancel) => {
|
||||
cancel && isFunction(cancel) && cancel();
|
||||
});
|
||||
pendingMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
removePending(config: AxiosRequestConfig) {
|
||||
const url = getPendingUrl(config);
|
||||
|
||||
if (pendingMap.has(url)) {
|
||||
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
|
||||
const cancel = pendingMap.get(url);
|
||||
cancel && cancel(url);
|
||||
pendingMap.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 重置
|
||||
*/
|
||||
reset(): void {
|
||||
pendingMap = new Map<string, Canceler>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 数据处理类,可以根据项目自行配置
|
||||
*/
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import type { RequestOptions, Result } from './types';
|
||||
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||
authenticationScheme?: string;
|
||||
transform?: AxiosTransform;
|
||||
requestOptions?: RequestOptions;
|
||||
}
|
||||
|
||||
export abstract class AxiosTransform {
|
||||
/**
|
||||
* @description: 请求之前处理配置
|
||||
* @description: Process configuration before request
|
||||
*/
|
||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 请求成功处理
|
||||
*/
|
||||
transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
|
||||
|
||||
/**
|
||||
* @description: 请求失败处理
|
||||
*/
|
||||
requestCatch?: (e: Error) => Promise<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器
|
||||
*/
|
||||
requestInterceptors?: (
|
||||
config: AxiosRequestConfig,
|
||||
options: CreateAxiosOptions
|
||||
) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器
|
||||
*/
|
||||
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器错误处理
|
||||
*/
|
||||
requestInterceptorsCatch?: (error: Error) => void;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器错误处理
|
||||
*/
|
||||
responseInterceptorsCatch?: (error: Error) => void;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export function checkStatus(status: number, msg: string): void {
|
||||
const $message = window['$message'];
|
||||
switch (status) {
|
||||
case 400:
|
||||
$message.error(msg);
|
||||
break;
|
||||
// 401: 未登录
|
||||
// 未登录则跳转登录页面,并携带当前页面的路径
|
||||
// 在登录成功后返回当前页面,这一步需要在登录页操作。
|
||||
case 61:
|
||||
$message.error('用户没有权限(令牌、用户名、密码错误)!');
|
||||
break;
|
||||
case 403:
|
||||
$message.error('用户得到授权,但是访问是被禁止的。!');
|
||||
break;
|
||||
// 404请求不存在
|
||||
case 404:
|
||||
$message.error('网络请求错误,未找到该资源!');
|
||||
break;
|
||||
case 405:
|
||||
$message.error('网络请求错误,请求方法未允许!');
|
||||
break;
|
||||
case 408:
|
||||
$message.error('网络请求超时');
|
||||
break;
|
||||
case 500:
|
||||
$message.error('服务器错误,请稍候重试!');
|
||||
break;
|
||||
case 501:
|
||||
$message.error('网络未实现');
|
||||
break;
|
||||
case 502:
|
||||
$message.error('网络错误');
|
||||
break;
|
||||
case 503:
|
||||
$message.error('服务不可用,服务器暂时过载或维护!');
|
||||
break;
|
||||
case 504:
|
||||
$message.error('网络超时');
|
||||
break;
|
||||
case 505:
|
||||
$message.error('http版本不支持该请求!');
|
||||
break;
|
||||
default:
|
||||
$message.error(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { isObject, isString } from '@/utils/is';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
export function joinTimestamp<T extends boolean>(
|
||||
join: boolean,
|
||||
restful: T
|
||||
): T extends true ? string : object;
|
||||
|
||||
export function joinTimestamp(join: boolean, restful = false): string | object {
|
||||
if (!join) {
|
||||
return restful ? '' : {};
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
if (restful) {
|
||||
return `?_t=${now}`;
|
||||
}
|
||||
return { _t: now };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Format request parameter time
|
||||
*/
|
||||
export function formatRequestDate(params: Recordable) {
|
||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in params) {
|
||||
if (params[key] && params[key]._isAMomentObject) {
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT);
|
||||
}
|
||||
if (isString(key)) {
|
||||
const value = params[key];
|
||||
if (value) {
|
||||
try {
|
||||
params[key] = isString(value) ? value.trim() : value;
|
||||
} catch (error) {
|
||||
throw new Error(error as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isObject(params[key])) {
|
||||
formatRequestDate(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
|
||||
import { VAxios } from './Axios';
|
||||
import { AxiosTransform } from './axiosTransform';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { checkStatus } from './checkStatus';
|
||||
import { formatRequestDate, joinTimestamp } from './helper';
|
||||
import { ContentTypeEnum, RequestEnum, ResultEnum } from '@/enums/httpEnum';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { isString, isUrl } from '@/utils/is/';
|
||||
import { deepMerge } from '@/utils';
|
||||
import { setObjToUrlParams } from '@/utils/urlUtils';
|
||||
import { CreateAxiosOptions, RequestOptions, Result } from './types';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
import router from '@/router';
|
||||
import { storage } from '@/utils/Storage';
|
||||
import { encodeParams } from '@/utils/urlUtils';
|
||||
import { delNullProperty } from '@/utils/array';
|
||||
import { useI18nStore } from '@/store/modules/i18n';
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix || '';
|
||||
|
||||
/**
|
||||
* @description: 数据处理,方便区分多种处理方式
|
||||
*/
|
||||
const transform: AxiosTransform = {
|
||||
/**
|
||||
* @description: 处理请求数据
|
||||
*/
|
||||
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||
const {
|
||||
isShowMessage = true,
|
||||
isShowErrorMessage,
|
||||
isShowSuccessMessage,
|
||||
successMessageText,
|
||||
errorMessageText,
|
||||
isTransformResponse,
|
||||
isReturnNativeResponse,
|
||||
} = options;
|
||||
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
if (isReturnNativeResponse) {
|
||||
return res;
|
||||
}
|
||||
// 不进行任何处理,直接返回
|
||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||
if (!isTransformResponse) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
const response = res.data;
|
||||
const $dialog = window['$dialog'];
|
||||
const $message = window['$message'];
|
||||
|
||||
if (!response) {
|
||||
// return '[HTTP] Request has no return value';
|
||||
throw new Error('请求出错,请稍候重试');
|
||||
}
|
||||
|
||||
// 这里 code,result,message为 后台统一的字段,需要修改为项目自己的接口返回格式
|
||||
const { code, data, message } = response;
|
||||
|
||||
// 请求成功
|
||||
const hasSuccess = response && Reflect.has(response, 'code') && code === ResultEnum.SUCCESS;
|
||||
// 是否显示提示信息
|
||||
if (isShowMessage) {
|
||||
if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
|
||||
// 是否显示自定义信息提示
|
||||
$dialog.success({
|
||||
type: 'success',
|
||||
content: successMessageText || message || '操作成功!',
|
||||
});
|
||||
} else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
|
||||
// 是否显示自定义信息提示
|
||||
$message.error(message || errorMessageText || '操作失败!');
|
||||
} else if (!hasSuccess && options.errorMessageMode === 'modal') {
|
||||
// errorMessageMode=‘custom-modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||
$dialog.info({
|
||||
title: '提示',
|
||||
content: message,
|
||||
positiveText: '确定',
|
||||
onPositiveClick: () => {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 接口请求成功,直接返回结果
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
return data;
|
||||
}
|
||||
// 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
|
||||
let errorMsg = message;
|
||||
switch (code) {
|
||||
// 请求失败
|
||||
case ResultEnum.ERROR:
|
||||
$message.error(errorMsg);
|
||||
break;
|
||||
// 登录超时
|
||||
case ResultEnum.TIMEOUT:
|
||||
const LoginName = PageEnum.BASE_LOGIN_NAME;
|
||||
const LoginPath = PageEnum.BASE_LOGIN;
|
||||
if (router.currentRoute.value.name === LoginName) return;
|
||||
// 到登录页
|
||||
errorMsg = message ?? '登录超时,请重新登录!';
|
||||
$dialog.warning({
|
||||
title: '提示',
|
||||
content: errorMsg, // '登录身份已失效,请重新登录!',
|
||||
positiveText: '确定',
|
||||
//negativeText: '取消',
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
onPositiveClick: () => {
|
||||
storage.clear();
|
||||
window.location.href = LoginPath;
|
||||
},
|
||||
onNegativeClick: () => {},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log('unknown status code:' + code);
|
||||
$message.error(errorMsg);
|
||||
}
|
||||
|
||||
throw new Error(errorMsg);
|
||||
},
|
||||
|
||||
// 请求之前处理config
|
||||
beforeRequestHook: (config, options) => {
|
||||
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
|
||||
|
||||
const isUrlStr = isUrl(config.url as string);
|
||||
|
||||
if (!isUrlStr && joinPrefix) {
|
||||
config.url = `${urlPrefix}${config.url}`;
|
||||
}
|
||||
|
||||
if (!isUrlStr && apiUrl && isString(apiUrl)) {
|
||||
config.url = `${apiUrl}${config.url}`;
|
||||
}
|
||||
const params = config.params || {};
|
||||
const data = config.data || false;
|
||||
if (config.method?.toUpperCase() === RequestEnum.GET) {
|
||||
if (!isString(params)) {
|
||||
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
|
||||
} else {
|
||||
// 兼容restful风格
|
||||
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
|
||||
config.params = undefined;
|
||||
}
|
||||
} else {
|
||||
if (!isString(params)) {
|
||||
formatDate && formatRequestDate(params);
|
||||
if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
|
||||
config.data = data;
|
||||
config.params = params;
|
||||
} else {
|
||||
config.data = params;
|
||||
config.params = undefined;
|
||||
}
|
||||
if (joinParamsToUrl) {
|
||||
config.url = setObjToUrlParams(
|
||||
config.url as string,
|
||||
Object.assign({}, config.params, config.data)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 兼容restful风格
|
||||
config.url = config.url + params;
|
||||
config.params = undefined;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 请求拦截器处理
|
||||
*/
|
||||
requestInterceptors: (config, options) => {
|
||||
// 请求之前处理config
|
||||
const i18nStore = useI18nStore();
|
||||
const userStore = useUserStoreWidthOut();
|
||||
const token = userStore.getToken;
|
||||
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
|
||||
// jwt token
|
||||
(config as Recordable).headers.Authorization = options.authenticationScheme
|
||||
? `${options.authenticationScheme} ${token}`
|
||||
: token;
|
||||
}
|
||||
config.headers.Locale = i18nStore.getLocale();
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 响应错误处理
|
||||
*/
|
||||
responseInterceptorsCatch: (error: any) => {
|
||||
const $dialog = window['$dialog'];
|
||||
const $message = window['$message'];
|
||||
const { response, code, message } = error || {};
|
||||
// TODO 此处要根据后端接口返回格式修改
|
||||
const msg: string =
|
||||
response && response.data && response.data.message ? response.data.message : '';
|
||||
const err: string = error.toString();
|
||||
try {
|
||||
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
|
||||
$message.error('接口请求超时,请刷新页面重试!');
|
||||
return;
|
||||
}
|
||||
if (err && err.includes('Network Error')) {
|
||||
$dialog.info({
|
||||
title: '网络异常',
|
||||
content: '请检查您的网络连接是否正常',
|
||||
positiveText: '确定',
|
||||
//negativeText: '取消',
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
onPositiveClick: () => {},
|
||||
onNegativeClick: () => {},
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error as any);
|
||||
}
|
||||
// 请求是否被取消
|
||||
const isCancel = axios.isCancel(error);
|
||||
if (!isCancel) {
|
||||
checkStatus(error.response && error.response.status, msg);
|
||||
} else {
|
||||
console.warn(error, '请求被取消!');
|
||||
}
|
||||
//return Promise.reject(error);
|
||||
return Promise.reject(response?.data);
|
||||
},
|
||||
};
|
||||
|
||||
function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||
return new VAxios(
|
||||
deepMerge(
|
||||
{
|
||||
timeout: 30 * 1000,
|
||||
authenticationScheme: '',
|
||||
// 接口前缀
|
||||
prefixUrl: urlPrefix,
|
||||
headers: { 'Content-Type': ContentTypeEnum.JSON },
|
||||
// 数据处理方式
|
||||
transform,
|
||||
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||
requestOptions: {
|
||||
// 默认将prefix 添加到url
|
||||
joinPrefix: true,
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
isReturnNativeResponse: false,
|
||||
// 需要对返回数据进行处理
|
||||
isTransformResponse: true,
|
||||
// post请求的时候添加参数到url
|
||||
joinParamsToUrl: false,
|
||||
// 格式化提交参数时间
|
||||
formatDate: true,
|
||||
// 消息提示类型
|
||||
errorMessageMode: 'none',
|
||||
// 接口地址
|
||||
apiUrl: globSetting.apiUrl,
|
||||
// 接口拼接地址
|
||||
urlPrefix: urlPrefix,
|
||||
// 是否加入时间戳
|
||||
joinTime: false,
|
||||
// 忽略重复请求
|
||||
ignoreCancelToken: true,
|
||||
// 是否携带token
|
||||
withToken: true,
|
||||
},
|
||||
withCredentials: false,
|
||||
},
|
||||
opt || {}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const http = createAxios();
|
||||
|
||||
// 导出
|
||||
export const jumpExport = function (url, params) {
|
||||
jump(url, params);
|
||||
};
|
||||
|
||||
// 跳转
|
||||
export const jump = function (url, params) {
|
||||
window.location.href =
|
||||
urlPrefix +
|
||||
url +
|
||||
'?' +
|
||||
encodeParams({
|
||||
...delNullProperty(params),
|
||||
...{ authorization: useUserStoreWidthOut().token },
|
||||
});
|
||||
};
|
||||
|
||||
// 项目,多个不同 api 地址,直接在这里导出多个
|
||||
// src/api ts 里面接口,就可以单独使用这个请求,
|
||||
// import { httpTwo } from '@/utils/http/axios'
|
||||
// export const httpTwo = createAxios({
|
||||
// requestOptions: {
|
||||
// apiUrl: 'http://localhost:9001',
|
||||
// urlPrefix: 'api',
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,65 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { AxiosTransform } from './axiosTransform';
|
||||
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||
transform?: AxiosTransform;
|
||||
requestOptions?: RequestOptions;
|
||||
authenticationScheme?: string;
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
export interface UploadFileParams {
|
||||
// 其他参数
|
||||
data?: Recordable;
|
||||
// 文件参数接口字段名
|
||||
name?: string;
|
||||
// 文件
|
||||
file: File | Blob;
|
||||
// 文件名称
|
||||
filename?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
// 请求参数拼接到url
|
||||
joinParamsToUrl?: boolean;
|
||||
// 格式化请求参数时间
|
||||
formatDate?: boolean;
|
||||
// 是否显示提示信息
|
||||
isShowMessage?: boolean;
|
||||
// 是否解析成JSON
|
||||
isParseToJson?: boolean;
|
||||
// 成功的文本信息
|
||||
successMessageText?: string;
|
||||
// 是否显示成功信息
|
||||
isShowSuccessMessage?: boolean;
|
||||
// 是否显示失败信息
|
||||
isShowErrorMessage?: boolean;
|
||||
// 错误的文本信息
|
||||
errorMessageText?: string;
|
||||
// 是否加入url
|
||||
joinPrefix?: boolean;
|
||||
// 接口地址, 不填则使用默认apiUrl
|
||||
apiUrl?: string;
|
||||
// 请求拼接路径
|
||||
urlPrefix?: string;
|
||||
// 错误消息提示类型
|
||||
errorMessageMode?: 'none' | 'modal';
|
||||
// 是否添加时间戳
|
||||
joinTime?: boolean;
|
||||
// 不进行任何处理,直接返回
|
||||
isTransformResponse?: boolean;
|
||||
// 是否返回原生响应头
|
||||
isReturnNativeResponse?: boolean;
|
||||
//忽略重复请求
|
||||
ignoreCancelToken?: boolean;
|
||||
// 是否携带token
|
||||
withToken?: boolean;
|
||||
}
|
||||
|
||||
export interface Result<T = any> {
|
||||
code: number;
|
||||
type?: 'success' | 'error' | 'warning';
|
||||
message: string;
|
||||
data?: T;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component } from 'vue';
|
||||
import { renderIcon } from '@/utils/index';
|
||||
import * as antdIcons from '@vicons/antd';
|
||||
// import * as x5Icons from '@vicons/ionicons5';
|
||||
|
||||
export const Icons = {};
|
||||
for (const element of Object.keys(antdIcons)) {
|
||||
Icons[element] = renderIcon(antdIcons[element]);
|
||||
}
|
||||
|
||||
// for (const element of Object.keys(x5Icons)) {
|
||||
// constantRouterIcon[element] = renderIcon(x5Icons[element]);
|
||||
// }
|
||||
|
||||
export function getIconComponent(name: string): Component {
|
||||
return Icons[name];
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
import { h, unref } from 'vue';
|
||||
import type { App, Plugin } from 'vue';
|
||||
import {
|
||||
NAvatar,
|
||||
NBadge,
|
||||
NButton,
|
||||
NIcon,
|
||||
NImage,
|
||||
NPopover,
|
||||
NTable,
|
||||
NTag,
|
||||
NTooltip,
|
||||
SelectRenderTag,
|
||||
} from 'naive-ui';
|
||||
import { EllipsisHorizontalCircleOutline } from '@vicons/ionicons5';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { isArray, isJsonString, isNullObject, isObject } from './is/index';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { VNode } from 'vue';
|
||||
import { DictType, useDictStore } from '@/store/modules/dict';
|
||||
import { fallbackSrc } from '@/utils/hotgo';
|
||||
import { getFileExt } from '@/utils/urlUtils';
|
||||
|
||||
export const renderTooltip = (trigger, content) => {
|
||||
return h(NTooltip, null, {
|
||||
trigger: () => trigger,
|
||||
default: () => content,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* render 图标
|
||||
* */
|
||||
export function renderIcon(icon, props = null) {
|
||||
return () => h(NIcon, props, { default: () => h(icon) });
|
||||
}
|
||||
|
||||
/**
|
||||
* render new Tag
|
||||
* */
|
||||
const newTagColors = { color: '#f90', textColor: '#fff', borderColor: '#f90' };
|
||||
|
||||
export function renderNew(type = 'warning', text = 'New', color: object = newTagColors) {
|
||||
return () =>
|
||||
h(
|
||||
NTag as any,
|
||||
{
|
||||
type,
|
||||
round: true,
|
||||
size: 'small',
|
||||
color,
|
||||
},
|
||||
{ default: () => text }
|
||||
);
|
||||
}
|
||||
|
||||
// render 标记
|
||||
export function renderBadge(node: VNode) {
|
||||
return h(
|
||||
NBadge,
|
||||
{
|
||||
dot: true,
|
||||
type: 'info',
|
||||
},
|
||||
{ default: () => node }
|
||||
);
|
||||
}
|
||||
|
||||
// render 标签
|
||||
export const renderTag: SelectRenderTag = ({ option }) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: option.listClass as 'success' | 'warning' | 'error' | 'info' | 'primary' | 'default',
|
||||
},
|
||||
{ default: () => option.label }
|
||||
);
|
||||
};
|
||||
|
||||
// renderOptionTag 选项标签
|
||||
export const renderOptionTag = (type: DictType, value: any) => {
|
||||
if (isNullObject(value)) {
|
||||
return ``;
|
||||
}
|
||||
const dict = useDictStore();
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
style: {
|
||||
marginRight: '6px',
|
||||
},
|
||||
type: dict.getType(type, value),
|
||||
bordered: false,
|
||||
},
|
||||
{
|
||||
default: () => dict.getLabel(type, value),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// render 图片
|
||||
export const renderImage = (image: string) => {
|
||||
if (!image || image === '') {
|
||||
return ``;
|
||||
}
|
||||
return h(NImage, {
|
||||
width: 32,
|
||||
height: 32,
|
||||
src: image,
|
||||
fallbackSrc: fallbackSrc(),
|
||||
style: {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
'max-width': '100%',
|
||||
'max-height': '100%',
|
||||
'margin-left': '2px',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// render 图片组
|
||||
export const renderImageGroup = (images: any) => {
|
||||
if (isJsonString(images)) {
|
||||
images = JSON.parse(images);
|
||||
}
|
||||
if (isNullObject(images) || !isArray(images)) {
|
||||
return ``;
|
||||
}
|
||||
return images.map((image: string) => {
|
||||
return renderImage(image);
|
||||
});
|
||||
};
|
||||
|
||||
// render 文件
|
||||
export const renderFile = (file: string) => {
|
||||
if (!file || file === '') {
|
||||
return ``;
|
||||
}
|
||||
return h(
|
||||
NAvatar,
|
||||
{
|
||||
size: 'small',
|
||||
style: {
|
||||
'margin-left': '2px',
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => getFileExt(file),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// render 文件组
|
||||
export const renderFileGroup = (files: any) => {
|
||||
if (isJsonString(files)) {
|
||||
files = JSON.parse(files);
|
||||
}
|
||||
if (isNullObject(files) || !isArray(files)) {
|
||||
return ``;
|
||||
}
|
||||
return files.map((file: string) => {
|
||||
return renderFile(file);
|
||||
});
|
||||
};
|
||||
|
||||
export interface MemberSumma {
|
||||
id: number; // 用户ID
|
||||
realName: string; // 真实姓名
|
||||
username: string; // 用户名
|
||||
avatar: string; // 头像
|
||||
}
|
||||
|
||||
// render 操作人摘要
|
||||
export const renderPopoverMemberSumma = (member: MemberSumma | null | undefined) => {
|
||||
if (!member) {
|
||||
return '';
|
||||
}
|
||||
return h(
|
||||
NPopover,
|
||||
{ trigger: 'hover' },
|
||||
{
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
text: true,
|
||||
iconPlacement: 'right',
|
||||
},
|
||||
{ default: () => member.realName, icon: renderIcon(EllipsisHorizontalCircleOutline) }
|
||||
),
|
||||
default: () =>
|
||||
h(
|
||||
NTable,
|
||||
{
|
||||
props: {
|
||||
bordered: false,
|
||||
'single-line': false,
|
||||
size: 'small',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('thead', [
|
||||
h('tr', { align: 'center' }, [
|
||||
h('th', '用户ID'),
|
||||
h('th', '头像'),
|
||||
h('th', '姓名'),
|
||||
h('th', '用户名'),
|
||||
]),
|
||||
]),
|
||||
h('tbody', [
|
||||
h('tr', { align: 'center' }, [
|
||||
h('td', member.id),
|
||||
h('td', h(NAvatar, { src: member.avatar, round: true, size: 'small' })),
|
||||
h('td', member.realName),
|
||||
h('td', member.username),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// render html
|
||||
export function renderHtmlTooltip(content: string) {
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
const html = h('p', { id: 'app' }, [
|
||||
h('div', {
|
||||
innerHTML: content,
|
||||
}),
|
||||
]);
|
||||
return renderTooltip(html, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归组装菜单格式
|
||||
*/
|
||||
export function generatorMenu(routerMap: Array<any>) {
|
||||
return filterRouter(routerMap).map((item) => {
|
||||
const isRoot = isRootRouter(item);
|
||||
const info = isRoot ? item.children[0] : item;
|
||||
const currentMenu = {
|
||||
...info,
|
||||
...info.meta,
|
||||
label: info.meta?.title,
|
||||
key: info.name,
|
||||
icon: isRoot ? item.meta?.icon : info.meta?.icon,
|
||||
};
|
||||
// 是否有子菜单,并递归处理
|
||||
if (info.children && info.children.length > 0) {
|
||||
// Recursion
|
||||
currentMenu.children = generatorMenu(info.children);
|
||||
|
||||
// 当生成后子集为空,则删除子集空数组,否则加载时仍为目录格式!
|
||||
if (currentMenu.children.length === 0) {
|
||||
delete currentMenu.children;
|
||||
}
|
||||
}
|
||||
return currentMenu;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合菜单
|
||||
* */
|
||||
export function generatorMenuMix(routerMap: Array<any>, routerName: string, location: string) {
|
||||
const cloneRouterMap = cloneDeep(routerMap);
|
||||
const newRouter = filterRouter(cloneRouterMap);
|
||||
if (location === 'header') {
|
||||
const firstRouter: any[] = [];
|
||||
newRouter.forEach((item) => {
|
||||
const isRoot = isRootRouter(item);
|
||||
const info = isRoot ? item.children[0] : item;
|
||||
info.children = undefined;
|
||||
const currentMenu = {
|
||||
...info,
|
||||
...info.meta,
|
||||
label: info.meta?.title,
|
||||
key: info.name,
|
||||
};
|
||||
firstRouter.push(currentMenu);
|
||||
});
|
||||
return firstRouter;
|
||||
} else {
|
||||
return getChildrenRouter(newRouter.filter((item) => item.name === routerName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归组装子菜单
|
||||
* */
|
||||
export function getChildrenRouter(routerMap: Array<any>) {
|
||||
return filterRouter(routerMap).map((item) => {
|
||||
const isRoot = isRootRouter(item);
|
||||
const info = isRoot ? item.children[0] : item;
|
||||
const currentMenu = {
|
||||
...info,
|
||||
...info.meta,
|
||||
label: info.meta?.title,
|
||||
key: info.name,
|
||||
};
|
||||
// 是否有子菜单,并递归处理
|
||||
if (info.children && info.children.length > 0) {
|
||||
// Recursion
|
||||
currentMenu.children = getChildrenRouter(info.children);
|
||||
}
|
||||
return currentMenu;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断根路由 Router
|
||||
* */
|
||||
export function isRootRouter(item) {
|
||||
if (item.meta?.alwaysShow != true && item.children?.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if (item.meta?.alwaysShow != true) {
|
||||
// if (item.children?.length > 0) {
|
||||
// // 如果存在子级。且只要有一个不是隐藏状态的,则判断不是跟路由
|
||||
// for (let i = 0; i < item.children.length; i++) {
|
||||
// if (item.children[i]?.hidden == false) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制根路由转换
|
||||
* @param item
|
||||
*/
|
||||
export function mandatoryRootConvert(item) {
|
||||
if (item.meta?.isRoot === true) {
|
||||
}
|
||||
|
||||
// 默认
|
||||
return item.children[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除Router
|
||||
* */
|
||||
export function filterRouter(routerMap: Array<any>) {
|
||||
return routerMap.filter((item) => {
|
||||
return (
|
||||
(item.meta?.hidden || false) != true &&
|
||||
!['/:path(.*)*', '/', PageEnum.REDIRECT, PageEnum.BASE_LOGIN].includes(item.path)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const withInstall = <T>(component: T, alias?: string) => {
|
||||
const comp = component as any;
|
||||
comp.install = (app: App) => {
|
||||
// @ts-ignore
|
||||
app.component(comp.name || comp.displayName, component);
|
||||
if (alias) {
|
||||
app.config.globalProperties[alias] = component;
|
||||
}
|
||||
};
|
||||
return component as T & Plugin;
|
||||
};
|
||||
|
||||
// dynamic use hook props
|
||||
export function getDynamicProps<T, U>(props: T): Partial<U> {
|
||||
const ret: Recordable = {};
|
||||
|
||||
// @ts-ignore
|
||||
Object.keys(props).map((key) => {
|
||||
ret[key] = unref((props as Recordable)[key]);
|
||||
});
|
||||
|
||||
return ret as Partial<U>;
|
||||
}
|
||||
|
||||
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
|
||||
let key: string;
|
||||
for (key in target) {
|
||||
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the passed percentage to the R, G or B of a HEX color
|
||||
* @param {string} color The color to change
|
||||
* @param {number} amount The amount to change the color by
|
||||
* @returns {string} The processed part of the color
|
||||
*/
|
||||
function addLight(color: string, amount: number) {
|
||||
const cc = parseInt(color, 16) + amount;
|
||||
const c = cc > 255 ? 255 : cc;
|
||||
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightens a 6 char HEX color according to the passed percentage
|
||||
* @param {string} color The color to change
|
||||
* @param {number} amount The amount to change the color by
|
||||
* @returns {string} The processed color represented as HEX
|
||||
*/
|
||||
export function lighten(color: string, amount: number) {
|
||||
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color;
|
||||
amount = Math.trunc((255 * amount) / 100);
|
||||
return `#${addLight(color.substring(0, 2), amount)}${addLight(
|
||||
color.substring(2, 4),
|
||||
amount
|
||||
)}${addLight(color.substring(4, 6), amount)}`;
|
||||
}
|
||||
|
||||
// 获取树的所有节点key
|
||||
export function getAllExpandKeys(treeData: any): any[] {
|
||||
let expandedKeys: any = [];
|
||||
const expandKeys = (items: any[]) => {
|
||||
items.forEach((item: any) => {
|
||||
expandedKeys.push(item.key);
|
||||
if (item.children && item.children.length > 0) {
|
||||
expandKeys(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
expandKeys(unref(treeData));
|
||||
|
||||
// 去重并转换为数组
|
||||
expandedKeys = Array.from(new Set(expandedKeys));
|
||||
return expandedKeys;
|
||||
}
|
||||
|
||||
// 从树中查找指定节点
|
||||
export function findTreeNode(data: any, key?: string | number, keyField = 'key'): any {
|
||||
for (const item of data) {
|
||||
if (item[keyField] == key) {
|
||||
return item;
|
||||
} else {
|
||||
if (item.children && item.children.length) {
|
||||
const foundItem = findTreeNode(item.children, key);
|
||||
if (foundItem) {
|
||||
return foundItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
const toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* @description: 判断值是否未某个类型
|
||||
*/
|
||||
export function is(val: unknown, type: string) {
|
||||
return toString.call(val) === `[object ${type}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为函数
|
||||
*/
|
||||
export function isFunction<T = Function>(val: unknown): val is T {
|
||||
return is(val, 'Function') || is(val, 'AsyncFunction');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否已定义
|
||||
*/
|
||||
export const isDef = <T = unknown>(val?: T): val is T => {
|
||||
return typeof val !== 'undefined';
|
||||
};
|
||||
|
||||
export const isUnDef = <T = unknown>(val?: T): val is T => {
|
||||
return !isDef(val);
|
||||
};
|
||||
/**
|
||||
* @description: 是否为对象
|
||||
*/
|
||||
export const isObject = (val: any): val is Record<any, any> => {
|
||||
return val !== null && is(val, 'Object');
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: 是否为时间
|
||||
*/
|
||||
export function isDate(val: unknown): val is Date {
|
||||
return is(val, 'Date');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为数值
|
||||
*/
|
||||
export function isNumber(val: unknown): val is number {
|
||||
return is(val, 'Number');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为AsyncFunction
|
||||
*/
|
||||
export function isAsyncFunction<T = any>(val: unknown): val is Promise<T> {
|
||||
return is(val, 'AsyncFunction');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为promise
|
||||
*/
|
||||
export function isPromise<T = any>(val: unknown): val is Promise<T> {
|
||||
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为字符串
|
||||
*/
|
||||
export function isString(val: unknown): val is string {
|
||||
return is(val, 'String');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为boolean类型
|
||||
*/
|
||||
export function isBoolean(val: unknown): val is boolean {
|
||||
return is(val, 'Boolean');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为数组
|
||||
*/
|
||||
export function isArray(val: any): val is Array<any> {
|
||||
return val && Array.isArray(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为字符串数组
|
||||
*/
|
||||
export function isArrayString(val: any): val is Array<any> {
|
||||
return Array.isArray(val) && val.every((item: any) => typeof item === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否为转为string的json
|
||||
*/
|
||||
export function isJsonString(value: any) {
|
||||
try {
|
||||
const toObj = JSON.parse(value);
|
||||
if (toObj && typeof toObj === 'object') {
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isNullObject(value: any) {
|
||||
return (
|
||||
isNullOrUnDef(value) ||
|
||||
JSON.stringify(value) === '{}' ||
|
||||
JSON.stringify(value) === '[]' ||
|
||||
value === ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 是否客户端
|
||||
*/
|
||||
export const isClient = () => {
|
||||
return typeof window !== 'undefined';
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: 是否为浏览器
|
||||
*/
|
||||
export const isWindow = (val: any): val is Window => {
|
||||
return typeof window !== 'undefined' && is(val, 'Window');
|
||||
};
|
||||
|
||||
export const isElement = (val: unknown): val is Element => {
|
||||
return isObject(val) && !!val.tagName;
|
||||
};
|
||||
|
||||
export const isServer = typeof window === 'undefined';
|
||||
|
||||
// 是否为图片节点
|
||||
export function isImageDom(o: Element) {
|
||||
return o && ['IMAGE', 'IMG'].includes(o.tagName);
|
||||
}
|
||||
|
||||
export function isNull(val: unknown): val is null {
|
||||
return val === null;
|
||||
}
|
||||
|
||||
export function isNullAndUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) && isNull(val);
|
||||
}
|
||||
|
||||
export function isNullOrUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) || isNull(val);
|
||||
}
|
||||
|
||||
export function isEmpty(value: any): boolean {
|
||||
return value === undefined || value === null || value === '';
|
||||
}
|
||||
|
||||
// 判断字串符是否以字母开头
|
||||
export function isLetterBegin(str) {
|
||||
return /^[A-z]/.test(str);
|
||||
}
|
||||
|
||||
export function isUrl(url: string): boolean {
|
||||
return /(^http|https:\/\/)/g.test(url);
|
||||
}
|
||||
|
||||
// 判断是否为微信浏览器
|
||||
export function isWechatBrowser(): boolean {
|
||||
return /micromessenger/.test(navigator.userAgent.toLowerCase()) ? true : false;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as echarts from 'echarts/core';
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
RadarChart,
|
||||
} from 'echarts/charts';
|
||||
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
LegendComponent,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
} from 'echarts/components';
|
||||
|
||||
import { SVGRenderer } from 'echarts/renderers';
|
||||
|
||||
echarts.use([
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
RadarChart,
|
||||
SVGRenderer,
|
||||
PictorialBarChart,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
]);
|
||||
|
||||
export default echarts;
|
||||
@@ -0,0 +1,9 @@
|
||||
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
|
||||
|
||||
export function warn(message: string) {
|
||||
console.warn(`[${projectName} warn]:${message}`);
|
||||
}
|
||||
|
||||
export function error(message: string) {
|
||||
throw new Error(`[${projectName} error]:${message}`);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CSSProperties, VNodeChild } from 'vue';
|
||||
import {
|
||||
createTypes,
|
||||
VueTypeExtendCallback,
|
||||
VueTypeValidableDef,
|
||||
VueTypesInterface,
|
||||
} from 'vue-types';
|
||||
|
||||
export type VueNode = VNodeChild | JSX.Element;
|
||||
|
||||
type ExtendedPropTypes = VueTypesInterface & {
|
||||
readonly style: VueTypeValidableDef<CSSProperties>;
|
||||
readonly VNodeChild: VueTypeValidableDef<VueNode>;
|
||||
};
|
||||
|
||||
class CustomVueTypes
|
||||
extends (createTypes({
|
||||
func: undefined,
|
||||
bool: undefined,
|
||||
string: undefined,
|
||||
number: undefined,
|
||||
object: undefined,
|
||||
integer: undefined,
|
||||
}) as VueTypesInterface)
|
||||
implements ExtendedPropTypes
|
||||
{
|
||||
static extend(types: VueTypeExtendCallback[]): CustomVueTypes {
|
||||
return types.reduce((result, { name, ...callbacks }) => {
|
||||
result[name] = { getter: true, ...callbacks };
|
||||
return result;
|
||||
}, this);
|
||||
}
|
||||
|
||||
readonly style!: VueTypeValidableDef<CSSProperties>;
|
||||
readonly VNodeChild!: VueTypeValidableDef<VueNode>;
|
||||
}
|
||||
|
||||
const propTypes = CustomVueTypes.extend([
|
||||
{
|
||||
name: 'style',
|
||||
type: [String, Object],
|
||||
default: undefined,
|
||||
},
|
||||
{
|
||||
name: 'VNodeChild',
|
||||
type: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
export { propTypes };
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 树形数组转平级
|
||||
* @date 2023-01-09
|
||||
* @returns {array}
|
||||
*/
|
||||
export const tree2FlatArray = (tree: any[], childrenKey: string = "children") => {
|
||||
return tree.reduce((arr, item) => {
|
||||
var children = item?.[childrenKey] ?? [];
|
||||
var obj = item;
|
||||
delete obj[childrenKey];
|
||||
//解构赋值+默认值
|
||||
return arr.concat([obj], tree2FlatArray(children)); //children部分进行递归
|
||||
}, []);
|
||||
};
|
||||
/**
|
||||
* 树形数组转对象
|
||||
* @date 2023-01-09
|
||||
* @returns {object}
|
||||
*/
|
||||
export const tree2Objects = (
|
||||
tree: any[],
|
||||
key: string = "id",
|
||||
childrenKey: string = "children"
|
||||
) => {
|
||||
let rows = tree2FlatArray(tree, childrenKey);
|
||||
let objects = {};
|
||||
rows.forEach((item) => {
|
||||
objects[item[key]] = item;
|
||||
});
|
||||
return objects;
|
||||
};
|
||||
|
||||
/**
|
||||
* 树形菜单数据输出
|
||||
* @date 2022-04-20
|
||||
* @param {object} data - 数据
|
||||
* @param {string} pid="parent_id" - 父类名称
|
||||
* @param {string} children="children" - 子类名称
|
||||
* @returns {object}
|
||||
*/
|
||||
export const treesBy = (
|
||||
data: any[],
|
||||
pid: string = "parent_id",
|
||||
children: string = "children"
|
||||
) => {
|
||||
let map = {},
|
||||
val = <any>[];
|
||||
data.forEach((item) => {
|
||||
item[children] = [];
|
||||
map[item.id] = item;
|
||||
});
|
||||
data.forEach((item) => {
|
||||
const parent = map[item[pid]];
|
||||
if (!!parent) {
|
||||
parent[children].push(item);
|
||||
} else {
|
||||
val.push(item);
|
||||
}
|
||||
});
|
||||
return val;
|
||||
};
|
||||
/**
|
||||
* 格式化树形菜单,省市区
|
||||
* @param {object} org
|
||||
* @returns {object}
|
||||
*/
|
||||
export const mapTree = (org: any, keyId = "v", keyChild = "s", keyName = "n") => {
|
||||
const haveChildren =
|
||||
org[keyChild] && Array.isArray(org[keyChild]) && org[keyChild].length > 0;
|
||||
return {
|
||||
key: org[keyId],
|
||||
value: String(org[keyId]),
|
||||
label: org[keyName],
|
||||
isLeaf: true,
|
||||
children: haveChildren ? org[keyChild].map((i) => mapTree(i)) : [],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化树形菜单,省市区
|
||||
* @param {object} org
|
||||
* @returns {object}
|
||||
*/
|
||||
export const toFlatArray = (
|
||||
tree: any[],
|
||||
parentId: string | number,
|
||||
keyId: string | number = "key"
|
||||
) => {
|
||||
return tree.reduce((t, _) => {
|
||||
const child = _["children"];
|
||||
return [
|
||||
...t,
|
||||
{ [keyId]: _[keyId], parentId },
|
||||
...(child && child.length ? toFlatArray(child, _[keyId]) : []),
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import router from '@/router';
|
||||
|
||||
/**
|
||||
* 将对象添加当作参数拼接到URL上面
|
||||
* @param baseUrl 需要拼接的url
|
||||
* @param obj 参数对象
|
||||
* @returns {string} 拼接后的对象
|
||||
* 例子:
|
||||
* let obj = {a: '3', b: '4'}
|
||||
* setObjToUrlParams('www.baidu.com', obj)
|
||||
* ==>www.baidu.com?a=3&b=4
|
||||
*/
|
||||
export function setObjToUrlParams(baseUrl: string, obj: object): string {
|
||||
let parameters = '';
|
||||
let url = '';
|
||||
for (const key in obj) {
|
||||
parameters += key + '=' + encodeURIComponent(obj[key]) + '&';
|
||||
}
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
if (/\?$/.test(baseUrl)) {
|
||||
url = baseUrl + parameters;
|
||||
} else {
|
||||
url = baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function encodeParams(obj) {
|
||||
const arr = [];
|
||||
for (const p in obj) {
|
||||
// @ts-ignore
|
||||
arr.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
|
||||
}
|
||||
return arr.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀
|
||||
*/
|
||||
export function getFileExt(fileName: string) {
|
||||
if (fileName === undefined || fileName === '') {
|
||||
return ``;
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当访问的url,不含参数
|
||||
*/
|
||||
export function getNowUrl(): string {
|
||||
const w = window.location;
|
||||
return w.protocol + '//' + w.host + w.pathname;
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
export function goBackOrToPage(to: RouteLocationRaw): void {
|
||||
if (router.currentRoute.value.matched.length > 0) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push(to).catch((e) => {
|
||||
console.log('返回上一页失败:', e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新hash路由get参数,不刷新页面
|
||||
export function pushHashRouterParameter(url, key, value) {
|
||||
const urlParts = url.split('?');
|
||||
let newUrl = '';
|
||||
if (urlParts.length >= 2) {
|
||||
const baseUrl = urlParts[0];
|
||||
const queryString = urlParts[1];
|
||||
const searchParams = new URLSearchParams(queryString);
|
||||
searchParams.set(key, value);
|
||||
newUrl = `${baseUrl}?${searchParams.toString()}`;
|
||||
} else {
|
||||
newUrl = `${url}?${key}=${value}`;
|
||||
}
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { FormItemRule } from 'naive-ui';
|
||||
import * as yaml from 'js-yaml';
|
||||
/**
|
||||
* @description 表单验证封装
|
||||
*/
|
||||
export const validate = {
|
||||
// 验证ipv4
|
||||
ip(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
// 支持通配符的ipv4正则
|
||||
const ipv4Regex =
|
||||
/^(?:[1-9]?[0-9]|1[0-9]{2}|2(?:[0-4][0-9]|5[0-5]))(?!.*?\.\*\.[*\d])(?:\.(?:(?:[1-9]?[0-9]|1[0-9]{2}|2(?:[0-4][0-9]|5[0-5]))|\*)){1,3}$/;
|
||||
// Ipv6:
|
||||
const ipv6Regex =
|
||||
/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请输入IP'));
|
||||
} else if (!ipv4Regex.test(value) && !ipv6Regex.test(value)) {
|
||||
callback(new Error('请输入正确的IP'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
//0-100百分比验证
|
||||
percentage(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const reg = /^([1-9]{1,2}$)|(^[0-9]{1,2}\.[0-9]{1,2}$)|100$/;
|
||||
if (!value && !rule.required) {
|
||||
callback(new Error('请输入比例'));
|
||||
} else if (!reg.test(value)) {
|
||||
callback(new Error('请输入0-100的数字'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 手机号 (eg:138********,159********)
|
||||
phone(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regPhone = /^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入手机号码'));
|
||||
} else if (!regPhone.test(value)) {
|
||||
callback(new Error('手机号格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 用户名 (eg:a123456)
|
||||
userName(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regUserName = /^[0-9a-zA-Z]{6,16}$/;
|
||||
if (!value && !rule.required) {
|
||||
callback(new Error('请输入登录账号'));
|
||||
} else if (!regUserName.test(value)) {
|
||||
callback(new Error('请输入6-16位由字母和数字组成的登录账号'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 账号
|
||||
account(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regex = /^[\w_\d]{6,16}$/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入账号'));
|
||||
} else if (!regex.test(value)) {
|
||||
callback(new Error('请输入6-16位由字母、数字或下划线组成的账号'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 密码
|
||||
password(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regPassword = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,18}$/;
|
||||
if (!value && !rule.required) {
|
||||
callback(new Error('请输入密码'));
|
||||
} else if (!regPassword.test(value)) {
|
||||
callback(new Error('密码格式错误!必须包含6-18为字母和数字'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 邮箱
|
||||
email(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regEmails = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
|
||||
// console.log('isRequired is: ', JSON.stringify(isRequired))
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入邮箱'));
|
||||
} else if (!regEmails.test(value)) {
|
||||
callback(new Error('邮箱格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 金额验证
|
||||
amount(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regAmount = /(^[0-9]{1,10}$)|(^[0-9]{1,10}[\.]{1}[0-9]{1,2}$)/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入金额'));
|
||||
} else if (!regAmount.test(value)) {
|
||||
callback(new Error('金额格式错误,最多允许输入10位整数及2位小数'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 身份证验证
|
||||
idCard(rule: FormItemRule, value: any, callback: Function, isEnabled = true): boolean | Error {
|
||||
const regIdCard =
|
||||
/^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X|x)$/;
|
||||
if (!isEnabled) {
|
||||
callback();
|
||||
} else if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入身份证号'));
|
||||
} else if (!regIdCard.test(value)) {
|
||||
callback(new Error('身份证号码格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 银行卡验证
|
||||
bank(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regBank = /^([1-9]{1})(\d{15}|\d{16}|\d{18})$/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback();
|
||||
} else if (!regBank.test(value)) {
|
||||
callback(new Error('银行卡号码格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 非零正整数验证
|
||||
num(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const reg = /^\+?[1-9][0-9]*$/;
|
||||
if (!value && !rule.required) {
|
||||
callback(new Error('请填写非零正整数'));
|
||||
} else {
|
||||
if (!reg.test(value)) {
|
||||
callback(new Error('请输入非零的正整数'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 银行卡
|
||||
bankCard(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regBankCard = /^(\d{16}|\d{19})$/;
|
||||
if (value == '' && !rule.required) {
|
||||
callback(new Error('请输入银行卡号'));
|
||||
} else {
|
||||
if (!regBankCard.test(value)) {
|
||||
callback(new Error('银行卡号格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 固话格式
|
||||
tel(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regTel = /^(0\d{2,3}-?)?\d{7,8}$/;
|
||||
if (value == '' && !rule.required) {
|
||||
callback(new Error('请输入座机号码'));
|
||||
} else {
|
||||
if (!regTel.test(value)) {
|
||||
callback(new Error('座机号码格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// QQ号码
|
||||
qq(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regex = /^[1-9][0-9]{4,}$/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入QQ号码'));
|
||||
} else {
|
||||
if (!regex.test(value)) {
|
||||
callback(new Error('QQ号码格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// weibo号
|
||||
weibo(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
const regex = /^[0-9a-zA-Z\u4e00-\u9fa5_-]*$/;
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
} else if (!value) {
|
||||
callback(new Error('请输入微博账号'));
|
||||
} else {
|
||||
if (!regex.test(value)) {
|
||||
callback(new Error('微博号码格式错误'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// YAML格式验证
|
||||
yaml(rule: FormItemRule, value: any, callback: Function): boolean | Error {
|
||||
if (!value && !rule.required) {
|
||||
callback();
|
||||
return true;
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请输入YAML配置'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 js-yaml 进行同步验证
|
||||
yaml.load(value);
|
||||
callback();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
callback(new Error(`YAML格式错误: ${error.message}`));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// 不验证
|
||||
none(_rule: FormItemRule, _value: any, callback: Function): boolean {
|
||||
callback();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import { SocketEnum } from '@/enums/socketEnum';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
import { isJsonString } from '@/utils/is';
|
||||
import { registerGlobalMessage } from '@/utils/websocket/registerMessage';
|
||||
|
||||
// WebSocket消息格式
|
||||
export interface WebSocketMessage {
|
||||
event: string;
|
||||
data: any;
|
||||
code: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
let socket: WebSocket;
|
||||
let isActive: boolean;
|
||||
const messageHandler: Map<string, Function> = new Map();
|
||||
|
||||
export default () => {
|
||||
const heartCheck = {
|
||||
timeout: 5000,
|
||||
timeoutObj: setTimeout(() => {}),
|
||||
serverTimeoutObj: setInterval(() => {}),
|
||||
reset: function () {
|
||||
clearTimeout(this.timeoutObj);
|
||||
clearTimeout(this.serverTimeoutObj);
|
||||
return this;
|
||||
},
|
||||
start: function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
clearTimeout(this.timeoutObj);
|
||||
clearTimeout(this.serverTimeoutObj);
|
||||
this.timeoutObj = setTimeout(function () {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
event: SocketEnum.EventPing,
|
||||
})
|
||||
);
|
||||
self.serverTimeoutObj = setTimeout(function () {
|
||||
console.log('[WebSocket] 关闭服务');
|
||||
socket.close();
|
||||
}, self.timeout);
|
||||
}, this.timeout);
|
||||
},
|
||||
};
|
||||
|
||||
const useUserStore = useUserStoreWidthOut();
|
||||
let lockReconnect = false;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const createSocket = () => {
|
||||
console.log('[WebSocket] createSocket...');
|
||||
if (useUserStore.token === '' || useUserStore.config?.wsAddr == '') {
|
||||
console.error('[WebSocket] 用户未登录,稍后重试...');
|
||||
resetReconnect();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
socket = new WebSocket(`${useUserStore.config?.wsAddr}?authorization=${useUserStore.token}`);
|
||||
init();
|
||||
if (lockReconnect) {
|
||||
lockReconnect = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[WebSocket] createSocket err: ${e}`);
|
||||
resetReconnect();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const resetReconnect = () => {
|
||||
if (lockReconnect) {
|
||||
lockReconnect = false;
|
||||
}
|
||||
reconnect();
|
||||
};
|
||||
|
||||
const reconnect = () => {
|
||||
console.log('[WebSocket] lockReconnect:' + lockReconnect);
|
||||
if (lockReconnect) return;
|
||||
lockReconnect = true;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
createSocket();
|
||||
}, 1000 * 10);
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
socket.onopen = function (_) {
|
||||
console.log('[WebSocket] 已连接');
|
||||
heartCheck.reset().start();
|
||||
isActive = true;
|
||||
};
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
isActive = true;
|
||||
// console.log('WebSocket:收到一条消息', event.data);
|
||||
|
||||
if (!isJsonString(event.data)) {
|
||||
console.log('[WebSocket] message incorrect format:' + JSON.stringify(event));
|
||||
return;
|
||||
}
|
||||
|
||||
heartCheck.reset().start();
|
||||
|
||||
const message = JSON.parse(event.data) as WebSocketMessage;
|
||||
onMessage(message);
|
||||
};
|
||||
|
||||
socket.onerror = function (_) {
|
||||
console.log('[WebSocket] 发生错误');
|
||||
reconnect();
|
||||
isActive = false;
|
||||
};
|
||||
|
||||
socket.onclose = function (_) {
|
||||
console.log('[WebSocket] 已关闭');
|
||||
heartCheck.reset();
|
||||
reconnect();
|
||||
isActive = false;
|
||||
};
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
socket.close();
|
||||
isActive = false;
|
||||
};
|
||||
};
|
||||
|
||||
createSocket();
|
||||
registerGlobalMessage();
|
||||
};
|
||||
|
||||
function onMessage(message: WebSocketMessage) {
|
||||
let handled = false;
|
||||
messageHandler.forEach((value: Function, key: string) => {
|
||||
if (message.event === key || key === '*') {
|
||||
handled = true;
|
||||
value.call(null, message);
|
||||
}
|
||||
});
|
||||
|
||||
if (!handled) {
|
||||
console.log('[WebSocket] messageHandler not registered. message:' + JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
export function sendMsg(event: string, data: any = null, isRetry = true) {
|
||||
if (socket === undefined || !isActive) {
|
||||
if (!isRetry) {
|
||||
console.log('[WebSocket] 连接异常,发送失败!');
|
||||
return;
|
||||
}
|
||||
console.log('[WebSocket] 连接异常,等待重试..');
|
||||
setTimeout(() => {
|
||||
sendMsg(event, data);
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.send(JSON.stringify({ event, data }));
|
||||
} catch (err: any) {
|
||||
console.log('[WebSocket] 发送消息失败,err:', err.message);
|
||||
if (!isRetry) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[WebSocket] 等待重试..');
|
||||
setTimeout(() => {
|
||||
sendMsg(event, data);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息处理
|
||||
export function addOnMessage(key: string, value: Function): void {
|
||||
messageHandler.set(key, value);
|
||||
}
|
||||
|
||||
// 移除消息处理
|
||||
export function removeOnMessage(key: string): boolean {
|
||||
return messageHandler.delete(key);
|
||||
}
|
||||
|
||||
// 查看所有消息处理
|
||||
export function getAllOnMessage(): Map<string, Function> {
|
||||
return messageHandler;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { TABS_ROUTES } from '@/store/mutation-types';
|
||||
import { SocketEnum } from '@/enums/socketEnum';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import { addOnMessage, WebSocketMessage } from '@/utils/websocket/index';
|
||||
|
||||
// 注册全局消息监听
|
||||
export function registerGlobalMessage() {
|
||||
// 心跳
|
||||
addOnMessage(SocketEnum.EventPing, function (_message: WebSocketMessage) {
|
||||
// console.log('ping..');
|
||||
});
|
||||
|
||||
// 强制退出
|
||||
addOnMessage(SocketEnum.EventKick, function (_message: WebSocketMessage) {
|
||||
const useUserStore = useUserStoreWidthOut();
|
||||
useUserStore.logout().then(() => {
|
||||
// 移除标签页
|
||||
localStorage.removeItem(TABS_ROUTES);
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// 消息通知
|
||||
addOnMessage(SocketEnum.EventNotice, function (message: WebSocketMessage) {
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
notificationStore.triggerNewMessages(message.data);
|
||||
});
|
||||
|
||||
// 更多全局消息处理都可以在这里注册
|
||||
// ...
|
||||
}
|
||||
Reference in New Issue
Block a user