feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 错误处理工具
|
||||
*/
|
||||
|
||||
import { ElMessage, ElNotification } from 'element-plus'
|
||||
|
||||
// 错误类型枚举
|
||||
export enum ErrorType {
|
||||
NETWORK = 'NETWORK',
|
||||
AUTH = 'AUTH',
|
||||
VALIDATION = 'VALIDATION',
|
||||
BUSINESS = 'BUSINESS',
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
}
|
||||
|
||||
// 错误信息接口
|
||||
export interface ErrorInfo {
|
||||
type: ErrorType
|
||||
code?: string | number
|
||||
message: string
|
||||
details?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误分类器
|
||||
*/
|
||||
export class ErrorClassifier {
|
||||
/**
|
||||
* 分析错误类型
|
||||
*/
|
||||
static classify(error: any): ErrorInfo {
|
||||
// 网络错误
|
||||
if (error.code === 'NETWORK_ERROR' || error.message?.includes('Network Error')) {
|
||||
return {
|
||||
type: ErrorType.NETWORK,
|
||||
code: error.code,
|
||||
message: '网络连接失败,请检查网络设置'
|
||||
}
|
||||
}
|
||||
|
||||
// 认证错误
|
||||
if (error.status === 401 || error.code === 401) {
|
||||
return {
|
||||
type: ErrorType.AUTH,
|
||||
code: 401,
|
||||
message: '登录已过期,请重新登录'
|
||||
}
|
||||
}
|
||||
|
||||
// 权限错误
|
||||
if (error.status === 403 || error.code === 403) {
|
||||
return {
|
||||
type: ErrorType.AUTH,
|
||||
code: 403,
|
||||
message: '没有权限访问该资源'
|
||||
}
|
||||
}
|
||||
|
||||
// 验证错误
|
||||
if (error.status === 400 || error.code === 400) {
|
||||
return {
|
||||
type: ErrorType.VALIDATION,
|
||||
code: 400,
|
||||
message: error.message || '请求参数错误'
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器错误
|
||||
if (error.status >= 500 || error.code >= 500) {
|
||||
return {
|
||||
type: ErrorType.NETWORK,
|
||||
code: error.status || error.code,
|
||||
message: '服务器内部错误,请稍后重试'
|
||||
}
|
||||
}
|
||||
|
||||
// 业务错误
|
||||
if (error.message) {
|
||||
return {
|
||||
type: ErrorType.BUSINESS,
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
details: error
|
||||
}
|
||||
}
|
||||
|
||||
// 未知错误
|
||||
return {
|
||||
type: ErrorType.UNKNOWN,
|
||||
message: '发生未知错误,请稍后重试',
|
||||
details: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理器
|
||||
*/
|
||||
export class ErrorHandler {
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
static handle(error: any, options: {
|
||||
showMessage?: boolean
|
||||
showNotification?: boolean
|
||||
logError?: boolean
|
||||
} = {}) {
|
||||
const {
|
||||
showMessage = true,
|
||||
showNotification = false,
|
||||
logError = true
|
||||
} = options
|
||||
|
||||
const errorInfo = ErrorClassifier.classify(error)
|
||||
|
||||
// 记录错误日志
|
||||
if (logError) {
|
||||
console.error('错误处理:', {
|
||||
type: errorInfo.type,
|
||||
code: errorInfo.code,
|
||||
message: errorInfo.message,
|
||||
details: errorInfo.details,
|
||||
originalError: error
|
||||
})
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
if (showMessage) {
|
||||
this.showErrorMessage(errorInfo)
|
||||
}
|
||||
|
||||
// 显示错误通知
|
||||
if (showNotification) {
|
||||
this.showErrorNotification(errorInfo)
|
||||
}
|
||||
|
||||
return errorInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误消息
|
||||
*/
|
||||
private static showErrorMessage(errorInfo: ErrorInfo) {
|
||||
switch (errorInfo.type) {
|
||||
case ErrorType.NETWORK:
|
||||
ElMessage.error({
|
||||
message: errorInfo.message,
|
||||
duration: 5000
|
||||
})
|
||||
break
|
||||
case ErrorType.AUTH:
|
||||
ElMessage.warning({
|
||||
message: errorInfo.message,
|
||||
duration: 3000
|
||||
})
|
||||
break
|
||||
case ErrorType.VALIDATION:
|
||||
ElMessage.warning({
|
||||
message: errorInfo.message,
|
||||
duration: 3000
|
||||
})
|
||||
break
|
||||
case ErrorType.BUSINESS:
|
||||
ElMessage.error({
|
||||
message: errorInfo.message,
|
||||
duration: 4000
|
||||
})
|
||||
break
|
||||
default:
|
||||
ElMessage.error({
|
||||
message: errorInfo.message,
|
||||
duration: 4000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误通知
|
||||
*/
|
||||
private static showErrorNotification(errorInfo: ErrorInfo) {
|
||||
ElNotification.error({
|
||||
title: '错误提示',
|
||||
message: errorInfo.message,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证相关错误
|
||||
*/
|
||||
static handleAuthError(error: any) {
|
||||
const errorInfo = this.handle(error, {
|
||||
showMessage: true,
|
||||
logError: true
|
||||
})
|
||||
|
||||
// 如果是认证错误,可能需要跳转到登录页
|
||||
if (errorInfo.type === ErrorType.AUTH && errorInfo.code === 401) {
|
||||
// 清除本地认证信息
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user_info')
|
||||
|
||||
// 延迟跳转,让用户看到错误消息
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return errorInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理API请求错误
|
||||
*/
|
||||
static handleApiError(error: any, context?: string) {
|
||||
const contextMessage = context ? `${context}: ` : ''
|
||||
|
||||
const errorInfo = this.handle(error, {
|
||||
showMessage: true,
|
||||
logError: true
|
||||
})
|
||||
|
||||
// 添加上下文信息
|
||||
if (context) {
|
||||
console.error(`${contextMessage}`, errorInfo)
|
||||
}
|
||||
|
||||
return errorInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理表单验证错误
|
||||
*/
|
||||
static handleValidationError(error: any, fieldName?: string) {
|
||||
let message = error.message || '表单验证失败'
|
||||
|
||||
if (fieldName) {
|
||||
message = `${fieldName}: ${message}`
|
||||
}
|
||||
|
||||
ElMessage.warning({
|
||||
message,
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
return {
|
||||
type: ErrorType.VALIDATION,
|
||||
message,
|
||||
details: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理装饰器
|
||||
*/
|
||||
export function handleError(options?: {
|
||||
showMessage?: boolean
|
||||
showNotification?: boolean
|
||||
logError?: boolean
|
||||
}) {
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
try {
|
||||
return await originalMethod.apply(this, args)
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error, options)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
// 导出常用方法
|
||||
export const handleApiError = ErrorHandler.handleApiError
|
||||
export const handleAuthError = ErrorHandler.handleAuthError
|
||||
export const handleValidationError = ErrorHandler.handleValidationError
|
||||
@@ -1,229 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
// 配置dayjs
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 时间格式化
|
||||
export const formatTime = {
|
||||
// 相对时间
|
||||
relative: (date: string | Date) => dayjs(date).fromNow(),
|
||||
|
||||
// 标准格式
|
||||
standard: (date: string | Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
|
||||
// 日期格式
|
||||
date: (date: string | Date) => dayjs(date).format('YYYY-MM-DD'),
|
||||
|
||||
// 时间格式
|
||||
time: (date: string | Date) => dayjs(date).format('HH:mm:ss'),
|
||||
|
||||
// 友好格式
|
||||
friendly: (date: string | Date) => {
|
||||
const now = dayjs()
|
||||
const target = dayjs(date)
|
||||
const diffDays = now.diff(target, 'day')
|
||||
|
||||
if (diffDays === 0) {
|
||||
return target.format('HH:mm')
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天 ' + target.format('HH:mm')
|
||||
} else if (diffDays < 7) {
|
||||
return target.format('M月D日 HH:mm')
|
||||
} else {
|
||||
return target.format('YYYY年M月D日')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
export const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
export const throttle = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let lastTime = 0
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
const now = Date.now()
|
||||
if (now - lastTime >= wait) {
|
||||
lastTime = now
|
||||
func(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
export const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
// 深拷贝
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as T
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => deepClone(item)) as T
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {} as T
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// 文件大小格式化
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
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]
|
||||
}
|
||||
|
||||
// 数字格式化
|
||||
export const formatNumber = (num: number): string => {
|
||||
if (num < 1000) return num.toString()
|
||||
if (num < 10000) return (num / 1000).toFixed(1) + 'K'
|
||||
if (num < 100000000) return (num / 10000).toFixed(1) + '万'
|
||||
return (num / 100000000).toFixed(1) + '亿'
|
||||
}
|
||||
|
||||
// 颜色工具
|
||||
export const colorUtils = {
|
||||
// 十六进制转RGB
|
||||
hexToRgb: (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null
|
||||
},
|
||||
|
||||
// RGB转十六进制
|
||||
rgbToHex: (r: number, g: number, b: number) => {
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||
},
|
||||
|
||||
// 获取随机颜色
|
||||
random: () => {
|
||||
return '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储工具
|
||||
export const storage = {
|
||||
set: (key: string, value: any) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
} catch (error) {
|
||||
console.error('Storage set error:', error)
|
||||
}
|
||||
},
|
||||
|
||||
get: <T = any>(key: string, defaultValue?: T): T | null => {
|
||||
try {
|
||||
const item = localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : defaultValue || null
|
||||
} catch (error) {
|
||||
console.error('Storage get error:', error)
|
||||
return defaultValue || null
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key: string) => {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('Storage remove error:', error)
|
||||
}
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
try {
|
||||
localStorage.clear()
|
||||
} catch (error) {
|
||||
console.error('Storage clear error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// URL工具
|
||||
export const urlUtils = {
|
||||
// 获取查询参数
|
||||
getQuery: (name: string): string | null => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
return urlParams.get(name)
|
||||
},
|
||||
|
||||
// 设置查询参数
|
||||
setQuery: (params: Record<string, string>) => {
|
||||
const url = new URL(window.location.href)
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
},
|
||||
|
||||
// 删除查询参数
|
||||
removeQuery: (name: string) => {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete(name)
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// 设备检测
|
||||
export const deviceUtils = {
|
||||
isMobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
|
||||
isIOS: () => /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
isAndroid: () => /Android/.test(navigator.userAgent),
|
||||
isWechat: () => /MicroMessenger/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
// 验证工具
|
||||
export const validators = {
|
||||
email: (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
|
||||
phone: (phone: string) => /^1[3-9]\d{9}$/.test(phone),
|
||||
password: (password: string) => password.length >= 6,
|
||||
url: (url: string) => {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* MessageService 测试工具
|
||||
* 用于验证消息服务是否正常工作
|
||||
*/
|
||||
|
||||
import MessageService from '@/services/message'
|
||||
|
||||
export const testMessageService = async () => {
|
||||
console.log('🧪 开始测试 MessageService...')
|
||||
|
||||
try {
|
||||
// 测试获取最近消息
|
||||
console.log('📝 测试获取最近消息...')
|
||||
const recentMessages = await MessageService.getRecentMessages({ limit: 5 })
|
||||
console.log('✅ 最近消息:', recentMessages)
|
||||
|
||||
// 测试分页获取消息
|
||||
console.log('📄 测试分页获取消息...')
|
||||
const pageMessages = await MessageService.getUserMessages(1, 10)
|
||||
console.log('✅ 分页消息:', pageMessages)
|
||||
|
||||
console.log('🎉 MessageService 测试完成!')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ MessageService 测试失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 在开发环境下可以在控制台调用 window.testMessageService() 进行测试
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).testMessageService = testMessageService
|
||||
}
|
||||
+234
-104
@@ -1,118 +1,248 @@
|
||||
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import router from '@/router'
|
||||
/**
|
||||
* HTTP请求工具
|
||||
* 基于axios封装的统一请求实例
|
||||
*/
|
||||
|
||||
// 获取API基础URL
|
||||
const getApiBaseUrl = () => {
|
||||
// 开发环境使用代理
|
||||
if (import.meta.env.DEV) {
|
||||
return '/api'
|
||||
}
|
||||
// 生产环境使用环境变量
|
||||
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api'
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { envConfig } from '@/config/env'
|
||||
|
||||
// 请求响应接口
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
success: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 请求配置接口
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
// 是否显示loading
|
||||
showLoading?: boolean
|
||||
// 是否显示错误消息
|
||||
showError?: boolean
|
||||
// 是否需要token
|
||||
needToken?: boolean
|
||||
}
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: getApiBaseUrl(),
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('API Base URL:', getApiBaseUrl())
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 从localStorage获取token
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (token && config.headers) {
|
||||
// 在请求头中添加Authorization
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
const createAxiosInstance = (): AxiosInstance => {
|
||||
const instance = axios.create({
|
||||
baseURL: envConfig.apiBaseUrl,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
|
||||
console.log('发送请求:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
hasToken: !!token,
|
||||
headers: config.headers
|
||||
})
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求拦截器错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data } = response
|
||||
// 标准后端格式: { code, message, data, timestamp }
|
||||
if (typeof data === 'object' && data !== null && 'code' in data) {
|
||||
if (data.code !== 200) {
|
||||
message.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config: any) => {
|
||||
// 添加token
|
||||
const token = localStorage.getItem('access_token')
|
||||
console.log('🔑 请求拦截器 - Token状态:', {
|
||||
hasToken: !!token,
|
||||
tokenPreview: token ? `${token.substring(0, 20)}...` : 'null',
|
||||
url: config.url,
|
||||
needToken: config.needToken
|
||||
})
|
||||
|
||||
if (token && config.needToken !== false) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('🔑 已添加Authorization头')
|
||||
} else {
|
||||
console.log('🔑 未添加Authorization头 - 原因:', !token ? '无token' : 'needToken=false')
|
||||
}
|
||||
// 只返回data字段, 兼容验证码等所有接口
|
||||
return data.data
|
||||
|
||||
// 添加请求ID用于追踪
|
||||
config.headers['X-Request-ID'] = generateRequestId()
|
||||
|
||||
// 打印请求日志
|
||||
if (envConfig.debug) {
|
||||
console.log('🚀 Request:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
headers: config.headers
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.error('❌ Request Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
// 兼容极特殊情况(如验证码图片流等)
|
||||
return data
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应拦截器错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
const { data } = response
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// token过期或无效
|
||||
message.error('登录已过期,请重新登录')
|
||||
|
||||
// 清除本地存储的用户信息
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
|
||||
// 清除store中的用户信息
|
||||
const userStore = useUserStore()
|
||||
userStore.setToken('')
|
||||
userStore.setUserInfo(null)
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
break
|
||||
|
||||
case 403:
|
||||
message.error('没有权限访问该资源')
|
||||
break
|
||||
|
||||
case 404:
|
||||
message.error('请求的资源不存在')
|
||||
break
|
||||
|
||||
case 500:
|
||||
message.error('服务器内部错误')
|
||||
break
|
||||
|
||||
default:
|
||||
message.error(data?.message || '请求失败')
|
||||
// 打印响应日志
|
||||
if (envConfig.debug) {
|
||||
console.log('✅ Response:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: data
|
||||
})
|
||||
}
|
||||
} else if (error.request) {
|
||||
message.error('网络连接失败,请检查网络')
|
||||
} else {
|
||||
message.error('请求配置错误')
|
||||
|
||||
// 检查业务状态码
|
||||
if (data.code === 200 || data.success) {
|
||||
return data
|
||||
}
|
||||
|
||||
// 处理业务错误
|
||||
const errorMessage = data.message || '请求失败'
|
||||
|
||||
// 特殊错误码处理
|
||||
switch (data.code) {
|
||||
case 401:
|
||||
console.warn('🚫 业务层401错误:', errorMessage)
|
||||
// 只有在非登录接口时才处理401
|
||||
if (!response.config.url?.includes('/auth/login')) {
|
||||
handleUnauthorized()
|
||||
} else {
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('没有权限访问该资源')
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(errorMessage))
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.error('❌ Response Error:', error)
|
||||
|
||||
let errorMessage = '网络请求失败'
|
||||
|
||||
if (error.response) {
|
||||
// 服务器响应错误
|
||||
const { status, data } = error.response
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
errorMessage = '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
errorMessage = '未授权,请重新登录'
|
||||
console.warn('🚫 HTTP层401错误')
|
||||
// 只有在非登录接口时才处理401
|
||||
if (!error.config?.url?.includes('/auth/login')) {
|
||||
handleUnauthorized()
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
errorMessage = '拒绝访问'
|
||||
break
|
||||
case 404:
|
||||
errorMessage = '请求地址不存在'
|
||||
break
|
||||
case 408:
|
||||
errorMessage = '请求超时'
|
||||
break
|
||||
case 500:
|
||||
errorMessage = '服务器内部错误'
|
||||
break
|
||||
case 502:
|
||||
errorMessage = '网关错误'
|
||||
break
|
||||
case 503:
|
||||
errorMessage = '服务不可用'
|
||||
break
|
||||
case 504:
|
||||
errorMessage = '网关超时'
|
||||
break
|
||||
default:
|
||||
errorMessage = (data as any)?.message || `请求失败 (${status})`
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 网络错误
|
||||
errorMessage = '网络连接失败,请检查网络'
|
||||
} else {
|
||||
// 其他错误
|
||||
errorMessage = error.message || '请求配置错误'
|
||||
}
|
||||
|
||||
ElMessage.error(errorMessage)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// 处理未授权
|
||||
const handleUnauthorized = () => {
|
||||
console.warn('🚫 收到401未授权响应')
|
||||
|
||||
// 检查当前页面是否是登录页,避免在登录页面重复处理
|
||||
if (window.location.pathname === '/login') {
|
||||
console.log('🚫 当前在登录页面,不处理401错误')
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
// 不立即清除认证信息,而是提示用户
|
||||
ElMessageBox.confirm(
|
||||
'登录状态已过期,请重新登录',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '重新登录',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
// 用户确认后才清除认证信息
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user_info')
|
||||
|
||||
// 跳转到登录页
|
||||
window.location.href = '/login'
|
||||
}).catch(() => {
|
||||
// 用户取消,不清除认证信息,让用户继续操作
|
||||
console.log('🚫 用户取消重新登录')
|
||||
})
|
||||
}
|
||||
|
||||
// 生成请求ID
|
||||
const generateRequestId = (): string => {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
||||
}
|
||||
|
||||
// 创建请求实例
|
||||
export const request = createAxiosInstance()
|
||||
|
||||
// 导出请求方法
|
||||
export const http = {
|
||||
get: <T = any>(url: string, config?: RequestConfig) =>
|
||||
request.get<any, ApiResponse<T>>(url, config),
|
||||
|
||||
post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
|
||||
request.post<any, ApiResponse<T>>(url, data, config),
|
||||
|
||||
put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
|
||||
request.put<any, ApiResponse<T>>(url, data, config),
|
||||
|
||||
delete: <T = any>(url: string, config?: RequestConfig) =>
|
||||
request.delete<any, ApiResponse<T>>(url, config),
|
||||
|
||||
patch: <T = any>(url: string, data?: any, config?: RequestConfig) =>
|
||||
request.patch<any, ApiResponse<T>>(url, data, config)
|
||||
}
|
||||
|
||||
export default request
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* WebSocket连接测试工具
|
||||
* 用于测试WebSocket连接和消息发送功能
|
||||
*/
|
||||
|
||||
import webSocketService from '@/services/websocket'
|
||||
|
||||
export class WebSocketTester {
|
||||
private isConnected = false
|
||||
private testResults: string[] = []
|
||||
|
||||
/**
|
||||
* 运行WebSocket连接测试
|
||||
*/
|
||||
async runConnectionTest(): Promise<boolean> {
|
||||
this.testResults = []
|
||||
this.log('开始WebSocket连接测试...')
|
||||
|
||||
try {
|
||||
// 测试连接
|
||||
await webSocketService.connect('test_user_' + Date.now(), {
|
||||
onConnect: () => {
|
||||
this.isConnected = true
|
||||
this.log('✅ WebSocket连接成功')
|
||||
},
|
||||
onDisconnect: () => {
|
||||
this.isConnected = false
|
||||
this.log('❌ WebSocket连接断开')
|
||||
},
|
||||
onError: (error) => {
|
||||
this.log(`❌ WebSocket错误: ${error.userMessage || error.message || '未知错误'}`)
|
||||
},
|
||||
onMessage: (message) => {
|
||||
this.log(`📨 收到消息: ${message.type} - ${message.content}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 等待连接建立
|
||||
await this.waitForConnection(5000)
|
||||
|
||||
if (this.isConnected) {
|
||||
this.log('✅ 连接测试通过')
|
||||
return true
|
||||
} else {
|
||||
this.log('❌ 连接测试失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`❌ 连接测试异常: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试消息发送
|
||||
*/
|
||||
async testMessageSending(): Promise<boolean> {
|
||||
if (!this.isConnected) {
|
||||
this.log('❌ 未连接,无法测试消息发送')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('开始测试消息发送...')
|
||||
|
||||
// 设置测试会话ID
|
||||
webSocketService.setConversationId('test_conversation_' + Date.now())
|
||||
|
||||
// 发送测试消息
|
||||
webSocketService.sendChatMessage('这是一条测试消息')
|
||||
|
||||
this.log('✅ 消息发送成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
this.log(`❌ 消息发送失败: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接测试
|
||||
*/
|
||||
testDisconnection(): void {
|
||||
this.log('开始测试断开连接...')
|
||||
webSocketService.disconnect()
|
||||
this.log('✅ 断开连接完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试结果
|
||||
*/
|
||||
getTestResults(): string[] {
|
||||
return [...this.testResults]
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空测试结果
|
||||
*/
|
||||
clearResults(): void {
|
||||
this.testResults = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录测试日志
|
||||
*/
|
||||
private log(message: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
const logMessage = `[${timestamp}] ${message}`
|
||||
this.testResults.push(logMessage)
|
||||
console.log(logMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待连接建立
|
||||
*/
|
||||
private waitForConnection(timeout: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
const checkConnection = () => {
|
||||
if (this.isConnected) {
|
||||
resolve()
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error('连接超时'))
|
||||
} else {
|
||||
setTimeout(checkConnection, 100)
|
||||
}
|
||||
}
|
||||
|
||||
checkConnection()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出测试实例
|
||||
export const wsTest = new WebSocketTester()
|
||||
|
||||
// 开发环境下添加到全局对象,方便调试
|
||||
if (import.meta.env.DEV) {
|
||||
(window as any).wsTest = wsTest
|
||||
}
|
||||
Reference in New Issue
Block a user