diff --git a/package.json b/package.json index 4f82fd25..686fa115 100644 --- a/package.json +++ b/package.json @@ -121,5 +121,6 @@ }, "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", "author": "有来开源组织", - "license": "MIT" + "license": "MIT", + "packageManager": "pnpm@9.1.3+sha512.7c2ea089e1a6af306409c4fc8c4f0897bdac32b772016196c469d9428f1fe2d5a21daf8ad6512762654ac645b5d9136bb210ec9a00afa8dbc4677843ba362ecd" } diff --git a/src/api/dict-data.ts b/src/api/dict-data.ts new file mode 100644 index 00000000..79a2e90b --- /dev/null +++ b/src/api/dict-data.ts @@ -0,0 +1,157 @@ +import request from "@/utils/request"; + +const DICT_DATA_BASE_URL = "/api/v1/dict-data"; + +class DictDataAPI { + /** + * 获取字典分页列表 + * + * @param queryParams 查询参数 + * @returns 字典分页结果 + */ + static getPage(queryParams: DictDataPageQuery) { + return request>({ + url: `${DICT_DATA_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + } + + /** + * 获取字典数据表单 + * + * @param id 字典ID + * @returns 字典数据表单 + */ + static getFormData(id: number) { + return request>({ + url: `${DICT_DATA_BASE_URL}/${id}/form`, + method: "get", + }); + } + + /** + * 新增字典数据 + * + * @param data 字典数据 + */ + static add(data: DictDataForm) { + return request({ + url: `${DICT_DATA_BASE_URL}`, + method: "post", + data: data, + }); + } + + /** + * 修改字典数据 + * + * @param id 字典ID + * @param data 字典数据 + */ + static update(id: number, data: DictDataForm) { + return request({ + url: `${DICT_DATA_BASE_URL}/${id}`, + method: "put", + data: data, + }); + } + + /** + * 删除字典 + * + * @param ids 字典ID,多个以英文逗号(,)分隔 + */ + static deleteByIds(ids: string) { + return request({ + url: `${DICT_DATA_BASE_URL}/${ids}`, + method: "delete", + }); + } + + /** + * 获取字典的数据项 + * + * @param dictCode 字典编码 + * @returns 字典数据项 + */ + static getOptions(dictCode: string) { + return request({ + url: `${DICT_DATA_BASE_URL}/${dictCode}/options`, + method: "get", + }); + } +} + +export default DictDataAPI; + +/** + * 字典查询参数 + */ +export interface DictDataPageQuery extends PageQuery { + /** 关键字(字典数据值/标签) */ + keywords?: string; + + /** 字典编码 */ + dictCode?: string; +} + +/** + * 字典分页对象 + */ +export interface DictDataPageVO { + /** + * 字典ID + */ + id: number; + /** + * 字典编码 + */ + dictCode: string; + /** + * 字典数据值 + */ + value: string; + /** + * 字典数据标签 + */ + label: string; + /** + * 状态(1:启用,0:禁用) + */ + status: number; + /** + * 字典排序 + */ + sort?: number; +} + +/** + * 字典 + */ +export interface DictDataForm { + /** + * 字典ID + */ + id?: number; + /** + * 字典编码 + */ + dictCode?: string; + /** + * 字典数据值 + */ + value?: string; + /** + * 字典数据标签 + */ + label?: string; + /** + * 状态(1:启用,0:禁用) + */ + status?: number; + /** + * 字典排序 + */ + sort?: number; +} diff --git a/src/api/dict.ts b/src/api/dict.ts index 83cd675d..8fc18cc1 100644 --- a/src/api/dict.ts +++ b/src/api/dict.ts @@ -80,19 +80,6 @@ class DictAPI { method: "get", }); } - - /** - * 获取字典的数据项 - * - * @param typeCode 字典编码 - * @returns 字典数据项 - */ - static getOptions(code: string) { - return request({ - url: `${DICT_BASE_URL}/${code}/options`, - method: "get", - }); - } } export default DictAPI; @@ -105,6 +92,11 @@ export interface DictPageQuery extends PageQuery { * 关键字(字典名称/编码) */ keywords?: string; + + /** + * 字典状态(1:启用,0:禁用) + */ + status?: number; } /** @@ -122,45 +114,13 @@ export interface DictPageVO { /** * 字典编码 */ - code: string; + dictCode: string; /** - * 字典状态(1-启用,0-禁用) + * 字典状态(1:启用,0:禁用) */ status: number; - /** - * 字典项列表 - */ - dictItems: DictItem[]; } -/** - * 字典项 - */ -export interface DictItem { - /** - * 字典项ID - */ - id?: number; - /** - * 字典项名称 - */ - name?: string; - /** - * 字典项值 - */ - value?: string; - /** - * 排序 - */ - sort?: number; - /** - * 状态(1-启用,0-禁用) - */ - status?: number; -} - -// TypeScript 类型声明 - /** * 字典 */ @@ -176,7 +136,7 @@ export interface DictForm { /** * 字典编码 */ - code?: string; + dictCode?: string; /** * 字典状态(1-启用,0-禁用) */ @@ -185,8 +145,4 @@ export interface DictForm { * 备注 */ remark?: string; - /** - * 字典数据项列表 - */ - dictItems?: DictItem[]; } diff --git a/src/api/notice.ts b/src/api/notice.ts index f52702e4..a442cc0e 100644 --- a/src/api/notice.ts +++ b/src/api/notice.ts @@ -73,7 +73,7 @@ class NoticeAPI { */ static publish(id: number) { return request({ - url: `${NOTICE_BASE_URL}/release/${id}`, + url: `${NOTICE_BASE_URL}/${id}/publish`, method: "patch", }); } @@ -84,32 +84,21 @@ class NoticeAPI { * @param id 撤回的通知id * @returns */ - static revoke(id: number): Promise<[]> { + static revoke(id: number) { return request({ url: `${NOTICE_BASE_URL}/${id}/revoke`, method: "patch", }); } - - /** - * 获取未读消息 - */ - static getUnreadList() { - return request({ - url: `${NOTICE_BASE_URL}/unread`, - method: "get", - }); - } - /** * 查看通知 * * @param id */ - static getDetail(id: number): Promise { - return request({ + static getDetail(id: string) { + return request({ url: `${NOTICE_BASE_URL}/${id}/detail`, - method: "PATCH", + method: "get", }); } @@ -117,7 +106,7 @@ class NoticeAPI { static readAll() { return request({ url: `${NOTICE_BASE_URL}/read-all`, - method: "PATCH", + method: "put", }); } @@ -139,6 +128,8 @@ export interface NoticePageQuery extends PageQuery { title?: string; /** 发布状态(0:未发布,1:已发布,-1:已撤回) */ publishStatus?: number; + + isRead?: number; } /** 通知公告表单对象 */ @@ -151,7 +142,7 @@ export interface NoticeForm { /** 通知类型 */ type?: number; /** 优先级(L:低,M:中,H:高) */ - level?: number; + level?: string; /** 目标类型(1-全体 2-指定) */ targetType?: number; /** 目标ID合集,以,分割 */ @@ -160,7 +151,7 @@ export interface NoticeForm { /** 通知公告分页对象 */ export interface NoticePageVO { - id?: string; + id: string; /** 通知标题 */ title?: string; /** 通知内容 */ diff --git a/src/api/user.ts b/src/api/user.ts index e7ac59c3..beebdad7 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -199,8 +199,8 @@ class UserAPI { /** * 获取用户下拉列表 */ - static getOptions(): AxiosPromise { - return request({ + static getOptions() { + return request({ url: `${USER_BASE_URL}/options`, method: "get", }); diff --git a/src/components/Dictionary/index.vue b/src/components/Dictionary/index.vue index 763c2c6f..d56ed7e1 100644 --- a/src/components/Dictionary/index.vue +++ b/src/components/Dictionary/index.vue @@ -16,18 +16,16 @@ diff --git a/src/components/Notice/index.vue b/src/components/Notice/index.vue index 1caf557c..b7b63340 100644 --- a/src/components/Notice/index.vue +++ b/src/components/Notice/index.vue @@ -1,43 +1,41 @@ - + diff --git a/src/enums/MessageTypeEnum.ts b/src/enums/MessageTypeEnum.ts deleted file mode 100644 index 071f1b26..00000000 --- a/src/enums/MessageTypeEnum.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* 消息类型枚举 */ -export const enum MessageTypeEnum { - /* 消息 */ - MESSAGE = "SYSTEM_MESSAGE", - /* 通知 */ - NOTICE = "NOTICE", - /* 待办 */ - TODO = "TODO", -} - -export const MessageTypeLabels = { - [MessageTypeEnum.MESSAGE]: "消息", - // [MessageTypeEnum.NOTICE]: "通知", - // [MessageTypeEnum.TODO]: "待办", -}; diff --git a/src/layout/components/NavBar/components/NavbarAction.vue b/src/layout/components/NavBar/components/NavbarAction.vue index 2c0022c6..aba51152 100644 --- a/src/layout/components/NavBar/components/NavbarAction.vue +++ b/src/layout/components/NavBar/components/NavbarAction.vue @@ -124,11 +124,6 @@ function logout() { } } -:deep(.message .el-badge__content.is-fixed.is-dot) { - top: 5px; - right: 10px; -} - :deep(.el-divider--horizontal) { margin: 10px 0; } diff --git a/src/router/index.ts b/src/router/index.ts index 8f1f151e..b18a792d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -58,60 +58,14 @@ export const constantRoutes: RouteRecordRaw[] = [ component: () => import("@/views/profile/index.vue"), meta: { title: "个人中心", icon: "user", hidden: true }, }, + { + path: "myNotice", + name: "MyNotice", + component: () => import("@/views/system/notice/my-notice.vue"), + meta: { title: "我的通知", icon: "user", hidden: true }, + }, ], }, - - // 外部链接 - // { - // path: "/external-link", - // component: Layout, - // children: [ { - // component: () => import("@/views/external-link/index.vue"), - // path: "https://www.cnblogs.com/haoxianrui/", - // meta: { title: "外部链接", icon: "link" }, - // }, - // ], - // }, - // 多级嵌套路由 - /* { - path: '/nested', - component: Layout, - redirect: '/nested/level1/level2', - name: 'Nested', - meta: {title: '多级菜单', icon: 'nested'}, - children: [ - { - path: 'level1', - component: () => import('@/views/nested/level1/index.vue'), - name: 'Level1', - meta: {title: '菜单一级'}, - redirect: '/nested/level1/level2', - children: [ - { - path: 'level2', - component: () => import('@/views/nested/level1/level2/index.vue'), - name: 'Level2', - meta: {title: '菜单二级'}, - redirect: '/nested/level1/level2/level3', - children: [ - { - path: 'level3-1', - component: () => import('@/views/nested/level1/level2/level3/index1.vue'), - name: 'Level3-1', - meta: {title: '菜单三级-1'} - }, - { - path: 'level3-2', - component: () => import('@/views/nested/level1/level2/level3/index2.vue'), - name: 'Level3-2', - meta: {title: '菜单三级-2'} - } - ] - } - ] - }, - ] - }*/ ]; /** diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index c8aa2ce3..db62ab55 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -69,7 +69,7 @@ export const useUserStore = defineStore("user", () => { // remove token function resetToken() { return new Promise((resolve) => { - localStorage.setItem(TOKEN_KEY, ""); + removeToken(); resetRouter(); resolve(); }); diff --git a/src/types/components.d.ts b/src/types/components.d.ts index f947408e..fe6a01e9 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -13,10 +13,7 @@ declare module "vue" { Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"]; CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"]; CURD: (typeof import("./../components/CURD/index.vue"))["default"]; - DeptTree: (typeof import("./../views/system/user/components/dept-tree.vue"))["default"]; - UserImport: (typeof import("./../views/system/user/components/user-import.vue"))["default"]; Dictionary: (typeof import("./../components/Dictionary/index.vue"))["default"]; - DictItem: (typeof import("./../views/system/dict/components/dict-item.vue"))["default"]; ElBacktop: (typeof import("element-plus/es"))["ElBacktop"]; ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"]; ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"]; @@ -75,7 +72,7 @@ declare module "vue" { LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"]; MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"]; Notice: (typeof import("./../components/Notice/index.vue"))["default"]; - NoticeModal: (typeof import("./../components/NoticeModal/index.vue"))["default"]; + NoticeDetail: (typeof import("../views/system/notice/notice-detail.vue"))["default"]; LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"]; MultiUpload: (typeof import("./../components/Upload/MultiUpload.vue"))["default"]; NavBar: (typeof import("./../layout/components/NavBar/index.vue"))["default"]; diff --git a/src/utils/socket.ts b/src/utils/socket.ts index fa6a87e4..e98ee6bc 100644 --- a/src/utils/socket.ts +++ b/src/utils/socket.ts @@ -1,79 +1,38 @@ 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; // 心跳间隔时间(毫秒) +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 client: Client | null = null; + private reconnectAttempts: number = 0; private messageHandlers: Map void)[]> = - new Map(); // 保存订阅的消息回调 + new Map(); constructor() {} - /** - * 获取已有的 WebSocket 客户端 - * - * @param endpoint WebSocket 连接地址 - * @returns WebSocket Client 实例或 undefined - */ - public getClient(endpoint: string): Client | undefined { - return this.clients.get(endpoint); - } + private getOrCreateClient(onError?: (error: any) => void): Client { + const endpoint = import.meta.env.VITE_APP_WS_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]); + if (this.client) { + return this.client; } - 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 动态设置连接地址 + this.client = new Client({ + brokerURL: endpoint, connectHeaders: { Authorization: getToken(), }, heartbeatIncoming: HEARTBEAT_INTERVAL_MS, heartbeatOutgoing: HEARTBEAT_INTERVAL_MS, onConnect: () => { - console.log(`已连接到 ${endpoint}`); - client.subscribe(endpoint, (message) => { - onMessage(message.body); // 收到消息时调用回调 + console.log(`已连接到 WebSocket 服务器: ${endpoint}`); + this.messageHandlers.forEach((handlers, topic) => { + handlers.forEach((handler) => { + this.subscribeToTopic(topic, handler); + }); }); }, onStompError: (frame) => { @@ -84,66 +43,66 @@ class WebSocketManager { if (onError) { onError(frame); } - this.handleReconnect(endpoint); // 出现错误时处理重连 + this.handleReconnect(); }, onDisconnect: () => { console.log(`已断开连接: ${endpoint}`); - this.handleReconnect(endpoint); // 断开时处理重连 + this.handleReconnect(); }, }); - client.activate(); - return client; + this.client.activate(); + return this.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(); // 主动断开已有连接 - } + public subscribeToTopic( + topic: string, + onMessage: (message: string) => void, + onError?: (error: any) => void + ) { + if (!this.client || !this.client.connected) { + console.log("WebSocket 尚未连接,正在连接..."); + this.getOrCreateClient(onError); } - // 重连次数未达到最大次数时继续重连 - if (attemptCount < MAX_RECONNECT_ATTEMPTS) { - this.reconnectAttempts.set(endpoint, attemptCount + 1); + if (this.messageHandlers.has(topic)) { + this.messageHandlers.get(topic)?.push(onMessage); + } else { + this.messageHandlers.set(topic, [onMessage]); + } + + if (this.client?.connected) { + console.log(`正在订阅主题: ${topic}`); + this.client.subscribe(topic, (message) => { + const handlers = this.messageHandlers.get(topic); + handlers?.forEach((handler) => handler(message.body)); + }); + } + } + + private handleReconnect() { + if (this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + this.reconnectAttempts++; console.log( - `尝试重连 (${attemptCount + 1}/${MAX_RECONNECT_ATTEMPTS}): ${endpoint}` + `重连尝试 (${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})` ); + setTimeout(() => { - const originalOnMessage = this.messageHandlers.get(endpoint) || []; - this.getOrCreateClient( - endpoint, - (message) => originalOnMessage.forEach((handler) => handler(message)), - () => {} - ); + this.client?.deactivate(); + this.client = null; + this.getOrCreateClient(); }, RECONNECT_DELAY_MS); } else { - console.error(`达到最大重连次数: ${endpoint}`); - this.reconnectAttempts.delete(endpoint); // 超过最大重连次数后清除重连记录 + console.error("达到最大重连次数,停止重连"); } } - /** - * 断开所有 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(); + public disconnect() { + if (this.client) { + console.log("断开 WebSocket 连接"); + this.client.deactivate(); + this.client = null; + } } } diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index f7d1ce1f..0333d3e0 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -188,15 +188,13 @@ diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue index 60d32252..0d32e299 100644 --- a/src/views/system/dict/index.vue +++ b/src/views/system/dict/index.vue @@ -1,12 +1,12 @@ - +