refactor(frontend): 重命名前端项目为 gym-manage-web

This commit is contained in:
张翔
2026-04-17 18:37:45 +08:00
parent deb961c427
commit 45bb89fc7f
140 changed files with 2 additions and 2 deletions
+44
View File
@@ -0,0 +1,44 @@
import { format, parseISO } from 'date-fns'
import { zhCN } from 'date-fns/locale'
export function formatDateTime(date: string | Date | null | undefined): string {
if (!date) {
return '-'
}
try {
const dateObj = typeof date === 'string' ? parseISO(date) : date
return format(dateObj, 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })
} catch (error) {
console.error('时间格式化失败:', error)
return '-'
}
}
export function formatDate(date: string | Date | null | undefined): string {
if (!date) {
return '-'
}
try {
const dateObj = typeof date === 'string' ? parseISO(date) : date
return format(dateObj, 'yyyy-MM-dd', { locale: zhCN })
} catch (error) {
console.error('日期格式化失败:', error)
return '-'
}
}
export function formatTime(date: string | Date | null | undefined): string {
if (!date) {
return '-'
}
try {
const dateObj = typeof date === 'string' ? parseISO(date) : date
return format(dateObj, 'HH:mm:ss', { locale: zhCN })
} catch (error) {
console.error('时间格式化失败:', error)
return '-'
}
}
+113
View File
@@ -0,0 +1,113 @@
import { ElMessage } from 'element-plus'
export interface ApiError {
code: string
message: string
details?: Record<string, any>
timestamp: string
path: string
}
export class ApiErrorHandler {
static handle(error: any): void {
if (!error.response) {
this.handleNetworkError(error)
return
}
const { status, data } = error.response
const apiError = data as ApiError
switch (status) {
case 400:
this.handleBadRequest(apiError)
break
case 401:
this.handleUnauthorized(apiError)
break
case 403:
this.handleForbidden(apiError)
break
case 404:
this.handleNotFound(apiError)
break
case 409:
this.handleConflict(apiError)
break
case 422:
this.handleValidationError(apiError)
break
case 500:
this.handleInternalServerError(apiError)
break
case 502:
case 503:
case 504:
this.handleServiceUnavailable(apiError)
break
default:
this.handleUnknownError(apiError)
}
}
private static handleNetworkError(error: any): void {
ElMessage.error('网络连接失败,请检查网络设置')
console.error('Network Error:', error)
}
private static handleBadRequest(error: ApiError): void {
ElMessage.error(error.message || '请求参数错误')
console.error('Bad Request:', error)
}
private static handleUnauthorized(error: ApiError): void {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
window.location.href = '/login'
console.error('Unauthorized:', error)
}
private static handleForbidden(error: ApiError): void {
ElMessage.error('没有权限访问该资源')
console.error('Forbidden:', error)
}
private static handleNotFound(error: ApiError): void {
ElMessage.error(error.message || '请求的资源不存在')
console.error('Not Found:', error)
}
private static handleConflict(error: ApiError): void {
ElMessage.error(error.message || '资源冲突,请刷新后重试')
console.error('Conflict:', error)
}
private static handleValidationError(error: ApiError): void {
if (error.details) {
const messages = Object.values(error.details).join('、')
ElMessage.error(messages)
} else {
ElMessage.error(error.message || '数据验证失败')
}
console.error('Validation Error:', error)
}
private static handleInternalServerError(error: ApiError): void {
ElMessage.error('服务器内部错误,请稍后重试')
console.error('Internal Server Error:', error)
}
private static handleServiceUnavailable(error: ApiError): void {
ElMessage.error('服务暂时不可用,请稍后重试')
console.error('Service Unavailable:', error)
}
private static handleUnknownError(error: ApiError): void {
ElMessage.error(error.message || '未知错误')
console.error('Unknown Error:', error)
}
}
export const handleApiError = (error: any): void => {
ApiErrorHandler.handle(error)
}
+57
View File
@@ -0,0 +1,57 @@
import { usePermissionStore } from '@/stores/permission'
export interface PermissionMapping {
[key: string]: string | string[]
}
const permissionMapping: PermissionMapping = {
'GET /users': 'system:user:list',
'POST /users': 'system:user:add',
'PUT /users': 'system:user:edit',
'DELETE /users': 'system:user:remove',
'GET /roles': 'system:role:list',
'POST /roles': 'system:role:add',
'PUT /roles': 'system:role:edit',
'DELETE /roles': 'system:role:remove',
'GET /menus': 'system:menu:list',
'POST /menus': 'system:menu:add',
'PUT /menus': 'system:menu:edit',
'DELETE /menus': 'system:menu:remove',
'GET /dict': 'system:dict:list',
'POST /dict': 'system:dict:add',
'PUT /dict': 'system:dict:edit',
'DELETE /dict': 'system:dict:remove',
'GET /sys/config': 'system:config:list',
'POST /sys/config': 'system:config:add',
'PUT /sys/config': 'system:config:edit',
'DELETE /sys/config': 'system:config:remove',
'GET /files': 'system:file:list',
'POST /files': 'system:file:upload',
'DELETE /files': 'system:file:delete',
}
export function checkApiPermission(method: string, url: string): boolean {
const permissionStore = usePermissionStore()
const key = `${method.toUpperCase()} ${url.split('?')[0]}`
const requiredPermission = permissionMapping[key]
if (!requiredPermission) {
return true
}
if (key === 'GET /menus') {
return true
}
if (Array.isArray(requiredPermission)) {
return requiredPermission.some(p => permissionStore.hasPermission(p))
}
return permissionStore.hasPermission(requiredPermission)
}
export function getRequiredPermission(method: string, url: string): string | string[] | null {
const key = `${method.toUpperCase()} ${url.split('?')[0]}`
return permissionMapping[key] || null
}
+68
View File
@@ -0,0 +1,68 @@
import axios, { AxiosRequestConfig } from 'axios'
import { generateSignatureHeaders } from './signature'
import { checkApiPermission } from './permission'
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
request.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('token')
if (token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${token}`
}
const method = config.method?.toUpperCase() || 'GET'
let url = config.url || ''
const body = config.data
if (config.params && Object.keys(config.params).length > 0) {
const queryParams = new URLSearchParams()
Object.entries(config.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.append(key, String(value))
}
})
const queryString = queryParams.toString()
if (queryString) {
url += (url.includes('?') ? '&' : '?') + queryString
}
}
const fullPath = `/api${url.startsWith('/') ? url : '/' + url}`
const signatureHeaders = generateSignatureHeaders(method, fullPath, body)
config.headers = config.headers || {}
Object.assign(config.headers, signatureHeaders)
if (!checkApiPermission(method, url)) {
const error = new Error('无权限访问此接口')
;(error as any).response = {
status: 403,
data: { message: '无权限访问此接口' }
}
return Promise.reject(error)
}
return config
},
(error) => Promise.reject(error)
)
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
if (!window.location.pathname.includes('/login')) {
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)
export default request
+96
View File
@@ -0,0 +1,96 @@
import CryptoJS from 'crypto-js'
const SIGNATURE_SECRET = import.meta.env.VITE_SIGNATURE_SECRET || 'NovalonManageSystemSecretKey2026'
export interface SignatureHeaders {
'X-Signature': string
'X-Timestamp': string
'X-Nonce': string
}
export function generateSignature(
method: string,
path: string,
query: string = '',
body: string = '',
timestamp: number,
nonce: string
): string {
const stringToSign = buildStringToSign(method, path, query, '', timestamp, nonce)
const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET)
const signatureBase64 = CryptoJS.enc.Base64.stringify(signature)
return signatureBase64
}
export function generateSignatureHeaders(
method: string,
url: string,
body?: any
): SignatureHeaders {
const timestamp = Date.now()
const nonce = generateNonce()
const { path, query } = parseUrl(url)
const bodyString = body ? JSON.stringify(body) : ''
const signature = generateSignature(
method.toUpperCase(),
path,
query || '',
bodyString,
timestamp,
nonce
)
return {
'X-Signature': signature,
'X-Timestamp': timestamp.toString(),
'X-Nonce': nonce
}
}
function buildStringToSign(
method: string,
path: string,
query: string,
body: string,
timestamp: number,
nonce: string
): string {
return [
method,
path,
query || '',
body || '',
timestamp.toString(),
nonce
].join('\n')
}
function generateNonce(): string {
const timestamp = Date.now().toString(36)
const randomPart = Math.random().toString(36).substring(2, 15)
return `${timestamp}-${randomPart}`
}
function parseUrl(url: string): { path: string; query: string } {
if (url.startsWith('http://') || url.startsWith('https://')) {
const urlObj = new URL(url)
return {
path: urlObj.pathname,
query: urlObj.search.substring(1)
}
}
const queryIndex = url.indexOf('?')
if (queryIndex === -1) {
return { path: url, query: '' }
}
return {
path: url.substring(0, queryIndex),
query: url.substring(queryIndex + 1)
}
}