feat: add web application

This commit is contained in:
richjohnrambo
2026-10-10 20:55:59 +08:00
parent 3eb5d78875
commit 50fcfd2d3d
571 changed files with 73992 additions and 135 deletions
+98
View File
@@ -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;
+127
View File
@@ -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;
+64
View File
@@ -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));
}
+142
View File
@@ -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];
}
+74
View File
@@ -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;
}
+206
View File
@@ -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;
},
};
}
+165
View File
@@ -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);
}
+76
View File
@@ -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);
}
});
}
+25
View File
@@ -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();
},
};
+85
View File
@@ -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;
}
+186
View File
@@ -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;
+186
View File
@@ -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;
}
+233
View File
@@ -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);
}
}
+61
View File
@@ -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;
}
+47
View File
@@ -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);
}
}
+47
View File
@@ -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]);
}
}
}
+309
View File
@@ -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',
// },
// });
+65
View File
@@ -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;
}
+17
View File
@@ -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];
}
+453
View File
@@ -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;
}
+165
View File
@@ -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;
}
+53
View File
@@ -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;
+9
View File
@@ -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}`);
}
+50
View File
@@ -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 };
+97
View File
@@ -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]) : []),
];
}, []);
};
+81
View File
@@ -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);
}
+251
View File
@@ -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;
},
};
+188
View File
@@ -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);
});
// 更多全局消息处理都可以在这里注册
// ...
}