feat: 完成情绪博物馆项目重构和功能增强 - 新增日记评论和帖子功能 - 重构前端架构,优化用户体验 - 完善WebSocket通信机制 - 更新项目文档和部署配置

This commit is contained in:
2025-07-27 10:05:59 +08:00
parent 6903ac1c0d
commit cc886cd4d5
126 changed files with 21179 additions and 15734 deletions
+282
View File
@@ -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
-229
View File
@@ -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
}
}
}
+33
View File
@@ -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
View File
@@ -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
-141
View File
@@ -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
}