refactor: ♻️ 存入localStorage移除前缀,websocket hook重构优化
This commit is contained in:
@@ -9,7 +9,7 @@ VITE_APP_API_URL=https://api.youlai.tech # 线上
|
|||||||
# VITE_APP_API_URL=http://localhost:8989 # 本地
|
# VITE_APP_API_URL=http://localhost:8989 # 本地
|
||||||
|
|
||||||
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
|
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
|
||||||
VITE_APP_WS_ENDPOINT=
|
VITE_APP_WS_ENDPOINT=ws://localhost:8989/ws
|
||||||
|
|
||||||
# 启用 Mock 服务
|
# 启用 Mock 服务
|
||||||
VITE_MOCK_DEV_SERVER=false
|
VITE_MOCK_DEV_SERVER=false
|
||||||
|
|||||||
151
src/hooks/useStomp.ts
Normal file
151
src/hooks/useStomp.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { ref, watch, onMounted } from "vue";
|
||||||
|
import { Client, IMessage, StompSubscription } from "@stomp/stompjs";
|
||||||
|
import { getAccessToken } from "@/utils/auth";
|
||||||
|
|
||||||
|
export interface UseStompOptions {
|
||||||
|
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
|
||||||
|
brokerURL?: string;
|
||||||
|
/** 用于鉴权的 token,不传时使用 getToken() 的返回值 */
|
||||||
|
token?: string;
|
||||||
|
/** 重连延迟,单位毫秒,默认为 5000 */
|
||||||
|
reconnectDelay?: number;
|
||||||
|
/** 是否开启调试日志 */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStomp(options: UseStompOptions = {}) {
|
||||||
|
// 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取
|
||||||
|
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
|
||||||
|
const defaultToken = getAccessToken();
|
||||||
|
|
||||||
|
// 将 brokerURL 定义为响应式 ref,便于动态修改
|
||||||
|
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
|
||||||
|
const token = options.token ?? defaultToken;
|
||||||
|
|
||||||
|
// 连接状态标记
|
||||||
|
const isConnected = ref(false);
|
||||||
|
// 存储所有订阅
|
||||||
|
const subscriptions = new Map<string, StompSubscription>();
|
||||||
|
|
||||||
|
// 用于保存 STOMP 客户端的实例
|
||||||
|
let client: Client | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 STOMP 客户端
|
||||||
|
* 只有在 brokerURL 非空时才会初始化客户端
|
||||||
|
*/
|
||||||
|
const initializeClient = () => {
|
||||||
|
if (!brokerURL.value) {
|
||||||
|
console.warn(
|
||||||
|
"brokerURL is required. Please set the WebSocket URL in your .env file (VITE_APP_WS_ENDPOINT)."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
client = new Client({
|
||||||
|
brokerURL: brokerURL.value,
|
||||||
|
reconnectDelay: options.reconnectDelay ?? 5000,
|
||||||
|
debug: options.debug ? (msg) => console.log("[STOMP]", msg) : () => {},
|
||||||
|
connectHeaders: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
heartbeatIncoming: 4000,
|
||||||
|
heartbeatOutgoing: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.onConnect = (frame) => {
|
||||||
|
isConnected.value = true;
|
||||||
|
console.log("STOMP connected", frame);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onStompError = (frame) => {
|
||||||
|
console.error("Broker reported error: " + frame.headers["message"]);
|
||||||
|
console.error("Additional details: " + frame.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.onWebSocketClose = (evt) => {
|
||||||
|
isConnected.value = false;
|
||||||
|
console.warn("WebSocket closed", evt);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 brokerURL 的变化,若地址改变则重新初始化
|
||||||
|
watch(brokerURL, (newURL, oldURL) => {
|
||||||
|
if (newURL !== oldURL) {
|
||||||
|
console.log(`brokerURL changed from ${oldURL} to ${newURL}`);
|
||||||
|
// 断开当前连接,重新激活客户端
|
||||||
|
if (client && client.connected) {
|
||||||
|
client.deactivate();
|
||||||
|
}
|
||||||
|
brokerURL.value = newURL;
|
||||||
|
initializeClient(); // 重新初始化客户端
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在组件挂载时检查并初始化客户端
|
||||||
|
onMounted(() => {
|
||||||
|
initializeClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活连接(如果已经连接或正在激活则直接返回)
|
||||||
|
*/
|
||||||
|
const connect = () => {
|
||||||
|
if (client && (client.connected || client.active)) {
|
||||||
|
console.log("Already connected or connecting, skipping connect() call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client?.activate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅指定主题
|
||||||
|
* @param destination 目标主题地址
|
||||||
|
* @param callback 接收到消息时的回调函数
|
||||||
|
* @returns 返回订阅 id,用于后续取消订阅
|
||||||
|
*/
|
||||||
|
const subscribe = (destination: string, callback: (message: IMessage) => void): string => {
|
||||||
|
if (client) {
|
||||||
|
const subscription = client.subscribe(destination, callback);
|
||||||
|
subscriptions.set(subscription.id, subscription);
|
||||||
|
return subscription.id;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消指定订阅
|
||||||
|
* @param subscriptionId 要取消的订阅 id
|
||||||
|
*/
|
||||||
|
const unsubscribe = (subscriptionId: string) => {
|
||||||
|
const subscription = subscriptions.get(subscriptionId);
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscriptions.delete(subscriptionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动断开连接(如果未连接则不执行)
|
||||||
|
*/
|
||||||
|
const disconnect = () => {
|
||||||
|
if (client && !(client.connected || client.active)) {
|
||||||
|
console.log("Already disconnected, skipping disconnect() call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client?.deactivate();
|
||||||
|
isConnected.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
isConnected,
|
||||||
|
connect,
|
||||||
|
subscribe,
|
||||||
|
unsubscribe,
|
||||||
|
disconnect,
|
||||||
|
brokerURL, // 暴露 brokerURL 以便组件中动态修改
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -167,8 +167,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NoticeAPI, { NoticePageVO } from "@/api/system/notice";
|
import NoticeAPI, { NoticePageVO } from "@/api/system/notice";
|
||||||
import WebSocketManager from "@/utils/websocket";
|
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
import { useStomp } from "@/hooks/useStomp";
|
||||||
|
|
||||||
const activeTab = ref("notice");
|
const activeTab = ref("notice");
|
||||||
const notices = ref<NoticePageVO[]>([]);
|
const notices = ref<NoticePageVO[]>([]);
|
||||||
@@ -176,15 +176,23 @@ const messages = ref<any[]>([]);
|
|||||||
const tasks = ref<any[]>([]);
|
const tasks = ref<any[]>([]);
|
||||||
const noticeDetailRef = ref();
|
const noticeDetailRef = ref();
|
||||||
|
|
||||||
|
// 初始化 useStomp hook(这里仅用于订阅通知消息),同时调用 connect 建立连接
|
||||||
|
const { connect, subscribe, disconnect } = useStomp({
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
|
||||||
// 获取未读消息列表并连接 WebSocket
|
// 获取未读消息列表并连接 WebSocket
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then((data) => {
|
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then((data) => {
|
||||||
notices.value = data.list;
|
notices.value = data.list;
|
||||||
});
|
});
|
||||||
|
|
||||||
WebSocketManager.subscribeToTopic("/user/queue/message", (message) => {
|
// 建立连接
|
||||||
|
connect();
|
||||||
|
|
||||||
|
subscribe("/user/queue/message", (message) => {
|
||||||
console.log("收到消息:", message);
|
console.log("收到消息:", message);
|
||||||
const data = JSON.parse(message);
|
const data = JSON.parse(message.body);
|
||||||
const id = data.id;
|
const id = data.id;
|
||||||
if (!notices.value.some((notice) => notice.id == id)) {
|
if (!notices.value.some((notice) => notice.id == id)) {
|
||||||
notices.value.unshift({
|
notices.value.unshift({
|
||||||
@@ -224,6 +232,11 @@ function markAllAsRead() {
|
|||||||
notices.value = [];
|
notices.value = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 如果需要取消订阅,可以在这里调用 disconnect 或 unsubscribe(本示例直接断开连接)
|
||||||
|
disconnect();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<div class="flex-center h100% p13px">
|
<div class="flex-center h100% p13px">
|
||||||
<img :src="userStore.userInfo.avatar" class="rounded-full mr-10px w24px h24px" />
|
<img :src="userStore.userInfo.avatar" class="rounded-full mr10px w24px h24px" />
|
||||||
<span>{{ userStore.userInfo.username }}</span>
|
<span>{{ userStore.userInfo.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { setupRouter } from "@/router";
|
|||||||
import { setupStore } from "@/store";
|
import { setupStore } from "@/store";
|
||||||
import { setupElIcons } from "./icons";
|
import { setupElIcons } from "./icons";
|
||||||
import { setupPermission } from "./permission";
|
import { setupPermission } from "./permission";
|
||||||
import webSocketManager from "@/utils/websocket";
|
|
||||||
import { InstallCodeMirror } from "codemirror-editor-vue3";
|
import { InstallCodeMirror } from "codemirror-editor-vue3";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -23,8 +22,6 @@ export default {
|
|||||||
setupElIcons(app);
|
setupElIcons(app);
|
||||||
// 路由守卫
|
// 路由守卫
|
||||||
setupPermission();
|
setupPermission();
|
||||||
// 初始化 WebSocket
|
|
||||||
webSocketManager.setupWebSocket();
|
|
||||||
// 注册 CodeMirror
|
// 注册 CodeMirror
|
||||||
app.use(InstallCodeMirror);
|
app.use(InstallCodeMirror);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router";
|
import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router";
|
||||||
import NProgress from "@/utils/nprogress";
|
import NProgress from "@/utils/nprogress";
|
||||||
import { getToken } from "@/utils/auth";
|
import { getAccessToken } from "@/utils/auth";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { usePermissionStore, useUserStore } from "@/store";
|
import { usePermissionStore, useUserStore } from "@/store";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export function setupPermission() {
|
|||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
|
|
||||||
const isLogin = !!getToken(); // 判断是否登录
|
const isLogin = !!getAccessToken(); // 判断是否登录
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
if (to.path === "/login") {
|
if (to.path === "/login") {
|
||||||
// 已登录,访问登录页,跳转到首页
|
// 已登录,访问登录页,跳转到首页
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useDictStoreHook } from "@/store/modules/dict";
|
|||||||
import AuthAPI, { type LoginFormData } from "@/api/auth";
|
import AuthAPI, { type LoginFormData } from "@/api/auth";
|
||||||
import UserAPI, { type UserInfo } from "@/api/system/user";
|
import UserAPI, { type UserInfo } from "@/api/system/user";
|
||||||
|
|
||||||
import { setToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
|
import { setAccessToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
|
||||||
|
|
||||||
export const useUserStore = defineStore("user", () => {
|
export const useUserStore = defineStore("user", () => {
|
||||||
const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo);
|
const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo);
|
||||||
@@ -20,8 +20,8 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
AuthAPI.login(LoginFormData)
|
AuthAPI.login(LoginFormData)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const { tokenType, accessToken, refreshToken } = data;
|
const { accessToken, refreshToken } = data;
|
||||||
setToken(tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
setAccessToken(accessToken); // eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||||
setRefreshToken(refreshToken);
|
setRefreshToken(refreshToken);
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
@@ -77,8 +77,8 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
AuthAPI.refreshToken(refreshToken)
|
AuthAPI.refreshToken(refreshToken)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const { tokenType, accessToken, refreshToken } = data;
|
const { accessToken, refreshToken } = data;
|
||||||
setToken(tokenType + " " + accessToken);
|
setAccessToken(accessToken);
|
||||||
setRefreshToken(refreshToken);
|
setRefreshToken(refreshToken);
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ const ACCESS_TOKEN_KEY = "access_token";
|
|||||||
// 刷新 token 缓存的 key
|
// 刷新 token 缓存的 key
|
||||||
const REFRESH_TOKEN_KEY = "refresh_token";
|
const REFRESH_TOKEN_KEY = "refresh_token";
|
||||||
|
|
||||||
function getToken(): string {
|
function getAccessToken(): string {
|
||||||
return localStorage.getItem(ACCESS_TOKEN_KEY) || "";
|
return localStorage.getItem(ACCESS_TOKEN_KEY) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setToken(token: string) {
|
function setAccessToken(token: string) {
|
||||||
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
localStorage.setItem(ACCESS_TOKEN_KEY, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,4 +24,4 @@ function clearToken() {
|
|||||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getToken, setToken, clearToken, getRefreshToken, setRefreshToken };
|
export { getAccessToken, setAccessToken, clearToken, getRefreshToken, setRefreshToken };
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axio
|
|||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { ResultEnum } from "@/enums/ResultEnum";
|
import { ResultEnum } from "@/enums/ResultEnum";
|
||||||
import { getToken } from "@/utils/auth";
|
import { getAccessToken } from "@/utils/auth";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
@@ -16,10 +16,10 @@ const service = axios.create({
|
|||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const accessToken = getToken();
|
const accessToken = getAccessToken();
|
||||||
// 如果 Authorization 设置为 no-auth,则不携带 Token,用于登录、刷新 Token 等接口
|
// 如果 Authorization 设置为 no-auth,则不携带 Token,用于登录、刷新 Token 等接口
|
||||||
if (config.headers.Authorization !== "no-auth" && accessToken) {
|
if (config.headers.Authorization !== "no-auth" && accessToken) {
|
||||||
config.headers.Authorization = accessToken;
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
} else {
|
} else {
|
||||||
delete config.headers.Authorization;
|
delete config.headers.Authorization;
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// 封装需要重试的请求
|
// 封装需要重试的请求
|
||||||
const retryRequest = () => {
|
const retryRequest = () => {
|
||||||
config.headers.Authorization = getToken();
|
config.headers.Authorization = getAccessToken();
|
||||||
resolve(service(config));
|
resolve(service(config));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import { Client } from "@stomp/stompjs";
|
|
||||||
import { getToken } from "@/utils/auth";
|
|
||||||
|
|
||||||
class WebSocketManager {
|
|
||||||
private client: Client | null = null;
|
|
||||||
private messageHandlers: Map<string, ((message: string) => void)[]> = new Map();
|
|
||||||
private reconnectAttempts = 0;
|
|
||||||
private maxReconnectAttempts = 3; // 自定义最大重试次数
|
|
||||||
private reconnectDelay = 5000; // 重试延迟(单位:毫秒)
|
|
||||||
|
|
||||||
// 初始化 WebSocket 客户端
|
|
||||||
setupWebSocket() {
|
|
||||||
const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
|
|
||||||
|
|
||||||
// 如果没有配置 WebSocket 端点或显式关闭,直接返回
|
|
||||||
if (!endpoint) {
|
|
||||||
console.log("WebSocket 已被禁用,如需打开请在配置文件中配置 VITE_APP_WS_ENDPOINT");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client && this.client.connected) {
|
|
||||||
console.log("客户端已存在并且连接正常");
|
|
||||||
return this.client;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client = new Client({
|
|
||||||
brokerURL: endpoint,
|
|
||||||
connectHeaders: {
|
|
||||||
Authorization: getToken(),
|
|
||||||
},
|
|
||||||
heartbeatIncoming: 30000,
|
|
||||||
heartbeatOutgoing: 30000,
|
|
||||||
reconnectDelay: 0, // 设置为 0 禁用重连
|
|
||||||
onConnect: () => {
|
|
||||||
console.log(`连接到 WebSocket 服务器: ${endpoint}`);
|
|
||||||
this.reconnectAttempts = 0; // 重置重连计数
|
|
||||||
this.messageHandlers.forEach((handlers, topic) => {
|
|
||||||
handlers.forEach((handler) => {
|
|
||||||
this.subscribeToTopic(topic, handler);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onStompError: (frame) => {
|
|
||||||
console.error(`连接错误: ${frame.headers["message"]}`);
|
|
||||||
console.error(`错误详情: ${frame.body}`);
|
|
||||||
},
|
|
||||||
onDisconnect: () => {
|
|
||||||
console.log(`WebSocket 连接已断开: ${endpoint}`);
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
||||||
console.log(`正在尝试重连... 尝试次数: ${this.reconnectAttempts}`);
|
|
||||||
} else {
|
|
||||||
console.log("重连次数已达上限,停止重连");
|
|
||||||
this.client?.deactivate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅主题
|
|
||||||
public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
|
|
||||||
console.log(`正在订阅主题: ${topic}`);
|
|
||||||
if (!this.client || !this.client.connected) {
|
|
||||||
this.setupWebSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.messageHandlers.has(topic)) {
|
|
||||||
this.messageHandlers.get(topic)?.push(onMessage);
|
|
||||||
} else {
|
|
||||||
this.messageHandlers.set(topic, [onMessage]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client?.connected) {
|
|
||||||
this.client.subscribe(topic, (message) => {
|
|
||||||
const handlers = this.messageHandlers.get(topic);
|
|
||||||
handlers?.forEach((handler) => handler(message.body));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 断开 WebSocket 连接
|
|
||||||
public disconnect() {
|
|
||||||
if (this.client) {
|
|
||||||
console.log("断开 WebSocket 连接");
|
|
||||||
this.client.deactivate();
|
|
||||||
this.client = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new WebSocketManager();
|
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<el-card>
|
<el-card>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
|
<!-- 输入框允许修改 websocket 地址(注意:修改后不会自动更新已创建的 hook 实例) -->
|
||||||
<el-input v-model="socketEndpoint" class="w-220px" />
|
<el-input v-model="socketEndpoint" class="w-220px" />
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -94,98 +95,88 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Client } from "@stomp/stompjs";
|
import { useStomp } from "@/hooks/useStomp";
|
||||||
|
import { getAccessToken } from "@/utils/auth"; // 此处可与 hook 内 getToken 保持一致
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { getToken } from "@/utils/auth";
|
|
||||||
|
|
||||||
const userStore = useUserStoreHook();
|
const userStore = useUserStoreHook();
|
||||||
const isConnected = ref(false);
|
// 用于手动调整 WebSocket 地址
|
||||||
const socketEndpoint = ref(import.meta.env.VITE_APP_WS_ENDPOINT);
|
const socketEndpoint = ref(import.meta.env.VITE_APP_WS_ENDPOINT);
|
||||||
|
// 同步连接状态
|
||||||
const receiver = ref("root");
|
const isConnected = ref(false);
|
||||||
|
// 消息接收列表
|
||||||
interface MessageType {
|
interface MessageType {
|
||||||
type?: string;
|
type?: string;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = ref<MessageType[]>([]);
|
const messages = ref<MessageType[]>([]);
|
||||||
|
// 广播消息内容
|
||||||
|
const topicMessage = ref("亲爱的朋友们,系统已恢复最新状态。");
|
||||||
|
// 点对点消息内容(默认示例)
|
||||||
|
const queneMessage = ref("Hi, " + userStore.userInfo.username + " 这里是点对点消息示例!");
|
||||||
|
const receiver = ref("root");
|
||||||
|
|
||||||
const topicMessage = ref("亲爱的大冤种们,由于一只史诗级的BUG,系统版本已经被迫回退到了0.0.1。"); // 广播消息
|
// 调用 useStomp hook,默认使用 socketEndpoint 和 token(此处用 getAccessToken())
|
||||||
|
const {
|
||||||
const queneMessage = ref(
|
isConnected: stompConnected,
|
||||||
"hi , " + receiver.value + " , 我是" + userStore.userInfo.username + " , 想和你交个朋友 ! "
|
connect,
|
||||||
);
|
subscribe,
|
||||||
|
disconnect,
|
||||||
let stompClient: Client;
|
client,
|
||||||
|
} = useStomp({
|
||||||
function connectWebSocket() {
|
|
||||||
stompClient = new Client({
|
|
||||||
brokerURL: socketEndpoint.value,
|
brokerURL: socketEndpoint.value,
|
||||||
connectHeaders: {
|
token: getAccessToken(),
|
||||||
Authorization: getToken(),
|
reconnectDelay: 5000,
|
||||||
},
|
debug: true,
|
||||||
debug: (str: any) => {
|
});
|
||||||
console.log(str);
|
|
||||||
},
|
|
||||||
onConnect: () => {
|
|
||||||
console.log("连接成功");
|
|
||||||
isConnected.value = true;
|
|
||||||
messages.value.push({
|
|
||||||
sender: "Server",
|
|
||||||
content: "Websocket 已连接",
|
|
||||||
type: "tip",
|
|
||||||
});
|
|
||||||
|
|
||||||
stompClient.subscribe("/topic/notice", (res: any) => {
|
// 同步 hook 的连接状态到组件
|
||||||
|
watch(stompConnected, (newVal) => {
|
||||||
|
isConnected.value = newVal;
|
||||||
|
if (newVal) {
|
||||||
|
// 连接成功后,订阅广播和点对点消息主题
|
||||||
|
subscribe("/topic/notice", (res) => {
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
sender: "Server",
|
sender: "Server",
|
||||||
content: res.body,
|
content: res.body,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
subscribe("/user/queue/greeting", (res) => {
|
||||||
stompClient.subscribe("/user/queue/greeting", (res: any) => {
|
|
||||||
const messageData = JSON.parse(res.body) as MessageType;
|
const messageData = JSON.parse(res.body) as MessageType;
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
sender: messageData.sender,
|
sender: messageData.sender,
|
||||||
content: messageData.content,
|
content: messageData.content,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
|
||||||
onStompError: (frame: any) => {
|
|
||||||
console.error("Broker reported error: " + frame.headers["message"]);
|
|
||||||
console.error("Additional details: " + frame.body);
|
|
||||||
},
|
|
||||||
onDisconnect: () => {
|
|
||||||
isConnected.value = false;
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
sender: "Server",
|
sender: "Server",
|
||||||
content: "Websocket 已断开",
|
content: "Websocket 已连接",
|
||||||
type: "tip",
|
type: "tip",
|
||||||
});
|
});
|
||||||
},
|
} else {
|
||||||
});
|
|
||||||
|
|
||||||
stompClient.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectWebSocket() {
|
|
||||||
if (stompClient && stompClient.connected) {
|
|
||||||
stompClient.deactivate();
|
|
||||||
isConnected.value = false;
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
sender: "Server",
|
sender: "Server",
|
||||||
content: "Websocket 已断开",
|
content: "Websocket 已断开",
|
||||||
type: "tip",
|
type: "tip",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 连接 WebSocket
|
||||||
|
function connectWebSocket() {
|
||||||
|
connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 断开 WebSocket
|
||||||
|
function disconnectWebSocket() {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送广播消息
|
||||||
function sendToAll() {
|
function sendToAll() {
|
||||||
if (stompClient.connected) {
|
if (client && client.connected) {
|
||||||
stompClient.publish({
|
client.publish({
|
||||||
destination: "/topic/notice",
|
destination: "/topic/notice",
|
||||||
body: topicMessage.value,
|
body: topicMessage.value,
|
||||||
});
|
});
|
||||||
@@ -196,9 +187,10 @@ function sendToAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送点对点消息
|
||||||
function sendToUser() {
|
function sendToUser() {
|
||||||
if (stompClient.connected) {
|
if (client && client.connected) {
|
||||||
stompClient.publish({
|
client.publish({
|
||||||
destination: "/app/sendToUser/" + receiver.value,
|
destination: "/app/sendToUser/" + receiver.value,
|
||||||
body: queneMessage.value,
|
body: queneMessage.value,
|
||||||
});
|
});
|
||||||
@@ -212,6 +204,10 @@ function sendToUser() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
disconnectWebSocket();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -219,40 +215,33 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message--sent {
|
.message--sent {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
background-color: #dcf8c6;
|
background-color: #dcf8c6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message--received {
|
.message--received {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
background-color: #e8e8e8;
|
background-color: #e8e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-sender {
|
.message-sender {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-receiver {
|
.message-receiver {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-message {
|
.tip-message {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user