签入版本
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// 默认缓存期限为7天
|
||||
const DEFAULT_CACHE_TIME = 7 * 24 * 60 * 60 * 1000; // 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 ? Date.now() + expire : null,
|
||||
});
|
||||
this.storage.setItem(this.getKey(key), stringData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取缓存
|
||||
* @param {string} key 缓存键
|
||||
* @param {*=} def 默认值
|
||||
*/
|
||||
get(key: string, def: any = null) {
|
||||
const item = this.storage.getItem(this.getKey(key));
|
||||
if (item) {
|
||||
try {
|
||||
const data = JSON.parse(item);
|
||||
const { value, expire } = data;
|
||||
// 在有效期内直接返回
|
||||
if (expire === null || expire >= Date.now()) {
|
||||
return value;
|
||||
}
|
||||
this.remove(key);
|
||||
} catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存删除某项
|
||||
* @param {string} key
|
||||
*/
|
||||
remove(key: string) {
|
||||
this.storage.removeItem(this.getKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
* @memberOf Cache
|
||||
*/
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置cookie
|
||||
* @param {string} name cookie 名称
|
||||
* @param {*} value cookie 值
|
||||
* @param {number=} expire 过期时间
|
||||
* 如果过期时间未设置,默认关闭浏览器自动删除
|
||||
* @example
|
||||
*/
|
||||
setCookie(name: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
|
||||
document.cookie = `${this.getKey(name)}=${value}; Max-Age=${expire}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名字获取cookie值
|
||||
* @param name
|
||||
*/
|
||||
getCookie(name: string): string {
|
||||
const cookieArr = document.cookie.split('; ');
|
||||
for (let i = 0, length = cookieArr.length; i < length; i++) {
|
||||
const kv = cookieArr[i].split('=');
|
||||
if (kv[0] === this.getKey(name)) {
|
||||
return kv[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名字删除指定的cookie
|
||||
* @param {string} key
|
||||
*/
|
||||
removeCookie(key: string) {
|
||||
this.setCookie(key, 1, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空cookie,使所有cookie失效
|
||||
*/
|
||||
clearCookie(): void {
|
||||
const keys = document.cookie.match(/[^ =;]+(?==)/g);
|
||||
if (keys) {
|
||||
for (let i = keys.length; i--; ) {
|
||||
document.cookie = keys[i] + '=0;expire=' + new Date(0).toUTCString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return new Storage();
|
||||
};
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
export default Storage;
|
||||
@@ -0,0 +1,282 @@
|
||||
import { isObject } from '@vue/shared';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
import { RouteLocationNormalized, useRoute } from 'vue-router';
|
||||
import { watch } from 'vue';
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
|
||||
|
||||
export function isExternal(path: string) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取正确的路经
|
||||
* @param {String} path 数据
|
||||
*/
|
||||
export function getNormalPath(path: string) {
|
||||
if (path.length === 0 || !path || path == 'undefined') {
|
||||
return path;
|
||||
}
|
||||
const newPath = path.replace('//', '/');
|
||||
const length = newPath.length;
|
||||
if (newPath[length - 1] === '/') {
|
||||
return newPath.slice(0, length - 1);
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
export const isEmpty = (value: unknown) => {
|
||||
return value == null && typeof value == 'undefined';
|
||||
};
|
||||
|
||||
/**
|
||||
* @description对象格式化为Query语法
|
||||
* @param { Object } params
|
||||
* @return {string} Query语法
|
||||
*/
|
||||
export function objectToQuery(params: Record<string, any>): string {
|
||||
let query = '';
|
||||
for (const props of Object.keys(params)) {
|
||||
const value = params[props];
|
||||
const part = encodeURIComponent(props) + '=';
|
||||
if (!isEmpty(value)) {
|
||||
if (isObject(value)) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!isEmpty(value[key])) {
|
||||
const params = props + '[' + key + ']';
|
||||
const subPart = encodeURIComponent(params) + '=';
|
||||
query += subPart + encodeURIComponent(value[key]) + '&';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query += part + encodeURIComponent(value) + '&';
|
||||
}
|
||||
}
|
||||
}
|
||||
return query.slice(0, -1);
|
||||
}
|
||||
|
||||
export const addUnit = (value: string | number, unit = 'px') => {
|
||||
return !Object.is(Number(value), NaN) ? `${value}${unit}` : value;
|
||||
};
|
||||
|
||||
export function useWatchRoute(callback: (route: RouteLocationNormalizedLoaded) => void) {
|
||||
const route = useRoute();
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
callback(route);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
return {
|
||||
route,
|
||||
};
|
||||
}
|
||||
|
||||
// declare type MessageType = '' | 'success' | 'warning' | 'info' | 'error';
|
||||
|
||||
export function confirm(msg: string, type: 'warning') {
|
||||
return ElMessageBox.confirm(msg, '温馨提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: type,
|
||||
});
|
||||
}
|
||||
|
||||
export function message(msg: string, type = 'success') {
|
||||
ElMessage[type](msg);
|
||||
}
|
||||
|
||||
let loadingInstance = null;
|
||||
|
||||
// 打开全局loading
|
||||
export function loading(msg: string) {
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: msg,
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭全局loading
|
||||
export function closeLoading() {
|
||||
loadingInstance?.close();
|
||||
}
|
||||
|
||||
export const arrayToTree = (
|
||||
data: any[],
|
||||
props = { id: 'id', parentId: 'pid', children: 'children' },
|
||||
) => {
|
||||
data = cloneDeep(data);
|
||||
const { id, parentId, children } = props;
|
||||
const result: any[] = [];
|
||||
const map = new Map();
|
||||
data.forEach((item) => {
|
||||
map.set(item[id], item);
|
||||
const parent = map.get(item[parentId]);
|
||||
if (parent) {
|
||||
parent[children] = parent[children] ?? [];
|
||||
parent[children].push(item);
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 树转数组,队列实现广度优先遍历
|
||||
* @param {Array} data 数据
|
||||
* @param {Object} props `{ children: 'children' }`
|
||||
*/
|
||||
|
||||
export const treeToArray = (data: any[], props = { children: 'children' }) => {
|
||||
data = cloneDeep(data);
|
||||
const { children } = props;
|
||||
const newData = [];
|
||||
const queue: any[] = [];
|
||||
data.forEach((child: any) => queue.push(child));
|
||||
while (queue.length) {
|
||||
const item: any = queue.shift();
|
||||
if (item[children]) {
|
||||
item[children].forEach((child: any) => queue.push(child));
|
||||
delete item[children];
|
||||
}
|
||||
newData.push(item);
|
||||
}
|
||||
return newData;
|
||||
};
|
||||
/**
|
||||
* 数组转树型结构
|
||||
*/
|
||||
export const buildTree = (array) => {
|
||||
const tree = {}; // 用于存储树形结构的临时对象
|
||||
const result = []; // 最终的树形结构数组
|
||||
|
||||
array.forEach((item) => {
|
||||
tree[item.id] = { ...item, children: [] }; // 初始化树节点
|
||||
});
|
||||
|
||||
array.forEach((item) => {
|
||||
if (item.parentId === 0) {
|
||||
result.push(tree[item.id]); // 根节点直接添加到结果数组
|
||||
} else if (tree[item.parentId]) {
|
||||
tree[item.parentId].children.push(tree[item.id]); // 子节点添加到父节点的children数组
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
/***
|
||||
* 递归查找
|
||||
*/
|
||||
export const findTreeByPath = (tree, path, result = []) => {
|
||||
for (const node of tree) {
|
||||
if (node.path === path) {
|
||||
result.push(node);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
findTreeByPath(node.children, path, result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
export const getComponentName = (route: RouteLocationNormalized) => {
|
||||
return route.matched[route.matched.length - 1]?.components?.default?.name;
|
||||
};
|
||||
|
||||
export function streamFileDownload(file: any, fileName = '文件名称.zip') {
|
||||
const blob = new Blob([file], { type: 'application/octet-stream;charset=UTF-8' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link); // 下载完成移除元素
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function filterName(list: any, value: any, key = 'value', label = 'label') {
|
||||
let name = '';
|
||||
const child = list.find((item) => item[key] == value);
|
||||
name = child ? child[label] : '';
|
||||
return name;
|
||||
}
|
||||
|
||||
export function filterDatetime(value: any, format = 'yyyy-MM-dd') {
|
||||
if (value) {
|
||||
return dateFtt(format, new Date(parseInt(value)));
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function generateUUID() {
|
||||
if (typeof crypto === 'object') {
|
||||
if (typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
|
||||
const callback = (c: any) => {
|
||||
const num = Number(c);
|
||||
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(
|
||||
16,
|
||||
);
|
||||
};
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback);
|
||||
}
|
||||
}
|
||||
let timestamp = new Date().getTime();
|
||||
let performanceNow =
|
||||
(typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0;
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
let random = Math.random() * 16;
|
||||
if (timestamp > 0) {
|
||||
random = (timestamp + random) % 16 | 0;
|
||||
timestamp = Math.floor(timestamp / 16);
|
||||
} else {
|
||||
random = (performanceNow + random) % 16 | 0;
|
||||
performanceNow = Math.floor(performanceNow / 16);
|
||||
}
|
||||
return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
}
|
||||
export function toUnderline(str: string) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
}
|
||||
export function downBlobFile(url: any, query: any, fileName: string) {
|
||||
return request({
|
||||
url: url,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
params: query,
|
||||
}).then((response) => {
|
||||
handleBlobFile(response, fileName);
|
||||
});
|
||||
}
|
||||
function dateFtt(fmt, date) {
|
||||
//author: meizz
|
||||
const o = {
|
||||
'M+': date.getMonth() + 1, //月份
|
||||
'd+': date.getDate(), //日
|
||||
'h+': date.getHours(), //小时
|
||||
'm+': date.getMinutes(), //分
|
||||
's+': date.getSeconds(), //秒
|
||||
'q+': Math.floor((date.getMonth() + 3) / 3), //季度
|
||||
S: date.getMilliseconds(), //毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt))
|
||||
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
|
||||
for (const k in o)
|
||||
if (new RegExp('(' + k + ')').test(fmt))
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length),
|
||||
);
|
||||
return fmt;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @description 获取用户浏览器版本及系统信息
|
||||
* @param {string='zh-cn' | 'en'} lang 返回中文的信息还是英文的
|
||||
* @constructor
|
||||
*/
|
||||
export default function BrowserType(lang: 'zh-cn' | 'en' = 'en') {
|
||||
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const testUa = (regexp) => regexp.test(ua);
|
||||
const testVs = (regexp) =>
|
||||
ua
|
||||
.match(regexp)
|
||||
?.toString()
|
||||
.replace(/[^0-9|_.]/g, '')
|
||||
.replace(/_/g, '.');
|
||||
// 系统
|
||||
const system =
|
||||
new Map([
|
||||
[testUa(/windows|win32|win64|wow32|wow64/g), 'windows'], // windows系统
|
||||
[testUa(/macintosh|macintel/g), 'macos'], // macos系统
|
||||
[testUa(/x11/g), 'linux'], // linux系统
|
||||
[testUa(/android|adr/g), 'android'], // android系统
|
||||
[testUa(/ios|iphone|ipad|ipod|iwatch/g), 'ios'], // ios系统
|
||||
]).get(true) || 'unknow';
|
||||
|
||||
// 系统版本
|
||||
const systemVs =
|
||||
new Map([
|
||||
[
|
||||
'windows',
|
||||
new Map([
|
||||
[testUa(/windows nt 5.0|windows 2000/g), '2000'],
|
||||
[testUa(/windows nt 5.1|windows xp/g), 'xp'],
|
||||
[testUa(/windows nt 5.2|windows 2003/g), '2003'],
|
||||
[testUa(/windows nt 6.0|windows vista/g), 'vista'],
|
||||
[testUa(/windows nt 6.1|windows 7/g), '7'],
|
||||
[testUa(/windows nt 6.2|windows 8/g), '8'],
|
||||
[testUa(/windows nt 6.3|windows 8.1/g), '8.1'],
|
||||
[testUa(/windows nt 10.0|windows 10/g), '10'],
|
||||
]).get(true),
|
||||
],
|
||||
['macos', testVs(/os x [\d._]+/g)],
|
||||
['android', testVs(/android [\d._]+/g)],
|
||||
['ios', testVs(/os [\d._]+/g)],
|
||||
]).get(system) || 'unknow';
|
||||
|
||||
// 平台
|
||||
let platform = 'unknow';
|
||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||
platform = 'desktop'; // 桌面端
|
||||
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||
platform = 'mobile'; // 移动端
|
||||
}
|
||||
// 内核和载体
|
||||
const [engine = 'unknow', supporter = 'unknow'] = new Map([
|
||||
[
|
||||
testUa(/applewebkit/g),
|
||||
[
|
||||
'webkit',
|
||||
new Map([
|
||||
// webkit内核
|
||||
[testUa(/safari/g), 'safari'], // safari浏览器
|
||||
[testUa(/chrome/g), 'chrome'], // chrome浏览器
|
||||
[testUa(/opr/g), 'opera'], // opera浏览器
|
||||
[testUa(/edge/g), 'edge'], // edge浏览器
|
||||
]).get(true),
|
||||
] || 'unknow',
|
||||
], // [webkit内核, xxx浏览器]
|
||||
[testUa(/gecko/g) && testUa(/firefox/g), ['gecko', 'firefox']], // [gecko内核,firefox浏览器]
|
||||
[testUa(/presto/g), ['presto', 'opera']], // [presto内核,opera浏览器]
|
||||
[testUa(/trident|compatible|msie/g), ['trident', 'iexplore']], // [trident内核,iexplore浏览器]
|
||||
]).get(true) || ['unknow', 'unknow'];
|
||||
|
||||
// 内核版本
|
||||
const engineVs =
|
||||
new Map([
|
||||
['webkit', testVs(/applewebkit\/[\d._]+/g)],
|
||||
['gecko', testVs(/gecko\/[\d._]+/g)],
|
||||
['presto', testVs(/presto\/[\d._]+/g)],
|
||||
['trident', testVs(/trident\/[\d._]+/g)],
|
||||
]).get(engine) || 'unknow';
|
||||
|
||||
// 载体版本
|
||||
const supporterVs =
|
||||
new Map([
|
||||
['firefox', testVs(/firefox\/[\d._]+/g)],
|
||||
['opera', testVs(/opr\/[\d._]+/g)],
|
||||
['iexplore', testVs(/(msie [\d._]+)|(rv:[\d._]+)/g)],
|
||||
['edge', testVs(/edge\/[\d._]+/g)],
|
||||
['safari', testVs(/version\/[\d._]+/g)],
|
||||
['chrome', testVs(/chrome\/[\d._]+/g)],
|
||||
]).get(supporter) || 'unknow';
|
||||
|
||||
// 外壳和外壳版本
|
||||
const [shell = 'none', shellVs = 'unknow'] = new Map([
|
||||
[testUa(/micromessenger/g), ['wechat', testVs(/micromessenger\/[\d._]+/g)]], // [微信浏览器,]
|
||||
[testUa(/qqbrowser/g), ['qq', testVs(/qqbrowser\/[\d._]+/g)]], // [QQ浏览器,]
|
||||
[testUa(/ucbrowser/g), ['uc', testVs(/ucbrowser\/[\d._]+/g)]], // [UC浏览器,]
|
||||
[testUa(/qihu 360se/g), ['360', 'unknow']], // [360浏览器(无版本),]
|
||||
[testUa(/2345explorer/g), ['2345', testVs(/2345explorer\/[\d._]+/g)]], // [2345浏览器,]
|
||||
[testUa(/metasr/g), ['sougou', 'unknow']], // [搜狗浏览器(无版本),]
|
||||
[testUa(/lbbrowser/g), ['liebao', 'unknow']], // [猎豹浏览器(无版本),]
|
||||
[testUa(/maxthon/g), ['maxthon', testVs(/maxthon\/[\d._]+/g)]], // [遨游浏览器,]
|
||||
]).get(true) || ['none', 'unknow'];
|
||||
|
||||
return {
|
||||
'zh-cn': Object.assign(
|
||||
{
|
||||
内核: engine, // 内核: webkit gecko presto trident
|
||||
内核版本: engineVs, // 内核版本
|
||||
平台: platform, // 平台: desktop mobile
|
||||
载体: supporter, // 载体: chrome safari firefox opera iexplore edge
|
||||
载体版本: supporterVs, // 载体版本
|
||||
系统: system, // 系统: windows macos linux android ios
|
||||
系统版本: systemVs, // 系统版本
|
||||
},
|
||||
shell === 'none'
|
||||
? {}
|
||||
: {
|
||||
外壳: shell, // 外壳: wechat qq uc 360 2345 sougou liebao maxthon
|
||||
外壳版本: shellVs, // 外壳版本
|
||||
},
|
||||
),
|
||||
en: Object.assign(
|
||||
{
|
||||
engine, // 内核: webkit gecko presto trident
|
||||
engineVs, // 内核版本
|
||||
platform, // 平台: desktop mobile
|
||||
supporter, // 载体: chrome safari firefox opera iexplore edge
|
||||
supporterVs, // 载体版本
|
||||
system, // 系统: windows macos linux android ios
|
||||
systemVs, // 系统版本
|
||||
},
|
||||
shell === 'none'
|
||||
? {}
|
||||
: {
|
||||
shell, // 外壳: wechat qq uc 360 2345 sougou liebao maxthon
|
||||
shellVs, // 外壳版本
|
||||
},
|
||||
),
|
||||
}[lang];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export default {
|
||||
//hex颜色转rgb颜色
|
||||
HexToRgb(str) {
|
||||
str = str.replace('#', '');
|
||||
const hxs = str.match(/../g);
|
||||
for (let i = 0; i < 3; i++) hxs[i] = parseInt(hxs[i], 16);
|
||||
return hxs;
|
||||
},
|
||||
//rgb颜色转hex颜色
|
||||
RgbToHex(a, b, c) {
|
||||
const hexs = [a.toString(16), b.toString(16), c.toString(16)];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (hexs[i].length == 1) hexs[i] = '0' + hexs[i];
|
||||
}
|
||||
return '#' + hexs.join('');
|
||||
},
|
||||
//加深
|
||||
darken(color, level) {
|
||||
const rgbc = this.HexToRgb(color);
|
||||
for (let i = 0; i < 3; i++) rgbc[i] = Math.floor(rgbc[i] * (1 - level));
|
||||
return this.RgbToHex(rgbc[0], rgbc[1], rgbc[2]);
|
||||
},
|
||||
//变淡
|
||||
lighten(color, level) {
|
||||
const rgbc = this.HexToRgb(color);
|
||||
for (let i = 0; i < 3; i++) rgbc[i] = Math.floor((255 - rgbc[i]) * level + rgbc[i]);
|
||||
return this.RgbToHex(rgbc[0], rgbc[1], rgbc[2]);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
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 {
|
||||
return dayjs(date).format(formatStr);
|
||||
}
|
||||
|
||||
export function formatToDate(date: Date, formatStr = DATE_FORMAT): string {
|
||||
return dayjs(date).format(formatStr);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { upperFirst } from 'lodash-es';
|
||||
|
||||
export interface ViewportOffsetResult {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
rightIncludeBody: number;
|
||||
bottomIncludeBody: number;
|
||||
}
|
||||
|
||||
export function getBoundingClientRect(element: Element): DOMRect | number {
|
||||
if (!element || !element.getBoundingClientRect) {
|
||||
return 0;
|
||||
}
|
||||
return element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function trim(string: string) {
|
||||
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '');
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function hasClass(el: Element, cls: string) {
|
||||
if (!el || !cls) return false;
|
||||
if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.');
|
||||
if (el.classList) {
|
||||
return el.classList.contains(cls);
|
||||
} else {
|
||||
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function addClass(el: Element, cls: string) {
|
||||
if (!el) return;
|
||||
let curClass = el.className;
|
||||
const classes = (cls || '').split(' ');
|
||||
|
||||
for (let i = 0, j = classes.length; i < j; i++) {
|
||||
const clsName = classes[i];
|
||||
if (!clsName) continue;
|
||||
|
||||
if (el.classList) {
|
||||
el.classList.add(clsName);
|
||||
} else if (!hasClass(el, clsName)) {
|
||||
curClass += ' ' + clsName;
|
||||
}
|
||||
}
|
||||
if (!el.classList) {
|
||||
el.className = curClass;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function removeClass(el: Element, cls: string) {
|
||||
if (!el || !cls) return;
|
||||
const classes = cls.split(' ');
|
||||
let curClass = ' ' + el.className + ' ';
|
||||
|
||||
for (let i = 0, j = classes.length; i < j; i++) {
|
||||
const clsName = classes[i];
|
||||
if (!clsName) continue;
|
||||
|
||||
if (el.classList) {
|
||||
el.classList.remove(clsName);
|
||||
} else if (hasClass(el, clsName)) {
|
||||
curClass = curClass.replace(' ' + clsName + ' ', ' ');
|
||||
}
|
||||
}
|
||||
if (!el.classList) {
|
||||
el.className = trim(curClass);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the left and top offset of the current element
|
||||
* left: the distance between the leftmost element and the left side of the document
|
||||
* top: the distance from the top of the element to the top of the document
|
||||
* right: the distance from the far right of the element to the right of the document
|
||||
* bottom: the distance from the bottom of the element to the bottom of the document
|
||||
* rightIncludeBody: the distance between the leftmost element and the right side of the document
|
||||
* bottomIncludeBody: the distance from the bottom of the element to the bottom of the document
|
||||
*
|
||||
* @description:
|
||||
*/
|
||||
export function getViewportOffset(element: Element): ViewportOffsetResult {
|
||||
const doc = document.documentElement;
|
||||
|
||||
const docScrollLeft = doc.scrollLeft;
|
||||
const docScrollTop = doc.scrollTop;
|
||||
const docClientLeft = doc.clientLeft;
|
||||
const docClientTop = doc.clientTop;
|
||||
|
||||
const pageXOffset = window.pageXOffset;
|
||||
const pageYOffset = window.pageYOffset;
|
||||
|
||||
const box = getBoundingClientRect(element);
|
||||
|
||||
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect;
|
||||
|
||||
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0);
|
||||
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0);
|
||||
const offsetLeft = retLeft + pageXOffset;
|
||||
const offsetTop = rectTop + pageYOffset;
|
||||
|
||||
const left = offsetLeft - scrollLeft;
|
||||
const top = offsetTop - scrollTop;
|
||||
|
||||
const clientWidth = window.document.documentElement.clientWidth;
|
||||
const clientHeight = window.document.documentElement.clientHeight;
|
||||
return {
|
||||
left: left,
|
||||
top: top,
|
||||
right: clientWidth - rectWidth - left,
|
||||
bottom: clientHeight - rectHeight - top,
|
||||
rightIncludeBody: clientWidth - left,
|
||||
bottomIncludeBody: clientHeight - top,
|
||||
};
|
||||
}
|
||||
|
||||
export function hackCss(attr: string, value: string) {
|
||||
const prefix: string[] = ['webkit', 'Moz', 'ms', 'OT'];
|
||||
|
||||
const styleObj: any = {};
|
||||
prefix.forEach((item) => {
|
||||
styleObj[`${item}${upperFirst(attr)}`] = value;
|
||||
});
|
||||
return {
|
||||
...styleObj,
|
||||
[attr]: value,
|
||||
};
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function on(
|
||||
element: Element | HTMLElement | Document | Window,
|
||||
event: string,
|
||||
handler: EventListenerOrEventListenerObject,
|
||||
): void {
|
||||
if (element && event && handler) {
|
||||
element.addEventListener(event, handler, false);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function off(
|
||||
element: Element | HTMLElement | Document | Window,
|
||||
event: string,
|
||||
handler: Fn,
|
||||
): void {
|
||||
if (element && event && handler) {
|
||||
element.removeEventListener(event, handler, false);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function once(el: HTMLElement, event: string, fn: EventListener): void {
|
||||
const listener = function (this: any, ...args: unknown[]) {
|
||||
if (fn) {
|
||||
fn.apply(this, args);
|
||||
}
|
||||
off(el, event, listener);
|
||||
};
|
||||
on(el, event, listener);
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @description: base64 to blob
|
||||
*/
|
||||
export function dataURLtoBlob(base64Buf: string): Blob {
|
||||
const arr = base64Buf.split(',');
|
||||
const typeItem = arr[0];
|
||||
const mime = typeItem.match(/:(.*?);/)![1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
}
|
||||
|
||||
/**
|
||||
* img url to base64
|
||||
* @param url
|
||||
*/
|
||||
export function urlToBase64(url: string, mineType?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>;
|
||||
const ctx = canvas!.getContext('2d');
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = '';
|
||||
img.onload = function () {
|
||||
if (!canvas || !ctx) {
|
||||
return reject();
|
||||
}
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.width;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const dataURL = canvas.toDataURL(mineType || 'image/png');
|
||||
canvas = null;
|
||||
resolve(dataURL);
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* base64 to file
|
||||
* @param dataURL
|
||||
*/
|
||||
export function base64ToFile(dataurl:any, filename:any) {
|
||||
let arr = dataurl.split(",");
|
||||
let mime = arr[0].match(/:(.*?);/)[1];
|
||||
let bstr = atob(arr[1]);
|
||||
let len = bstr.length;
|
||||
let u8arr = new Uint8Array(len);
|
||||
while (len--) {
|
||||
u8arr[len] = bstr.charCodeAt(len);
|
||||
}
|
||||
return new File([u8arr], filename, {type: mime});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { openWindow } from '..';
|
||||
import { dataURLtoBlob, urlToBase64 } from './base64Conver';
|
||||
import { TargetContext } from '/#/index';
|
||||
|
||||
/**
|
||||
* Download online pictures
|
||||
* @param url
|
||||
* @param filename
|
||||
* @param mime
|
||||
* @param bom
|
||||
*/
|
||||
export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) {
|
||||
urlToBase64(url).then((base64) => {
|
||||
downloadByBase64(base64, filename, mime, bom);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download pictures based on base64
|
||||
* @param buf
|
||||
* @param filename
|
||||
* @param mime
|
||||
* @param bom
|
||||
*/
|
||||
export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) {
|
||||
const base64Buf = dataURLtoBlob(buf);
|
||||
downloadByData(base64Buf, filename, mime, bom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download according to the background interface file stream
|
||||
* @param {*} data
|
||||
* @param {*} filename
|
||||
* @param {*} mime
|
||||
* @param {*} bom
|
||||
*/
|
||||
export function downloadByData(data: BlobPart, name: string, mime?: string, bom?: BlobPart) {
|
||||
const blobData = typeof bom !== 'undefined' ? [bom, data] : [data];
|
||||
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });
|
||||
const fileName =
|
||||
name?.match(/filename\*?=(utf-8'')?([^;]+)/i)?.[2]?.replace(/['"]/g, '') || 'export.xlsx';
|
||||
if (typeof window.navigator.msSaveBlob !== 'undefined') {
|
||||
window.navigator.msSaveBlob(blob, decodeURIComponent(fileName));
|
||||
} else {
|
||||
const blobURL = window.URL.createObjectURL(blob);
|
||||
const tempLink = document.createElement('a');
|
||||
tempLink.style.display = 'none';
|
||||
tempLink.href = blobURL;
|
||||
tempLink.setAttribute('download', decodeURIComponent(fileName));
|
||||
if (typeof tempLink.download === 'undefined') {
|
||||
tempLink.setAttribute('target', '_blank');
|
||||
}
|
||||
document.body.appendChild(tempLink);
|
||||
tempLink.click();
|
||||
document.body.removeChild(tempLink);
|
||||
window.URL.revokeObjectURL(blobURL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file according to file address
|
||||
* @param {*} sUrl
|
||||
*/
|
||||
export function downloadByUrl({
|
||||
url,
|
||||
target = '_blank',
|
||||
fileName,
|
||||
}: {
|
||||
url: string;
|
||||
target?: TargetContext;
|
||||
fileName?: string;
|
||||
}): boolean {
|
||||
const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
|
||||
const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1;
|
||||
if (/(iP)/g.test(window.navigator.userAgent)) {
|
||||
console.error('Your browser does not support download!');
|
||||
return false;
|
||||
}
|
||||
if (isChrome || isSafari) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.target = target;
|
||||
|
||||
if (link.download !== undefined) {
|
||||
link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length);
|
||||
}
|
||||
|
||||
if (document.createEvent) {
|
||||
const e = document.createEvent('MouseEvents');
|
||||
e.initEvent('click', true, true);
|
||||
link.dispatchEvent(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (url.indexOf('?') === -1) {
|
||||
url += '?download';
|
||||
}
|
||||
|
||||
openWindow(url, { target });
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 时间日期转换
|
||||
* @param date 当前时间,new Date() 格式
|
||||
* @param format 需要转换的时间格式字符串
|
||||
* @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd`
|
||||
* @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ"
|
||||
* @description format 星期:"YYYY-mm-dd HH:MM:SS WWW"
|
||||
* @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ"
|
||||
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
|
||||
* @returns 返回拼接后的时间字符串
|
||||
*/
|
||||
export function formatDate(date: Date, format: string): string {
|
||||
const we = date.getDay(); // 星期
|
||||
const z = getWeek(date); // 周
|
||||
const qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
|
||||
const opt: { [key: string]: string } = {
|
||||
'Y+': date.getFullYear().toString(), // 年
|
||||
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
|
||||
'd+': date.getDate().toString(), // 日
|
||||
'H+': date.getHours().toString(), // 时
|
||||
'M+': date.getMinutes().toString(), // 分
|
||||
'S+': date.getSeconds().toString(), // 秒
|
||||
'q+': qut, // 季度
|
||||
};
|
||||
// 中文数字 (星期)
|
||||
const week: { [key: string]: string } = {
|
||||
'0': '日',
|
||||
'1': '一',
|
||||
'2': '二',
|
||||
'3': '三',
|
||||
'4': '四',
|
||||
'5': '五',
|
||||
'6': '六',
|
||||
};
|
||||
// 中文数字(季度)
|
||||
const quarter: { [key: string]: string } = {
|
||||
'1': '一',
|
||||
'2': '二',
|
||||
'3': '三',
|
||||
'4': '四',
|
||||
};
|
||||
if (/(W+)/.test(format))
|
||||
format = format.replace(
|
||||
RegExp.$1,
|
||||
RegExp.$1.length > 1
|
||||
? RegExp.$1.length > 2
|
||||
? '星期' + week[we]
|
||||
: '周' + week[we]
|
||||
: week[we],
|
||||
);
|
||||
if (/(Q+)/.test(format))
|
||||
format = format.replace(
|
||||
RegExp.$1,
|
||||
RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut],
|
||||
);
|
||||
if (/(Z+)/.test(format))
|
||||
format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '');
|
||||
for (const k in opt) {
|
||||
const r = new RegExp('(' + k + ')').exec(format);
|
||||
// 若输入的长度不为1,则前面补零
|
||||
if (r)
|
||||
format = format.replace(
|
||||
r[1],
|
||||
RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'),
|
||||
);
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期是第几周
|
||||
* @param dateTime 当前传入的日期值
|
||||
* @returns 返回第几周数字值
|
||||
*/
|
||||
export function getWeek(dateTime: Date): number {
|
||||
const temptTime = new Date(dateTime.getTime());
|
||||
// 周几
|
||||
const weekday = temptTime.getDay() || 7;
|
||||
// 周1+5天=周六
|
||||
temptTime.setDate(temptTime.getDate() - weekday + 1 + 5);
|
||||
let firstDay = new Date(temptTime.getFullYear(), 0, 1);
|
||||
const dayOfWeek = firstDay.getDay();
|
||||
let spendDay = 1;
|
||||
if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1;
|
||||
firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay);
|
||||
const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000);
|
||||
const result = Math.ceil(d / 7);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前`
|
||||
* @param param 当前时间,new Date() 格式或者字符串时间格式
|
||||
* @param format 需要转换的时间格式字符串
|
||||
* @description param 10秒: 10 * 1000
|
||||
* @description param 1分: 60 * 1000
|
||||
* @description param 1小时: 60 * 60 * 1000
|
||||
* @description param 24小时:60 * 60 * 24 * 1000
|
||||
* @description param 3天: 60 * 60* 24 * 1000 * 3
|
||||
* @returns 返回拼接后的时间字符串
|
||||
*/
|
||||
export function formatPast(param: string | Date, format = 'YYYY-mm-dd'): string {
|
||||
// 传入格式处理、存储转换值
|
||||
let t: any, s: number;
|
||||
// 获取js 时间戳
|
||||
let time: number = new Date().getTime();
|
||||
// 是否是对象
|
||||
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
|
||||
// 当前时间戳 - 传入时间戳
|
||||
time = Number.parseInt(`${time - t}`);
|
||||
if (time < 10000) {
|
||||
// 10秒内
|
||||
return '刚刚';
|
||||
} else if (time < 60000 && time >= 10000) {
|
||||
// 超过10秒少于1分钟内
|
||||
s = Math.floor(time / 1000);
|
||||
return `${s}秒前`;
|
||||
} else if (time < 3600000 && time >= 60000) {
|
||||
// 超过1分钟少于1小时
|
||||
s = Math.floor(time / 60000);
|
||||
return `${s}分钟前`;
|
||||
} else if (time < 86400000 && time >= 3600000) {
|
||||
// 超过1小时少于24小时
|
||||
s = Math.floor(time / 3600000);
|
||||
return `${s}小时前`;
|
||||
} else if (time < 259200000 && time >= 86400000) {
|
||||
// 超过1天少于3天内
|
||||
s = Math.floor(time / 86400000);
|
||||
return `${s}天前`;
|
||||
} else {
|
||||
// 超过3天
|
||||
const date = typeof param === 'string' || 'object' ? new Date(param) : param;
|
||||
return formatDate(date, format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间问候语
|
||||
* @param param 当前时间,new Date() 格式
|
||||
* @description param 调用 `formatAxis(new Date())` 输出 `上午好`
|
||||
* @returns 返回拼接后的时间字符串
|
||||
*/
|
||||
export function formatAxis(param: Date): string {
|
||||
const hour: number = new Date(param).getHours();
|
||||
if (hour < 6) return '凌晨好';
|
||||
else if (hour < 9) return '早上好';
|
||||
else if (hour < 12) return '上午好';
|
||||
else if (hour < 14) return '中午好';
|
||||
else if (hour < 17) return '下午好';
|
||||
else if (hour < 19) return '傍晚好';
|
||||
else if (hour < 22) return '晚上好';
|
||||
else return '夜里好';
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
export function parseTime(time, pattern) {
|
||||
if (arguments.length === 0 || !time) {
|
||||
return null;
|
||||
}
|
||||
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
|
||||
let date;
|
||||
if (typeof time === 'object') {
|
||||
date = time;
|
||||
} else {
|
||||
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time);
|
||||
} else if (typeof time === 'string') {
|
||||
time = time
|
||||
.replace(new RegExp(/-/gm), '/')
|
||||
.replace('T', ' ')
|
||||
.replace(new RegExp(/\.[\d]{3}/gm), '');
|
||||
}
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000;
|
||||
}
|
||||
date = new Date(time);
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay(),
|
||||
};
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key];
|
||||
// Note: getDay() returns 0 on Sunday
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value];
|
||||
}
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0;
|
||||
});
|
||||
return time_str;
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
export function parseDate(time, pattern) {
|
||||
if (arguments.length === 0 || !time) {
|
||||
return null;
|
||||
}
|
||||
const format = pattern || '{y}-{m}-{d}';
|
||||
let date;
|
||||
if (typeof time === 'object') {
|
||||
date = time;
|
||||
} else {
|
||||
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time);
|
||||
} else if (typeof time === 'string') {
|
||||
time = time
|
||||
.replace(new RegExp(/-/gm), '/')
|
||||
.replace('T', ' ')
|
||||
.replace(new RegExp(/\.[\d]{3}/gm), '');
|
||||
}
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000;
|
||||
}
|
||||
date = new Date(time);
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay(),
|
||||
};
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key];
|
||||
// Note: getDay() returns 0 on Sunday
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value];
|
||||
}
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0;
|
||||
});
|
||||
return time_str;
|
||||
}
|
||||
|
||||
export const dateTimeStr = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
export const dateStr = 'YYYY-MM-DD';
|
||||
|
||||
export const timeStr = 'HH:mm:ss';
|
||||
|
||||
export function formatSeconds(second: any) {
|
||||
second = parseInt(second); // 秒
|
||||
let duration;
|
||||
const days = Math.floor(second / 86400);
|
||||
const hours = Math.floor((second % 86400) / 3600);
|
||||
const minutes = Math.floor(((second % 86400) % 3600) / 60);
|
||||
const seconds = Math.floor(((second % 86400) % 3600) % 60);
|
||||
if (days > 0) duration = days + '天' + hours + '时' + minutes + '分' + seconds + '秒';
|
||||
else if (hours > 0) duration = hours + '时' + minutes + '分' + seconds + '秒';
|
||||
else if (minutes > 0) duration = minutes + '分' + seconds + '秒';
|
||||
else if (seconds > 0) duration = seconds + '秒';
|
||||
return duration;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
interface TreeHelperConfig {
|
||||
id: string;
|
||||
children: string;
|
||||
pid: string;
|
||||
}
|
||||
const DEFAULT_CONFIG: TreeHelperConfig = {
|
||||
id: 'id',
|
||||
children: 'children',
|
||||
pid: 'pid',
|
||||
};
|
||||
|
||||
const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config);
|
||||
|
||||
export function filter<T = any>(tree: T[], func: (n: T) => boolean): T[] {
|
||||
const children = 'children';
|
||||
|
||||
function listFilter(list: T[]) {
|
||||
return list
|
||||
.map((node: any) => ({ ...node }))
|
||||
.filter((node) => {
|
||||
node[children] = node[children] && listFilter(node[children]);
|
||||
return func(node) || (node[children] && node[children].length);
|
||||
});
|
||||
}
|
||||
|
||||
return listFilter(tree);
|
||||
}
|
||||
|
||||
export function forEach<T = any>(
|
||||
tree: T[],
|
||||
func: (n: T) => any,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): void {
|
||||
config = getConfig(config);
|
||||
const list: any[] = [...tree];
|
||||
const { children } = config;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
//func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿
|
||||
if (func(list[i])) {
|
||||
return;
|
||||
}
|
||||
children && list[i][children] && list.splice(i + 1, 0, ...list[i][children]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Tree 中 value
|
||||
* @param data 树形数据结构
|
||||
* @param key 树形中 key 字段
|
||||
**/
|
||||
export function getTreeValues<T = any>(data: T[], key = 'key') {
|
||||
const keys: any[] = [];
|
||||
data.map((item: any) => {
|
||||
!keys.includes(item[key]) && keys.push(item[key]);
|
||||
if (item.children && item.children.length) {
|
||||
getTreeValues(item.children);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 Tree 中 字段
|
||||
* @param data 树形数据结构
|
||||
* @param labelKey 树形中 labe 字段
|
||||
* @param valueKey 树形中 value 字段
|
||||
**/
|
||||
export function replaceParams<T = any>(data: T[], labelKey: string, valueKey: string) {
|
||||
const newData: any[] = [];
|
||||
for (const item of data) {
|
||||
const info: any = item;
|
||||
if (info.children && info.children.length > 0) {
|
||||
info.children = replaceParams(info.children, labelKey, valueKey);
|
||||
}
|
||||
const param = {
|
||||
...item,
|
||||
label: item[labelKey],
|
||||
value: item[valueKey],
|
||||
};
|
||||
newData.push(param);
|
||||
}
|
||||
return newData;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Slots } from 'vue';
|
||||
import { isFunction } from '@/utils/is';
|
||||
|
||||
/**
|
||||
* @description: Get slot to prevent empty error
|
||||
*/
|
||||
export function getSlot(slots: Slots, slot = 'default', data?: any) {
|
||||
if (!slots || !Reflect.has(slots, slot)) {
|
||||
return null;
|
||||
}
|
||||
if (!isFunction(slots[slot])) {
|
||||
console.error(`${slot} is not a function!`);
|
||||
return null;
|
||||
}
|
||||
const slotFn = slots[slot];
|
||||
if (!slotFn) return null;
|
||||
return slotFn(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* extends slots
|
||||
* @param slots
|
||||
* @param excludeKeys
|
||||
*/
|
||||
export function extendSlots(slots: Slots, excludeKeys: string[] = []) {
|
||||
const slotKeys = Object.keys(slots);
|
||||
const ret: any = {};
|
||||
slotKeys.map((key) => {
|
||||
if (excludeKeys.includes(key)) {
|
||||
return null;
|
||||
}
|
||||
ret[key] = () => getSlot(slots, key);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios, { AxiosRequestConfig, Canceler } from 'axios';
|
||||
import qs from 'qs';
|
||||
|
||||
import { isFunction } from '@/utils/is/index';
|
||||
|
||||
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
|
||||
let pendingMap = new Map<string, Canceler>();
|
||||
|
||||
export const getPendingUrl = (config: AxiosRequestConfig) =>
|
||||
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join('&');
|
||||
|
||||
export class AxiosCanceler {
|
||||
/**
|
||||
* 添加请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
addPending(config: AxiosRequestConfig) {
|
||||
this.removePending(config);
|
||||
const url = getPendingUrl(config);
|
||||
config.cancelToken =
|
||||
config.cancelToken ||
|
||||
new axios.CancelToken((cancel) => {
|
||||
if (!pendingMap.has(url)) {
|
||||
// 如果 pending 中不存在当前请求,则添加进去
|
||||
pendingMap.set(url, cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 清空所有pending
|
||||
*/
|
||||
removeAllPending() {
|
||||
pendingMap.forEach((cancel) => {
|
||||
cancel && isFunction(cancel) && cancel();
|
||||
});
|
||||
pendingMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
removePending(config: AxiosRequestConfig) {
|
||||
const url = getPendingUrl(config);
|
||||
|
||||
if (pendingMap.has(url)) {
|
||||
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
|
||||
const cancel = pendingMap.get(url);
|
||||
cancel && cancel(url);
|
||||
pendingMap.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 重置
|
||||
*/
|
||||
reset(): void {
|
||||
pendingMap = new Map<string, Canceler>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 数据处理类,可以根据项目自行配置
|
||||
*/
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import type { RequestOptions, Result } from './types';
|
||||
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||
authenticationScheme?: string;
|
||||
transform?: AxiosTransform;
|
||||
requestOptions?: RequestOptions;
|
||||
}
|
||||
|
||||
export abstract class AxiosTransform {
|
||||
/**
|
||||
* @description: 请求之前处理配置
|
||||
* @description: Process configuration before request
|
||||
*/
|
||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 请求成功处理
|
||||
*/
|
||||
transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
|
||||
|
||||
/**
|
||||
* @description: 请求失败处理
|
||||
*/
|
||||
requestCatch?: (e: Error) => Promise<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器
|
||||
*/
|
||||
requestInterceptors?: (
|
||||
config: AxiosRequestConfig,
|
||||
options: CreateAxiosOptions,
|
||||
) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器
|
||||
*/
|
||||
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器错误处理
|
||||
*/
|
||||
requestInterceptorsCatch?: (error: Error) => void;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器错误处理
|
||||
*/
|
||||
responseInterceptorsCatch?: (error: Error) => void;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ElMessage } from 'element-plus';
|
||||
export function checkStatus(status: number, msg: string): void {
|
||||
switch (status) {
|
||||
case 400:
|
||||
ElMessage.error(msg);
|
||||
break;
|
||||
// 401: 未登录
|
||||
// 未登录则跳转登录页面,并携带当前页面的路径
|
||||
// 在登录成功后返回当前页面,这一步需要在登录页操作。
|
||||
case 401:
|
||||
ElMessage.error('用户没有权限(令牌、用户名、密码错误)!');
|
||||
break;
|
||||
case 403:
|
||||
ElMessage.error('用户得到授权,但是访问是被禁止的。!');
|
||||
break;
|
||||
// 404请求不存在
|
||||
case 404:
|
||||
ElMessage.error('网络请求错误,未找到该资源!');
|
||||
break;
|
||||
case 405:
|
||||
ElMessage.error('网络请求错误,请求方法未允许!');
|
||||
break;
|
||||
case 408:
|
||||
ElMessage.error('网络请求超时');
|
||||
break;
|
||||
case 500:
|
||||
ElMessage.error('服务器错误,请联系管理员!');
|
||||
break;
|
||||
case 501:
|
||||
ElMessage.error('网络未实现');
|
||||
break;
|
||||
case 502:
|
||||
ElMessage.error('网络错误');
|
||||
break;
|
||||
case 503:
|
||||
ElMessage.error('服务不可用,服务器暂时过载或维护!');
|
||||
break;
|
||||
case 504:
|
||||
ElMessage.error('网络超时');
|
||||
break;
|
||||
case 505:
|
||||
ElMessage.error('http版本不支持该请求!');
|
||||
break;
|
||||
default:
|
||||
ElMessage.error(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { isObject, isString } from '@/utils/is';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
export function joinTimestamp<T extends boolean>(
|
||||
join: boolean,
|
||||
restful: T,
|
||||
): T extends true ? string : object;
|
||||
|
||||
export function joinTimestamp(join: boolean, restful = false): string | object {
|
||||
if (!join) {
|
||||
return restful ? '' : {};
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
if (restful) {
|
||||
return `?_t=${now}`;
|
||||
}
|
||||
return { _t: now };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Format request parameter time
|
||||
*/
|
||||
export function formatRequestDate(params: Recordable) {
|
||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in params) {
|
||||
if (params[key] && params[key]._isAMomentObject) {
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT);
|
||||
}
|
||||
if (isString(key)) {
|
||||
const value = params[key];
|
||||
if (value) {
|
||||
try {
|
||||
params[key] = isString(value) ? value.trim() : value;
|
||||
} catch (error) {
|
||||
throw new Error(error as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isObject(params[key])) {
|
||||
formatRequestDate(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
|
||||
import { VAxios } from './Axios';
|
||||
import { AxiosTransform } from './axiosTransform';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { checkStatus } from './checkStatus';
|
||||
import { joinTimestamp, formatRequestDate } from './helper';
|
||||
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
|
||||
import { isString } from '@/utils/is/';
|
||||
import { deepMerge, isUrl } from '@/utils';
|
||||
import { setObjToUrlParams } from '@/utils/urlUtils';
|
||||
|
||||
import { RequestOptions, Result, CreateAxiosOptions } from './types';
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix || '';
|
||||
|
||||
import router from '@/router';
|
||||
import { storage } from '@/utils/Storage';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
if (!res.data) {
|
||||
// return '[HTTP] Request has no return value';
|
||||
throw new Error('请求出错,请稍候重试');
|
||||
}
|
||||
// 这里 code,result,message为 后台统一的字段,需要修改为项目自己的接口返回格式
|
||||
const { code, data, msg } = res.data;
|
||||
// 请求成功
|
||||
const hasSuccess = data && code === ResultEnum.SUCCESS;
|
||||
// 是否显示提示信息
|
||||
if (isShowMessage) {
|
||||
if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
|
||||
// 是否显示自定义信息提示
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: successMessageText || message || '操作成功!',
|
||||
});
|
||||
} else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
|
||||
// 是否显示自定义信息提示
|
||||
ElMessage.error(msg || errorMessageText || '操作失败!');
|
||||
} else if (!hasSuccess && options.errorMessageMode === 'modal') {
|
||||
// errorMessageMode=‘custom-modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||
ElMessageBox.confirm(msg, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
showCancelButton: false,
|
||||
closeOnClickModal:false,
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: 'Delete completed',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: 'Delete canceled',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 接口请求成功,直接返回结果
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
return data;
|
||||
}
|
||||
// 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
|
||||
let errorMsg = msg;
|
||||
switch (code) {
|
||||
// 请求失败
|
||||
case ResultEnum.ERROR:
|
||||
ElMessage.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 = '登录超时,请重新登录!';
|
||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
showCancelButton: false,
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
storage.clear();
|
||||
window.location.href = LoginPath;
|
||||
})
|
||||
.catch(() => {});
|
||||
break;
|
||||
}
|
||||
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 instanceof FormData)
|
||||
) {
|
||||
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 token = storage.get('ACCESS-TOKEN', '');
|
||||
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 { response, code, msg } = error || {};
|
||||
// TODO 此处要根据后端接口返回格式修改
|
||||
const message: string =
|
||||
response && response.data && response.data.msg ? response.data.msg : '';
|
||||
const err: string = error.toString();
|
||||
try {
|
||||
if (code === 'ECONNABORTED' && msg.indexOf('timeout') !== -1) {
|
||||
ElMessage.error('接口请求超时,请刷新页面重试!');
|
||||
return;
|
||||
}
|
||||
if (err && err.includes('Network Error')) {
|
||||
ElMessageBox.confirm('请检查您的网络连接是否正常', '网络异常', {
|
||||
confirmButtonText: '确定',
|
||||
showCancelButton: false,
|
||||
type: 'warning',
|
||||
})
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
.then(() => { })
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
.catch(() => { });
|
||||
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, message);
|
||||
} 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: 'Bearer',
|
||||
// 接口前缀
|
||||
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: true,
|
||||
// 忽略重复请求
|
||||
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',
|
||||
// },
|
||||
// });
|
||||
@@ -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;
|
||||
result?: T;
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { h, unref } from 'vue';
|
||||
import type { App, Plugin } from 'vue';
|
||||
import { ElIcon, ElTag } from 'element-plus';
|
||||
import { PageEnum } from '@/enums/pageEnum';
|
||||
import { isObject, isString, isNumber } from './is/index';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { TargetContext } from '/#/index';
|
||||
|
||||
/**
|
||||
* render 图标
|
||||
* */
|
||||
export function renderIcon(icon) {
|
||||
return () => h(ElIcon, 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(
|
||||
ElTag 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,
|
||||
title: 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);
|
||||
}
|
||||
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,
|
||||
title: info.meta?.title,
|
||||
key: info.name,
|
||||
};
|
||||
firstRouter.push(currentMenu);
|
||||
});
|
||||
return firstRouter;
|
||||
} else {
|
||||
const currentRouters = newRouter.filter((item) => item.name === routerName);
|
||||
const childrenRouter = currentRouters.length ? currentRouters[0].children || [] : [];
|
||||
return getChildrenRouter(childrenRouter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归组装子菜单
|
||||
* */
|
||||
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,
|
||||
title: 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) {
|
||||
return item.meta?.alwaysShow === true && item.children?.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除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)}`;
|
||||
}
|
||||
|
||||
export function openWindow(
|
||||
url: string,
|
||||
opt?: { target?: TargetContext | string; noopener?: boolean; noreferrer?: boolean },
|
||||
) {
|
||||
const { target = '__blank', noopener = true, noreferrer = true } = opt || {};
|
||||
const feature: string[] = [];
|
||||
|
||||
noopener && feature.push('noopener=yes');
|
||||
noreferrer && feature.push('noreferrer=yes');
|
||||
debugger;
|
||||
window.open(url, target, feature.join(','));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理css单位
|
||||
* */
|
||||
export function cssUnit(value: string | number, unit = 'px') {
|
||||
return isNumber(value) || (isString(value) && value.indexOf(unit as string) === -1)
|
||||
? `${value}${unit}`
|
||||
: value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否 url
|
||||
* */
|
||||
export function isUrl(url: string) {
|
||||
return /(^http|https:\/\/)/g.test(url);
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as echarts from 'echarts/core';
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
RadarChart,
|
||||
GaugeChart,
|
||||
} 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,
|
||||
GaugeChart,
|
||||
SVGRenderer,
|
||||
PictorialBarChart,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
]);
|
||||
|
||||
export default echarts;
|
||||
@@ -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';
|
||||
@@ -0,0 +1,9 @@
|
||||
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
|
||||
|
||||
export function warn(message: string) {
|
||||
console.warn(`[${projectName} warn]:${message}`);
|
||||
}
|
||||
|
||||
export function error(message: string) {
|
||||
throw new Error(`[${projectName} error]:${message}`);
|
||||
}
|
||||
@@ -0,0 +1,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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export function useLockFn(fn: (...args: any[]) => Promise<any>) {
|
||||
const isLock = ref(false);
|
||||
const lockFn = async (...args: any[]) => {
|
||||
if (isLock.value) return;
|
||||
isLock.value = true;
|
||||
try {
|
||||
await fn(...args);
|
||||
} finally {
|
||||
isLock.value = false;
|
||||
}
|
||||
};
|
||||
return {
|
||||
isLock,
|
||||
lockFn,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 判断是否为空
|
||||
* @param val 数据
|
||||
*/
|
||||
export const validateNull = (val: any) => {
|
||||
if (typeof val === 'boolean') {
|
||||
return false;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
return false;
|
||||
}
|
||||
if (val instanceof Array) {
|
||||
if (val.length === 0) return true;
|
||||
} else if (val instanceof Object) {
|
||||
if (JSON.stringify(val) === '{}') return true;
|
||||
} else {
|
||||
if (val === 'null' || val == null || val === 'undefined' || val === undefined || val === '')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const rule = {
|
||||
/**
|
||||
* 校验 请输入中文、英文、数字包括下划线
|
||||
* 名称校验
|
||||
*/
|
||||
validatorNameCn(rule: any, value: any, callback: any) {
|
||||
const acount = /^[\u4E00-\u9FA5A-Za-z0-9_]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入中文、英文、数字包括下划线'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 校验 请输入大写英文、下划线
|
||||
* 名称校验
|
||||
*/
|
||||
validatorCapital(rule: any, value: any, callback: any) {
|
||||
const acount = /^[A-Z_]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入大写英文、下划线'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验 请输入小写英文、下划线
|
||||
* 名称校验
|
||||
*/
|
||||
validatorLowercase(rule: any, value: any, callback: any) {
|
||||
const acount = /^[a-z_]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入小写英文、下划线'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验 请输入小写英文
|
||||
* 名称校验
|
||||
*/
|
||||
validatorLower(rule: any, value: any, callback: any) {
|
||||
const acount = /^[a-z]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入小写英文'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验首尾空白字符的正则表达式
|
||||
*
|
||||
*/
|
||||
checkSpace(rule: any, value: any, callback: any) {
|
||||
const longrg = /[^\s]+$/;
|
||||
if (!longrg.test(value)) {
|
||||
callback(new Error('请输入非空格信息'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验手机号
|
||||
*/
|
||||
validatePhone(rule: any, value: any, callback: any) {
|
||||
const isPhone = /^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
|
||||
|
||||
if (value.indexOf('****') >= 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!isPhone.test(value)) {
|
||||
callback(new Error('请输入合法手机号'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/* 数字 */
|
||||
number(rule, value, callback) {
|
||||
validateFn('number', rule, value, callback, '包含非数字字符');
|
||||
},
|
||||
|
||||
/* 字母 */
|
||||
letter(rule, value, callback) {
|
||||
validateFn('letter', rule, value, callback, '包含非字母字符');
|
||||
},
|
||||
|
||||
/* 字母和数字 */
|
||||
letterAndNumber(rule, value, callback) {
|
||||
validateFn('letterAndNumber', rule, value, callback, '只能输入字母或数字');
|
||||
},
|
||||
|
||||
/* 手机号码 */
|
||||
mobilePhone(rule, value, callback) {
|
||||
validateFn('mobilePhone', rule, value, callback, '手机号码格式有误');
|
||||
},
|
||||
|
||||
/* 字母开头,仅可包含数字 */
|
||||
letterStartNumberIncluded(rule, value, callback) {
|
||||
validateFn('letterStartNumberIncluded', rule, value, callback, '必须以字母开头,可包含数字');
|
||||
},
|
||||
|
||||
/* 禁止中文输入 */
|
||||
noChinese(rule, value, callback) {
|
||||
validateFn('noChinese', rule, value, callback, '不可输入中文字符');
|
||||
},
|
||||
|
||||
/* 必须中文输入 */
|
||||
chinese(rule, value, callback) {
|
||||
validateFn('chinese', rule, value, callback, '只能输入中文字符');
|
||||
},
|
||||
|
||||
/* 电子邮箱 */
|
||||
email(rule, value, callback) {
|
||||
validateFn('email', rule, value, callback, '邮箱格式有误');
|
||||
},
|
||||
|
||||
/* URL网址 */
|
||||
url(rule, value, callback) {
|
||||
validateFn('url', rule, value, callback, 'URL格式有误');
|
||||
},
|
||||
|
||||
regExp(rule, value, callback) {
|
||||
if (validateNull(value) || value.length <= 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = new RegExp(rule.regExp);
|
||||
|
||||
if (!pattern.test(value)) {
|
||||
const errTxt = rule.errorMsg || 'invalid value';
|
||||
callback(new Error(errTxt));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc [自定义校验规则]
|
||||
* @example
|
||||
* import { validateRule } from "@/utils/validateRules";
|
||||
* rules: [
|
||||
* { validator: validateRule.emailValue, trigger: 'blur'}
|
||||
* ]
|
||||
*/
|
||||
|
||||
export const getRegExp = function (validatorName) {
|
||||
const commonRegExp = {
|
||||
number: '^[-]?\\d+(\\.\\d+)?$',
|
||||
letter: '^[A-Za-z]+$',
|
||||
letterAndNumber: '^[A-Za-z0-9]+$',
|
||||
mobilePhone: '^[1][3-9][0-9]{9}$',
|
||||
letterStartNumberIncluded: '^[A-Za-z]+[A-Za-z\\d]*$',
|
||||
noChinese: '^[^\u4e00-\u9fa5]+$',
|
||||
chinese: '^[\u4e00-\u9fa5]+$',
|
||||
email: '^([-_A-Za-z0-9.]+)@([_A-Za-z0-9]+\\.)+[A-Za-z0-9]{2,3}$',
|
||||
url: '(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]',
|
||||
};
|
||||
return commonRegExp[validatorName];
|
||||
};
|
||||
|
||||
const validateFn = (validatorName, rule, value, callback, defaultErrorMsg) => {
|
||||
if (validateNull(value) || value.length <= 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const reg = new RegExp(getRegExp(validatorName));
|
||||
|
||||
if (!reg.test(value)) {
|
||||
const errTxt = rule.errorMsg || defaultErrorMsg;
|
||||
callback(new Error(errTxt));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
// 页面添加水印效果
|
||||
const setWatermark = (str: string) => {
|
||||
const id = '1.23452384164.123412416';
|
||||
if (document.getElementById(id) !== null)
|
||||
document.body.removeChild(<HTMLElement>document.getElementById(id));
|
||||
const can = document.createElement('canvas');
|
||||
can.width = 250;
|
||||
can.height = 180;
|
||||
const cans = <CanvasRenderingContext2D>can.getContext('2d');
|
||||
cans.rotate((-20 * Math.PI) / 180);
|
||||
cans.font = '18px Vedana';
|
||||
cans.fillStyle = 'rgba(200, 200, 200, 0.40)';
|
||||
cans.textBaseline = 'middle';
|
||||
cans.fillText(str, can.width / 10, can.height / 2);
|
||||
const div = document.createElement('div');
|
||||
div.id = id;
|
||||
div.style.pointerEvents = 'none';
|
||||
div.style.top = '0px';
|
||||
div.style.left = '0px';
|
||||
div.style.position = 'fixed';
|
||||
div.style.zIndex = '10000000';
|
||||
div.style.width = `${document.documentElement.clientWidth}px`;
|
||||
div.style.height = `${document.documentElement.clientHeight}px`;
|
||||
div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`;
|
||||
document.body.appendChild(div);
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 页面添加水印效果
|
||||
* @method set 设置水印
|
||||
* @method del 删除水印
|
||||
*/
|
||||
const watermark = {
|
||||
// 设置水印
|
||||
set: (str: string) => {
|
||||
let id = setWatermark(str);
|
||||
if (document.getElementById(id) === null) id = setWatermark(str);
|
||||
},
|
||||
// 删除水印
|
||||
del: () => {
|
||||
const id = '1.23452384164.123412416';
|
||||
if (document.getElementById(id) !== null)
|
||||
document.body.removeChild(<HTMLElement>document.getElementById(id));
|
||||
},
|
||||
};
|
||||
|
||||
// 导出方法
|
||||
export default watermark;
|
||||
Reference in New Issue
Block a user