initial commit

This commit is contained in:
Mr.April
2026-01-16 22:04:48 +08:00
parent 28cc3ec005
commit c5244e2b75
95 changed files with 27633 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
import type { FormInstance } from 'element-plus'
import { isNull, trim } from 'lodash-es'
import type { CSSProperties } from 'vue'
import type { TranslateOptions } from 'vue-i18n'
type anyObj = { [key: string]: any }
/**
* 获取token
* @returns
*/
export function getToken(): null | string {
return useMemberStore().token
}
/**
* 获取资源完整地址
* @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
}
/**
* 获取一组资源的完整地址
* @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
/**
* 鉴权
* 提供 string 将根据当前路由 path 自动拼接和鉴权,还可以提供路由的 name 对象进行鉴权
* @param node
*/
export function auth(node: string | { name: string; subNodeName?: string }) {
const store = usePersonalCenterStore()
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
}
/*
* 格式化时间戳
*/
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())
}
if (dateTime.toString().length === 10) {
dateTime = +dateTime * 1000
}
const date = new Date(Number(dateTime))
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)',
}
}
+24
View File
@@ -0,0 +1,24 @@
import { ISDARK } from '~/stores/constant/keys'
export function getDark() {
const dark = useCookie(ISDARK)
return dark.value && parseInt(dark.value) ? true : false
}
export function setDark(val: boolean) {
const dark = useCookie(ISDARK)
dark.value = val ? '1' : '0'
updateHtmlDarkClass(val)
}
export function updateHtmlDarkClass(val: boolean) {
if (import.meta.server) return
const htmlEl = document.getElementsByTagName('html')[0]
htmlEl.setAttribute('class', val ? 'dark' : '')
}
export function initDark() {
updateHtmlDarkClass(getDark())
}
if (import.meta.client) initDark()
+76
View File
@@ -0,0 +1,76 @@
import { nextTick } from 'vue'
class Language {
private i18n: any;
private loadLocale: Array<string> = []; //已加载的语言
public file: string = '';
constructor(i18n: any) {
this.i18n = i18n
}
/**
*
* @param locale 设置语言
*/
public setI18nLanguage(locale: string) {
if (this.i18n.mode === 'legacy') {
this.i18n.global.locale = locale
} else {
this.i18n.global.locale = locale
}
let html = document.querySelector('html')
html && html.setAttribute('lang', locale)
}
/**
* 加载语言包
* @param path
* @param locale
* @returns
*/
public async loadLocaleMessages(app: string, path: string, locale: string) {
try {
const file = path == '/' ? 'index' : path.replace('/', '').replaceAll('/', '.')
if (this.loadLocale.includes(`${app}/${locale}/${file}`)) {
return nextTick()
}
this.loadLocale.push(`${app}/${locale}/${file}`)
if (app != 'app' && !this.loadLocale.includes(`${app}/${locale}/pages`)) {
const pagesMessages = await import(`~/addon/${app}/lang/${locale}/pages.json`)
this.i18n.global.mergeLocaleMessage(locale, pagesMessages)
this.loadLocale.push(`${app}/${locale}/pages`)
}
// 引入语言包文件
const messages = await import(app == 'app' ? `~/app/lang/${locale}/${file}.json` : `~/addon/${app}/lang/${locale}/${file}.json`)
const prefix = this.getFileKey(app, path)
let data: Record<string, string> = {}
Object.keys(messages.default).forEach(key => {
data[`${prefix}.${key}`] = messages.default[key]
})
this.i18n.global.mergeLocaleMessage(locale, data)
this.setI18nLanguage(locale)
return nextTick()
} catch {
this.setI18nLanguage(locale)
return nextTick()
}
}
/**
* @param app
* @param path
*/
public getFileKey = (app: string, path: string) => {
const file = path == '/' ? 'index' : path.replace('/', '').replaceAll('/', '.')
return `${app}.${file}`
}
}
export default Language
+184
View File
@@ -0,0 +1,184 @@
import { ElMessage } from 'element-plus'
import qs from 'qs'
interface ConfigOption {
showErrorMessage?: boolean
showSuccessMessage?: boolean,
headers?: Headers
}
interface FetchOptions {
baseURL: string
headers: Record<string, any>
onRequest?: (data: any) => void
onResponse?: (data: any) => void
onResponseError?: (data: any) => void
showErrorMessage?: boolean
showSuccessMessage?: boolean
watch: boolean
}
class Http {
private options: FetchOptions = {
baseURL: '',
headers: {},
watch: false
}
public constructor() {
/**
* 全局请求拦截器
*/
this.options.onRequest = (data) => {
const runtimeConfig = useRuntimeConfig()
this.options.baseURL = runtimeConfig.public.VITE_APP_BASE_URL || `${location.origin}/api/`
this.options.headers[runtimeConfig.public.VITE_REQUEST_HEADER_CHANNEL_KEY] = 'pc'
if (getToken()) this.options.headers[runtimeConfig.public.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
}
/**
* 全局响应拦截器
*/
this.options.onResponse = ({ response, options }) => {
const { _data: data } = response
this.handleNetworkError(response)
if (data.code != undefined) {
if (data.code == 1) {
if (options.showSuccessMessage) ElMessage({ message: data.msg, type: 'success' })
} else {
if (options.showErrorMessage === false) return;
if (data.code == 0 || data.code == 400) {
ElMessage({ message: data.msg, type: 'error' })
} else {
this.handleAuthError(data.code)
}
}
}
}
}
public get(url: string, query = {}, config: ConfigOption = {}) {
url += '?' + qs.stringify(query)
return this.request(url, 'GET', {}, config)
}
public post(url: string, body = {}, config: ConfigOption = {}) {
return this.request(url, 'POST', { body }, config)
}
public put(url: string, body = {}, config: ConfigOption = {}) {
return this.request(url, 'PUT', { body }, config)
}
public delete(url: string, config: ConfigOption = {}) {
return this.request(url, 'DELETE', {}, config)
}
/**
* 发送请求
* @param url
* @param method
* @param param
* @param config
*/
private request(url: string, method: string, param: AnyObject = {}, config: ConfigOption = {}) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 处理首次请求baseurl空的问题
const runtimeConfig = useRuntimeConfig()
!this.options.baseURL && (this.options.baseURL = runtimeConfig.public.VITE_APP_BASE_URL || `${location.origin}/api/`)
this.options.baseURL.substr(-1) != '/' && (this.options.baseURL += '/')
// 处理数组格式
for (const key in param.query) {
if (param.query[key] instanceof Array) {
param.query[key].forEach((item, index) => {
param.query[`${key}[${index}]`] = item
});
delete param.query[key]
}
}
useFetch(url, { ...this.options, method, ...config, ...param }).then((response) => {
const { data: { value }, error } = response
if (value) {
if (value.code && value.code == 1) {
resolve(value)
} else {
if (value.type && value.type == 'application/zip') {
resolve(value)
} else {
reject(value)
}
}
} else {
reject(error)
}
}).catch(err => {
reject(err)
})
}, this.options.baseURL ? 0 : 500);
})
}
private handleAuthError(code: number) {
switch (code) {
case 401:
useMemberStore().logout()
break;
case 402:
navigateTo('/site/close', { replace: true })
break;
}
}
private handleNetworkError(err: any) {
if (err.status && err.status != 200) {
let errMessage = ''
switch (err.status) {
case 400:
errMessage = t('request.400')
break
case 401:
errMessage = t('request.401')
break
case 403:
errMessage = t('request.403')
break
case 404:
errMessage = err.url + t('request.404')
break
case 405:
errMessage = t('request.405')
break
case 408:
errMessage = t('request.408')
break
case 409:
errMessage = t('request.409')
break
case 500:
errMessage = t('request.500')
break
case 501:
errMessage = t('request.501')
break
case 502:
errMessage = t('request.502')
break
case 503:
errMessage = t('request.503')
break
case 504:
errMessage = t('request.504')
break
case 505:
errMessage = t('request.505')
break
}
ElMessage({ message: errMessage, type: 'error' })
}
}
}
const request = new Http()
export default request
+129
View File
@@ -0,0 +1,129 @@
import type { RouteRecordRaw } from 'vue-router'
import type { Menus } from '~/stores/interface'
/**
* 获取第一个菜单
*/
export const getFirstRoute = (routes: RouteRecordRaw[]): false | RouteRecordRaw => {
const routerPaths: string[] = []
const router = useRouter()
const routers = router.getRoutes()
routers.forEach((item) => {
if (item.path) routerPaths.push(item.path)
})
let find: boolean | RouteRecordRaw = false
for (const key in routes) {
if (routes[key].meta?.type != 'menu_dir' && routerPaths.indexOf(routes[key].path) !== -1) {
return routes[key]
} else if (routes[key].children && routes[key].children?.length) {
find = getFirstRoute(routes[key].children!)
if (find) return find
}
}
return find
}
/**
* 打开菜单
* @param menu 菜单数据
*/
export const onClickMenu = (menu: RouteRecordRaw) => {
switch (menu.meta?.menu_type) {
case 'iframe':
case 'tab':
navigateTo(menu.path)
break
case 'link':
window.open(menu.path, '_blank')
break
default:
alert('菜单类型未识别')
// ElNotification({
// message: $t('utils.Navigation failed, the menu type is unrecognized!'),
// type: 'error',
// })
break
}
}
/**
* 处理权限节点
* @param routes 路由数据
* @param prefix 节点前缀
* @returns 组装好的权限节点
*/
export const handleAuthNode = (routes: any, prefix = '/') => {
const authNode: Map<string, string[]> = new Map([])
assembleAuthNode(routes, authNode, prefix, prefix)
return authNode
}
const assembleAuthNode = (routes: any, authNode: Map<string, string[]>, prefix = '/', parent = '/') => {
const authNodeTemp = []
for (const key in routes) {
if (routes[key].type == 'button') authNodeTemp.push(prefix + routes[key].name)
if (routes[key].children && routes[key].children.length > 0) {
assembleAuthNode(routes[key].children, authNode, prefix, prefix + routes[key].name)
}
}
if (authNodeTemp && authNodeTemp.length > 0) {
authNode.set(parent, authNodeTemp)
}
}
export const registerMenus = (rules: any, menus: any) => {
const systemConfig = useSystemStore()
// if (rules.length) {
// personalCenter.mergeAuthNode(handleAuthNode(rules, '/'))
// systemConfig.setHeadNav(handleMenus(rules, '/', ['nav']))
// personalCenter.mergeNavUserMenus(handleMenus(rules, '/', ['nav_user_menu']))
// }
// if (menus.length) {
// const menupersonalCenterBaseRoute = '/user/'
// personalCenter.mergeAuthNode(handleAuthNode(menus, menupersonalCenterBaseRoute))
// personalCenter.mergeNavUserMenus(handleMenus(menus, '/', ['nav_user_menu']))
// personalCenter.setShowHeadline(menus.length > 1)
// personalCenter.setUserMenus(handleMenus(menus, menupersonalCenterBaseRoute, ['menu', 'menu_dir']))
// }
}
export const handleMenus = (rules: any, prefix = '/', type = ['nav']) => {
const menus: Menus[] = []
for (const key in rules) {
if (rules[key].extend == 'add_rules_only') {
continue
}
if (rules[key].type == 'menu_dir' && rules[key].children && !rules[key].children.length) {
continue
}
let children: Menus[] = []
if (rules[key].children && rules[key].children.length > 0) {
children = handleMenus(rules[key].children, prefix, type)
}
if (type.includes(rules[key].type)) {
let path = ''
if (['link', 'iframe'].includes(rules[key].menu_type)) {
path = rules[key].url
} else {
path = prefix + rules[key].path
}
menus.push({
...rules[key],
meta: {
/**
* 在 meta 中存储多个可能用到的属性
* 兼容 RouteRecordRaw 和 RouteLocationNormalizedLoaded 类型
*/
id: rules[key].id,
type: rules[key].type,
menu_type: rules[key].menu_type,
},
path: path,
children: children,
})
}
}
return menus
}
+55
View File
@@ -0,0 +1,55 @@
interface setParam {
key: string,
data: any,
success?: () => {},
fail?: (err: any) => {}
}
class Storage {
/**
* 设置缓存
* @param param
*/
public set(param: setParam) {
try {
window.localStorage.setItem(param.key, JSON.stringify(param.data))
typeof param.success == 'function' && param.success()
} catch (error) {
typeof param.fail == 'function' && param.fail(error)
}
}
/**
* 获取缓存
* @param key
* @returns
*/
public get(key: string) {
try {
const json: any = window.localStorage.getItem(key)
return JSON.parse(json)
} catch (error) {
return null
}
}
/**
* 移除指定缓存
* @param key
*/
public remove(key: string | string[]) {
if (typeof key == 'string') window.localStorage.removeItem(key)
else key.forEach(item => { window.localStorage.removeItem(item) })
}
/**
* 清理缓存
*/
public clear() {
window.localStorage.clear()
}
}
const storage = new Storage()
export default storage
+232
View File
@@ -0,0 +1,232 @@
const validate = {
/**
* 验证电子邮箱格式
*/
email(value: string) {
return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value)
},
/**
* 验证手机格式
*/
mobile(value: string) {
return /^1[23456789]\d{9}$/.test(value)
},
/**
* 验证URL格式
*/
url(value: string) {
return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/
.test(value)
},
/**
* 验证日期格式
*/
date(value: string) {
if (!value) return false
// 判断是否数值或者字符串数值(意味着为时间戳),转为数值,否则new Date无法识别字符串时间戳
if (this.number(value)) value = +value
return !/Invalid|NaN/.test(new Date(value).toString())
},
/**
* 验证ISO类型的日期格式
*/
dateISO(value: string) {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)
},
/**
* 验证十进制数字
*/
number(value: string) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
},
/**
* 验证字符串
*/
string(value: string) {
return typeof value === 'string'
},
/**
* 验证整数
*/
digits(value: string) {
return /^\d+$/.test(value)
},
/**
* 验证身份证号码
*/
idCard(value: string) {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
value
)
},
/**
* 是否车牌号
*/
carNo(value: string) {
// 新能源车牌
const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/
// 旧车牌
const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/
if (value.length === 7) {
return creg.test(value)
} if (value.length === 8) {
return xreg.test(value)
}
return false
},
/**
* 金额,只允许2位小数
*/
amount(value: string) {
// 金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value)
},
/**
* 中文
*/
chinese(value: string) {
const reg = /^[\u4e00-\u9fa5]+$/gi
return reg.test(value)
},
/**
* 只能输入字母
*/
letter(value: string) {
return /^[a-zA-Z]*$/.test(value)
},
/**
* 只能是字母或者数字
*/
enOrNum(value: string) {
// 英文或者数字
const reg = /^[0-9a-zA-Z]*$/g
return reg.test(value)
},
/**
* 验证是否包含某个值
*/
contains(value: string, param: string) {
return value.indexOf(param) >= 0
},
/**
* 验证一个值范围[min, max]
*/
range(value: number, param: number[]) {
return value >= param[0] && value <= param[1]
},
/**
* 验证一个长度范围[min, max]
*/
rangeLength(value: string, param: number[]) {
return value.length >= param[0] && value.length <= param[1]
},
/**
* 是否固定电话
*/
landline(value: string) {
const reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/
return reg.test(value)
},
/**
* 判断是否为空
*/
empty(value: any) {
switch (typeof value) {
case 'undefined':
return true
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true
break
case 'boolean':
if (!value) return true
break
case 'number':
if (value === 0 || isNaN(value)) return true
break
case 'object':
if (value === null || value.length === 0) return true
for (const i in value) {
return false
}
return true
}
return false
},
/**
* 是否json字符串
*/
jsonString(value: object) {
if (typeof value === 'string') {
try {
const obj = JSON.parse(value)
if (typeof obj === 'object' && obj) {
return true
}
return false
} catch (e) {
return false
}
}
return false
},
/**
* 是否数组
*/
array(value: []) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
},
/**
* 是否对象
*/
object(value: object) {
return Object.prototype.toString.call(value) === '[object Object]'
},
/**
* 是否短信验证码
*/
code(value: string, len = 6) {
return new RegExp(`^\\d{${len}}$`).test(value)
},
/**
* 是否函数方法
* @param {Object} value
*/
func(value: string) {
return typeof value === 'function'
},
/**
* 是否promise对象
* @param {Object} value
*/
promise(value: Promise<any>) {
return this.object(value) && this.func(value.then) && this.func(value.catch)
},
/** 是否图片格式
* @param {Object} value
*/
image(value: string) {
const newValue = value.split('?')[0]
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i
return IMAGE_REGEXP.test(newValue)
},
/**
* 是否视频格式
* @param {Object} value
*/
video(value: string) {
const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i
return VIDEO_REGEXP.test(value)
},
/**
* 是否为正则对象
* @param o
*/
regExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]'
}
}
export default validate