diff --git a/.env.development b/.env.development index 6d2f8ddf0c2d3e834f62b98f7d50402ad3ef0db8..b2b28c8bd387ff29aa834b755158f89a3f3f8ec7 100644 --- a/.env.development +++ b/.env.development @@ -23,3 +23,6 @@ VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbf # 客户端id VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' + +# websocket 开关 +VITE_APP_WEBSOCKET = true diff --git a/.env.production b/.env.production index d723d2ad314b3d3bc1495a4a29b51968e72c7d81..c4635427e445766807dda9d4dd79406d1e7dc949 100644 --- a/.env.production +++ b/.env.production @@ -26,3 +26,6 @@ VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbf # 客户端id VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' + +# websocket 开关 +VITE_APP_WEBSOCKET = true diff --git a/package.json b/package.json index 9b26e96dad69dbc49c209b8987a54449bec20612..13e0f926115ee9fdd440066bf41bac341e3a6e1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ruoyi-vue-plus", - "version": "5.1.0", + "version": "5.1.1", "description": "RuoYi-Vue-Plus多租户管理系统", "author": "LionLi", "license": "MIT", diff --git a/src/api/login.ts b/src/api/login.ts index 7419fb33c9f5f1d4dcaa3ebc10ca3911e3d9442a..100a5e9f7df7262ad518c11b85805544f17d81e6 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -29,6 +29,11 @@ export function login(data: LoginData): AxiosPromise { // 注册方法 export function register(data: any) { + const params = { + ...data, + clientId: clientId, + grantType: 'password' + }; return request({ url: '/auth/register', headers: { @@ -36,7 +41,7 @@ export function register(data: any) { isEncrypt: true }, method: 'post', - data: data + data: params }); } diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue index 85c6ca039a2a93e2af5c5e4ca1e4c1b046bda59d..b922c6c4062009d7f267175377e65bfb54bc9588 100644 --- a/src/components/HeaderSearch/index.vue +++ b/src/components/HeaderSearch/index.vue @@ -88,7 +88,7 @@ const initFuse = (list: Router) => { } // Filter out the routes that can be displayed in the sidebar // And generate the internationalized title -const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = [], query: any = {}) => { +const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => { let res: Router = [] routes.forEach(r => { // skip hidden router @@ -114,7 +114,7 @@ const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: strin // recursive child routes if (r.children) { - const tempRoutes = generateRoutes(r.children, data.path, data.title, data.query); + const tempRoutes = generateRoutes(r.children, data.path, data.title); if (tempRoutes.length >= 1) { res = [...res, ...tempRoutes]; } diff --git a/src/components/RightToolbar/index.vue b/src/components/RightToolbar/index.vue index 6f7b14fecbc008d93415f1c8b49361066254fdd8..6be382f7a2b21cb40f904d1f9822c53b4112df86 100644 --- a/src/components/RightToolbar/index.vue +++ b/src/components/RightToolbar/index.vue @@ -8,7 +8,7 @@ -
+
显示/隐藏列
{ line-height: 24px; text-align: center; } +.show-btn { + margin-left: 12px; +} diff --git a/src/lang/en_US.ts b/src/lang/en_US.ts index 59df4baf6a1ef48a12c722e0a9962b329ac107a7..034ea910fb8f3729e774f4c699e1ea93eea6c22c 100644 --- a/src/lang/en_US.ts +++ b/src/lang/en_US.ts @@ -18,6 +18,7 @@ export default { language: 'Language', dashboard: 'Dashboard', document: 'Document', + message: 'Message', layoutSize: 'Layout Size', selectTenant: 'Select Tenant', layoutSetting: 'Layout Setting', diff --git a/src/lang/zh_CN.ts b/src/lang/zh_CN.ts index d778f7d95109e39ba5c6b081930ad887d3dc7115..666a400851e46f991ab52a2fde24acc000ed6a38 100644 --- a/src/lang/zh_CN.ts +++ b/src/lang/zh_CN.ts @@ -17,6 +17,7 @@ export default { language: '语言', dashboard: '首页', document: '项目文档', + message: '消息', layoutSize: '布局大小', selectTenant: '选择租户', layoutSetting: '布局设置', diff --git a/src/layout/components/IframeToggle/index.vue b/src/layout/components/IframeToggle/index.vue index 9ffae005e9e0fec0723649d03110ecd1e95a7156..efb2b7a6dcd87fb4bb472e1db01e2b15e575c2d1 100644 --- a/src/layout/components/IframeToggle/index.vue +++ b/src/layout/components/IframeToggle/index.vue @@ -5,7 +5,7 @@ :key="item.path" :iframeId="'iframe' + index" v-show="route.path === item.path" - :src="item.meta ? item.meta.link : ''" + :src="iframeUrl(item.meta ? item.meta.link : '', item.query)" > @@ -15,5 +15,13 @@ import InnerLink from "../InnerLink/index.vue"; import useTagsViewStore from '@/store/modules/tagsView'; const route = useRoute(); -const tagsViewStore = useTagsViewStore() +const tagsViewStore = useTagsViewStore(); + +function iframeUrl(url: string, query: any) { + if (Object.keys(query).length > 0) { + let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&"); + return url + "?" + params; + } + return url; +} diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index 3c0e45dfceab74425ab39a8e489c562c7c386af2..7818fd46d3cddbe3af33fd642623d01d41e27e7b 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -27,6 +27,21 @@
+ + +
+ + + + +
+
@@ -81,10 +96,14 @@ import { getTenantList } from "@/api/login"; import { dynamicClear, dynamicTenant } from "@/api/system/tenant"; import { ComponentInternalInstance } from "vue"; import { TenantVO } from "@/api/types"; +import notice from './notice/index.vue'; +import useNoticeStore from '@/store/modules/notice'; const appStore = useAppStore(); const userStore = useUserStore(); const settingsStore = useSettingsStore(); +const noticeStore = storeToRefs(useNoticeStore()); +const newNotice = ref(0); const { proxy } = getCurrentInstance() as ComponentInternalInstance; @@ -161,6 +180,11 @@ const handleCommand = (command: string) => { commandMap[command](); } } + +//用深度监听 消息 +watch(() => noticeStore.state.value.notices, (newVal, oldVal) => { + newNotice.value = newVal.filter((item: any) => !item.read).length; +}, { deep: true }); diff --git a/src/store/modules/notice.ts b/src/store/modules/notice.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3f8e5ad718c1055b1782ef5c8d8b8248158b0e5 --- /dev/null +++ b/src/store/modules/notice.ts @@ -0,0 +1,42 @@ +import { defineStore } from 'pinia'; + +interface NoticeItem { + title?: string; + read: boolean; + message: any; + time: string; +} + +export const useNoticeStore = defineStore('notice', () => { + const state = reactive({ + notices: [] as NoticeItem[] + }); + + const addNotice = (notice: NoticeItem) => { + state.notices.push(notice); + }; + + const removeNotice = (notice: NoticeItem) => { + state.notices.splice(state.notices.indexOf(notice), 1); + }; + + //实现全部已读 + const readAll = () => { + state.notices.forEach((item) => { + item.read = true; + }); + }; + + const clearNotice = () => { + state.notices = []; + }; + return { + state, + addNotice, + removeNotice, + readAll, + clearNotice + }; +}); + +export default useNoticeStore; diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index dcb3cd4f59ea35ada8f74837417793a0689e2486..ed64e46eeb8779427c9adad985f9688198e7471a 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -100,6 +100,10 @@ export const usePermissionStore = defineStore('permission', () => { } if (lastRouter) { el.path = lastRouter.path + '/' + el.path; + if (el.children && el.children.length) { + children = children.concat(filterChildren(el.children, el)) + return + } } children = children.concat(el); }); diff --git a/src/types/env.d.ts b/src/types/env.d.ts index fabf641c2b1822209188c9a47f0ed8df2f8f37b2..8389dc58c9abbc4f6bdc5ea127f5521ea8109701 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -69,6 +69,7 @@ interface ImportMetaEnv { VITE_APP_ENV: string; VITE_APP_RSA_PUBLIC_KEY: string; VITE_APP_CLIENT_ID: string; + VITE_APP_WEBSOCKET: boolean; } interface ImportMeta { readonly env: ImportMetaEnv; diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ba02432b6c45b92ade20ad9fc87bda91f6674dc --- /dev/null +++ b/src/utils/websocket.ts @@ -0,0 +1,141 @@ +/** + * @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 useNoticeStore from '@/store/modules/notice'; +import { ElNotification } from "element-plus"; + +const { addNotice } = useNoticeStore(); + +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) { + 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(); + } + }, 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; + } + 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/index.vue b/src/views/index.vue index 438c1afcdd015d1666c90210baac586716209e69..7f658d8177dd257527942fba9b160c29a05941a3 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -33,7 +33,7 @@ * 部署方式 Docker 容器编排 一键部署业务集群
* 国际化 SpringMessage Spring标准国际化方案

-

当前版本: v5.1.0

+

当前版本: v5.1.1

¥免费开源

@@ -78,7 +78,7 @@ * 分布式监控 Prometheus、Grafana 全方位性能监控
* 其余与 Vue 版本一致

-

当前版本: v2.1.0

+

当前版本: v2.1.1

¥免费开源

@@ -96,6 +96,12 @@