1
0
mirror of https://github.com/fumiama/paper-manager.git synced 2026-06-09 18:30:23 +08:00

add frontend/vben from vben-admin-thin

This commit is contained in:
源文雨
2023-03-10 17:18:32 +08:00
parent 30cd57ef76
commit 2a0fdeae31
469 changed files with 42028 additions and 0 deletions

View 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)
}

View 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
frontend/vben/src/utils/cache/index.ts vendored Normal file
View 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
frontend/vben/src/utils/cache/memory.ts vendored Normal file
View 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 = {}
}
}

View File

@@ -0,0 +1,125 @@
import type { 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,
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[]
[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不同步的问题
ss.set(APP_SESSION_CACHE_KEY, {
...omit(sessionMemory.getCache),
...pick(ss.get(APP_SESSION_CACHE_KEY), [TOKEN_KEY, USER_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()

View File

@@ -0,0 +1,109 @@
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()
}

View 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()
}

View 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)}`
}

View 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 | undefined = undefined,
format = DATE_TIME_FORMAT,
): string {
return dayjs(date).format(format)
}
export function formatToDate(
date: dayjs.Dayjs | undefined = undefined,
format = DATE_FORMAT,
): string {
return dayjs(date).format(format)
}
export const dateUtil = dayjs

View File

@@ -0,0 +1,180 @@
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,
}
}
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
})
}
}

View File

@@ -0,0 +1,83 @@
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,
} = 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,
}
}
/**
* @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
}

View 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)
}

View 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()
}
},
})
}

View 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
})
}

View File

@@ -0,0 +1,96 @@
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 isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1
const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1
if (/(iP)/g.test(window.navigator.userAgent)) {
console.error('Your browser does not support download!')
return false
}
if (isChrome || isSafari) {
const link = document.createElement('a')
link.href = url
link.target = target
if (link.download !== undefined) {
link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length)
}
if (document.createEvent) {
const e = document.createEvent('MouseEvents')
e.initEvent('click', true, true)
link.dispatchEvent(e)
return true
}
}
if (url.indexOf('?') === -1) {
url += '?download'
}
openWindow(url, { target })
return true
}

View File

@@ -0,0 +1,216 @@
interface TreeHelperConfig {
id: string
children: string
pid: string
}
// 默认配置
const DEFAULT_CONFIG: TreeHelperConfig = {
id: 'id',
children: 'children',
pid: 'pid',
}
// 获取配置。 Object.assign 从一个或多个源对象复制到目标对象
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,
// Partial 将 T 中的所有属性设为可选
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) => {
// 递归调用 对含有children项 进行再次调用自身函数 listFilter
node[children] = node[children] && listFilter(node[children])
// 执行传入的回调 func 进行过滤
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
* @description: 提取树指定结构
*/
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
* @description: 提取树指定结构
*/
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)
}
})
}

View 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] = (data?: any) => getSlot(slots, key, data)
})
return ret
}

View File

@@ -0,0 +1,237 @@
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'
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) {
formData.append(customFilename, params.file, params.filename)
} else {
formData.append(customFilename, params.file)
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key]
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item)
})
return
}
formData.append(key, params.data![key])
})
}
return this.axiosInstance.request<T>({
...config,
method: 'POST',
data: 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)
}
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, transformResponseHook } = 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 (transformResponseHook && isFunction(transformResponseHook)) {
try {
const ret = transformResponseHook(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 (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt))
return
}
if (axios.isAxiosError(e)) {
// rewrite error message from axios in here
}
reject(e)
})
})
}
}

View 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>()
}
}

View 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))
}
}

View 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: 处理响应数据
*/
transformResponseHook?: (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
}

View 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('sys.api.errMsg401')
if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
userStore.setSessionTimeout(true)
} else {
userStore.logout(true)
}
break
case 403:
errMessage = t('sys.api.errMsg403')
break
// 404请求不存在
case 404:
errMessage = t('sys.api.errMsg404')
break
case 405:
errMessage = t('sys.api.errMsg405')
break
case 408:
errMessage = t('sys.api.errMsg408')
break
case 500:
errMessage = t('sys.api.errMsg500')
break
case 501:
errMessage = t('sys.api.errMsg501')
break
case 502:
errMessage = t('sys.api.errMsg502')
break
case 503:
errMessage = t('sys.api.errMsg503')
break
case 504:
errMessage = t('sys.api.errMsg504')
break
case 505:
errMessage = t('sys.api.errMsg505')
break
default:
}
if (errMessage) {
if (errorMessageMode === 'modal') {
createErrorModal({ title: t('sys.api.errorTip'), content: errMessage })
} else if (errorMessageMode === 'message') {
error({ content: errMessage, key: `global_error_message_status_${status}` })
}
}
}

View 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])
}
}
}

View File

@@ -0,0 +1,267 @@
// 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 } 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 { useI18n } from '/@/hooks/web/useI18n'
import { joinTimestamp, formatRequestDate } from './helper'
import { useUserStoreWithOut } from '/@/store/modules/user'
import { AxiosRetry } from '/@/utils/http/axios/axiosRetry'
const globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix
const { createMessage, createErrorModal } = useMessage()
/**
* @description: 数据处理,方便区分多种处理方式
*/
const transform: AxiosTransform = {
/**
* @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
*/
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { t } = useI18n()
const { isTransformResponse, isReturnNativeResponse } = options
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
return res.data
}
// 错误的时候返回
const { data } = res
if (!data) {
// return '[HTTP] Request has no return value';
throw new Error(t('sys.api.apiRequestFailed'))
}
// 这里 coderesultmessage为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code, result, message } = data
// 这里逻辑可以根据项目进行修改
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
if (hasSuccess) {
return result
}
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
// 如果不希望中断当前请求请return数据否则直接抛出异常即可
let timeoutMsg = ''
switch (code) {
case ResultEnum.TIMEOUT:
timeoutMsg = t('sys.api.timeoutMessage')
const userStore = useUserStoreWithOut()
userStore.setToken(undefined)
userStore.logout(true)
break
default:
if (message) {
timeoutMsg = message
}
}
// errorMessageMode=modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
if (options.errorMessageMode === 'modal') {
createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg })
} else if (options.errorMessageMode === 'message') {
createMessage.error(timeoutMsg)
}
throw new Error(timeoutMsg || t('sys.api.apiRequestFailed'))
},
// 请求之前处理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 {
// 兼容restful风格
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`
config.params = undefined
}
} else {
if (!isString(params)) {
formatDate && formatRequestDate(params)
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 || config.data instanceof FormData)
) {
config.data = data
config.params = params
} else {
// 非GET请求如果没有提供data则将params视为data
config.data = params
config.params = undefined
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data),
)
}
} else {
// 兼容restful风格
config.url = config.url + params
config.params = undefined
}
}
return config
},
/**
* @description: 请求拦截器处理
*/
requestInterceptors: (config, options) => {
// 请求之前处理config
const token = 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 { 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('sys.api.apiTimeoutMessage')
}
if (err?.includes('Network Error')) {
errMessage = t('sys.api.networkExceptionMsg')
}
if (errMessage) {
if (errorMessageMode === 'modal') {
createErrorModal({ title: t('sys.api.errorTip'), 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 schemese.g: Bearer
// authenticationScheme: 'Bearer',
authenticationScheme: '',
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: true,
count: 5,
waitTime: 100,
},
},
},
opt || {},
),
)
}
export const defHttp = createAxios()
// other api url
// export const otherHttp = createAxios({
// requestOptions: {
// apiUrl: 'xxx',
// urlPrefix: 'xxx',
// },
// });

View File

@@ -0,0 +1,92 @@
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
}

View File

@@ -0,0 +1,99 @@
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 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 {
const reg =
/^(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?(\/#\/)?(?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}

View 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

View File

@@ -0,0 +1,9 @@
const projectName = import.meta.env.VITE_GLOB_APP_TITLE
export function warn(message: string) {
console.warn(`[${projectName} warn]:${message}`)
}
export function error(message: string) {
throw new Error(`[${projectName} error]:${message}`)
}

View 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()
},
}
}

View 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 }

View File

@@ -0,0 +1,185 @@
// copy from element-plus
import { warn } from 'vue'
import { isObject } from '@vue/shared'
import { fromPairs } from 'lodash-es'
import type { ExtractPropTypes, PropType } from '@vue/runtime-core'
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

View 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] }

View File

@@ -0,0 +1,28 @@
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)
}