mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-14 05:03:49 +08:00
v2.0
This commit is contained in:
98
web/src/utils/Drag.ts
Normal file
98
web/src/utils/Drag.ts
Normal 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
web/src/utils/Storage.ts
Normal file
127
web/src/utils/Storage.ts
Normal 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;
|
||||
144
web/src/utils/array.ts
Normal file
144
web/src/utils/array.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
export function arrayDelIndex(array: any, keyName: string, key: string): any {
|
||||
if (array === null || array === undefined || array.length === 0) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const newArray = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i][keyName] !== undefined && array[i][keyName] === key) {
|
||||
continue;
|
||||
}
|
||||
// @ts-ignore
|
||||
newArray.push(array[i]);
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function arrayAddIndex(array: any, keyName: string, key: string, row: any): any {
|
||||
if (array === null || array === undefined) {
|
||||
return array;
|
||||
}
|
||||
const newArray = [];
|
||||
|
||||
if (array.length === 0) {
|
||||
// @ts-ignore
|
||||
newArray.push(row);
|
||||
} else {
|
||||
let isFor = false;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i][keyName] !== undefined && array[i][keyName] === key) {
|
||||
array[i] = row;
|
||||
isFor = true;
|
||||
}
|
||||
// @ts-ignore
|
||||
newArray.push(array[i]);
|
||||
}
|
||||
|
||||
if (!isFor) {
|
||||
// @ts-ignore
|
||||
newArray.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function objDalEmpty(obj: object): object {
|
||||
for (const key in obj) {
|
||||
if (obj[key] === '' || obj[key] === undefined || obj[key] == null || obj[key].length === 0) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function filterArray(condition, data) {
|
||||
return data.filter((item) => {
|
||||
return Object.keys(condition).every((key) => {
|
||||
return String(item[key]).toLowerCase().includes(String(condition[key]).trim().toLowerCase());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function findIndex(value, arr) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = arr[i];
|
||||
if (item.value == value) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重追加
|
||||
* @param array
|
||||
* @param son
|
||||
*/
|
||||
export function onlyPush(array: any, son: any) {}
|
||||
|
||||
/**
|
||||
* 对象拷贝
|
||||
* @param obj2
|
||||
* @param obj1
|
||||
*/
|
||||
export function copyObj(obj2: any, obj1: any) {
|
||||
console.log('obj1:' + JSON.stringify(obj1));
|
||||
for (const key in obj1) {
|
||||
if (obj2[key] !== undefined) {
|
||||
obj2[key] = obj1[key];
|
||||
}
|
||||
}
|
||||
return obj2;
|
||||
}
|
||||
142
web/src/utils/browser-type.ts
Normal file
142
web/src/utils/browser-type.ts
Normal 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];
|
||||
}
|
||||
157
web/src/utils/dateUtil.ts
Normal file
157
web/src/utils/dateUtil.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
const DATE_FORMAT = 'YYYY-MM-DD ';
|
||||
|
||||
export function formatToDateTime(date: Date, formatStr = DATE_TIME_FORMAT): string {
|
||||
const date2 = new Date(date);
|
||||
return format(date2, formatStr);
|
||||
}
|
||||
|
||||
export function formatToDate(date: Date, formatStr = DATE_FORMAT): string {
|
||||
return format(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 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 '刚刚';
|
||||
}
|
||||
165
web/src/utils/domUtils.ts
Normal file
165
web/src/utils/domUtils.ts
Normal 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);
|
||||
}
|
||||
75
web/src/utils/downloadFile.ts
Normal file
75
web/src/utils/downloadFile.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 根据文件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');
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
87
web/src/utils/env.ts
Normal file
87
web/src/utils/env.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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_PROD_MOCK,
|
||||
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_PROD_MOCK,
|
||||
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;
|
||||
}
|
||||
200
web/src/utils/http/axios/Axios.ts
Normal file
200
web/src/utils/http/axios/Axios.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
|
||||
|
||||
import axios from 'axios';
|
||||
import { AxiosCanceler } from './axiosCancel';
|
||||
import { isFunction } from '@/utils/is';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
|
||||
import { ContentTypeEnum } from '@/enums/httpEnum';
|
||||
|
||||
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) {
|
||||
const formData = new window.FormData();
|
||||
const customFilename = params.name || 'file';
|
||||
|
||||
if (params.filename) {
|
||||
formData.append(customFilename, params.file, params.filename);
|
||||
} else {
|
||||
formData.append(customFilename, params.file);
|
||||
}
|
||||
|
||||
if (params.data) {
|
||||
Object.keys(params.data).forEach((key) => {
|
||||
const value = params.data![key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
formData.append(`${key}[]`, item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(key, params.data![key]);
|
||||
});
|
||||
}
|
||||
|
||||
return this.axiosInstance.request<T>({
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||
ignoreCancelToken: true,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
web/src/utils/http/axios/axiosCancel.ts
Normal file
61
web/src/utils/http/axios/axiosCancel.ts
Normal 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>();
|
||||
}
|
||||
}
|
||||
52
web/src/utils/http/axios/axiosTransform.ts
Normal file
52
web/src/utils/http/axios/axiosTransform.ts
Normal file
@@ -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
web/src/utils/http/axios/checkStatus.ts
Normal file
47
web/src/utils/http/axios/checkStatus.ts
Normal 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
web/src/utils/http/axios/helper.ts
Normal file
47
web/src/utils/http/axios/helper.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
288
web/src/utils/http/axios/index.ts
Normal file
288
web/src/utils/http/axios/index.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
// 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 } from '@/utils/is/';
|
||||
import { deepMerge, isUrl } 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';
|
||||
|
||||
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为 后台统一的字段,需要修改为项目自己的接口返回格式
|
||||
let { 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 = '登录超时,请重新登录!';
|
||||
$dialog.warning({
|
||||
title: '提示',
|
||||
content: '登录身份已失效,请重新登录!',
|
||||
positiveText: '确定',
|
||||
//negativeText: '取消',
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
onPositiveClick: () => {
|
||||
storage.clear();
|
||||
window.location.href = LoginPath;
|
||||
},
|
||||
onNegativeClick: () => {},
|
||||
});
|
||||
break;
|
||||
}
|
||||
$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 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;
|
||||
}
|
||||
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: 10 * 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();
|
||||
|
||||
// 项目,多个不同 api 地址,直接在这里导出多个
|
||||
// src/api ts 里面接口,就可以单独使用这个请求,
|
||||
// import { httpTwo } from '@/utils/http/axios'
|
||||
// export const httpTwo = createAxios({
|
||||
// requestOptions: {
|
||||
// apiUrl: 'http://localhost:9001',
|
||||
// urlPrefix: 'api',
|
||||
// },
|
||||
// });
|
||||
65
web/src/utils/http/axios/types.ts
Normal file
65
web/src/utils/http/axios/types.ts
Normal 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;
|
||||
}
|
||||
253
web/src/utils/index.ts
Normal file
253
web/src/utils/index.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { h, unref } from 'vue';
|
||||
import type { App, Plugin } from 'vue';
|
||||
import { NIcon, NTag } from 'naive-ui';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { isObject } from './is/index';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* render 图标
|
||||
* */
|
||||
export function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { 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 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归组装菜单格式
|
||||
*/
|
||||
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 === 1) {
|
||||
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) => {
|
||||
app.component(comp.name || comp.displayName, component);
|
||||
if (alias) {
|
||||
app.config.globalProperties[alias] = component;
|
||||
}
|
||||
};
|
||||
return component as T & Plugin;
|
||||
};
|
||||
|
||||
/**
|
||||
* 找到对应的节点
|
||||
* */
|
||||
let result = null;
|
||||
|
||||
export function getTreeItem(data: any[], key?: string | number): any {
|
||||
data.map((item) => {
|
||||
if (item.key === key) {
|
||||
result = item;
|
||||
} else {
|
||||
if (item.children && item.children.length) {
|
||||
getTreeItem(item.children, key);
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到所有节点
|
||||
* */
|
||||
const treeAll: any[] = [];
|
||||
|
||||
export function getTreeAll(data: any[]): any[] {
|
||||
data.map((item) => {
|
||||
treeAll.push(item.key);
|
||||
if (item.children && item.children.length) {
|
||||
getTreeAll(item.children);
|
||||
}
|
||||
});
|
||||
return treeAll;
|
||||
}
|
||||
|
||||
// dynamic use hook props
|
||||
export function getDynamicProps<T, U>(props: T): Partial<U> {
|
||||
const ret: Recordable = {};
|
||||
|
||||
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)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否 url
|
||||
* */
|
||||
export function isUrl(url: string) {
|
||||
return /^(http|https):\/\//g.test(url);
|
||||
}
|
||||
118
web/src/utils/is/index.ts
Normal file
118
web/src/utils/is/index.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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 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);
|
||||
}
|
||||
53
web/src/utils/lib/echarts.ts
Normal file
53
web/src/utils/lib/echarts.ts
Normal 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;
|
||||
12
web/src/utils/lodashChunk.ts
Normal file
12
web/src/utils/lodashChunk.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 这里按需引入lodash的一些方法,方便维护
|
||||
*/
|
||||
|
||||
// export {default as xxx} from 'lodash/xxx'
|
||||
|
||||
export { default as cloneDeep } from 'lodash/cloneDeep';
|
||||
export { default as intersection } from 'lodash/intersection';
|
||||
export { default as get } from 'lodash/get';
|
||||
export { default as upperFirst } from 'lodash/upperFirst';
|
||||
export { default as omit } from 'lodash/omit';
|
||||
export { default as debounce } from 'lodash/debounce';
|
||||
9
web/src/utils/log.ts
Normal file
9
web/src/utils/log.ts
Normal 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}`);
|
||||
}
|
||||
33
web/src/utils/propTypes.ts
Normal file
33
web/src/utils/propTypes.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { CSSProperties, VNodeChild } from 'vue';
|
||||
import { createTypes, VueTypeValidableDef, VueTypesInterface } from 'vue-types';
|
||||
|
||||
export type VueNode = VNodeChild | JSX.Element;
|
||||
|
||||
type PropTypes = VueTypesInterface & {
|
||||
readonly style: VueTypeValidableDef<CSSProperties>;
|
||||
readonly VNodeChild: VueTypeValidableDef<VueNode>;
|
||||
};
|
||||
|
||||
const propTypes = createTypes({
|
||||
func: undefined,
|
||||
bool: undefined,
|
||||
string: undefined,
|
||||
number: undefined,
|
||||
object: undefined,
|
||||
integer: undefined,
|
||||
}) as PropTypes;
|
||||
|
||||
propTypes.extend([
|
||||
{
|
||||
name: 'style',
|
||||
getter: true,
|
||||
type: [String, Object],
|
||||
default: undefined,
|
||||
},
|
||||
{
|
||||
name: 'VNodeChild',
|
||||
getter: true,
|
||||
type: undefined,
|
||||
},
|
||||
]);
|
||||
export { propTypes };
|
||||
24
web/src/utils/urlUtils.ts
Normal file
24
web/src/utils/urlUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 将对象添加当作参数拼接到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;
|
||||
}
|
||||
186
web/src/utils/websocket.ts
Normal file
186
web/src/utils/websocket.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { SocketEnum } from '@/enums/socketEnum';
|
||||
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
||||
import { useUserStoreWidthOut } from '@/store/modules/user';
|
||||
import { TABS_ROUTES } from '@/store/mutation-types';
|
||||
|
||||
let socket: WebSocket;
|
||||
let isActive: boolean;
|
||||
|
||||
export function getSocket(): WebSocket {
|
||||
console.log('socket:', socket);
|
||||
if (socket === undefined) {
|
||||
location.reload();
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
export function getActive(): boolean {
|
||||
console.log('isActive:', isActive);
|
||||
return isActive;
|
||||
}
|
||||
|
||||
export function sendMsg(event: string, data = null, isRetry = true) {
|
||||
if (socket === undefined || !isActive) {
|
||||
if (!isRetry) {
|
||||
console.log('socket连接异常,发送失败!');
|
||||
return;
|
||||
}
|
||||
console.log('socket连接异常,等待重试..');
|
||||
setTimeout(function () {
|
||||
sendMsg(event, data);
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
event: event,
|
||||
data: data,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
console.log('ws发送消息失败,等待重试,err:' + err.message);
|
||||
if (!isRetry) {
|
||||
return;
|
||||
}
|
||||
setTimeout(function () {
|
||||
sendMsg(event, data);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
export function addOnMessage(onMessageList: any, func: Function) {
|
||||
let exist = false;
|
||||
for (let i = 0; i < onMessageList.length; i++) {
|
||||
if (onMessageList[i].name == func.name) {
|
||||
onMessageList[i] = func;
|
||||
exist = true;
|
||||
}
|
||||
}
|
||||
if (!exist) {
|
||||
onMessageList.push(func);
|
||||
}
|
||||
}
|
||||
|
||||
export default (onMessage: Function) => {
|
||||
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,
|
||||
})
|
||||
);
|
||||
console.log('ping');
|
||||
self.serverTimeoutObj = setTimeout(function () {
|
||||
console.log('关闭服务');
|
||||
socket.close();
|
||||
}, self.timeout);
|
||||
}, this.timeout);
|
||||
},
|
||||
};
|
||||
|
||||
const notificationStore = notificationStoreWidthOut();
|
||||
const useUserStore = useUserStoreWidthOut();
|
||||
let lockReconnect = false;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const createSocket = () => {
|
||||
console.log('createSocket...');
|
||||
try {
|
||||
if (useUserStore.token === '') {
|
||||
throw new Error('用户未登录,稍后重试...');
|
||||
}
|
||||
socket = new WebSocket(useUserStore.config.wsAddr + '?authorization=' + useUserStore.token);
|
||||
init();
|
||||
} catch (e) {
|
||||
console.log('createSocket err:' + e);
|
||||
reconnect();
|
||||
}
|
||||
if (lockReconnect) {
|
||||
lockReconnect = false;
|
||||
}
|
||||
};
|
||||
const reconnect = () => {
|
||||
console.log('lockReconnect:' + lockReconnect);
|
||||
if (lockReconnect) return;
|
||||
lockReconnect = true;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
createSocket();
|
||||
}, SocketEnum.HeartBeatInterval);
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
socket.onopen = function (_) {
|
||||
console.log('WebSocket:已连接');
|
||||
heartCheck.reset().start();
|
||||
isActive = true;
|
||||
};
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
isActive = true;
|
||||
console.log('WebSocket:收到一条消息', event.data);
|
||||
|
||||
let isHeart = false;
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'ping') {
|
||||
isHeart = true;
|
||||
}
|
||||
|
||||
// 强制退出
|
||||
if (message.event === 'kick') {
|
||||
useUserStore.logout().then(() => {
|
||||
// 移除标签页
|
||||
localStorage.removeItem(TABS_ROUTES);
|
||||
location.reload();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知
|
||||
if (message.event === 'notice') {
|
||||
notificationStore.addMessages(event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onMessage && !isHeart) {
|
||||
onMessage.call(null, event);
|
||||
}
|
||||
heartCheck.reset().start();
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
Reference in New Issue
Block a user