feat: 项目结构重构优化
This commit is contained in:
@@ -1,91 +0,0 @@
|
||||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
import { useUserStoreHook } from "@/store/modules/user-store";
|
||||
import { AuthStorage, redirectToLogin } from "@/utils/auth";
|
||||
|
||||
/**
|
||||
* 重试请求的回调函数类型
|
||||
*/
|
||||
type RetryCallback = () => void;
|
||||
|
||||
/**
|
||||
* Token刷新组合式函数
|
||||
*/
|
||||
export function useTokenRefresh() {
|
||||
// Token 刷新相关状态
|
||||
let isRefreshingToken = false;
|
||||
const pendingRequests: RetryCallback[] = [];
|
||||
|
||||
/**
|
||||
* 刷新 Token 并重试请求
|
||||
*/
|
||||
async function refreshTokenAndRetry(
|
||||
config: InternalAxiosRequestConfig,
|
||||
httpRequest: any
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 封装需要重试的请求
|
||||
const retryRequest = () => {
|
||||
const newToken = AuthStorage.getAccessToken();
|
||||
if (newToken && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
}
|
||||
httpRequest(config).then(resolve).catch(reject);
|
||||
};
|
||||
|
||||
// 将请求加入等待队列
|
||||
pendingRequests.push(retryRequest);
|
||||
|
||||
// 如果没有正在刷新,则开始刷新流程
|
||||
if (!isRefreshingToken) {
|
||||
isRefreshingToken = true;
|
||||
|
||||
useUserStoreHook()
|
||||
.refreshToken()
|
||||
.then(() => {
|
||||
// 刷新成功,重试所有等待的请求
|
||||
pendingRequests.forEach((callback) => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error("Retry request error:", error);
|
||||
}
|
||||
});
|
||||
// 清空队列
|
||||
pendingRequests.length = 0;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error("Token refresh failed:", error);
|
||||
// 刷新失败,先 reject 所有等待的请求,再清空队列
|
||||
const failedRequests = [...pendingRequests];
|
||||
pendingRequests.length = 0;
|
||||
|
||||
// 拒绝所有等待的请求
|
||||
failedRequests.forEach(() => {
|
||||
reject(new Error("Token refresh failed"));
|
||||
});
|
||||
|
||||
// 跳转登录页
|
||||
await redirectToLogin("登录状态已失效,请重新登录");
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshingToken = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刷新状态(用于外部判断)
|
||||
*/
|
||||
function getRefreshStatus() {
|
||||
return {
|
||||
isRefreshing: isRefreshingToken,
|
||||
pendingCount: pendingRequests.length,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
refreshTokenAndRetry,
|
||||
getRefreshStatus,
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
export { useStomp } from "./websocket/useStomp";
|
||||
export { useDictSync } from "./websocket/useDictSync";
|
||||
export type { DictMessage } from "./websocket/useDictSync";
|
||||
export { useOnlineCount } from "./websocket/useOnlineCount";
|
||||
export { useTokenRefresh } from "./auth/useTokenRefresh";
|
||||
|
||||
export { useLayout } from "./layout/useLayout";
|
||||
export { useLayoutMenu } from "./layout/useLayoutMenu";
|
||||
export { useDeviceDetection } from "./layout/useDeviceDetection";
|
||||
// WebSocket 服务
|
||||
export { setupWebSocket, cleanupWebSocket } from "./websocket";
|
||||
export { useStomp, useDictSync, useOnlineCount } from "./websocket";
|
||||
export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./websocket";
|
||||
|
||||
// AI 相关
|
||||
export { useAiAction } from "./ai/useAiAction";
|
||||
export type { UseAiActionOptions, AiActionHandler } from "./ai/useAiAction";
|
||||
|
||||
// 表格相关
|
||||
export { useTableSelection } from "./table/useTableSelection";
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { watchEffect, computed } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { useAppStore } from "@/store";
|
||||
import { DeviceEnum } from "@/enums/settings";
|
||||
|
||||
/**
|
||||
* 设备检测和响应式处理
|
||||
* 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
|
||||
*/
|
||||
export function useDeviceDetection() {
|
||||
const appStore = useAppStore();
|
||||
const { width } = useWindowSize();
|
||||
|
||||
// 桌面设备断点
|
||||
const DESKTOP_BREAKPOINT = 992;
|
||||
|
||||
// 计算设备类型
|
||||
const isDesktop = computed(() => width.value >= DESKTOP_BREAKPOINT);
|
||||
const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
|
||||
|
||||
// 监听屏幕尺寸变化,自动调整设备类型和侧边栏状态
|
||||
watchEffect(() => {
|
||||
const deviceType = isDesktop.value ? DeviceEnum.DESKTOP : DeviceEnum.MOBILE;
|
||||
|
||||
// 更新设备类型
|
||||
appStore.toggleDevice(deviceType);
|
||||
|
||||
// 根据设备类型调整侧边栏状态
|
||||
if (isDesktop.value) {
|
||||
appStore.openSideBar();
|
||||
} else {
|
||||
appStore.closeSideBar();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isDesktop,
|
||||
isMobile,
|
||||
};
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useAppStore, useSettingsStore } from "@/store";
|
||||
import { defaultSettings } from "@/settings";
|
||||
|
||||
/**
|
||||
* 布局相关的通用逻辑
|
||||
*/
|
||||
export function useLayout() {
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 计算当前布局模式
|
||||
const currentLayout = computed(() => settingsStore.layout);
|
||||
|
||||
// 侧边栏展开状态
|
||||
const isSidebarOpen = computed(() => appStore.sidebar.opened);
|
||||
|
||||
// 是否显示标签视图
|
||||
const isShowTagsView = computed(() => settingsStore.showTagsView);
|
||||
|
||||
// 是否显示设置面板
|
||||
const isShowSettings = computed(() => defaultSettings.showSettings);
|
||||
|
||||
// 是否显示Logo
|
||||
const isShowLogo = computed(() => settingsStore.showAppLogo);
|
||||
|
||||
// 是否移动设备
|
||||
const isMobile = computed(() => appStore.device === "mobile");
|
||||
|
||||
// 布局CSS类
|
||||
const layoutClass = computed(() => ({
|
||||
hideSidebar: !appStore.sidebar.opened,
|
||||
openSidebar: appStore.sidebar.opened,
|
||||
mobile: appStore.device === "mobile",
|
||||
[`layout-${settingsStore.layout}`]: true,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 处理切换侧边栏的展开/收起状态
|
||||
*/
|
||||
function toggleSidebar() {
|
||||
appStore.toggleSidebar();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭侧边栏(移动端)
|
||||
*/
|
||||
function closeSidebar() {
|
||||
appStore.closeSideBar();
|
||||
}
|
||||
|
||||
return {
|
||||
currentLayout,
|
||||
isSidebarOpen,
|
||||
isShowTagsView,
|
||||
isShowSettings,
|
||||
isShowLogo,
|
||||
isMobile,
|
||||
layoutClass,
|
||||
toggleSidebar,
|
||||
closeSidebar,
|
||||
};
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useRoute } from "vue-router";
|
||||
import { useAppStore, usePermissionStore } from "@/store";
|
||||
|
||||
/**
|
||||
* 布局菜单处理逻辑
|
||||
*/
|
||||
export function useLayoutMenu() {
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
// 顶部菜单激活路径
|
||||
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
|
||||
|
||||
// 常规路由(左侧菜单或顶部菜单)
|
||||
const routes = computed(() => permissionStore.routes);
|
||||
|
||||
// 混合布局左侧菜单路由
|
||||
const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus);
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route;
|
||||
|
||||
// 如果设置了activeMenu,则使用
|
||||
if (meta?.activeMenu) {
|
||||
return meta.activeMenu;
|
||||
}
|
||||
|
||||
return path;
|
||||
});
|
||||
|
||||
return {
|
||||
routes,
|
||||
sideMenuRoutes,
|
||||
activeMenu,
|
||||
activeTopMenuPath,
|
||||
};
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import NoticeAPI, {
|
||||
type NoticePageVO,
|
||||
type NoticeDetailVO,
|
||||
type NoticePageQuery,
|
||||
} from "@/api/system/notice";
|
||||
import { useStomp } from "@/composables/websocket/useStomp";
|
||||
import router from "@/router";
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 5;
|
||||
|
||||
const noticeList = ref<NoticePageVO[]>([]);
|
||||
const noticeDialogVisible = ref(false);
|
||||
const noticeDetail = ref<NoticeDetailVO | null>(null);
|
||||
|
||||
const { subscribe, unsubscribe, isConnected } = useStomp();
|
||||
let subscribed = false;
|
||||
|
||||
function normalizeQuery(params?: Partial<NoticePageQuery>): NoticePageQuery {
|
||||
return {
|
||||
pageNum: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
isRead: 0,
|
||||
...(params || {}),
|
||||
} as NoticePageQuery;
|
||||
}
|
||||
|
||||
async function fetchMyNotices(params?: Partial<NoticePageQuery>) {
|
||||
const query = normalizeQuery(params);
|
||||
const page = await NoticeAPI.getMyNoticePage(query);
|
||||
noticeList.value = page.list || [];
|
||||
}
|
||||
|
||||
async function readNotice(id: string) {
|
||||
const data = await NoticeAPI.getDetail(id);
|
||||
noticeDetail.value = data;
|
||||
noticeDialogVisible.value = true;
|
||||
|
||||
const index = noticeList.value.findIndex((item) => item.id === id);
|
||||
if (index >= 0) {
|
||||
noticeList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function markAllAsRead() {
|
||||
await NoticeAPI.readAll();
|
||||
noticeList.value = [];
|
||||
}
|
||||
|
||||
function viewMore() {
|
||||
router.push({ name: "MyNotice" });
|
||||
}
|
||||
|
||||
function setupStompSubscription() {
|
||||
if (subscribed || !isConnected.value) return;
|
||||
|
||||
subscribe("/user/queue/message", (message: any) => {
|
||||
try {
|
||||
const data = JSON.parse(message.body || "{}");
|
||||
const id = data.id;
|
||||
if (!id) return;
|
||||
|
||||
if (!noticeList.value.some((item) => item.id === id)) {
|
||||
noticeList.value.unshift({
|
||||
id,
|
||||
title: data.title,
|
||||
type: data.type,
|
||||
publishTime: data.publishTime,
|
||||
} as NoticePageVO);
|
||||
|
||||
ElNotification({
|
||||
title: "您收到一条新的通知消息!",
|
||||
message: data.title,
|
||||
type: "success",
|
||||
position: "bottom-right",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("解析通知消息失败", e);
|
||||
}
|
||||
});
|
||||
|
||||
subscribed = true;
|
||||
}
|
||||
|
||||
export function useNotificationCenter() {
|
||||
onMounted(() => {
|
||||
fetchMyNotices();
|
||||
setupStompSubscription();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe("/user/queue/message");
|
||||
subscribed = false;
|
||||
});
|
||||
|
||||
return {
|
||||
noticeList,
|
||||
noticeDialogVisible,
|
||||
noticeDetail,
|
||||
fetchMyNotices,
|
||||
readNotice,
|
||||
markAllAsRead,
|
||||
viewMore,
|
||||
};
|
||||
}
|
||||
61
src/composables/websocket/index.ts
Normal file
61
src/composables/websocket/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* WebSocket 服务统一管理
|
||||
*
|
||||
* @description
|
||||
* 提供 WebSocket 服务的统一初始化和清理接口
|
||||
* - 字典同步服务
|
||||
* - 在线用户统计服务
|
||||
*
|
||||
* @author 有来技术团队
|
||||
*/
|
||||
|
||||
import { useDictSync } from "./useDictSync";
|
||||
import { useOnlineCount } from "./useOnlineCount";
|
||||
|
||||
/**
|
||||
* 初始化所有 WebSocket 服务
|
||||
*
|
||||
* 应在应用启动时调用,统一初始化所有 WebSocket 连接
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 在 main.ts 中调用
|
||||
* setupWebSocket();
|
||||
* ```
|
||||
*/
|
||||
export function setupWebSocket() {
|
||||
// 初始化字典同步服务
|
||||
const dictSync = useDictSync();
|
||||
dictSync.initialize();
|
||||
|
||||
// 初始化在线用户统计服务
|
||||
const onlineCount = useOnlineCount();
|
||||
onlineCount.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有 WebSocket 连接
|
||||
*
|
||||
* 应在用户登出时调用,释放所有 WebSocket 资源
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 在 user store 的 logout 方法中调用
|
||||
* cleanupWebSocket();
|
||||
* ```
|
||||
*/
|
||||
export function cleanupWebSocket() {
|
||||
// 清理字典同步服务
|
||||
const dictSync = useDictSync();
|
||||
dictSync.cleanup();
|
||||
|
||||
// 清理在线用户统计服务
|
||||
const onlineCount = useOnlineCount();
|
||||
onlineCount.cleanup();
|
||||
}
|
||||
|
||||
// 导出所有 WebSocket 相关的 composables
|
||||
export { useDictSync } from "./useDictSync";
|
||||
export { useOnlineCount } from "./useOnlineCount";
|
||||
export { useStomp } from "./useStomp";
|
||||
export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./useDictSync";
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDictStoreHook } from "@/store/modules/dict-store";
|
||||
import { useDictStoreHook } from "@/store/modules/dict";
|
||||
import { useStomp } from "./useStomp";
|
||||
import type { IMessage } from "@stomp/stompjs";
|
||||
|
||||
@@ -158,14 +158,6 @@ function createDictSyncComposable() {
|
||||
initialize,
|
||||
cleanup,
|
||||
onDictChange,
|
||||
|
||||
// 别名方法(向后兼容)
|
||||
initWebSocket: initialize,
|
||||
closeWebSocket: cleanup,
|
||||
onDictMessage: onDictChange,
|
||||
|
||||
// 用于测试和调试
|
||||
handleDictChangeMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,7 +170,7 @@ function createDictSyncComposable() {
|
||||
* ```ts
|
||||
* const dictSync = useDictSync();
|
||||
*
|
||||
* // 初始化(在应用启动时调用)
|
||||
* // 初始化(通常在应用启动时调用)
|
||||
* dictSync.initialize();
|
||||
*
|
||||
* // 注册回调
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ref, onMounted, onUnmounted, getCurrentInstance } from "vue";
|
||||
import { useStomp } from "./useStomp";
|
||||
import { registerWebSocketInstance } from "@/utils/websocket";
|
||||
import { AuthStorage } from "@/utils/auth";
|
||||
|
||||
/**
|
||||
@@ -40,9 +39,6 @@ function createOnlineCountComposable() {
|
||||
// 订阅 ID
|
||||
let subscriptionId: string | null = null;
|
||||
|
||||
// 注册到全局实例管理器
|
||||
registerWebSocketInstance("onlineCount", stomp);
|
||||
|
||||
/**
|
||||
* 处理在线用户数量消息
|
||||
*/
|
||||
@@ -135,10 +131,6 @@ function createOnlineCountComposable() {
|
||||
// 方法
|
||||
initialize,
|
||||
cleanup,
|
||||
|
||||
// 别名方法(向后兼容)
|
||||
initWebSocket: initialize,
|
||||
closeWebSocket: cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,15 +139,12 @@ function createOnlineCountComposable() {
|
||||
*
|
||||
* 用于实时显示系统在线用户数量
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @param options.autoInit 是否在组件挂载时自动初始化(默认 true)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 在组件中使用
|
||||
* // 在组件中使用(推荐)
|
||||
* const { onlineUserCount, isConnected } = useOnlineCount();
|
||||
*
|
||||
* // 手动控制初始化
|
||||
* // 手动控制初始化(高级用法)
|
||||
* const { onlineUserCount, initialize, cleanup } = useOnlineCount({ autoInit: false });
|
||||
* onMounted(() => initialize());
|
||||
* onUnmounted(() => cleanup());
|
||||
@@ -169,18 +158,20 @@ export function useOnlineCount(options: { autoInit?: boolean } = {}) {
|
||||
globalInstance = createOnlineCountComposable();
|
||||
}
|
||||
|
||||
// 只在组件上下文中且 autoInit 为 true 时使用生命周期钩子
|
||||
// 组件级自动初始化(仅在组件上下文中生效)
|
||||
const instance = getCurrentInstance();
|
||||
if (autoInit && instance) {
|
||||
onMounted(() => {
|
||||
// 只有在未连接时才尝试初始化
|
||||
// 防止重复初始化:只有在未连接时才尝试初始化
|
||||
if (!globalInstance!.isConnected.value) {
|
||||
globalInstance!.initialize();
|
||||
}
|
||||
});
|
||||
|
||||
// 注意:不在卸载时关闭连接,保持全局连接
|
||||
onUnmounted(() => {});
|
||||
// 注意:组件卸载时不关闭连接,保持全局连接
|
||||
onUnmounted(() => {
|
||||
// 全局连接由 cleanupWebSocket() 统一管理
|
||||
});
|
||||
}
|
||||
|
||||
return globalInstance;
|
||||
|
||||
@@ -117,19 +117,9 @@ export function useStomp(options: UseStompOptions = {}) {
|
||||
/**
|
||||
* 日志输出(支持调试模式控制)
|
||||
*/
|
||||
const log = (...args: any[]) => {
|
||||
if (config.debug) {
|
||||
console.log("[useStomp]", ...args);
|
||||
}
|
||||
};
|
||||
|
||||
const logWarn = (...args: any[]) => {
|
||||
console.warn("[useStomp]", ...args);
|
||||
};
|
||||
|
||||
const logError = (...args: any[]) => {
|
||||
console.error("[useStomp]", ...args);
|
||||
};
|
||||
const log = config.debug ? (...args: any[]) => console.log("[useStomp]", ...args) : () => {};
|
||||
const logWarn = (...args: any[]) => console.warn("[useStomp]", ...args);
|
||||
const logError = (...args: any[]) => console.error("[useStomp]", ...args);
|
||||
|
||||
/**
|
||||
* 恢复所有订阅
|
||||
|
||||
Reference in New Issue
Block a user