wip: 字典 websocket 实时更新
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user