wip: 临时提交
This commit is contained in:
@@ -303,12 +303,8 @@ export interface DictItemForm {
|
||||
* 字典项下拉选项
|
||||
*/
|
||||
export interface DictItemOption {
|
||||
/** 字典数据值 */
|
||||
value: string;
|
||||
|
||||
/** 字典数据标签 */
|
||||
value: number | string;
|
||||
label: string;
|
||||
|
||||
/** 标签类型 */
|
||||
tagType: string;
|
||||
tagType?: "" | "success" | "info" | "warning" | "danger";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
232
src/hooks/useWebSocketDict.js
Normal file
232
src/hooks/useWebSocketDict.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { ref } from "vue";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { useDictStoreHook } from "@/store/modules/dict.store";
|
||||
import SockJS from "sockjs-client";
|
||||
import Stomp from "webstomp-client";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
export function useWebSocketDict() {
|
||||
const userStore = useUserStore();
|
||||
const dictStore = useDictStoreHook();
|
||||
|
||||
// WebSocket状态
|
||||
const isConnected = ref(false);
|
||||
const stompClient = ref(null);
|
||||
const subscriptions = ref([]);
|
||||
|
||||
/**
|
||||
* 初始化WebSocket
|
||||
*/
|
||||
const initWebSocket = async () => {
|
||||
try {
|
||||
await connectWebSocket();
|
||||
setupDictSubscription();
|
||||
} catch (error) {
|
||||
console.error("初始化WebSocket失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭WebSocket
|
||||
*/
|
||||
const closeWebSocket = () => {
|
||||
disconnectWebSocket();
|
||||
};
|
||||
|
||||
/**
|
||||
* 连接WebSocket服务器
|
||||
*/
|
||||
const connectWebSocket = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const serverUrl = import.meta.env.VITE_APP_BASE_API + "/ws";
|
||||
|
||||
// 创建SockJS连接
|
||||
const socket = new SockJS(serverUrl);
|
||||
|
||||
// 创建STOMP客户端
|
||||
const client = Stomp.over(socket);
|
||||
|
||||
// 禁用调试日志
|
||||
client.debug = () => {};
|
||||
|
||||
// 添加认证头信息
|
||||
const headers = {
|
||||
Authorization: userStore.token,
|
||||
};
|
||||
|
||||
// 连接到WebSocket服务器
|
||||
client.connect(
|
||||
headers,
|
||||
() => {
|
||||
stompClient.value = client;
|
||||
isConnected.value = true;
|
||||
console.log("WebSocket连接成功");
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
console.error("WebSocket连接失败:", error);
|
||||
isConnected.value = false;
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建WebSocket连接时出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 断开WebSocket连接
|
||||
*/
|
||||
const disconnectWebSocket = () => {
|
||||
// 取消所有订阅
|
||||
subscriptions.value.forEach((subscription) => {
|
||||
if (subscription && typeof subscription.unsubscribe === "function") {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
subscriptions.value = [];
|
||||
|
||||
// 断开连接
|
||||
if (stompClient.value && stompClient.value.connected) {
|
||||
stompClient.value.disconnect();
|
||||
stompClient.value = null;
|
||||
}
|
||||
|
||||
isConnected.value = false;
|
||||
console.log("WebSocket连接已断开");
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置字典订阅
|
||||
*/
|
||||
const setupDictSubscription = () => {
|
||||
// 订阅字典更新
|
||||
subscribe("/topic/dict", (message) => {
|
||||
handleDictEvent(message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理字典事件
|
||||
* @param {Object} event 字典事件
|
||||
*/
|
||||
const handleDictEvent = (event) => {
|
||||
// 尝试解析消息,防止服务端发送字符串格式的JSON
|
||||
let eventData = event;
|
||||
if (typeof event === "string") {
|
||||
try {
|
||||
eventData = JSON.parse(event);
|
||||
} catch (error) {
|
||||
console.error("解析WebSocket消息失败:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { type, dictCode } = eventData;
|
||||
|
||||
if (type === "DICT_UPDATED") {
|
||||
// 删除缓存,强制重新加载
|
||||
dictStore.removeDictItem(dictCode);
|
||||
console.log(`字典 ${dictCode} 已更新,缓存已清除`);
|
||||
ElMessage.success(`字典 ${dictCode} 已更新`);
|
||||
} else if (type === "DICT_DELETED") {
|
||||
// 删除缓存
|
||||
dictStore.removeDictItem(dictCode);
|
||||
console.log(`字典 ${dictCode} 已删除,缓存已清除`);
|
||||
ElMessage.warning(`字典 ${dictCode} 已删除`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送消息到WebSocket服务器
|
||||
* @param {string} destination 目标地址
|
||||
* @param {string} content 消息内容
|
||||
*/
|
||||
const sendMessage = (destination, content) => {
|
||||
if (!isConnected.value || !stompClient.value) {
|
||||
console.error("WebSocket未连接,无法发送消息");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送消息
|
||||
stompClient.value.send(
|
||||
destination,
|
||||
JSON.stringify({
|
||||
content: content,
|
||||
sender: userStore.userInfo.username,
|
||||
timestamp: new Date().getTime(),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("发送消息失败:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 订阅WebSocket主题
|
||||
* @param {string} destination 订阅地址
|
||||
* @param {Function} callback 回调函数
|
||||
*/
|
||||
const subscribe = (destination, callback) => {
|
||||
if (!isConnected.value || !stompClient.value) {
|
||||
console.error("WebSocket未连接,无法订阅");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 订阅主题
|
||||
const subscription = stompClient.value.subscribe(destination, (message) => {
|
||||
if (message.body) {
|
||||
try {
|
||||
// 尝试解析JSON格式消息
|
||||
const data = JSON.parse(message.body);
|
||||
|
||||
// 如果返回的是JSON字符串,再次解析
|
||||
if (typeof data === "string" && data.startsWith("{") && data.endsWith("}")) {
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
callback(parsedData);
|
||||
} catch (e) {
|
||||
// 如果再次解析失败,传递原始数据
|
||||
callback(data);
|
||||
}
|
||||
} else {
|
||||
// 直接传递已解析的数据
|
||||
callback(data);
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果解析失败,传递原始消息
|
||||
console.warn("解析WebSocket消息失败,传递原始消息:", e);
|
||||
callback(message.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 保存订阅引用,以便后续取消订阅
|
||||
subscriptions.value.push(subscription);
|
||||
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
console.error("订阅失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connectWebSocket,
|
||||
disconnectWebSocket,
|
||||
sendMessage,
|
||||
subscribe,
|
||||
initWebSocket,
|
||||
closeWebSocket,
|
||||
handleDictEvent,
|
||||
};
|
||||
}
|
||||
248
src/hooks/useWebSocketDict.ts
Normal file
248
src/hooks/useWebSocketDict.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { ref } from "vue";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { useDictStoreHook } from "@/store/modules/dict.store";
|
||||
import SockJS from "sockjs-client";
|
||||
import Stomp, { Client, Subscription } from "webstomp-client";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
// 字典WebSocket事件类型定义
|
||||
interface DictWebSocketEvent {
|
||||
type: "DICT_UPDATED" | "DICT_DELETED";
|
||||
dictCode: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// 消息类型定义
|
||||
interface WebSocketMessage {
|
||||
content: string;
|
||||
sender: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function useWebSocketDict() {
|
||||
const userStore = useUserStore();
|
||||
const dictStore = useDictStoreHook();
|
||||
|
||||
// WebSocket状态
|
||||
const isConnected = ref<boolean>(false);
|
||||
const stompClient = ref<Client | null>(null);
|
||||
const subscriptions = ref<Subscription[]>([]);
|
||||
|
||||
/**
|
||||
* 初始化WebSocket
|
||||
*/
|
||||
const initWebSocket = async (): Promise<void> => {
|
||||
try {
|
||||
await connectWebSocket();
|
||||
setupDictSubscription();
|
||||
} catch (error) {
|
||||
console.error("初始化WebSocket失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭WebSocket
|
||||
*/
|
||||
const closeWebSocket = (): void => {
|
||||
disconnectWebSocket();
|
||||
};
|
||||
|
||||
/**
|
||||
* 连接WebSocket服务器
|
||||
*/
|
||||
const connectWebSocket = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const serverUrl = import.meta.env.VITE_APP_BASE_API + "/ws";
|
||||
|
||||
// 创建SockJS连接
|
||||
const socket = new SockJS(serverUrl);
|
||||
|
||||
// 创建STOMP客户端
|
||||
const client = Stomp.over(socket);
|
||||
|
||||
// 禁用调试日志
|
||||
client.debug = () => {};
|
||||
|
||||
// 添加认证头信息
|
||||
const headers = {
|
||||
Authorization: userStore.token,
|
||||
};
|
||||
|
||||
// 连接到WebSocket服务器
|
||||
client.connect(
|
||||
headers,
|
||||
() => {
|
||||
stompClient.value = client;
|
||||
isConnected.value = true;
|
||||
console.log("WebSocket连接成功");
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
console.error("WebSocket连接失败:", error);
|
||||
isConnected.value = false;
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("创建WebSocket连接时出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 断开WebSocket连接
|
||||
*/
|
||||
const disconnectWebSocket = (): void => {
|
||||
// 取消所有订阅
|
||||
subscriptions.value.forEach((subscription) => {
|
||||
if (subscription && typeof subscription.unsubscribe === "function") {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
subscriptions.value = [];
|
||||
|
||||
// 断开连接
|
||||
if (stompClient.value && stompClient.value.connected) {
|
||||
stompClient.value.disconnect();
|
||||
stompClient.value = null;
|
||||
}
|
||||
|
||||
isConnected.value = false;
|
||||
console.log("WebSocket连接已断开");
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置字典订阅
|
||||
*/
|
||||
const setupDictSubscription = (): void => {
|
||||
// 订阅字典更新
|
||||
subscribe("/topic/dict", (message: any) => {
|
||||
handleDictEvent(message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理字典事件
|
||||
* @param {Object | string} event 字典事件
|
||||
*/
|
||||
const handleDictEvent = (event: any): void => {
|
||||
// 尝试解析消息,防止服务端发送字符串格式的JSON
|
||||
let eventData: DictWebSocketEvent;
|
||||
if (typeof event === "string") {
|
||||
try {
|
||||
eventData = JSON.parse(event) as DictWebSocketEvent;
|
||||
} catch (error) {
|
||||
console.error("解析WebSocket消息失败:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
eventData = event as DictWebSocketEvent;
|
||||
}
|
||||
|
||||
const { type, dictCode } = eventData;
|
||||
|
||||
if (type === "DICT_UPDATED") {
|
||||
// 删除缓存,强制重新加载
|
||||
dictStore.removeDictItem(dictCode);
|
||||
console.log(`字典 ${dictCode} 已更新,缓存已清除`);
|
||||
ElMessage.success(`字典 ${dictCode} 已更新`);
|
||||
} else if (type === "DICT_DELETED") {
|
||||
// 删除缓存
|
||||
dictStore.removeDictItem(dictCode);
|
||||
console.log(`字典 ${dictCode} 已删除,缓存已清除`);
|
||||
ElMessage.warning(`字典 ${dictCode} 已删除`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送消息到WebSocket服务器
|
||||
* @param {string} destination 目标地址
|
||||
* @param {string} content 消息内容
|
||||
* @returns {boolean} 是否发送成功
|
||||
*/
|
||||
const sendMessage = (destination: string, content: string): boolean => {
|
||||
if (!isConnected.value || !stompClient.value) {
|
||||
console.error("WebSocket未连接,无法发送消息");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送消息
|
||||
const message: WebSocketMessage = {
|
||||
content: content,
|
||||
sender: userStore.userInfo.username,
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
|
||||
stompClient.value.send(destination, JSON.stringify(message), {});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("发送消息失败:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 订阅WebSocket主题
|
||||
* @param {string} destination 订阅地址
|
||||
* @param {Function} callback 回调函数
|
||||
* @returns {Subscription | null} 订阅对象
|
||||
*/
|
||||
const subscribe = (destination: string, callback: (data: any) => void): Subscription | null => {
|
||||
if (!isConnected.value || !stompClient.value) {
|
||||
console.error("WebSocket未连接,无法订阅");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 订阅主题
|
||||
const subscription = stompClient.value.subscribe(destination, (message) => {
|
||||
if (message.body) {
|
||||
try {
|
||||
// 尝试解析JSON格式消息
|
||||
const data = JSON.parse(message.body);
|
||||
|
||||
// 如果返回的是JSON字符串,再次解析
|
||||
if (typeof data === "string" && data.startsWith("{") && data.endsWith("}")) {
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
callback(parsedData);
|
||||
} catch {
|
||||
// 如果再次解析失败,传递原始数据
|
||||
callback(data);
|
||||
}
|
||||
} else {
|
||||
// 直接传递已解析的数据
|
||||
callback(data);
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果解析失败,传递原始消息
|
||||
console.warn("解析WebSocket消息失败,传递原始消息:", e);
|
||||
callback(message.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 保存订阅引用,以便后续取消订阅
|
||||
subscriptions.value.push(subscription);
|
||||
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
console.error("订阅失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connectWebSocket,
|
||||
disconnectWebSocket,
|
||||
sendMessage,
|
||||
subscribe,
|
||||
initWebSocket,
|
||||
closeWebSocket,
|
||||
handleDictEvent,
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { setupRouter } from "@/router";
|
||||
import { setupStore } from "@/store";
|
||||
import { setupElIcons } from "./icons";
|
||||
import { setupPermission } from "./permission";
|
||||
import { setupWebSocket } from "./websocket";
|
||||
import { InstallCodeMirror } from "codemirror-editor-vue3";
|
||||
|
||||
export default {
|
||||
@@ -22,6 +23,8 @@ export default {
|
||||
setupElIcons(app);
|
||||
// 路由守卫
|
||||
setupPermission();
|
||||
// WebSocket服务
|
||||
setupWebSocket();
|
||||
// 注册 CodeMirror
|
||||
app.use(InstallCodeMirror);
|
||||
},
|
||||
|
||||
16
src/plugins/websocket.ts
Normal file
16
src/plugins/websocket.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useWebSocketDict } from "@/hooks/useWebSocketDict";
|
||||
|
||||
/**
|
||||
* 初始化WebSocket服务
|
||||
*/
|
||||
export function setupWebSocket() {
|
||||
const dictWebSocket = useWebSocketDict();
|
||||
|
||||
// 初始化字典WebSocket服务
|
||||
dictWebSocket.initWebSocket();
|
||||
|
||||
// 在窗口关闭前断开WebSocket连接
|
||||
window.addEventListener("beforeunload", () => {
|
||||
dictWebSocket.closeWebSocket();
|
||||
});
|
||||
}
|
||||
@@ -42,6 +42,16 @@ export const useDictStore = defineStore("dict", () => {
|
||||
return dictCache.value[dictCode] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除指定字典项
|
||||
* @param dictCode 字典编码
|
||||
*/
|
||||
const removeDictItem = (dictCode: string) => {
|
||||
if (dictCache.value[dictCode]) {
|
||||
Reflect.deleteProperty(dictCache.value, dictCode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空字典缓存
|
||||
*/
|
||||
@@ -52,6 +62,7 @@ export const useDictStore = defineStore("dict", () => {
|
||||
return {
|
||||
loadDictItems,
|
||||
getDictItems,
|
||||
removeDictItem,
|
||||
clearDictCache,
|
||||
};
|
||||
});
|
||||
|
||||
15
src/types/websocket.ts
Normal file
15
src/types/websocket.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* WebSocket相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 字典WebSocket事件类型
|
||||
*/
|
||||
export interface DictWebSocketEvent {
|
||||
/** 事件类型:更新或删除 */
|
||||
type: "DICT_UPDATED" | "DICT_DELETED";
|
||||
/** 字典编码 */
|
||||
dictCode: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
259
src/views/demo/dict-websocket.vue
Normal file
259
src/views/demo/dict-websocket.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>字典WebSocket实时更新演示</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-alert type="info" :closable="false">
|
||||
<p>本示例展示了当字典数据在服务端更新时,如何通过WebSocket实时更新前端缓存。</p>
|
||||
<p class="mt-2">
|
||||
当管理员修改字典数据后,其他在线用户的字典缓存将自动刷新,无需手动刷新页面。
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
<div class="mt-4">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<template #header>字典选择</template>
|
||||
<div>
|
||||
<el-form>
|
||||
<el-form-item label="选择字典">
|
||||
<el-select v-model="selectedDict" placeholder="请选择字典" @change="loadDict">
|
||||
<el-option
|
||||
v-for="item in dictList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-if="dictItems.length > 0" :data="dictItems" border>
|
||||
<el-table-column prop="label" label="字典标签" />
|
||||
<el-table-column prop="value" label="字典值" />
|
||||
</el-table>
|
||||
<el-empty v-else description="请选择字典类型" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<template #header>WebSocket消息</template>
|
||||
<div class="websocket-log">
|
||||
<div v-if="logMessages.length === 0" class="text-center py-4 text-gray-400">
|
||||
暂无WebSocket消息
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(msg, index) in logMessages"
|
||||
:key="index"
|
||||
class="log-message"
|
||||
:class="{
|
||||
'bg-blue-50': msg.type === 'info',
|
||||
'bg-green-50': msg.type === 'success',
|
||||
'bg-red-50': msg.type === 'error',
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold">{{ msg.title }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ formatTime(msg.time) }}</span>
|
||||
</div>
|
||||
<pre class="text-sm mt-1">{{ JSON.stringify(msg.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<el-card shadow="hover">
|
||||
<template #header>模拟服务端更新字典</template>
|
||||
<p>这里模拟后端管理员更新字典数据后发送WebSocket通知</p>
|
||||
<p class="mt-2 mb-4 text-gray-500">注意:这只是前端模拟,实际应用中由后端触发</p>
|
||||
|
||||
<el-form :model="simulateForm" label-width="100px" class="demo-form">
|
||||
<el-form-item label="字典编码">
|
||||
<el-input v-model="simulateForm.dictCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="事件类型">
|
||||
<el-radio-group v-model="simulateForm.eventType">
|
||||
<el-radio label="DICT_UPDATED">字典更新</el-radio>
|
||||
<el-radio label="DICT_DELETED">字典删除</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="simulateDictEvent">模拟发送WebSocket消息</el-button>
|
||||
<el-button type="danger" @click="clearCache">清空字典缓存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDictStoreHook } from "@/store/modules/dict.store";
|
||||
import { DictWebSocketEvent } from "@/types/websocket";
|
||||
import { ref, reactive } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { DictItemOption } from "@/api/system/dict.api";
|
||||
import { useDateFormat } from "@vueuse/core";
|
||||
import { useWebSocketDict } from "@/hooks/useWebSocketDict";
|
||||
|
||||
// 字典列表
|
||||
const dictList = ref([
|
||||
{ label: "性别", value: "gender" },
|
||||
{ label: "状态", value: "status" },
|
||||
{ label: "用户类型", value: "user_type" },
|
||||
]);
|
||||
|
||||
// 选中的字典
|
||||
const selectedDict = ref("");
|
||||
// 字典项列表
|
||||
const dictItems = ref<DictItemOption[]>([]);
|
||||
// 字典store
|
||||
const dictStore = useDictStoreHook();
|
||||
|
||||
// 日志消息类型
|
||||
interface LogMessage {
|
||||
title: string;
|
||||
type: string;
|
||||
data: any;
|
||||
time: Date;
|
||||
}
|
||||
|
||||
// WebSocket日志消息
|
||||
const logMessages = ref<LogMessage[]>([]);
|
||||
|
||||
// 模拟表单
|
||||
const simulateForm = reactive({
|
||||
dictCode: "",
|
||||
eventType: "DICT_UPDATED",
|
||||
});
|
||||
|
||||
// 加载字典数据
|
||||
const loadDict = async (dictCode: string) => {
|
||||
await dictStore.loadDictItems(dictCode);
|
||||
dictItems.value = dictStore.getDictItems(dictCode);
|
||||
|
||||
// 添加日志
|
||||
addLogMessage({
|
||||
title: "加载字典数据",
|
||||
type: "info",
|
||||
data: {
|
||||
dictCode,
|
||||
items: dictItems.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 模拟字典事件
|
||||
const simulateDictEvent = () => {
|
||||
const { dictCode, eventType } = simulateForm;
|
||||
|
||||
if (!dictCode) {
|
||||
ElMessage.warning("请输入字典编码");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造字典事件
|
||||
const event: DictWebSocketEvent = {
|
||||
type: eventType as "DICT_UPDATED" | "DICT_DELETED",
|
||||
dictCode,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 添加日志
|
||||
addLogMessage({
|
||||
title: "模拟WebSocket消息",
|
||||
type: "success",
|
||||
data: event,
|
||||
});
|
||||
|
||||
// 导入WebSocket字典钩子
|
||||
const { handleDictEvent } = useWebSocketDict();
|
||||
|
||||
// 手动调用处理函数,模拟收到WebSocket消息
|
||||
handleDictEvent(event);
|
||||
|
||||
// 如果是当前选中的字典被更新,则刷新显示
|
||||
if (selectedDict.value === dictCode) {
|
||||
setTimeout(() => {
|
||||
dictItems.value = dictStore.getDictItems(dictCode);
|
||||
|
||||
addLogMessage({
|
||||
title: "字典数据已更新",
|
||||
type: "info",
|
||||
data: {
|
||||
dictCode,
|
||||
items: dictItems.value,
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空字典缓存
|
||||
const clearCache = () => {
|
||||
dictStore.clearDictCache();
|
||||
dictItems.value = [];
|
||||
selectedDict.value = "";
|
||||
|
||||
addLogMessage({
|
||||
title: "字典缓存已清空",
|
||||
type: "error",
|
||||
data: {},
|
||||
});
|
||||
|
||||
ElMessage.success("字典缓存已清空");
|
||||
};
|
||||
|
||||
// 添加日志消息
|
||||
const addLogMessage = (message: { title: string; type: string; data: any }) => {
|
||||
logMessages.value.unshift({
|
||||
...message,
|
||||
time: new Date(),
|
||||
});
|
||||
|
||||
// 最多显示20条日志
|
||||
if (logMessages.value.length > 20) {
|
||||
logMessages.value.pop();
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date: Date) => {
|
||||
return useDateFormat(date, "HH:mm:ss").value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.websocket-log {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #409eff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
390
src/views/system/websocket/websocket-test.vue
Normal file
390
src/views/system/websocket/websocket-test.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>WebSocket测试</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="连接状态" name="status">
|
||||
<el-alert
|
||||
:title="isConnected ? '已连接到WebSocket服务器' : '未连接到WebSocket服务器'"
|
||||
:type="isConnected ? 'success' : 'error'"
|
||||
:description="connectionMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button
|
||||
:type="isConnected ? 'warning' : 'primary'"
|
||||
:loading="connecting"
|
||||
@click="toggleConnection"
|
||||
>
|
||||
{{ isConnected ? "断开连接" : "连接" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="字典更新通知" name="dict">
|
||||
<p class="section-desc">触发字典更新通知,所有在线用户会收到该通知</p>
|
||||
<div class="form-container">
|
||||
<el-form :model="dictForm" label-width="100px">
|
||||
<el-form-item label="字典编码">
|
||||
<el-input v-model="dictForm.dictCode" placeholder="请输入字典编码" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="sendDictUpdate">发送字典更新通知</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="用户消息" name="message">
|
||||
<p class="section-desc">向特定用户发送消息</p>
|
||||
<div class="form-container">
|
||||
<el-form :model="messageForm" label-width="100px">
|
||||
<el-form-item label="接收用户">
|
||||
<el-input v-model="messageForm.receiver" placeholder="请输入接收用户的用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="消息内容">
|
||||
<el-input
|
||||
v-model="messageForm.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入消息内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="sendUserMessage">发送消息</el-button>
|
||||
<el-button type="success" @click="sendBroadcast">发送广播</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="在线用户" name="online">
|
||||
<div class="stats-container">
|
||||
<el-statistic title="当前在线用户数">
|
||||
<template #value>
|
||||
<div class="statistic-value">{{ onlineStats.total }}</div>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loadingUsers" :data="onlineUsers" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" width="180" />
|
||||
<el-table-column prop="nickname" label="昵称" width="180" />
|
||||
<el-table-column prop="loginTime" label="登录时间">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.loginTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="fetchOnlineUsers">刷新列表</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="消息记录" name="logs">
|
||||
<div class="logs-container">
|
||||
<div v-for="(log, index) in messageLogs" :key="index" class="log-item">
|
||||
<div class="log-time">{{ formatDate(log.timestamp) }}</div>
|
||||
<div class="log-content" :class="{ 'log-broadcast': log.isBroadcast }">
|
||||
<span class="log-sender">{{ log.sender }}</span>
|
||||
: {{ log.content }}
|
||||
<el-tag v-if="log.isBroadcast" size="small" type="warning">广播</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="messageLogs.length === 0" class="empty-logs">暂无消息记录</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="danger" @click="clearLogs">清空记录</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useWebSocketDict } from "@/hooks/useWebSocketDict";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { getDictList } from "@/api/dict";
|
||||
import { getOnlineUsers, getOnlineStats } from "@/api/user";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { connectWebSocket, disconnectWebSocket, isConnected, sendMessage, subscribe } =
|
||||
useWebSocketDict();
|
||||
|
||||
// 状态变量
|
||||
const activeTab = ref("status");
|
||||
const connecting = ref(false);
|
||||
const connectionMessage = ref("WebSocket连接状态");
|
||||
const messageLogs = ref([]);
|
||||
const onlineUsers = ref([]);
|
||||
const onlineStats = ref({ total: 0 });
|
||||
const loadingUsers = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const dictForm = ref({
|
||||
dictCode: "gender",
|
||||
});
|
||||
|
||||
const messageForm = ref({
|
||||
receiver: "",
|
||||
content: "",
|
||||
});
|
||||
|
||||
// 连接和断开WebSocket
|
||||
const toggleConnection = async () => {
|
||||
connecting.value = true;
|
||||
try {
|
||||
if (isConnected.value) {
|
||||
disconnectWebSocket();
|
||||
connectionMessage.value = "已断开连接";
|
||||
} else {
|
||||
await connectWebSocket();
|
||||
connectionMessage.value = "已成功连接到WebSocket服务器";
|
||||
setupSubscriptions();
|
||||
}
|
||||
} catch (error) {
|
||||
connectionMessage.value = `连接失败: ${error.message}`;
|
||||
ElMessage.error(`WebSocket连接失败: ${error.message}`);
|
||||
} finally {
|
||||
connecting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 设置订阅
|
||||
const setupSubscriptions = () => {
|
||||
// 订阅字典更新
|
||||
subscribe("/topic/dict", (message) => {
|
||||
addMessageLog({
|
||||
sender: "System",
|
||||
content: `字典 ${message.dictCode} 已更新`,
|
||||
timestamp: new Date().getTime(),
|
||||
isBroadcast: true,
|
||||
});
|
||||
ElMessage.success(`字典 ${message.dictCode} 已更新`);
|
||||
});
|
||||
|
||||
// 订阅用户消息
|
||||
const username = userStore.userInfo.username;
|
||||
subscribe(`/user/${username}/messages`, (message) => {
|
||||
addMessageLog({
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
isBroadcast: false,
|
||||
});
|
||||
ElMessage.info(`收到来自 ${message.sender} 的消息`);
|
||||
});
|
||||
|
||||
// 订阅广播消息
|
||||
subscribe("/topic/public", (message) => {
|
||||
addMessageLog({
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
isBroadcast: true,
|
||||
});
|
||||
ElMessage.info(`收到来自 ${message.sender} 的广播消息`);
|
||||
});
|
||||
|
||||
// 订阅在线用户更新
|
||||
subscribe("/topic/users/online", (message) => {
|
||||
ElMessage.info(`用户 ${message.username} ${message.online ? "上线" : "下线"}`);
|
||||
fetchOnlineUsers();
|
||||
fetchOnlineStats();
|
||||
});
|
||||
};
|
||||
|
||||
// 发送字典更新通知
|
||||
const sendDictUpdate = async () => {
|
||||
try {
|
||||
// 调用字典API触发更新
|
||||
await getDictList({ dictCode: dictForm.value.dictCode });
|
||||
ElMessage.success("字典更新通知已发送");
|
||||
} catch (error) {
|
||||
ElMessage.error(`发送失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 发送用户消息
|
||||
const sendUserMessage = () => {
|
||||
if (!messageForm.value.receiver || !messageForm.value.content) {
|
||||
ElMessage.warning("请输入接收用户和消息内容");
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage(`/app/sendToUser/${messageForm.value.receiver}`, messageForm.value.content);
|
||||
|
||||
// 记录发送的消息
|
||||
addMessageLog({
|
||||
sender: userStore.userInfo.username,
|
||||
content: `[发送给 ${messageForm.value.receiver}] ${messageForm.value.content}`,
|
||||
timestamp: new Date().getTime(),
|
||||
isBroadcast: false,
|
||||
});
|
||||
|
||||
ElMessage.success("消息已发送");
|
||||
};
|
||||
|
||||
// 发送广播消息
|
||||
const sendBroadcast = () => {
|
||||
if (!messageForm.value.content) {
|
||||
ElMessage.warning("请输入消息内容");
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage("/app/broadcast", messageForm.value.content);
|
||||
|
||||
ElMessage.success("广播消息已发送");
|
||||
};
|
||||
|
||||
// 获取在线用户
|
||||
const fetchOnlineUsers = async () => {
|
||||
loadingUsers.value = true;
|
||||
try {
|
||||
const res = await getOnlineUsers();
|
||||
onlineUsers.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error(`获取在线用户失败: ${error.message}`);
|
||||
} finally {
|
||||
loadingUsers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取在线用户统计
|
||||
const fetchOnlineStats = async () => {
|
||||
try {
|
||||
const res = await getOnlineStats();
|
||||
onlineStats.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error(`获取在线统计失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加消息日志
|
||||
const addMessageLog = (log) => {
|
||||
messageLogs.value.unshift(log);
|
||||
// 限制日志数量
|
||||
if (messageLogs.value.length > 100) {
|
||||
messageLogs.value = messageLogs.value.slice(0, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
messageLogs.value = [];
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await connectWebSocket();
|
||||
connectionMessage.value = "已成功连接到WebSocket服务器";
|
||||
setupSubscriptions();
|
||||
await fetchOnlineUsers();
|
||||
await fetchOnlineStats();
|
||||
} catch (error) {
|
||||
connectionMessage.value = `连接失败: ${error.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
max-width: 600px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
height: 400px;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-sender {
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.log-broadcast .log-sender {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.empty-logs {
|
||||
padding: 20px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.statistic-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user