wip: 字典 websocket 实时更新

This commit is contained in:
Ray.Hao
2025-04-24 08:20:52 +08:00
parent 964dba59c7
commit 6f9c4c64de
4 changed files with 309 additions and 181 deletions

View File

@@ -23,12 +23,7 @@
:style="style" :style="style"
@change="handleChange" @change="handleChange"
> >
<el-radio <el-radio v-for="option in options" :key="option.value" :value="option.value">
v-for="option in options"
:key="option.value"
:label="option.label"
:value="option.value"
>
{{ option.label }} {{ option.label }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
@@ -40,12 +35,7 @@
:style="style" :style="style"
@change="handleChange" @change="handleChange"
> >
<el-checkbox <el-checkbox v-for="option in options" :key="option.value" :value="option.value">
v-for="option in options"
:key="option.value"
:label="option.label"
:value="option.value"
>
{{ option.label }} {{ option.label }}
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>

View File

@@ -77,20 +77,44 @@ export function useWebSocketDict() {
try { try {
// 尝试解析消息 // 尝试解析消息
const eventData = JSON.parse(message.body) as DictEvent; const eventData = JSON.parse(message.body) as DictEvent;
console.log(
`[WebSocket] 接收到字典事件: ${eventData.type}, 字典编码: ${eventData.dictCode}`,
eventData
);
if (eventData.type === "DICT_UPDATED") { if (eventData.type === "DICT_UPDATED") {
// 删除缓存,强制重新加载 // 删除缓存,强制重新加载
dictStore.removeDictItem(eventData.dictCode); dictStore.removeDictItem(eventData.dictCode);
console.log(`字典 ${eventData.dictCode} 已更新,缓存已清除`); console.log(`[WebSocket] 字典 ${eventData.dictCode} 已更新,缓存已清除`);
ElMessage.success(`字典 ${eventData.dictCode} 已更新`); ElMessage.success(`字典 ${eventData.dictCode} 已更新`);
// 派发自定义事件,通知组件刷新数据
window.dispatchEvent(
new CustomEvent("dict-updated", {
detail: {
dictCode: eventData.dictCode,
timestamp: eventData.timestamp,
},
})
);
} else if (eventData.type === "DICT_DELETED") { } else if (eventData.type === "DICT_DELETED") {
// 删除缓存 // 删除缓存
dictStore.removeDictItem(eventData.dictCode); dictStore.removeDictItem(eventData.dictCode);
console.log(`字典 ${eventData.dictCode} 已删除,缓存已清除`); console.log(`[WebSocket] 字典 ${eventData.dictCode} 已删除,缓存已清除`);
ElMessage.warning(`字典 ${eventData.dictCode} 已删除`); ElMessage.warning(`字典 ${eventData.dictCode} 已删除`);
// 派发自定义事件,通知组件刷新数据
window.dispatchEvent(
new CustomEvent("dict-deleted", {
detail: {
dictCode: eventData.dictCode,
timestamp: eventData.timestamp,
},
})
);
} }
} catch (error) { } catch (error) {
console.error("解析字典WebSocket消息失败:", error); console.error("[WebSocket] 解析字典WebSocket消息失败:", error, message.body);
} }
}; };

View File

@@ -210,8 +210,8 @@
<div class="flex-x-between"> <div class="flex-x-between">
<span>访问趋势</span> <span>访问趋势</span>
<el-radio-group v-model="visitTrendDateRange" size="small"> <el-radio-group v-model="visitTrendDateRange" size="small">
<el-radio-button label="近7天" :value="7" /> <el-radio-button :value="7">近7天</el-radio-button>
<el-radio-button label="30" :value="30" /> <el-radio-button :value="30">近30天</el-radio-button>
</el-radio-group> </el-radio-group>
</div> </div>
</template> </template>

View File

@@ -3,124 +3,160 @@
<el-card class="box-card"> <el-card class="box-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>字典WebSocket实时更新演示</span> <span>性别字典WebSocket实时更新演示</span>
</div> </div>
</template> </template>
<el-alert type="info" :closable="false">
<p>本示例展示了当字典数据在服务端更新时如何通过WebSocket实时更新前端缓存</p> <el-alert type="info" :closable="false" class="mb-4">
<p class="mt-2"> 本示例展示WebSocket实时更新字典缓存的效果您可以编辑"男"性别字典项保存后后端将通过WebSocket通知所有客户端刷新缓存
当管理员修改字典数据后其他在线用户的字典缓存将自动刷新无需手动刷新页面
</p>
</el-alert> </el-alert>
<div class="mt-4"> <el-row :gutter="16">
<el-row :gutter="20"> <!-- 列1: 性别字典项编辑 -->
<el-col :span="12"> <el-col :span="8">
<el-card shadow="hover"> <el-card shadow="hover" class="dict-card">
<template #header>字典选择</template> <template #header>
<div> <div class="flex justify-between items-center">
<el-form> <span>性别字典项 - </span>
<el-form-item label="选择字典"> <el-button type="warning" size="small" @click="loadMaleDict">重新加载</el-button>
<el-select v-model="selectedDict" placeholder="请选择字典" @change="loadDict"> </div>
<el-option </template>
v-for="item in dictList" <div v-loading="formLoading">
:key="item.value" <div v-if="dictForm" class="dict-form">
:label="item.label" <el-form :model="dictForm" label-width="80px">
:value="item.value" <el-form-item label="字典编码">
/> <el-input v-model="dictForm.dictCode" disabled />
</el-form-item>
<el-form-item label="字典标签">
<el-input v-model="dictForm.label" />
</el-form-item>
<el-form-item label="字典值">
<el-input v-model="dictForm.value" disabled />
</el-form-item>
<el-form-item label="标记颜色">
<el-select
v-model="dictForm.tagType"
placeholder="选择标签类型"
style="width: 100%"
>
<el-option value="success" label="success">
<el-tag type="success">success</el-tag>
</el-option>
<el-option value="warning" label="warning">
<el-tag type="warning">warning</el-tag>
</el-option>
<el-option value="danger" label="danger">
<el-tag type="danger">danger</el-tag>
</el-option>
<el-option value="info" label="info">
<el-tag type="info">info</el-tag>
</el-option>
<el-option value="primary" label="primary">
<el-tag type="primary">primary</el-tag>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveDict">保存</el-button>
<el-button @click="loadMaleDict">重置</el-button>
</el-form-item>
</el-form> </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> </div>
</el-card> <el-empty v-else description="暂无字典数据" />
</el-col> </div>
</el-card>
</el-col>
<el-col :span="12"> <!-- 列2: WebSocket消息 -->
<el-card shadow="hover"> <el-col :span="8">
<template #header>WebSocket消息</template> <el-card shadow="hover" class="dict-card">
<div class="websocket-log"> <template #header>
<div v-if="logMessages.length === 0" class="text-center py-4 text-gray-400"> <div class="flex justify-between items-center">
暂无WebSocket消息 <span>WebSocket消息</span>
</div> </div>
<div v-else> </template>
<div <div class="websocket-log">
v-for="(msg, index) in logMessages" <div v-if="logMessages.length === 0" class="text-center py-4 text-gray-400">
:key="index" 暂无WebSocket消息
class="log-message" </div>
:class="{ <div v-else>
'bg-blue-50': msg.type === 'info', <div
'bg-green-50': msg.type === 'success', v-for="(msg, index) in logMessages"
'bg-red-50': msg.type === 'error', :key="index"
}" class="log-message"
> :class="{
<div class="flex justify-between"> 'bg-blue-50': msg.type === 'info',
<span class="font-bold">{{ msg.title }}</span> 'bg-green-50': msg.type === 'success',
<span class="text-gray-500 text-sm">{{ formatTime(msg.time) }}</span> 'bg-red-50': msg.type === 'error',
</div> }"
<pre class="text-sm mt-1">{{ JSON.stringify(msg.data, null, 2) }}</pre> >
<div class="flex justify-between">
<span class="font-bold">{{ msg.title }}</span>
<span class="text-gray-500 text-sm">{{ formatTime(msg.time) }}</span>
</div> </div>
<pre class="text-sm mt-1">{{ JSON.stringify(msg.data, null, 2) }}</pre>
</div> </div>
</div> </div>
</el-card> </div>
</el-col> </el-card>
</el-row> </el-col>
</div>
<div class="mt-4"> <!-- 列3: 字典缓存数据 -->
<el-card shadow="hover"> <el-col :span="8">
<template #header>模拟服务端更新字典</template> <el-card shadow="hover" class="dict-card">
<p>这里模拟后端管理员更新字典数据后发送WebSocket通知</p> <template #header>
<p class="mt-2 mb-4 text-gray-500">注意这只是前端模拟实际应用中由后端触发</p> <div class="flex justify-between items-center">
<span>字典缓存数据</span>
<el-form :model="simulateForm" label-width="100px" class="demo-form"> <div>
<el-form-item label="字典编码"> <span class="text-sm text-gray-500">最后更新: {{ lastUpdateTime }}</span>
<el-input v-model="simulateForm.dictCode" /> <el-tag v-if="dictCacheStatus" type="success" class="ml-2" size="small">
</el-form-item> 已缓存
<el-form-item label="事件类型"> </el-tag>
<el-radio-group v-model="simulateForm.eventType"> <el-tag v-else type="danger" class="ml-2" size="small">未缓存</el-tag>
<el-radio label="DICT_UPDATED">字典更新</el-radio> </div>
<el-radio label="DICT_DELETED">字典删除</el-radio> </div>
</el-radio-group> </template>
</el-form-item> <div class="cache-content">
<el-form-item> <pre class="cache-data">{{
<el-button type="primary" @click="simulateDictEvent">模拟发送WebSocket消息</el-button> JSON.stringify(dictStore.getDictItems("gender"), null, 2)
<el-button type="danger" @click="clearCache">清空字典缓存</el-button> }}</pre>
</el-form-item> </div>
</el-form> </el-card>
</el-card> </el-col>
</div> </el-row>
</el-card> </el-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDictStoreHook } from "@/store/modules/dict.store"; import { useDictStoreHook } from "@/store/modules/dict.store";
import { DictWebSocketEvent } from "@/types/websocket"; import { ref, computed, onMounted } from "vue";
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { DictItemOption } from "@/api/system/dict.api";
import { useDateFormat } from "@vueuse/core"; import { useDateFormat } from "@vueuse/core";
import { useWebSocketDict } from "@/hooks/useWebSocketDict"; import DictAPI from "@/api/system/dict.api";
// 字典列表 // 性别字典编码
const dictList = ref([ const DICT_CODE = "gender";
{ label: "性别", value: "gender" }, // 男性字典项ID
{ label: "状态", value: "status" }, const MALE_ITEM_ID = 1;
{ label: "用户类型", value: "user_type" },
]);
// 选中的字典
const selectedDict = ref("");
// 字典项列表
const dictItems = ref<DictItemOption[]>([]);
// 字典store // 字典store
const dictStore = useDictStoreHook(); const dictStore = useDictStoreHook();
// 表单加载状态
const formLoading = ref(false);
// 保存状态
const saving = ref(false);
// 最后更新时间
const lastUpdateTime = ref("-");
// 字典表单数据
const dictForm = ref<{
id?: number;
dictCode: string;
label: string;
value: string;
tagType?: string;
status?: number;
} | null>(null);
// 日志消息类型 // 日志消息类型
interface LogMessage { interface LogMessage {
@@ -130,92 +166,131 @@ interface LogMessage {
time: Date; time: Date;
} }
// 当前选中字典的缓存状态
const dictCacheStatus = computed(() => {
// 检查字典是否在缓存中
return dictStore.getDictItems(DICT_CODE).length > 0;
});
// WebSocket日志消息 // WebSocket日志消息
const logMessages = ref<LogMessage[]>([]); const logMessages = ref<LogMessage[]>([]);
// 模拟表单 // 加载男性字典表单数据
const simulateForm = reactive({ const loadMaleDict = async () => {
dictCode: "", formLoading.value = true;
eventType: "DICT_UPDATED", try {
}); // 先确保字典缓存已加载
if (!dictCacheStatus.value) {
await dictStore.loadDictItems(DICT_CODE);
}
// 加载字典数据 // 获取男性字典项表单数据 - 使用接口 /dicts/gender/items/1/form
const loadDict = async (dictCode: string) => { const data = await DictAPI.getDictItemFormData(DICT_CODE, MALE_ITEM_ID);
await dictStore.loadDictItems(dictCode);
dictItems.value = dictStore.getDictItems(dictCode);
// 添加日志 // 调试日志
addLogMessage({ console.log("表单数据响应:", data);
title: "加载字典数据",
type: "info", if (data) {
data: { dictForm.value = data;
dictCode, // 确保字典值类型为字符串
items: dictItems.value, if (dictForm.value && dictForm.value.value) {
}, dictForm.value.value = String(dictForm.value.value);
}); }
} else {
// 创建一个默认的表单数据用于显示
dictForm.value = {
dictCode: DICT_CODE,
label: "男",
value: "1",
tagType: "primary",
status: 1,
};
ElMessage.warning("未获取到表单数据,已创建默认数据");
}
// 更新时间
lastUpdateTime.value = useDateFormat(new Date(), "YYYY-MM-DD HH:mm:ss").value;
// 添加WebSocket连接日志
addLogMessage({
title: "WebSocket连接成功",
type: "success",
data: {
message: "已与服务器建立WebSocket连接",
time: new Date().toISOString(),
},
});
} catch (error) {
console.error("加载字典数据失败:", error);
ElMessage.error("加载字典数据失败: " + (error.message || "未知错误"));
// 创建一个默认的表单数据用于显示
dictForm.value = {
dictCode: DICT_CODE,
label: "男",
value: "1",
tagType: "primary",
status: 1,
};
ElMessage.warning("加载失败,已创建默认数据");
} finally {
formLoading.value = false;
}
}; };
// 模拟字典事件 // 保存字典项
const simulateDictEvent = () => { const saveDict = async () => {
const { dictCode, eventType } = simulateForm; if (!dictForm.value) return;
if (!dictCode) { saving.value = true;
ElMessage.warning("请输入字典编码"); try {
return; // 调用API保存
} await DictAPI.updateDictItem(DICT_CODE, MALE_ITEM_ID, dictForm.value);
// 构造字典事件 // 清除缓存
const event: DictWebSocketEvent = { dictStore.removeDictItem(DICT_CODE);
type: eventType as "DICT_UPDATED" | "DICT_DELETED",
dictCode,
timestamp: Date.now(),
};
// 添加日志 // 更新时间
addLogMessage({ lastUpdateTime.value = useDateFormat(new Date(), "YYYY-MM-DD HH:mm:ss").value;
title: "模拟WebSocket消息",
type: "success",
data: event,
});
// 导入WebSocket字典钩子 // 添加WebSocket消息推送日志
const { handleDictEvent } = useWebSocketDict(); addLogMessage({
title: "接收到WebSocket消息",
// 手动调用处理函数模拟收到WebSocket消息 type: "info",
handleDictEvent(event); data: {
type: "DICT_UPDATED",
// 如果是当前选中的字典被更新,则刷新显示 dictCode: DICT_CODE,
if (selectedDict.value === dictCode) { item: {
setTimeout(() => { id: MALE_ITEM_ID,
dictItems.value = dictStore.getDictItems(dictCode); label: dictForm.value.label,
value: dictForm.value.value,
addLogMessage({
title: "字典数据已更新",
type: "info",
data: {
dictCode,
items: dictItems.value,
}, },
}); timestamp: Date.now(),
},
});
ElMessage.success("保存成功后端已触发WebSocket通知");
// 重新加载字典数据以显示最新缓存
setTimeout(() => {
dictStore.loadDictItems(DICT_CODE);
}, 500); }, 500);
} catch (error) {
console.error("保存字典项失败:", error);
ElMessage.error("保存失败: " + (error.message || "未知错误"));
addLogMessage({
title: "保存字典项失败",
type: "error",
data: {
dictCode: DICT_CODE,
error: error.message || "未知错误",
},
});
} finally {
saving.value = false;
} }
}; };
// 清空字典缓存
const clearCache = () => {
dictStore.clearDictCache();
dictItems.value = [];
selectedDict.value = "";
addLogMessage({
title: "字典缓存已清空",
type: "error",
data: {},
});
ElMessage.success("字典缓存已清空");
};
// 添加日志消息 // 添加日志消息
const addLogMessage = (message: { title: string; type: string; data: any }) => { const addLogMessage = (message: { title: string; type: string; data: any }) => {
logMessages.value.unshift({ logMessages.value.unshift({
@@ -233,11 +308,28 @@ const addLogMessage = (message: { title: string; type: string; data: any }) => {
const formatTime = (date: Date) => { const formatTime = (date: Date) => {
return useDateFormat(date, "HH:mm:ss").value; return useDateFormat(date, "HH:mm:ss").value;
}; };
// 组件挂载时加载性别字典
onMounted(() => {
loadMaleDict();
});
</script> </script>
<style scoped> <style scoped>
.dict-card {
display: flex;
flex-direction: column;
height: 600px;
overflow: hidden;
}
.dict-card :deep(.el-card__body) {
flex: 1;
overflow: auto;
}
.websocket-log { .websocket-log {
max-height: 400px; height: 100%;
overflow-y: auto; overflow-y: auto;
} }
@@ -252,8 +344,30 @@ const formatTime = (date: Date) => {
margin-bottom: 0; margin-bottom: 0;
} }
.cache-content {
height: 100%;
overflow: hidden;
}
pre { pre {
padding: 8px;
overflow-y: auto;
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
background-color: #f8f9fa;
border-radius: 4px;
}
.cache-data {
height: 100%;
padding: 8px;
overflow-y: auto;
font-size: 12px;
background-color: #f8f9fa;
border-radius: 4px;
}
.dict-form {
margin-bottom: 20px;
} }
</style> </style>