405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
import * as elIcons from '@element-plus/icons-vue'
|
||
import { useTitle } from '@vueuse/core'
|
||
import type { FormInstance } from 'element-plus'
|
||
import { isArray, isNull, trim, trimStart } from 'lodash-es'
|
||
import type { App } from 'vue'
|
||
import { nextTick } from 'vue'
|
||
import type { TranslateOptions } from 'vue-i18n'
|
||
import { i18n } from '../lang'
|
||
import { useSiteConfig } from '../stores/siteConfig'
|
||
import { getUrl } from './axios'
|
||
import Icon from '/@/components/icon/index.vue'
|
||
import router from '/@/router/index'
|
||
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||
import { useNavTabs } from '/@/stores/navTabs'
|
||
|
||
export function registerIcons(app: App) {
|
||
/*
|
||
* 全局注册 Icon
|
||
* 使用方式: <Icon name="name" size="size" color="color" />
|
||
* 详见<待完善>
|
||
*/
|
||
app.component('Icon', Icon)
|
||
|
||
/*
|
||
* 全局注册element Plus的icon
|
||
*/
|
||
const icons = elIcons as any
|
||
for (const i in icons) {
|
||
app.component(`el-icon-${icons[i].name}`, icons[i])
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载网络css文件
|
||
* @param url css资源url
|
||
*/
|
||
export function loadCss(url: string): void {
|
||
const link = document.createElement('link')
|
||
link.rel = 'stylesheet'
|
||
link.href = url
|
||
link.crossOrigin = 'anonymous'
|
||
document.getElementsByTagName('head')[0].appendChild(link)
|
||
}
|
||
|
||
/**
|
||
* 加载网络js文件
|
||
* @param url js资源url
|
||
*/
|
||
export function loadJs(url: string): void {
|
||
const link = document.createElement('script')
|
||
link.src = url
|
||
document.body.appendChild(link)
|
||
}
|
||
|
||
/**
|
||
* 根据路由 meta.title 设置浏览器标题
|
||
*/
|
||
export function setTitleFromRoute() {
|
||
nextTick(() => {
|
||
if (typeof router.currentRoute.value.meta.title != 'string') {
|
||
return
|
||
}
|
||
const webTitle = i18n.global.te(router.currentRoute.value.meta.title)
|
||
? i18n.global.t(router.currentRoute.value.meta.title)
|
||
: router.currentRoute.value.meta.title
|
||
const title = useTitle()
|
||
const siteConfig = useSiteConfig()
|
||
title.value = `${webTitle}${siteConfig.siteName ? ' - ' + siteConfig.siteName : ''}`
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 设置浏览器标题
|
||
* @param webTitle 新的标题
|
||
*/
|
||
export function setTitle(webTitle: string) {
|
||
if (router.currentRoute.value) {
|
||
router.currentRoute.value.meta.title = webTitle
|
||
}
|
||
nextTick(() => {
|
||
const title = useTitle()
|
||
const siteConfig = useSiteConfig()
|
||
title.value = `${webTitle}${siteConfig.siteName ? ' - ' + siteConfig.siteName : ''}`
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 是否是外部链接
|
||
* @param path
|
||
*/
|
||
export function isExternal(path: string): boolean {
|
||
return /^(https?|ftp|mailto|tel):/.test(path)
|
||
}
|
||
|
||
/**
|
||
* 全局防抖
|
||
* 与 _.debounce 不同的是,间隔期间如果再次传递不同的函数,两个函数也只会执行一次
|
||
* @param fn 执行函数
|
||
* @param ms 间隔毫秒数
|
||
*/
|
||
export const debounce = (fn: Function, ms: number) => {
|
||
return (...args: any[]) => {
|
||
if (window.lazy) {
|
||
clearTimeout(window.lazy)
|
||
}
|
||
window.lazy = window.setTimeout(() => {
|
||
fn(...args)
|
||
}, ms)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据pk字段的值从数组中获取key
|
||
* @param arr
|
||
* @param pk
|
||
* @param value
|
||
*/
|
||
export const getArrayKey = (arr: any, pk: string, value: any): any => {
|
||
for (const key in arr) {
|
||
if (arr[key][pk] == value) {
|
||
return key
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 表单重置
|
||
* @param formEl
|
||
*/
|
||
export const onResetForm = (formEl?: FormInstance | null) => {
|
||
typeof formEl?.resetFields == 'function' && formEl.resetFields()
|
||
}
|
||
|
||
/**
|
||
* 将数据构建为ElTree的data {label:'', children: []}
|
||
* @param data
|
||
*/
|
||
export const buildJsonToElTreeData = (data: any): ElTreeData[] => {
|
||
if (typeof data == 'object') {
|
||
const childrens = []
|
||
for (const key in data) {
|
||
childrens.push({
|
||
label: key + ': ' + data[key],
|
||
children: buildJsonToElTreeData(data[key]),
|
||
})
|
||
}
|
||
return childrens
|
||
} else {
|
||
return []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 是否在后台应用内
|
||
* @param path 不传递则通过当前路由 path 检查
|
||
*/
|
||
export const isAdminApp = (path = '') => {
|
||
const regex = new RegExp(`^${adminBaseRoutePath}`)
|
||
if (path) {
|
||
return regex.test(path)
|
||
}
|
||
if (regex.test(getCurrentRoutePath())) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 是否为手机设备
|
||
*/
|
||
export const isMobile = () => {
|
||
return !!navigator.userAgent.match(
|
||
/android|webos|ip(hone|ad|od)|opera (mini|mobi|tablet)|iemobile|windows.+(phone|touch)|mobile|fennec|kindle (Fire)|Silk|maemo|blackberry|playbook|bb10\; (touch|kbd)|Symbian(OS)|Ubuntu Touch/i
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 从一个文件路径中获取文件名
|
||
* @param path 文件路径
|
||
*/
|
||
export const getFileNameFromPath = (path: string) => {
|
||
const paths = path.split('/')
|
||
return paths[paths.length - 1]
|
||
}
|
||
|
||
export function auth(node: string): boolean
|
||
export function auth(node: { name: string; subNodeName?: string }): boolean
|
||
|
||
/**
|
||
* 鉴权
|
||
* 提供 string 将根据当前路由 path 自动拼接和鉴权,还可以提供路由的 name 对象进行鉴权
|
||
* @param node
|
||
*/
|
||
export function auth(node: string | { name: string; subNodeName?: string }) {
|
||
const store = isAdminApp() ? useNavTabs() : useMemberCenter()
|
||
if (typeof node === 'string') {
|
||
const path = getCurrentRoutePath()
|
||
if (store.state.authNode.has(path)) {
|
||
const subNodeName = path + (path == '/' ? '' : '/') + node
|
||
if (store.state.authNode.get(path)!.some((v: string) => v == subNodeName)) {
|
||
return true
|
||
}
|
||
}
|
||
} else {
|
||
// 节点列表中没有找到 name
|
||
if (!node.name || !store.state.authNode.has(node.name)) return false
|
||
|
||
// 无需继续检查子节点或未找到子节点
|
||
if (!node.subNodeName || store.state.authNode.get(node.name)?.includes(node.subNodeName)) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 获取资源完整地址
|
||
* @param relativeUrl 资源相对地址
|
||
* @param domain 指定域名
|
||
*/
|
||
export const fullUrl = (relativeUrl: string, domain = '') => {
|
||
const siteConfig = useSiteConfig()
|
||
if (!domain) {
|
||
domain = siteConfig.cdnUrl ? siteConfig.cdnUrl : getUrl()
|
||
}
|
||
if (!relativeUrl) return domain
|
||
|
||
const regUrl = new RegExp(/^http(s)?:\/\//)
|
||
const regexImg = new RegExp(/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i)
|
||
if (!domain || regUrl.test(relativeUrl) || regexImg.test(relativeUrl)) {
|
||
return relativeUrl
|
||
}
|
||
|
||
let url = domain + relativeUrl
|
||
if (domain === siteConfig.cdnUrl && siteConfig.cdnUrlParams) {
|
||
const separator = url.includes('?') ? '&' : '?'
|
||
url += separator + siteConfig.cdnUrlParams
|
||
}
|
||
return url
|
||
}
|
||
|
||
/**
|
||
* 获取路由 path
|
||
*/
|
||
export const getCurrentRoutePath = () => {
|
||
let path = router.currentRoute.value.path
|
||
if (path == '/') path = trimStart(window.location.hash, '#')
|
||
if (path.indexOf('?') !== -1) path = path.replace(/\?.*/, '')
|
||
return path
|
||
}
|
||
|
||
/**
|
||
* 获取根据当前路由路径动态加载的语言翻译
|
||
* @param key 无需语言路径的翻译key,亦可使用完整路径
|
||
* @param named — 命名插值的值
|
||
* @param options — 其他翻译选项
|
||
* @returns — Translated message
|
||
*/
|
||
export const __ = (key: string, named?: Record<string, unknown>, options?: TranslateOptions<string>) => {
|
||
let langPath = ''
|
||
const path = getCurrentRoutePath()
|
||
if (isAdminApp()) {
|
||
langPath = path.slice(path.indexOf(adminBaseRoutePath) + adminBaseRoutePath.length)
|
||
langPath = trim(langPath, '/').replaceAll('/', '.')
|
||
} else {
|
||
langPath = trim(path, '/').replaceAll('/', '.')
|
||
}
|
||
langPath = langPath ? langPath + '.' + key : key
|
||
return i18n.global.te(langPath)
|
||
? i18n.global.t(langPath, named ?? {}, options ? options : {})
|
||
: i18n.global.t(key, named ?? {}, options ? options : {})
|
||
}
|
||
|
||
/**
|
||
* 文件类型效验,前端根据服务端配置进行初步检查
|
||
* @param fileName 文件名
|
||
* @param fileType 文件 mimeType,不一定存在
|
||
*/
|
||
export const checkFileMimetype = (fileName: string, fileType: string) => {
|
||
if (!fileName) return false
|
||
const siteConfig = useSiteConfig()
|
||
const allowedSuffixes = isArray(siteConfig.upload.allowedSuffixes)
|
||
? siteConfig.upload.allowedSuffixes
|
||
: siteConfig.upload.allowedSuffixes.toLowerCase().split(',')
|
||
|
||
const allowedMimeTypes = isArray(siteConfig.upload.allowedMimeTypes)
|
||
? siteConfig.upload.allowedMimeTypes
|
||
: siteConfig.upload.allowedMimeTypes.toLowerCase().split(',')
|
||
|
||
const fileSuffix = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||
if (allowedSuffixes.includes(fileSuffix) || allowedSuffixes.includes('.' + fileSuffix)) {
|
||
return true
|
||
}
|
||
if (fileType && allowedMimeTypes.includes(fileType)) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 获取一组资源的完整地址
|
||
* @param relativeUrls 资源相对地址
|
||
* @param domain 指定域名
|
||
*/
|
||
export const arrayFullUrl = (relativeUrls: string | string[], domain = '') => {
|
||
if (typeof relativeUrls === 'string') {
|
||
relativeUrls = relativeUrls == '' ? [] : relativeUrls.split(',')
|
||
}
|
||
for (const key in relativeUrls) {
|
||
relativeUrls[key] = fullUrl(relativeUrls[key], domain)
|
||
}
|
||
return relativeUrls
|
||
}
|
||
|
||
/**
|
||
* 格式化时间戳
|
||
* @param dateTime 时间戳,默认使用当前时间戳
|
||
* @param fmt 格式化方式,默认:yyyy-mm-dd hh:MM:ss
|
||
*/
|
||
export const timeFormat = (dateTime: string | number | null = null, fmt = 'yyyy-mm-dd hh:MM:ss') => {
|
||
if (dateTime == 'none') {
|
||
return i18n.global.t('None')
|
||
}
|
||
|
||
if (isNull(dateTime)) {
|
||
dateTime = Number(new Date())
|
||
}
|
||
|
||
/**
|
||
* 1. 秒级时间戳(10位)需要转换为毫秒级,才能供 Date 对象直接使用
|
||
* 2. yyyy-mm-dd 也是10位,使用 isFinite 进行排除
|
||
*/
|
||
if (String(dateTime).length === 10 && isFinite(Number(dateTime))) {
|
||
dateTime = +dateTime * 1000
|
||
}
|
||
|
||
let date = new Date(dateTime)
|
||
if (isNaN(date.getTime())) {
|
||
date = new Date(Number(dateTime))
|
||
if (isNaN(date.getTime())) {
|
||
return 'Invalid Date'
|
||
}
|
||
}
|
||
|
||
let ret
|
||
const opt: anyObj = {
|
||
'y+': date.getFullYear().toString(), // 年
|
||
'm+': (date.getMonth() + 1).toString(), // 月
|
||
'd+': date.getDate().toString(), // 日
|
||
'h+': date.getHours().toString(), // 时
|
||
'M+': date.getMinutes().toString(), // 分
|
||
's+': date.getSeconds().toString(), // 秒
|
||
}
|
||
for (const k in opt) {
|
||
ret = new RegExp('(' + k + ')').exec(fmt)
|
||
if (ret) {
|
||
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : padStart(opt[k], ret[1].length, '0'))
|
||
}
|
||
}
|
||
return fmt
|
||
}
|
||
|
||
/**
|
||
* 字符串补位
|
||
*/
|
||
const padStart = (str: string, maxLength: number, fillString = ' ') => {
|
||
if (str.length >= maxLength) return str
|
||
|
||
const fillLength = maxLength - str.length
|
||
let times = Math.ceil(fillLength / fillString.length)
|
||
while ((times >>= 1)) {
|
||
fillString += fillString
|
||
if (times === 1) {
|
||
fillString += fillString
|
||
}
|
||
}
|
||
return fillString.slice(0, fillLength) + str
|
||
}
|
||
|
||
/**
|
||
* 根据当前时间生成问候语
|
||
*/
|
||
export const getGreet = () => {
|
||
const now = new Date()
|
||
const hour = now.getHours()
|
||
let greet = ''
|
||
|
||
if (hour < 5) {
|
||
greet = i18n.global.t('utils.Late at night, pay attention to your body!')
|
||
} else if (hour < 9) {
|
||
greet = i18n.global.t('utils.good morning!') + i18n.global.t('utils.welcome back')
|
||
} else if (hour < 12) {
|
||
greet = i18n.global.t('utils.Good morning!') + i18n.global.t('utils.welcome back')
|
||
} else if (hour < 14) {
|
||
greet = i18n.global.t('utils.Good noon!') + i18n.global.t('utils.welcome back')
|
||
} else if (hour < 18) {
|
||
greet = i18n.global.t('utils.good afternoon') + i18n.global.t('utils.welcome back')
|
||
} else if (hour < 24) {
|
||
greet = i18n.global.t('utils.Good evening') + i18n.global.t('utils.welcome back')
|
||
} else {
|
||
greet = i18n.global.t('utils.Hello!') + i18n.global.t('utils.welcome back')
|
||
}
|
||
return greet
|
||
}
|