first commit
This commit is contained in:
295
src/utils/index.js
Normal file
295
src/utils/index.js
Normal file
@@ -0,0 +1,295 @@
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
// 设置dayjs语言
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
/**
|
||||
* 日期格式化
|
||||
* @param {Date|string} date 日期
|
||||
* @param {string} format 格式
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
*/
|
||||
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对时间
|
||||
* @param {Date|string} date 日期
|
||||
* @returns {string} 相对时间字符串
|
||||
*/
|
||||
export const fromNow = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).fromNow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号格式化
|
||||
* @param {string} phone 手机号
|
||||
* @returns {string} 格式化后的手机号
|
||||
*/
|
||||
export const formatPhone = (phone) => {
|
||||
if (!phone) return ''
|
||||
return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3')
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额格式化
|
||||
* @param {number} amount 金额
|
||||
* @param {number} decimals 小数位数
|
||||
* @returns {string} 格式化后的金额
|
||||
*/
|
||||
export const formatMoney = (amount, decimals = 2) => {
|
||||
if (amount === null || amount === undefined) return '0.00'
|
||||
return Number(amount).toFixed(decimals)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件大小格式化
|
||||
* @param {number} bytes 字节数
|
||||
* @returns {string} 格式化后的文件大小
|
||||
*/
|
||||
export const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func 要防抖的函数
|
||||
* @param {number} wait 等待时间
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
export const debounce = (func, wait) => {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func 要节流的函数
|
||||
* @param {number} limit 限制时间
|
||||
* @returns {Function} 节流后的函数
|
||||
*/
|
||||
export const throttle = (func, limit) => {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝
|
||||
* @param {any} obj 要拷贝的对象
|
||||
* @returns {any} 拷贝后的对象
|
||||
*/
|
||||
export const deepClone = (obj) => {
|
||||
if (obj === null || typeof obj !== 'object') return obj
|
||||
if (obj instanceof Date) return new Date(obj.getTime())
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item))
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {}
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
clonedObj[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return clonedObj
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID
|
||||
* @returns {string} UUID字符串
|
||||
*/
|
||||
export const generateUUID = () => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c == 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param {string} phone 手机号
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
export const validatePhone = (phone) => {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
* @param {string} email 邮箱
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
export const validateEmail = (email) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号
|
||||
* @param {string} idCard 身份证号
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
export const validateIdCard = (idCard) => {
|
||||
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||
return idCardRegex.test(idCard)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL参数
|
||||
* @param {string} name 参数名
|
||||
* @returns {string|null} 参数值
|
||||
*/
|
||||
export const getUrlParam = (name) => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
return urlParams.get(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置URL参数
|
||||
* @param {string} name 参数名
|
||||
* @param {string} value 参数值
|
||||
*/
|
||||
export const setUrlParam = (name, value) => {
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.set(name, value)
|
||||
window.history.replaceState({}, '', url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除URL参数
|
||||
* @param {string} name 参数名
|
||||
*/
|
||||
export const removeUrlParam = (name) => {
|
||||
const url = new URL(window.location)
|
||||
url.searchParams.delete(name)
|
||||
window.history.replaceState({}, '', url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param {string} text 要复制的文本
|
||||
* @returns {Promise<boolean>} 是否复制成功
|
||||
*/
|
||||
export const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch (err) {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
return true
|
||||
} catch (err) {
|
||||
document.body.removeChild(textArea)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param {string} url 文件URL
|
||||
* @param {string} filename 文件名
|
||||
*/
|
||||
export const downloadFile = (url, filename) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器信息
|
||||
* @returns {object} 浏览器信息
|
||||
*/
|
||||
export const getBrowserInfo = () => {
|
||||
const ua = navigator.userAgent
|
||||
const browser = {
|
||||
name: '',
|
||||
version: '',
|
||||
os: ''
|
||||
}
|
||||
|
||||
// 检测浏览器
|
||||
if (ua.includes('Chrome')) {
|
||||
browser.name = 'Chrome'
|
||||
} else if (ua.includes('Firefox')) {
|
||||
browser.name = 'Firefox'
|
||||
} else if (ua.includes('Safari')) {
|
||||
browser.name = 'Safari'
|
||||
} else if (ua.includes('Edge')) {
|
||||
browser.name = 'Edge'
|
||||
} else if (ua.includes('MSIE') || ua.includes('Trident')) {
|
||||
browser.name = 'IE'
|
||||
}
|
||||
|
||||
// 检测操作系统
|
||||
if (ua.includes('Windows')) {
|
||||
browser.os = 'Windows'
|
||||
} else if (ua.includes('Mac')) {
|
||||
browser.os = 'Mac'
|
||||
} else if (ua.includes('Linux')) {
|
||||
browser.os = 'Linux'
|
||||
} else if (ua.includes('Android')) {
|
||||
browser.os = 'Android'
|
||||
} else if (ua.includes('iOS')) {
|
||||
browser.os = 'iOS'
|
||||
}
|
||||
|
||||
return browser
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为移动设备
|
||||
* @returns {boolean} 是否为移动设备
|
||||
*/
|
||||
export const isMobile = () => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为微信浏览器
|
||||
* @returns {boolean} 是否为微信浏览器
|
||||
*/
|
||||
export const isWeChat = () => {
|
||||
return /MicroMessenger/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为支付宝浏览器
|
||||
* @returns {boolean} 是否为支付宝浏览器
|
||||
*/
|
||||
export const isAlipay = () => {
|
||||
return /AlipayClient/i.test(navigator.userAgent)
|
||||
}
|
||||
193
src/utils/performance.js
Normal file
193
src/utils/performance.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 渲染性能优化工具函数
|
||||
*/
|
||||
|
||||
// 检测设备性能等级
|
||||
export const getDevicePerformanceLevel = () => {
|
||||
const hardwareConcurrency = navigator.hardwareConcurrency || 4
|
||||
const memory = navigator.deviceMemory || 4
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
const isLowEnd = hardwareConcurrency <= 4 || memory <= 4 || isMobile
|
||||
|
||||
if (isLowEnd) {
|
||||
return 'low'
|
||||
} else if (hardwareConcurrency <= 8 || memory <= 8) {
|
||||
return 'medium'
|
||||
} else {
|
||||
return 'high'
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否支持硬件加速
|
||||
export const supportsHardwareAcceleration = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
|
||||
return !!gl
|
||||
}
|
||||
|
||||
// 检测是否支持backdrop-filter
|
||||
export const supportsBackdropFilter = () => {
|
||||
return CSS.supports('backdrop-filter', 'blur(1px)')
|
||||
}
|
||||
|
||||
// 检测用户是否偏好减少动画
|
||||
export const prefersReducedMotion = () => {
|
||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
// 应用渲染性能优化策略
|
||||
export const applyRenderOptimizations = () => {
|
||||
const performanceLevel = getDevicePerformanceLevel()
|
||||
const supportsHardware = supportsHardwareAcceleration()
|
||||
const supportsBackdrop = supportsBackdropFilter()
|
||||
const reducedMotion = prefersReducedMotion()
|
||||
|
||||
// 添加性能等级类到body
|
||||
document.body.classList.add(`performance-${performanceLevel}`)
|
||||
|
||||
// 如果不支持硬件加速,添加降级类
|
||||
if (!supportsHardware) {
|
||||
document.body.classList.add('no-hardware-acceleration')
|
||||
}
|
||||
|
||||
// 如果不支持backdrop-filter,添加降级类
|
||||
if (!supportsBackdrop) {
|
||||
document.body.classList.add('no-backdrop-filter')
|
||||
}
|
||||
|
||||
// 如果用户偏好减少动画,添加相应类
|
||||
if (reducedMotion) {
|
||||
document.body.classList.add('reduced-motion')
|
||||
}
|
||||
|
||||
return {
|
||||
performanceLevel,
|
||||
supportsHardware,
|
||||
supportsBackdrop,
|
||||
reducedMotion
|
||||
}
|
||||
}
|
||||
|
||||
// 懒加载优化
|
||||
export const createIntersectionObserver = (callback, options = {}) => {
|
||||
const defaultOptions = {
|
||||
root: null,
|
||||
rootMargin: '50px',
|
||||
threshold: 0.1,
|
||||
...options
|
||||
}
|
||||
|
||||
return new IntersectionObserver(callback, defaultOptions)
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
export const debounce = (func, wait) => {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
export const throttle = (func, limit) => {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 优化图片加载
|
||||
export const optimizeImageLoading = () => {
|
||||
const images = document.querySelectorAll('img[data-src]')
|
||||
const imageObserver = createIntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target
|
||||
img.src = img.dataset.src
|
||||
img.classList.remove('lazy')
|
||||
imageObserver.unobserve(img)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
images.forEach(img => imageObserver.observe(img))
|
||||
}
|
||||
|
||||
// 优化动画性能
|
||||
export const optimizeAnimations = () => {
|
||||
const animatedElements = document.querySelectorAll('[class*="animate-"]')
|
||||
|
||||
animatedElements.forEach(element => {
|
||||
// 添加硬件加速
|
||||
element.style.transform = 'translateZ(0)'
|
||||
element.style.willChange = 'transform'
|
||||
})
|
||||
}
|
||||
|
||||
// 减少重绘和重排
|
||||
export const optimizeLayout = () => {
|
||||
// 批量DOM操作
|
||||
const batchDOMUpdates = (updates) => {
|
||||
requestAnimationFrame(() => {
|
||||
updates.forEach(update => update())
|
||||
})
|
||||
}
|
||||
|
||||
return { batchDOMUpdates }
|
||||
}
|
||||
|
||||
// 内存优化
|
||||
export const optimizeMemory = () => {
|
||||
// 清理事件监听器
|
||||
const cleanupEventListeners = () => {
|
||||
// 在组件卸载时调用
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
const cleanupTimers = () => {
|
||||
// 清理所有定时器
|
||||
}
|
||||
|
||||
return { cleanupEventListeners, cleanupTimers }
|
||||
}
|
||||
|
||||
// 初始化渲染性能优化
|
||||
export const initRenderOptimizations = () => {
|
||||
// 应用基础优化
|
||||
const config = applyRenderOptimizations()
|
||||
|
||||
// 根据性能等级应用不同策略
|
||||
switch (config.performanceLevel) {
|
||||
case 'low':
|
||||
// 低端设备:禁用复杂动画,简化UI
|
||||
document.body.classList.add('low-performance-mode')
|
||||
break
|
||||
case 'medium':
|
||||
// 中端设备:适度优化
|
||||
document.body.classList.add('medium-performance-mode')
|
||||
break
|
||||
case 'high':
|
||||
// 高端设备:启用所有功能
|
||||
document.body.classList.add('high-performance-mode')
|
||||
break
|
||||
}
|
||||
|
||||
// 优化图片加载
|
||||
optimizeImageLoading()
|
||||
|
||||
// 优化动画
|
||||
optimizeAnimations()
|
||||
|
||||
return config
|
||||
}
|
||||
242
src/utils/permission.js
Normal file
242
src/utils/permission.js
Normal file
@@ -0,0 +1,242 @@
|
||||
// 权限指令
|
||||
export const permission = {
|
||||
inserted(el, binding) {
|
||||
const { value } = binding
|
||||
const roles = value.roles || []
|
||||
const permissions = value.permissions || []
|
||||
const userRole = el._vue.$store.getters.userRole || 'user'
|
||||
const userPermissions = el._vue.$store.getters.userPermissions || []
|
||||
|
||||
let hasPermission = true
|
||||
|
||||
// 检查角色权限
|
||||
if (roles.length > 0) {
|
||||
if (!roles.includes(userRole)) {
|
||||
hasPermission = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查功能权限
|
||||
if (permissions.length > 0) {
|
||||
const hasAllPermissions = permissions.every(permission =>
|
||||
userPermissions.includes(permission)
|
||||
)
|
||||
if (!hasAllPermissions) {
|
||||
hasPermission = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPermission) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
},
|
||||
|
||||
update(el, binding) {
|
||||
const { value } = binding
|
||||
const roles = value.roles || []
|
||||
const permissions = value.permissions || []
|
||||
const userRole = el._vue.$store.getters.userRole || 'user'
|
||||
const userPermissions = el._vue.$store.getters.userPermissions || []
|
||||
|
||||
let hasPermission = true
|
||||
|
||||
// 检查角色权限
|
||||
if (roles.length > 0) {
|
||||
if (!roles.includes(userRole)) {
|
||||
hasPermission = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查功能权限
|
||||
if (permissions.length > 0) {
|
||||
const hasAllPermissions = permissions.every(permission =>
|
||||
userPermissions.includes(permission)
|
||||
)
|
||||
if (!hasAllPermissions) {
|
||||
hasPermission = false
|
||||
}
|
||||
}
|
||||
|
||||
el.style.display = hasPermission ? '' : 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// 角色指令
|
||||
export const role = {
|
||||
inserted(el, binding) {
|
||||
const { value } = binding
|
||||
const userRole = el._vue.$store.getters.userRole || 'user'
|
||||
|
||||
if (!value.includes(userRole)) {
|
||||
el.style.display = 'none'
|
||||
}
|
||||
},
|
||||
|
||||
update(el, binding) {
|
||||
const { value } = binding
|
||||
const userRole = el._vue.$store.getters.userRole || 'user'
|
||||
|
||||
el.style.display = value.includes(userRole) ? '' : 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// 权限检查工具函数
|
||||
export const checkPermission = {
|
||||
// 检查角色权限
|
||||
hasRole(userRole, requiredRoles) {
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true
|
||||
}
|
||||
return requiredRoles.includes(userRole)
|
||||
},
|
||||
|
||||
// 检查功能权限
|
||||
hasPermission(userPermissions, requiredPermissions) {
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true
|
||||
}
|
||||
return requiredPermissions.every(permission =>
|
||||
userPermissions.includes(permission)
|
||||
)
|
||||
},
|
||||
|
||||
// 检查综合权限
|
||||
hasAccess(userRole, userPermissions, requiredRoles, requiredPermissions) {
|
||||
const hasRoleAccess = this.hasRole(userRole, requiredRoles)
|
||||
const hasPermissionAccess = this.hasPermission(userPermissions, requiredPermissions)
|
||||
return hasRoleAccess && hasPermissionAccess
|
||||
}
|
||||
}
|
||||
|
||||
// 权限常量
|
||||
export const PERMISSIONS = {
|
||||
// 统计相关权限
|
||||
STATISTICS_VIEW: 'statistics:view',
|
||||
STATISTICS_CREATE: 'statistics:create',
|
||||
STATISTICS_UPDATE: 'statistics:update',
|
||||
STATISTICS_DELETE: 'statistics:delete',
|
||||
STATISTICS_EXPORT: 'statistics:export',
|
||||
|
||||
// 仪表板相关权限
|
||||
DASHBOARD_VIEW: 'dashboard:view',
|
||||
DASHBOARD_CREATE: 'dashboard:create',
|
||||
DASHBOARD_UPDATE: 'dashboard:update',
|
||||
DASHBOARD_DELETE: 'dashboard:delete',
|
||||
|
||||
// 报告相关权限
|
||||
REPORT_VIEW: 'report:view',
|
||||
REPORT_CREATE: 'report:create',
|
||||
REPORT_UPDATE: 'report:update',
|
||||
REPORT_DELETE: 'report:delete',
|
||||
REPORT_EXPORT: 'report:export',
|
||||
|
||||
// 分析相关权限
|
||||
ANALYSIS_VIEW: 'analysis:view',
|
||||
ANALYSIS_RUN: 'analysis:run',
|
||||
ANALYSIS_EXPORT: 'analysis:export',
|
||||
|
||||
// 管理相关权限
|
||||
ADMIN_VIEW: 'admin:view',
|
||||
ADMIN_MANAGE: 'admin:manage',
|
||||
ADMIN_SETTINGS: 'admin:settings'
|
||||
}
|
||||
|
||||
// 角色常量
|
||||
export const ROLES = {
|
||||
ADMIN: 'admin',
|
||||
MANAGER: 'manager',
|
||||
ANALYST: 'analyst',
|
||||
USER: 'user'
|
||||
}
|
||||
|
||||
// 角色权限映射
|
||||
export const ROLE_PERMISSIONS = {
|
||||
[ROLES.ADMIN]: [
|
||||
PERMISSIONS.STATISTICS_VIEW,
|
||||
PERMISSIONS.STATISTICS_CREATE,
|
||||
PERMISSIONS.STATISTICS_UPDATE,
|
||||
PERMISSIONS.STATISTICS_DELETE,
|
||||
PERMISSIONS.STATISTICS_EXPORT,
|
||||
PERMISSIONS.DASHBOARD_VIEW,
|
||||
PERMISSIONS.DASHBOARD_CREATE,
|
||||
PERMISSIONS.DASHBOARD_UPDATE,
|
||||
PERMISSIONS.DASHBOARD_DELETE,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.REPORT_CREATE,
|
||||
PERMISSIONS.REPORT_UPDATE,
|
||||
PERMISSIONS.REPORT_DELETE,
|
||||
PERMISSIONS.REPORT_EXPORT,
|
||||
PERMISSIONS.ANALYSIS_VIEW,
|
||||
PERMISSIONS.ANALYSIS_RUN,
|
||||
PERMISSIONS.ANALYSIS_EXPORT,
|
||||
PERMISSIONS.ADMIN_VIEW,
|
||||
PERMISSIONS.ADMIN_MANAGE,
|
||||
PERMISSIONS.ADMIN_SETTINGS
|
||||
],
|
||||
[ROLES.MANAGER]: [
|
||||
PERMISSIONS.STATISTICS_VIEW,
|
||||
PERMISSIONS.STATISTICS_EXPORT,
|
||||
PERMISSIONS.DASHBOARD_VIEW,
|
||||
PERMISSIONS.DASHBOARD_CREATE,
|
||||
PERMISSIONS.DASHBOARD_UPDATE,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.REPORT_CREATE,
|
||||
PERMISSIONS.REPORT_EXPORT,
|
||||
PERMISSIONS.ANALYSIS_VIEW,
|
||||
PERMISSIONS.ANALYSIS_RUN,
|
||||
PERMISSIONS.ANALYSIS_EXPORT
|
||||
],
|
||||
[ROLES.ANALYST]: [
|
||||
PERMISSIONS.STATISTICS_VIEW,
|
||||
PERMISSIONS.STATISTICS_EXPORT,
|
||||
PERMISSIONS.DASHBOARD_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.REPORT_EXPORT,
|
||||
PERMISSIONS.ANALYSIS_VIEW,
|
||||
PERMISSIONS.ANALYSIS_RUN,
|
||||
PERMISSIONS.ANALYSIS_EXPORT
|
||||
],
|
||||
[ROLES.USER]: [
|
||||
PERMISSIONS.STATISTICS_VIEW,
|
||||
PERMISSIONS.DASHBOARD_VIEW,
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.ANALYSIS_VIEW
|
||||
]
|
||||
}
|
||||
|
||||
// 获取用户权限
|
||||
export const getUserPermissions = (userRole) => {
|
||||
return ROLE_PERMISSIONS[userRole] || []
|
||||
}
|
||||
|
||||
// 检查用户是否有特定权限
|
||||
export const hasUserPermission = (userRole, permission) => {
|
||||
const userPermissions = getUserPermissions(userRole)
|
||||
return userPermissions.includes(permission)
|
||||
}
|
||||
|
||||
// 检查用户是否有任意权限
|
||||
export const hasAnyPermission = (userRole, permissions) => {
|
||||
const userPermissions = getUserPermissions(userRole)
|
||||
return permissions.some(permission => userPermissions.includes(permission))
|
||||
}
|
||||
|
||||
// 检查用户是否有所有权限
|
||||
export const hasAllPermissions = (userRole, permissions) => {
|
||||
const userPermissions = getUserPermissions(userRole)
|
||||
return permissions.every(permission => userPermissions.includes(permission))
|
||||
}
|
||||
|
||||
export default {
|
||||
permission,
|
||||
role,
|
||||
checkPermission,
|
||||
PERMISSIONS,
|
||||
ROLES,
|
||||
ROLE_PERMISSIONS,
|
||||
getUserPermissions,
|
||||
hasUserPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions
|
||||
}
|
||||
|
||||
303
src/utils/request.js
Normal file
303
src/utils/request.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import router from '@/router'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 60000, // 1分钟超时(用于调试页面)
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
|
||||
// 简单的认证错误事件总线
|
||||
const authEventBus = {
|
||||
listeners: [],
|
||||
onAuthError(callback) {
|
||||
this.listeners.push(callback)
|
||||
},
|
||||
emitAuthError(message) {
|
||||
this.listeners.forEach(callback => {
|
||||
try {
|
||||
callback(message)
|
||||
} catch (error) {
|
||||
console.error('认证错误处理回调执行失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('access_token')
|
||||
const tokenType = localStorage.getItem('token_type') || 'Bearer'
|
||||
if (token) {
|
||||
config.headers.Authorization = `${tokenType} ${token}`
|
||||
}
|
||||
|
||||
// 添加请求ID用于追踪
|
||||
config.headers['X-Request-ID'] = Date.now().toString()
|
||||
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
console.error('请求拦截器错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
const { data, status } = response
|
||||
|
||||
// 检查HTTP状态码
|
||||
if (status >= 200 && status < 300) {
|
||||
// 如果是blob响应(文件下载),直接返回data
|
||||
if (data instanceof Blob) {
|
||||
return data
|
||||
}
|
||||
// 严格按照后端响应格式处理
|
||||
if (data.success === true) {
|
||||
// 成功响应,返回data字段
|
||||
return {
|
||||
success: true,
|
||||
data: data.data,
|
||||
message: data.message,
|
||||
requestId: data.request_id,
|
||||
timestamp: data.timestamp,
|
||||
pagination: data.pagination,
|
||||
meta: data.meta
|
||||
}
|
||||
} else if (data.success === false) {
|
||||
// 业务逻辑错误
|
||||
const message = data.message || '请求失败'
|
||||
// 检查是否有验证错误
|
||||
if (data.errors && typeof data.errors === 'object') {
|
||||
// 处理验证错误
|
||||
const errorMessages = []
|
||||
Object.keys(data.errors).forEach(field => {
|
||||
const fieldErrors = data.errors[field]
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
// 将字段的所有错误信息添加到数组中
|
||||
errorMessages.push(...fieldErrors)
|
||||
} else if (typeof fieldErrors === 'string') {
|
||||
// 单个错误字符串
|
||||
errorMessages.push(fieldErrors)
|
||||
}
|
||||
})
|
||||
|
||||
// 显示所有验证错误
|
||||
if (errorMessages.length > 0) {
|
||||
errorMessages.forEach(errorMsg => {
|
||||
ElMessage.error(errorMsg)
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
} else {
|
||||
// 普通业务错误
|
||||
ElMessage.error(message)
|
||||
}
|
||||
|
||||
return Promise.reject({
|
||||
type: 'business',
|
||||
message,
|
||||
errors: data.errors,
|
||||
requestId: data.request_id,
|
||||
timestamp: data.timestamp
|
||||
})
|
||||
} else {
|
||||
// 没有success字段,可能是直接返回数据
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
message: '请求成功'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// HTTP状态码错误
|
||||
const message = data?.message || `HTTP错误: ${status}`
|
||||
ElMessage.error(message)
|
||||
return Promise.reject({
|
||||
type: 'http',
|
||||
status,
|
||||
message,
|
||||
data: data
|
||||
})
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('响应拦截器错误:', error)
|
||||
|
||||
// 网络错误
|
||||
if (!error.response) {
|
||||
ElMessage.error('网络连接失败,请检查网络设置')
|
||||
return Promise.reject({
|
||||
type: 'network',
|
||||
message: '网络连接失败'
|
||||
})
|
||||
}
|
||||
|
||||
const { status, data } = error.response
|
||||
|
||||
// 根据状态码处理不同类型的错误
|
||||
let errorMessage = ''
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
// 请求参数错误
|
||||
errorMessage = data?.message || '请求参数错误'
|
||||
ElMessage.error(errorMessage)
|
||||
break
|
||||
|
||||
case 401:
|
||||
// 认证失败 - 使用事件总线通知
|
||||
errorMessage = data?.message || '登录已过期,请重新登录'
|
||||
handleAuthError(errorMessage)
|
||||
break
|
||||
|
||||
case 403:
|
||||
// 权限不足
|
||||
errorMessage = data?.message || '权限不足,无法访问此资源'
|
||||
ElMessage.error(errorMessage)
|
||||
break
|
||||
|
||||
case 404:
|
||||
// 资源不存在
|
||||
errorMessage = data?.message || '请求的资源不存在'
|
||||
ElMessage.error(errorMessage)
|
||||
break
|
||||
|
||||
case 409:
|
||||
// 资源冲突
|
||||
errorMessage = data?.message || '资源冲突,请检查输入信息'
|
||||
ElMessage.error(errorMessage)
|
||||
break
|
||||
|
||||
case 422:
|
||||
// 参数校验失败
|
||||
errorMessage = data?.message || '请求参数验证失败'
|
||||
|
||||
// 检查是否有验证错误
|
||||
if (data?.errors && typeof data.errors === 'object') {
|
||||
// 处理验证错误
|
||||
const errorMessages = []
|
||||
Object.keys(data.errors).forEach(field => {
|
||||
const fieldErrors = data.errors[field]
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
// 将字段的所有错误信息添加到数组中
|
||||
errorMessages.push(...fieldErrors)
|
||||
} else if (typeof fieldErrors === 'string') {
|
||||
// 单个错误字符串
|
||||
errorMessages.push(fieldErrors)
|
||||
}
|
||||
})
|
||||
|
||||
// 显示所有验证错误
|
||||
if (errorMessages.length > 0) {
|
||||
errorMessages.forEach(errorMsg => {
|
||||
ElMessage.error(errorMsg)
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
break
|
||||
|
||||
case 429:
|
||||
// 请求频率限制
|
||||
errorMessage = data?.message || '请求过于频繁,请稍后再试'
|
||||
ElMessage.error(errorMessage)
|
||||
break
|
||||
|
||||
case 500:
|
||||
// 服务器内部错误
|
||||
errorMessage = data?.message || '服务器内部错误,请稍后再试'
|
||||
ElMessage.error(errorMessage)
|
||||
break
|
||||
|
||||
default:
|
||||
// 其他错误
|
||||
errorMessage = data?.message || `请求失败 (${status})`
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
|
||||
return Promise.reject({
|
||||
type: 'http',
|
||||
status,
|
||||
message: data?.message || `HTTP错误: ${status}`,
|
||||
data: data,
|
||||
requestId: data?.request_id,
|
||||
timestamp: data?.timestamp
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// 处理认证错误的函数
|
||||
const handleAuthError = (message) => {
|
||||
// 清理localStorage
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('token_type')
|
||||
|
||||
// 通过事件总线通知其他组件
|
||||
authEventBus.emitAuthError(message)
|
||||
|
||||
// 显示错误消息
|
||||
ElMessage.error(message)
|
||||
|
||||
// 跳转到登录页面(如果不在登录页面)
|
||||
if (router.currentRoute.value.path !== '/auth/login') {
|
||||
router.push('/auth/login')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出请求实例
|
||||
export default request
|
||||
|
||||
// 导出认证事件总线
|
||||
export { authEventBus }
|
||||
|
||||
// 导出响应处理工具函数
|
||||
export const handleResponse = (response) => {
|
||||
if (response.success) {
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.message || '请求失败')
|
||||
}
|
||||
|
||||
// 导出错误处理工具函数
|
||||
export const handleError = (error) => {
|
||||
if (error.type === 'business') {
|
||||
// 业务逻辑错误
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
errors: error.errors
|
||||
}
|
||||
} else if (error.type === 'http') {
|
||||
// HTTP错误
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
status: error.status
|
||||
}
|
||||
} else if (error.type === 'network') {
|
||||
// 网络错误
|
||||
return {
|
||||
success: false,
|
||||
message: error.message
|
||||
}
|
||||
}
|
||||
|
||||
// 未知错误
|
||||
return {
|
||||
success: false,
|
||||
message: '未知错误'
|
||||
}
|
||||
}
|
||||
239
src/utils/version.js
Normal file
239
src/utils/version.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 版本管理工具
|
||||
* 用于管理应用版本和token版本,实现自动退出登录和页面刷新
|
||||
*/
|
||||
|
||||
// 版本配置
|
||||
export const VERSION_CONFIG = {
|
||||
// 当前token版本,修改JWT密钥时需要更新
|
||||
TOKEN_VERSION: '1.0.0',
|
||||
|
||||
// 当前应用版本,前端更新时需要更新
|
||||
APP_VERSION: '1.0.0',
|
||||
|
||||
// 版本检查间隔(毫秒)
|
||||
CHECK_INTERVAL: 60000, // 60秒检查一次
|
||||
|
||||
// 是否启用自动版本检查
|
||||
AUTO_CHECK: true
|
||||
}
|
||||
|
||||
// 本地存储键名
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN_VERSION: 'token_version',
|
||||
APP_VERSION: 'app_version',
|
||||
LAST_CHECK_TIME: 'last_version_check_time'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地存储的版本信息
|
||||
*/
|
||||
export const getLocalVersions = () => {
|
||||
return {
|
||||
tokenVersion: localStorage.getItem(STORAGE_KEYS.TOKEN_VERSION) || '1.0.0',
|
||||
appVersion: localStorage.getItem(STORAGE_KEYS.APP_VERSION) || '1.0.0',
|
||||
lastCheckTime: localStorage.getItem(STORAGE_KEYS.LAST_CHECK_TIME) || '0'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存版本信息到本地存储
|
||||
*/
|
||||
export const saveLocalVersions = (versions) => {
|
||||
if (versions.tokenVersion) {
|
||||
localStorage.setItem(STORAGE_KEYS.TOKEN_VERSION, versions.tokenVersion)
|
||||
}
|
||||
if (versions.appVersion) {
|
||||
localStorage.setItem(STORAGE_KEYS.APP_VERSION, versions.appVersion)
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEYS.LAST_CHECK_TIME, Date.now().toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除版本信息
|
||||
*/
|
||||
export const clearLocalVersions = () => {
|
||||
localStorage.removeItem(STORAGE_KEYS.TOKEN_VERSION)
|
||||
localStorage.removeItem(STORAGE_KEYS.APP_VERSION)
|
||||
localStorage.removeItem(STORAGE_KEYS.LAST_CHECK_TIME)
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
* @param {string} version1 版本1
|
||||
* @param {string} version2 版本2
|
||||
* @returns {number} 1: version1 > version2, -1: version1 < version2, 0: 相等
|
||||
*/
|
||||
export const compareVersions = (version1, version2) => {
|
||||
const v1Parts = version1.split('.').map(Number)
|
||||
const v2Parts = version2.split('.').map(Number)
|
||||
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const v1 = v1Parts[i] || 0
|
||||
const v2 = v2Parts[i] || 0
|
||||
|
||||
if (v1 > v2) return 1
|
||||
if (v1 < v2) return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要更新
|
||||
*/
|
||||
export const checkForUpdates = () => {
|
||||
const localVersions = getLocalVersions()
|
||||
const currentTime = Date.now()
|
||||
const lastCheckTime = parseInt(localVersions.lastCheckTime)
|
||||
|
||||
// 检查是否到了检查时间
|
||||
if (currentTime - lastCheckTime < VERSION_CONFIG.CHECK_INTERVAL) {
|
||||
return { needsCheck: false }
|
||||
}
|
||||
|
||||
// 检查token版本
|
||||
const tokenVersionMatch = localVersions.tokenVersion === VERSION_CONFIG.TOKEN_VERSION
|
||||
if (!tokenVersionMatch) {
|
||||
return {
|
||||
needsCheck: true,
|
||||
needsLogout: true,
|
||||
reason: 'token_version_mismatch',
|
||||
current: localVersions.tokenVersion,
|
||||
expected: VERSION_CONFIG.TOKEN_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用版本
|
||||
const appVersionMatch = localVersions.appVersion === VERSION_CONFIG.APP_VERSION
|
||||
if (!appVersionMatch) {
|
||||
return {
|
||||
needsCheck: true,
|
||||
needsRefresh: true,
|
||||
reason: 'app_version_mismatch',
|
||||
current: localVersions.appVersion,
|
||||
expected: VERSION_CONFIG.APP_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
// 更新检查时间
|
||||
saveLocalVersions({})
|
||||
|
||||
return { needsCheck: true, upToDate: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本检查器类
|
||||
*/
|
||||
export class VersionChecker {
|
||||
constructor() {
|
||||
this.checkInterval = null
|
||||
this.isChecking = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始自动版本检查
|
||||
*/
|
||||
startAutoCheck() {
|
||||
if (!VERSION_CONFIG.AUTO_CHECK || this.checkInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
this.checkInterval = setInterval(() => {
|
||||
this.performCheck()
|
||||
}, VERSION_CONFIG.CHECK_INTERVAL)
|
||||
|
||||
console.log('版本自动检查已启动,检查间隔:', VERSION_CONFIG.CHECK_INTERVAL, 'ms')
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止自动版本检查
|
||||
*/
|
||||
stopAutoCheck() {
|
||||
if (this.checkInterval) {
|
||||
clearInterval(this.checkInterval)
|
||||
this.checkInterval = null
|
||||
console.log('版本自动检查已停止')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行版本检查
|
||||
*/
|
||||
async performCheck() {
|
||||
if (this.isChecking) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isChecking = true
|
||||
|
||||
try {
|
||||
const result = checkForUpdates()
|
||||
|
||||
if (result.needsCheck && !result.upToDate) {
|
||||
// 触发版本更新事件
|
||||
this.handleVersionUpdate(result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('版本检查失败:', error)
|
||||
} finally {
|
||||
this.isChecking = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理版本更新
|
||||
*/
|
||||
handleVersionUpdate(result) {
|
||||
if (result.needsLogout) {
|
||||
console.warn('Token版本不匹配,需要重新登录:', result)
|
||||
|
||||
// 触发退出登录事件
|
||||
const event = new CustomEvent('version:logout', {
|
||||
detail: {
|
||||
reason: result.reason,
|
||||
current: result.current,
|
||||
expected: result.expected
|
||||
}
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
if (result.needsRefresh) {
|
||||
console.log('应用版本已更新,需要刷新页面:', result)
|
||||
|
||||
// 触发页面刷新事件
|
||||
const event = new CustomEvent('version:refresh', {
|
||||
detail: {
|
||||
reason: result.reason,
|
||||
current: result.current,
|
||||
expected: result.expected
|
||||
}
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发版本检查
|
||||
*/
|
||||
async manualCheck() {
|
||||
await this.performCheck()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局版本检查器实例
|
||||
export const versionChecker = new VersionChecker()
|
||||
|
||||
// 导出默认配置
|
||||
export default {
|
||||
VERSION_CONFIG,
|
||||
getLocalVersions,
|
||||
saveLocalVersions,
|
||||
clearLocalVersions,
|
||||
compareVersions,
|
||||
checkForUpdates,
|
||||
versionChecker
|
||||
}
|
||||
Reference in New Issue
Block a user