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 * 使用方式: * 详见<待完善> */ 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, options?: TranslateOptions) => { 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 }