fe-core-cli
Version:
All-in-one scaffolding, cli integrated with Vue3 ecological chain, cli of H5 page, mock template cli of Node
676 lines (608 loc) • 17 kB
text/typescript
/**
* @author Gilbert
* @Description: 通用的工具函数
* @date 31.05.2023
*/
import { isArray, isNumber, uniq, cloneDeep, isObject, isFunction, find } from 'lodash-es'
import type { Slots } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { REGEX } from './regex'
import { HTTP_PATTERN } from './constant'
import axios from 'axios'
/**
* 文件下载
* 解决浏览器把下载文件识别为预览文件的问题
* @param url 下载地址
* @param fileName 自定义文件名
*/
/**
* 文件下载
* 解决浏览器把下载文件识别为预览文件的问题
* @param url 下载地址
* @param fileName 自定义文件名
* @return 无返回
* @example
* ``` typescript
* urlDownloadFile('
* https://cdnfile.corp.xxx.com/kc/files/a/sogame-static/gameclound-compatibility/Android.xlsx',
* '测试的文件.xlsx'
* )
*
* ```
*/
export const urlDownloadFile = (url: string, fileName: string) => {
const link = document.createElement('a')
link.style.display = 'none'
document.body.appendChild(link)
const name = encodeURIComponent(fileName)
// response-content-type=application/octet-stream : 解决点击下载变成预览的功能
// response-content-disposition: 解决自定义文件名的问题
// eslint-disable-next-line max-len
link.href = `${url}?response-content-type=application/octet-stream;charset=UTF-8&response-content-disposition=attachment;filename=${name}`
link.target = '__blank'
link.click()
window.URL.revokeObjectURL(link.href)
}
// 轮询函数类型
export type loopIntervalModel = {
start: () => void
stop: () => void
}
// eslint-disable-next-line no-unused-vars
export type loopIntervalCallback = (stop: () => void) => Promise<void | number>
/**
* 轮询函数
* @param callback 回调函数
* @param interval 时间间隔
*/
export function loopInterval(callback: loopIntervalCallback, interval = 2000): loopIntervalModel {
let timer: any
let isStop = false
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const loop = async () => {
try {
await callback(stop)
} catch (err) {
throw new Error('轮询出错:', err as any)
}
if (isStop) {
return
}
return (timer = setTimeout(loop, interval))
}
const stop = () => {
isStop = true
clearTimeout(timer)
}
const start = async () => {
isStop = false
await loop()
}
return {
start,
stop
}
}
export interface treeItemModel {
[key: string]: any
children?: treeItemModel[]
}
/**
* 树形结构的遍历 map
* @param tree
* @param func
* @return{Array}
*/
export const treeForeachMap = <T = treeItemModel, K = treeItemModel>(
tree: T[],
// eslint-disable-next-line no-unused-vars
func: (item: T) => K | boolean
): K[] => {
if (!isArray(tree)) {
return []
}
return tree.reduce((prev: K[], curr) => {
const result = func(curr)
if (!result) {
return prev
}
const currData = result === true ? curr : (result as K)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const childrenData = curr.children ? curr.children : []
if (childrenData.length > 0) {
prev.push({
...currData,
children: treeForeachMap(childrenData, func)
} as K)
} else {
prev.push({
...currData
} as K)
}
return prev
}, [])
}
/**
* 扁平数组转换成树形结构数据
* 这种必须要有个根结点,不然会出问题
* @param arr
* @param pid
* @return {obj: object}
*/
export function arrFormatTree(arr: Record<string, any>[], pid: object) {
const map = new WeakMap() // 生成map存储元素
for (const item of arr) {
// 若map中没有当前元素,添加并初始化children
if (!map.has(item.id)) {
map.set(item.id, { ...item, children: [] })
} else {
// 若不存在父元素 pid,后续会构造一个空的父元素,就会出现当前元素 item 已经存在的情况;此时需要将当前元素与已存在的元素(创建的父元素)进行合并
map.set(item.id, { ...map.get(item.id), ...item })
}
// 查找父元素,存在则将该元素插入到children
if (map.has(item.pid)) {
map.get(item.pid).children.push(map.get(item.id))
} else {
// 否则初始化父元素,并插入children
map.set(item.pid, { children: [map.get(item.id)] })
}
}
return map.get(pid)
}
/**
* 映射对象转换成表格映射参数对象
* {
* test1: '测试用户'
* } =>
* {
* test1: {
* text: 测试用户
* }
* }
* @param data
*/
export const objMapFormatToTableValueEnum = (data: Record<string, any>) => {
const valueEnum: Record<string, any> = {}
Object.entries(data).forEach(([key, value]) => {
valueEnum[key] = {
text: value
}
})
return valueEnum
}
export const MEMORY_SIZE_UNIT = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as const
// 联合类型
export type symbolModel = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB'
/**
* 目标内存大小转换成字节
* @param num
* @param symbol 单位
*/
export const byteConvert = (num: number, symbol: symbolModel) => {
const targetIndex = MEMORY_SIZE_UNIT.findIndex((item) => item === symbol) || 0
if (!isNumber(num)) {
return 0
}
return num * Math.pow(2, 10 * targetIndex)
}
/**
* 获取base64地址
* @param file
*/
export function getBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result as string)
reader.onerror = (error) => reject(error)
})
}
/**
* 获取图片的像素{
* width: '',
* height: ''
* }
* @param img
*/
export const getImgPx = (
img: string
): Promise<{
width: number
height: number
}> => {
const image = new Image()
image.src = img
return new Promise((resolve) => {
image.onload = () => {
const width = image.width
const height = image.height
resolve({ width, height })
}
})
}
/**
* 校验对象数组中,指定key值是否存在相同的值
* true: 存在重复
* false: 不存在重复
* @param data
* @param key
* @return Boolean
*/
export function checkObjectArrRepeatValue<T>(data: T[], key: keyof T) {
const targetKeyValues = data.map((item) => item[key])
const uniqValues = uniq(targetKeyValues)
return targetKeyValues.length !== uniqValues.length
}
/**
* 格式化 undefined || null 为 '-'
* @param data
*/
export const formatUndefinedOrNull = (data: any) => {
return data || data === 0 ? data : '-'
}
// 深度合并
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
let key: string
const res: any = cloneDeep(src)
for (key in target) {
res[key] = isObject(res[key]) ? deepMerge(res[key], target[key]) : (res[key] = target[key])
}
return res
}
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)
}
/**
* 通过分析matched成为面包屑数据
* @param matched
*/
export const getBreadcrumb = (matched: RouteLocationNormalizedLoaded['matched']) => {
return matched.filter((routeItem) => !routeItem.meta?.hideBreadcrumb)
}
/**
* 常量转换成选项数组
* @param textObj
* @param valArr
*/
export function constToOptions<T, V extends keyof T>(textObj: T, valArr: V[], label = 'label', value = 'value') {
return valArr.map((item) => {
return {
[label]: textObj[item],
[value]: item
}
})
}
/**
* 校验是字符串否为url
* 它可以检测以 "http"、"https" 或 "ftp" 开头的URL
* @param url{string}
* @typeParam 类型参数
* @return{Boolean} true: 表示是个可用url
* @example 使用例子
* ``` typescript
* const result = isValidURL('test')
* ===> false
*
* const result = isValidURL('https://ksg-ui.staging.xxx.com/utilModules/README.html')
* ===> true
* ```
*/
export function isValidURL(url: string) {
return REGEX.urlPattern.test(url)
}
/**
* 判断是否问图片类型
* @param fileName
* @example 使用例子
* ``` typescript
* const fileName = 'myImage.jpg';
* const result = isImageFile(fileName)
* ===> true
* ```
*/
export function isImageFile(fileName: string) {
// 将文件名转换为小写以确保不区分大小写
const ext = fileName.toLowerCase()
// 定义常见的图片文件扩展名
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
// 检查文件的扩展名是否在图片扩展名列表中
return imageExtensions.some((extension) => ext.endsWith(extension))
}
/**
* 判断是否为视频文件类型
* @param fileName
* @example 使用例子
* ``` typescript
* const fileName = 'myVideo.mp4';
* const result = isImageFile(fileName)
* ===> true
* ```
*/
export function isVideoFile(fileName: string) {
const ext = fileName.toLowerCase()
const videoExtensions = ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm']
return videoExtensions.some((extension) => ext.endsWith(extension))
}
type TreeItem = {
[key: string]: any
children?: Record<string, any>[]
}
/**
* 树形数组中
* 递归函数来查找特定 ID 的元素
* @param tree
* @param id
* @param key 默认要查找的唯一值的 字段名
* @example 使用例子
* ``` typescript
* const treeData =
* [{
* id: '1',
* name: '第一层数据',
* children: [{
* id: '1-1',
* name: '第1-1层数据'
* }]
* },
* {
* id: '2',
* name: '第二层数据'
* }],
* const target = findElementById(treeData, '1-1')
* ===>
* {
* id: '1-1',
* name: '第1-1层数据'
* }
* ```
*/
export function findElementById<T = Record<string, any>>(tree: T[], id: string | number, key = 'id'): T | null {
const foundElement = find(tree, { [key]: id })
if (foundElement) {
return foundElement as T
}
for (const node of tree) {
const nodeItem = node as TreeItem
if (nodeItem.children && nodeItem.children.length > 0) {
const foundInChild = findElementById(nodeItem.children, id, key)
if (foundInChild) {
return foundInChild as T
}
}
}
return null
}
/**
* 指定关键的字段查找,默认ID。更新该项数据
* @param tree
* @param id
* @param newData
* @param key
* @return []
* @example 使用例子
* ``` typescript
* const treeData =
* [{
* id: '1',
* name: '第一层数据',
* children: [{
* id: '1-1',
* name: '第1-1层数据'
* }]
* },
* {
* id: '2',
* name: '第二层数据'
* }],
* const newData = {
* newData: '新增数据'
* }
* const target = updateTreeNodeById(treeData, '1-1', newData)
* ===>
* [{
* id: '1',
* name: '第一层数据',
* children: [{
* id: '1-1',
* name: '第1-1层数据',
* newData: '新增数据'
* }]
* },
* {
* id: '2',
* name: '第二层数据'
* }],
* ```
*/
export function updateTreeNodeById<T = Record<string, any>>(
tree: T[],
id: number,
newData: Partial<T>,
key = 'id'
): T[] {
return tree.map((node) => {
const nodeItem = node as TreeItem
if (node[key as keyof typeof node] === id) {
// 找到匹配的节点,使用新数据覆盖旧数据
return { ...node, ...newData }
} else if (nodeItem.children?.length) {
// 递归更新子节点
return { ...node, children: updateTreeNodeById(nodeItem.children, id, newData, key) }
}
return node
})
}
/**
* 树形数组中,根据ID删除该项
* @param tree
* @param id
* @param key
* @example 使用例子
* ``` typescript
* const treeData =
* [{
* id: '1',
* name: '第一层数据',
* children: [{
* id: '1-1',
* name: '第1-1层数据'
* }]
* },
* {
* id: '2',
* name: '第二层数据'
* }],
* const newData = {
* newData: '新增数据'
* }
* const target = deleteTreeNodeById(treeData, '1-1')
* ===>
* [{
* id: '1',
* name: '第一层数据',
* children: []
* },
* {
* id: '2',
* name: '第二层数据'
* }],
* ```
*/
export function deleteTreeNodeById<T = Record<string, any>>(tree: T[], id: number, key = 'id'): T[] {
return tree.reduce((previousValue: T[], node) => {
if (node[key as keyof typeof node] === id) {
// 如果找到匹配的节点,不将其添加到结果中
return previousValue
}
const nodeItem = node as TreeItem
// 防止便利过后新增了空的 children 字段
let children = {}
if (nodeItem.children?.length) {
// 递归处理子节点,并将非匹配节点添加到结果中
children = {
children: deleteTreeNodeById(nodeItem.children || [], id, key)
}
}
const updatedNode = { ...node, ...children }
return [...previousValue, updatedNode]
}, [])
}
/**
* 用于检查给定的 URL 是否以 "http://" 或 "https://" 开头
* @param url
*/
export function isHttpOrHttps(url: string) {
// 正则表达式,匹配以 "http://" 或 "https://" 开头的 URL
return HTTP_PATTERN.test(url)
}
export type QueueModel<T> = (() => Promise<T>) | null
export type ProgressFunc<T> = (res: T) => void
/**
* 请求队列限制并发数量
*/
export class RequestQueue<T> {
private queue: QueueModel<T>[] = []
public concurrencyLimit = 3
private pendingRequestList: Promise<T>[] = []
public currRequest: any
constructor(concurrencyLimit: number, currRequest: any) {
this.pendingRequestList = []
this.queue = []
this.concurrencyLimit = concurrencyLimit || this.concurrencyLimit
this.currRequest = currRequest
}
// 终止请求
clearAll() {
// 清空剩余的队列信息
this.queue = []
// 取消正发出去的请求
this.pendingRequestList.forEach((promise) => this.currRequest.cancelRequest(promise))
}
concurrentReq(tasks: QueueModel<T>[], onProgress: ProgressFunc<T>, limit?: number): Promise<T[]> {
this.queue = tasks
const currLimit = limit || this.concurrencyLimit
return new Promise((resolve, reject) => {
let num = 0 // 当前在跑的请求数量(在跑的请求数量要小于限制的数量)
const results: T[] = [] // 最终并发请求结果存放的数组
// 递归闭包函数调用发请求,Promise返回最终结果
const goNext = () => {
if (this.queue.length === 0 && num === 0) {
// 当没有更多任务且没有请求正在进行时
resolve(results) // 所有请求已完成,resolve吐出去返回结果
return
}
while (num < currLimit && this.queue.length > 0) {
// 当请求任务小于3且还有任务继续干时,goNext
const promiseTask = this.queue.shift() // 把并发任务数组中第一项剔除掉,并拿到第一项(请求接口)
if (!promiseTask) {
return
}
num = num + 1 // 并记录一下当前的请求数量
const nextRequestPromise = promiseTask()
this.pendingRequestList.push(nextRequestPromise)
nextRequestPromise
.then((res) => {
num = num - 1 // 请求成功,就把计数减一
results.push(res) // 把请求的结果依次存起来
onProgress(res)
goNext() // 递归调用,继续执行下一个请求任务
})
.catch((err) => {
num = num - 1 // 请求失败,也把计数减一
console.error(`此接口:${promiseTask}请求失败,报错信息:${err}`)
results.push(err)
if (axios.isCancel(err)) {
reject('cancel')
}
goNext() // 递归调用,继续执行下一个请求任务
})
.finally(() => {
// 请求结束后,需要手动清理
this.pendingRequestList = this.pendingRequestList.filter((promise) => promise === nextRequestPromise)
})
}
}
goNext()
})
}
}
export type VideoInfoModel = {
duration: number
width: number
height: number
}
/**
* 获取视频信息
* @param file
* @return {
* duration
* width
* height
* }
*/
export const getVideoPlayerInfo = (file: File): Promise<VideoInfoModel> => {
return new Promise((resolve) => {
const videoElement = document.createElement('video')
videoElement.src = URL.createObjectURL(file)
videoElement.addEventListener('loadedmetadata', function () {
resolve({
duration: videoElement.duration,
width: videoElement.videoWidth,
height: videoElement.videoHeight
})
})
})
}