diff --git a/.env.development b/.env.development index d96f96c..7f94868 100644 --- a/.env.development +++ b/.env.development @@ -28,5 +28,8 @@ VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3C # 客户端id VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' -# websocket 开关 -VITE_APP_WEBSOCKET = true +# websocket 开关 默认使用sse推送 +VITE_APP_WEBSOCKET = false + +# sse 开关 +VITE_APP_SSE = true diff --git a/.env.production b/.env.production index e47ac42..4cba452 100644 --- a/.env.production +++ b/.env.production @@ -31,5 +31,8 @@ VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3C # 客户端id VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' -# websocket 开关 -VITE_APP_WEBSOCKET = true +# websocket 开关 默认使用sse推送 +VITE_APP_WEBSOCKET = false + +# sse 开关 +VITE_APP_SSE = true diff --git a/package.json b/package.json index f1c8084..e67d60f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ruoyi-vue-plus", - "version": "5.2.1", + "version": "5.2.3", "description": "RuoYi-Vue-Plus多租户管理系统", "author": "LionLi", "license": "MIT", @@ -30,7 +30,7 @@ "diagram-js": "12.3.0", "didi": "9.0.2", "echarts": "5.5.0", - "element-plus": "2.7.5", + "element-plus": "2.7.8", "file-saver": "2.0.5", "fuse.js": "7.0.0", "highlight.js": "11.9.0", @@ -40,7 +40,7 @@ "nprogress": "0.2.0", "pinia": "2.1.7", "screenfull": "6.0.2", - "vue": "3.4.25", + "vue": "3.4.34", "vue-cropper": "1.1.1", "vue-i18n": "9.10.2", "vue-router": "4.3.2", @@ -81,7 +81,7 @@ "unplugin-icons": "0.18.5", "unplugin-vue-components": "0.26.0", "unplugin-vue-setup-extend-plus": "1.0.1", - "vite": "5.2.10", + "vite": "5.2.12", "vite-plugin-compression": "0.5.1", "vite-plugin-svg-icons": "2.0.1", "vitest": "1.5.0", diff --git a/src/api/login.ts b/src/api/login.ts index b6955de..c7c291e 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -51,6 +51,10 @@ export function register(data: any) { * 注销 */ export function logout() { + request({ + url: '/resource/sse/close', + method: 'get' + }); return request({ url: '/auth/logout', method: 'post' diff --git a/src/api/system/tenant/index.ts b/src/api/system/tenant/index.ts index 4380dbe..7b7b93f 100644 --- a/src/api/system/tenant/index.ts +++ b/src/api/system/tenant/index.ts @@ -91,3 +91,11 @@ export function syncTenantPackage(tenantId: string | number, packageId: string | params: data }); } + +// 同步租户字典 +export function syncTenantDict() { + return request({ + url: '/system/tenant/syncTenantDict', + method: 'get', + }); +} diff --git a/src/api/workflow/nodeConfig/index.ts b/src/api/workflow/nodeConfig/index.ts deleted file mode 100644 index 3270c17..0000000 --- a/src/api/workflow/nodeConfig/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import request from '@/utils/request'; -import { AxiosPromise } from 'axios'; -import { NodeConfigVO, NodeConfigForm, NodeConfigQuery } from '@/api/workflow/nodeConfig/types'; - -/** - * 查询节点配置列表 - * @param query - * @returns {*} - */ - -export const listNodeConfig = (query?: NodeConfigQuery): AxiosPromise => { - return request({ - url: '/workflow/nodeConfig/list', - method: 'get', - params: query - }); -}; - -/** - * 查询节点配置详细 - * @param id - */ -export const getNodeConfig = (id: string | number): AxiosPromise => { - return request({ - url: '/workflow/nodeConfig/' + id, - method: 'get' - }); -}; - -/** - * 新增节点配置 - * @param data - */ -export const addNodeConfig = (data: NodeConfigForm) => { - return request({ - url: '/workflow/nodeConfig', - method: 'post', - data: data - }); -}; - -/** - * 修改节点配置 - * @param data - */ -export const updateNodeConfig = (data: NodeConfigForm) => { - return request({ - url: '/workflow/nodeConfig', - method: 'put', - data: data - }); -}; - -/** - * 删除节点配置 - * @param id - */ -export const delNodeConfig = (id: string | number | Array) => { - return request({ - url: '/workflow/nodeConfig/' + id, - method: 'delete' - }); -}; diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index 2c753ee..9bafdb3 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -132,6 +132,7 @@ const dynamicTenantEvent = async (tenantId: string) => { dynamic.value = true; proxy?.$tab.closeAllPage(); proxy?.$router.push('/'); + proxy?.$tab.refreshPage(); } }; @@ -140,6 +141,7 @@ const dynamicClearEvent = async () => { dynamic.value = false; proxy?.$tab.closeAllPage(); proxy?.$router.push('/'); + proxy?.$tab.refreshPage(); }; /** 租户列表 */ diff --git a/src/layout/index.vue b/src/layout/index.vue index 29fb5ff..ce47a30 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -27,6 +27,7 @@ import { AppMain, Navbar, Settings, TagsView } from './components'; import useAppStore from '@/store/modules/app'; import useSettingsStore from '@/store/modules/settings'; import { initWebSocket } from '@/utils/websocket'; +import { initSSE } from "@/utils/sse"; const settingsStore = useSettingsStore(); const theme = computed(() => settingsStore.theme); @@ -71,6 +72,10 @@ onMounted(() => { initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + '/resource/websocket'); }); +onMounted(() => { + initSSE(import.meta.env.VITE_APP_BASE_API + '/resource/sse'); +}); + const handleClickOutside = () => { useAppStore().closeSideBar({ withoutAnimation: false }); }; diff --git a/src/permission.ts b/src/permission.ts index 6771f8c..125438b 100644 --- a/src/permission.ts +++ b/src/permission.ts @@ -40,6 +40,7 @@ router.beforeEach(async (to, from, next) => { router.addRoute(route); // 动态添加可访问路由表 } }); + // @ts-ignore next({ path: to.path, replace: true, params: to.params, query: to.query, hash: to.hash, name: to.name as string }); // hack方法 确保addRoutes已完成 } } else { diff --git a/src/plugins/tab.ts b/src/plugins/tab.ts index dd240cd..86421a8 100644 --- a/src/plugins/tab.ts +++ b/src/plugins/tab.ts @@ -1,5 +1,5 @@ import router from '@/router'; -import { RouteLocationMatched, RouteLocationNormalized } from 'vue-router'; +import {RouteLocationMatched, RouteLocationNormalized, RouteLocationRaw} from 'vue-router'; import useTagsViewStore from '@/store/modules/tagsView'; export default { @@ -41,7 +41,7 @@ export default { }); }, // 关闭当前tab页签,打开新页签 - closeOpenPage(obj: RouteLocationNormalized): void { + closeOpenPage(obj: RouteLocationRaw): void { useTagsViewStore().delView(router.currentRoute.value); if (obj !== undefined) { router.push(obj); diff --git a/src/router/index.ts b/src/router/index.ts index 438708f..86e0092 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -159,7 +159,7 @@ export const dynamicRoutes: RouteRecordRaw[] = [ path: 'index/:tableId(\\d+)', component: () => import('@/views/tool/gen/editTable.vue'), name: 'GenEdit', - meta: { title: '修改生成配置', activeMenu: '/tool/gen', icon: '' } + meta: { title: '修改生成配置', activeMenu: '/tool/gen', icon: '', noCache: true } } ] }, diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 2e719ba..e90df4c 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -21,13 +21,13 @@ export const usePermissionStore = defineStore('permission', () => { const sidebarRouters = ref([]); const getRoutes = (): RouteRecordRaw[] => { - return routes.value; + return routes.value as RouteRecordRaw[]; }; const getSidebarRoutes = (): RouteRecordRaw[] => { - return sidebarRouters.value; + return sidebarRouters.value as RouteRecordRaw[]; }; const getTopbarRoutes = (): RouteRecordRaw[] => { - return topbarRouters.value; + return topbarRouters.value as RouteRecordRaw[]; }; const setRoutes = (newRoutes: RouteRecordRaw[]): void => { diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index b9502eb..9756ac2 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -6,10 +6,10 @@ export const useTagsViewStore = defineStore('tagsView', () => { const iframeViews = ref([]); const getVisitedViews = (): RouteLocationNormalized[] => { - return visitedViews.value; + return visitedViews.value as RouteLocationNormalized[]; }; const getIframeViews = (): RouteLocationNormalized[] => { - return iframeViews.value; + return iframeViews.value as RouteLocationNormalized[]; }; const getCachedViews = (): string[] => { return cachedViews.value; @@ -31,7 +31,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { const delIframeView = (view: RouteLocationNormalized): Promise => { return new Promise((resolve) => { iframeViews.value = iframeViews.value.filter((item: RouteLocationNormalized) => item.path !== view.path); - resolve([...iframeViews.value]); + resolve([...iframeViews.value as RouteLocationNormalized[]]); }); }; const addVisitedView = (view: RouteLocationNormalized): void => { @@ -54,7 +54,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { delCachedView(view); } resolve({ - visitedViews: [...visitedViews.value], + visitedViews: [...visitedViews.value as RouteLocationNormalized[]], cachedViews: [...cachedViews.value] }); }); @@ -68,7 +68,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { break; } } - resolve([...visitedViews.value]); + resolve([...visitedViews.value as RouteLocationNormalized[]]); }); }; const delCachedView = (view?: RouteLocationNormalized): Promise => { @@ -92,7 +92,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { delOthersVisitedViews(view); delOthersCachedViews(view); resolve({ - visitedViews: [...visitedViews.value], + visitedViews: [...visitedViews.value as RouteLocationNormalized[]], cachedViews: [...cachedViews.value] }); }); @@ -103,7 +103,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { visitedViews.value = visitedViews.value.filter((v: RouteLocationNormalized) => { return v.meta?.affix || v.path === view.path; }); - resolve([...visitedViews.value]); + resolve([...visitedViews.value as RouteLocationNormalized[]]); }); }; const delOthersCachedViews = (view: RouteLocationNormalized): Promise => { @@ -124,7 +124,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { delAllVisitedViews(); delAllCachedViews(); resolve({ - visitedViews: [...visitedViews.value], + visitedViews: [...visitedViews.value as RouteLocationNormalized[]], cachedViews: [...cachedViews.value] }); }); @@ -132,7 +132,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { const delAllVisitedViews = (): Promise => { return new Promise((resolve) => { visitedViews.value = visitedViews.value.filter((tag: RouteLocationNormalized) => tag.meta?.affix); - resolve([...visitedViews.value]); + resolve([...visitedViews.value as RouteLocationNormalized[]]); }); }; @@ -167,7 +167,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { } return false; }); - resolve([...visitedViews.value]); + resolve([...visitedViews.value as RouteLocationNormalized[]]); }); }; const delLeftTags = (view: RouteLocationNormalized): Promise => { @@ -186,7 +186,7 @@ export const useTagsViewStore = defineStore('tagsView', () => { } return false; }); - resolve([...visitedViews.value]); + resolve([...visitedViews.value as RouteLocationNormalized[]]); }); }; diff --git a/src/types/env.d.ts b/src/types/env.d.ts index 777c858..1fb9f62 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -19,6 +19,7 @@ interface ImportMetaEnv { VITE_APP_RSA_PRIVATE_KEY: string; VITE_APP_CLIENT_ID: string; VITE_APP_WEBSOCKET: string; + VITE_APP_SSE: string; } interface ImportMeta { readonly env: ImportMetaEnv; diff --git a/src/utils/request.ts b/src/utils/request.ts index f2a69f0..f8f1f53 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -10,6 +10,7 @@ import FileSaver from 'file-saver'; import { getLanguage } from '@/lang'; import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto'; import { encrypt, decrypt } from '@/utils/jsencrypt'; +import router from "@/router"; const encryptHeader = 'encrypt-key'; let downloadLoadingInstance: LoadingInstance; @@ -134,8 +135,13 @@ service.interceptors.response.use( }).then(() => { isRelogin.show = false; useUserStore().logout().then(() => { - location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index'; - }); + router.replace({ + path: '/login', + query: { + redirect: encodeURIComponent(router.currentRoute.value.fullPath || '/') + } + }) + }); }).catch(() => { isRelogin.show = false; }); diff --git a/src/utils/sse.ts b/src/utils/sse.ts new file mode 100644 index 0000000..9174f0d --- /dev/null +++ b/src/utils/sse.ts @@ -0,0 +1,45 @@ +import { getToken } from '@/utils/auth'; +import { ElNotification } from 'element-plus'; +import useNoticeStore from '@/store/modules/notice'; + +// 初始化 +export const initSSE = (url: any) => { + if (import.meta.env.VITE_APP_SSE === 'false') { + return; + } + + url = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID + const { + data, + error + } = useEventSource(url, [], { + autoReconnect: { + retries: 10, + delay: 3000, + onFailed() { + console.log('Failed to connect after 10 retries'); + } + } + }); + + watch(error, () => { + console.log('SSE connection error:', error.value); + error.value = null; + }); + + watch(data, () => { + if (!data.value) return; + useNoticeStore().addNotice({ + message: data.value, + read: false, + time: new Date().toLocaleString() + }); + ElNotification({ + title: '消息', + message: data.value, + type: 'success', + duration: 3000 + }); + data.value = null; + }); +}; diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts index d4dd8a8..ade13ef 100644 --- a/src/utils/websocket.ts +++ b/src/utils/websocket.ts @@ -1,139 +1,51 @@ -/** - * @module initWebSocket 初始化 - * @module websocketonopen 连接成功 - * @module websocketonerror 连接失败 - * @module websocketclose 断开连接 - * @module resetHeart 重置心跳 - * @module sendSocketHeart 心跳发送 - * @module reconnect 重连 - * @module sendMsg 发送数据 - * @module websocketonmessage 接收数据 - * @module test 测试收到消息传递 - * @description socket 通信 - * @param {any} url socket地址 - * @param {any} websocket websocket 实例 - * @param {any} heartTime 心跳定时器实例 - * @param {number} socketHeart 心跳次数 - * @param {number} HeartTimeOut 心跳超时时间 - * @param {number} socketError 错误次数 - */ - import { getToken } from '@/utils/auth'; import { ElNotification } from 'element-plus'; import useNoticeStore from '@/store/modules/notice'; -let socketUrl: any = ''; // socket地址 -let websocket: any = null; // websocket 实例 -let heartTime: any = null; // 心跳定时器实例 -let socketHeart = 0 as number; // 心跳次数 -const HeartTimeOut = 10000; // 心跳超时时间 10000 = 10s -let socketError = 0 as number; // 错误次数 - // 初始化socket export const initWebSocket = (url: any) => { if (import.meta.env.VITE_APP_WEBSOCKET === 'false') { return; } - socketUrl = url; - // 初始化 websocket - websocket = new WebSocket(url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID); - websocketonopen(); - websocketonmessage(); - websocketonerror(); - websocketclose(); - sendSocketHeart(); - return websocket; -}; - -// socket 连接成功 -export const websocketonopen = () => { - websocket.onopen = function () { - console.log('连接 websocket 成功'); - resetHeart(); - }; -}; - -// socket 连接失败 -export const websocketonerror = () => { - websocket.onerror = function (e: any) { - console.log('连接 websocket 失败', e); - }; -}; - -// socket 断开链接 -export const websocketclose = () => { - websocket.onclose = function (e: any) { - console.log('断开连接', e); - }; -}; - -// socket 重置心跳 -export const resetHeart = () => { - socketHeart = 0; - socketError = 0; - clearInterval(heartTime); - sendSocketHeart(); -}; - -// socket心跳发送 -export const sendSocketHeart = () => { - heartTime = setInterval(() => { - // 如果连接正常则发送心跳 - if (websocket.readyState == 1) { - // if (socketHeart <= 30) { - websocket.send( - JSON.stringify({ - type: 'ping' - }) - ); - socketHeart = socketHeart + 1; - } else { - // 重连 - reconnect(); + url = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID + useWebSocket(url, { + autoReconnect: { + // 重连最大次数 + retries: 3, + // 重连间隔 + delay: 1000, + onFailed() { + console.log('websocket重连失败'); + }, + }, + heartbeat: { + message: JSON.stringify({type: 'ping'}), + // 发送心跳的间隔 + interval: 10000, + // 接收到心跳response的超时时间 + pongTimeout: 2000, + }, + onConnected() { + console.log('websocket已经连接'); + }, + onDisconnected() { + console.log('websocket已经断开'); + }, + onMessage: (_, e) => { + if (e.data.indexOf('ping') > 0) { + return; + } + useNoticeStore().addNotice({ + message: e.data, + read: false, + time: new Date().toLocaleString() + }); + ElNotification({ + title: '消息', + message: e.data, + type: 'success', + duration: 3000 + }); } - }, HeartTimeOut); -}; - -// socket重连 -export const reconnect = () => { - if (socketError <= 2) { - clearInterval(heartTime); - initWebSocket(socketUrl); - socketError = socketError + 1; - // eslint-disable-next-line prettier/prettier - console.log('socket重连', socketError); - } else { - // eslint-disable-next-line prettier/prettier - console.log('重试次数已用完'); - clearInterval(heartTime); - } -}; - -// socket 发送数据 -export const sendMsg = (data: any) => { - websocket.send(data); -}; - -// socket 接收数据 -export const websocketonmessage = () => { - websocket.onmessage = function (e: any) { - if (e.data.indexOf('heartbeat') > 0) { - resetHeart(); - } - if (e.data.indexOf('ping') > 0) { - return; - } - useNoticeStore().addNotice({ - message: e.data, - read: false, - time: new Date().toLocaleString() - }); - ElNotification({ - title: '消息', - message: e.data, - type: 'success', - duration: 3000 - }); - return e.data; - }; + }); }; diff --git a/src/views/monitor/logininfor/index.vue b/src/views/monitor/logininfor/index.vue index 03d0d8f..4cc890d 100644 --- a/src/views/monitor/logininfor/index.vue +++ b/src/views/monitor/logininfor/index.vue @@ -198,7 +198,7 @@ const handleExport = () => { { ...queryParams.value }, - `config_${new Date().getTime()}.xlsx` + `logininfor_${new Date().getTime()}.xlsx` ); }; diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue index d44a04a..4ad70d0 100644 --- a/src/views/system/dict/index.vue +++ b/src/views/system/dict/index.vue @@ -49,6 +49,9 @@ 刷新缓存 + + 同步租户字典 + @@ -109,11 +112,15 @@