Initial commit

This commit is contained in:
2026-04-23 16:58:11 +08:00
commit 267eba1eca
2582 changed files with 273338 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
/**
* 判断是否 十六进制颜色值.
* 输入形式可为 #fff000 #f00
*
* @param String color 十六进制颜色值
* @return Boolean
*/
export const 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 const 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 const hexToRGB = (hex: string, opacity?: number) => {
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 opacity
? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')'
: 'RGB(' + sColorChange.join(',') + ')'
}
return sHex
}
export const 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 const 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 const 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
*/
const 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
*/
const 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
*/
const 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 const 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
*/
const 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)}`
}
/**
* Mixes two colors.
*
* @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`.
* @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`.
* @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1.
* @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`.
*/
export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
let color = '#'
for (let i = 0; i <= 2; i++) {
const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16)
const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16)
const c = Math.round(c1 * weight + c2 * (1 - weight))
color += c.toString(16).padStart(2, '0')
}
return color
}
+17
View File
@@ -0,0 +1,17 @@
/**
* 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.ConfigType, format = DATE_TIME_FORMAT): string {
return dayjs(date).format(format)
}
export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
return dayjs(date).format(format)
}
export const dateUtil = dayjs
+289
View File
@@ -0,0 +1,289 @@
import { isServer } from './is'
const ieVersion = isServer ? 0 : Number((document as any).documentMode)
const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
const MOZ_HACK_REGEXP = /^moz([A-Z])/
export interface ViewportOffsetResult {
left: number
top: number
right: number
bottom: number
rightIncludeBody: number
bottomIncludeBody: number
}
/* istanbul ignore next */
const trim = function (string: string) {
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
}
/* istanbul ignore next */
const camelCase = function (name: string) {
return name
.replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) {
return offset ? letter.toUpperCase() : letter
})
.replace(MOZ_HACK_REGEXP, 'Moz$1')
}
/* 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)
}
}
export function getBoundingClientRect(element: Element): DOMRect | number {
if (!element || !element.getBoundingClientRect) {
return 0
}
return element.getBoundingClientRect()
}
/**
* 获取当前元素的left、top偏移
* left:元素最左侧距离文档左侧的距离
* top:元素最顶端距离文档顶端的距离
* right:元素最右侧距离文档右侧的距离
* bottom:元素最底端距离文档底端的距离
* rightIncludeBody:元素最左侧距离文档右侧的距离
* bottomIncludeBody:元素最底端距离文档最底部的距离
*
* @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
}
}
/* istanbul ignore next */
export const on = function (
element: HTMLElement | Document | Window,
event: string,
handler: EventListenerOrEventListenerObject
): void {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export const off = function (
element: HTMLElement | Document | Window,
event: string,
handler: any
): void {
if (element && event && handler) {
element.removeEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export const once = function (el: HTMLElement, event: string, fn: EventListener): void {
const listener = function (this: any, ...args: unknown[]) {
if (fn) {
// @ts-ignore
fn.apply(this, args)
}
off(el, event, listener)
}
on(el, event, listener)
}
/* istanbul ignore next */
export const getStyle =
ieVersion < 9
? function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'styleFloat'
}
try {
switch (styleName) {
case 'opacity':
try {
return element.filters.item('alpha').opacity / 100
} catch (e) {
return 1.0
}
default:
return element.style[styleName] || element.currentStyle
? element.currentStyle[styleName]
: null
}
} catch (e) {
return element.style[styleName]
}
}
: function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
}
try {
const computed = (document as any).defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
/* istanbul ignore next */
export function setStyle(element: Element | any, styleName: any, value: any) {
if (!element || !styleName) return
if (typeof styleName === 'object') {
for (const prop in styleName) {
if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
setStyle(element, prop, styleName[prop])
}
}
} else {
styleName = camelCase(styleName)
if (styleName === 'opacity' && ieVersion < 9) {
element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
} else {
element.style[styleName] = value
}
}
}
/* istanbul ignore next */
export const isScroll = (el: Element, vertical: any) => {
if (isServer) return
const determinedDirection = vertical !== null || vertical !== undefined
const overflow = determinedDirection
? vertical
? getStyle(el, 'overflow-y')
: getStyle(el, 'overflow-x')
: getStyle(el, 'overflow')
return overflow.match(/(scroll|auto)/)
}
/* istanbul ignore next */
export const getScrollContainer = (el: Element, vertical?: any) => {
if (isServer) return
let parent: any = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) {
return window
}
if (isScroll(parent, vertical)) {
return parent
}
parent = parent.parentNode
}
return parent
}
/* istanbul ignore next */
export const isInContainer = (el: Element, container: any) => {
if (isServer || !el || !container) return false
const elRect = el.getBoundingClientRect()
let containerRect
if ([window, document, document.documentElement, null, undefined].includes(container)) {
containerRect = {
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
left: 0
}
} else {
containerRect = container.getBoundingClientRect()
}
return (
elRect.top < containerRect.bottom &&
elRect.bottom > containerRect.top &&
elRect.right > containerRect.left &&
elRect.left < containerRect.right
)
}
+136
View File
@@ -0,0 +1,136 @@
/**
*
* @param component 需要注册的组件
* @param alias 组件别名
* @returns any
*/
export const withInstall = <T>(component: T, alias?: string) => {
const comp = component as any
comp.install = (app: any) => {
app.component(comp.name || comp.displayName, component)
if (alias) {
app.config.globalProperties[alias] = component
}
}
return component as T & Plugin
}
/**
* @param str 需要转下划线的驼峰字符串
* @returns 字符串下划线
*/
export const humpToUnderline = (str: string): string => {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
/**
* @param str 需要转驼峰的下划线字符串
* @returns 字符串驼峰
*/
export const underlineToHump = (str: string): string => {
if (!str) return ''
return str.replace(/\-(\w)/g, (_, letter: string) => {
return letter.toUpperCase()
})
}
/**
* 驼峰转横杠
*/
export const humpToDash = (str: string): string => {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val)
}
export const getCssVar = (prop: string, dom = document.documentElement) => {
return getComputedStyle(dom).getPropertyValue(prop)
}
/**
* 查找数组对象的某个下标
* @param {Array} ary 查找的数组
* @param {Functon} fn 判断的方法
*/
export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {
if (ary.findIndex) {
return ary.findIndex(fn)
}
let index = -1
ary.some((item: T, i: number, ary: Array<T>) => {
const ret: T = fn(item, i, ary)
if (ret) {
index = i
return ret
}
})
return index
}
export const trim = (str: string) => {
return str.replace(/(^\s*)|(\s*$)/g, '')
}
/**
* @param {Date | number | string} time 需要转换的时间
* @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
*/
export function formatTime(time: Date | number | string, fmt: string) {
if (!time) return ''
else {
const date = new Date(time)
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
S: date.getMilliseconds()
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
)
}
}
return fmt
}
}
/**
* 生成随机字符串
*/
export function toAnyString() {
const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
const r: number = (Math.random() * 16) | 0
const v: number = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString()
})
return str
}
/**
* 首字母大写
*/
export function firstUpperCase(str: string) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
}
/**
* 把对象转为formData
*/
export function objToFormData(obj: Recordable) {
const formData = new FormData()
Object.keys(obj).forEach((key) => {
formData.append(key, obj[key])
})
return formData
}
+117
View File
@@ -0,0 +1,117 @@
// copy to vben-admin
const toString = Object.prototype.toString
export const is = (val: unknown, type: string) => {
return toString.call(val) === `[object ${type}]`
}
export const isDef = <T = unknown>(val?: T): val is T => {
return typeof val !== 'undefined'
}
export const isUnDef = <T = unknown>(val?: T): val is T => {
return !isDef(val)
}
export const isObject = (val: any): val is Record<any, any> => {
return val !== null && is(val, 'Object')
}
export const 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 const isDate = (val: unknown): val is Date => {
return is(val, 'Date')
}
export const isNull = (val: unknown): val is null => {
return val === null
}
export const isNullAndUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) && isNull(val)
}
export const isNullOrUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) || isNull(val)
}
export const isNumber = (val: unknown): val is number => {
return is(val, 'Number')
}
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
export const isString = (val: unknown): val is string => {
return is(val, 'String')
}
export const isFunction = (val: unknown): val is Function => {
return typeof val === 'function'
}
export const isBoolean = (val: unknown): val is boolean => {
return is(val, 'Boolean')
}
export const isRegExp = (val: unknown): val is RegExp => {
return is(val, 'RegExp')
}
export const isArray = (val: any): val is Array<any> => {
return val && Array.isArray(val)
}
export const isWindow = (val: any): val is Window => {
return typeof window !== 'undefined' && is(val, 'Window')
}
export const isElement = (val: unknown): val is Element => {
return isObject(val) && !!val.tagName
}
export const isMap = (val: unknown): val is Map<any, any> => {
return is(val, 'Map')
}
export const isServer = typeof window === 'undefined'
export const isClient = !isServer
export const isUrl = (path: string): boolean => {
try {
new URL(path)
return true
} catch (_error) {
return false
}
}
export const isDark = (): boolean => {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
// 是否是图片链接
export const isImgPath = (path: string): boolean => {
return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
}
export const isEmptyVal = (val: any): boolean => {
return val === '' || val === null || val === undefined
}
+24
View File
@@ -0,0 +1,24 @@
import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types'
import { CSSProperties } from 'vue'
type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties>
}
const newPropTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
number: undefined,
object: undefined,
integer: undefined
}) as PropTypes
class propTypes extends newPropTypes {
static get style() {
return toValidableType('style', {
type: [String, Object]
})
}
}
export { propTypes }
+197
View File
@@ -0,0 +1,197 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import type {
Router,
RouteLocationNormalized,
RouteRecordNormalized,
RouteRecordRaw
} from 'vue-router'
import { isUrl } from '@/utils/is'
import { omit, cloneDeep } from 'lodash-es'
const modules = import.meta.glob('../views/**/*.{vue,tsx}')
/* Layout */
export const Layout = () => import('@/layout/Layout.vue')
export const getParentLayout = () => {
return () =>
new Promise((resolve) => {
resolve({
name: 'ParentLayout'
})
})
}
export const 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 generateRoutesByFrontEnd = (
routes: AppRouteRecordRaw[],
keys: string[],
basePath = '/'
): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
for (const route of routes) {
const meta = route.meta ?? {}
// skip some route
if (meta.hidden && !meta.canTo) {
continue
}
let data: Nullable<AppRouteRecordRaw> = null
let onlyOneChild: Nullable<string> = null
if (route.children && route.children.length === 1 && !meta.alwaysShow) {
onlyOneChild = (
isUrl(route.children[0].path)
? route.children[0].path
: pathResolve(pathResolve(basePath, route.path), route.children[0].path)
) as string
}
// 开发者可以根据实际情况进行扩展
for (const item of keys) {
// 通过路径去匹配
if (isUrl(item) && (onlyOneChild === item || route.path === item)) {
data = Object.assign({}, route)
} else {
const routePath = (onlyOneChild ?? pathResolve(basePath, route.path)).trim()
if (routePath === item || meta.followRoute === item) {
data = Object.assign({}, route)
}
}
}
// recursive child routes
if (route.children && data) {
data.children = generateRoutesByFrontEnd(
route.children,
keys,
pathResolve(basePath, data.path)
)
}
if (data) {
res.push(data as AppRouteRecordRaw)
}
}
return res
}
// 后端控制路由生成
export const generateRoutesByServer = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
for (const route of routes) {
const data: AppRouteRecordRaw = {
path: route.path,
name: route.name,
redirect: route.redirect,
meta: route.meta
}
if (route.component) {
const comModule = modules[`../${route.component}.vue`] || modules[`../${route.component}.tsx`]
const component = route.component as string
if (!comModule && !component.includes('#')) {
console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`)
} else {
// 动态加载路由文件,可根据实际情况进行自定义逻辑
data.component =
component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule
}
}
// recursive child routes
if (route.children) {
data.children = generateRoutesByServer(route.children)
}
res.push(data as AppRouteRecordRaw)
}
return res
}
export const pathResolve = (parentPath: string, path: string) => {
if (isUrl(path)) return path
const childPath = path.startsWith('/') || !path ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/\//g, '/').trim()
}
// 路由降级
export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => {
const modules: AppRouteRecordRaw[] = cloneDeep(routes)
for (let index = 0; index < modules.length; index++) {
const route = modules[index]
if (!isMultipleRoute(route)) {
continue
}
promoteRouteLevel(route)
}
return modules
}
// 层级是否大于2
const isMultipleRoute = (route: AppRouteRecordRaw) => {
if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
return false
}
const children = route.children
let flag = false
for (let index = 0; index < children.length; index++) {
const child = children[index]
if (child.children?.length) {
flag = true
break
}
}
return flag
}
// 生成二级路由
const promoteRouteLevel = (route: AppRouteRecordRaw) => {
let router: Router | null = createRouter({
routes: [route as RouteRecordRaw],
history: createWebHashHistory()
})
const routes = router.getRoutes()
addToChildren(routes, route.children || [], route)
router = null
route.children = route.children?.map((item) => omit(item, 'children'))
}
// 添加所有子菜单
const addToChildren = (
routes: RouteRecordNormalized[],
children: AppRouteRecordRaw[],
routeModule: AppRouteRecordRaw
) => {
for (let index = 0; index < children.length; index++) {
const child = children[index]
const route = routes.find((item) => item.name === child.name)
if (!route) {
continue
}
routeModule.children = routeModule.children || []
if (!routeModule.children.find((item) => item.name === route.name)) {
routeModule.children?.push(route as unknown as AppRouteRecordRaw)
}
if (child.children?.length) {
addToChildren(routes, child.children, routeModule)
}
}
}
+207
View File
@@ -0,0 +1,207 @@
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 const 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 const 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 const 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 const 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 const 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 const 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 const 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 const 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 const treeMap = <T = any>(
treeData: T[],
opt: { children?: string; conversion: Fn }
): T[] => {
return treeData.map((item) => treeMapEach(item, opt))
}
/**
* @description: Extract tree specified structure
*/
export const 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 const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => {
treeDatas.forEach((element) => {
const newNode = callBack(element, parentNode) || element
if (element.children) {
eachTree(element.children, callBack, newNode)
}
})
}
+16
View File
@@ -0,0 +1,16 @@
import { Slots } from 'vue'
import { isFunction } from '@/utils/is'
export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => {
// Reflect.has 判断一个对象是否存在某个属性
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)
}