初始版本提交
This commit is contained in:
26
src/utils/auth/index.ts
Normal file
26
src/utils/auth/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Persistent, BasicKeys } from '/@/utils/cache/persistent';
|
||||
import { CacheTypeEnum } from '/@/enums/cacheEnum';
|
||||
import projectSetting from '/@/settings/projectSetting';
|
||||
import { TOKEN_KEY } from '/@/enums/cacheEnum';
|
||||
|
||||
const { permissionCacheType } = projectSetting;
|
||||
const isLocal = permissionCacheType === CacheTypeEnum.LOCAL;
|
||||
|
||||
export function getToken() {
|
||||
return getAuthCache(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getAuthCache<T>(key: BasicKeys) {
|
||||
const fn = isLocal ? Persistent.getLocal : Persistent.getSession;
|
||||
return fn(key) as T;
|
||||
}
|
||||
|
||||
export function setAuthCache(key: BasicKeys, value) {
|
||||
const fn = isLocal ? Persistent.setLocal : Persistent.setSession;
|
||||
return fn(key, value, true);
|
||||
}
|
||||
|
||||
export function clearAuthCache(immediate = true) {
|
||||
const fn = isLocal ? Persistent.clearLocal : Persistent.clearSession;
|
||||
return fn(immediate);
|
||||
}
|
||||
52
src/utils/bem.ts
Normal file
52
src/utils/bem.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { prefixCls } from '/@/settings/designSetting';
|
||||
|
||||
type Mod = string | { [key: string]: any };
|
||||
type Mods = Mod | Mod[];
|
||||
|
||||
export type BEM = ReturnType<typeof createBEM>;
|
||||
|
||||
function genBem(name: string, mods?: Mods): string {
|
||||
if (!mods) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof mods === 'string') {
|
||||
return ` ${name}--${mods}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(mods)) {
|
||||
return mods.reduce<string>((ret, item) => ret + genBem(name, item), '');
|
||||
}
|
||||
|
||||
return Object.keys(mods).reduce((ret, key) => ret + (mods[key] ? genBem(name, key) : ''), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* bem helper
|
||||
* b() // 'button'
|
||||
* b('text') // 'button__text'
|
||||
* b({ disabled }) // 'button button--disabled'
|
||||
* b('text', { disabled }) // 'button__text button__text--disabled'
|
||||
* b(['disabled', 'primary']) // 'button button--disabled button--primary'
|
||||
*/
|
||||
export function buildBEM(name: string) {
|
||||
return (el?: Mods, mods?: Mods): Mods => {
|
||||
if (el && typeof el !== 'string') {
|
||||
mods = el;
|
||||
el = '';
|
||||
}
|
||||
|
||||
el = el ? `${name}__${el}` : name;
|
||||
|
||||
return `${el}${genBem(el, mods)}`;
|
||||
};
|
||||
}
|
||||
|
||||
export function createBEM(name: string) {
|
||||
return [buildBEM(`${prefixCls}-${name}`)];
|
||||
}
|
||||
|
||||
export function createNamespace(name: string) {
|
||||
const prefixedName = `${prefixCls}-${name}`;
|
||||
return [prefixedName, buildBEM(prefixedName)] as const;
|
||||
}
|
||||
32
src/utils/cache/index.ts
vendored
Normal file
32
src/utils/cache/index.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import { getStorageShortName } from '/@/utils/env';
|
||||
import { createStorage as create, CreateStorageParams } from './storageCache';
|
||||
import { enableStorageEncryption } from '/@/settings/encryptionSetting';
|
||||
import { DEFAULT_CACHE_TIME } from '/@/settings/encryptionSetting';
|
||||
|
||||
export type Options = Partial<CreateStorageParams>;
|
||||
|
||||
const createOptions = (storage: Storage, options: Options = {}): Options => {
|
||||
return {
|
||||
// No encryption in debug mode
|
||||
hasEncrypt: enableStorageEncryption,
|
||||
storage,
|
||||
prefixKey: getStorageShortName(),
|
||||
...options,
|
||||
};
|
||||
};
|
||||
|
||||
export const WebStorage = create(createOptions(sessionStorage));
|
||||
|
||||
export const createStorage = (storage: Storage = sessionStorage, options: Options = {}) => {
|
||||
return create(createOptions(storage, options));
|
||||
};
|
||||
|
||||
export const createSessionStorage = (options: Options = {}) => {
|
||||
return createStorage(sessionStorage, { ...options, timeout: DEFAULT_CACHE_TIME });
|
||||
};
|
||||
|
||||
export const createLocalStorage = (options: Options = {}) => {
|
||||
return createStorage(localStorage, { ...options, timeout: DEFAULT_CACHE_TIME });
|
||||
};
|
||||
|
||||
export default WebStorage;
|
||||
107
src/utils/cache/memory.ts
vendored
Normal file
107
src/utils/cache/memory.ts
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
export interface Cache<V = any> {
|
||||
value?: V;
|
||||
timeoutId?: ReturnType<typeof setTimeout>;
|
||||
time?: number;
|
||||
alive?: number;
|
||||
}
|
||||
|
||||
const NOT_ALIVE = 0;
|
||||
|
||||
export class Memory<T = any, V = any> {
|
||||
private cache: { [key in keyof T]?: Cache<V> } = {};
|
||||
private alive: number;
|
||||
|
||||
constructor(alive = NOT_ALIVE) {
|
||||
// Unit second
|
||||
this.alive = alive * 1000;
|
||||
}
|
||||
|
||||
get getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
setCache(cache) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
// get<K extends keyof T>(key: K) {
|
||||
// const item = this.getItem(key);
|
||||
// const time = item?.time;
|
||||
// if (!isNullOrUnDef(time) && time < new Date().getTime()) {
|
||||
// this.remove(key);
|
||||
// }
|
||||
// return item?.value ?? undefined;
|
||||
// }
|
||||
|
||||
get<K extends keyof T>(key: K) {
|
||||
return this.cache[key];
|
||||
}
|
||||
|
||||
set<K extends keyof T>(key: K, value: V, expires?: number) {
|
||||
let item = this.get(key);
|
||||
|
||||
if (!expires || (expires as number) <= 0) {
|
||||
expires = this.alive;
|
||||
}
|
||||
if (item) {
|
||||
if (item.timeoutId) {
|
||||
clearTimeout(item.timeoutId);
|
||||
item.timeoutId = undefined;
|
||||
}
|
||||
item.value = value;
|
||||
} else {
|
||||
item = { value, alive: expires };
|
||||
this.cache[key] = item;
|
||||
}
|
||||
|
||||
if (!expires) {
|
||||
return value;
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
/**
|
||||
* Prevent overflow of the setTimeout Maximum delay value
|
||||
* Maximum delay value 2,147,483,647 ms
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
|
||||
*/
|
||||
item.time = expires > now ? expires : now + expires;
|
||||
item.timeoutId = setTimeout(
|
||||
() => {
|
||||
this.remove(key);
|
||||
},
|
||||
expires > now ? expires - now : expires,
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
remove<K extends keyof T>(key: K) {
|
||||
const item = this.get(key);
|
||||
Reflect.deleteProperty(this.cache, key);
|
||||
if (item) {
|
||||
clearTimeout(item.timeoutId!);
|
||||
return item.value;
|
||||
}
|
||||
}
|
||||
|
||||
resetCache(cache: { [K in keyof T]: Cache }) {
|
||||
Object.keys(cache).forEach((key) => {
|
||||
const k = key as any as keyof T;
|
||||
const item = cache[k];
|
||||
if (item && item.time) {
|
||||
const now = new Date().getTime();
|
||||
const expire = item.time;
|
||||
if (expire > now) {
|
||||
this.set(k, item.value, expire);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
Object.keys(this.cache).forEach((key) => {
|
||||
const item = this.cache[key];
|
||||
item.timeoutId && clearTimeout(item.timeoutId);
|
||||
});
|
||||
this.cache = {};
|
||||
}
|
||||
}
|
||||
132
src/utils/cache/persistent.ts
vendored
Normal file
132
src/utils/cache/persistent.ts
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
import type { LockInfo, UserInfo } from '/#/store';
|
||||
import type { ProjectConfig } from '/#/config';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
import { createLocalStorage, createSessionStorage } from '/@/utils/cache';
|
||||
import { Memory } from './memory';
|
||||
import {
|
||||
TOKEN_KEY,
|
||||
USER_INFO_KEY,
|
||||
ROLES_KEY,
|
||||
LOCK_INFO_KEY,
|
||||
PROJ_CFG_KEY,
|
||||
APP_LOCAL_CACHE_KEY,
|
||||
APP_SESSION_CACHE_KEY,
|
||||
MULTIPLE_TABS_KEY,
|
||||
} from '/@/enums/cacheEnum';
|
||||
import { DEFAULT_CACHE_TIME } from '/@/settings/encryptionSetting';
|
||||
import { toRaw } from 'vue';
|
||||
import { pick, omit } from 'lodash-es';
|
||||
|
||||
interface BasicStore {
|
||||
[TOKEN_KEY]: string | number | null | undefined;
|
||||
[USER_INFO_KEY]: UserInfo;
|
||||
[ROLES_KEY]: string[];
|
||||
[LOCK_INFO_KEY]: LockInfo;
|
||||
[PROJ_CFG_KEY]: ProjectConfig;
|
||||
[MULTIPLE_TABS_KEY]: RouteLocationNormalized[];
|
||||
}
|
||||
|
||||
type LocalStore = BasicStore;
|
||||
|
||||
type SessionStore = BasicStore;
|
||||
|
||||
export type BasicKeys = keyof BasicStore;
|
||||
type LocalKeys = keyof LocalStore;
|
||||
type SessionKeys = keyof SessionStore;
|
||||
|
||||
const ls = createLocalStorage();
|
||||
const ss = createSessionStorage();
|
||||
|
||||
const localMemory = new Memory(DEFAULT_CACHE_TIME);
|
||||
const sessionMemory = new Memory(DEFAULT_CACHE_TIME);
|
||||
|
||||
function initPersistentMemory() {
|
||||
const localCache = ls.get(APP_LOCAL_CACHE_KEY);
|
||||
const sessionCache = ss.get(APP_SESSION_CACHE_KEY);
|
||||
localCache && localMemory.resetCache(localCache);
|
||||
sessionCache && sessionMemory.resetCache(sessionCache);
|
||||
}
|
||||
|
||||
export class Persistent {
|
||||
static getLocal<T>(key: LocalKeys) {
|
||||
return localMemory.get(key)?.value as Nullable<T>;
|
||||
}
|
||||
|
||||
static setLocal(key: LocalKeys, value: LocalStore[LocalKeys], immediate = false): void {
|
||||
localMemory.set(key, toRaw(value));
|
||||
immediate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache);
|
||||
}
|
||||
|
||||
static removeLocal(key: LocalKeys, immediate = false): void {
|
||||
localMemory.remove(key);
|
||||
immediate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache);
|
||||
}
|
||||
|
||||
static clearLocal(immediate = false): void {
|
||||
localMemory.clear();
|
||||
immediate && ls.clear();
|
||||
}
|
||||
|
||||
static getSession<T>(key: SessionKeys) {
|
||||
return sessionMemory.get(key)?.value as Nullable<T>;
|
||||
}
|
||||
|
||||
static setSession(key: SessionKeys, value: SessionStore[SessionKeys], immediate = false): void {
|
||||
sessionMemory.set(key, toRaw(value));
|
||||
immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache);
|
||||
}
|
||||
|
||||
static removeSession(key: SessionKeys, immediate = false): void {
|
||||
sessionMemory.remove(key);
|
||||
immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache);
|
||||
}
|
||||
static clearSession(immediate = false): void {
|
||||
sessionMemory.clear();
|
||||
immediate && ss.clear();
|
||||
}
|
||||
|
||||
static clearAll(immediate = false) {
|
||||
sessionMemory.clear();
|
||||
localMemory.clear();
|
||||
if (immediate) {
|
||||
ls.clear();
|
||||
ss.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', function () {
|
||||
// TOKEN_KEY 在登录或注销时已经写入到storage了,此处为了解决同时打开多个窗口时token不同步的问题
|
||||
// LOCK_INFO_KEY 在锁屏和解锁时写入,此处也不应修改
|
||||
ls.set(APP_LOCAL_CACHE_KEY, {
|
||||
...omit(localMemory.getCache, LOCK_INFO_KEY),
|
||||
...pick(ls.get(APP_LOCAL_CACHE_KEY), [TOKEN_KEY, USER_INFO_KEY, LOCK_INFO_KEY]),
|
||||
});
|
||||
ss.set(APP_SESSION_CACHE_KEY, {
|
||||
...omit(sessionMemory.getCache, LOCK_INFO_KEY),
|
||||
...pick(ss.get(APP_SESSION_CACHE_KEY), [TOKEN_KEY, USER_INFO_KEY, LOCK_INFO_KEY]),
|
||||
});
|
||||
});
|
||||
|
||||
function storageChange(e: any) {
|
||||
const { key, newValue, oldValue } = e;
|
||||
|
||||
if (!key) {
|
||||
Persistent.clearAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!newValue && !!oldValue) {
|
||||
if (APP_LOCAL_CACHE_KEY === key) {
|
||||
Persistent.clearLocal();
|
||||
}
|
||||
if (APP_SESSION_CACHE_KEY === key) {
|
||||
Persistent.clearSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('storage', storageChange);
|
||||
|
||||
initPersistentMemory();
|
||||
111
src/utils/cache/storageCache.ts
vendored
Normal file
111
src/utils/cache/storageCache.ts
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
import { cacheCipher } from '/@/settings/encryptionSetting';
|
||||
import type { EncryptionParams } from '/@/utils/cipher';
|
||||
import { AesEncryption } from '/@/utils/cipher';
|
||||
import { isNullOrUnDef } from '/@/utils/is';
|
||||
|
||||
export interface CreateStorageParams extends EncryptionParams {
|
||||
prefixKey: string;
|
||||
storage: Storage;
|
||||
hasEncrypt: boolean;
|
||||
timeout?: Nullable<number>;
|
||||
}
|
||||
export const createStorage = ({
|
||||
prefixKey = '',
|
||||
storage = sessionStorage,
|
||||
key = cacheCipher.key,
|
||||
iv = cacheCipher.iv,
|
||||
timeout = null,
|
||||
hasEncrypt = true,
|
||||
}: Partial<CreateStorageParams> = {}) => {
|
||||
if (hasEncrypt && [key.length, iv.length].some((item) => item !== 16)) {
|
||||
throw new Error('When hasEncrypt is true, the key or iv must be 16 bits!');
|
||||
}
|
||||
|
||||
const encryption = new AesEncryption({ key, iv });
|
||||
|
||||
/**
|
||||
* Cache class
|
||||
* Construction parameters can be passed into sessionStorage, localStorage,
|
||||
* @class Cache
|
||||
* @example
|
||||
*/
|
||||
const WebStorage = class WebStorage {
|
||||
private storage: Storage;
|
||||
private prefixKey?: string;
|
||||
private encryption: AesEncryption;
|
||||
private hasEncrypt: boolean;
|
||||
/**
|
||||
*
|
||||
* @param {*} storage
|
||||
*/
|
||||
constructor() {
|
||||
this.storage = storage;
|
||||
this.prefixKey = prefixKey;
|
||||
this.encryption = encryption;
|
||||
this.hasEncrypt = hasEncrypt;
|
||||
}
|
||||
|
||||
private getKey(key: string) {
|
||||
return `${this.prefixKey}${key}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
* @param {*} expire Expiration time in seconds
|
||||
* @memberof Cache
|
||||
*/
|
||||
set(key: string, value: any, expire: number | null = timeout) {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
time: Date.now(),
|
||||
expire: !isNullOrUnDef(expire) ? new Date().getTime() + expire * 1000 : null,
|
||||
});
|
||||
const stringifyValue = this.hasEncrypt
|
||||
? this.encryption.encryptByAES(stringData)
|
||||
: stringData;
|
||||
this.storage.setItem(this.getKey(key), stringifyValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read cache
|
||||
* @param {string} key
|
||||
* @param {*} def
|
||||
* @memberof Cache
|
||||
*/
|
||||
get(key: string, def: any = null): any {
|
||||
const val = this.storage.getItem(this.getKey(key));
|
||||
if (!val) return def;
|
||||
|
||||
try {
|
||||
const decVal = this.hasEncrypt ? this.encryption.decryptByAES(val) : val;
|
||||
const data = JSON.parse(decVal);
|
||||
const { value, expire } = data;
|
||||
if (isNullOrUnDef(expire) || expire >= new Date().getTime()) {
|
||||
return value;
|
||||
}
|
||||
this.remove(key);
|
||||
} catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache based on key
|
||||
* @param {string} key
|
||||
* @memberof Cache
|
||||
*/
|
||||
remove(key: string) {
|
||||
this.storage.removeItem(this.getKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all caches of this instance
|
||||
*/
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
};
|
||||
return new WebStorage();
|
||||
};
|
||||
55
src/utils/cipher.ts
Normal file
55
src/utils/cipher.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { encrypt, decrypt } from 'crypto-js/aes';
|
||||
import { parse } from 'crypto-js/enc-utf8';
|
||||
import pkcs7 from 'crypto-js/pad-pkcs7';
|
||||
import ECB from 'crypto-js/mode-ecb';
|
||||
import md5 from 'crypto-js/md5';
|
||||
import UTF8 from 'crypto-js/enc-utf8';
|
||||
import Base64 from 'crypto-js/enc-base64';
|
||||
|
||||
export interface EncryptionParams {
|
||||
key: string;
|
||||
iv: string;
|
||||
}
|
||||
|
||||
export class AesEncryption {
|
||||
private key;
|
||||
private iv;
|
||||
|
||||
constructor(opt: Partial<EncryptionParams> = {}) {
|
||||
const { key, iv } = opt;
|
||||
if (key) {
|
||||
this.key = parse(key);
|
||||
}
|
||||
if (iv) {
|
||||
this.iv = parse(iv);
|
||||
}
|
||||
}
|
||||
|
||||
get getOptions() {
|
||||
return {
|
||||
mode: ECB,
|
||||
padding: pkcs7,
|
||||
iv: this.iv,
|
||||
};
|
||||
}
|
||||
|
||||
encryptByAES(cipherText: string) {
|
||||
return encrypt(cipherText, this.key, this.getOptions).toString();
|
||||
}
|
||||
|
||||
decryptByAES(cipherText: string) {
|
||||
return decrypt(cipherText, this.key, this.getOptions).toString(UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
export function encryptByBase64(cipherText: string) {
|
||||
return UTF8.parse(cipherText).toString(Base64);
|
||||
}
|
||||
|
||||
export function decodeByBase64(cipherText: string) {
|
||||
return Base64.parse(cipherText).toString(UTF8);
|
||||
}
|
||||
|
||||
export function encryptByMd5(password: string) {
|
||||
return md5(password).toString();
|
||||
}
|
||||
19
src/utils/codeformat.ts
Normal file
19
src/utils/codeformat.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import prettier from 'prettier/standalone';
|
||||
import parserHtml from 'prettier/parser-html';
|
||||
import parserTs from 'prettier/parser-typescript';
|
||||
import parserBabel from 'prettier/parser-babel';
|
||||
|
||||
export const formatCode = (code: string, type = 'typescript'): string => {
|
||||
return prettier.format(code, {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
printWidth: 100,
|
||||
trailingComma: 'all',
|
||||
proseWrap: 'never',
|
||||
htmlWhitespaceSensitivity: 'strict',
|
||||
endOfLine: 'auto',
|
||||
paser: type,
|
||||
plugins: [parserTs, parserBabel, parserHtml],
|
||||
vueIndentScriptAndStyle: true,
|
||||
});
|
||||
};
|
||||
151
src/utils/color.ts
Normal file
151
src/utils/color.ts
Normal file
@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 判断是否 十六进制颜色值.
|
||||
* 输入形式可为 #fff000 #f00
|
||||
*
|
||||
* @param String color 十六进制颜色值
|
||||
* @return Boolean
|
||||
*/
|
||||
export function isHexColor(color: string) {
|
||||
const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/;
|
||||
return reg.test(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB 颜色值转换为 十六进制颜色值.
|
||||
* r, g, 和 b 需要在 [0, 255] 范围内
|
||||
*
|
||||
* @return String 类似#ff00ff
|
||||
* @param r
|
||||
* @param g
|
||||
* @param b
|
||||
*/
|
||||
export function rgbToHex(r: number, g: number, b: number) {
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const hex = ((r << 16) | (g << 8) | b).toString(16);
|
||||
return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a HEX color to its RGB representation
|
||||
* @param {string} hex The color to transform
|
||||
* @returns The RGB representation of the passed color
|
||||
*/
|
||||
export function hexToRGB(hex: string) {
|
||||
let sHex = hex.toLowerCase();
|
||||
if (isHexColor(hex)) {
|
||||
if (sHex.length === 4) {
|
||||
let sColorNew = '#';
|
||||
for (let i = 1; i < 4; i += 1) {
|
||||
sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1));
|
||||
}
|
||||
sHex = sColorNew;
|
||||
}
|
||||
const sColorChange: number[] = [];
|
||||
for (let i = 1; i < 7; i += 2) {
|
||||
sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2)));
|
||||
}
|
||||
return 'RGB(' + sColorChange.join(',') + ')';
|
||||
}
|
||||
return sHex;
|
||||
}
|
||||
|
||||
export function colorIsDark(color: string) {
|
||||
if (!isHexColor(color)) return;
|
||||
const [r, g, b] = hexToRGB(color)
|
||||
.replace(/(?:\(|\)|rgb|RGB)*/g, '')
|
||||
.split(',')
|
||||
.map((item) => Number(item));
|
||||
return r * 0.299 + g * 0.578 + b * 0.114 < 192;
|
||||
}
|
||||
|
||||
/**
|
||||
* Darkens a HEX color given the passed percentage
|
||||
* @param {string} color The color to process
|
||||
* @param {number} amount The amount to change the color by
|
||||
* @returns {string} The HEX representation of the processed color
|
||||
*/
|
||||
export function darken(color: string, amount: number) {
|
||||
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color;
|
||||
amount = Math.trunc((255 * amount) / 100);
|
||||
return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight(
|
||||
color.substring(2, 4),
|
||||
amount,
|
||||
)}${subtractLight(color.substring(4, 6), amount)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)}`;
|
||||
}
|
||||
|
||||
/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */
|
||||
/**
|
||||
* 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)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates luminance of an rgb color
|
||||
* @param {number} r red
|
||||
* @param {number} g green
|
||||
* @param {number} b blue
|
||||
*/
|
||||
function luminanace(r: number, g: number, b: number) {
|
||||
const a = [r, g, b].map((v) => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates contrast between two rgb colors
|
||||
* @param {string} rgb1 rgb color 1
|
||||
* @param {string} rgb2 rgb color 2
|
||||
*/
|
||||
function contrast(rgb1: string[], rgb2: number[]) {
|
||||
return (
|
||||
(luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
|
||||
(luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what the best text color is (black or white) based con the contrast with the background
|
||||
* @param hexColor - Last selected color by the user
|
||||
*/
|
||||
export function calculateBestTextColor(hexColor: string) {
|
||||
const rgbColor = hexToRGB(hexColor.substring(1));
|
||||
const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]);
|
||||
|
||||
return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts the indicated 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 subtractLight(color: string, amount: number) {
|
||||
const cc = parseInt(color, 16) - amount;
|
||||
const c = cc < 0 ? 0 : cc;
|
||||
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`;
|
||||
}
|
||||
23
src/utils/dateUtil.ts
Normal file
23
src/utils/dateUtil.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Independent time operation tool to facilitate subsequent switch to dayjs
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
const DATE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
export function formatToDateTime(
|
||||
date: dayjs.Dayjs | Date | undefined | string | number | null = undefined,
|
||||
format = DATE_TIME_FORMAT,
|
||||
): string {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
export function formatToDate(
|
||||
date: dayjs.Dayjs | Date | undefined | string | number | null = undefined,
|
||||
format = DATE_FORMAT,
|
||||
): string {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
export const dateUtil = dayjs;
|
||||
181
src/utils/domUtils.ts
Normal file
181
src/utils/domUtils.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import type { FunctionArgs } from '@vueuse/core';
|
||||
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 - 1,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
|
||||
let locked = false;
|
||||
// @ts-ignore
|
||||
return function (...args: any[]) {
|
||||
if (locked) return;
|
||||
locked = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
// @ts-ignore
|
||||
fn.apply(this, args);
|
||||
locked = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
89
src/utils/env.ts
Normal file
89
src/utils/env.ts
Normal file
@ -0,0 +1,89 @@
|
||||
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_UPLOAD_PREVIEW,
|
||||
VITE_GLOB_OUT_LINK_URL,
|
||||
VITE_GLOB_PRINT_BASE_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_UPLOAD_PREVIEW,
|
||||
VITE_GLOB_OUT_LINK_URL,
|
||||
VITE_GLOB_PRINT_BASE_URL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Development mode
|
||||
*/
|
||||
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;
|
||||
}
|
||||
430
src/utils/event/design.ts
Normal file
430
src/utils/event/design.ts
Normal file
@ -0,0 +1,430 @@
|
||||
import Pinyin from 'js-pinyin'; //将汉字转为拼音
|
||||
Pinyin.setOptions({ charCase: 1 });
|
||||
import { requestMagicApi } from '/@/api/magicApi';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { BasicColumn } from '/@/components/Table';
|
||||
import { isNil } from 'lodash-es';
|
||||
import opencc from 'node-opencc';
|
||||
import { TableFieldConfig } from '/@/model/generator/tableStructureConfig';
|
||||
|
||||
//setOptions中传入对象,对象可传两个参数
|
||||
//charCase: 输出拼音的大小写模式,0-首字母大写;1-全小写;2-全大写
|
||||
//checkPolyphone:是否检查多音字
|
||||
//方法:1.getCamelChars: 获取拼音首字母 2.getFullChars: 获取拼音
|
||||
|
||||
/* 判断字符串是否有中文 有则在前面添加下划线(第一个字符不添加)并转换成小写拼音 */
|
||||
export function changeToPinyin(label: string, isUpper?: boolean) {
|
||||
const labelArr = label.split('');
|
||||
let fieldName = '';
|
||||
labelArr.map((item: string, index: number) => {
|
||||
const reg = /^[\u4e00-\u9fa5]+$/;
|
||||
item = opencc.traditionalToSimplified(item);
|
||||
fieldName += reg.test(item) && index !== 0 ? '_' + item : item;
|
||||
});
|
||||
const pinyin = Pinyin.getFullChars(fieldName.replace(/\s+/g, ''));
|
||||
return isUpper ? pinyin.toUpperCase() : pinyin;
|
||||
}
|
||||
|
||||
/* 如果没有下划线,不需要处理
|
||||
如果有下划线,用下划线切割,第一个下划线左边的全部小写,后面的首字母大写,首字母后面的全部小写 */
|
||||
export function camelCaseString(string: string) {
|
||||
if (!string) return;
|
||||
const stringLower = string.toLowerCase();
|
||||
const len = stringLower.length;
|
||||
let str = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = stringLower.charAt(i);
|
||||
if (c === '_') {
|
||||
if (++i < len) {
|
||||
str = str.concat(stringLower.charAt(i).toUpperCase());
|
||||
}
|
||||
} else {
|
||||
str = str.concat(c);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export async function apiConfigFunc(apiConfig, isCustomForm = false, formModel?, index?) {
|
||||
if (!apiConfig?.path) return [];
|
||||
const queryParam = {};
|
||||
const headerParam = {};
|
||||
const bodyParam = {};
|
||||
if (apiConfig?.apiParams) {
|
||||
for (const param of apiConfig.apiParams) {
|
||||
//queryString
|
||||
if (param.key === '1' && param.tableInfo && param.tableInfo.length) {
|
||||
for (const query of param.tableInfo) {
|
||||
queryParam[query.name] = getParamsValue(query, formModel, isCustomForm, index);
|
||||
}
|
||||
}
|
||||
//header
|
||||
if (param.key === '2' && param.tableInfo && param.tableInfo.length) {
|
||||
for (const head of param.tableInfo) {
|
||||
headerParam[head.name] = getParamsValue(head, formModel, isCustomForm, index, true);
|
||||
}
|
||||
}
|
||||
//body
|
||||
if (param.key === '3' && param.tableInfo && param.tableInfo.length) {
|
||||
for (const body of param.tableInfo) {
|
||||
bodyParam[body.name] = getParamsValue(body, formModel, isCustomForm, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await requestMagicApi({
|
||||
method: apiConfig?.method,
|
||||
url: apiConfig.path,
|
||||
headers: headerParam,
|
||||
body: bodyParam,
|
||||
query: queryParam,
|
||||
});
|
||||
if (res && Array.isArray(res)) return res; //不分页接口
|
||||
if (res && Array.isArray(res.list)) return res.list; //分页接口
|
||||
return res;
|
||||
}
|
||||
|
||||
function getParamsValue(params, formModel, isCustomForm, index, isHeaders: Boolean = false) {
|
||||
const userStore = useUserStore();
|
||||
let value;
|
||||
if (params.bindType === 'value') {
|
||||
value = params.value;
|
||||
} else if (params.bindType === 'data') {
|
||||
const paramsArr = params.value.split('-');
|
||||
if (paramsArr[0] === '3') {
|
||||
//当前信息
|
||||
value = isHeaders
|
||||
? encodeURIComponent(userStore.getUserInfo[paramsArr[1]])
|
||||
: userStore.getUserInfo[paramsArr[1]];
|
||||
} else {
|
||||
if (!formModel) return;
|
||||
let headerValue = '';
|
||||
if (params.value) {
|
||||
const value = isValidJSON(params.value);
|
||||
if (value && value.bindTable) {
|
||||
const table = !isCustomForm
|
||||
? value.bindTable + 'List'
|
||||
: camelCaseString(value.bindTable + '_List');
|
||||
const field = !isCustomForm ? value.bindField : camelCaseString(value.bindField);
|
||||
|
||||
if (formModel[table!] && formModel[table!][index || 0]) {
|
||||
headerValue = formModel[table!][index || 0][field];
|
||||
}
|
||||
} else {
|
||||
headerValue = !isCustomForm
|
||||
? formModel[value.bindField!]
|
||||
: formModel[camelCaseString(value.bindField)!];
|
||||
}
|
||||
}
|
||||
|
||||
value = isHeaders ? encodeURIComponent(headerValue) : headerValue;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
export function isValidJSON(str) {
|
||||
try {
|
||||
const val = JSON.parse(str);
|
||||
return val;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列表值 生成table html
|
||||
*/
|
||||
export function generateTableHtml(columns: BasicColumn[], datas: Recordable[]): string {
|
||||
const headArray: {
|
||||
key: string;
|
||||
name: string;
|
||||
}[] = [];
|
||||
const bodySortArray: string[][] = [];
|
||||
|
||||
let result = `<table>`;
|
||||
|
||||
result += `<tr>`;
|
||||
//遍历所有列配置 设置为表头
|
||||
for (const col of columns) {
|
||||
if (col.dataIndex) {
|
||||
headArray.push({
|
||||
key: col.dataIndex as string,
|
||||
name: col.title as string,
|
||||
});
|
||||
}
|
||||
result += `<th>${col.title}</th>`;
|
||||
}
|
||||
result += `</tr>`;
|
||||
//遍历所有数据
|
||||
|
||||
for (const item of datas) {
|
||||
result += `<tr>`;
|
||||
const row: string[] = [];
|
||||
for (const head of headArray) {
|
||||
const index = Object.keys(item).indexOf(head.key);
|
||||
row[index] = item[head.key];
|
||||
result += `<td>${item[head.key]}</td>`;
|
||||
}
|
||||
bodySortArray.push(row);
|
||||
result += `</tr>`;
|
||||
}
|
||||
|
||||
result += '</table>';
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列表值 生成table html
|
||||
*/
|
||||
export function generateTableJson(columns: BasicColumn[], datasource: Recordable[]): Recordable[] {
|
||||
const result: Recordable[] = [];
|
||||
|
||||
for (const item of datasource) {
|
||||
//替换所有key
|
||||
const newRow: Recordable = {};
|
||||
Object.keys(item).forEach((key) => {
|
||||
const col = columns.find((x) => x.dataIndex == key);
|
||||
if (col?.title) {
|
||||
newRow[col?.title as string] = isNil(item[key]) ? '' : item[key];
|
||||
}
|
||||
});
|
||||
result.push(newRow);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function checkTabCanDelete(layout) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < layout.length; i++) {
|
||||
const o = layout[i];
|
||||
for (let i = 0; i < o.list.length; i++) {
|
||||
const k = o.list[i];
|
||||
if (k.type == 'form' || k.type == 'one-for-one') {
|
||||
count += 1;
|
||||
break;
|
||||
} else if (k.type == 'tab' || k.type == 'card' || k.type == 'grid') {
|
||||
count = checkTabCanDelete(k.layout);
|
||||
if (count > 0) break;
|
||||
} else if (k.options?.required) {
|
||||
count += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (count > 0) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
const needDicDefaultValue = [
|
||||
'select',
|
||||
'associate-select',
|
||||
'associate-popup',
|
||||
'multiple-popup',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'button',
|
||||
];
|
||||
export function changeCompsApiConfig(list, designType, TableFieldConfigs) {
|
||||
list.forEach((item) => {
|
||||
if (needDicDefaultValue.includes(item.type)) {
|
||||
//主表
|
||||
if (
|
||||
item.type == 'button' &&
|
||||
item.options.isSpecial &&
|
||||
item.options.buttonType == 2 &&
|
||||
designType !== 'data'
|
||||
) {
|
||||
changeTableColumns(item.options.tableColumns, TableFieldConfigs);
|
||||
}
|
||||
if (item.options?.datasourceType == 'api') {
|
||||
changeApiParams(item.options.apiConfig.apiParams, designType, TableFieldConfigs);
|
||||
changeOutputParams(item.options.apiConfig.outputParams, designType, TableFieldConfigs);
|
||||
} else if (item.options?.datasourceType == 'dic') {
|
||||
changeDicOptions(item.options.dicOptions, designType, TableFieldConfigs);
|
||||
}
|
||||
} else if (item.type == 'computational' || item.type == 'money-chinese') {
|
||||
item.options.computationalConfig.forEach((o) => {
|
||||
if (o.bindField && o.bindTable) {
|
||||
const comp = TableFieldConfigs.find((k) => k.key == o.key);
|
||||
if (comp) {
|
||||
o.bindField = comp.fieldName;
|
||||
o.bindTable = comp.tableName;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (['tab', 'grid', 'card'].includes(item.type)) {
|
||||
for (const child of item.layout!) {
|
||||
changeCompsApiConfig(child.list, designType, TableFieldConfigs);
|
||||
}
|
||||
} else if (item.type === 'table-layout') {
|
||||
for (const child of item.layout!) {
|
||||
for (const el of child.list) {
|
||||
changeCompsApiConfig(el.children, designType, TableFieldConfigs);
|
||||
}
|
||||
}
|
||||
} else if (item.type === 'one-for-one' || item.type === 'form') {
|
||||
if (item.children.length) {
|
||||
changeCompsApiConfig(item.children, designType, TableFieldConfigs);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
export function changeEventApiConfig(formEventConfig, designType, TableFieldConfigs) {
|
||||
for (const config in formEventConfig) {
|
||||
formEventConfig[config].forEach((item) => {
|
||||
if (item.isUserDefined) {
|
||||
item.nodeInfo.processEvent.forEach((o) => {
|
||||
if (o.operateType == 'api') {
|
||||
changeApiParams(o.operateConfig.apiParams, designType, TableFieldConfigs);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function changeApiParams(apiparams, designType, TableFieldConfigs) {
|
||||
if (!apiparams) return;
|
||||
apiparams.forEach((param) => {
|
||||
param.tableInfo?.forEach((info) => {
|
||||
if (info.bindType == 'data') {
|
||||
if (info.value) {
|
||||
const val = isValidJSON(info.value);
|
||||
if (designType == 'code') {
|
||||
if (val && val.bindField) {
|
||||
const comp = TableFieldConfigs.filter((o) => o.key == val.fieldKey);
|
||||
if (comp.length == 1) val.bindField = comp[0].fieldName;
|
||||
if (comp.length == 2) {
|
||||
const field = new Array(2);
|
||||
comp.forEach((o) => {
|
||||
if (o.fieldStartName) field[0] = o.fieldStartName;
|
||||
if (o.fieldEndName) field[1] = o.fieldEndName;
|
||||
});
|
||||
val.bindField = field.join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (designType !== 'data') {
|
||||
if (val && val.bindTable) {
|
||||
const comp = TableFieldConfigs.find((o) => o.key == val.fieldKey);
|
||||
if (comp) val.bindTable = comp.tableName;
|
||||
}
|
||||
}
|
||||
info.value = JSON.stringify(val);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function changeOutputParams(outputParams, designType, TableFieldConfigs) {
|
||||
if (designType == 'code' && outputParams) {
|
||||
outputParams.forEach((item) => {
|
||||
const comp = TableFieldConfigs.find((o) => o.key == item.component);
|
||||
if (comp) {
|
||||
item.bindField = comp.fieldName;
|
||||
if (item.bindTable) item.bindTable = comp.tableName;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function changeDicOptions(dicOptions, designType, TableFieldConfigs) {
|
||||
if (designType == 'code' && dicOptions) {
|
||||
dicOptions.forEach((item) => {
|
||||
const comp = TableFieldConfigs.find((o) => o.key == item.component);
|
||||
if (comp) {
|
||||
item.bindField = comp.fieldName;
|
||||
if (item.bindTable) item.bindTable = comp.tableName;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function changeTableColumns(tableColumns, TableFieldConfigs) {
|
||||
tableColumns &&
|
||||
tableColumns.forEach((item) => {
|
||||
const comp = TableFieldConfigs.find((o) => o.key == item.key);
|
||||
if (comp) {
|
||||
item.bindField = comp.fieldName;
|
||||
item.bindTable = comp.tableName;
|
||||
}
|
||||
});
|
||||
}
|
||||
export function getMainTable(tableStructureConfigs) {
|
||||
const TableFieldConfigs: TableFieldConfig[] = [];
|
||||
tableStructureConfigs?.forEach((x) => {
|
||||
x.tableFieldConfigs.forEach((o) => {
|
||||
o.tableName = x.tableName;
|
||||
TableFieldConfigs.push(o);
|
||||
});
|
||||
});
|
||||
return TableFieldConfigs;
|
||||
}
|
||||
|
||||
export function testPwdState(pwd) {
|
||||
let score = 0;
|
||||
|
||||
const pwdArr = pwd.split('');
|
||||
const numArr = pwdArr.filter((x) => /[0-9]/.test(x)); //数字数组
|
||||
const alpArr = pwdArr.filter((x) => /[a-z]/.test(x)); //小写字母数组
|
||||
const ALPArr = pwdArr.filter((x) => /[A-Z]/.test(x)); //大写字母数组
|
||||
const charArr = pwdArr.filter((x) => /[^a-zA-Z0-9]/.test(x)); //特殊字符数组
|
||||
|
||||
if (pwd.length <= 4) {
|
||||
//小于等于4个字符
|
||||
score += 5;
|
||||
} else if (pwd.length >= 5 && pwd.length <= 7) {
|
||||
//5-7个字符
|
||||
score += 10;
|
||||
} else if (pwd.length > 8) {
|
||||
//8个字符以上
|
||||
score += 25;
|
||||
}
|
||||
|
||||
if (alpArr.length === pwdArr.length || ALPArr.length === pwdArr.length) {
|
||||
//全部都是大写字母或者小写字母
|
||||
score += 10;
|
||||
} else if (alpArr.length && ALPArr.length) {
|
||||
//大小写混合
|
||||
score += 20;
|
||||
}
|
||||
|
||||
if (numArr.length === 1) {
|
||||
//只有一个数字
|
||||
score += 10;
|
||||
} else if (numArr.length >= 3) {
|
||||
//大于等于3个数字
|
||||
score += 20;
|
||||
}
|
||||
|
||||
if (charArr.length === 1) {
|
||||
//只有一个符号
|
||||
score += 10;
|
||||
} else if (charArr.length > 1) {
|
||||
//大于一个符号
|
||||
score += 25;
|
||||
}
|
||||
|
||||
if (ALPArr.length && alpArr.length && numArr.length && charArr.length) {
|
||||
//含有大写字母、小写字母、数字、符号
|
||||
score += 5;
|
||||
} else if ((ALPArr.length || alpArr.length) && numArr.length && charArr.length) {
|
||||
//含有字母、数字、符号
|
||||
score += 3;
|
||||
} else if ((ALPArr.length || alpArr.length) && numArr.length) {
|
||||
//含有字母、数字
|
||||
score += 2;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
export function validateScript(str) {
|
||||
if (str.indexOf('<script') !== -1) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
}
|
||||
|
||||
export function testRegLength(reg) {
|
||||
if (reg.length > 255) {
|
||||
throw new Error('字符长度超出255个字符');
|
||||
}
|
||||
}
|
||||
42
src/utils/event/index.ts
Normal file
42
src/utils/event/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const isServer = typeof window === 'undefined';
|
||||
|
||||
/* istanbul ignore next */
|
||||
function resizeHandler(entries: any[]) {
|
||||
for (const entry of entries) {
|
||||
const listeners = entry.target.__resizeListeners__ || [];
|
||||
if (listeners.length) {
|
||||
listeners.forEach((fn: () => any) => {
|
||||
fn();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function addResizeListener(element: any, fn: () => any) {
|
||||
if (isServer) return;
|
||||
if (!element.__resizeListeners__) {
|
||||
element.__resizeListeners__ = [];
|
||||
element.__ro__ = new ResizeObserver(resizeHandler);
|
||||
element.__ro__.observe(element);
|
||||
}
|
||||
element.__resizeListeners__.push(fn);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function removeResizeListener(element: any, fn: () => any) {
|
||||
if (!element || !element.__resizeListeners__) return;
|
||||
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
|
||||
if (!element.__resizeListeners__.length) {
|
||||
element.__ro__.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerWindowResize() {
|
||||
const event = document.createEvent('HTMLEvents');
|
||||
event.initEvent('resize', true, true);
|
||||
(event as any).eventType = 'message';
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
327
src/utils/export.ts
Normal file
327
src/utils/export.ts
Normal file
@ -0,0 +1,327 @@
|
||||
// import { createCellPos } from './translateNumToLetter'
|
||||
import Excel from 'exceljs';
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
|
||||
const exportExcel = function (luckysheet, value) {
|
||||
// 参数为luckysheet.getluckysheetfile()获取的对象
|
||||
// 1.创建工作簿,可以为工作簿添加属性
|
||||
const workbook = new Excel.Workbook();
|
||||
// 2.创建表格,第二个参数可以配置创建什么样的工作表
|
||||
if (Object.prototype.toString.call(luckysheet) === '[object Object]') {
|
||||
luckysheet = [luckysheet];
|
||||
}
|
||||
luckysheet.forEach(function (table) {
|
||||
if (table.data.length === 0) return true;
|
||||
// ws.getCell('B2').fill = fills.
|
||||
const worksheet = workbook.addWorksheet(table.name);
|
||||
const merge = (table.config && table.config.merge) || {};
|
||||
const borderInfo = (table.config && table.config.borderInfo) || {};
|
||||
// 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
|
||||
setStyleAndValue(table.data, worksheet);
|
||||
setMerge(merge, worksheet);
|
||||
setBorder(borderInfo, worksheet);
|
||||
return true;
|
||||
});
|
||||
|
||||
// return
|
||||
// 4.写入 buffer
|
||||
const buffer = workbook.xlsx.writeBuffer().then((data) => {
|
||||
// console.log('data', data)
|
||||
const blob = new Blob([data], {
|
||||
type: 'application/vnd.ms-excel;charset=utf-8',
|
||||
});
|
||||
console.log('导出成功!');
|
||||
FileSaver.saveAs(blob, `${value}.xlsx`);
|
||||
});
|
||||
return buffer;
|
||||
};
|
||||
|
||||
const setMerge = function (luckyMerge = {}, worksheet) {
|
||||
const mergearr = Object.values(luckyMerge);
|
||||
mergearr.forEach((elem) => {
|
||||
const elemAny = elem as any;
|
||||
// elem格式:{r: 0, c: 0, rs: 1, cs: 2}
|
||||
// 按开始行,开始列,结束行,结束列合并(相当于 K10:M12)
|
||||
worksheet.mergeCells(
|
||||
elemAny.r + 1,
|
||||
elemAny.c + 1,
|
||||
elemAny.r + elemAny.rs,
|
||||
elemAny.c + elemAny.cs,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const setBorder = function (luckyBorderInfo, worksheet) {
|
||||
if (!Array.isArray(luckyBorderInfo)) return;
|
||||
// console.log('luckyBorderInfo', luckyBorderInfo)
|
||||
luckyBorderInfo.forEach(function (elem) {
|
||||
// 现在只兼容到borderType 为range的情况
|
||||
// console.log('ele', elem)
|
||||
if (elem.rangeType === 'range') {
|
||||
const border = borderConvert(elem.borderType, elem.style, elem.color);
|
||||
const rang = elem.range[0];
|
||||
// console.log('range', rang)
|
||||
const row = rang.row;
|
||||
const column = rang.column;
|
||||
for (let i = row[0] + 1; i < row[1] + 2; i++) {
|
||||
for (let y = column[0] + 1; y < column[1] + 2; y++) {
|
||||
worksheet.getCell(i, y).border = border;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elem.rangeType === 'cell') {
|
||||
// col_index: 2
|
||||
// row_index: 1
|
||||
// b: {
|
||||
// color: '#d0d4e3'
|
||||
// style: 1
|
||||
// }
|
||||
const { col_index, row_index } = elem.value;
|
||||
const borderData = Object.assign({}, elem.value);
|
||||
delete borderData.col_index;
|
||||
delete borderData.row_index;
|
||||
const border = addborderToCell(borderData);
|
||||
// console.log('bordre', border, borderData)
|
||||
worksheet.getCell(row_index + 1, col_index + 1).border = border;
|
||||
}
|
||||
// console.log(rang.column_focus + 1, rang.row_focus + 1)
|
||||
// worksheet.getCell(rang.row_focus + 1, rang.column_focus + 1).border = border
|
||||
});
|
||||
};
|
||||
const setStyleAndValue = function (cellArr, worksheet) {
|
||||
if (!Array.isArray(cellArr)) return;
|
||||
cellArr.forEach(function (row, rowid) {
|
||||
row.every(function (cell, columnid) {
|
||||
if (!cell) return true;
|
||||
const fill = fillConvert(cell.bg);
|
||||
|
||||
const font = fontConvert(cell.ff, cell.fc, cell.bl, cell.it, cell.fs, cell.cl, cell.ul);
|
||||
const alignment = alignmentConvert(cell.vt, cell.ht, cell.tb, cell.tr);
|
||||
let value: any = '';
|
||||
|
||||
if (cell.f) {
|
||||
value = { formula: cell.f, result: cell.v };
|
||||
} else if (!cell.v && cell.ct && cell.ct.s) {
|
||||
// xls转为xlsx之后,内部存在不同的格式,都会进到富文本里,即值不存在与cell.v,而是存在于cell.ct.s之后
|
||||
// value = cell.ct.s[0].v
|
||||
cell.ct.s.forEach((arr) => {
|
||||
value += arr.v;
|
||||
});
|
||||
} else {
|
||||
value = cell.v;
|
||||
}
|
||||
// style 填入到_value中可以实现填充色
|
||||
const letter = createCellPos(columnid);
|
||||
const target = worksheet.getCell(letter + (rowid + 1));
|
||||
// console.log('1233', letter + (rowid + 1))
|
||||
for (const _ in fill) {
|
||||
target.fill = fill;
|
||||
break;
|
||||
}
|
||||
target.font = font;
|
||||
target.alignment = alignment;
|
||||
target.value = value;
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const fillConvert = function (bg) {
|
||||
if (!bg) {
|
||||
return {};
|
||||
}
|
||||
// const bgc = bg.replace('#', '')
|
||||
const fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: bg.replace('#', '') },
|
||||
};
|
||||
return fill;
|
||||
};
|
||||
|
||||
const fontConvert = function (ff = 0, fc = '#000000', bl = 0, it = 0, fs = 10, cl = 0, ul = 0) {
|
||||
// luckysheet:ff(样式), fc(颜色), bl(粗体), it(斜体), fs(大小), cl(删除线), ul(下划线)
|
||||
const luckyToExcel = {
|
||||
0: '微软雅黑',
|
||||
1: '宋体(Song)',
|
||||
2: '黑体(ST Heiti)',
|
||||
3: '楷体(ST Kaiti)',
|
||||
4: '仿宋(ST FangSong)',
|
||||
5: '新宋体(ST Song)',
|
||||
6: '华文新魏',
|
||||
7: '华文行楷',
|
||||
8: '华文隶书',
|
||||
9: 'Arial',
|
||||
10: 'Times New Roman ',
|
||||
11: 'Tahoma ',
|
||||
12: 'Verdana',
|
||||
num2bl: function (num) {
|
||||
return num === 0 ? false : true;
|
||||
},
|
||||
};
|
||||
// 出现Bug,导入的时候ff为luckyToExcel的val
|
||||
|
||||
const font = {
|
||||
name: typeof ff === 'number' ? luckyToExcel[ff] : ff,
|
||||
family: 1,
|
||||
size: fs,
|
||||
color: { argb: fc.replace('#', '') },
|
||||
bold: luckyToExcel.num2bl(bl),
|
||||
italic: luckyToExcel.num2bl(it),
|
||||
underline: luckyToExcel.num2bl(ul),
|
||||
strike: luckyToExcel.num2bl(cl),
|
||||
};
|
||||
|
||||
return font;
|
||||
};
|
||||
|
||||
const alignmentConvert = function (vt = 'default', ht = 'default', tb = 'default', tr = 'default') {
|
||||
// luckysheet:vt(垂直), ht(水平), tb(换行), tr(旋转)
|
||||
const luckyToExcel = {
|
||||
vertical: {
|
||||
0: 'middle',
|
||||
1: 'top',
|
||||
2: 'bottom',
|
||||
default: 'top',
|
||||
},
|
||||
horizontal: {
|
||||
0: 'center',
|
||||
1: 'left',
|
||||
2: 'right',
|
||||
default: 'left',
|
||||
},
|
||||
wrapText: {
|
||||
0: false,
|
||||
1: false,
|
||||
2: true,
|
||||
default: false,
|
||||
},
|
||||
textRotation: {
|
||||
0: 0,
|
||||
1: 45,
|
||||
2: -45,
|
||||
3: 'vertical',
|
||||
4: 90,
|
||||
5: -90,
|
||||
default: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const alignment = {
|
||||
vertical: luckyToExcel.vertical[vt],
|
||||
horizontal: luckyToExcel.horizontal[ht],
|
||||
wrapText: luckyToExcel.wrapText[tb],
|
||||
textRotation: luckyToExcel.textRotation[tr],
|
||||
};
|
||||
return alignment;
|
||||
};
|
||||
|
||||
const borderConvert = function (borderType, style = 1, color = '#000') {
|
||||
// 对应luckysheet的config中borderinfo的的参数
|
||||
if (!borderType) {
|
||||
return {};
|
||||
}
|
||||
const luckyToExcel = {
|
||||
type: {
|
||||
'border-all': 'all',
|
||||
'border-top': 'top',
|
||||
'border-right': 'right',
|
||||
'border-bottom': 'bottom',
|
||||
'border-left': 'left',
|
||||
},
|
||||
style: {
|
||||
0: 'none',
|
||||
1: 'thin',
|
||||
2: 'hair',
|
||||
3: 'dotted',
|
||||
4: 'dashDot', // 'Dashed',
|
||||
5: 'dashDot',
|
||||
6: 'dashDotDot',
|
||||
7: 'double',
|
||||
8: 'medium',
|
||||
9: 'mediumDashed',
|
||||
10: 'mediumDashDot',
|
||||
11: 'mediumDashDotDot',
|
||||
12: 'slantDashDot',
|
||||
13: 'thick',
|
||||
},
|
||||
};
|
||||
const template = {
|
||||
style: luckyToExcel.style[style],
|
||||
color: { argb: color.replace('#', '') },
|
||||
};
|
||||
const border = {};
|
||||
if (luckyToExcel.type[borderType] === 'all') {
|
||||
border['top'] = template;
|
||||
border['right'] = template;
|
||||
border['bottom'] = template;
|
||||
border['left'] = template;
|
||||
} else {
|
||||
border[luckyToExcel.type[borderType]] = template;
|
||||
}
|
||||
// console.log('border', border)
|
||||
return border;
|
||||
};
|
||||
|
||||
function addborderToCell(borders) {
|
||||
const border = {};
|
||||
const luckyExcel = {
|
||||
type: {
|
||||
l: 'left',
|
||||
r: 'right',
|
||||
b: 'bottom',
|
||||
t: 'top',
|
||||
},
|
||||
style: {
|
||||
0: 'none',
|
||||
1: 'thin',
|
||||
2: 'hair',
|
||||
3: 'dotted',
|
||||
4: 'dashDot', // 'Dashed',
|
||||
5: 'dashDot',
|
||||
6: 'dashDotDot',
|
||||
7: 'double',
|
||||
8: 'medium',
|
||||
9: 'mediumDashed',
|
||||
10: 'mediumDashDot',
|
||||
11: 'mediumDashDotDot',
|
||||
12: 'slantDashDot',
|
||||
13: 'thick',
|
||||
},
|
||||
};
|
||||
// console.log('borders', borders)
|
||||
for (const bor in borders) {
|
||||
// console.log(bor)
|
||||
if (borders[bor].color.indexOf('rgb') === -1) {
|
||||
border[luckyExcel.type[bor]] = {
|
||||
style: luckyExcel.style[borders[bor].style],
|
||||
color: { argb: borders[bor].color.replace('#', '') },
|
||||
};
|
||||
} else {
|
||||
border[luckyExcel.type[bor]] = {
|
||||
style: luckyExcel.style[borders[bor].style],
|
||||
color: { argb: borders[bor].color },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
function createCellPos(n) {
|
||||
const ordA = 'A'.charCodeAt(0);
|
||||
|
||||
const ordZ = 'Z'.charCodeAt(0);
|
||||
const len = ordZ - ordA + 1;
|
||||
let s = '';
|
||||
while (n >= 0) {
|
||||
s = String.fromCharCode((n % len) + ordA) + s;
|
||||
|
||||
n = Math.floor(n / len) - 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export { exportExcel };
|
||||
63
src/utils/factory/createAsyncComponent.tsx
Normal file
63
src/utils/factory/createAsyncComponent.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
// FunctionalComponent, CSSProperties
|
||||
} from 'vue';
|
||||
import { Spin } from 'ant-design-vue';
|
||||
import { noop } from '/@/utils';
|
||||
|
||||
// const Loading: FunctionalComponent<{ size: 'small' | 'default' | 'large' }> = (props) => {
|
||||
// const style: CSSProperties = {
|
||||
// position: 'absolute',
|
||||
// display: 'flex',
|
||||
// justifyContent: 'center',
|
||||
// alignItems: 'center',
|
||||
// };
|
||||
// return (
|
||||
// <div style={style}>
|
||||
// <Spin spinning={true} size={props.size} />
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
interface Options {
|
||||
size?: 'default' | 'small' | 'large';
|
||||
delay?: number;
|
||||
timeout?: number;
|
||||
loading?: boolean;
|
||||
retry?: boolean;
|
||||
}
|
||||
|
||||
export function createAsyncComponent(loader: Fn, options: Options = {}) {
|
||||
const { size = 'small', delay = 100, timeout = 30000, loading = false, retry = true } = options;
|
||||
return defineAsyncComponent({
|
||||
loader,
|
||||
loadingComponent: loading ? <Spin spinning={true} size={size} /> : undefined,
|
||||
// The error component will be displayed if a timeout is
|
||||
// provided and exceeded. Default: Infinity.
|
||||
// TODO
|
||||
timeout,
|
||||
// errorComponent
|
||||
// Defining if component is suspensible. Default: true.
|
||||
// suspensible: false,
|
||||
delay,
|
||||
/**
|
||||
*
|
||||
* @param {*} error Error message object
|
||||
* @param {*} retry A function that indicating whether the async component should retry when the loader promise rejects
|
||||
* @param {*} fail End of failure
|
||||
* @param {*} attempts Maximum allowed retries number
|
||||
*/
|
||||
onError: !retry
|
||||
? noop
|
||||
: (error, retry, fail, attempts) => {
|
||||
if (error.message.match(/fetch/) && attempts <= 3) {
|
||||
// retry on fetch errors, 3 max attempts
|
||||
retry();
|
||||
} else {
|
||||
// Note that retry/fail are like resolve/reject of a promise:
|
||||
// one of them must be called for the error handling to continue.
|
||||
fail();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
41
src/utils/file/base64Conver.ts
Normal file
41
src/utils/file/base64Conver.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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 = window.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;
|
||||
});
|
||||
}
|
||||
102
src/utils/file/download.ts
Normal file
102
src/utils/file/download.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { openWindow } from '..';
|
||||
import { dataURLtoBlob, urlToBase64 } from './base64Conver';
|
||||
|
||||
/**
|
||||
* 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, filename: string, mime?: string, bom?: BlobPart) {
|
||||
const blobData = typeof bom !== 'undefined' ? [bom, data] : [data];
|
||||
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });
|
||||
|
||||
const blobURL = window.URL.createObjectURL(blob);
|
||||
const tempLink = document.createElement('a');
|
||||
tempLink.style.display = 'none';
|
||||
tempLink.href = blobURL;
|
||||
tempLink.setAttribute('download', 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 x = new window.XMLHttpRequest();
|
||||
x.open('GET', url, true);
|
||||
x.responseType = 'blob';
|
||||
x.onload = () => {
|
||||
let fileUrl = window.URL.createObjectURL(x.response);
|
||||
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 = fileUrl;
|
||||
link.target = target;
|
||||
|
||||
if (link.download !== undefined) {
|
||||
link.download = fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1, fileUrl.length);
|
||||
}
|
||||
|
||||
if (document.createEvent) {
|
||||
const e = document.createEvent('MouseEvents');
|
||||
e.initEvent('click', true, true);
|
||||
link.dispatchEvent(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (fileUrl.indexOf('?') === -1) {
|
||||
fileUrl += '?download';
|
||||
}
|
||||
openWindow(fileUrl, { target });
|
||||
};
|
||||
x.send();
|
||||
return true;
|
||||
}
|
||||
48
src/utils/formSettings/formJson.ts
Normal file
48
src/utils/formSettings/formJson.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { buildOption } from '/@/utils/helper/designHelper';
|
||||
import { getFormTemplate } from '/@/api/form/design';
|
||||
import { FormTemplateModel } from '/@/api/form/design/model';
|
||||
import { FormJson } from '/@/model/generator/codeGenerator';
|
||||
import { SystemComponentConfig } from '/@/model/workflow/bpmnConfig';
|
||||
export async function getSystemJson(formId: string) {
|
||||
try {
|
||||
const systemJson = await getFormTemplate(formId);
|
||||
const systemComponent = {
|
||||
functionalModule: systemJson.functionalModule,
|
||||
functionName: systemJson.functionName,
|
||||
functionFormName: 'Form',
|
||||
};
|
||||
return { ...systemJson, systemComponent };
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
// const file = importLocale(formId);
|
||||
// try {
|
||||
// const result = await file;
|
||||
// return result && result.formProps ? result.formProps : null;
|
||||
// } catch (error) {
|
||||
// return null;
|
||||
// }
|
||||
}
|
||||
/*
|
||||
系统表单工作流权限获取
|
||||
*/
|
||||
export function importWorkflowPermission(systemComponent: SystemComponentConfig) {
|
||||
return import(
|
||||
`./../../views/${systemComponent.functionalModule}/${systemComponent.functionName}/components/workflowPermission.ts`
|
||||
);
|
||||
}
|
||||
|
||||
// export function importLocale(locale: string) {
|
||||
// return import(`./../../views/dev/${locale}/components/config.ts`);
|
||||
// }
|
||||
|
||||
export async function getCustomJson(formId: string) {
|
||||
try {
|
||||
const res = await getFormTemplate(formId);
|
||||
const model = JSON.parse(res.formJson) as FormTemplateModel;
|
||||
const formJson = model.formJson as unknown as FormJson;
|
||||
return res.formJson ? buildOption(formJson, false) : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
406
src/utils/helper/appDataDesignHelper.ts
Normal file
406
src/utils/helper/appDataDesignHelper.ts
Normal file
@ -0,0 +1,406 @@
|
||||
import { LocationType } from '/@/enums/desktop';
|
||||
import { DesktopInfoItem } from '/@/model/mobileDesign/designer';
|
||||
|
||||
/**
|
||||
* 构建app数据设计页代码
|
||||
* @param model
|
||||
* @param _tableInfo
|
||||
* @returns
|
||||
*/
|
||||
export function buildAppDataCode(model: Array<DesktopInfoItem>): string {
|
||||
const compMap: any[] = [];
|
||||
let importCode = '';
|
||||
let configCode = '';
|
||||
let code = `<template>
|
||||
<view class="data-display">
|
||||
`;
|
||||
if (model.length) {
|
||||
model.forEach((o, i) => {
|
||||
const key = o.type;
|
||||
if (!compMap.includes(key)) compMap.push(key);
|
||||
const config: any = o.config;
|
||||
const apiConfig = config.apiConfig
|
||||
? `apiConfig: {
|
||||
name: "${config.apiConfig.name}",
|
||||
method: "${config.apiConfig.method}",
|
||||
path: "${config.apiConfig.path}",
|
||||
requestParamsConfigs: ${JSON.stringify(config.apiConfig.requestParamsConfigs) || []},
|
||||
requestHeaderConfigs: ${JSON.stringify(config.apiConfig.requestHeaderConfigs) || []},
|
||||
requestBodyConfigs:${JSON.stringify(config.apiConfig.requestBodyConfigs) || []}
|
||||
},`
|
||||
: '';
|
||||
const idx = i > 0 ? `_${i}` : '';
|
||||
switch (key) {
|
||||
case 'Dashboard':
|
||||
code += `<Dashboard :config="dashboardConfig${idx}"></Dashboard>
|
||||
`;
|
||||
configCode += `const dashboardConfig${idx} = ref({
|
||||
${apiConfig}
|
||||
numColor: "${config.numColor}",
|
||||
labelColor: "${config.labelColor}",
|
||||
dashboard: ${JSON.stringify(config.dashboard) || []}
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'Banner':
|
||||
code += `<view style="margin: 0 -10px 10px;">
|
||||
<Banner :config="bannerConfig${idx}"></Banner>
|
||||
</view>
|
||||
`;
|
||||
|
||||
const imgs: string[] = config.imgs.map((k) => {
|
||||
return k.url;
|
||||
});
|
||||
configCode += `const bannerConfig${idx} = ref({
|
||||
height: ${o.h},
|
||||
imgs: ${JSON.stringify(imgs)},
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'ChartLine':
|
||||
code += `<LineChart :config="lineConfig${idx}"></LineChart>
|
||||
`;
|
||||
const Llegend = getLegend(config.legend);
|
||||
const dataList: any[] = [];
|
||||
config.dataList.forEach((k) => {
|
||||
const yAxis: any[] = [];
|
||||
config.yAxis.forEach((j, i) => {
|
||||
yAxis.push({
|
||||
type: 'value', //value数值,categories类别
|
||||
position: j.position,
|
||||
title: j.name,
|
||||
titleFontColor: j.nameTextStyle.color,
|
||||
titleFontSize: j.nameTextStyle.fontSize,
|
||||
// min: j.min,
|
||||
// max: j.max,
|
||||
format: i === 0 ? 'lineYAxis' : 'lineYAxisTwo',
|
||||
axisFormat: j.axisLabel.formatter,
|
||||
fontColor: j.axisLabel.color,
|
||||
axisLine: j.axisLine.show,
|
||||
axisLineColor: j.axisLine.lineStyle.color,
|
||||
});
|
||||
});
|
||||
|
||||
dataList.push({
|
||||
title: k.title,
|
||||
valueKey: k.valueKey,
|
||||
apiConfig: k.apiConfig,
|
||||
indicator: k.indicator,
|
||||
options: {
|
||||
extra: {
|
||||
mix: {
|
||||
column: {
|
||||
seriesGap: 120,
|
||||
},
|
||||
area: {
|
||||
gradient: true,
|
||||
opacity: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
data: yAxis,
|
||||
},
|
||||
xAxis: {
|
||||
title: config.xAxis[0].name,
|
||||
titleFontColor: config.xAxis[0].nameTextStyle.color,
|
||||
titleFontSize: config.xAxis[0].nameTextStyle.fontSize,
|
||||
fontColor: config.xAxis[0].axisLabel.color,
|
||||
axisLine: config.xAxis[0].axisLine.show,
|
||||
axisLineColor: config.xAxis[0].axisLine.lineStyle.color,
|
||||
format: 'lineXAxis',
|
||||
axisFormat: config.xAxis[0].axisLabel.formatter,
|
||||
},
|
||||
dataLabel: config.label.show,
|
||||
legend: Llegend,
|
||||
},
|
||||
});
|
||||
});
|
||||
configCode += `const lineConfig${idx} = ref({
|
||||
height: ${o.h},
|
||||
title: "${config.title}",
|
||||
condition: ${JSON.stringify(config.condition)},
|
||||
count: ${JSON.stringify(config.count)},
|
||||
dataList:${JSON.stringify(dataList)},
|
||||
line: ${JSON.stringify(config.line)},
|
||||
fontSize:${config.label.fontSize},
|
||||
fontColor:"${config.label.color}",
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'Pie':
|
||||
code += `<PieChart :config="pieConfig${idx}"></PieChart>
|
||||
`;
|
||||
const Plegend = getLegend(config.echarts.legend);
|
||||
const type =
|
||||
config.echarts.series[0].radius[0] &&
|
||||
config.echarts.series[0].radius[0].split('%')[0] > 0
|
||||
? 'ring'
|
||||
: config.echarts.series[0].roseType
|
||||
? 'rose'
|
||||
: 'pie';
|
||||
let opts = '';
|
||||
if (type == 'pie') {
|
||||
opts = `pie: {
|
||||
"customRadius": ${config.echarts.series[0].radius[1].split('%')[0]},
|
||||
"labelWidth": 10,
|
||||
"border": false
|
||||
}`;
|
||||
} else if (type == 'ring') {
|
||||
opts = `ring: {
|
||||
ringWidth: ${config.echarts.series[0].radius[0].split('%')[0]}, //内圈大小
|
||||
labelWidth: 10,
|
||||
border: false,
|
||||
customRadius:${config.echarts.series[0].radius[1].split('%')[0]} //外圈大小
|
||||
}`;
|
||||
} else {
|
||||
opts = `rose: {
|
||||
type: "${config.echarts.series[0].roseType}",
|
||||
minRadius: ${config.echarts.series[0].radius[0].split('%')[0]},
|
||||
labelWidth: 10,
|
||||
border: false,
|
||||
}`;
|
||||
}
|
||||
configCode += `const pieConfig${idx} = ref({
|
||||
${apiConfig}
|
||||
labelKey: "${config.labelKey}",
|
||||
valueKey: "${config.valueKey}",
|
||||
title: "${config.title}",
|
||||
height: ${o.h},
|
||||
fontSize:${config.echarts.series[0].label.fontSize},
|
||||
fontColor:"${config.echarts.series[0].label.color}",
|
||||
pieType:"${type}", //rose玫瑰图,pie饼图,ring环形图
|
||||
options:{
|
||||
dataLabel: ${config.echarts.series[0].label.show},
|
||||
labelFormat:"${config.echarts.series[0].label.formatter}",
|
||||
legend: ${JSON.stringify(Plegend)},
|
||||
color: ${JSON.stringify(config.colors)},
|
||||
extra: {${opts}}
|
||||
}
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'Radar':
|
||||
code += `<RadarChart :config="radarConfig${idx}"></RadarChart>
|
||||
`;
|
||||
const Rlegend = getLegend(config.echarts.legend);
|
||||
configCode += `const radarConfig${idx} = ref({
|
||||
${apiConfig}
|
||||
labelKey: "${config.labelKey}",
|
||||
title: "${config.title}",
|
||||
height: ${o.h},
|
||||
indicator:${JSON.stringify(config.echarts.radar.indicator) || []},
|
||||
pointShape:"${config.echarts.series[0].symbol}",
|
||||
options:{
|
||||
dataLabel: ${config.echarts.series[0].label.show},
|
||||
legend: ${JSON.stringify(Rlegend)},
|
||||
color: ${JSON.stringify(config.colors)},
|
||||
extra: {
|
||||
"radar": {
|
||||
radius:${config.echarts.radar.radius},
|
||||
"gridType": "circle",
|
||||
"gridColor": "#CCCCCC",
|
||||
border:true,
|
||||
"opacity": ${config.echarts.showAreaStyle},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'Gauge':
|
||||
code += `<GaugeChart :config="gaugeConfig${idx}"></GaugeChart>
|
||||
`;
|
||||
configCode += `const gaugeConfig${idx} = ref({
|
||||
${apiConfig}
|
||||
labelKey: "${config.labelKey}",
|
||||
title: "${config.title}",
|
||||
height: ${o.h},
|
||||
valueKey: "${config.valueKey}",
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'Funnel':
|
||||
code += `<FunnelChart :config="funnelConfig${idx}"></FunnelChart>
|
||||
`;
|
||||
const Flegend = getLegend(config.echarts.legend);
|
||||
configCode += `const funnelConfig${idx} = ref({
|
||||
${apiConfig}
|
||||
fontSize:${config.echarts.series[0].label.fontSize},
|
||||
fontColor:"${config.echarts.series[0].label.color}",
|
||||
options:{
|
||||
labelFormat:"${config.echarts.series[0].label.formatter}",
|
||||
dataLabel:${config.echarts.series[0].label.show},
|
||||
color: ${JSON.stringify(config.colors)},
|
||||
padding: [${config.echarts.series[0].top || 0},${
|
||||
config.echarts.series[0].right || 0
|
||||
},${config.echarts.series[0].bottom || 0},${config.echarts.series[0].left || 0}],
|
||||
legend:${JSON.stringify(Flegend)},
|
||||
extra: {
|
||||
funnel: {
|
||||
activeOpacity: 0.3,
|
||||
activeWidth: 10,
|
||||
border: true,
|
||||
borderWidth: ${config.echarts.series[0].gap},
|
||||
borderColor: "#FFFFFF",
|
||||
fillOpacity: 1,
|
||||
labelAlign: "${
|
||||
config.echarts.series[0].label.show
|
||||
? config.echarts.series[0].label.position
|
||||
: 'left'
|
||||
}",
|
||||
type: "${
|
||||
config.echarts.series[0].sort == 'descending' ? 'triangle' : 'pyramid'
|
||||
}" //triangle倒三角,pyramid金字塔
|
||||
}
|
||||
}
|
||||
},
|
||||
labelKey: "${config.labelKey}",
|
||||
title: "${config.title}",
|
||||
height: ${o.h},
|
||||
valueKey: "${config.valueKey}",
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'ChartBar':
|
||||
code += `<BarChart :config="barConfig${idx}"></BarChart>
|
||||
`;
|
||||
configCode += `const barConfig${idx} = ref({
|
||||
${apiConfig}
|
||||
labelKey: "${config.labelKey}",
|
||||
targetKey:"${config.targetKey}",
|
||||
title: "${config.title}",
|
||||
height: ${o.h},
|
||||
unit: "${config.unit}",
|
||||
valueKey: "${config.valueKey}"
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'MyTask':
|
||||
code += `<MyTask :config="taskConfig${idx}"></MyTask>
|
||||
`;
|
||||
configCode += `const taskConfig${idx} = ref({
|
||||
title: "${config.title}",
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'TodoList':
|
||||
code += `<TodoList :config="todoConfig${idx}"></TodoList>
|
||||
`;
|
||||
configCode += `const todoConfig${idx} = ref({
|
||||
title: "${config.title}",
|
||||
maxRows: ${config.maxRows},
|
||||
});
|
||||
`;
|
||||
break;
|
||||
case 'Modules':
|
||||
code += `<Modules :config="modulesConfig${idx}"></Modules>
|
||||
`;
|
||||
configCode += `const modulesConfig${idx} = ref({
|
||||
title: "${config.title}",
|
||||
functions: ${JSON.stringify(config.functions)},
|
||||
});
|
||||
`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
compMap.forEach((o) => {
|
||||
switch (o) {
|
||||
case 'Dashboard':
|
||||
importCode += `import Dashboard from '@/components/dataDisplay/Dashboard.vue';
|
||||
`;
|
||||
break;
|
||||
case 'Banner':
|
||||
importCode += `import Banner from '@/components/dataDisplay/Banner.vue';
|
||||
`;
|
||||
break;
|
||||
case 'ChartLine':
|
||||
importCode += `import LineChart from '@/components/dataDisplay/LineChart.vue';
|
||||
`;
|
||||
break;
|
||||
case 'Pie':
|
||||
importCode += `import PieChart from '@/components/dataDisplay/PieChart.vue';
|
||||
`;
|
||||
break;
|
||||
case 'Radar':
|
||||
importCode += `import RadarChart from '@/components/dataDisplay/RadarChart.vue';
|
||||
`;
|
||||
break;
|
||||
case 'Gauge':
|
||||
importCode += `import GaugeChart from '@/components/dataDisplay/GaugeChart.vue';
|
||||
`;
|
||||
break;
|
||||
case 'Funnel':
|
||||
importCode += `import FunnelChart from '@/components/dataDisplay/FunnelChart.vue';
|
||||
`;
|
||||
break;
|
||||
case 'ChartBar':
|
||||
importCode += `import BarChart from '@/components/dataDisplay/BarChart.vue';
|
||||
`;
|
||||
break;
|
||||
case 'MyTask':
|
||||
importCode += `import MyTask from '@/components/dataDisplay/MyTask.vue';
|
||||
`;
|
||||
break;
|
||||
case 'TodoList':
|
||||
importCode += `import TodoList from '@/components/dataDisplay/TodoList.vue';
|
||||
`;
|
||||
break;
|
||||
case 'Modules':
|
||||
importCode += `import Modules from '@/components/dataDisplay/Modules.vue';
|
||||
`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
code += `</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
${importCode}
|
||||
${configCode}
|
||||
</script>
|
||||
|
||||
<style></style>`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function getLegend(config) {
|
||||
let left = 'left';
|
||||
let top = 'bottom';
|
||||
if (config.position == LocationType.LEFT_TOP) {
|
||||
left = config.orient == 'horizontal' ? 'left' : 'top';
|
||||
top = config.orient == 'horizontal' ? 'top' : 'left';
|
||||
} else if (config.position == LocationType.RIGHT_TOP) {
|
||||
left = config.orient == 'horizontal' ? 'right' : 'top';
|
||||
top = config.orient == 'horizontal' ? 'top' : 'right';
|
||||
} else if (config.position == LocationType.LEFT_BOTTOM) {
|
||||
left = config.orient == 'horizontal' ? 'left' : 'bottom';
|
||||
top = config.orient == 'horizontal' ? 'bottom' : 'left';
|
||||
} else if (config.position == LocationType.RIGHT_BOTTOM) {
|
||||
left = config.orient == 'horizontal' ? 'right' : 'bottom';
|
||||
top = config.orient == 'horizontal' ? 'bottom' : 'right';
|
||||
} else if (config.position == LocationType.TOP_CENTER) {
|
||||
left = 'center';
|
||||
top = config.orient == 'horizontal' ? 'top' : 'left';
|
||||
} else if (config.position == LocationType.BOTTOM_CENTER) {
|
||||
left = 'center';
|
||||
top = config.orient == 'horizontal' ? 'bottom' : 'left';
|
||||
}
|
||||
const legend = {
|
||||
show: config.show,
|
||||
position: top,
|
||||
float: left,
|
||||
margin: config.padding,
|
||||
legendShape: config.icon,
|
||||
fontColor: config.textStyle.color,
|
||||
};
|
||||
return legend;
|
||||
}
|
||||
1403
src/utils/helper/designHelper.ts
Normal file
1403
src/utils/helper/designHelper.ts
Normal file
File diff suppressed because it is too large
Load Diff
530
src/utils/helper/exportHelper.ts
Normal file
530
src/utils/helper/exportHelper.ts
Normal file
@ -0,0 +1,530 @@
|
||||
import { camelCaseString } from '/@/utils/event/design';
|
||||
|
||||
//查询为inputNumber的组件
|
||||
const iptNumComponents = ['number', 'slider', 'rate', 'computational', 'money-chinese'];
|
||||
|
||||
const remoteComponents = [
|
||||
'select',
|
||||
'associate-select',
|
||||
'associate-popup',
|
||||
'multiple-popup',
|
||||
'checkbox',
|
||||
'radio',
|
||||
];
|
||||
|
||||
export const handleSearchForm = (option, schema, item, isNeedTrans, convertCamel = true) => {
|
||||
if (!isNeedTrans) {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'Input',
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'Input',
|
||||
};
|
||||
}
|
||||
if (iptNumComponents.includes(schema.type)) {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'InputNumber',
|
||||
componentProps:{
|
||||
style:{ width:'100%' }
|
||||
},
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
style: { width: '100%' },
|
||||
},
|
||||
};
|
||||
}
|
||||
if (remoteComponents.includes(schema.type)) {
|
||||
if (option && option?.datasourceType === 'staticData') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'XjrSelect',
|
||||
componentProps: {
|
||||
datasourceType:'staticData',
|
||||
staticOptions: ${JSON.stringify(option.staticOptions)},
|
||||
labelField: '${option.labelField}',
|
||||
valueField: '${option.valueField}',
|
||||
${
|
||||
schema.type === 'checkbox' || schema.type === 'multiple-popup' ? "mode:'multiple'," : ''
|
||||
}
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'XjrSelect',
|
||||
componentProps: {
|
||||
datasourceType: 'staticData',
|
||||
staticOptions: option.staticOptions,
|
||||
labelField: option.labelField,
|
||||
valueField: option.valueField,
|
||||
mode:
|
||||
schema.type === 'checkbox' || schema.type === 'multiple-popup' ? 'multiple' : '',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
} else if (option && option?.datasourceType === 'dic') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'XjrSelect',
|
||||
componentProps: {
|
||||
datasourceType:'dic',
|
||||
params: ${JSON.stringify(option.params)},
|
||||
labelField: '${option.labelField}',
|
||||
valueField: '${option.valueField}',
|
||||
${
|
||||
schema.type === 'checkbox' || schema.type === 'multiple-popup' ? "mode:'multiple'," : ''
|
||||
}
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'XjrSelect',
|
||||
componentProps: {
|
||||
datasourceType: 'dic',
|
||||
params: option.params,
|
||||
labelField: option.labelField,
|
||||
valueField: option.valueField,
|
||||
mode:
|
||||
schema.type === 'checkbox' || schema.type === 'multiple-popup' ? 'multiple' : '',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
} else if (option && option?.datasourceType === 'api') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'XjrSelect',
|
||||
componentProps: {
|
||||
datasourceType:'api',
|
||||
apiConfig: ${JSON.stringify(option.apiConfig)},
|
||||
labelField: '${option.labelField}',
|
||||
valueField: '${option.valueField}',
|
||||
${schema.type === 'checkbox' || schema.type === 'multiple-popup' ? "mode:'multiple'," : ''}
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'XjrSelect',
|
||||
componentProps: {
|
||||
datasourceType: 'api',
|
||||
apiConfig: option.apiConfig,
|
||||
labelField: option.labelField,
|
||||
valueField: option.valueField,
|
||||
mode:
|
||||
schema.type === 'checkbox' || schema.type === 'multiple-popup' ? 'multiple' : '',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (schema.type === 'switch') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'Select',
|
||||
componentProps:{
|
||||
getPopupContainer: () => document.body,
|
||||
options:[
|
||||
{
|
||||
label:'开',
|
||||
value: 1
|
||||
},{
|
||||
label:'关',
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: '开',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '关',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'time') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'TimeRangePicker',
|
||||
componentProps:{
|
||||
format:'${option.format}',
|
||||
style: { width: '100%' },
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'TimeRangePicker',
|
||||
componentProps: {
|
||||
format: option.format,
|
||||
style: { width: '100%' },
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'date') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'RangePicker',
|
||||
componentProps:{
|
||||
format:'${option.format}',
|
||||
style: { width: '100%' },
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: option.format,
|
||||
style: { width: '100%' },
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'user') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'User',
|
||||
componentProps:{
|
||||
suffix:'ant-design:setting-outlined',
|
||||
placeholder:'请选择'
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'User',
|
||||
componentProps: {
|
||||
suffix: 'ant-design:setting-outlined',
|
||||
placeholder: '请选择',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'area') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'Area',
|
||||
componentProps:{
|
||||
suffix:'ant-design:setting-outlined',
|
||||
placeholder:'请选择',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'Area',
|
||||
componentProps: {
|
||||
suffix: 'ant-design:setting-outlined',
|
||||
placeholder: '请选择',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'organization') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: '${schema!.component}',
|
||||
componentProps:{
|
||||
placeholder:'请选择',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: schema!.component,
|
||||
componentProps: {
|
||||
placeholder: '请选择',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'cascader') {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: '${schema!.component}',
|
||||
componentProps: {
|
||||
apiConfig: ${JSON.stringify(option.apiConfig)},
|
||||
showFormat: '${option.showFormat}',
|
||||
separator: '${option.separator}',
|
||||
selectedConfig: 'any',
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: schema!.component,
|
||||
componentProps: {
|
||||
apiConfig: option.apiConfig,
|
||||
showFormat: option.showFormat,
|
||||
separator: option.separator,
|
||||
selectedConfig: 'any',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (schema.type === 'info') {
|
||||
if (option && option.infoType === 0) {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'User',
|
||||
componentProps:{
|
||||
suffix:'ant-design:setting-outlined',
|
||||
placeholder:'请选择'
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'User',
|
||||
componentProps: {
|
||||
suffix: 'ant-design:setting-outlined',
|
||||
placeholder: '请选择',
|
||||
},
|
||||
};
|
||||
} else if (option && option.infoType === 1) {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'Dept',
|
||||
componentProps:{
|
||||
placeholder:'请选择',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'Dept',
|
||||
componentProps: {
|
||||
placeholder: '请选择',
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
} else if (option && option.infoType === 2) {
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
style: { width: '100%' },
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
style: { width: '100%' },
|
||||
getPopupContainer: () => document.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return convertCamel
|
||||
? `{
|
||||
field: '${camelCaseString(item.fieldName)}',
|
||||
label: '${schema!.label}',
|
||||
component: '${schema!.component}',
|
||||
},
|
||||
`
|
||||
: {
|
||||
field: item.fieldName,
|
||||
label: schema!.label,
|
||||
component: schema!.component,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleAppSearchForm = (option, schema, item, isNeedTrans, convertCamel = true) => {
|
||||
if (!schema) return;
|
||||
const fieldName = convertCamel ? camelCaseString(item.fieldName) : item.fieldName;
|
||||
const field = `field: '${fieldName}',`;
|
||||
const params = `key: '${schema.key}',
|
||||
name: '${schema!.label}',
|
||||
label: '${schema!.label}',`;
|
||||
if (isNeedTrans) {
|
||||
if (schema.component == 'TimeRange') {
|
||||
return `{
|
||||
${params}
|
||||
field: '${fieldName + 'Start,' + fieldName + 'End'}',
|
||||
component: componentType.timeRange, //组件类型
|
||||
startTimeField:'${fieldName + 'Start'}',
|
||||
endTimeField:'${fieldName + 'End'}',
|
||||
componentProps: {
|
||||
startTimePlaceholder: '开始时间',
|
||||
endTimePlaceholder: '结束时间',
|
||||
isRange: true,
|
||||
}, //组件的所有配置信息
|
||||
},`;
|
||||
} else if (schema.component == 'DateTime') {
|
||||
return `{
|
||||
${params}
|
||||
field: '${fieldName + 'Start,' + fieldName + 'End'}',
|
||||
component: componentType.dateRange, //组件类型
|
||||
startTimeField:'${fieldName + 'Start'}',
|
||||
endTimeField:'${fieldName + 'End'}',
|
||||
componentProps: {
|
||||
formatType:'${
|
||||
option.format == 'YYYY-MM'
|
||||
? 'month'
|
||||
: option.format == 'YYYY'
|
||||
? 'year'
|
||||
: option.format == 'YYYY-MM-DD HH:mm:ss'
|
||||
? 'datetime'
|
||||
: 'date'
|
||||
}',
|
||||
type: "datetimerange", //日期时间范围选择器
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
}, //组件的所有配置信息
|
||||
},`;
|
||||
} else if (schema.component == 'Switch') {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.select, //组件类型
|
||||
componentProps: {
|
||||
localdata:[{text:'开',value:1},{text:'关',value:0}],
|
||||
},
|
||||
},`;
|
||||
} else if (
|
||||
schema.component == 'Checkbox' ||
|
||||
schema.component == 'Radio' ||
|
||||
schema.component == 'Select'
|
||||
) {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.select, //组件类型
|
||||
componentProps: ${JSON.stringify(schema.componentProps)},
|
||||
},`;
|
||||
} else if (schema.component == 'colorPicker') {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.color, //组件类型
|
||||
defaultValue:null,
|
||||
componentProps: {},
|
||||
},`;
|
||||
} else if (schema.component == 'InputNumber') {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.input, //组件类型
|
||||
componentProps: {
|
||||
type: 'number',
|
||||
},
|
||||
},`;
|
||||
} else if (schema.component == 'Info') {
|
||||
if (schema.componentProps.infoType === 0) {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.user, //组件类型
|
||||
componentProps: {
|
||||
placeholder: '请选择人员',
|
||||
suffixIcon: 'ant-design:setting-outlined',
|
||||
},
|
||||
},`;
|
||||
} else if (schema.componentProps.infoType === 1) {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.organization, //组件类型
|
||||
componentProps: {
|
||||
placeholder: '请选择组织架构',
|
||||
},
|
||||
},`;
|
||||
} else if (schema.componentProps.infoType === 2) {
|
||||
return `{
|
||||
${params}
|
||||
field: '${fieldName + 'Start,' + fieldName + 'End'}',
|
||||
component: componentType.dateRange, //组件类型
|
||||
startTimeField:'${fieldName + 'Start'}',
|
||||
endTimeField:'${fieldName + 'End'}',
|
||||
componentProps: {
|
||||
formatType: 'datetime',
|
||||
type: 'datetimerange', //日期时间范围选择器
|
||||
startPlaceholder: '开始日期',
|
||||
endPlaceholder: '结束日期',
|
||||
},
|
||||
},`;
|
||||
}
|
||||
} else {
|
||||
return JSON.stringify(schema) + ',';
|
||||
}
|
||||
} else {
|
||||
return `{
|
||||
${params}
|
||||
${field}
|
||||
component: componentType.input, //组件类型
|
||||
componentProps: {
|
||||
placeholder: '请输入${schema.label}',
|
||||
}
|
||||
},`;
|
||||
}
|
||||
};
|
||||
153
src/utils/helper/generatorAppHelper.ts
Normal file
153
src/utils/helper/generatorAppHelper.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { camelCaseString } from '../event/design';
|
||||
import { buildAppFormProps } from './designHelper';
|
||||
import { ComponentOptionModel, FormJson } from '/@/model/generator/codeGenerator';
|
||||
|
||||
export const buildAppSchemeStrBySchema = (json: FormJson, convertCamel = true) => {
|
||||
const props = buildAppFormProps(json, convertCamel);
|
||||
return props;
|
||||
};
|
||||
|
||||
export const buildAppSchemeStr = (list: ComponentOptionModel[], convertCamel: boolean): string => {
|
||||
const code = `${list.map((item) => {
|
||||
if (item.type === 'divider') {
|
||||
return `{
|
||||
key: '${item.key}',
|
||||
field: ${convertCamel ? camelCaseString(item.bindField) : item.bindField}
|
||||
name: '${item.label}',
|
||||
component: componentType.divider,
|
||||
componentProps: {
|
||||
text: '${item.options!.defaultValue}',
|
||||
align: '${item.options!.orientation}',
|
||||
},
|
||||
}`;
|
||||
}
|
||||
//如果是栅格布局组件 手机端没有 必须要扁平化
|
||||
else if (item.type === 'grid') {
|
||||
let gridStr = ``;
|
||||
item.layout?.map((el) => {
|
||||
gridStr += buildAppSchemeStr(el.list, convertCamel);
|
||||
});
|
||||
|
||||
return gridStr;
|
||||
} //如果是tab组件 转换为手机端配置
|
||||
else if (item.type === 'tab') {
|
||||
return `{
|
||||
key: '${item.key}',
|
||||
name: '${item.label}',
|
||||
component: componentType.segmented,
|
||||
layout: [${item.layout?.map((el, index) => {
|
||||
return `{
|
||||
name: '${el.name}',
|
||||
value: '${index}',
|
||||
children: [${buildAppSchemeStr(el.list, convertCamel)}],
|
||||
}`;
|
||||
})}],
|
||||
}`;
|
||||
} else if (item.type === 'card') {
|
||||
return `{
|
||||
key: '${item.key}',
|
||||
name: '${item.label}',
|
||||
component: componentType.collapse,
|
||||
layout: [${item.layout?.map((el, index) => {
|
||||
return `{
|
||||
name: '${el.name}',
|
||||
value: '${index}',
|
||||
children: [${buildAppSchemeStr(el.list, convertCamel)}],
|
||||
}`;
|
||||
})}],
|
||||
}`;
|
||||
} else if (item.type === 'subForm') {
|
||||
return `{
|
||||
field: '${item.bindField}', //字段
|
||||
key: '${item.key}',
|
||||
name: '${item.label}',
|
||||
component: componentType.subForm,
|
||||
columns: [${buildAppSchemeStr(item.children!, convertCamel)}],
|
||||
}`;
|
||||
} else {
|
||||
return buildAppDefaultSchema(item, convertCamel);
|
||||
}
|
||||
})}`;
|
||||
return code;
|
||||
};
|
||||
|
||||
export function buildAppDefaultSchema(model: ComponentOptionModel, convertCamel: boolean): string {
|
||||
const compType = buildAppComponentTypeStr(model.type);
|
||||
const schema = `{
|
||||
key: '${model.key}',
|
||||
field: '${(convertCamel ? camelCaseString(model.bindField)! : model.bindField) || ''}',
|
||||
label: '${model.label}',
|
||||
component: '${compType}',
|
||||
defaultValue: '${model.options?.defaultValue}',
|
||||
componentProps: {
|
||||
placeholder: '请输入${model.label}',
|
||||
},
|
||||
name: '${model.label}',
|
||||
}`;
|
||||
return schema;
|
||||
}
|
||||
|
||||
export function buildAppComponentTypeStr(type: string): string {
|
||||
switch (type) {
|
||||
case 'input':
|
||||
return 'componentType.input';
|
||||
|
||||
case 'password':
|
||||
return 'componentType.input';
|
||||
|
||||
case 'textarea':
|
||||
return 'componentType.input';
|
||||
|
||||
case 'number':
|
||||
return 'componentType.inputNumber';
|
||||
|
||||
case 'radio':
|
||||
return 'componentType.radio';
|
||||
|
||||
case 'checkbox':
|
||||
return 'componentType.checkbox';
|
||||
|
||||
case 'select':
|
||||
return 'componentType.select';
|
||||
|
||||
case 'cascader':
|
||||
return 'componentType.select';
|
||||
|
||||
case 'time':
|
||||
return 'componentType.dateTime';
|
||||
case 'date':
|
||||
return 'componentType.dateTime';
|
||||
|
||||
case 'time-range':
|
||||
return 'componentType.dateRange';
|
||||
|
||||
case 'date-range':
|
||||
return 'componentType.dateRange';
|
||||
|
||||
case 'rate':
|
||||
return 'componentType.inputNumber';
|
||||
|
||||
case 'switch':
|
||||
return 'componentType.switch';
|
||||
|
||||
case 'slider':
|
||||
return 'componentType.inputNumber';
|
||||
|
||||
case 'divider':
|
||||
return 'componentType.divider';
|
||||
|
||||
case 'upload':
|
||||
return 'componentType.input';
|
||||
|
||||
case 'richtext-editor':
|
||||
return 'componentType.input';
|
||||
|
||||
case 'form':
|
||||
return 'componentType.subForm';
|
||||
case 'one-for-one':
|
||||
return 'componentType.subForm';
|
||||
|
||||
default:
|
||||
return 'componentType.input';
|
||||
}
|
||||
}
|
||||
2951
src/utils/helper/generatorHelper.ts
Normal file
2951
src/utils/helper/generatorHelper.ts
Normal file
File diff suppressed because it is too large
Load Diff
204
src/utils/helper/treeHelper.ts
Normal file
204
src/utils/helper/treeHelper.ts
Normal file
@ -0,0 +1,204 @@
|
||||
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);
|
||||
|
||||
// tree from list
|
||||
export function listToTree<T = any>(list: any[], config: Partial<TreeHelperConfig> = {}): T[] {
|
||||
const conf = getConfig(config) as TreeHelperConfig;
|
||||
const nodeMap = new Map();
|
||||
const result: T[] = [];
|
||||
const { id, children, pid } = conf;
|
||||
|
||||
for (const node of list) {
|
||||
node[children] = node[children] || [];
|
||||
nodeMap.set(node[id], node);
|
||||
}
|
||||
for (const node of list) {
|
||||
const parent = nodeMap.get(node[pid]);
|
||||
(parent ? parent[children] : result).push(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function treeToList<T = any>(tree: any, config: Partial<TreeHelperConfig> = {}): T {
|
||||
config = getConfig(config);
|
||||
const { children } = config;
|
||||
const result: any = [...tree];
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (!result[i][children!]) continue;
|
||||
result.splice(i + 1, 0, ...result[i][children!]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findNode<T = any>(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T | null {
|
||||
config = getConfig(config);
|
||||
const { children } = config;
|
||||
const list = [...tree];
|
||||
for (const node of list) {
|
||||
if (func(node)) return node;
|
||||
node[children!] && list.push(...node[children!]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findNodeAll<T = any>(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T[] {
|
||||
config = getConfig(config);
|
||||
const { children } = config;
|
||||
const list = [...tree];
|
||||
const result: T[] = [];
|
||||
for (const node of list) {
|
||||
func(node) && result.push(node);
|
||||
node[children!] && list.push(...node[children!]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findPath<T = any>(
|
||||
tree: any,
|
||||
func: Fn,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T | T[] | null {
|
||||
config = getConfig(config);
|
||||
const path: T[] = [];
|
||||
const list = [...tree];
|
||||
const visitedSet = new Set();
|
||||
const { children } = config;
|
||||
while (list.length) {
|
||||
const node = list[0];
|
||||
if (visitedSet.has(node)) {
|
||||
path.pop();
|
||||
list.shift();
|
||||
} else {
|
||||
visitedSet.add(node);
|
||||
node[children!] && list.unshift(...node[children!]);
|
||||
path.push(node);
|
||||
if (func(node)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findPathAll(tree: any, func: Fn, config: Partial<TreeHelperConfig> = {}) {
|
||||
config = getConfig(config);
|
||||
const path: any[] = [];
|
||||
const list = [...tree];
|
||||
const result: any[] = [];
|
||||
const visitedSet = new Set(),
|
||||
{ children } = config;
|
||||
while (list.length) {
|
||||
const node = list[0];
|
||||
if (visitedSet.has(node)) {
|
||||
path.pop();
|
||||
list.shift();
|
||||
} else {
|
||||
visitedSet.add(node);
|
||||
node[children!] && list.unshift(...node[children!]);
|
||||
path.push(node);
|
||||
func(node) && result.push([...path]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function filter<T = any>(
|
||||
tree: T[],
|
||||
func: (n: T) => boolean,
|
||||
config: Partial<TreeHelperConfig> = {},
|
||||
): T[] {
|
||||
config = getConfig(config);
|
||||
const children = config.children as string;
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Extract tree specified structure
|
||||
*/
|
||||
export function treeMap<T = any>(treeData: T[], opt: { children?: string; conversion: Fn }): T[] {
|
||||
return treeData.map((item) => treeMapEach(item, opt));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Extract tree specified structure
|
||||
*/
|
||||
export function treeMapEach(
|
||||
data: any,
|
||||
{ children = 'children', conversion }: { children?: string; conversion: Fn },
|
||||
) {
|
||||
const haveChildren = Array.isArray(data[children]) && data[children].length > 0;
|
||||
const conversionData = conversion(data) || {};
|
||||
if (haveChildren) {
|
||||
return {
|
||||
...conversionData,
|
||||
[children]: data[children].map((i: number) =>
|
||||
treeMapEach(i, {
|
||||
children,
|
||||
conversion,
|
||||
}),
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...conversionData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历树结构
|
||||
* @param treeDatas 树
|
||||
* @param callBack 回调
|
||||
* @param parentNode 父节点
|
||||
*/
|
||||
export function eachTree(treeDatas: any[], callBack: Fn, parentNode = {}) {
|
||||
treeDatas.forEach((element) => {
|
||||
const newNode = callBack(element, parentNode) || element;
|
||||
if (element.children) {
|
||||
eachTree(element.children, callBack, newNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
35
src/utils/helper/tsxHelper.tsx
Normal file
35
src/utils/helper/tsxHelper.tsx
Normal file
@ -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;
|
||||
}
|
||||
330
src/utils/http/axios/Axios.ts
Normal file
330
src/utils/http/axios/Axios.ts
Normal file
@ -0,0 +1,330 @@
|
||||
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
|
||||
import type { RequestOptions, Result, UploadFileParams } from '/#/axios';
|
||||
import type { CreateAxiosOptions } from './axiosTransform';
|
||||
import axios from 'axios';
|
||||
import qs from 'qs';
|
||||
import { AxiosCanceler } from './axiosCancel';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ContentTypeEnum } from '/@/enums/httpEnum';
|
||||
import { RequestEnum } from '/@/enums/httpEnum';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { router } from '/@/router';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
|
||||
export * from './axiosTransform';
|
||||
|
||||
/**
|
||||
* @description: axios module
|
||||
*/
|
||||
export class VAxios {
|
||||
private axiosInstance: AxiosInstance;
|
||||
private readonly options: CreateAxiosOptions;
|
||||
|
||||
constructor(options: CreateAxiosOptions) {
|
||||
this.options = options;
|
||||
this.axiosInstance = axios.create(options);
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Create axios instance
|
||||
*/
|
||||
private createAxios(config: CreateAxiosOptions): void {
|
||||
this.axiosInstance = axios.create(config);
|
||||
}
|
||||
|
||||
private getTransform() {
|
||||
const { transform } = this.options;
|
||||
return transform;
|
||||
}
|
||||
|
||||
getAxios(): AxiosInstance {
|
||||
return this.axiosInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Reconfigure axios
|
||||
*/
|
||||
configAxios(config: CreateAxiosOptions) {
|
||||
if (!this.axiosInstance) {
|
||||
return;
|
||||
}
|
||||
this.createAxios(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Set general header
|
||||
*/
|
||||
setHeader(headers: any): void {
|
||||
if (!this.axiosInstance) {
|
||||
return;
|
||||
}
|
||||
Object.assign(this.axiosInstance.defaults.headers, headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Interceptor configuration
|
||||
*/
|
||||
private setupInterceptors() {
|
||||
const transform = this.getTransform();
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
requestInterceptors,
|
||||
requestInterceptorsCatch,
|
||||
responseInterceptors,
|
||||
responseInterceptorsCatch,
|
||||
} = transform;
|
||||
|
||||
const axiosCanceler = new AxiosCanceler();
|
||||
|
||||
// Request interceptor configuration processing
|
||||
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
|
||||
// If cancel repeat request is turned on, then cancel repeat request is prohibited
|
||||
// @ts-ignore
|
||||
const { ignoreCancelToken } = config.requestOptions;
|
||||
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);
|
||||
|
||||
// Request interceptor error capture
|
||||
requestInterceptorsCatch &&
|
||||
isFunction(requestInterceptorsCatch) &&
|
||||
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
|
||||
|
||||
// Response result interceptor processing
|
||||
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
|
||||
res && axiosCanceler.removePending(res.config);
|
||||
if (responseInterceptors && isFunction(responseInterceptors)) {
|
||||
res = responseInterceptors(res);
|
||||
}
|
||||
return res;
|
||||
}, undefined);
|
||||
|
||||
// Response result interceptor error capture
|
||||
responseInterceptorsCatch &&
|
||||
isFunction(responseInterceptorsCatch) &&
|
||||
this.axiosInstance.interceptors.response.use(undefined, (error) => {
|
||||
// @ts-ignore
|
||||
return responseInterceptorsCatch(this.axiosInstance, error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: File Upload
|
||||
*/
|
||||
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
|
||||
const formData = new window.FormData();
|
||||
const customFilename = params.name || 'file';
|
||||
|
||||
if (params.filename) {
|
||||
if (params.file instanceof Array) {
|
||||
for (const f of params.file as File[]) {
|
||||
formData.append(customFilename, f, params.filename);
|
||||
}
|
||||
} else {
|
||||
formData.append(customFilename, params.file, params.filename);
|
||||
}
|
||||
} else {
|
||||
if (params.file instanceof Array) {
|
||||
for (const f of params.file as File[]) {
|
||||
formData.append(customFilename, f);
|
||||
}
|
||||
} 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.request<T>({
|
||||
...config,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
transformRequest: [
|
||||
function () {
|
||||
return formData;
|
||||
},
|
||||
],
|
||||
headers: {
|
||||
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||
// @ts-ignore
|
||||
ignoreCancelToken: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// support form-data
|
||||
supportFormData(config: AxiosRequestConfig) {
|
||||
const headers = config.headers || this.options.headers;
|
||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
|
||||
|
||||
if (
|
||||
contentType !== ContentTypeEnum.FORM_URLENCODED ||
|
||||
!Reflect.has(config, 'data') ||
|
||||
config.method?.toUpperCase() === RequestEnum.GET
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
data: qs.stringify(config.data, { arrayFormat: 'brackets' }),
|
||||
};
|
||||
}
|
||||
|
||||
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'GET' }, options);
|
||||
}
|
||||
|
||||
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'POST' }, options);
|
||||
}
|
||||
|
||||
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'PUT' }, options);
|
||||
}
|
||||
|
||||
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'DELETE' }, options);
|
||||
}
|
||||
download<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
let conf: CreateAxiosOptions = cloneDeep(config);
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt: RequestOptions = Object.assign({}, requestOptions, options);
|
||||
|
||||
const { beforeRequestHook, requestCatchHook } = transform || {};
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||
conf = beforeRequestHook(conf, opt);
|
||||
}
|
||||
conf.requestOptions = opt;
|
||||
|
||||
conf = this.supportFormData(conf);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<any, AxiosResponse<Result>>(conf)
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error | AxiosError) => {
|
||||
if (requestCatchHook && isFunction(requestCatchHook)) {
|
||||
reject(requestCatchHook(e, opt));
|
||||
return;
|
||||
}
|
||||
if (axios.isAxiosError(e)) {
|
||||
// rewrite error message from axios in here
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
desktopApi<T = any>(url, method, options?: RequestOptions): Promise<T> {
|
||||
let conf: CreateAxiosOptions = cloneDeep({ url, method });
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt: RequestOptions = Object.assign({}, requestOptions, options);
|
||||
|
||||
const { beforeRequestHook, requestCatchHook } = transform || {};
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||
conf = beforeRequestHook(conf, opt);
|
||||
}
|
||||
conf.requestOptions = opt;
|
||||
|
||||
conf = this.supportFormData(conf);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<any, AxiosResponse<Result>>(conf)
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
resolve(res.data.data as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error | AxiosError) => {
|
||||
if (requestCatchHook && isFunction(requestCatchHook)) {
|
||||
reject(requestCatchHook(e, opt));
|
||||
return;
|
||||
}
|
||||
if (axios.isAxiosError(e)) {
|
||||
// rewrite error message from axios in here
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
let conf: CreateAxiosOptions = cloneDeep(config);
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt: RequestOptions = Object.assign({}, requestOptions, options);
|
||||
|
||||
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||
conf = beforeRequestHook(conf, opt);
|
||||
}
|
||||
conf.requestOptions = opt;
|
||||
|
||||
conf = this.supportFormData(conf);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<any, AxiosResponse<Result>>(conf)
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
if (transformRequestHook && isFunction(transformRequestHook)) {
|
||||
try {
|
||||
const ret = transformRequestHook(res, opt);
|
||||
resolve(ret);
|
||||
} catch (err) {
|
||||
reject(err || new Error('request error!'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error | AxiosError) => {
|
||||
if (e.message.includes('timeout') || e.message.includes('Network Error')) {
|
||||
const userStore = useUserStore();
|
||||
userStore.setToken(undefined);
|
||||
userStore.setSessionTimeout(false);
|
||||
userStore.setUserInfo(null);
|
||||
router.push(PageEnum.BASE_LOGIN);
|
||||
}
|
||||
if (requestCatchHook && isFunction(requestCatchHook)) {
|
||||
reject(requestCatchHook(e, opt));
|
||||
return;
|
||||
}
|
||||
if (axios.isAxiosError(e)) {
|
||||
// rewrite error message from axios in here
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
60
src/utils/http/axios/axiosCancel.ts
Normal file
60
src/utils/http/axios/axiosCancel.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type { AxiosRequestConfig, Canceler } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
|
||||
// Used to store the identification and cancellation function of each request
|
||||
let pendingMap = new Map<string, Canceler>();
|
||||
|
||||
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
|
||||
|
||||
export class AxiosCanceler {
|
||||
/**
|
||||
* Add request
|
||||
* @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)) {
|
||||
// If there is no current request in pending, add it
|
||||
pendingMap.set(url, cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Clear all pending
|
||||
*/
|
||||
removeAllPending() {
|
||||
pendingMap.forEach((cancel) => {
|
||||
cancel && isFunction(cancel) && cancel();
|
||||
});
|
||||
pendingMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removal request
|
||||
* @param {Object} config
|
||||
*/
|
||||
removePending(config: AxiosRequestConfig) {
|
||||
const url = getPendingUrl(config);
|
||||
|
||||
if (pendingMap.has(url)) {
|
||||
// If there is a current request identifier in pending,
|
||||
// the current request needs to be cancelled and removed
|
||||
const cancel = pendingMap.get(url);
|
||||
cancel && cancel(url);
|
||||
pendingMap.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: reset
|
||||
*/
|
||||
reset(): void {
|
||||
pendingMap = new Map<string, Canceler>();
|
||||
}
|
||||
}
|
||||
28
src/utils/http/axios/axiosRetry.ts
Normal file
28
src/utils/http/axios/axiosRetry.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { AxiosError, AxiosInstance } from 'axios';
|
||||
/**
|
||||
* 请求重试机制
|
||||
*/
|
||||
|
||||
export class AxiosRetry {
|
||||
/**
|
||||
* 重试
|
||||
*/
|
||||
retry(AxiosInstance: AxiosInstance, error: AxiosError) {
|
||||
// @ts-ignore
|
||||
const { config } = error.response;
|
||||
const { waitTime, count } = config?.requestOptions?.retryRequest;
|
||||
config.__retryCount = config.__retryCount || 0;
|
||||
if (config.__retryCount >= count) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
config.__retryCount += 1;
|
||||
return this.delay(waitTime).then(() => AxiosInstance(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟
|
||||
*/
|
||||
private delay(waitTime: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||
}
|
||||
}
|
||||
52
src/utils/http/axios/axiosTransform.ts
Normal file
52
src/utils/http/axios/axiosTransform.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Data processing class, can be configured according to the project
|
||||
*/
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import type { RequestOptions, Result } from '/#/axios';
|
||||
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||
authenticationScheme?: string;
|
||||
transform?: AxiosTransform;
|
||||
requestOptions?: RequestOptions;
|
||||
}
|
||||
|
||||
export abstract class AxiosTransform {
|
||||
/**
|
||||
* @description: Process configuration before request
|
||||
* @description: Process configuration before request
|
||||
*/
|
||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: Request successfully processed
|
||||
*/
|
||||
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
|
||||
|
||||
/**
|
||||
* @description: 请求失败处理
|
||||
*/
|
||||
requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器
|
||||
*/
|
||||
requestInterceptors?: (
|
||||
config: AxiosRequestConfig,
|
||||
options: CreateAxiosOptions,
|
||||
) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器
|
||||
*/
|
||||
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器错误处理
|
||||
*/
|
||||
requestInterceptorsCatch?: (error: Error) => void;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器错误处理
|
||||
*/
|
||||
responseInterceptorsCatch?: (axiosInstance: AxiosResponse, error: Error) => void;
|
||||
}
|
||||
80
src/utils/http/axios/checkStatus.ts
Normal file
80
src/utils/http/axios/checkStatus.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import type { ErrorMessageMode } from '/#/axios';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
// import router from '/@/router';
|
||||
// import { PageEnum } from '/@/enums/pageEnum';
|
||||
import { useUserStoreWithOut } from '/@/store/modules/user';
|
||||
import projectSetting from '/@/settings/projectSetting';
|
||||
import { SessionTimeoutProcessingEnum } from '/@/enums/appEnum';
|
||||
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
const error = createMessage.error!;
|
||||
const stp = projectSetting.sessionTimeoutProcessing;
|
||||
|
||||
export function checkStatus(
|
||||
status: number,
|
||||
msg: string,
|
||||
errorMessageMode: ErrorMessageMode = 'message',
|
||||
): void {
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStoreWithOut();
|
||||
let errMessage = '';
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
errMessage = `${msg}`;
|
||||
break;
|
||||
// 401: Not logged in
|
||||
// Jump to the login page if not logged in, and carry the path of the current page
|
||||
// Return to the current page after successful login. This step needs to be operated on the login page.
|
||||
case 401:
|
||||
userStore.setToken(undefined);
|
||||
errMessage = msg || t('用户没有权限(令牌、用户名、密码错误)!');
|
||||
if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
|
||||
userStore.setSessionTimeout(true);
|
||||
} else {
|
||||
userStore.logout(true);
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
errMessage = t('用户得到授权,但是访问是被禁止的!');
|
||||
break;
|
||||
// 404请求不存在
|
||||
case 404:
|
||||
errMessage = t('网络请求错误,未找到该资源!');
|
||||
break;
|
||||
case 405:
|
||||
errMessage = t('网络请求错误,请求方法未允许!');
|
||||
break;
|
||||
case 408:
|
||||
errMessage = t('网络请求超时!');
|
||||
break;
|
||||
case 500:
|
||||
errMessage = t('服务器错误,请联系管理员!');
|
||||
break;
|
||||
case 501:
|
||||
errMessage = t('网络未实现!');
|
||||
break;
|
||||
case 502:
|
||||
errMessage = t('网络错误!');
|
||||
break;
|
||||
case 503:
|
||||
errMessage = t('服务不可用,服务器暂时过载或维护!');
|
||||
break;
|
||||
case 504:
|
||||
errMessage = t('网络超时!');
|
||||
break;
|
||||
case 505:
|
||||
errMessage = t('http版本不支持该请求!');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
if (errMessage) {
|
||||
if (errorMessageMode === 'modal') {
|
||||
createErrorModal({ title: t('错误提示'), content: errMessage });
|
||||
} else if (errorMessageMode === 'message') {
|
||||
error({ content: errMessage, key: `global_error_message_status_${status}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/utils/http/axios/helper.ts
Normal file
48
src/utils/http/axios/helper.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { isObject, isString } from '/@/utils/is';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
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) {
|
||||
const format = params[key]?.format ?? null;
|
||||
if (format && typeof format === 'function') {
|
||||
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: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isObject(params[key])) {
|
||||
formatRequestDate(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
270
src/utils/http/axios/index.ts
Normal file
270
src/utils/http/axios/index.ts
Normal file
@ -0,0 +1,270 @@
|
||||
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
|
||||
// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { clone, isBoolean } from 'lodash-es';
|
||||
import type { RequestOptions, Result } from '/#/axios';
|
||||
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
|
||||
import { VAxios } from './Axios';
|
||||
import { checkStatus } from './checkStatus';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum';
|
||||
import { isString } from '/@/utils/is';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
import { setObjToUrlParams, deepMerge } from '/@/utils';
|
||||
import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { joinTimestamp, formatRequestDate } from './helper';
|
||||
import { useUserStoreWithOut } from '/@/store/modules/user';
|
||||
import { AxiosRetry } from '/@/utils/http/axios/axiosRetry';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import { validateScript } from '/@/utils/event/design';
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix;
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
|
||||
/**
|
||||
* @description: 数据处理,方便区分多种处理方式
|
||||
*/
|
||||
const transform: AxiosTransform = {
|
||||
/**
|
||||
* @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误
|
||||
*/
|
||||
transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||
const { t } = useI18n();
|
||||
const { isTransformResponse, isReturnNativeResponse } = options;
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
if (isReturnNativeResponse) {
|
||||
return res;
|
||||
}
|
||||
// 不进行任何处理,直接返回
|
||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||
if (!isTransformResponse) {
|
||||
return res.data;
|
||||
}
|
||||
// 错误的时候返回
|
||||
|
||||
const { data: result } = res;
|
||||
if (!result) {
|
||||
// return '[HTTP] Request has no return value';
|
||||
throw new Error(t('请求出错,请稍候重试'));
|
||||
}
|
||||
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
|
||||
const { code, data, msg } = result;
|
||||
|
||||
// 这里逻辑可以根据项目进行修改
|
||||
const hasSuccess = code === ResultEnum.SUCCESS;
|
||||
if (hasSuccess) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
|
||||
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
||||
let timeoutMsg = '';
|
||||
switch (code) {
|
||||
case ResultEnum.UNAUTHRAZATION:
|
||||
timeoutMsg = t('登录超时,请重新登录!');
|
||||
const userStore = useUserStoreWithOut();
|
||||
userStore.setToken(undefined);
|
||||
userStore.logout(true);
|
||||
const go = useGo();
|
||||
go('/login');
|
||||
break;
|
||||
default:
|
||||
if (msg) {
|
||||
timeoutMsg = msg;
|
||||
}
|
||||
}
|
||||
|
||||
// errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
||||
if (options.errorMessageMode === 'modal') {
|
||||
createErrorModal({ title: t('错误提示'), content: timeoutMsg });
|
||||
} else if (options.errorMessageMode === 'message') {
|
||||
createMessage.error(timeoutMsg);
|
||||
}
|
||||
|
||||
throw new Error(timeoutMsg || t('请求出错,请稍候重试'));
|
||||
},
|
||||
|
||||
// 请求之前处理config
|
||||
beforeRequestHook: (config, options) => {
|
||||
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
|
||||
|
||||
if (joinPrefix) {
|
||||
config.url = `${urlPrefix}${config.url}`;
|
||||
}
|
||||
|
||||
if (apiUrl && isString(apiUrl)) {
|
||||
config.url = `${apiUrl}${config.url}`;
|
||||
}
|
||||
const params = config.params || {};
|
||||
const data = config.data || false;
|
||||
formatDate && data && !isString(data) && formatRequestDate(data);
|
||||
if (config.method?.toUpperCase() === RequestEnum.GET) {
|
||||
if (!isString(params)) {
|
||||
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
|
||||
} else {
|
||||
validateScript(params);
|
||||
// 兼容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 {
|
||||
// 非GET请求如果没有提供data,则将params视为data
|
||||
config.data = params;
|
||||
config.params = undefined;
|
||||
}
|
||||
if (isBoolean(joinParamsToUrl) && 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 = 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: 响应拦截器处理
|
||||
*/
|
||||
responseInterceptors: (res: AxiosResponse<any>) => {
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 响应错误处理
|
||||
*/
|
||||
responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
|
||||
const { t } = useI18n();
|
||||
const errorLogStore = useErrorLogStoreWithOut();
|
||||
errorLogStore.addAjaxErrorInfo(error);
|
||||
const { response, code, message, config } = error || {};
|
||||
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
|
||||
const msg: string = response?.data?.error?.message ?? '';
|
||||
const err: string = error?.toString?.() ?? '';
|
||||
let errMessage = '';
|
||||
|
||||
try {
|
||||
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
|
||||
errMessage = t('接口请求超时,请刷新页面重试!');
|
||||
}
|
||||
if (err?.includes('Network Error')) {
|
||||
errMessage = t('网络异常,请检查您的网络连接是否正常!');
|
||||
}
|
||||
|
||||
if (errMessage) {
|
||||
if (errorMessageMode === 'modal') {
|
||||
createErrorModal({ title: t('错误提示'), content: errMessage });
|
||||
} else if (errorMessageMode === 'message') {
|
||||
createMessage.error(errMessage);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error as unknown as string);
|
||||
}
|
||||
|
||||
checkStatus(error?.response?.status, msg, errorMessageMode);
|
||||
|
||||
// 添加自动重试机制 保险起见 只针对GET请求
|
||||
const retryRequest = new AxiosRetry();
|
||||
const { isOpenRetry } = config.requestOptions?.retryRequest;
|
||||
config.method?.toUpperCase() === RequestEnum.GET &&
|
||||
isOpenRetry &&
|
||||
// @ts-ignore
|
||||
retryRequest.retry(axiosInstance, error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
};
|
||||
|
||||
function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||
return new VAxios(
|
||||
deepMerge(
|
||||
{
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||
// authentication schemes,e.g: Bearer
|
||||
// authenticationScheme: 'Bearer',
|
||||
authenticationScheme: 'Bearer',
|
||||
timeout: 10 * 1000,
|
||||
// 基础接口地址
|
||||
// baseURL: globSetting.apiUrl,
|
||||
|
||||
headers: { 'Content-Type': ContentTypeEnum.JSON },
|
||||
// 如果是form-data格式
|
||||
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
||||
// 数据处理方式
|
||||
transform: clone(transform),
|
||||
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||
requestOptions: {
|
||||
// 默认将prefix 添加到url
|
||||
joinPrefix: true,
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
isReturnNativeResponse: false,
|
||||
// 需要对返回数据进行处理
|
||||
isTransformResponse: true,
|
||||
// post请求的时候添加参数到url
|
||||
joinParamsToUrl: false,
|
||||
// 格式化提交参数时间
|
||||
formatDate: true,
|
||||
// 消息提示类型
|
||||
errorMessageMode: 'message',
|
||||
// 接口地址
|
||||
apiUrl: globSetting.apiUrl,
|
||||
// 接口拼接地址
|
||||
urlPrefix: urlPrefix,
|
||||
// 是否加入时间戳
|
||||
joinTime: true,
|
||||
// 忽略重复请求
|
||||
ignoreCancelToken: true,
|
||||
// 是否携带token
|
||||
withToken: true,
|
||||
retryRequest: {
|
||||
isOpenRetry: false,
|
||||
count: 5,
|
||||
waitTime: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
opt || {},
|
||||
),
|
||||
);
|
||||
}
|
||||
export const defHttp = createAxios();
|
||||
|
||||
// other api url
|
||||
// export const otherHttp = createAxios({
|
||||
// requestOptions: {
|
||||
// apiUrl: 'xxx',
|
||||
// urlPrefix: 'xxx',
|
||||
// },
|
||||
// });
|
||||
105
src/utils/index.ts
Normal file
105
src/utils/index.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
|
||||
import type { App, Plugin } from 'vue';
|
||||
|
||||
import { unref } from 'vue';
|
||||
import { isObject } from '/@/utils/is';
|
||||
|
||||
export const noop = () => {};
|
||||
|
||||
/**
|
||||
* @description: Set ui mount node
|
||||
*/
|
||||
export function getPopupContainer(node?: HTMLElement): HTMLElement {
|
||||
return (node?.parentNode as HTMLElement) ?? document.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the object as a parameter to the URL
|
||||
* @param baseUrl url
|
||||
* @param obj
|
||||
* @returns {string}
|
||||
* eg:
|
||||
* let obj = {a: '3', b: '4'}
|
||||
* setObjToUrlParams('www.baidu.com', obj)
|
||||
* ==>www.baidu.com?a=3&b=4
|
||||
*/
|
||||
export function setObjToUrlParams(baseUrl: string, obj: any): string {
|
||||
let parameters = '';
|
||||
for (const key in obj) {
|
||||
parameters += key + '=' + encodeURIComponent(obj[key]) + '&';
|
||||
}
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
window.open(url, target, feature.join(','));
|
||||
}
|
||||
|
||||
// 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 getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized {
|
||||
if (!route) return route;
|
||||
const { matched, ...opt } = route;
|
||||
return {
|
||||
...opt,
|
||||
matched: (matched
|
||||
? matched.map((item) => ({
|
||||
meta: item.meta,
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
}))
|
||||
: undefined) as RouteRecordNormalized[],
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
// 验证是否外部地址
|
||||
export function isExternal(path: string) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path);
|
||||
}
|
||||
|
||||
// 复制文本
|
||||
export function copy(text: string) {
|
||||
const input = document.createElement('textarea');
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
131
src/utils/is.ts
Normal file
131
src/utils/is.ts
Normal file
@ -0,0 +1,131 @@
|
||||
const toString = Object.prototype.toString;
|
||||
|
||||
export function is(val: unknown, type: string) {
|
||||
return toString.call(val) === `[object ${type}]`;
|
||||
}
|
||||
|
||||
export function isDef<T = unknown>(val?: T): val is T {
|
||||
return typeof val !== 'undefined';
|
||||
}
|
||||
|
||||
export function isUnDef<T = unknown>(val?: T): val is T {
|
||||
return !isDef(val);
|
||||
}
|
||||
|
||||
export function isObject(val: any): val is Record<any, any> {
|
||||
return val !== null && is(val, 'Object');
|
||||
}
|
||||
|
||||
export function isEmpty<T = unknown>(val: T): val is T {
|
||||
if (isArray(val) || isString(val)) {
|
||||
return val.length === 0;
|
||||
}
|
||||
|
||||
if (val instanceof Map || val instanceof Set) {
|
||||
return val.size === 0;
|
||||
}
|
||||
|
||||
if (isObject(val)) {
|
||||
return Object.keys(val).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isDate(val: unknown): val is Date {
|
||||
return is(val, 'Date');
|
||||
}
|
||||
|
||||
export function isNull(val: unknown): val is null {
|
||||
return val === null;
|
||||
}
|
||||
|
||||
export function isNullAndUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) && isNull(val);
|
||||
}
|
||||
|
||||
export function isNullOrUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) || isNull(val);
|
||||
}
|
||||
|
||||
export function isNumber(val: unknown): val is number {
|
||||
return is(val, 'Number');
|
||||
}
|
||||
|
||||
export function stringIsNumber(val) {
|
||||
const regPos = /^\d+(\.\d+)?$/; //非负浮点数
|
||||
const regNeg =
|
||||
/^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; //负浮点数
|
||||
if (regPos.test(val) || regNeg.test(val)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPromise<T = any>(val: unknown): val is Promise<T> {
|
||||
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch);
|
||||
}
|
||||
|
||||
export function isString(val: unknown): val is string {
|
||||
return is(val, 'String');
|
||||
}
|
||||
|
||||
export function isFunction(val: unknown): val is Function {
|
||||
return typeof val === 'function';
|
||||
}
|
||||
|
||||
export function isBoolean(val: unknown): val is boolean {
|
||||
return is(val, 'Boolean');
|
||||
}
|
||||
|
||||
export function isRegExp(val: unknown): val is RegExp {
|
||||
return is(val, 'RegExp');
|
||||
}
|
||||
|
||||
export function isArray(val: any): val is Array<any> {
|
||||
return val && Array.isArray(val);
|
||||
}
|
||||
|
||||
export function isWindow(val: any): val is Window {
|
||||
return typeof window !== 'undefined' && is(val, 'Window');
|
||||
}
|
||||
|
||||
export function isElement(val: unknown): val is Element {
|
||||
return isObject(val) && !!val.tagName;
|
||||
}
|
||||
|
||||
export function isMap(val: unknown): val is Map<any, any> {
|
||||
return is(val, 'Map');
|
||||
}
|
||||
|
||||
export const isServer = typeof window === 'undefined';
|
||||
|
||||
export const isClient = !isServer;
|
||||
|
||||
export function isUrl(path: string): boolean {
|
||||
if (!path.includes('//')) return false;
|
||||
|
||||
const urlRegex = /^(ftp|http|https):\/\/[^ "]+$/;
|
||||
|
||||
return urlRegex.test(path);
|
||||
|
||||
// const sRegex =
|
||||
// '^((https|http|ftp|rtsp|mms)?://)' +
|
||||
// "?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?" + //ftp的user@
|
||||
// '(([0-9]{1,3}.){3}[0-9]{1,3}' + // IP形式的URL- 199.194.52.184
|
||||
// '|' + // 允许IP和DOMAIN(域名)
|
||||
// "([0-9a-z_!~*'()-]+.)*" + // 域名- www.
|
||||
// '([0-9a-z][0-9a-z-]{0,61})?[0-9a-z].' + // 二级域名
|
||||
// '[a-z]{2,6})' + // first level domain- .com or .museum
|
||||
// '(:[0-9]{1,4})?' + // 端口- :80
|
||||
// '((/?)|' + // a slash isn't required if there is no file name
|
||||
// "(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)$";
|
||||
// const re = new RegExp(sRegex);
|
||||
// return re.test(path);
|
||||
// const reg =
|
||||
// /^(?:(http|https|ftp):\/\/)?((?:[\w-]+\.)+[a-z0-9]+)((?:\/[^/?#]*)+)?(\?[^#]+)?(#.+)?$/i;
|
||||
// // const reg =
|
||||
// // /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
|
||||
// return reg.test(path);
|
||||
}
|
||||
57
src/utils/lib/echarts.ts
Normal file
57
src/utils/lib/echarts.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as echarts from 'echarts/core';
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
RadarChart,
|
||||
ScatterChart,
|
||||
} from 'echarts/charts';
|
||||
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
LegendComponent,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
GraphicComponent,
|
||||
} 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,
|
||||
GraphicComponent,
|
||||
ScatterChart,
|
||||
]);
|
||||
|
||||
export default echarts;
|
||||
9
src/utils/log.ts
Normal file
9
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}`);
|
||||
}
|
||||
101
src/utils/mitt.ts
Normal file
101
src/utils/mitt.ts
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* copy to https://github.com/developit/mitt
|
||||
* Expand clear method
|
||||
*/
|
||||
|
||||
export type EventType = string | symbol;
|
||||
|
||||
// An event handler can take an optional event argument
|
||||
// and should not return a value
|
||||
export type Handler<T = any> = (event?: T) => void;
|
||||
export type WildcardHandler = (type: EventType, event?: any) => void;
|
||||
|
||||
// An array of all currently registered event handlers for a type
|
||||
export type EventHandlerList = Array<Handler>;
|
||||
export type WildCardEventHandlerList = Array<WildcardHandler>;
|
||||
|
||||
// A map of event types and their corresponding event handlers.
|
||||
export type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;
|
||||
|
||||
export interface Emitter {
|
||||
all: EventHandlerMap;
|
||||
|
||||
on<T = any>(type: EventType, handler: Handler<T>): void;
|
||||
on(type: '*', handler: WildcardHandler): void;
|
||||
|
||||
off<T = any>(type: EventType, handler: Handler<T>): void;
|
||||
off(type: '*', handler: WildcardHandler): void;
|
||||
|
||||
emit<T = any>(type: EventType, event?: T): void;
|
||||
emit(type: '*', event?: any): void;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mitt: Tiny (~200b) functional event emitter / pubsub.
|
||||
* @name mitt
|
||||
* @returns {Mitt}
|
||||
*/
|
||||
export default function mitt(all?: EventHandlerMap): Emitter {
|
||||
all = all || new Map();
|
||||
|
||||
return {
|
||||
/**
|
||||
* A Map of event names to registered handler functions.
|
||||
*/
|
||||
all,
|
||||
|
||||
/**
|
||||
* Register an event handler for the given type.
|
||||
* @param {string|symbol} type Type of event to listen for, or `"*"` for all events
|
||||
* @param {Function} handler Function to call in response to given event
|
||||
* @memberOf mitt
|
||||
*/
|
||||
on<T = any>(type: EventType, handler: Handler<T>) {
|
||||
const handlers = all?.get(type);
|
||||
const added = handlers && handlers.push(handler);
|
||||
if (!added) {
|
||||
all?.set(type, [handler]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an event handler for the given type.
|
||||
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"`
|
||||
* @param {Function} handler Handler function to remove
|
||||
* @memberOf mitt
|
||||
*/
|
||||
off<T = any>(type: EventType, handler: Handler<T>) {
|
||||
const handlers = all?.get(type);
|
||||
if (handlers) {
|
||||
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke all handlers for the given type.
|
||||
* If present, `"*"` handlers are invoked after type-matched handlers.
|
||||
*
|
||||
* Note: Manually firing "*" handlers is not supported.
|
||||
*
|
||||
* @param {string|symbol} type The event type to invoke
|
||||
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
|
||||
* @memberOf mitt
|
||||
*/
|
||||
emit<T = any>(type: EventType, evt: T) {
|
||||
((all?.get(type) || []) as EventHandlerList).slice().map((handler) => {
|
||||
handler(evt);
|
||||
});
|
||||
((all?.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => {
|
||||
handler(type, evt);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all
|
||||
*/
|
||||
clear() {
|
||||
this.all.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
34
src/utils/propTypes.ts
Normal file
34
src/utils/propTypes.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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>;
|
||||
// readonly trueBool: VueTypeValidableDef<boolean>;
|
||||
};
|
||||
|
||||
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 };
|
||||
185
src/utils/props.ts
Normal file
185
src/utils/props.ts
Normal file
@ -0,0 +1,185 @@
|
||||
// copy from element-plus
|
||||
|
||||
import { warn } from 'vue';
|
||||
import { isObject } from '/@/utils/is';
|
||||
import { fromPairs } from 'lodash-es';
|
||||
import type { ExtractPropTypes } from 'vue';
|
||||
import type { Mutable } from './types';
|
||||
|
||||
const wrapperKey = Symbol();
|
||||
export type PropWrapper<T> = { [wrapperKey]: T };
|
||||
|
||||
export const propKey = Symbol();
|
||||
|
||||
type ResolveProp<T> = ExtractPropTypes<{
|
||||
key: { type: T; required: true };
|
||||
}>['key'];
|
||||
type ResolvePropType<T> = ResolveProp<T> extends { type: infer V } ? V : ResolveProp<T>;
|
||||
type ResolvePropTypeWithReadonly<T> = Readonly<T> extends Readonly<Array<infer A>>
|
||||
? ResolvePropType<A[]>
|
||||
: ResolvePropType<T>;
|
||||
|
||||
type IfUnknown<T, V> = [unknown] extends [T] ? V : T;
|
||||
|
||||
export type BuildPropOption<T, D extends BuildPropType<T, V, C>, R, V, C> = {
|
||||
type?: T;
|
||||
values?: readonly V[];
|
||||
required?: R;
|
||||
default?: R extends true
|
||||
? never
|
||||
: D extends Record<string, unknown> | Array<any>
|
||||
? () => D
|
||||
: (() => D) | D;
|
||||
validator?: ((val: any) => val is C) | ((val: any) => boolean);
|
||||
};
|
||||
|
||||
type _BuildPropType<T, V, C> =
|
||||
| (T extends PropWrapper<unknown>
|
||||
? T[typeof wrapperKey]
|
||||
: [V] extends [never]
|
||||
? ResolvePropTypeWithReadonly<T>
|
||||
: never)
|
||||
| V
|
||||
| C;
|
||||
export type BuildPropType<T, V, C> = _BuildPropType<
|
||||
IfUnknown<T, never>,
|
||||
IfUnknown<V, never>,
|
||||
IfUnknown<C, never>
|
||||
>;
|
||||
|
||||
type _BuildPropDefault<T, D> = [T] extends [
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
Record<string, unknown> | Array<any> | Function,
|
||||
]
|
||||
? D
|
||||
: D extends () => T
|
||||
? ReturnType<D>
|
||||
: D;
|
||||
|
||||
export type BuildPropDefault<T, D, R> = R extends true
|
||||
? { readonly default?: undefined }
|
||||
: {
|
||||
readonly default: Exclude<D, undefined> extends never
|
||||
? undefined
|
||||
: Exclude<_BuildPropDefault<T, D>, undefined>;
|
||||
};
|
||||
export type BuildPropReturn<T, D, R, V, C> = {
|
||||
readonly type: PropType<BuildPropType<T, V, C>>;
|
||||
readonly required: IfUnknown<R, false>;
|
||||
readonly validator: ((val: unknown) => boolean) | undefined;
|
||||
[propKey]: true;
|
||||
} & BuildPropDefault<BuildPropType<T, V, C>, IfUnknown<D, never>, IfUnknown<R, false>>;
|
||||
|
||||
/**
|
||||
* @description Build prop. It can better optimize prop types
|
||||
* @description 生成 prop,能更好地优化类型
|
||||
* @example
|
||||
// limited options
|
||||
// the type will be PropType<'light' | 'dark'>
|
||||
buildProp({
|
||||
type: String,
|
||||
values: ['light', 'dark'],
|
||||
} as const)
|
||||
* @example
|
||||
// limited options and other types
|
||||
// the type will be PropType<'small' | 'medium' | number>
|
||||
buildProp({
|
||||
type: [String, Number],
|
||||
values: ['small', 'medium'],
|
||||
validator: (val: unknown): val is number => typeof val === 'number',
|
||||
} as const)
|
||||
@link see more: https://github.com/element-plus/element-plus/pull/3341
|
||||
*/
|
||||
export function buildProp<
|
||||
T = never,
|
||||
D extends BuildPropType<T, V, C> = never,
|
||||
R extends boolean = false,
|
||||
V = never,
|
||||
C = never,
|
||||
>(option: BuildPropOption<T, D, R, V, C>, key?: string): BuildPropReturn<T, D, R, V, C> {
|
||||
// filter native prop type and nested prop, e.g `null`, `undefined` (from `buildProps`)
|
||||
if (!isObject(option) || !!option[propKey]) return option as any;
|
||||
|
||||
const { values, required, default: defaultValue, type, validator } = option;
|
||||
|
||||
const _validator =
|
||||
values || validator
|
||||
? (val: unknown) => {
|
||||
let valid = false;
|
||||
let allowedValues: unknown[] = [];
|
||||
|
||||
if (values) {
|
||||
allowedValues = [...values, defaultValue];
|
||||
valid ||= allowedValues.includes(val);
|
||||
}
|
||||
if (validator) valid ||= validator(val);
|
||||
|
||||
if (!valid && allowedValues.length > 0) {
|
||||
const allowValuesText = [...new Set(allowedValues)]
|
||||
.map((value) => JSON.stringify(value))
|
||||
.join(', ');
|
||||
warn(
|
||||
`Invalid prop: validation failed${
|
||||
key ? ` for prop "${key}"` : ''
|
||||
}. Expected one of [${allowValuesText}], got value ${JSON.stringify(val)}.`,
|
||||
);
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type:
|
||||
typeof type === 'object' && Object.getOwnPropertySymbols(type).includes(wrapperKey)
|
||||
? type[wrapperKey]
|
||||
: type,
|
||||
required: !!required,
|
||||
default: defaultValue,
|
||||
validator: _validator,
|
||||
[propKey]: true,
|
||||
} as unknown as BuildPropReturn<T, D, R, V, C>;
|
||||
}
|
||||
|
||||
type NativePropType = [((...args: any) => any) | { new (...args: any): any } | undefined | null];
|
||||
|
||||
export const buildProps = <
|
||||
O extends {
|
||||
[K in keyof O]: O[K] extends BuildPropReturn<any, any, any, any, any>
|
||||
? O[K]
|
||||
: [O[K]] extends NativePropType
|
||||
? O[K]
|
||||
: O[K] extends BuildPropOption<infer T, infer D, infer R, infer V, infer C>
|
||||
? D extends BuildPropType<T, V, C>
|
||||
? BuildPropOption<T, D, R, V, C>
|
||||
: never
|
||||
: never;
|
||||
},
|
||||
>(
|
||||
props: O,
|
||||
) =>
|
||||
fromPairs(
|
||||
Object.entries(props).map(([key, option]) => [key, buildProp(option as any, key)]),
|
||||
) as unknown as {
|
||||
[K in keyof O]: O[K] extends { [propKey]: boolean }
|
||||
? O[K]
|
||||
: [O[K]] extends NativePropType
|
||||
? O[K]
|
||||
: O[K] extends BuildPropOption<
|
||||
infer T,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
infer _D,
|
||||
infer R,
|
||||
infer V,
|
||||
infer C
|
||||
>
|
||||
? BuildPropReturn<T, O[K]['default'], R, V, C>
|
||||
: never;
|
||||
};
|
||||
|
||||
export const definePropType = <T>(val: any) => ({ [wrapperKey]: val } as PropWrapper<T>);
|
||||
|
||||
export const keyOf = <T>(arr: T) => Object.keys(arr) as Array<keyof T>;
|
||||
export const mutable = <T extends readonly any[] | Record<string, unknown>>(val: T) =>
|
||||
val as Mutable<typeof val>;
|
||||
|
||||
export const componentSize = ['large', 'medium', 'small', 'mini'] as const;
|
||||
42
src/utils/types.ts
Normal file
42
src/utils/types.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// copy from element-plus
|
||||
|
||||
import type { CSSProperties, Plugin } from 'vue';
|
||||
|
||||
type OptionalKeys<T extends Record<string, unknown>> = {
|
||||
[K in keyof T]: T extends Record<K, T[K]> ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
type RequiredKeys<T extends Record<string, unknown>> = Exclude<keyof T, OptionalKeys<T>>;
|
||||
|
||||
type MonoArgEmitter<T, Keys extends keyof T> = <K extends Keys>(evt: K, arg?: T[K]) => void;
|
||||
|
||||
type BiArgEmitter<T, Keys extends keyof T> = <K extends Keys>(evt: K, arg: T[K]) => void;
|
||||
|
||||
export type EventEmitter<T extends Record<string, unknown>> = MonoArgEmitter<T, OptionalKeys<T>> &
|
||||
BiArgEmitter<T, RequiredKeys<T>>;
|
||||
|
||||
export type AnyFunction<T> = (...args: any[]) => T;
|
||||
|
||||
export type PartialReturnType<T extends (...args: unknown[]) => unknown> = Partial<ReturnType<T>>;
|
||||
|
||||
export type SFCWithInstall<T> = T & Plugin;
|
||||
|
||||
export type Nullable<T> = T | null;
|
||||
|
||||
export type RefElement = Nullable<HTMLElement>;
|
||||
|
||||
export type CustomizedHTMLElement<T> = HTMLElement & T;
|
||||
|
||||
export type Indexable<T> = {
|
||||
[key: string]: T;
|
||||
};
|
||||
|
||||
export type Hash<T> = Indexable<T>;
|
||||
|
||||
export type TimeoutHandle = ReturnType<typeof global.setTimeout>;
|
||||
|
||||
export type ComponentSize = 'large' | 'medium' | 'small' | 'mini';
|
||||
|
||||
export type StyleValue = string | CSSProperties | Array<StyleValue>;
|
||||
|
||||
export type Mutable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
38
src/utils/uuid.ts
Normal file
38
src/utils/uuid.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import SnowflakeId from 'snowflake-id';
|
||||
const hexList: string[] = [];
|
||||
for (let i = 0; i <= 15; i++) {
|
||||
hexList[i] = i.toString(16);
|
||||
}
|
||||
|
||||
export function buildUUID(): string {
|
||||
let uuid = '';
|
||||
for (let i = 1; i <= 36; i++) {
|
||||
if (i === 9 || i === 14 || i === 19 || i === 24) {
|
||||
uuid += '-';
|
||||
} else if (i === 15) {
|
||||
uuid += 4;
|
||||
} else if (i === 20) {
|
||||
uuid += hexList[(Math.random() * 4) | 8];
|
||||
} else {
|
||||
uuid += hexList[(Math.random() * 16) | 0];
|
||||
}
|
||||
}
|
||||
return uuid.replace(/-/g, '');
|
||||
}
|
||||
|
||||
let unique = 0;
|
||||
export function buildShortUUID(prefix = ''): string {
|
||||
const time = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000000000);
|
||||
unique++;
|
||||
return prefix + '_' + random + unique + String(time);
|
||||
}
|
||||
|
||||
export function buildSnowFlakeId(num = 0) {
|
||||
const snowflake = new SnowflakeId();
|
||||
const arr: any[] = [];
|
||||
for (let i = 0; i < num; i++) {
|
||||
arr.push(snowflake.generate());
|
||||
}
|
||||
return num ? arr : snowflake.generate();
|
||||
}
|
||||
Reference in New Issue
Block a user