diff --git a/src/api/interceptors/request.js b/src/api/interceptors/request.js index 1d8c8c0..6e180c1 100644 --- a/src/api/interceptors/request.js +++ b/src/api/interceptors/request.js @@ -12,6 +12,7 @@ export function setupRequestInterceptors(instance) { // 返回修改后的请求配置,继续发送请求 return config }, error => { + console.log("请求错误",error) // 将错误以 Promise reject 的方式抛出,供调用方捕获处理 return Promise.reject(error) }) diff --git a/src/api/interceptors/response.js b/src/api/interceptors/response.js index 8a5e4d3..a5207d6 100644 --- a/src/api/interceptors/response.js +++ b/src/api/interceptors/response.js @@ -1,7 +1,8 @@ //响应拦截器 import axios from 'axios'; // 引入axios用于创建新请求 -import { ElMessage } from 'element-plus'; // 引入Element Plus的消息组件 +import { ElMessage, ElMessageBox } from 'element-plus'; // 引入Element Plus的消息组件 import store from '../../store'; // 引入store用于统一处理登出 +import axiosInstance from '../../utils/axios'; // 引入已配置的axios实例 export function setupResponseInterceptors(instance) { // 用于存储正在刷新token的Promise @@ -11,16 +12,25 @@ export function setupResponseInterceptors(instance) { // 统一处理登录过期的函数 const handleTokenExpired = (message = '登录已过期,请重新登录') => { - // 显示提示消息 - ElMessage.error(message); - - // 使用store的logout action清除认证状态 - store.dispatch('logout'); - - // 延迟跳转,让用户有时间看到提示 - setTimeout(() => { + // 显示确认对话框 + ElMessageBox.confirm( + message, + '提示', + { + confirmButtonText: '重新登录', + type: 'warning', + showCancelButton: false, + center: true + } + ).then(() => { + // 用户点击确认按钮后执行 + // 使用store的logout action清除认证状态 + store.dispatch('logout'); + // 跳转到登录页 window.location.href = '/login'; - }, 1500); + }).catch(() => { + // 用户关闭对话框,不做任何操作 + }); }; instance.interceptors.response.use( @@ -35,9 +45,7 @@ export function setupResponseInterceptors(instance) { } return data; }, - async error => { - console.log('响应错误:', error); - + async error => { // 获取原始请求配置 const originalRequest = error.config; @@ -51,10 +59,10 @@ export function setupResponseInterceptors(instance) { const errorResponse = error.response; console.log('错误响应:', errorResponse); const errorData = errorResponse.data || {}; - const errorMessage = errorData.message || errorData.error || errorResponse.statusText || ''; + const errorMessage = errorData.status || errorData.message || errorData.error || errorResponse.statusText || ''; const errorStack = errorData.stack || ''; const statusCode = errorResponse.status; - + console.log('错误响应:', statusCode); // 处理403错误(权限不足) if (errorResponse.status === "403") { // 显示权限不足提示,但不重定向 @@ -63,21 +71,12 @@ export function setupResponseInterceptors(instance) { } // 检查是否为令牌错误(检查多种可能的错误信息格式) - const isTokenError = - statusCode === 401 || - (statusCode === 500 && ( - errorMessage.includes('令牌') || - errorMessage.includes('token') || - errorMessage.includes('Token') || - errorStack.includes('TokenIllegal') || - errorStack.includes('TheTokenIllegalException') || - (typeof errorData === 'string' && errorData.includes('令牌')) - )); - + const isTokenError = statusCode === 500 + // 如果是令牌错误,尝试刷新令牌或直接跳转登录页 if (isTokenError) { // 如果是401错误且没有在刷新中,尝试刷新令牌 - if (statusCode === 401 && !originalRequest._retry) { + if (statusCode === 401 || statusCode === 500 || statusCode === 403 && !originalRequest._retry ) { // 标记该请求已尝试过重试 originalRequest._retry = true; @@ -88,7 +87,6 @@ export function setupResponseInterceptors(instance) { try { // 从localStorage获取refreshToken const refreshToken = localStorage.getItem('refreshToken'); - if (!refreshToken) { // 无refreshToken,跳转到登录页 handleTokenExpired(); @@ -99,20 +97,26 @@ export function setupResponseInterceptors(instance) { const formData = new FormData(); formData.append('refreshToken', refreshToken); - // 调用刷新token接口 - const response = await axios.post('/admin/getAccessToken', formData); + // 调用刷新token接口,使用配置好的axios实例 + const response = await axiosInstance.post('/admin/getAccessToken', formData); - // 获取新的token - const { accessToken, refreshToken: newRefreshToken } = response.data.data; + // 根据实际返回的数据结构获取token + const responseData = response.data; + const accessToken = responseData.accessToken; + const newRefreshToken = responseData.refreshToken; // 更新本地存储 localStorage.setItem('accessToken', accessToken); - if (newRefreshToken) { - localStorage.setItem('refreshToken', newRefreshToken); - } + localStorage.setItem('refreshToken', newRefreshToken); // 更新当前请求的Authorization头 - originalRequest.headers.Authorization = accessToken; + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + + // 更新全局axios和axiosInstance的默认Authorization头,确保后续所有请求都带上新token + axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; + axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; + + console.log('新token设置成功:', accessToken); // 执行队列中的所有请求 requests.forEach(cb => cb(accessToken)); diff --git a/src/store/Index.js b/src/store/Index.js index 114eadf..536a9b8 100644 --- a/src/store/Index.js +++ b/src/store/Index.js @@ -1,5 +1,6 @@ import { createStore } from 'vuex' import { adminApi } from '../api/index.js' +import webSocketService from '../utils/webSocket.js' export default createStore({ state: { @@ -31,6 +32,9 @@ export default createStore({ localStorage.removeItem('refreshToken') // 清除本地缓存 localStorage.removeItem('userInfo') + + // 断开WebSocket连接 + webSocketService.disconnect() }, SET_USER_INFO(state, userInfo) { // 初始化 用户信息 @@ -44,7 +48,7 @@ export default createStore({ try { // 调用登录接口 const response = await adminApi.login(data) - + console.log("响应",response) // 检查响应状态 if (response.code !== 200) { throw new Error(response.message || '登录失败') @@ -52,6 +56,17 @@ export default createStore({ // 设置 状态 commit('SET_TOKEN', response.data) + + // 添加更多日志,确保token存在 + console.log("登录成功,准备连接WebSocket") + console.log("accessToken值:", response.data.accessToken) + + // 登录成功后连接WebSocket + if (response.data.accessToken) { + webSocketService.connect(response.data.accessToken) + } else { + console.error("无法连接WebSocket: accessToken不存在") + } try { // 调用查询接口获取用户信息 diff --git a/src/utils/webSocket.js b/src/utils/webSocket.js new file mode 100644 index 0000000..b025959 --- /dev/null +++ b/src/utils/webSocket.js @@ -0,0 +1,181 @@ +/** + * WebSocket连接工具类 + */ + +// WebSocket URL +const WS_URL = 'ws://146.56.192.164:9090/ws'; + +class WebSocketService { + constructor() { + this.socket = null; + this.isConnected = false; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.reconnectInterval = 3000; // 3秒 + this.listeners = new Map(); + } + + /** + * 初始化WebSocket连接 + */ + connect() { + if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) { + console.log('WebSocket已连接或正在连接中'); + return; + } + + try { + // 获取存储在localStorage中的token + const token = localStorage.getItem('accessToken'); + if (!token) { + console.error('WebSocket连接失败: 未找到accessToken'); + return; + } + + // 创建WebSocket连接,通过传递header中的token + this.socket = new WebSocket(WS_URL+'?token='+token); + + // 由于浏览器WebSocket API不允许直接设置headers,我们需要在连接建立后的第一条消息中发送token + this.socket.onopen = () => { + console.log('WebSocket连接已建立'); + this.isConnected = true; + this.reconnectAttempts = 0; + + // 连接后立即发送认证信息 + this.sendMessage({ + type: 'auth', + token: token + }); + + // 触发已注册的连接事件监听器 + this.triggerEvent('connect'); + }; + + this.socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('收到WebSocket消息:', data); + this.triggerEvent('message', data); + } catch (error) { + console.error('解析WebSocket消息失败:', error); + this.triggerEvent('error', error); + } + }; + + this.socket.onclose = (event) => { + console.log('WebSocket连接已关闭:', event); + this.isConnected = false; + this.triggerEvent('disconnect'); + + // 尝试重连 + this.attemptReconnect(); + }; + + this.socket.onerror = (error) => { + console.error('WebSocket错误:', error); + this.triggerEvent('error', error); + }; + } catch (error) { + console.error('初始化WebSocket失败:', error); + this.triggerEvent('error', error); + } + } + + /** + * 尝试重新连接 + */ + attemptReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.log('达到最大重连次数'); + return; + } + + this.reconnectAttempts++; + console.log(`尝试第 ${this.reconnectAttempts} 次重连...`); + + setTimeout(() => { + this.connect(); + }, this.reconnectInterval); + } + + /** + * 发送消息到WebSocket服务器 + * @param {Object} data - 要发送的数据 + */ + sendMessage(data) { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + console.error('WebSocket未连接,无法发送消息'); + return false; + } + + try { + const message = typeof data === 'string' ? data : JSON.stringify(data); + this.socket.send(message); + return true; + } catch (error) { + console.error('发送WebSocket消息失败:', error); + return false; + } + } + + /** + * 关闭WebSocket连接 + */ + disconnect() { + if (this.socket) { + this.socket.close(); + this.socket = null; + this.isConnected = false; + } + } + + /** + * 注册事件监听器 + * @param {string} event - 事件名称 + * @param {Function} callback - 回调函数 + */ + on(event, callback) { + if (!this.listeners.has(event)) { + this.listeners.set(event, []); + } + this.listeners.get(event).push(callback); + } + + /** + * 注销事件监听器 + * @param {string} event - 事件名称 + * @param {Function} callback - 回调函数 + */ + off(event, callback) { + if (!this.listeners.has(event)) return; + + const callbacks = this.listeners.get(event); + const index = callbacks.indexOf(callback); + if (index !== -1) { + callbacks.splice(index, 1); + } + } + + /** + * 触发事件 + * @param {string} event - 事件名称 + * @param {*} data - 事件数据 + */ + triggerEvent(event, data) { + if (!this.listeners.has(event)) return; + + const callbacks = this.listeners.get(event); + callbacks.forEach(callback => { + try { + callback(data); + } catch (error) { + console.error(`执行${event}事件监听器出错:`, error); + } + }); + } +} + +// 创建单例实例 +const webSocketService = new WebSocketService(); + +export default webSocketService; \ No newline at end of file diff --git a/src/views/Login/index.vue b/src/views/Login/index.vue index 7f3d546..3fe6ef0 100644 --- a/src/views/Login/index.vue +++ b/src/views/Login/index.vue @@ -29,7 +29,8 @@ import { ref, reactive, getCurrentInstance, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' - import store from '../../store' + import store from '../../store/Index.js' + import { nextTick } from 'vue' // 获取 全局变量 const global = getCurrentInstance()?.appContext.config.globalProperties.$global @@ -81,13 +82,21 @@ // 填充数据 sendForm.append('password', form.password) sendForm.append('code', form.captcha) // 添加验证码参数 + + console.log('准备调用store.dispatch("login")', sendForm) // 调用接口 await store.dispatch('login', sendForm) + console.log('store.dispatch("login")调用成功') + // 定义 跳转地址 - const redirect = router.currentRoute.value.query.redirect || '/' - // 执行跳转 - router.replace(redirect) - ElMessage.success('登录成功') + const redirect = router.currentRoute.value.query.redirect || '/welcome' + console.log("跳转地址",redirect) + nextTick(() => { + window.location.href = redirect + // 执行跳转 + router.replace(redirect) + ElMessage.success('登录成功') + }) } catch (error) { console.error('登录错误详情:', error) refreshCaptcha()