From 6f63834491f98f687787c09b213804782c60fc37 Mon Sep 17 00:00:00 2001 From: ray <1490493387@qq.com> Date: Fri, 13 Sep 2024 21:55:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8defineModel?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2useVModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pagination/index.vue | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue index c080e602..9cbaeafa 100644 --- a/src/components/Pagination/index.vue +++ b/src/components/Pagination/index.vue @@ -16,20 +16,12 @@ From b797acbb496fdd7e1a61e9e6f0aeb1978c26fa13 Mon Sep 17 00:00:00 2001 From: ray <1490493387@qq.com> Date: Fri, 27 Sep 2024 08:40:00 +0800 Subject: [PATCH 2/2] =?UTF-8?q?wip:=20:construction:=20=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=85=AC=E5=91=8A=E9=87=8D=E6=9E=84=E4=B8=B4=E6=97=B6=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/notice.ts | 117 ++++++++------ src/api/socket.ts | 113 ------------- src/components/Notice/index.vue | 149 +++++++++-------- src/components/NoticeModal/index.vue | 110 ------------- src/components/Upload/FileUpload.vue | 8 +- src/components/Upload/ImageUpload.vue | 4 +- src/enums/CacheEnum.ts | 4 - .../NavBar/components/NavbarAction.vue | 2 +- src/plugins/permission.ts | 7 +- src/store/modules/user.ts | 5 +- src/utils/auth.ts | 19 +++ src/utils/request.ts | 4 +- src/utils/socket.ts | 150 ++++++++++++++++++ src/views/demo/websocket.vue | 4 +- src/views/notice/index.vue | 3 - src/views/system/notice/index.vue | 6 +- vite.config.ts | 2 + 17 files changed, 340 insertions(+), 367 deletions(-) delete mode 100644 src/api/socket.ts delete mode 100644 src/components/NoticeModal/index.vue delete mode 100644 src/enums/CacheEnum.ts create mode 100644 src/utils/auth.ts create mode 100644 src/utils/socket.ts diff --git a/src/api/notice.ts b/src/api/notice.ts index efa94850..f52702e4 100644 --- a/src/api/notice.ts +++ b/src/api/notice.ts @@ -27,6 +27,7 @@ class NoticeAPI { /** * 添加通知公告 + * * @param data Notice表单数据 * @returns */ @@ -70,7 +71,7 @@ class NoticeAPI { * @param id 被发布的通知公告id * @returns */ - static releaseNotice(id: number) { + static publish(id: number) { return request({ url: `${NOTICE_BASE_URL}/release/${id}`, method: "patch", @@ -83,19 +84,18 @@ class NoticeAPI { * @param id 撤回的通知id * @returns */ - static recallNotice(id: number): Promise<[]> { + static revoke(id: number): Promise<[]> { return request({ - url: `${NOTICE_BASE_URL}/recall/${id}`, + url: `${NOTICE_BASE_URL}/${id}/revoke`, method: "patch", }); } /** * 获取未读消息 - * @returns 消息 */ - static listUnreadNotice() { - return request({ + static getUnreadList() { + return request({ url: `${NOTICE_BASE_URL}/unread`, method: "get", }); @@ -103,36 +103,25 @@ class NoticeAPI { /** * 查看通知 - * @param id - */ - static readNotice(id: number): Promise { - return request({ - url: `${NOTICE_BASE_URL}/read/${id}`, - method: "PATCH", - }); - } - - /** - * 查看通知详情 + * * @param id */ static getDetail(id: number): Promise { return request({ - url: `${NOTICE_BASE_URL}/detail/${id}`, - method: "get", - }); - } - - /** - * 全部已读 - */ - static readAllNotice() { - return request({ - url: `${NOTICE_BASE_URL}/readAll`, + url: `${NOTICE_BASE_URL}/${id}/detail`, method: "PATCH", }); } + /* 全部已读 */ + static readAll() { + return request({ + url: `${NOTICE_BASE_URL}/read-all`, + method: "PATCH", + }); + } + + /** 获取我的通知分页列表 */ static getMyNoticePage(queryParams?: NoticePageQuery) { return request>({ url: `${NOTICE_BASE_URL}/my/page`, @@ -148,8 +137,8 @@ export default NoticeAPI; export interface NoticePageQuery extends PageQuery { /** 标题 */ title?: string; - /** 发布状态(0-未发布 1已发布 2已撤回) */ - sendStatus?: number; + /** 发布状态(0:未发布,1:已发布,-1:已撤回) */ + publishStatus?: number; } /** 通知公告表单对象 */ @@ -160,13 +149,13 @@ export interface NoticeForm { /** 通知内容 */ content?: string; /** 通知类型 */ - noticeType?: number; - /** 优先级(0-低 1-中 2-高) */ - priority?: number; - /** 目标类型(0-全体 1-指定) */ - tarType?: number; + type?: number; + /** 优先级(L:低,M:中,H:高) */ + level?: number; + /** 目标类型(1-全体 2-指定) */ + targetType?: number; /** 目标ID合集,以,分割 */ - tarIds?: string; + targetUserIds?: string; } /** 通知公告分页对象 */ @@ -177,33 +166,67 @@ export interface NoticePageVO { /** 通知内容 */ content?: string; /** 通知类型 */ - noticeType?: number; + type?: number; /** 发布人 */ - releaseBy?: bigint; + publisherId?: bigint; /** 优先级(0-低 1-中 2-高) */ priority?: number; /** 目标类型(0-全体 1-指定) */ - tarType?: number; + targetType?: number; /** 发布状态(0-未发布 1已发布 2已撤回) */ - releaseStatus?: number; + publishStatus?: number; /** 发布时间 */ - releaseTime?: Date; + publishTime?: Date; /** 撤回时间 */ - recallTime?: Date; + revokeTime?: Date; } export interface NoticeDetailVO { + /** 通知ID */ id?: string; + /** 通知标题 */ title?: string; + /** 通知内容 */ content?: string; + /** 通知类型 */ - noticeType?: number; + type?: number; + /** 发布人 */ - releaseBy?: string; - /** 优先级(0-低 1-中 2-高) */ - priority?: number; + publisherName?: string; + + /** 优先级(L-低 M-中 H-高) */ + level?: string; + /** 发布时间 */ - releaseTime?: Date; + publishTime?: Date; + + /** 发布状态 */ + publishStatus?: number; +} + +/** 用户通知分页列表 */ +interface UserNoticePageVO { + /** 通知ID */ + id: number; + + /** 通知标题 */ + title: string; + + /** 通知类型 */ + typeLabel: string; + + /** 发布人姓名 */ + publisherName: string; + + /** 通知级别 */ + levelLabel: string; + + /** 发布时间 */ + publishTime: string; + + /** 是否已读 */ + isReadLabel: string; } diff --git a/src/api/socket.ts b/src/api/socket.ts deleted file mode 100644 index 37038076..00000000 --- a/src/api/socket.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Client } from "@stomp/stompjs"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; - -const MAX_RETRIES = 3; // 最大重试次数 -const RETRY_DELAY_MS = 5000; // 重试延迟时间,单位:毫秒 -const HEARTBEAT_INTERVAL = 30000; // 心跳间隔时间,单位:毫秒 - -class Socket { - private clients: Map = new Map(); - private retryCountMap: Map = new Map(); - private subscriptions: Map void)[]> = new Map(); - constructor() {} - - public getWebSocketClient( - url: string, - onMessage: (message: string) => void, - onError?: (error: any) => void - ): Client { - if (this.clients.has(url)) { - // 如果连接已存在,添加新的订阅回调 - this.subscriptions.get(url)?.push(onMessage); - return this.clients.get(url)!; - } else { - const client = this.createClient(url, onMessage, onError); - this.clients.set(url, client); - this.subscriptions.set(url, [onMessage]); - return client; - } - } - - /** - * 创建 WebSocket 客户端 - * @param url WebSocket订阅地址 - * @param onMessage 收到消息时的回调 - * @param onError 出现错误时的回调 - * @private - */ - private createClient( - url: string, - onMessage: (message: string) => void, - onError?: (error: any) => void - ): Client { - const token = localStorage.getItem(TOKEN_KEY) || ""; - const client = new Client({ - brokerURL: import.meta.env.VITE_APP_WS_ENDPOINT, - connectHeaders: { - Authorization: token, - }, - heartbeatIncoming: HEARTBEAT_INTERVAL, - heartbeatOutgoing: HEARTBEAT_INTERVAL, - onConnect: () => { - console.log(`Connected to ${url}`); - client.subscribe(url, (message) => { - onMessage(message.body); - }); - }, - onStompError: (frame) => { - console.error(`Error on ${url}: ${frame.headers["message"]}`); - console.error(`Details: ${frame.body}`); - if (onError) { - onError(frame); - } - this.handleReconnect(url); - }, - onDisconnect: () => { - console.log(`连接失败 ${url}`); - this.handleReconnect(url); - }, - }); - - client.activate(); - return client; - } - - /** - * 处理重连 - * @param url WebSocket订阅地址 - */ - private handleReconnect(url: string) { - const retryCount = this.retryCountMap.get(url) || 0; - - if (retryCount < MAX_RETRIES) { - this.retryCountMap.set(url, retryCount + 1); - console.log(`重试连接 ${url} (${retryCount + 1}/${MAX_RETRIES})...`); - setTimeout( - () => - this.getWebSocketClient( - url, - () => {}, - () => {} - ), - RETRY_DELAY_MS - ); - } else { - console.error(`已经达到最大重试次数 ${url}`); - this.retryCountMap.delete(url); - } - } - - /** - * 断开所有 WebSocket 连接 - */ - public disconnectAll() { - this.clients.forEach((client, url) => { - console.log(`断开连接: ${url}`); - client.deactivate(); - }); - this.clients.clear(); - this.retryCountMap.clear(); - } -} - -export default new Socket(); diff --git a/src/components/Notice/index.vue b/src/components/Notice/index.vue index f7ecf8d0..1caf557c 100644 --- a/src/components/Notice/index.vue +++ b/src/components/Notice/index.vue @@ -1,5 +1,5 @@ - + @@ -40,7 +40,7 @@ - + 查看更多 @@ -50,7 +50,7 @@ v-if="messages.length > 0" type="primary" :underline="false" - @click="readAllNotice()" + @click="markAllAsRead" > 全部已读 @@ -58,99 +58,110 @@ - + + + + + + {{ currentMessage.title }} + + { + fullscreen = !fullscreen; + } + " + > + + + + + + + + + + + + + 系统通知 + + + 通知消息 + + + + + + + + diff --git a/src/components/NoticeModal/index.vue b/src/components/NoticeModal/index.vue deleted file mode 100644 index 2a958e03..00000000 --- a/src/components/NoticeModal/index.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - - - {{ message.title }} - - { - fullscreen = !fullscreen; - } - " - > - - - - - - - - - - - 系统通知 - 通知消息 - - - 低 - 中 - 高 - - {{ message.releaseBy }} - {{ message.releaseTime }} - - - - - - - diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue index a89b948f..acade164 100644 --- a/src/components/Upload/FileUpload.vue +++ b/src/components/Upload/FileUpload.vue @@ -65,11 +65,11 @@ import { UploadProgressEvent, UploadFiles, } from "element-plus"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; + import FileAPI from "@/api/file"; -import { ref, watch } from "vue"; -import { ElMessage } from "element-plus"; +import { getToken } from "@/utils/auth"; import { ResultEnum } from "@/enums/ResultEnum"; + const emit = defineEmits(["update:modelValue"]); const props = defineProps({ /** @@ -149,7 +149,7 @@ const props = defineProps({ type: Object, default: () => { return { - Authorization: localStorage.getItem(TOKEN_KEY), + Authorization: getToken(), }; }, }, diff --git a/src/components/Upload/ImageUpload.vue b/src/components/Upload/ImageUpload.vue index 9598764a..457897eb 100644 --- a/src/components/Upload/ImageUpload.vue +++ b/src/components/Upload/ImageUpload.vue @@ -52,7 +52,7 @@ import { UploadProps, } from "element-plus"; import FileAPI from "@/api/file"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; +import { getToken } from "@/utils/auth"; import { ResultEnum } from "@/enums/ResultEnum"; const emit = defineEmits(["update:modelValue"]); @@ -79,7 +79,7 @@ const props = defineProps({ type: Object, default: () => { return { - Authorization: localStorage.getItem(TOKEN_KEY), + Authorization: getToken(), }; }, }, diff --git a/src/enums/CacheEnum.ts b/src/enums/CacheEnum.ts deleted file mode 100644 index f3c434ba..00000000 --- a/src/enums/CacheEnum.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 令牌缓存Key - */ -export const TOKEN_KEY = "accessToken"; diff --git a/src/layout/components/NavBar/components/NavbarAction.vue b/src/layout/components/NavBar/components/NavbarAction.vue index c107c5d9..2c0022c6 100644 --- a/src/layout/components/NavBar/components/NavbarAction.vue +++ b/src/layout/components/NavBar/components/NavbarAction.vue @@ -23,7 +23,7 @@ - + diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts index f658a7ef..73376344 100644 --- a/src/plugins/permission.ts +++ b/src/plugins/permission.ts @@ -5,7 +5,8 @@ import { } from "vue-router"; import NProgress from "@/utils/nprogress"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; +import { isLogin } from "@/utils/auth"; + import router from "@/router"; import { usePermissionStore, useUserStore } from "@/store"; @@ -15,9 +16,7 @@ export function setupPermission() { router.beforeEach(async (to, from, next) => { NProgress.start(); - const hasToken = localStorage.getItem(TOKEN_KEY); - - if (hasToken) { + if (isLogin()) { if (to.path === "/login") { // 如果已登录,跳转到首页 next({ path: "/" }); diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 40bf52f2..c8aa2ce3 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -2,7 +2,7 @@ import AuthAPI, { type LoginData } from "@/api/auth"; import UserAPI, { type UserInfo } from "@/api/user"; import { resetRouter } from "@/router"; import { store } from "@/store"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; +import { setToken, removeToken } from "@/utils/auth"; export const useUserStore = defineStore("user", () => { const user = ref({ @@ -21,7 +21,7 @@ export const useUserStore = defineStore("user", () => { AuthAPI.login(loginData) .then((data) => { const { tokenType, accessToken } = data; - localStorage.setItem(TOKEN_KEY, tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx + setToken(tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx resolve(); }) .catch((error) => { @@ -57,7 +57,6 @@ export const useUserStore = defineStore("user", () => { return new Promise((resolve, reject) => { AuthAPI.logout() .then(() => { - localStorage.setItem(TOKEN_KEY, ""); location.reload(); // 清空路由 resolve(); }) diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 00000000..5268f1f4 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,19 @@ +const TOKEN_KEY = "v3-admin-token"; + +function getToken(): string { + return localStorage.getItem(TOKEN_KEY) || ""; +} + +function setToken(token: string) { + return localStorage.setItem(TOKEN_KEY, token); +} + +function removeToken() { + return localStorage.removeItem(TOKEN_KEY); +} + +function isLogin(): boolean { + return !!getToken(); +} + +export { getToken, setToken, removeToken, isLogin }; diff --git a/src/utils/request.ts b/src/utils/request.ts index 3f89b54d..f925f5b8 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,7 +1,7 @@ import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios"; import { useUserStoreHook } from "@/store/modules/user"; import { ResultEnum } from "@/enums/ResultEnum"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; +import { getToken } from "@/utils/auth"; import qs from "qs"; // 创建 axios 实例 @@ -17,7 +17,7 @@ const service = axios.create({ // 请求拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { - const accessToken = localStorage.getItem(TOKEN_KEY); + const accessToken = getToken(); if (accessToken) { config.headers.Authorization = accessToken; } diff --git a/src/utils/socket.ts b/src/utils/socket.ts new file mode 100644 index 00000000..fa6a87e4 --- /dev/null +++ b/src/utils/socket.ts @@ -0,0 +1,150 @@ +import { Client } from "@stomp/stompjs"; +import { getToken } from "@/utils/auth"; + +const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数 +const RECONNECT_DELAY_MS = 5000; // 重连延迟时间(毫秒) +const HEARTBEAT_INTERVAL_MS = 30000; // 心跳间隔时间(毫秒) + +class WebSocketManager { + private clients: Map = new Map(); // 保存所有 WebSocket 客户端 + private reconnectAttempts: Map = new Map(); // 记录各地址的重连次数 + private messageHandlers: Map void)[]> = + new Map(); // 保存订阅的消息回调 + + constructor() {} + + /** + * 获取已有的 WebSocket 客户端 + * + * @param endpoint WebSocket 连接地址 + * @returns WebSocket Client 实例或 undefined + */ + public getClient(endpoint: string): Client | undefined { + return this.clients.get(endpoint); + } + + /** + * 获取 WebSocket 客户端,如果已存在则返回已有客户端,否则创建新的客户端 + * + * @param endpoint WebSocket 连接地址 + * @param onMessage 收到消息时的回调 + * @param onError 出现错误时的回调 + * @returns WebSocket Client 实例 + */ + public getOrCreateClient( + endpoint: string, + onMessage: (message: string) => void, + onError?: (error: any) => void + ): Client { + let client = this.getClient(endpoint); + if (client) { + // 如果该地址已有连接,直接添加消息回调 + this.messageHandlers.get(endpoint)?.push(onMessage); + } else { + // 否则创建新客户端 + client = this.createClient(endpoint, onMessage, onError); + this.clients.set(endpoint, client); + this.messageHandlers.set(endpoint, [onMessage]); + } + return client; + } + + /** + * 创建 WebSocket 客户端 + * + * @param endpoint WebSocket 连接地址 + * @param onMessage 收到消息时的回调 + * @param onError 出现错误时的回调 + * @returns WebSocket Client 实例 + * @private + */ + private createClient( + endpoint: string, + onMessage: (message: string) => void, + onError?: (error: any) => void + ): Client { + const client = new Client({ + brokerURL: endpoint, // 使用传入的 endpoint 动态设置连接地址 + connectHeaders: { + Authorization: getToken(), + }, + heartbeatIncoming: HEARTBEAT_INTERVAL_MS, + heartbeatOutgoing: HEARTBEAT_INTERVAL_MS, + onConnect: () => { + console.log(`已连接到 ${endpoint}`); + client.subscribe(endpoint, (message) => { + onMessage(message.body); // 收到消息时调用回调 + }); + }, + onStompError: (frame) => { + console.error( + `连接错误: ${endpoint}, 错误消息: ${frame.headers["message"]}` + ); + console.error(`错误详情: ${frame.body}`); + if (onError) { + onError(frame); + } + this.handleReconnect(endpoint); // 出现错误时处理重连 + }, + onDisconnect: () => { + console.log(`已断开连接: ${endpoint}`); + this.handleReconnect(endpoint); // 断开时处理重连 + }, + }); + + client.activate(); + return client; + } + + /** + * 处理 WebSocket 重连 + * + * @param endpoint WebSocket 连接地址 + * @private + */ + private handleReconnect(endpoint: string) { + const attemptCount = this.reconnectAttempts.get(endpoint) || 0; + + if (this.clients.has(endpoint)) { + const client = this.clients.get(endpoint); + if (client && client.connected) { + client.deactivate(); // 主动断开已有连接 + } + } + + // 重连次数未达到最大次数时继续重连 + if (attemptCount < MAX_RECONNECT_ATTEMPTS) { + this.reconnectAttempts.set(endpoint, attemptCount + 1); + console.log( + `尝试重连 (${attemptCount + 1}/${MAX_RECONNECT_ATTEMPTS}): ${endpoint}` + ); + setTimeout(() => { + const originalOnMessage = this.messageHandlers.get(endpoint) || []; + this.getOrCreateClient( + endpoint, + (message) => originalOnMessage.forEach((handler) => handler(message)), + () => {} + ); + }, RECONNECT_DELAY_MS); + } else { + console.error(`达到最大重连次数: ${endpoint}`); + this.reconnectAttempts.delete(endpoint); // 超过最大重连次数后清除重连记录 + } + } + + /** + * 断开所有 WebSocket 连接 + * + * @param delay 延迟断开时间(毫秒),默认为 0 + */ + public disconnectAll(delay: number = 0) { + this.clients.forEach((client, endpoint) => { + console.log(`断开 WebSocket 连接: ${endpoint}`); + setTimeout(() => client.deactivate(), delay); // 延迟断开连接 + }); + this.clients.clear(); + this.reconnectAttempts.clear(); + } +} + +export default new WebSocketManager(); diff --git a/src/views/demo/websocket.vue b/src/views/demo/websocket.vue index fe9c8e79..1a49af54 100644 --- a/src/views/demo/websocket.vue +++ b/src/views/demo/websocket.vue @@ -107,7 +107,7 @@ import { Client } from "@stomp/stompjs"; import { useUserStoreHook } from "@/store/modules/user"; -import { TOKEN_KEY } from "@/enums/CacheEnum"; +import { getToken } from "@/utils/auth"; const userStore = useUserStoreHook(); const isConnected = ref(false); @@ -141,7 +141,7 @@ function connectWebSocket() { stompClient = new Client({ brokerURL: socketEndpoint.value, connectHeaders: { - Authorization: localStorage.getItem(TOKEN_KEY) || "", + Authorization: getToken(), }, debug: (str) => { console.log(str); diff --git a/src/views/notice/index.vue b/src/views/notice/index.vue index 4ea6cadc..c446129f 100644 --- a/src/views/notice/index.vue +++ b/src/views/notice/index.vue @@ -119,13 +119,10 @@ @pagination="handleQuery()" /> -