.env.development
@@ -23,3 +23,6 @@ # 客æ·ç«¯id VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' # websocket å¼å ³ VITE_APP_WEBSOCKET = true .env.production
@@ -26,3 +26,6 @@ # 客æ·ç«¯id VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' # websocket å¼å ³ VITE_APP_WEBSOCKET = true 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", src/api/login.ts
@@ -29,6 +29,11 @@ // æ³¨åæ¹æ³ export function register(data: any) { const params = { ...data, clientId: clientId, grantType: 'password' }; return request({ url: '/auth/register', headers: { @@ -36,7 +41,7 @@ isEncrypt: true }, method: 'post', data: data data: params }); } src/components/RightToolbar/index.vue
@@ -8,7 +8,7 @@ <el-button circle icon="Refresh" @click="refresh()" /> </el-tooltip> <el-tooltip class="item" effect="dark" content="æ¾ç¤º/éèå" placement="top" v-if="columns"> <div> <div class="show-btn"> <el-popover placement="bottom" trigger="click"> <div class="tree-header">æ¾ç¤º/éèå</div> <el-tree @@ -98,4 +98,7 @@ line-height: 24px; text-align: center; } .show-btn { margin-left: 12px; } </style> src/lang/en_US.ts
@@ -18,6 +18,7 @@ language: 'Language', dashboard: 'Dashboard', document: 'Document', message: 'Message', layoutSize: 'Layout Size', selectTenant: 'Select Tenant', layoutSetting: 'Layout Setting', src/lang/zh_CN.ts
@@ -17,6 +17,7 @@ language: 'è¯è¨', dashboard: 'é¦é¡µ', document: 'é¡¹ç®ææ¡£', message: 'æ¶æ¯', layoutSize: 'å¸å±å¤§å°', selectTenant: 'éæ©ç§æ·', layoutSetting: 'å¸å±è®¾ç½®', 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)" ></inner-link> </transition-group> </template> @@ -15,5 +15,13 @@ 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; } </script> src/layout/components/Navbar.vue
@@ -27,6 +27,21 @@ <svg-icon class-name="search-icon" icon-class="search" /> </div> </el-tooltip> <!-- æ¶æ¯ --> <el-tooltip :content="$t('navbar.message')" effect="dark" placement="bottom"> <div> <el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false"> <template #reference> <el-badge :value="newNotice > 0 ? newNotice : ''" :max="99"> <svg-icon icon-class="message" /> </el-badge> </template> <template #default> <notice></notice> </template> </el-popover> </div> </el-tooltip> <el-tooltip content="Github" effect="dark" placement="bottom"> <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> </el-tooltip> @@ -81,10 +96,14 @@ 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(<number>0); const { proxy } = getCurrentInstance() as ComponentInternalInstance; @@ -161,6 +180,11 @@ commandMap[command](); } } //ç¨æ·±åº¦çå¬ æ¶æ¯ watch(() => noticeStore.state.value.notices, (newVal, oldVal) => { newNotice.value = newVal.filter((item: any) => !item.read).length; }, { deep: true }); </script> <style lang="scss" scoped> @@ -169,6 +193,10 @@ height:30px; } :deep(.el-badge__content.is-fixed){ top: 12px; } .flex { display: flex; } src/layout/components/notice/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,134 @@ <template> <div class="layout-navbars-breadcrumb-user-news" v-loading="state.loading"> <div class="head-box"> <div class="head-box-title">éç¥å ¬å</div> <div class="head-box-btn" @click="readAll">å ¨é¨å·²è¯»</div> </div> <div class="content-box" v-loading="state.loading"> <template v-if="newsList.length > 0"> <div class="content-box-item" v-for="(v, k) in newsList" :key="k" @click="onNewsClick(k)"> <div class="item-conten"> <div>{{ v.message }}</div> <div class="content-box-msg"></div> <div class="content-box-time">{{ v.time }}</div> </div> <!-- 已读/æªè¯» --> <span v-if="v.read" class="el-tag el-tag--success el-tag--mini read">已读</span> <span v-else class="el-tag el-tag--danger el-tag--mini read">æªè¯»</span> </div> </template> <el-empty :description="'æ¶æ¯ä¸ºç©º'" v-else></el-empty> </div> <div class="foot-box" @click="onGoToGiteeClick" v-if="newsList.length > 0">åå¾gitee</div> </div> </template> <script setup lang="ts" name="layoutBreadcrumbUserNews"> import { ref } from "vue"; import { storeToRefs } from 'pinia' import { nextTick, onMounted, reactive } from "vue"; import useNoticeStore from '@/store/modules/notice'; const noticeStore = storeToRefs(useNoticeStore()); const {readAll} = useNoticeStore(); // å®ä¹åéå 容 const state = reactive({ loading: false, }); const newsList =ref([]) as any; /** * åå§åæ°æ® * @returns */ const getTableData = async () => { state.loading = true; newsList.value = noticeStore.state.value.notices; state.loading = false; }; //ç¹å»æ¶æ¯ï¼åå ¥å·²è¯» const onNewsClick = (item: any) => { newsList.value[item].read = true; //å¹¶ä¸åå ¥pinia noticeStore.state.value.notices = newsList.value; }; // åå¾éç¥ä¸å¿ç¹å» const onGoToGiteeClick = () => { window.open("https://gitee.com/dromara/RuoYi-Vue-Plus/tree/5.X/"); }; onMounted(() => { nextTick(() => { getTableData(); }); }); </script> <style scoped lang="scss"> .layout-navbars-breadcrumb-user-news { .head-box { display: flex; border-bottom: 1px solid var(--el-border-color-lighter); box-sizing: border-box; color: var(--el-text-color-primary); justify-content: space-between; height: 35px; align-items: center; .head-box-btn { color: var(--el-color-primary); font-size: 13px; cursor: pointer; opacity: 0.8; &:hover { opacity: 1; } } } .content-box { height: 300px; overflow: auto; font-size: 13px; .content-box-item { padding-top: 12px; display: flex; &:last-of-type { padding-bottom: 12px; } .content-box-msg { color: var(--el-text-color-secondary); margin-top: 5px; margin-bottom: 5px; } .content-box-time { color: var(--el-text-color-secondary); } .item-conten { width: 100%; display: flex; flex-direction: column; } } } .foot-box { height: 35px; color: var(--el-color-primary); font-size: 13px; cursor: pointer; opacity: 0.8; display: flex; align-items: center; justify-content: center; border-top: 1px solid var(--el-border-color-lighter); &:hover { opacity: 1; } } :deep(.el-empty__description p) { font-size: 13px; } } </style> 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; src/store/modules/permission.ts
@@ -100,6 +100,10 @@ } 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); }); src/types/env.d.ts
@@ -69,6 +69,7 @@ VITE_APP_ENV: string; VITE_APP_RSA_PUBLIC_KEY: string; VITE_APP_CLIENT_ID: string; VITE_APP_WEBSOCKET: boolean; } interface ImportMeta { readonly env: ImportMetaEnv; 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; }; }; src/views/index.vue
@@ -33,7 +33,7 @@ * é¨ç½²æ¹å¼ Docker 容å¨ç¼æ ä¸é®é¨ç½²ä¸å¡é群<br /> * å½é å SpringMessage Springæ åå½é åæ¹æ¡<br /> </p> <p><b>å½åçæ¬:</b> <span>v5.1.0</span></p> <p><b>å½åçæ¬:</b> <span>v5.1.1</span></p> <p> <el-tag type="danger">¥å è´¹å¼æº</el-tag> </p> @@ -78,7 +78,7 @@ * åå¸å¼çæ§ PrometheusãGrafana å ¨æ¹ä½æ§è½çæ§<br /> * å ¶ä½ä¸ Vue çæ¬ä¸è´<br /> </p> <p><b>å½åçæ¬:</b> <span>v2.1.0</span></p> <p><b>å½åçæ¬:</b> <span>v2.1.1</span></p> <p> <el-tag type="danger">¥å è´¹å¼æº</el-tag> </p> @@ -96,6 +96,12 @@ </template> <script setup name="Index" lang="ts"> import { initWebSocket } from '@/utils/websocket'; onMounted(() => { let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://' initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + "/resource/websocket"); }); const goTarget = (url:string) => { window.open(url, '__blank') src/views/monitor/logininfor/index.vue
@@ -76,6 +76,12 @@ sortable="custom" :sort-orders="['descending', 'ascending']" /> <el-table-column label="客æ·ç«¯" align="center" prop="clientKey" :show-overflow-tooltip="true" /> <el-table-column label="设å¤ç±»å" align="center"> <template #default="scope"> <dict-tag :options="sys_device_type" :value="scope.row.deviceType" /> </template> </el-table-column> <el-table-column label="å°å" align="center" prop="ipaddr" :show-overflow-tooltip="true" /> <el-table-column label="ç»å½å°ç¹" align="center" prop="loginLocation" :show-overflow-tooltip="true" /> <el-table-column label="æä½ç³»ç»" align="center" prop="os" :show-overflow-tooltip="true" /> @@ -103,6 +109,7 @@ import { LoginInfoQuery, LoginInfoVO } from "@/api/monitor/loginInfo/types"; const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type")); const { sys_common_status } = toRefs<any>(proxy?.useDict("sys_common_status")); const loginInfoList = ref<LoginInfoVO[]>([]); src/views/monitor/online/index.vue
@@ -29,6 +29,12 @@ </el-table-column> <el-table-column label="ä¼è¯ç¼å·" align="center" prop="tokenId" :show-overflow-tooltip="true" /> <el-table-column label="ç»å½åç§°" align="center" prop="userName" :show-overflow-tooltip="true" /> <el-table-column label="客æ·ç«¯" align="center" prop="clientKey" :show-overflow-tooltip="true" /> <el-table-column label="设å¤ç±»å" align="center"> <template #default="scope"> <dict-tag :options="sys_device_type" :value="scope.row.deviceType" /> </template> </el-table-column> <el-table-column label="æå±é¨é¨" align="center" prop="deptName" :show-overflow-tooltip="true" /> <el-table-column label="主æº" align="center" prop="ipaddr" :show-overflow-tooltip="true" /> <el-table-column label="ç»å½å°ç¹" align="center" prop="loginLocation" :show-overflow-tooltip="true" /> @@ -59,6 +65,7 @@ import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types"; const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { sys_device_type } = toRefs<any>(proxy?.useDict("sys_device_type")); const onlineList = ref<OnlineVO[]>([]); const loading = ref(true); src/views/system/user/profile/thirdParty.vue
@@ -20,7 +20,7 @@ <div id="git-user-binding"> <h4 class="provider-desc">ä½ å¯ä»¥ç»å®ä»¥ä¸ç¬¬ä¸æ¹å¸å·</h4> <div id="authlist" class="user-bind"> <a class="third-app" href="#" @click="authUrl('wechar');" title="ä½¿ç¨ å¾®ä¿¡ è´¦å·ææç»å½"> <a class="third-app" href="#" @click="authUrl('wechat');" title="ä½¿ç¨ å¾®ä¿¡ è´¦å·ææç»å½"> <div class="git-other-login-icon"> <svg-icon icon-class="wechat" /> </div> vite.config.ts
@@ -28,6 +28,7 @@ [env.VITE_APP_BASE_API]: { target: 'http://localhost:8080', changeOrigin: true, ws: true, rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') } }