2026-01-16 22:04:12 +08:00
|
|
|
|
import type { FormInstance } from 'element-plus'
|
|
|
|
|
|
import { isNull, trim } from 'lodash-es'
|
|
|
|
|
|
import type { CSSProperties } from 'vue'
|
|
|
|
|
|
import type { TranslateOptions } from 'vue-i18n'
|
2026-04-07 11:01:11 +08:00
|
|
|
|
import { useMemberStore } from '~/stores/member'
|
|
|
|
|
|
import { useConfigStore } from '~/stores/config'
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type anyObj = { [key: string]: any }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取token
|
2026-04-07 11:01:11 +08:00
|
|
|
|
* @param type token类型,默认为Bearer
|
|
|
|
|
|
* @returns 完整的token字符串
|
2026-01-16 22:04:12 +08:00
|
|
|
|
*/
|
2026-04-07 11:01:11 +08:00
|
|
|
|
export function getToken(type: string = 'Bearer'): null | string {
|
|
|
|
|
|
// 优先从 store 获取
|
|
|
|
|
|
let token = useMemberStore().token
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 store 中有token且不为空,直接返回
|
|
|
|
|
|
if (token && token.trim() !== '') {
|
|
|
|
|
|
return `${type} ${token}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 store 中没有,尝试从 cookie 获取(处理页面刷新后的情况)
|
|
|
|
|
|
const cookieToken = useCookie('token').value
|
|
|
|
|
|
if (cookieToken && cookieToken.trim() !== '') {
|
|
|
|
|
|
return `${type} ${cookieToken}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取refresh token
|
|
|
|
|
|
* @param type token类型,默认为Bearer
|
|
|
|
|
|
* @returns 完整的refresh token字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getRefreshToken(type: string = 'Bearer'): null | string {
|
|
|
|
|
|
// 优先从 store 获取
|
|
|
|
|
|
let refreshToken = useMemberStore().refreshToken
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 store 中有token且不为空,直接返回
|
|
|
|
|
|
if (refreshToken && refreshToken.trim() !== '') {
|
|
|
|
|
|
return `${type} ${refreshToken}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 store 中没有,尝试从 cookie 获取(处理页面刷新后的情况)
|
|
|
|
|
|
const cookieRefreshToken = useCookie('refreshToken').value
|
|
|
|
|
|
if (cookieRefreshToken && cookieRefreshToken.trim() !== '') {
|
|
|
|
|
|
return `${type} ${cookieRefreshToken}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取资源完整地址
|
|
|
|
|
|
* @param relativeUrl 资源相对地址
|
|
|
|
|
|
* @param domain 指定域名
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const fullUrl = (relativeUrl: string, domain = '') => {
|
|
|
|
|
|
const configStore = useConfigStore()
|
|
|
|
|
|
if (!domain) {
|
|
|
|
|
|
domain = configStore?.cdn_url ? configStore?.cdn_url : import.meta.env.VITE_API_BASE_URL
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
return domain + relativeUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 将Markdown内容中的绝对URL转换为相对路径
|
|
|
|
|
|
* @param markdown Markdown内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const convertMarkdownToRelative = (markdown: string): string => {
|
|
|
|
|
|
if (!markdown) return markdown
|
|
|
|
|
|
const configStore = useConfigStore()
|
|
|
|
|
|
const domain = configStore?.cdn_url || import.meta.env.VITE_API_BASE_URL || window.location.origin
|
|
|
|
|
|
|
|
|
|
|
|
return markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, url) => {
|
|
|
|
|
|
// 跳过 base64 图片和相对路径
|
|
|
|
|
|
if (url.startsWith('data:') || !url.startsWith('http://') && !url.startsWith('https://')) {
|
|
|
|
|
|
return match
|
|
|
|
|
|
}
|
|
|
|
|
|
// 将绝对URL转换为相对路径
|
|
|
|
|
|
try {
|
|
|
|
|
|
const urlObj = new URL(url)
|
|
|
|
|
|
const pathname = urlObj.pathname
|
|
|
|
|
|
return ``
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return match
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将Markdown内容中的相对路径转换为绝对URL
|
|
|
|
|
|
* @param markdown Markdown内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const convertMarkdownToFull = (markdown: string): string => {
|
|
|
|
|
|
if (!markdown) return markdown
|
|
|
|
|
|
const configStore = useConfigStore()
|
|
|
|
|
|
const domain = configStore?.cdn_url || import.meta.env.VITE_API_BASE_URL || window.location.origin
|
|
|
|
|
|
|
|
|
|
|
|
return markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, url) => {
|
|
|
|
|
|
// 跳过 base64 图片和外部URL(OSS/CDN)
|
|
|
|
|
|
if (url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://')) {
|
|
|
|
|
|
return match
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是相对路径,转换为绝对URL
|
|
|
|
|
|
const fullUrl = url.startsWith('/') ? `${domain}${url}` : `${domain}/${url}`
|
|
|
|
|
|
return ``
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 判断是否为外部URL(OSS/CDN等)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const isExternalUrl = (url: string): boolean => {
|
|
|
|
|
|
return /^https?:\/\//.test(url) || /^data:image\//.test(url)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 22:04:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取一组资源的完整地址
|
|
|
|
|
|
* @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 path 文件路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const getFileNameFromPath = (path: string) => {
|
|
|
|
|
|
const paths = path.split('/')
|
|
|
|
|
|
return paths[paths.length - 1]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否是外部链接
|
|
|
|
|
|
* @param path
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function isExternal(path: string): boolean {
|
|
|
|
|
|
return /^(https?|ftp|mailto|tel):/.test(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否为手机设备
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const isMobile = () => {
|
|
|
|
|
|
const event = useRequestEvent()
|
|
|
|
|
|
const userAgent = import.meta.client ? navigator.userAgent : event?.node.req.headers['user-agent']
|
|
|
|
|
|
if (!userAgent) return false
|
|
|
|
|
|
return !!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 fn 执行函数
|
|
|
|
|
|
* @param ms 间隔毫秒数
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const debounce = (fn: Function, ms: number) => {
|
|
|
|
|
|
return (...args: any[]) => {
|
|
|
|
|
|
if (globals.lazy) {
|
|
|
|
|
|
clearTimeout(globals.lazy)
|
|
|
|
|
|
}
|
|
|
|
|
|
globals.lazy = setTimeout(() => {
|
|
|
|
|
|
fn(...args)
|
|
|
|
|
|
}, ms)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表单重置
|
|
|
|
|
|
* @param formEl
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function onResetForm(formEl?: FormInstance | null) {
|
|
|
|
|
|
typeof formEl?.resetFields == 'function' && formEl?.resetFields()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取简洁的路由 path
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const getCurrentRoutePath = () => {
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
let path = router.currentRoute.value.path
|
|
|
|
|
|
if (path.indexOf('?') !== -1) path = path.replace(/\?.*/, '')
|
|
|
|
|
|
return path
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function auth(node: string): boolean
|
|
|
|
|
|
export function auth(node: { name: string; subNodeName?: string }): boolean
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-07 11:01:11 +08:00
|
|
|
|
* 权限检查函数
|
|
|
|
|
|
* @param node 权限节点,可以是字符串或对象
|
|
|
|
|
|
* @returns 是否有权限
|
2026-01-16 22:04:12 +08:00
|
|
|
|
*/
|
2026-04-07 11:01:11 +08:00
|
|
|
|
export function auth(node: string | { name: string; subNodeName?: string }): boolean {
|
|
|
|
|
|
// TODO: 实现权限检查逻辑
|
|
|
|
|
|
// 目前先返回 true,表示所有用户都有权限
|
|
|
|
|
|
return true
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* 格式化时间戳
|
|
|
|
|
|
*/
|
|
|
|
|
|
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())
|
|
|
|
|
|
}
|
2026-04-07 11:01:11 +08:00
|
|
|
|
|
|
|
|
|
|
let date: Date
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof dateTime === 'string') {
|
|
|
|
|
|
date = new Date(dateTime)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (dateTime.toString().length === 10) {
|
|
|
|
|
|
dateTime = +dateTime * 1000
|
|
|
|
|
|
}
|
|
|
|
|
|
date = new Date(Number(dateTime))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNaN(date.getTime())) {
|
|
|
|
|
|
return ''
|
2026-01-16 22:04:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* calc(100vh - minusHeight)
|
|
|
|
|
|
* @param minusHeight 需要减去的高度
|
|
|
|
|
|
* @returns CSSProperties
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function calcHeight(minusHeight = 60): CSSProperties {
|
|
|
|
|
|
return {
|
|
|
|
|
|
height: 'calc(100vh - ' + minusHeight.toString() + 'px)',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|