refactor: ♻️ 字典缓存加载方式修改为按需加载,添加字典实时更新通知

This commit is contained in:
ray
2025-04-27 18:30:24 +08:00
parent 6a245e9d26
commit 7a04eea075
10 changed files with 606 additions and 10043 deletions

9984
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -292,11 +292,14 @@ export interface DictItemForm {
*/
export interface DictItemOption {
/** 字典数据值 */
value: string;
value: string | number;
/** 字典数据标签 */
label: string;
/** 标签类型 */
tagType: string;
tagType?: "" | "success" | "info" | "warning" | "danger" | "primary";
/** 允许其他属性 */
[key: string]: any;
}

View File

@@ -19,31 +19,44 @@ const props = defineProps({
default: false,
},
});
type TagType = "success" | "warning" | "primary" | "danger" | "default";
const label = ref("");
const tagType = ref<TagType>();
const tagType = ref<string | undefined>();
const getLabelAndTagByValue = async (dictCode: string, value: any) => {
// 先从本地缓存中获取字典数据
const dictData = dictStore.getDictionary(dictCode);
console.log("dictData", dictData);
// 按需加载字典数据
await dictStore.loadDictItems(dictCode);
// 从缓存中获取字典数据
const dictItems = dictStore.getDictItems(dictCode);
// 查找对应的字典项
const dictEntry = dictData.find((item: any) => item.value == value);
const dictItem = dictItems.find((item) => item.value == value);
return {
label: dictEntry ? dictEntry.label : "",
tag: dictEntry ? dictEntry.tagType : undefined,
label: dictItem?.label || "",
tagType: dictItem?.tagType,
};
};
// 监听字典数据变化确保WebSocket更新时刷新标签
watch(
() => props.code && dictStore.getDictItems(props.code),
async () => {
if (props.code) {
await fetchLabelAndTag();
}
},
{ deep: true }
);
// 监听 props 的变化,获取并更新 label 和 tag
const fetchLabelAndTag = async () => {
const result = await getLabelAndTagByValue(props.code as string, props.modelValue);
console.log("result", result);
if (!props.code || props.modelValue === undefined) return;
const result = await getLabelAndTagByValue(props.code, props.modelValue);
label.value = result.label;
if (result.tag === "info") {
result.tag = "default";
}
tagType.value = result.tag as "success" | "warning" | "primary" | "danger" | "default";
tagType.value = result.tagType;
};
// 首次挂载时获取字典数据

View File

@@ -114,13 +114,28 @@ function handleChange(val: any) {
// 获取字典数据
onMounted(async () => {
if (!props.type) {
if (!props.code) {
return;
}
let dictData = dictStore.getDictionary(props.code);
options.value = dictData.map((item) => ({
// 按需加载字典数据
await dictStore.loadDictItems(props.code);
options.value = dictStore.getDictItems(props.code).map((item) => ({
label: item.label,
value: item.value,
}));
});
// 监听字典数据变化确保WebSocket更新时刷新选项
watch(
() => dictStore.getDictItems(props.code),
(newItems) => {
if (newItems.length > 0) {
options.value = newItems.map((item) => ({
label: item.label,
value: item.value,
}));
}
},
{ deep: true }
);
</script>

7
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,7 @@
/**
* 全局Hooks入口文件
* 导出所有可用的Hooks
*/
// 导出WebSocket相关Hook
export * from "./websocket";

View File

@@ -0,0 +1,285 @@
import { getToken as getAccessToken } from "@/utils/cache";
export interface UseStompOptions {
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
brokerURL?: string;
/** 重连延迟,单位毫秒,默认为 8000 */
reconnectDelay?: number;
/** 连接超时时间,单位毫秒,默认为 10000 */
connectionTimeout?: number;
/** 是否开启指数退避重连策略 */
useExponentialBackoff?: boolean;
/** 最大重连次数,默认为 5 */
maxReconnectAttempts?: number;
/** 最大重连延迟,单位毫秒,默认为 60000 */
maxReconnectDelay?: number;
/** 是否开启调试日志 */
debug?: boolean;
}
/**
* STOMP WebSocket连接Hook (UniApp版本)
* 用于管理WebSocket连接的建立、断开、重连和消息订阅
*/
export function useStomp(options: UseStompOptions = {}) {
// 默认配置
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
const reconnectDelay = options.reconnectDelay ?? 8000;
const connectionTimeout = options.connectionTimeout ?? 10000;
const useExponentialBackoff = options.useExponentialBackoff ?? false;
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
const maxReconnectDelay = options.maxReconnectDelay ?? 60000;
// 连接状态和计数
const isConnected = ref(false);
const reconnectCount = ref(0);
let reconnectTimer: number | null = null;
let connectionTimeoutTimer: number | null = null;
// 存储所有订阅
const subscriptions = ref<Record<string, any>>({});
// WebSocket实例
let socketTask: any = null;
const client = ref<any>(null);
/**
* 创建WebSocket连接
*/
const createSocketConnection = () => {
const token = getAccessToken();
if (!token) {
console.error("WebSocket连接失败未找到有效token");
return null;
}
// 创建WebSocket连接
try {
// 构建带有token的URL
let url = brokerURL.value;
if (url.indexOf("?") > -1) {
url += "&token=" + token;
} else {
url += "?token=" + token;
}
// 创建WebSocket连接
socketTask = uni.connectSocket({
url: url,
complete: () => {},
});
if (!socketTask) {
console.error("WebSocket连接创建失败");
return null;
}
// 设置WebSocket事件处理函数
socketTask.onOpen(() => {
isConnected.value = true;
reconnectCount.value = 0;
if (connectionTimeoutTimer) clearTimeout(connectionTimeoutTimer);
console.log("WebSocket连接已建立");
});
socketTask.onClose(() => {
isConnected.value = false;
console.log("WebSocket连接已关闭");
// 如果使用指数退避重连策略,则处理重连
if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) {
handleReconnect();
}
});
socketTask.onError((error: any) => {
console.error("WebSocket连接错误:", error);
});
socketTask.onMessage((res: any) => {
const message = JSON.parse(res.data);
// 处理订阅消息
if (message.subscription && subscriptions.value[message.subscription]) {
const subscription = subscriptions.value[message.subscription];
if (subscription.callback) {
subscription.callback(message);
}
}
});
return socketTask;
} catch (error) {
console.error("创建WebSocket连接时出错:", error);
return null;
}
};
/**
* 处理重连逻辑
*/
const handleReconnect = () => {
if (reconnectCount.value >= maxReconnectAttempts) {
console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`);
return;
}
reconnectCount.value++;
console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
// 使用指数退避策略
const delay = useExponentialBackoff
? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay)
: reconnectDelay;
// 清除之前的计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
// 设置重连计时器
reconnectTimer = setTimeout(() => {
if (!isConnected.value) {
connect();
}
}, delay) as unknown as number;
};
/**
* 建立WebSocket连接
*/
const connect = () => {
if (isConnected.value) {
return;
}
// 创建WebSocket连接
socketTask = createSocketConnection();
client.value = socketTask;
// 设置连接超时
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
}
connectionTimeoutTimer = setTimeout(() => {
if (!isConnected.value) {
console.warn("WebSocket连接超时");
if (useExponentialBackoff) {
handleReconnect();
}
}
}, connectionTimeout) as unknown as number;
};
/**
* 订阅主题
* @param destination 主题地址
* @param callback 回调函数
* @returns 订阅ID
*/
const subscribe = (destination: string, callback: (message: any) => void): string => {
if (!socketTask || !isConnected.value) {
console.warn("WebSocket未连接无法订阅:", destination);
return "";
}
// 生成唯一订阅ID
const subscriptionId = "sub-" + Math.random().toString(36).substr(2, 9);
// 发送订阅消息
socketTask.send({
data: JSON.stringify({
command: "SUBSCRIBE",
headers: {
id: subscriptionId,
destination: destination,
},
}),
success: () => {
console.log(`订阅成功: ${destination}, ID: ${subscriptionId}`);
},
fail: (err: any) => {
console.error(`订阅失败(${destination}):`, err);
},
});
// 保存订阅
subscriptions.value[subscriptionId] = {
destination,
callback,
};
return subscriptionId;
};
/**
* 取消订阅
* @param subscriptionId 订阅ID
*/
const unsubscribe = (subscriptionId: string) => {
if (!socketTask || !isConnected.value || !subscriptions.value[subscriptionId]) {
return;
}
try {
// 发送取消订阅消息
socketTask.send({
data: JSON.stringify({
command: "UNSUBSCRIBE",
headers: {
id: subscriptionId,
},
}),
success: () => {
console.log(`已取消订阅: ${subscriptionId}`);
},
fail: (err: any) => {
console.error(`取消订阅失败(${subscriptionId}):`, err);
},
});
} catch (error) {
console.error(`取消订阅失败(${subscriptionId}):`, error);
} finally {
Reflect.deleteProperty(subscriptions.value, subscriptionId);
}
};
/**
* 断开WebSocket连接
*/
const disconnect = () => {
if (!socketTask) {
return;
}
// 取消所有订阅
Object.keys(subscriptions.value).forEach(unsubscribe);
// 断开WebSocket连接
try {
socketTask.close({
success: () => {
console.log("WebSocket连接已断开");
isConnected.value = false;
socketTask = null;
},
fail: (err: any) => {
console.error("断开WebSocket连接失败:", err);
},
});
} catch (error) {
console.error("断开WebSocket连接失败:", error);
}
};
// 返回公开的API
return {
isConnected,
client,
connect,
disconnect,
subscribe,
unsubscribe,
};
}

View File

@@ -0,0 +1,10 @@
/**
* WebSocket相关Hook入口文件
* 统一导出所有WebSocket相关Hook
*/
// 核心基础Hook
export { useStomp } from "./core/useStomp";
// 业务服务Hook
export { useDictSync } from "./services/useDictSync";

View File

@@ -0,0 +1,188 @@
import { useDictStore } from "@/store/modules/dict";
import { useStomp } from "../core/useStomp";
import { ref } from "vue";
// 字典消息类型
export interface DictMessage {
dictCode: string;
timestamp: number;
}
// 字典事件回调类型
export type DictMessageCallback = (message: DictMessage) => void;
// 全局单例实例
let instance: ReturnType<typeof createDictSyncHook> | null = null;
/**
* 创建字典同步Hook
* 负责监听后端字典变更并同步到前端
*/
function createDictSyncHook() {
const dictStore = useDictStore();
// 使用现有的useStomp配置适合字典场景的重连参数
const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp({
reconnectDelay: 10000, // 使用更长的重连延迟 - 10秒
connectionTimeout: 15000, // 更长的连接超时时间 - 15秒
useExponentialBackoff: false, // 字典数据不需要指数退避策略
});
// 存储订阅ID
const subscriptionIds = ref<string[]>([]);
// 已订阅的主题
const subscribedTopics = ref<Set<string>>(new Set());
// 消息回调函数列表
const messageCallbacks = ref<DictMessageCallback[]>([]);
/**
* 注册字典消息回调
* @param callback 回调函数
*/
const onDictMessage = (callback: DictMessageCallback) => {
messageCallbacks.value.push(callback);
return () => {
// 返回取消注册的函数
const index = messageCallbacks.value.indexOf(callback);
if (index !== -1) {
messageCallbacks.value.splice(index, 1);
}
};
};
/**
* 初始化WebSocket
*/
const initWebSocket = async () => {
try {
// 连接WebSocket
connect();
// 设置字典订阅
setupDictSubscription();
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
}
};
/**
* 关闭WebSocket
*/
const closeWebSocket = () => {
// 取消所有订阅
subscriptionIds.value.forEach((id) => {
unsubscribe(id);
});
subscriptionIds.value = [];
subscribedTopics.value.clear();
// 断开连接
disconnect();
};
/**
* 设置字典订阅
*/
const setupDictSubscription = () => {
const topic = "/topic/dict";
// 防止重复订阅
if (subscribedTopics.value.has(topic)) {
console.log(`跳过重复订阅: ${topic}`);
return;
}
console.log(`开始尝试订阅字典主题: ${topic}`);
// 使用简化的重试逻辑依赖useStomp的连接管理
const attemptSubscribe = () => {
if (!isConnected.value) {
console.log("等待WebSocket连接建立...");
// 3秒后再次尝试
setTimeout(attemptSubscribe, 3000);
return;
}
// 检查是否已订阅
if (subscribedTopics.value.has(topic)) {
return;
}
console.log(`连接已建立,开始订阅: ${topic}`);
// 订阅字典更新
const subId = subscribe(topic, (message: any) => {
handleDictEvent(message);
});
if (subId) {
subscriptionIds.value.push(subId);
subscribedTopics.value.add(topic);
console.log(`字典主题订阅成功: ${topic}`);
} else {
console.warn(`字典主题订阅失败: ${topic}`);
}
};
// 开始尝试订阅
attemptSubscribe();
};
/**
* 处理字典事件
* @param message STOMP消息
*/
const handleDictEvent = (message: any) => {
if (!message.body) return;
try {
// 记录接收到的消息
console.log(`收到字典更新消息: ${message.body}`);
// 尝试解析消息
const parsedData = JSON.parse(message.body) as DictMessage;
const dictCode = parsedData.dictCode;
if (!dictCode) return;
// 清除缓存,等待按需加载
dictStore.removeDictItem(dictCode);
console.log(`字典缓存已清除: ${dictCode}`);
// 调用所有注册的回调函数
messageCallbacks.value.forEach((callback) => {
try {
callback(parsedData);
} catch (callbackError) {
console.error("[WebSocket] 回调执行失败:", callbackError);
}
});
// 显示提示消息
console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`);
} catch (error) {
console.error("[WebSocket] 解析消息失败:", error);
}
};
return {
isConnected,
initWebSocket,
closeWebSocket,
handleDictEvent,
onDictMessage,
};
}
/**
* 字典同步Hook
* 用于监听后端字典变更并同步到前端
*/
export function useDictSync() {
if (!instance) {
instance = createDictSyncHook();
}
return instance;
}

View File

@@ -1,38 +1,80 @@
import { defineStore } from "pinia";
import DictAPI, { type DictVO, type DictData } from "@/api/system/dict";
import { setDictCache, getDictCache } from "@/utils/cache";
import DictAPI, { type DictItemOption } from "@/api/system/dict";
const DICT_CACHE_KEY = "dict_cache";
export const useDictStore = defineStore("dict", () => {
const dictionary = ref<Record<string, DictData[]>>(getDictCache());
// 字典数据缓存
const dictCache = ref<Record<string, DictItemOption[]>>(uni.getStorageSync(DICT_CACHE_KEY) || {});
const setDictionary = (dict: DictVO) => {
dictionary.value[dict.dictCode] = dict.dictDataList;
setDictCache(dictionary.value);
// 监听dictCache变化同步到本地存储
watch(
dictCache,
(newVal) => {
uni.setStorageSync(DICT_CACHE_KEY, newVal);
},
{ deep: true }
);
// 请求队列(防止重复请求)
const requestQueue: Record<string, Promise<void>> = {};
/**
* 缓存字典数据
* @param dictCode 字典编码
* @param data 字典项列表
*/
const cacheDictItems = (dictCode: string, data: DictItemOption[]) => {
dictCache.value[dictCode] = data;
};
const loadDictionaries = async () => {
const dictList = await DictAPI.getList();
dictList.forEach(setDictionary);
/**
* 加载字典数据(如果缓存中没有则请求)
* @param dictCode 字典编码
*/
const loadDictItems = async (dictCode: string) => {
if (dictCache.value[dictCode]) return;
// 防止重复请求
if (!requestQueue[dictCode]) {
requestQueue[dictCode] = DictAPI.getDictItems(dictCode).then((data) => {
cacheDictItems(dictCode, data);
Reflect.deleteProperty(requestQueue, dictCode);
});
}
await requestQueue[dictCode];
};
const getDictionary = (dictCode: string): DictData[] => {
return dictionary.value[dictCode] || [];
/**
* 获取字典项列表
* @param dictCode 字典编码
* @returns 字典项列表
*/
const getDictItems = (dictCode: string): DictItemOption[] => {
return dictCache.value[dictCode] || [];
};
const clearDictionaryCache = () => {
dictionary.value = {};
/**
* 移除指定字典项
* @param dictCode 字典编码
*/
const removeDictItem = (dictCode: string) => {
if (dictCache.value[dictCode]) {
Reflect.deleteProperty(dictCache.value, dictCode);
}
};
const updateDictionaryCache = async () => {
clearDictionaryCache(); // 先清除旧缓存
await loadDictionaries(); // 重新加载最新字典数据
/**
* 清空字典缓存
*/
const clearDictCache = () => {
dictCache.value = {};
};
return {
dictionary,
setDictionary,
loadDictionaries,
getDictionary,
clearDictionaryCache,
updateDictionaryCache,
loadDictItems,
getDictItems,
removeDictItem,
clearDictCache,
};
});

View File

@@ -1,7 +1,5 @@
const TOKEN_KEY = "app-token";
const USER_INFO_KEY = "user-info";
const DICT_KEY = "dict";
import { type DictData } from "@/api/system/dict";
// 设置 token
export function setToken(token: string) {
@@ -33,24 +31,10 @@ export function clearUserInfo() {
uni.removeStorageSync(USER_INFO_KEY);
}
// 设置字典缓存
export function setDictCache(dict: Record<string, DictData[]>) {
uni.setStorageSync(DICT_KEY, dict);
}
// 获取字典缓存
export function getDictCache(): Record<string, DictData[]> {
return uni.getStorageSync(DICT_KEY) || {};
}
// 清除字典缓存
export function clearDictCache() {
uni.removeStorageSync(DICT_KEY);
}
// 清除所有缓存信息
export function clearAll() {
clearToken();
clearUserInfo();
clearDictCache();
// 清除字典缓存
uni.removeStorageSync("dict_cache");
}