Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
6
src/utils/auth/index.ts
Normal file
6
src/utils/auth/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 认证相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './rememberPassword'
|
||||
export * from './loginValidation'
|
||||
172
src/utils/auth/loginValidation.ts
Normal file
172
src/utils/auth/loginValidation.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 登录表单验证规则
|
||||
*/
|
||||
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
|
||||
/**
|
||||
* 用户名验证规则
|
||||
*/
|
||||
export const usernameRules = (t: (key: string) => string): FormItemRule[] => [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.placeholder[0]'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 20,
|
||||
message: t('login.validation.usernameLength'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: t('login.validation.usernamePattern'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 密码验证规则
|
||||
*/
|
||||
export const passwordRules = (t: (key: string) => string): FormItemRule[] => [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.placeholder[1]'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: t('login.validation.passwordLength'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 强密码验证规则(用于注册/修改密码)
|
||||
*/
|
||||
export const strongPasswordRules = (t: (key: string) => string): FormItemRule[] => [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.placeholder[1]'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 8,
|
||||
max: 20,
|
||||
message: t('login.validation.strongPasswordLength'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
// 必须包含大小写字母、数字
|
||||
const hasUpperCase = /[A-Z]/.test(value)
|
||||
const hasLowerCase = /[a-z]/.test(value)
|
||||
const hasNumber = /\d/.test(value)
|
||||
|
||||
if (!hasUpperCase || !hasLowerCase || !hasNumber) {
|
||||
callback(new Error(t('login.validation.strongPasswordPattern')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 确认密码验证规则
|
||||
*/
|
||||
export const confirmPasswordRules = (
|
||||
t: (key: string) => string,
|
||||
getPassword: () => string
|
||||
): FormItemRule[] => [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.validation.confirmPasswordRequired'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
if (value !== getPassword()) {
|
||||
callback(new Error(t('login.validation.confirmPasswordNotMatch')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 手机号验证规则
|
||||
*/
|
||||
export const phoneRules = (t: (key: string) => string, required = false): FormItemRule[] => {
|
||||
const rules: FormItemRule[] = []
|
||||
|
||||
if (required) {
|
||||
rules.push({
|
||||
required: true,
|
||||
message: t('login.validation.phoneRequired'),
|
||||
trigger: 'blur'
|
||||
})
|
||||
}
|
||||
|
||||
rules.push({
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: t('login.validation.phonePattern'),
|
||||
trigger: 'blur'
|
||||
})
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证规则
|
||||
*/
|
||||
export const emailRules = (t: (key: string) => string, required = false): FormItemRule[] => {
|
||||
const rules: FormItemRule[] = []
|
||||
|
||||
if (required) {
|
||||
rules.push({
|
||||
required: true,
|
||||
message: t('login.validation.emailRequired'),
|
||||
trigger: 'blur'
|
||||
})
|
||||
}
|
||||
|
||||
rules.push({
|
||||
type: 'email',
|
||||
message: t('login.validation.emailPattern'),
|
||||
trigger: 'blur'
|
||||
})
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码验证规则
|
||||
*/
|
||||
export const captchaRules = (t: (key: string) => string): FormItemRule[] => [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.validation.captchaRequired'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
len: 4,
|
||||
message: t('login.validation.captchaLength'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
80
src/utils/auth/rememberPassword.ts
Normal file
80
src/utils/auth/rememberPassword.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 记住密码功能
|
||||
* 使用 localStorage 存储加密后的用户名和密码
|
||||
*/
|
||||
|
||||
// 存储key
|
||||
const REMEMBER_KEY = 'remembered_credentials'
|
||||
|
||||
// 简单加密(实际项目中应使用更安全的加密方式)
|
||||
const encode = (str: string): string => {
|
||||
return btoa(encodeURIComponent(str))
|
||||
}
|
||||
|
||||
// 简单解密
|
||||
const decode = (str: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(atob(str))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存的凭证接口
|
||||
*/
|
||||
export interface RememberedCredentials {
|
||||
username: string
|
||||
password: string
|
||||
rememberPassword: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存登录凭证
|
||||
*/
|
||||
export const saveCredentials = (username: string, password: string, remember: boolean) => {
|
||||
if (remember) {
|
||||
const credentials = {
|
||||
u: encode(username),
|
||||
p: encode(password),
|
||||
r: true
|
||||
}
|
||||
localStorage.setItem(REMEMBER_KEY, JSON.stringify(credentials))
|
||||
} else {
|
||||
// 如果不记住密码,清除已保存的凭证
|
||||
localStorage.removeItem(REMEMBER_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存的登录凭证
|
||||
*/
|
||||
export const getRememberedCredentials = (): RememberedCredentials | null => {
|
||||
try {
|
||||
const saved = localStorage.getItem(REMEMBER_KEY)
|
||||
if (!saved) return null
|
||||
|
||||
const credentials = JSON.parse(saved)
|
||||
return {
|
||||
username: decode(credentials.u),
|
||||
password: decode(credentials.p),
|
||||
rememberPassword: credentials.r
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除保存的登录凭证
|
||||
*/
|
||||
export const clearRememberedCredentials = () => {
|
||||
localStorage.removeItem(REMEMBER_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有保存的凭证
|
||||
*/
|
||||
export const hasRememberedCredentials = (): boolean => {
|
||||
return localStorage.getItem(REMEMBER_KEY) !== null
|
||||
}
|
||||
44
src/utils/browser/bom.ts
Normal file
44
src/utils/browser/bom.ts
Normal 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
src/utils/browser/cookie.ts
Normal file
23
src/utils/browser/cookie.ts
Normal 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
src/utils/browser/index.ts
Normal file
6
src/utils/browser/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 浏览器相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './bom'
|
||||
export * from './cookie'
|
||||
119
src/utils/business/calculate.ts
Normal file
119
src/utils/business/calculate.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 业务计算工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 计算佣金
|
||||
* @param amount 交易金额(分)
|
||||
* @param rate 佣金比例(0-100)
|
||||
* @param type 佣金类型:percentage-百分比, fixed-固定金额
|
||||
*/
|
||||
export function calculateCommission(
|
||||
amount: number,
|
||||
rate: number,
|
||||
type: 'percentage' | 'fixed' = 'percentage'
|
||||
): number {
|
||||
if (type === 'fixed') {
|
||||
return rate
|
||||
}
|
||||
return Math.floor((amount * rate) / 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算折扣价
|
||||
* @param originalPrice 原价(分)
|
||||
* @param discount 折扣(0-100)
|
||||
*/
|
||||
export function calculateDiscountPrice(originalPrice: number, discount: number): number {
|
||||
return Math.floor((originalPrice * discount) / 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算流量使用率
|
||||
* @param used 已用流量(MB)
|
||||
* @param total 总流量(MB)
|
||||
*/
|
||||
export function calculateFlowUsageRate(used: number, total: number): number {
|
||||
if (total === 0) return 0
|
||||
return Math.min(100, Math.round((used / total) * 100))
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算剩余天数
|
||||
* @param expireTime 过期时间
|
||||
*/
|
||||
export function calculateRemainingDays(expireTime: string): number {
|
||||
const now = new Date().getTime()
|
||||
const expire = new Date(expireTime).getTime()
|
||||
const diff = expire - now
|
||||
|
||||
if (diff <= 0) return 0
|
||||
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算环比增长率
|
||||
* @param current 当前值
|
||||
* @param previous 上期值
|
||||
*/
|
||||
export function calculateGrowthRate(current: number, previous: number): number {
|
||||
if (previous === 0) return current > 0 ? 100 : 0
|
||||
|
||||
return Math.round(((current - previous) / previous) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算平均值
|
||||
* @param values 数值数组
|
||||
*/
|
||||
export function calculateAverage(values: number[]): number {
|
||||
if (values.length === 0) return 0
|
||||
|
||||
const sum = values.reduce((acc, val) => acc + val, 0)
|
||||
return Math.round((sum / values.length) * 100) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算提现手续费
|
||||
* @param amount 提现金额(分)
|
||||
* @param feeRate 手续费率(0-100)
|
||||
* @param feeType 手续费类型:percentage-百分比, fixed-固定
|
||||
*/
|
||||
export function calculateWithdrawalFee(
|
||||
amount: number,
|
||||
feeRate: number,
|
||||
feeType: 'percentage' | 'fixed' = 'percentage'
|
||||
): number {
|
||||
if (feeType === 'fixed') {
|
||||
return feeRate
|
||||
}
|
||||
return Math.floor((amount * feeRate) / 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算实际到账金额
|
||||
* @param amount 提现金额(分)
|
||||
* @param fee 手续费(分)
|
||||
*/
|
||||
export function calculateActualAmount(amount: number, fee: number): number {
|
||||
return Math.max(0, amount - fee)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算分页偏移量
|
||||
* @param page 当前页
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
export function calculateOffset(page: number, pageSize: number): number {
|
||||
return (page - 1) * pageSize
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算总页数
|
||||
* @param total 总记录数
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
export function calculateTotalPages(total: number, pageSize: number): number {
|
||||
return Math.ceil(total / pageSize)
|
||||
}
|
||||
185
src/utils/business/format.ts
Normal file
185
src/utils/business/format.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 格式化工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化流量(MB -> GB/MB)
|
||||
* @param mb 流量(MB)
|
||||
* @param decimal 保留小数位数
|
||||
*/
|
||||
export function formatFlow(mb: number | undefined, decimal = 2): string {
|
||||
if (mb === undefined || mb === null) return '-'
|
||||
|
||||
if (mb >= 1024) {
|
||||
return `${(mb / 1024).toFixed(decimal)} GB`
|
||||
}
|
||||
return `${mb.toFixed(decimal)} MB`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化金额(分 -> 元)
|
||||
* @param fen 金额(分)
|
||||
* @param showSymbol 是否显示货币符号
|
||||
*/
|
||||
export function formatMoney(fen: number | undefined, showSymbol = true): string {
|
||||
if (fen === undefined || fen === null) return '-'
|
||||
|
||||
const yuan = (fen / 100).toFixed(2)
|
||||
return showSymbol ? `¥${yuan}` : yuan
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化手机号(中间4位隐藏)
|
||||
* @param phone 手机号
|
||||
*/
|
||||
export function formatPhone(phone: string | undefined): string {
|
||||
if (!phone) return '-'
|
||||
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化ICCID(显示前5位和后4位)
|
||||
* @param iccid ICCID
|
||||
*/
|
||||
export function formatIccid(iccid: string | undefined): string {
|
||||
if (!iccid) return '-'
|
||||
|
||||
if (iccid.length > 10) {
|
||||
return `${iccid.substring(0, 5)}...${iccid.substring(iccid.length - 4)}`
|
||||
}
|
||||
return iccid
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化ICCID(完整显示,带分隔符)
|
||||
* @param iccid ICCID
|
||||
*/
|
||||
export function formatIccidFull(iccid: string | undefined): string {
|
||||
if (!iccid) return '-'
|
||||
|
||||
// 每4位添加一个空格
|
||||
return iccid.replace(/(\d{4})(?=\d)/g, '$1 ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param value 数值
|
||||
* @param total 总数
|
||||
* @param decimal 保留小数位数
|
||||
*/
|
||||
export function formatPercentage(
|
||||
value: number | undefined,
|
||||
total: number | undefined,
|
||||
decimal = 2
|
||||
): string {
|
||||
if (value === undefined || total === undefined || total === 0) return '-'
|
||||
|
||||
const percentage = ((value / total) * 100).toFixed(decimal)
|
||||
return `${percentage}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时长(秒 -> 时分秒)
|
||||
* @param seconds 秒数
|
||||
*/
|
||||
export function formatDuration(seconds: number | undefined): string {
|
||||
if (seconds === undefined || seconds === null) return '-'
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
|
||||
const parts: string[] = []
|
||||
if (hours > 0) parts.push(`${hours}时`)
|
||||
if (minutes > 0) parts.push(`${minutes}分`)
|
||||
if (secs > 0 || parts.length === 0) parts.push(`${secs}秒`)
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param bytes 字节数
|
||||
* @param decimal 保留小数位数
|
||||
*/
|
||||
export function formatFileSize(bytes: number | undefined, decimal = 2): string {
|
||||
if (bytes === undefined || bytes === null) return '-'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(decimal)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字(添加千分位)
|
||||
* @param num 数字
|
||||
*/
|
||||
export function formatNumber(num: number | undefined): string {
|
||||
if (num === undefined || num === null) return '-'
|
||||
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏银行卡号(显示前4位和后4位)
|
||||
* @param cardNo 银行卡号
|
||||
*/
|
||||
export function formatBankCard(cardNo: string | undefined): string {
|
||||
if (!cardNo) return '-'
|
||||
|
||||
if (cardNo.length > 8) {
|
||||
const stars = '*'.repeat(cardNo.length - 8)
|
||||
return `${cardNo.substring(0, 4)} ${stars} ${cardNo.substring(cardNo.length - 4)}`
|
||||
}
|
||||
return cardNo
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期范围
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
*/
|
||||
export function formatDateRange(startDate: string, endDate: string): string {
|
||||
if (!startDate || !endDate) return '-'
|
||||
|
||||
return `${startDate} ~ ${endDate}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param date 日期字符串或时间戳
|
||||
* @param format 格式化模板,默认 'YYYY-MM-DD HH:mm:ss'
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDateTime(
|
||||
date: string | number | Date | undefined | null,
|
||||
format: string = 'YYYY-MM-DD HH:mm:ss'
|
||||
): string {
|
||||
if (!date) return '-'
|
||||
|
||||
const d = new Date(date)
|
||||
if (isNaN(d.getTime())) return '-'
|
||||
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
7
src/utils/business/index.ts
Normal file
7
src/utils/business/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 业务工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './format'
|
||||
export * from './validate'
|
||||
export * from './calculate'
|
||||
126
src/utils/business/validate.ts
Normal file
126
src/utils/business/validate.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 业务验证工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 验证ICCID格式
|
||||
* @param iccid ICCID
|
||||
*/
|
||||
export function validateIccid(iccid: string): boolean {
|
||||
// ICCID通常是19-20位数字
|
||||
return /^\d{19,20}$/.test(iccid)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证IMSI格式
|
||||
* @param imsi IMSI
|
||||
*/
|
||||
export function validateImsi(imsi: string): boolean {
|
||||
// IMSI通常是15位数字
|
||||
return /^\d{15}$/.test(imsi)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号格式
|
||||
* @param phone 手机号
|
||||
*/
|
||||
export function validatePhone(phone: string): boolean {
|
||||
// 中国大陆手机号
|
||||
return /^1[3-9]\d{9}$/.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
* @param email 邮箱
|
||||
*/
|
||||
export function validateEmail(email: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号
|
||||
* @param idCard 身份证号
|
||||
*/
|
||||
export function validateIdCard(idCard: string): boolean {
|
||||
return /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(
|
||||
idCard
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证银行卡号
|
||||
* @param cardNo 银行卡号
|
||||
*/
|
||||
export function validateBankCard(cardNo: string): boolean {
|
||||
// 一般16-19位
|
||||
return /^\d{16,19}$/.test(cardNo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证IP地址
|
||||
* @param ip IP地址
|
||||
*/
|
||||
export function validateIP(ip: string): boolean {
|
||||
return /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证URL
|
||||
* @param url URL
|
||||
*/
|
||||
export function validateURL(url: string): boolean {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码强度
|
||||
* @param password 密码
|
||||
* @returns 强度等级:weak, medium, strong
|
||||
*/
|
||||
export function validatePasswordStrength(password: string): 'weak' | 'medium' | 'strong' {
|
||||
if (password.length < 6) return 'weak'
|
||||
|
||||
let strength = 0
|
||||
|
||||
// 包含小写字母
|
||||
if (/[a-z]/.test(password)) strength++
|
||||
// 包含大写字母
|
||||
if (/[A-Z]/.test(password)) strength++
|
||||
// 包含数字
|
||||
if (/\d/.test(password)) strength++
|
||||
// 包含特殊字符
|
||||
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++
|
||||
// 长度大于8
|
||||
if (password.length >= 8) strength++
|
||||
|
||||
if (strength <= 2) return 'weak'
|
||||
if (strength <= 3) return 'medium'
|
||||
return 'strong'
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证金额格式(正数,最多2位小数)
|
||||
* @param amount 金额
|
||||
*/
|
||||
export function validateAmount(amount: string | number): boolean {
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount
|
||||
return !isNaN(numAmount) && numAmount > 0 && /^\d+(\.\d{1,2})?$/.test(String(amount))
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证整数
|
||||
* @param value 值
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
export function validateInteger(value: number, min?: number, max?: number): boolean {
|
||||
if (!Number.isInteger(value)) return false
|
||||
if (min !== undefined && value < min) return false
|
||||
if (max !== undefined && value > max) return false
|
||||
return true
|
||||
}
|
||||
66
src/utils/constants/iconfont.ts
Normal file
66
src/utils/constants/iconfont.ts
Normal 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
src/utils/constants/index.ts
Normal file
6
src/utils/constants/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 常量定义相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './links'
|
||||
export * from './iconfont'
|
||||
22
src/utils/constants/links.ts
Normal file
22
src/utils/constants/links.ts
Normal 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/guide/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
src/utils/dataprocess/array.ts
Normal file
77
src/utils/dataprocess/array.ts
Normal 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
src/utils/dataprocess/format.ts
Normal file
28
src/utils/dataprocess/format.ts
Normal 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
src/utils/dataprocess/index.ts
Normal file
6
src/utils/dataprocess/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 数据处理相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './array'
|
||||
export * from './format'
|
||||
195
src/utils/http/README.md
Normal file
195
src/utils/http/README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# HTTP 请求使用说明
|
||||
|
||||
## 🚀 基础用法
|
||||
|
||||
```typescript
|
||||
import request from '@/utils/http'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
|
||||
// 基础GET请求
|
||||
const response = await request.get<BaseResponse<User[]>>({
|
||||
url: '/api/users'
|
||||
})
|
||||
|
||||
// 基础POST请求
|
||||
const result = await request.post<BaseResponse<User>>({
|
||||
url: '/api/users',
|
||||
data: { name: 'John', email: 'john@example.com' }
|
||||
})
|
||||
```
|
||||
|
||||
## ⚙️ 使用自定义请求选项
|
||||
|
||||
```typescript
|
||||
// 不显示错误消息
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: {
|
||||
errorMessageMode: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
// 不携带token的请求
|
||||
await request.get({
|
||||
url: '/api/public/data',
|
||||
requestOptions: {
|
||||
withToken: false
|
||||
}
|
||||
})
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: {
|
||||
joinTime: true // 会在URL后添加 ?_t=timestamp
|
||||
}
|
||||
})
|
||||
|
||||
// 使用不同的API地址
|
||||
await request.post({
|
||||
url: '/upload',
|
||||
data: formData,
|
||||
requestOptions: {
|
||||
apiUrl: 'https://upload.example.com'
|
||||
}
|
||||
})
|
||||
|
||||
// 组合使用
|
||||
await request.get({
|
||||
url: '/api/sensitive-data',
|
||||
requestOptions: {
|
||||
withToken: true,
|
||||
errorMessageMode: 'modal',
|
||||
joinTime: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 📋 RequestOptions 配置项
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | -------------------------------- | ----------- | ------------------- |
|
||||
| `errorMessageMode` | `'none' \| 'modal' \| 'message'` | `'message'` | 错误消息显示方式 |
|
||||
| `withToken` | `boolean` | `true` | 是否携带认证token |
|
||||
| `joinTime` | `boolean` | `false` | 是否添加时间戳 |
|
||||
| `apiUrl` | `string` | - | 自定义API基础地址 |
|
||||
| `joinParamsToUrl` | `boolean` | `false` | 是否将参数拼接到URL |
|
||||
| `formatDate` | `boolean` | `false` | 是否格式化日期 |
|
||||
| `isTransformResponse` | `boolean` | `true` | 是否转换响应数据 |
|
||||
| `isReturnNativeResponse` | `boolean` | `false` | 是否返回原生响应 |
|
||||
| `joinPrefix` | `boolean` | `true` | 是否添加前缀 |
|
||||
| `ignoreCancelToken` | `boolean` | `false` | 是否忽略取消令牌 |
|
||||
|
||||
## 🔄 迁移指南
|
||||
|
||||
### 从旧版本迁移
|
||||
|
||||
```typescript
|
||||
// 旧写法 ❌
|
||||
await request.get({
|
||||
url: '/api/data'
|
||||
})
|
||||
|
||||
// 新写法 ✅ (向后兼容)
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: {
|
||||
errorMessageMode: 'message' // 可选配置
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 错误处理方式对比
|
||||
|
||||
```typescript
|
||||
// 默认错误处理 (ElMessage)
|
||||
await request.get({ url: '/api/data' })
|
||||
|
||||
// 静默请求 (不显示错误消息)
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
})
|
||||
|
||||
// 模态框错误 (TODO: 需要实现ElMessageBox)
|
||||
await request.get({
|
||||
url: '/api/data',
|
||||
requestOptions: { errorMessageMode: 'modal' }
|
||||
})
|
||||
```
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
1. **API服务类中的使用**
|
||||
|
||||
```typescript
|
||||
export class UserService {
|
||||
// 公开接口,不需要token
|
||||
static getPublicInfo() {
|
||||
return request.get<BaseResponse<PublicInfo>>({
|
||||
url: '/api/public/info',
|
||||
requestOptions: {
|
||||
withToken: false,
|
||||
errorMessageMode: 'none' // 静默失败
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 需要认证的接口
|
||||
static getUserProfile() {
|
||||
return request.get<BaseResponse<UserProfile>>({
|
||||
url: '/api/user/profile',
|
||||
requestOptions: {
|
||||
withToken: true,
|
||||
joinTime: true // 防止缓存
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **文件上传场景**
|
||||
|
||||
```typescript
|
||||
const uploadFile = (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return request.post({
|
||||
url: '/api/upload',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
requestOptions: {
|
||||
withToken: true,
|
||||
errorMessageMode: 'modal' // 上传失败用模态框提示
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
3. **批量请求优化**
|
||||
|
||||
```typescript
|
||||
// 并发请求,但其中一个失败不影响其他
|
||||
const fetchDashboardData = async () => {
|
||||
const requests = [
|
||||
request.get({
|
||||
url: '/api/stats',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
}),
|
||||
request.get({
|
||||
url: '/api/charts',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
}),
|
||||
request.get({
|
||||
url: '/api/notifications',
|
||||
requestOptions: { errorMessageMode: 'none' }
|
||||
})
|
||||
]
|
||||
|
||||
const results = await Promise.allSettled(requests)
|
||||
return results
|
||||
}
|
||||
```
|
||||
262
src/utils/http/index.ts
Normal file
262
src/utils/http/index.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import EmojiText from '../ui/emojo'
|
||||
import { ApiStatus } from './status'
|
||||
import type { RequestOptions, ErrorMessageMode } from '@/types/api'
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
timeout: 15000, // 请求超时时间(毫秒)
|
||||
baseURL: import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_API_URL, // API地址:开发环境使用代理,生产环境使用完整URL
|
||||
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()
|
||||
|
||||
// 如果 token 存在,则设置请求头
|
||||
if (accessToken) {
|
||||
request.headers.set({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
})
|
||||
}
|
||||
|
||||
return request // 返回修改后的配置
|
||||
},
|
||||
(error) => {
|
||||
ElMessage.error(`服务器异常! ${EmojiText[500]}`) // 显示错误消息
|
||||
return Promise.reject(error) // 返回拒绝的 Promise
|
||||
}
|
||||
)
|
||||
|
||||
// Token 刷新相关
|
||||
let isRefreshing = false // 是否正在刷新 token
|
||||
let failedQueue: Array<{
|
||||
resolve: (value?: any) => void
|
||||
reject: (reason?: any) => void
|
||||
}> = [] // 失败队列,存储因 token 过期而失败的请求
|
||||
|
||||
// 处理失败队列
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue.forEach((prom) => {
|
||||
if (error) {
|
||||
prom.reject(error)
|
||||
} else {
|
||||
prom.resolve(token)
|
||||
}
|
||||
})
|
||||
failedQueue = []
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 401 未授权 - 尝试刷新 token
|
||||
if (response.data.code === ApiStatus.unauthorized) {
|
||||
const userStore = useUserStore()
|
||||
const originalRequest = response.config as any
|
||||
|
||||
// 如果没有 refreshToken,直接退出登录
|
||||
if (!userStore.refreshToken) {
|
||||
logOut()
|
||||
return Promise.reject(response)
|
||||
}
|
||||
|
||||
// 如果正在刷新 token,将请求加入队列
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject })
|
||||
})
|
||||
.then((token) => {
|
||||
originalRequest.headers['Authorization'] = `Bearer ${token}`
|
||||
return axiosInstance.request(originalRequest)
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 标记正在刷新
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
// 调用刷新 token 接口
|
||||
return new Promise((resolve, reject) => {
|
||||
axiosInstance
|
||||
.post('/api/auth/refresh', { refreshToken: userStore.refreshToken })
|
||||
.then((res) => {
|
||||
const { data } = res
|
||||
|
||||
// 刷新成功
|
||||
if (data.code === ApiStatus.success && data.data?.access_token) {
|
||||
// 更新 token
|
||||
userStore.setToken(data.data.access_token, data.data.refresh_token)
|
||||
|
||||
// 更新请求头
|
||||
originalRequest.headers['Authorization'] = `Bearer ${data.data.access_token}`
|
||||
axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${data.data.access_token}`
|
||||
|
||||
// 处理队列中的请求
|
||||
processQueue(null, data.data.access_token)
|
||||
|
||||
// 重试原请求
|
||||
resolve(axiosInstance.request(originalRequest))
|
||||
} else {
|
||||
// 刷新失败
|
||||
processQueue(new Error('Token refresh failed'), null)
|
||||
logOut()
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// 刷新失败
|
||||
processQueue(err, null)
|
||||
logOut()
|
||||
reject(err)
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false
|
||||
})
|
||||
})
|
||||
}
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (axios.isCancel(error)) {
|
||||
console.log('repeated request: ' + error.message)
|
||||
}
|
||||
// 注意:错误处理现在在request函数中根据requestOptions处理
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 扩展的请求配置接口
|
||||
interface ExtendedRequestConfig extends AxiosRequestConfig {
|
||||
requestOptions?: RequestOptions
|
||||
}
|
||||
|
||||
// 处理请求配置
|
||||
function processRequestConfig(config: ExtendedRequestConfig): AxiosRequestConfig {
|
||||
const { requestOptions, ...axiosConfig } = config
|
||||
|
||||
// 应用自定义请求选项
|
||||
if (requestOptions) {
|
||||
// 处理是否携带token
|
||||
if (requestOptions.withToken === false) {
|
||||
axiosConfig.headers = { ...axiosConfig.headers }
|
||||
delete axiosConfig.headers?.Authorization
|
||||
}
|
||||
|
||||
// 处理是否添加时间戳
|
||||
if (requestOptions.joinTime) {
|
||||
const timestamp = Date.now()
|
||||
if (axiosConfig.method?.toUpperCase() === 'GET') {
|
||||
axiosConfig.params = { ...axiosConfig.params, _t: timestamp }
|
||||
}
|
||||
}
|
||||
|
||||
// 处理API URL
|
||||
if (requestOptions.apiUrl) {
|
||||
axiosConfig.baseURL = requestOptions.apiUrl
|
||||
}
|
||||
}
|
||||
|
||||
return axiosConfig
|
||||
}
|
||||
|
||||
// 处理错误消息
|
||||
function handleErrorMessage(error: any, mode: ErrorMessageMode = 'message') {
|
||||
if (mode === 'none') return
|
||||
|
||||
const errorMessage = error.response?.data.msg
|
||||
const message = errorMessage
|
||||
? `${errorMessage} ${EmojiText[500]}`
|
||||
: `请求超时或服务器异常!${EmojiText[500]}`
|
||||
|
||||
if (mode === 'modal') {
|
||||
// TODO: 可以使用 ElMessageBox 显示模态框
|
||||
ElMessage.error(message)
|
||||
} else {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
// 请求
|
||||
async function request<T = any>(config: ExtendedRequestConfig): Promise<T> {
|
||||
const processedConfig = processRequestConfig(config)
|
||||
|
||||
// 对 POST | PUT 请求特殊处理
|
||||
if (
|
||||
processedConfig.method?.toUpperCase() === 'POST' ||
|
||||
processedConfig.method?.toUpperCase() === 'PUT'
|
||||
) {
|
||||
// 如果已经有 data,则保留原有的 data
|
||||
if (processedConfig.params && !processedConfig.data) {
|
||||
processedConfig.data = processedConfig.params
|
||||
processedConfig.params = undefined // 使用 undefined 而不是空对象
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.request<T>(processedConfig)
|
||||
return res.data
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
// 处理错误消息
|
||||
const errorMode = config.requestOptions?.errorMessageMode || 'message'
|
||||
handleErrorMessage(e, errorMode)
|
||||
}
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
// API 方法集合
|
||||
const api = {
|
||||
get<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'GET' }) // GET 请求
|
||||
},
|
||||
post<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'POST' }) // POST 请求
|
||||
},
|
||||
put<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'PUT' }) // PUT 请求
|
||||
},
|
||||
del<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'DELETE' }) // DELETE 请求
|
||||
},
|
||||
request<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config }) // 通用请求
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const logOut = () => {
|
||||
ElMessage.error(`登录已过期,请重新登录 ${EmojiText[500]}`)
|
||||
setTimeout(() => {
|
||||
useUserStore().logOut()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
export default api
|
||||
12
src/utils/http/status.ts
Normal file
12
src/utils/http/status.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 接口状态码
|
||||
*/
|
||||
export enum ApiStatus {
|
||||
success = 0, // 成功
|
||||
error = 400, // 错误
|
||||
unauthorized = 401, // 未授权
|
||||
forbidden = 403, // 禁止访问
|
||||
notFound = 404, // 未找到
|
||||
methodNotAllowed = 405, // 方法不允许
|
||||
internalServerError = 500 // 服务器错误
|
||||
}
|
||||
34
src/utils/index.ts
Normal file
34
src/utils/index.ts
Normal 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
src/utils/navigation/index.ts
Normal file
7
src/utils/navigation/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 路由和导航相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './jump'
|
||||
export * from './worktab'
|
||||
export * from './route'
|
||||
37
src/utils/navigation/jump.ts
Normal file
37
src/utils/navigation/jump.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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 firstChild = item.children.find((child) => !child.meta.isHide) || item.children[0]
|
||||
|
||||
// 如果第一个子菜单是外部链接则打开新窗口
|
||||
if (firstChild.meta?.link) {
|
||||
return openExternalLink(firstChild.meta.link)
|
||||
}
|
||||
|
||||
// 跳转到子菜单路径
|
||||
router.push(firstChild.path)
|
||||
}
|
||||
8
src/utils/navigation/route.ts
Normal file
8
src/utils/navigation/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 路由相关工具函数
|
||||
*/
|
||||
|
||||
// 检查是否为 iframe 路由
|
||||
export function isIframe(url: string): boolean {
|
||||
return url.startsWith('/iframe/')
|
||||
}
|
||||
42
src/utils/navigation/worktab.ts
Normal file
42
src/utils/navigation/worktab.ts
Normal 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
src/utils/storage/index.ts
Normal file
7
src/utils/storage/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 存储相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './storage'
|
||||
export * from './storage-config'
|
||||
export * from './storage-key-manager'
|
||||
92
src/utils/storage/storage-config.ts
Normal file
92
src/utils/storage/storage-config.ts
Normal 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
src/utils/storage/storage-key-manager.ts
Normal file
66
src/utils/storage/storage-key-manager.ts
Normal 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
src/utils/storage/storage.ts
Normal file
216
src/utils/storage/storage.ts
Normal 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
src/utils/sys/console.ts
Normal file
21
src/utils/sys/console.ts
Normal 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
src/utils/sys/index.ts
Normal file
6
src/utils/sys/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 系统管理相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './upgrade'
|
||||
export { default as mittBus } from './mittBus'
|
||||
9
src/utils/sys/mittBus.ts
Normal file
9
src/utils/sys/mittBus.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 全局事件总线,用于全局事件的发布与订阅
|
||||
* 用法:
|
||||
* mittBus.on('event', callback)
|
||||
* mittBus.emit('event', data)
|
||||
*/
|
||||
import mitt from 'mitt'
|
||||
const mittBus = mitt()
|
||||
export default mittBus
|
||||
241
src/utils/sys/upgrade.ts
Normal file
241
src/utils/sys/upgrade.ts
Normal 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
src/utils/theme/animation.ts
Normal file
51
src/utils/theme/animation.ts
Normal 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
src/utils/theme/index.ts
Normal file
5
src/utils/theme/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 主题相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './animation'
|
||||
231
src/utils/ui/colors.ts
Normal file
231
src/utils/ui/colors.ts
Normal 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
src/utils/ui/emojo.ts
Normal file
21
src/utils/ui/emojo.ts
Normal 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
src/utils/ui/index.ts
Normal file
8
src/utils/ui/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* UI 相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './colors'
|
||||
export * from './loading'
|
||||
export * from './tabs'
|
||||
export * from './emojo'
|
||||
38
src/utils/ui/loading.ts
Normal file
38
src/utils/ui/loading.ts
Normal 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
src/utils/ui/tabs.ts
Normal file
33
src/utils/ui/tabs.ts
Normal 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
src/utils/validation/formValidator.ts
Normal file
289
src/utils/validation/formValidator.ts
Normal 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
src/utils/validation/index.ts
Normal file
5
src/utils/validation/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 验证相关工具函数统一导出
|
||||
*/
|
||||
|
||||
export * from './formValidator'
|
||||
Reference in New Issue
Block a user