项目创建

This commit is contained in:
浅念
2025-07-04 17:16:38 +08:00
parent ce47b39fa5
commit 719911918f
372 changed files with 105035 additions and 32 deletions
+44
View File
@@ -0,0 +1,44 @@
/**
* 浏览器对象模型 (BOM) 相关工具函数
*/
// 当前网页地址
export function currentURL(): string {
return window.location.href
}
// 获取滚动条位置
export function getScrollPosition(el: HTMLElement | Window = window): { x: number; y: number } {
return el === window
? {
x: window.scrollX || document.documentElement.scrollLeft,
y: window.scrollY || document.documentElement.scrollTop
}
: {
x: (el as HTMLElement).scrollLeft,
y: (el as HTMLElement).scrollTop
}
}
// 获取 URL 参数
export function getURLParameters(url: string): Record<string, string> {
return Object.fromEntries(new URLSearchParams(url.split('?')[1]).entries())
}
// 复制文本
export function copy(str: string): boolean {
try {
navigator.clipboard.writeText(str)
return true
} catch (err) {
console.error('Copy failed:', err)
return false
}
}
// 检测设备类型
export function detectDeviceType(): 'Mobile' | 'Desktop' {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
? 'Mobile'
: 'Desktop'
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Cookie 操作相关工具函数
*/
// 设置 Cookie
export function setCookie(key: string, value: string, expireDays: number): void {
const date = new Date()
date.setDate(date.getDate() + expireDays)
document.cookie = `${key}=${encodeURIComponent(value)}${
expireDays ? `;expires=${date.toUTCString()}` : ''
}`
}
// 删除 Cookie
export function delCookie(name: string): void {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`
}
// 获取 Cookie
export function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
return match ? decodeURIComponent(match[1]) : null
}
+6
View File
@@ -0,0 +1,6 @@
/**
* 浏览器相关工具函数统一导出
*/
export * from './bom'
export * from './cookie'
+66
View File
@@ -0,0 +1,66 @@
// 提取 iconfont 图标
export interface IconfontType {
className: string
unicode?: string
}
interface StyleSheetError {
sheet: CSSStyleSheet
error: unknown
}
function extractIconFromRule(rule: CSSRule): IconfontType | null {
if (!(rule instanceof CSSStyleRule)) return null
const { selectorText, style } = rule
if (!selectorText?.startsWith('.iconsys-') || !selectorText.includes('::before')) return null
const className = selectorText.substring(1).replace('::before', '')
const content = style.getPropertyValue('content')
if (!content) return null
const unicode = content.replace(/['"\\]/g, '')
return {
className,
unicode: unicode ? `&#x${getUnicode(unicode)};` : undefined
}
}
const processedErrors = new Set<StyleSheetError>()
export function extractIconClasses(): IconfontType[] {
const iconInfos: IconfontType[] = []
try {
Array.from(document.styleSheets).forEach((sheet) => {
try {
const rules = Array.from(sheet.cssRules || sheet.rules)
rules.forEach((rule) => {
const iconInfo = extractIconFromRule(rule)
if (iconInfo) {
iconInfos.push(iconInfo)
}
})
} catch (error) {
const styleSheetError: StyleSheetError = { sheet, error }
if (!processedErrors.has(styleSheetError)) {
console.warn('Cannot read cssRules from stylesheet:', {
error,
sheetHref: sheet.href
})
processedErrors.add(styleSheetError)
}
}
})
} catch (error) {
console.error('Failed to process stylesheets:', error)
}
return iconInfos
}
export function getUnicode(charCode: string): string {
if (!charCode) return ''
return charCode.charCodeAt(0).toString(16).padStart(4, '0')
}
+6
View File
@@ -0,0 +1,6 @@
/**
* 常量定义相关工具函数统一导出
*/
export * from './links'
export * from './iconfont'
+22
View File
@@ -0,0 +1,22 @@
export const WEB_LINKS = {
// Github 主页
GITHUB_HOME: 'https://github.com/Daymychen',
// 项目 Github 主页
GITHUB: 'https://github.com/Daymychen/art-design-pro',
// 个人博客
BLOG: 'https://www.lingchen.kim',
// 项目文档
DOCS: 'https://www.lingchen.kim/art-design-pro/docs',
// 项目社区
COMMUNITY: 'https://www.lingchen.kim/art-design-pro/docs/zh/community/communicate.html',
// 个人 Bilibili 主页
BILIBILI: 'https://space.bilibili.com/425500936?spm_id_from=333.1007.0.0',
// 项目介绍
INTRODUCE: 'https://www.lingchen.kim/art-design-pro/docs/guide/introduce.html'
}
+77
View File
@@ -0,0 +1,77 @@
/**
* 数组相关工具函数
*/
// 数组去重
export function noRepeat<T>(arr: T[]): T[] {
return [...new Set(arr)]
}
// 查找数组最大值
export function arrayMax(arr: number[]): number {
if (!arr.length) throw new Error('Array is empty')
return Math.max(...arr)
}
// 查找数组最小值
export function arrayMin(arr: number[]): number {
if (!arr.length) throw new Error('Array is empty')
return Math.min(...arr)
}
// 数组分割
export function chunk<T>(arr: T[], size: number = 1): T[][] {
if (size <= 0) return [arr.slice()]
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
)
}
// 检查元素出现次数
export function countOccurrences<T>(arr: T[], value: T): number {
return arr.reduce((count, current) => (current === value ? count + 1 : count), 0)
}
// 扁平化数组
export function flatten<T>(arr: any[], depth: number = Infinity): T[] {
return arr.flat(depth)
}
// 返回两个数组的差集
export function difference<T>(arrA: T[], arrB: T[]): T[] {
const setB = new Set(arrB)
return arrA.filter((item) => !setB.has(item))
}
// 返回两个数组的交集
export function intersection<T>(arr1: T[], arr2: T[]): T[] {
const set2 = new Set(arr2)
return arr1.filter((item) => set2.has(item))
}
// 从右删除 n 个元素
export function dropRight<T>(arr: T[], n: number = 0): T[] {
return arr.slice(0, Math.max(0, arr.length - n))
}
// 返回间隔 nth 的元素
export function everyNth<T>(arr: T[], nth: number): T[] {
if (nth <= 0) return []
return arr.filter((_, i) => i % nth === nth - 1)
}
// 返回第 n 个元素
export function nthElement<T>(arr: T[], n: number = 0): T | undefined {
const index = n >= 0 ? n : arr.length + n
return arr[index]
}
// 数组乱序
export function shuffle<T>(arr: T[]): T[] {
const array = [...arr] // 创建新数组避免修改原数组
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
+28
View File
@@ -0,0 +1,28 @@
/**
* 数据格式化相关工具函数
*/
// 时间戳转时间
export function timestampToTime(timestamp: number = Date.now(), isMs: boolean = true): string {
const date = new Date(isMs ? timestamp : timestamp * 1000)
return date.toISOString().replace('T', ' ').slice(0, 19)
}
// 数字格式化(千位分隔符)
export function commafy(num: number): string {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 生成随机数
export function randomNum(min: number, max?: number): number {
if (max === undefined) {
max = min
min = 0
}
return Math.floor(Math.random() * (max - min + 1)) + min
}
// 移除HTML标签
export function removeHtmlTags(str: string = ''): string {
return str.replace(/<[^>]*>/g, '')
}
+6
View File
@@ -0,0 +1,6 @@
/**
* 数据处理相关工具函数统一导出
*/
export * from './array'
export * from './format'
+139
View File
@@ -0,0 +1,139 @@
import { AxiosError } from 'axios'
import { ElMessage } from 'element-plus'
import { ApiStatus } from './status'
import { $t } from '@/locales'
// 错误响应接口
export interface ErrorResponse {
code: number
msg: string
data?: unknown
}
// 错误日志数据接口
export interface ErrorLogData {
code: number
message: string
data?: unknown
timestamp: string
url?: string
method?: string
stack?: string
}
// 自定义 HttpError 类
export class HttpError extends Error {
public readonly code: number
public readonly data?: unknown
public readonly timestamp: string
public readonly url?: string
public readonly method?: string
constructor(
message: string,
code: number,
options?: {
data?: unknown
url?: string
method?: string
}
) {
super(message)
this.name = 'HttpError'
this.code = code
this.data = options?.data
this.timestamp = new Date().toISOString()
this.url = options?.url
this.method = options?.method
}
public toLogData(): ErrorLogData {
return {
code: this.code,
message: this.message,
data: this.data,
timestamp: this.timestamp,
url: this.url,
method: this.method,
stack: this.stack
}
}
}
/**
* 获取错误消息
* @param status 错误状态码
* @returns 错误消息
*/
const getErrorMessage = (status: number): string => {
const errorMap: Record<number, string> = {
[ApiStatus.unauthorized]: 'httpMsg.unauthorized',
[ApiStatus.forbidden]: 'httpMsg.forbidden',
[ApiStatus.notFound]: 'httpMsg.notFound',
[ApiStatus.methodNotAllowed]: 'httpMsg.methodNotAllowed',
[ApiStatus.requestTimeout]: 'httpMsg.requestTimeout',
[ApiStatus.internalServerError]: 'httpMsg.internalServerError',
[ApiStatus.badGateway]: 'httpMsg.badGateway',
[ApiStatus.serviceUnavailable]: 'httpMsg.serviceUnavailable',
[ApiStatus.gatewayTimeout]: 'httpMsg.gatewayTimeout'
}
return $t(errorMap[status] || 'httpMsg.internalServerError')
}
/**
* 处理错误
* @param error 错误对象
* @returns 错误对象
*/
export function handleError(error: AxiosError<ErrorResponse>): never {
// 处理取消的请求
if (error.code === 'ERR_CANCELED') {
console.warn('Request cancelled:', error.message)
throw new HttpError($t('httpMsg.requestCancelled'), ApiStatus.error)
}
const statusCode = error.response?.status
const errorMessage = error.response?.data?.msg || error.message
const requestConfig = error.config
// 处理网络错误
if (!error.response) {
throw new HttpError($t('httpMsg.networkError'), ApiStatus.error, {
url: requestConfig?.url,
method: requestConfig?.method?.toUpperCase()
})
}
// 处理 HTTP 状态码错误
const message = statusCode
? getErrorMessage(statusCode)
: errorMessage || $t('httpMsg.requestFailed')
throw new HttpError(message, statusCode || ApiStatus.error, {
data: error.response.data,
url: requestConfig?.url,
method: requestConfig?.method?.toUpperCase()
})
}
/**
* 显示错误消息
* @param error 错误对象
* @param showMessage 是否显示错误消息
*/
export function showError(error: HttpError, showMessage: boolean = true): void {
if (showMessage) {
ElMessage.error(error.message)
}
// 记录错误日志
console.error('[HTTP Error]', error.toLogData())
}
/**
* 判断是否为 HttpError 类型
* @param error 错误对象
* @returns 是否为 HttpError 类型
*/
export const isHttpError = (error: unknown): error is HttpError => {
return error instanceof HttpError
}
+164
View File
@@ -0,0 +1,164 @@
import axios, { InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/modules/user'
import { ApiStatus } from './status'
import { HttpError, handleError, showError } from './error'
import { $t } from '@/locales'
// 常量定义
const REQUEST_TIMEOUT = 15000 // 请求超时时间(毫秒)
const LOGOUT_DELAY = 1000 // 退出登录延迟时间(毫秒)
const MAX_RETRIES = 2 // 最大重试次数
const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)
// 扩展 AxiosRequestConfig 类型
interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
showErrorMessage?: boolean
}
const axiosInstance = axios.create({
timeout: REQUEST_TIMEOUT, // 请求超时时间(毫秒)
baseURL: import.meta.env.VITE_API_URL, // API地址
withCredentials: true, // 异步请求携带cookie
transformRequest: [(data) => JSON.stringify(data)], // 请求数据转换为 JSON 字符串
validateStatus: (status) => status >= 200 && status < 300, // 只接受 2xx 的状态码
headers: {
get: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' },
post: { 'Content-Type': 'application/json;charset=utf-8' }
},
transformResponse: [
(data, headers) => {
const contentType = headers['content-type']
if (contentType && contentType.includes('application/json')) {
try {
return JSON.parse(data)
} catch {
return data
}
}
return data
}
]
})
// 请求拦截器
axiosInstance.interceptors.request.use(
(request: InternalAxiosRequestConfig) => {
const { accessToken } = useUserStore()
const { language } = useUserStore()
request.headers.set('lang', language)
// 设置 token 和 请求头
if (accessToken) {
request.headers.set('Authorization', accessToken)
request.headers.set('Content-Type', 'application/json')
}
return request
},
(error) => {
showError(new HttpError($t('httpMsg.requestConfigError'), ApiStatus.error))
return Promise.reject(error)
}
)
// 响应拦截器
axiosInstance.interceptors.response.use(
(response: AxiosResponse<Api.Http.BaseResponse>) => {
const { code, msg } = response.data
switch (code) {
case ApiStatus.success:
return response
case ApiStatus.unauthorized:
logOut()
throw new HttpError(msg || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
default:
throw new HttpError(msg || $t('httpMsg.requestFailed'), code)
}
},
(error) => {
return Promise.reject(handleError(error))
}
)
// 请求重试函数
async function retryRequest<T>(
config: ExtendedAxiosRequestConfig,
retries: number = MAX_RETRIES
): Promise<T> {
try {
return await request<T>(config)
} catch (error) {
if (retries > 0 && error instanceof HttpError && shouldRetry(error.code)) {
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY))
return retryRequest<T>(config, retries - 1)
}
throw error
}
}
// 判断是否需要重试
function shouldRetry(statusCode: number): boolean {
return [
ApiStatus.requestTimeout,
ApiStatus.internalServerError,
ApiStatus.badGateway,
ApiStatus.serviceUnavailable,
ApiStatus.gatewayTimeout
].includes(statusCode)
}
// 请求函数
async function request<T = any>(config: ExtendedAxiosRequestConfig): Promise<T> {
// 对 POST | PUT 请求特殊处理
if (config.method?.toUpperCase() === 'POST' || config.method?.toUpperCase() === 'PUT') {
if (config.params && !config.data) {
config.data = config.params
config.params = undefined
}
}
try {
const res = await axiosInstance.request<Api.Http.BaseResponse<T>>(config)
if (config.method?.toUpperCase() === 'POST' || config.method?.toUpperCase() === 'PUT') {
return res.data as T
}else{
return res.data.data as T
}
} catch (error) {
if (error instanceof HttpError) {
// 根据配置决定是否显示错误消息
const showErrorMessage = config.showErrorMessage !== false
showError(error, showErrorMessage)
}
return Promise.reject(error)
}
}
// API 方法集合
const api = {
get<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
return retryRequest<T>({ ...config, method: 'GET' })
},
post<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
return retryRequest<T>({ ...config, method: 'POST' })
},
put<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
return retryRequest<T>({ ...config, method: 'PUT' })
},
del<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
return retryRequest<T>({ ...config, method: 'DELETE' })
},
request<T>(config: ExtendedAxiosRequestConfig): Promise<T> {
return retryRequest<T>({ ...config })
}
}
// 退出登录函数
const logOut = (): void => {
setTimeout(() => {
useUserStore().logOut()
}, LOGOUT_DELAY)
}
export default api
+19
View File
@@ -0,0 +1,19 @@
/**
* 接口状态码
*/
export enum ApiStatus {
// success = 200, // 成功
success = 0, // 成功
error = 400, // 错误
unauthorized = 401, // 未授权
forbidden = 403, // 禁止访问
notFound = 404, // 未找到
methodNotAllowed = 405, // 方法不允许
requestTimeout = 408, // 请求超时
internalServerError = 500, // 服务器错误
notImplemented = 501, // 未实现
badGateway = 502, // 网关错误
serviceUnavailable = 503, // 服务不可用
gatewayTimeout = 504, // 网关超时
httpVersionNotSupported = 505 // HTTP版本不支持
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Utils 工具函数统一导出
* 提供向后兼容性和便捷导入
*/
// UI 相关
export * from './ui'
// 浏览器相关
export * from './browser'
// 数据处理相关
export * from './dataprocess'
// 路由导航相关
export * from './navigation'
// 系统管理相关
export * from './sys'
// 常量定义相关
export * from './constants'
// 存储相关
export * from './storage'
// 主题相关
export * from './theme'
// HTTP 相关
export * from './http'
// 验证相关
export * from './validation'
+7
View File
@@ -0,0 +1,7 @@
/**
* 路由和导航相关工具函数统一导出
*/
export * from './jump'
export * from './worktab'
export * from './route'
+46
View File
@@ -0,0 +1,46 @@
import { AppRouteRecord } from '@/types/router'
import { router } from '@/router'
// 打开外部链接
export const openExternalLink = (link: string) => {
window.open(link, '_blank')
}
/**
* 菜单跳转
* @param item 菜单项
* @param jumpToFirst 是否跳转到第一个子菜单
* @returns
*/
export const handleMenuJump = (item: AppRouteRecord, jumpToFirst: boolean = false) => {
// 处理外部链接
const { link, isIframe } = item.meta
if (link && !isIframe) {
return openExternalLink(link)
}
// 如果不需要跳转到第一个子菜单,或者没有子菜单,直接跳转当前路径
if (!jumpToFirst || !item.children?.length) {
return router.push(item.path)
}
// 递归查找第一个可见的叶子节点菜单
const findFirstLeafMenu = (items: AppRouteRecord[]): AppRouteRecord => {
for (const child of items) {
if (!child.meta.isHide) {
return child.children?.length ? findFirstLeafMenu(child.children) : child
}
}
return items[0]
}
const firstChild = findFirstLeafMenu(item.children)
// 如果第一个子菜单是外部链接则打开新窗口
if (firstChild.meta?.link) {
return openExternalLink(firstChild.meta.link)
}
// 跳转到子菜单路径
router.push(firstChild.path)
}
+8
View File
@@ -0,0 +1,8 @@
/**
* 路由相关工具函数
*/
// 检查是否为 iframe 路由
export function isIframe(url: string): boolean {
return url.startsWith('/iframe/')
}
+42
View File
@@ -0,0 +1,42 @@
import { useWorktabStore } from '@/store/modules/worktab'
import { RouteLocationNormalized } from 'vue-router'
import { isIframe } from './route'
import { useSettingStore } from '@/store/modules/setting'
import { HOME_PAGE } from '@/router/routesAlias'
import { getIframeRoutes } from '@/router/utils/menuToRouter'
/**
* 根据当前路由信息设置工作标签页(worktab)
* @param to 当前路由对象
*/
export const setWorktab = (to: RouteLocationNormalized): void => {
const worktabStore = useWorktabStore()
const { meta, path, name, params, query } = to
if (!meta.isHideTab) {
// 如果是 iframe 页面,则特殊处理工作标签页
if (isIframe(path)) {
const iframeRoute = getIframeRoutes().find((route: any) => route.path === to.path)
if (iframeRoute?.meta) {
worktabStore.openTab({
title: iframeRoute.meta.title,
path,
name: name as string,
keepAlive: meta.keepAlive as boolean,
params,
query
})
}
} else if (useSettingStore().showWorkTab || path === HOME_PAGE) {
worktabStore.openTab({
title: meta.title as string,
path,
name: name as string,
keepAlive: meta.keepAlive as boolean,
params,
query,
fixedTab: meta.fixedTab as boolean
})
}
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* 存储相关工具函数统一导出
*/
export * from './storage'
export * from './storage-config'
export * from './storage-key-manager'
+92
View File
@@ -0,0 +1,92 @@
/**
* 存储配置管理
* 统一管理存储相关的常量和配置项
*/
export class StorageConfig {
/** 当前应用版本 */
static readonly CURRENT_VERSION = __APP_VERSION__
/** 存储键前缀 */
static readonly STORAGE_PREFIX = 'sys-v'
/** 版本键名 */
static readonly VERSION_KEY = 'sys-version'
/** 跳过升级检查的版本 */
static readonly SKIP_UPGRADE_VERSION = '1.0.0'
/** 升级处理延迟时间(毫秒) */
static readonly UPGRADE_DELAY = 1000
/** 登出延迟时间(毫秒) */
static readonly LOGOUT_DELAY = 1000
/**
* 生成版本化的存储键名
* @param storeId 存储ID
* @param version 版本号,默认使用当前版本
*/
static generateStorageKey(storeId: string, version: string = this.CURRENT_VERSION): string {
return `${this.STORAGE_PREFIX}${version}-${storeId}`
}
/**
* 生成旧版本的存储键名(不带分隔符)
* @param version 版本号,默认使用当前版本
*/
static generateLegacyKey(version: string = this.CURRENT_VERSION): string {
return `${this.STORAGE_PREFIX}${version}`
}
/**
* 创建存储键匹配的正则表达式
* @param storeId 存储ID
*/
static createKeyPattern(storeId: string): RegExp {
return new RegExp(`^${this.STORAGE_PREFIX}[^-]+-${storeId}$`)
}
/**
* 创建当前版本存储键匹配的正则表达式
*/
static createCurrentVersionPattern(): RegExp {
return new RegExp(`^${this.STORAGE_PREFIX}${this.CURRENT_VERSION}-`)
}
/**
* 创建任意版本存储键匹配的正则表达式
*/
static createVersionPattern(): RegExp {
return new RegExp(`^${this.STORAGE_PREFIX}`)
}
/**
* 检查是否为当前版本的键
*/
static isCurrentVersionKey(key: string): boolean {
return key.startsWith(`${this.STORAGE_PREFIX}${this.CURRENT_VERSION}`)
}
/**
* 检查是否为版本化的键
*/
static isVersionedKey(key: string): boolean {
return key.startsWith(this.STORAGE_PREFIX)
}
/**
* 从存储键中提取版本号
*/
static extractVersionFromKey(key: string): string | null {
const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}([^-]+)`))
return match ? match[1] : null
}
/**
* 从存储键中提取存储ID
*/
static extractStoreIdFromKey(key: string): string | null {
const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}[^-]+-(.+)$`))
return match ? match[1] : null
}
}
+66
View File
@@ -0,0 +1,66 @@
import { StorageConfig } from '@/utils/storage'
/**
* 存储键名管理器
* 负责处理版本化的存储键名生成和数据迁移
*/
export class StorageKeyManager {
/**
* 获取当前版本的存储键名
*/
private getCurrentVersionKey(storeId: string): string {
return StorageConfig.generateStorageKey(storeId)
}
/**
* 检查当前版本的数据是否存在
*/
private hasCurrentVersionData(key: string): boolean {
return localStorage.getItem(key) !== null
}
/**
* 查找其他版本的同名存储键
*/
private findExistingKey(storeId: string): string | null {
const storageKeys = Object.keys(localStorage)
const pattern = StorageConfig.createKeyPattern(storeId)
return storageKeys.find((key) => pattern.test(key) && localStorage.getItem(key)) || null
}
/**
* 将数据从旧版本迁移到当前版本
*/
private migrateData(fromKey: string, toKey: string): void {
try {
const existingData = localStorage.getItem(fromKey)
if (existingData) {
localStorage.setItem(toKey, existingData)
console.info(`[Storage] 已迁移数据: ${fromKey} → ${toKey}`)
}
} catch (error) {
console.warn(`[Storage] 数据迁移失败: ${fromKey}`, error)
}
}
/**
* 获取持久化存储的键名(支持自动数据迁移)
*/
getStorageKey(storeId: string): string {
const currentKey = this.getCurrentVersionKey(storeId)
// 优先使用当前版本的数据
if (this.hasCurrentVersionData(currentKey)) {
return currentKey
}
// 查找并迁移其他版本的数据
const existingKey = this.findExistingKey(storeId)
if (existingKey) {
this.migrateData(existingKey, currentKey)
}
return currentKey
}
}
+216
View File
@@ -0,0 +1,216 @@
import { ElMessage } from 'element-plus'
import { router } from '@/router'
import { useUserStore } from '@/store/modules/user'
import { StorageConfig } from '@/utils/storage/storage-config'
import { RoutesAlias } from '@/router/routesAlias'
/**
* 存储兼容性管理器
* 负责处理不同版本间的存储兼容性检查和数据验证
*/
class StorageCompatibilityManager {
/**
* 获取系统版本号
*/
getSystemVersion(): string | null {
return localStorage.getItem(StorageConfig.VERSION_KEY)
}
/**
* 获取系统存储数据(兼容旧格式)
*/
getSystemStorage(): any {
const version = this.getSystemVersion() || StorageConfig.CURRENT_VERSION
const legacyKey = StorageConfig.generateLegacyKey(version)
const data = localStorage.getItem(legacyKey)
return data ? JSON.parse(data) : null
}
/**
* 检查当前版本是否有存储数据
*/
private hasCurrentVersionStorage(): boolean {
const storageKeys = Object.keys(localStorage)
const currentVersionPattern = StorageConfig.createCurrentVersionPattern()
return storageKeys.some(
(key) => currentVersionPattern.test(key) && localStorage.getItem(key) !== null
)
}
/**
* 检查是否存在任何版本的存储数据
*/
private hasAnyVersionStorage(): boolean {
const storageKeys = Object.keys(localStorage)
const versionPattern = StorageConfig.createVersionPattern()
return storageKeys.some((key) => versionPattern.test(key) && localStorage.getItem(key) !== null)
}
/**
* 获取旧格式的本地存储数据
*/
private getLegacyStorageData(): Record<string, any> {
try {
const systemStorage = this.getSystemStorage()
return systemStorage || {}
} catch (error) {
console.warn('[Storage] 解析旧格式存储数据失败:', error)
return {}
}
}
/**
* 显示存储错误消息
*/
private showStorageError(): void {
ElMessage({
type: 'error',
offset: 40,
duration: 5000,
message: '系统检测到本地数据异常,请重新登录系统恢复使用!'
})
}
/**
* 执行系统登出
*/
private performSystemLogout(): void {
setTimeout(() => {
try {
localStorage.clear()
useUserStore().logOut()
router.push(RoutesAlias.Login)
console.info('[Storage] 已执行系统登出')
} catch (error) {
console.error('[Storage] 系统登出失败:', error)
}
}, StorageConfig.LOGOUT_DELAY)
}
/**
* 处理存储异常
*/
private handleStorageError(): void {
this.showStorageError()
this.performSystemLogout()
}
/**
* 检查是否在登录页面
*/
private isOnLoginPage(): boolean {
return location.href.includes(RoutesAlias.Login)
}
/**
* 验证存储数据完整性
*/
validateStorageData(): boolean {
// 如果在登录页面,跳过验证
if (this.isOnLoginPage()) {
return true
}
try {
// 优先检查新版本存储结构
if (this.hasCurrentVersionStorage()) {
console.debug('[Storage] 发现当前版本存储数据')
return true
}
// 检查是否有任何版本的存储数据
if (this.hasAnyVersionStorage()) {
console.debug('[Storage] 发现其他版本存储数据,可能需要迁移')
return true
}
// 检查旧版本存储结构
const legacyData = this.getLegacyStorageData()
if (Object.keys(legacyData).length === 0) {
console.warn('[Storage] 未发现任何存储数据,需要重新登录')
this.performSystemLogout()
return false
}
console.debug('[Storage] 发现旧版本存储数据')
return true
} catch (error) {
console.error('[Storage] 存储数据验证失败:', error)
this.handleStorageError()
return false
}
}
/**
* 检查存储是否为空
*/
isStorageEmpty(): boolean {
// 检查新版本存储结构
if (this.hasCurrentVersionStorage()) {
return false
}
// 检查是否有任何版本的存储数据
if (this.hasAnyVersionStorage()) {
return false
}
// 检查旧版本存储结构
const legacyData = this.getLegacyStorageData()
return Object.keys(legacyData).length === 0
}
/**
* 检查存储兼容性
*/
checkCompatibility(): boolean {
try {
const isValid = this.validateStorageData()
const isEmpty = this.isStorageEmpty()
if (isValid || isEmpty) {
console.debug('[Storage] 存储兼容性检查通过')
return true
}
console.warn('[Storage] 存储兼容性检查失败')
return false
} catch (error) {
console.error('[Storage] 兼容性检查异常:', error)
return false
}
}
}
// 创建存储兼容性管理器实例
const storageManager = new StorageCompatibilityManager()
/**
* 获取系统存储数据
*/
export function getSystemStorage(): any {
return storageManager.getSystemStorage()
}
/**
* 获取系统版本号
*/
export function getSysVersion(): string | null {
return storageManager.getSystemVersion()
}
/**
* 验证本地存储数据
*/
export function validateStorageData(): boolean {
return storageManager.validateStorageData()
}
/**
* 检查存储兼容性
*/
export function checkStorageCompatibility(): boolean {
return storageManager.checkCompatibility()
}
+21
View File
@@ -0,0 +1,21 @@
// ANSI 转义码生成网站 https://patorjk.com/software/taag/#p=display&f=Big&t=ABB%0A
const asciiArt = `
\x1b[32m✨ 你好!欢迎使用 Art Design Pro!
\x1b[0m
\x1b[36m💡 如果您觉得项目对您有帮助,请点击下面的链接为我点个 ★Star 支持一下!祝您使用愉快!
\x1b[0m
\x1b[33m🌟 GitHub: https://github.com/Daymychen/art-design-pro
\x1b[0m
\x1b[31m✨ 技术支持(QQ群): 821834289,如果你有任何问题,请加入QQ群,我们会在第一时间为你解答!
\x1b[0m
\x1b[36m _ _______ _________ ______ ________ ______ _____ ______ ____ _____ _______ _______ ___
\x1b[36m / \\ |_ __ \\ | _ _ | |_ _ \`.|_ __ |.' ____ \\ |_ _|.' ___ ||_ \\|_ _| |_ __ \\|_ __ \\ .' \`.
\x1b[36m / _ \\ | |__) ||_/ | | \\_| | | \`. \\ | |_ \\_|| (___ \\_| | | / .' \\_| | \\ | | | |__) | | |__) | / .-. \\
\x1b[36m / ___ \\ | __ / | | | | | | | _| _ _.____\`. | | | | ____ | |\\ \\| | | ___/ | __ / | | | |
\x1b[36m _/ / \\ \\_ _| | \\ \\_ _| |_ _| |_.' /_| |__/ || \\____) | _| |_\\ \`.___] |_| |_\\ |_ _| |_ _| | \\ \\_\\ \`-' /
\x1b[36m|____| |____||____| |___||_____| |______.'|________| \\______.'|_____|\\\`._____.'|_____|\\____| |_____| |____| |___|\\\`.___.'
\x1b[0m
`
console.log(asciiArt)
+6
View File
@@ -0,0 +1,6 @@
/**
* 系统管理相关工具函数统一导出
*/
export * from './upgrade'
export { default as mittBus } from './mittBus'
+9
View File
@@ -0,0 +1,9 @@
/**
* 全局事件总线,用于全局事件的发布与订阅
* 用法:
* mittBus.on('event', callback)
* mittBus.emit('event', data)
*/
import mitt from 'mitt'
const mittBus = mitt()
export default mittBus
+241
View File
@@ -0,0 +1,241 @@
import { upgradeLogList } from '@/mock/upgrade/changeLog'
import { ElNotification } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { StorageConfig } from '@/utils/storage/storage-config'
/**
* 版本管理器
* 负责处理版本比较、升级检测和数据清理
*/
class VersionManager {
/**
* 规范化版本号字符串,移除前缀 'v'
*/
private normalizeVersion(version: string): string {
return version.replace(/^v/, '')
}
/**
* 获取存储的版本号
*/
private getStoredVersion(): string | null {
return localStorage.getItem(StorageConfig.VERSION_KEY)
}
/**
* 设置版本号到存储
*/
private setStoredVersion(version: string): void {
localStorage.setItem(StorageConfig.VERSION_KEY, version)
}
/**
* 检查是否应该跳过升级处理
*/
private shouldSkipUpgrade(): boolean {
return StorageConfig.CURRENT_VERSION === StorageConfig.SKIP_UPGRADE_VERSION
}
/**
* 检查是否为首次访问
*/
private isFirstVisit(storedVersion: string | null): boolean {
return !storedVersion
}
/**
* 检查版本是否相同
*/
private isSameVersion(storedVersion: string): boolean {
return storedVersion === StorageConfig.CURRENT_VERSION
}
/**
* 查找旧的存储结构
*/
private findLegacyStorage(): { oldSysKey: string | null; oldVersionKeys: string[] } {
const storageKeys = Object.keys(localStorage)
const currentVersionPrefix = StorageConfig.generateStorageKey('').slice(0, -1) // 移除末尾的 '-'
// 查找旧的单一存储结构
const oldSysKey =
storageKeys.find(
(key) =>
StorageConfig.isVersionedKey(key) && key !== currentVersionPrefix && !key.includes('-')
) || null
// 查找旧版本的分离存储键
const oldVersionKeys = storageKeys.filter(
(key) =>
StorageConfig.isVersionedKey(key) &&
!StorageConfig.isCurrentVersionKey(key) &&
key.includes('-')
)
return { oldSysKey, oldVersionKeys }
}
/**
* 检查是否需要重新登录
*/
private shouldRequireReLogin(storedVersion: string): boolean {
const normalizedCurrent = this.normalizeVersion(StorageConfig.CURRENT_VERSION)
const normalizedStored = this.normalizeVersion(storedVersion)
return upgradeLogList.value.some((item) => {
const itemVersion = this.normalizeVersion(item.version)
return (
item.requireReLogin && itemVersion > normalizedStored && itemVersion <= normalizedCurrent
)
})
}
/**
* 构建升级通知消息
*/
private buildUpgradeMessage(requireReLogin: boolean): string {
const { title: content } = upgradeLogList.value[0]
const messageParts = [
`<p style="color: var(--art-gray-text-800) !important; padding-bottom: 5px;">`,
`系统已升级到 ${StorageConfig.CURRENT_VERSION} 版本,此次更新带来了以下改进:`,
`</p>`,
content
]
if (requireReLogin) {
messageParts.push(
`<p style="color: var(--main-color); padding-top: 5px;">升级完成,请重新登录后继续使用。</p>`
)
}
return messageParts.join('')
}
/**
* 显示升级通知
*/
private showUpgradeNotification(message: string): void {
ElNotification({
title: '系统升级公告',
message,
duration: 0,
type: 'success',
dangerouslyUseHTMLString: true
})
}
/**
* 清理旧版本数据
*/
private cleanupLegacyData(oldSysKey: string | null, oldVersionKeys: string[]): void {
// 清理旧的单一存储结构
if (oldSysKey) {
localStorage.removeItem(oldSysKey)
console.info(`[Upgrade] 已清理旧存储: ${oldSysKey}`)
}
// 清理旧版本的分离存储
oldVersionKeys.forEach((key) => {
localStorage.removeItem(key)
console.info(`[Upgrade] 已清理旧存储: ${key}`)
})
}
/**
* 执行升级后的登出操作
*/
private performLogout(): void {
try {
useUserStore().logOut()
console.info('[Upgrade] 已执行升级后登出')
} catch (error) {
console.error('[Upgrade] 升级后登出失败:', error)
}
}
/**
* 执行升级流程
*/
private async executeUpgrade(
storedVersion: string,
legacyStorage: ReturnType<typeof this.findLegacyStorage>
): Promise<void> {
try {
if (!upgradeLogList.value.length) {
console.warn('[Upgrade] 升级日志列表为空')
return
}
const requireReLogin = this.shouldRequireReLogin(storedVersion)
const message = this.buildUpgradeMessage(requireReLogin)
// 显示升级通知
this.showUpgradeNotification(message)
// 更新版本号
this.setStoredVersion(StorageConfig.CURRENT_VERSION)
// 清理旧数据
this.cleanupLegacyData(legacyStorage.oldSysKey, legacyStorage.oldVersionKeys)
// 执行登出(如果需要)
if (requireReLogin) {
this.performLogout()
}
console.info(`[Upgrade] 升级完成: ${storedVersion} → ${StorageConfig.CURRENT_VERSION}`)
} catch (error) {
console.error('[Upgrade] 系统升级处理失败:', error)
}
}
/**
* 系统升级处理主流程
*/
async processUpgrade(): Promise<void> {
// 跳过特定版本
if (this.shouldSkipUpgrade()) {
console.debug('[Upgrade] 跳过版本升级检查')
return
}
const storedVersion = this.getStoredVersion()
// 首次访问处理
if (this.isFirstVisit(storedVersion)) {
this.setStoredVersion(StorageConfig.CURRENT_VERSION)
console.info('[Upgrade] 首次访问,已设置当前版本')
return
}
// 版本相同,无需升级
if (this.isSameVersion(storedVersion!)) {
console.debug('[Upgrade] 版本相同,无需升级')
return
}
// 检查是否有需要升级的旧数据
const legacyStorage = this.findLegacyStorage()
if (!legacyStorage.oldSysKey && legacyStorage.oldVersionKeys.length === 0) {
this.setStoredVersion(StorageConfig.CURRENT_VERSION)
console.info('[Upgrade] 无旧数据,已更新版本号')
return
}
// 延迟执行升级流程,确保应用已完全加载
setTimeout(() => {
this.executeUpgrade(storedVersion!, legacyStorage)
}, StorageConfig.UPGRADE_DELAY)
}
}
// 创建版本管理器实例
const versionManager = new VersionManager()
/**
* 系统升级处理入口函数
*/
export async function systemUpgrade(): Promise<void> {
await versionManager.processUpgrade()
}
+51
View File
@@ -0,0 +1,51 @@
import { useCommon } from '@/composables/useCommon'
import { useTheme } from '@/composables/useTheme'
import { SystemThemeEnum } from '@/enums/appEnum'
import { useSettingStore } from '@/store/modules/setting'
const { LIGHT, DARK } = SystemThemeEnum
/**
* 主题切换动画
* @param e 鼠标点击事件
*/
export const themeAnimation = (e: any) => {
const x = e.clientX
const y = e.clientY
// 计算鼠标点击位置距离视窗的最大圆半径
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
// 设置CSS变量
document.documentElement.style.setProperty('--x', x + 'px')
document.documentElement.style.setProperty('--y', y + 'px')
document.documentElement.style.setProperty('--r', endRadius + 'px')
if (document.startViewTransition) {
document.startViewTransition(() => toggleTheme())
} else {
toggleTheme()
}
}
/**
* 切换主题
*/
const toggleTheme = () => {
useTheme().switchThemeStyles(useSettingStore().systemThemeType === LIGHT ? DARK : LIGHT)
useCommon().refresh()
}
/**
* 提升暗黑主题下页面刷新视觉体验
* @param addClass 是否添加 class
*/
export const setThemeTransitionClass = (addClass: boolean) => {
const el = document.getElementsByTagName('body')[0]
if (addClass) {
el.setAttribute('class', 'theme-change')
} else {
setTimeout(() => {
el.removeAttribute('class')
}, 300)
}
}
+5
View File
@@ -0,0 +1,5 @@
/**
* 主题相关工具函数统一导出
*/
export * from './animation'
+231
View File
@@ -0,0 +1,231 @@
import { useSettingStore } from '@/store/modules/setting'
import { ElMessage } from 'element-plus'
/**
* 颜色转换结果接口
*/
interface RgbaResult {
red: number
green: number
blue: number
rgba: string
}
/**
* 获取CSS变量值(别名函数)
* @param name CSS变量名
* @returns CSS变量值
*/
export function getCssVar(name: string): string {
return getComputedStyle(document.documentElement).getPropertyValue(name)
}
/**
* 验证hex颜色格式
* @param hex hex颜色值
* @returns 是否为有效的hex颜色
*/
function isValidHexColor(hex: string): boolean {
const cleanHex = hex.trim().replace(/^#/, '')
return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex)
}
/**
* 验证RGB颜色值
* @param r 红色值
* @param g 绿色值
* @param b 蓝色值
* @returns 是否为有效的RGB值
*/
function isValidRgbValue(r: number, g: number, b: number): boolean {
const isValid = (value: number) => Number.isInteger(value) && value >= 0 && value <= 255
return isValid(r) && isValid(g) && isValid(b)
}
/**
* 将hex颜色转换为RGBA
* @param hex hex颜色值 (支持 #FFF 或 #FFFFFF 格式)
* @param opacity 透明度 (0-1)
* @returns 包含RGB值和RGBA字符串的对象
*/
export function hexToRgba(hex: string, opacity: number): RgbaResult {
if (!isValidHexColor(hex)) {
throw new Error('Invalid hex color format')
}
// 移除可能存在的 # 前缀并转换为大写
let cleanHex = hex.trim().replace(/^#/, '').toUpperCase()
// 如果是缩写形式(如 FFF),转换为完整形式
if (cleanHex.length === 3) {
cleanHex = cleanHex
.split('')
.map((char) => char.repeat(2))
.join('')
}
// 解析 RGB 值
const [red, green, blue] = cleanHex.match(/\w\w/g)!.map((x) => parseInt(x, 16))
// 确保 opacity 在有效范围内
const validOpacity = Math.max(0, Math.min(1, opacity))
// 构建 RGBA 字符串
const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})`
return { red, green, blue, rgba }
}
/**
* 将hex颜色转换为RGB数组
* @param hexColor hex颜色值
* @returns RGB数组 [r, g, b]
*/
export function hexToRgb(hexColor: string): number[] {
if (!isValidHexColor(hexColor)) {
ElMessage.warning('输入错误的hex颜色值')
throw new Error('Invalid hex color format')
}
const cleanHex = hexColor.replace(/^#/, '')
let hex = cleanHex
// 处理缩写形式
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char.repeat(2))
.join('')
}
const hexPairs = hex.match(/../g)
if (!hexPairs) {
throw new Error('Invalid hex color format')
}
return hexPairs.map((hexPair) => parseInt(hexPair, 16))
}
/**
* 将RGB颜色转换为hex
* @param r 红色值 (0-255)
* @param g 绿色值 (0-255)
* @param b 蓝色值 (0-255)
* @returns hex颜色值
*/
export function rgbToHex(r: number, g: number, b: number): string {
if (!isValidRgbValue(r, g, b)) {
ElMessage.warning('输入错误的RGB颜色值')
throw new Error('Invalid RGB color values')
}
const toHex = (value: number) => {
const hex = value.toString(16)
return hex.length === 1 ? `0${hex}` : hex
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
/**
* 颜色混合
* @param color1 第一个颜色
* @param color2 第二个颜色
* @param ratio 混合比例 (0-1)
* @returns 混合后的颜色
*/
export function colourBlend(color1: string, color2: string, ratio: number): string {
const validRatio = Math.max(0, Math.min(1, Number(ratio)))
const rgb1 = hexToRgb(color1)
const rgb2 = hexToRgb(color2)
const blendedRgb = rgb1.map((value1, index) => {
const value2 = rgb2[index]
return Math.round(value1 * (1 - validRatio) + value2 * validRatio)
})
return rgbToHex(blendedRgb[0], blendedRgb[1], blendedRgb[2])
}
/**
* 获取变浅的颜色
* @param color 原始颜色
* @param level 变浅程度 (0-1)
* @param isDark 是否为暗色主题
* @returns 变浅后的颜色
*/
export function getLightColor(color: string, level: number, isDark: boolean = false): string {
if (!isValidHexColor(color)) {
ElMessage.warning('输入错误的hex颜色值')
throw new Error('Invalid hex color format')
}
if (isDark) {
return getDarkColor(color, level)
}
const rgb = hexToRgb(color)
const lightRgb = rgb.map((value) => Math.floor((255 - value) * level + value))
return rgbToHex(lightRgb[0], lightRgb[1], lightRgb[2])
}
/**
* 获取变深的颜色
* @param color 原始颜色
* @param level 变深程度 (0-1)
* @returns 变深后的颜色
*/
export function getDarkColor(color: string, level: number): string {
if (!isValidHexColor(color)) {
ElMessage.warning('输入错误的hex颜色值')
throw new Error('Invalid hex color format')
}
const rgb = hexToRgb(color)
const darkRgb = rgb.map((value) => Math.floor(value * (1 - level)))
return rgbToHex(darkRgb[0], darkRgb[1], darkRgb[2])
}
/**
* 处理 Element Plus 主题颜色
* @param theme 主题颜色
* @param isDark 是否为暗色主题
*/
export function handleElementThemeColor(theme: string, isDark: boolean = false): void {
document.documentElement.style.setProperty('--el-color-primary', theme)
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
getLightColor(theme, i / 10, isDark)
)
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-dark-${i}`,
getDarkColor(theme, i / 10)
)
}
}
/**
* 设置 Element Plus 主题颜色
* @param color 主题颜色
*/
export function setElementThemeColor(color: string): void {
const mixColor = '#ffffff'
const elStyle = document.documentElement.style
elStyle.setProperty('--el-color-primary', color)
handleElementThemeColor(color, useSettingStore().isDark)
// 生成更淡一点的颜色
for (let i = 1; i < 16; i++) {
const itemColor = colourBlend(color, mixColor, i / 16)
elStyle.setProperty(`--el-color-primary-custom-${i}`, itemColor)
}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* 表情
* 用于在消息提示的时候显示对应的表情
*
* 用法
* ElMessage.success(`${EmojiText[200]} 图片上传成功`)
* ElMessage.error(`${EmojiText[400]} 图片上传失败`)
* ElMessage.error(`${EmojiText[500]} 图片上传失败`)
*/
// macos 用户 按 shift + 6 可以唤出更多表情……
const EmojiText: { [key: string]: string } = {
'0': 'O_O', // 空
'200': '^_^', // 成功
'400': 'T_T', // 错误请求
'500': 'X_X' // 服务器内部错误,无法完成请求
}
// const EmojiIcon = ['🟢', '🔴', '🟡 ', '🚀', '✨', '💡', '🛠️', '🔥', '🎉', '🌟', '🌈']
export default EmojiText
+8
View File
@@ -0,0 +1,8 @@
/**
* UI 相关工具函数统一导出
*/
export * from './colors'
export * from './loading'
export * from './tabs'
export * from './emojo'
+38
View File
@@ -0,0 +1,38 @@
import { fourDotsSpinnerSvg } from '@/assets/svg/loading'
import { ElLoading } from 'element-plus'
const DEFAULT_LOADING_CONFIG = {
lock: true,
background: 'rgba(0, 0, 0, 0)',
svg: fourDotsSpinnerSvg,
svgViewBox: '0 0 40 40'
} as const
interface LoadingInstance {
close: () => void
}
let loadingInstance: LoadingInstance | null = null
export const loadingService = {
/**
* 显示 loading
* @returns 关闭 loading 的函数
*/
showLoading(): () => void {
if (!loadingInstance) {
loadingInstance = ElLoading.service(DEFAULT_LOADING_CONFIG)
}
return () => this.hideLoading()
},
/**
* 隐藏 loading
*/
hideLoading(): void {
if (loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* 标签页容器高度以及顶部距离配置
* @param {Object} tab-default - 默认标签页样式配置
* @param {number} openTop - 标签页打开时的顶部填充高度
* @param {number} closeTop - 标签页关闭时的顶部填充高度
* @param {number} openHeight - 标签页打开时的总高度
* @param {number} closeHeight - 标签页关闭时的总高度
*/
export const TAB_CONFIG = {
'tab-default': {
openTop: 106,
closeTop: 60,
openHeight: 121,
closeHeight: 75
},
'tab-card': {
openTop: 122,
closeTop: 78,
openHeight: 139,
closeHeight: 95
},
'tab-google': {
openTop: 122,
closeTop: 78,
openHeight: 139,
closeHeight: 95
}
}
// 获取当前 tab 样式配置,设置默认值
export const getTabConfig = (style: string) => {
return TAB_CONFIG[style as keyof typeof TAB_CONFIG] || TAB_CONFIG['tab-card'] // 默认使用 tab-card 配置
}
+289
View File
@@ -0,0 +1,289 @@
/**
* 表单验证工具函数
* 提供常用的表单字段验证方法
*/
/**
* 密码强度级别枚举
*/
export enum PasswordStrength {
WEAK = '弱',
MEDIUM = '中',
STRONG = '强'
}
/**
* 去除字符串首尾空格
* @param value 待处理的字符串
* @returns 返回去除首尾空格后的字符串
*/
export function trimSpaces(value: string): string {
if (typeof value !== 'string') {
return ''
}
return value.trim()
}
/**
* 验证手机号码(中国大陆)
* @param value 手机号码字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validatePhone(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
// 中国大陆手机号码:1开头,第二位为3-9,共11位数字
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(value.trim())
}
/**
* 验证固定电话号码(中国大陆)
* @param value 电话号码字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validateTelPhone(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
// 支持格式:区号-号码,如:010-12345678、0755-1234567
const telRegex = /^0\d{2,3}-?\d{7,8}$/
return telRegex.test(value.trim().replace(/\s+/g, ''))
}
/**
* 验证用户账号
* @param value 账号字符串
* @returns 返回验证结果,true表示格式正确
* @description 规则:字母开头,5-20位,支持字母、数字、下划线
*/
export function validateAccount(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
// 字母开头,5-20位,支持字母、数字、下划线
const accountRegex = /^[a-zA-Z][a-zA-Z0-9_]{4,19}$/
return accountRegex.test(value.trim())
}
/**
* 验证密码
* @param value 密码字符串
* @returns 返回验证结果,true表示格式正确
* @description 规则:6-20位,必须包含字母和数字
*/
export function validatePassword(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
const trimmedValue = value.trim()
// 长度检查
if (trimmedValue.length < 6 || trimmedValue.length > 20) {
return false
}
// 必须包含字母和数字
const hasLetter = /[a-zA-Z]/.test(trimmedValue)
const hasNumber = /\d/.test(trimmedValue)
return hasLetter && hasNumber
}
/**
* 验证强密码
* @param value 密码字符串
* @returns 返回验证结果,true表示格式正确
* @description 规则:8-20位,必须包含大写字母、小写字母、数字和特殊字符
*/
export function validateStrongPassword(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
const trimmedValue = value.trim()
// 长度检查
if (trimmedValue.length < 8 || trimmedValue.length > 20) {
return false
}
// 必须包含:大写字母、小写字母、数字、特殊字符
const hasUpperCase = /[A-Z]/.test(trimmedValue)
const hasLowerCase = /[a-z]/.test(trimmedValue)
const hasNumber = /\d/.test(trimmedValue)
const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue)
return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar
}
/**
* 获取密码强度
* @param value 密码字符串
* @returns 返回密码强度:弱、中、强
* @description 弱:纯数字/纯字母/纯特殊字符;中:两种组合;强:三种或以上组合
*/
export function getPasswordStrength(value: string): PasswordStrength {
if (!value || typeof value !== 'string') {
return PasswordStrength.WEAK
}
const trimmedValue = value.trim()
if (trimmedValue.length < 6) {
return PasswordStrength.WEAK
}
const hasUpperCase = /[A-Z]/.test(trimmedValue)
const hasLowerCase = /[a-z]/.test(trimmedValue)
const hasNumber = /\d/.test(trimmedValue)
const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue)
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(Boolean).length
if (typeCount >= 3) {
return PasswordStrength.STRONG
} else if (typeCount >= 2) {
return PasswordStrength.MEDIUM
} else {
return PasswordStrength.WEAK
}
}
/**
* 验证IPv4地址
* @param value IP地址字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validateIPv4Address(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
const trimmedValue = value.trim()
const ipRegex = /^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$/
if (!ipRegex.test(trimmedValue)) {
return false
}
// 额外检查每个段是否在有效范围内
const segments = trimmedValue.split('.')
return segments.every((segment) => {
const num = parseInt(segment, 10)
return num >= 0 && num <= 255
})
}
/**
* 验证邮箱地址
* @param value 邮箱地址字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validateEmail(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
const trimmedValue = value.trim()
// RFC 5322 标准的简化版邮箱正则
const emailRegex =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
return emailRegex.test(trimmedValue) && trimmedValue.length <= 254
}
/**
* 验证URL地址
* @param value URL字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validateURL(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
try {
new URL(value.trim())
return true
} catch {
return false
}
}
/**
* 验证身份证号码(中国大陆)
* @param value 身份证号码字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validateChineseIDCard(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
const trimmedValue = value.trim()
// 18位身份证号码正则
const idCardRegex =
/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
if (!idCardRegex.test(trimmedValue)) {
return false
}
// 验证校验码
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
let sum = 0
for (let i = 0; i < 17; i++) {
sum += parseInt(trimmedValue[i]) * weights[i]
}
const checkCode = checkCodes[sum % 11]
return trimmedValue[17].toUpperCase() === checkCode
}
/**
* 验证银行卡号
* @param value 银行卡号字符串
* @returns 返回验证结果,true表示格式正确
*/
export function validateBankCard(value: string): boolean {
if (!value || typeof value !== 'string') {
return false
}
const trimmedValue = value.trim().replace(/\s+/g, '')
// 银行卡号通常为13-19位数字
if (!/^\d{13,19}$/.test(trimmedValue)) {
return false
}
// Luhn算法验证
let sum = 0
let shouldDouble = false
for (let i = trimmedValue.length - 1; i >= 0; i--) {
let digit = parseInt(trimmedValue[i])
if (shouldDouble) {
digit *= 2
if (digit > 9) {
digit = (digit % 10) + 1
}
}
sum += digit
shouldDouble = !shouldDouble
}
return sum % 10 === 0
}
+5
View File
@@ -0,0 +1,5 @@
/**
* 验证相关工具函数统一导出
*/
export * from './formValidator'