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

View File

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

View File

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

View File

@@ -3,124 +3,160 @@
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>字典WebSocket实时更新演示</span>
<span>性别字典WebSocket实时更新演示</span>
</div>
</template>
<el-alert type="info" :closable="false">
<p>本示例展示了当字典数据在服务端更新时如何通过WebSocket实时更新前端缓存</p>
<p class="mt-2">
当管理员修改字典数据后其他在线用户的字典缓存将自动刷新无需手动刷新页面
</p>
<el-alert type="info" :closable="false" class="mb-4">
本示例展示WebSocket实时更新字典缓存的效果您可以编辑"男"性别字典项保存后后端将通过WebSocket通知所有客户端刷新缓存
</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-row :gutter="16">
<!-- 列1: 性别字典项编辑 -->
<el-col :span="8">
<el-card shadow="hover" class="dict-card">
<template #header>
<div class="flex justify-between items-center">
<span>性别字典项 - </span>
<el-button type="warning" size="small" @click="loadMaleDict">重新加载</el-button>
</div>
</template>
<div v-loading="formLoading">
<div v-if="dictForm" class="dict-form">
<el-form :model="dictForm" label-width="80px">
<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-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-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-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>
<!-- 列2: WebSocket消息 -->
<el-col :span="8">
<el-card shadow="hover" class="dict-card">
<template #header>
<div class="flex justify-between items-center">
<span>WebSocket消息</span>
</div>
</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>
</el-card>
</el-col>
</el-row>
</div>
</div>
</el-card>
</el-col>
<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>
<!-- 列3: 字典缓存数据 -->
<el-col :span="8">
<el-card shadow="hover" class="dict-card">
<template #header>
<div class="flex justify-between items-center">
<span>字典缓存数据</span>
<div>
<span class="text-sm text-gray-500">最后更新: {{ lastUpdateTime }}</span>
<el-tag v-if="dictCacheStatus" type="success" class="ml-2" size="small">
已缓存
</el-tag>
<el-tag v-else type="danger" class="ml-2" size="small">未缓存</el-tag>
</div>
</div>
</template>
<div class="cache-content">
<pre class="cache-data">{{
JSON.stringify(dictStore.getDictItems("gender"), null, 2)
}}</pre>
</div>
</el-card>
</el-col>
</el-row>
</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 { ref, computed, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { DictItemOption } from "@/api/system/dict.api";
import { useDateFormat } from "@vueuse/core";
import { useWebSocketDict } from "@/hooks/useWebSocketDict";
import DictAPI from "@/api/system/dict.api";
// 字典列表
const dictList = ref([
{ label: "性别", value: "gender" },
{ label: "状态", value: "status" },
{ label: "用户类型", value: "user_type" },
]);
// 性别字典编码
const DICT_CODE = "gender";
// 男性字典项ID
const MALE_ITEM_ID = 1;
// 选中的字典
const selectedDict = ref("");
// 字典项列表
const dictItems = ref<DictItemOption[]>([]);
// 字典store
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 {
@@ -130,92 +166,131 @@ interface LogMessage {
time: Date;
}
// 当前选中字典的缓存状态
const dictCacheStatus = computed(() => {
// 检查字典是否在缓存中
return dictStore.getDictItems(DICT_CODE).length > 0;
});
// WebSocket日志消息
const logMessages = ref<LogMessage[]>([]);
// 模拟表单
const simulateForm = reactive({
dictCode: "",
eventType: "DICT_UPDATED",
});
// 加载男性字典表单数据
const loadMaleDict = async () => {
formLoading.value = true;
try {
// 先确保字典缓存已加载
if (!dictCacheStatus.value) {
await dictStore.loadDictItems(DICT_CODE);
}
// 加载字典数据
const loadDict = async (dictCode: string) => {
await dictStore.loadDictItems(dictCode);
dictItems.value = dictStore.getDictItems(dictCode);
// 获取男性字典项表单数据 - 使用接口 /dicts/gender/items/1/form
const data = await DictAPI.getDictItemFormData(DICT_CODE, MALE_ITEM_ID);
// 添加日志
addLogMessage({
title: "加载字典数据",
type: "info",
data: {
dictCode,
items: dictItems.value,
},
});
// 调试日志
console.log("表单数据响应:", data);
if (data) {
dictForm.value = data;
// 确保字典值类型为字符串
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 { dictCode, eventType } = simulateForm;
// 保存字典项
const saveDict = async () => {
if (!dictForm.value) return;
if (!dictCode) {
ElMessage.warning("请输入字典编码");
return;
}
saving.value = true;
try {
// 调用API保存
await DictAPI.updateDictItem(DICT_CODE, MALE_ITEM_ID, dictForm.value);
// 构造字典事件
const event: DictWebSocketEvent = {
type: eventType as "DICT_UPDATED" | "DICT_DELETED",
dictCode,
timestamp: Date.now(),
};
// 清除缓存
dictStore.removeDictItem(DICT_CODE);
// 添加日志
addLogMessage({
title: "模拟WebSocket消息",
type: "success",
data: event,
});
// 更新时间
lastUpdateTime.value = useDateFormat(new Date(), "YYYY-MM-DD HH:mm:ss").value;
// 导入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,
// 添加WebSocket消息推送日志
addLogMessage({
title: "接收到WebSocket消息",
type: "info",
data: {
type: "DICT_UPDATED",
dictCode: DICT_CODE,
item: {
id: MALE_ITEM_ID,
label: dictForm.value.label,
value: dictForm.value.value,
},
});
timestamp: Date.now(),
},
});
ElMessage.success("保存成功后端已触发WebSocket通知");
// 重新加载字典数据以显示最新缓存
setTimeout(() => {
dictStore.loadDictItems(DICT_CODE);
}, 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 }) => {
logMessages.value.unshift({
@@ -233,11 +308,28 @@ const addLogMessage = (message: { title: string; type: string; data: any }) => {
const formatTime = (date: Date) => {
return useDateFormat(date, "HH:mm:ss").value;
};
// 组件挂载时加载性别字典
onMounted(() => {
loadMaleDict();
});
</script>
<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 {
max-height: 400px;
height: 100%;
overflow-y: auto;
}
@@ -252,8 +344,30 @@ const formatTime = (date: Date) => {
margin-bottom: 0;
}
.cache-content {
height: 100%;
overflow: hidden;
}
pre {
padding: 8px;
overflow-y: auto;
word-wrap: break-word;
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>