wip: 🚧 通知公告重构临时提交
This commit is contained in:
@@ -27,6 +27,7 @@ class NoticeAPI {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加通知公告
|
* 添加通知公告
|
||||||
|
*
|
||||||
* @param data Notice表单数据
|
* @param data Notice表单数据
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +71,7 @@ class NoticeAPI {
|
|||||||
* @param id 被发布的通知公告id
|
* @param id 被发布的通知公告id
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static releaseNotice(id: number) {
|
static publish(id: number) {
|
||||||
return request({
|
return request({
|
||||||
url: `${NOTICE_BASE_URL}/release/${id}`,
|
url: `${NOTICE_BASE_URL}/release/${id}`,
|
||||||
method: "patch",
|
method: "patch",
|
||||||
@@ -83,19 +84,18 @@ class NoticeAPI {
|
|||||||
* @param id 撤回的通知id
|
* @param id 撤回的通知id
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static recallNotice(id: number): Promise<[]> {
|
static revoke(id: number): Promise<[]> {
|
||||||
return request({
|
return request({
|
||||||
url: `${NOTICE_BASE_URL}/recall/${id}`,
|
url: `${NOTICE_BASE_URL}/${id}/revoke`,
|
||||||
method: "patch",
|
method: "patch",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取未读消息
|
* 获取未读消息
|
||||||
* @returns 消息
|
|
||||||
*/
|
*/
|
||||||
static listUnreadNotice() {
|
static getUnreadList() {
|
||||||
return request({
|
return request<any, UserNoticePageVO[]>({
|
||||||
url: `${NOTICE_BASE_URL}/unread`,
|
url: `${NOTICE_BASE_URL}/unread`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
@@ -103,36 +103,25 @@ class NoticeAPI {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查看通知
|
* 查看通知
|
||||||
* @param id
|
*
|
||||||
*/
|
|
||||||
static readNotice(id: number): Promise<NoticeDetailVO> {
|
|
||||||
return request({
|
|
||||||
url: `${NOTICE_BASE_URL}/read/${id}`,
|
|
||||||
method: "PATCH",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查看通知详情
|
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
static getDetail(id: number): Promise<NoticeDetailVO> {
|
static getDetail(id: number): Promise<NoticeDetailVO> {
|
||||||
return request({
|
return request({
|
||||||
url: `${NOTICE_BASE_URL}/detail/${id}`,
|
url: `${NOTICE_BASE_URL}/${id}/detail`,
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全部已读
|
|
||||||
*/
|
|
||||||
static readAllNotice() {
|
|
||||||
return request({
|
|
||||||
url: `${NOTICE_BASE_URL}/readAll`,
|
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 全部已读 */
|
||||||
|
static readAll() {
|
||||||
|
return request({
|
||||||
|
url: `${NOTICE_BASE_URL}/read-all`,
|
||||||
|
method: "PATCH",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取我的通知分页列表 */
|
||||||
static getMyNoticePage(queryParams?: NoticePageQuery) {
|
static getMyNoticePage(queryParams?: NoticePageQuery) {
|
||||||
return request<any, PageResult<NoticePageVO[]>>({
|
return request<any, PageResult<NoticePageVO[]>>({
|
||||||
url: `${NOTICE_BASE_URL}/my/page`,
|
url: `${NOTICE_BASE_URL}/my/page`,
|
||||||
@@ -148,8 +137,8 @@ export default NoticeAPI;
|
|||||||
export interface NoticePageQuery extends PageQuery {
|
export interface NoticePageQuery extends PageQuery {
|
||||||
/** 标题 */
|
/** 标题 */
|
||||||
title?: string;
|
title?: string;
|
||||||
/** 发布状态(0-未发布 1已发布 2已撤回) */
|
/** 发布状态(0:未发布,1:已发布,-1:已撤回) */
|
||||||
sendStatus?: number;
|
publishStatus?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通知公告表单对象 */
|
/** 通知公告表单对象 */
|
||||||
@@ -160,13 +149,13 @@ export interface NoticeForm {
|
|||||||
/** 通知内容 */
|
/** 通知内容 */
|
||||||
content?: string;
|
content?: string;
|
||||||
/** 通知类型 */
|
/** 通知类型 */
|
||||||
noticeType?: number;
|
type?: number;
|
||||||
/** 优先级(0-低 1-中 2-高) */
|
/** 优先级(L:低,M:中,H:高) */
|
||||||
priority?: number;
|
level?: number;
|
||||||
/** 目标类型(0-全体 1-指定) */
|
/** 目标类型(1-全体 2-指定) */
|
||||||
tarType?: number;
|
targetType?: number;
|
||||||
/** 目标ID合集,以,分割 */
|
/** 目标ID合集,以,分割 */
|
||||||
tarIds?: string;
|
targetUserIds?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通知公告分页对象 */
|
/** 通知公告分页对象 */
|
||||||
@@ -177,33 +166,67 @@ export interface NoticePageVO {
|
|||||||
/** 通知内容 */
|
/** 通知内容 */
|
||||||
content?: string;
|
content?: string;
|
||||||
/** 通知类型 */
|
/** 通知类型 */
|
||||||
noticeType?: number;
|
type?: number;
|
||||||
/** 发布人 */
|
/** 发布人 */
|
||||||
releaseBy?: bigint;
|
publisherId?: bigint;
|
||||||
/** 优先级(0-低 1-中 2-高) */
|
/** 优先级(0-低 1-中 2-高) */
|
||||||
priority?: number;
|
priority?: number;
|
||||||
/** 目标类型(0-全体 1-指定) */
|
/** 目标类型(0-全体 1-指定) */
|
||||||
tarType?: number;
|
targetType?: number;
|
||||||
/** 发布状态(0-未发布 1已发布 2已撤回) */
|
/** 发布状态(0-未发布 1已发布 2已撤回) */
|
||||||
releaseStatus?: number;
|
publishStatus?: number;
|
||||||
/** 发布时间 */
|
/** 发布时间 */
|
||||||
releaseTime?: Date;
|
publishTime?: Date;
|
||||||
/** 撤回时间 */
|
/** 撤回时间 */
|
||||||
recallTime?: Date;
|
revokeTime?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoticeDetailVO {
|
export interface NoticeDetailVO {
|
||||||
|
/** 通知ID */
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
/** 通知标题 */
|
/** 通知标题 */
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
/** 通知内容 */
|
/** 通知内容 */
|
||||||
content?: string;
|
content?: string;
|
||||||
|
|
||||||
/** 通知类型 */
|
/** 通知类型 */
|
||||||
noticeType?: number;
|
type?: number;
|
||||||
|
|
||||||
/** 发布人 */
|
/** 发布人 */
|
||||||
releaseBy?: string;
|
publisherName?: string;
|
||||||
/** 优先级(0-低 1-中 2-高) */
|
|
||||||
priority?: number;
|
/** 优先级(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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string, Client> = new Map();
|
|
||||||
private retryCountMap: Map<string, number> = new Map();
|
|
||||||
private subscriptions: Map<string, ((message: string) => 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();
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="nav-action-item" style="display: flex; justify-content: center">
|
<div>
|
||||||
<el-dropdown trigger="hover">
|
<el-dropdown trigger="hover">
|
||||||
<el-badge :is-dot="messages.length > 0" :offset="offset">
|
<el-badge :is-dot="messages.length > 0" :offset="offset">
|
||||||
<div class="flex-center h100% p10px">
|
<div class="flex-center h100% p10px">
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<div class="flex-x-between">
|
<div class="flex-x-between">
|
||||||
<el-link type="primary" :underline="false" @click="more">
|
<el-link type="primary" :underline="false" @click="viewMore">
|
||||||
<span class="text-xs">查看更多</span>
|
<span class="text-xs">查看更多</span>
|
||||||
<el-icon class="text-xs">
|
<el-icon class="text-xs">
|
||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
v-if="messages.length > 0"
|
v-if="messages.length > 0"
|
||||||
type="primary"
|
type="primary"
|
||||||
:underline="false"
|
:underline="false"
|
||||||
@click="readAllNotice()"
|
@click="markAllAsRead"
|
||||||
>
|
>
|
||||||
<span class="text-xs">全部已读</span>
|
<span class="text-xs">全部已读</span>
|
||||||
</el-link>
|
</el-link>
|
||||||
@@ -58,99 +58,110 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<NoticeModal ref="noticeModalRef" />
|
|
||||||
|
<!-- 弹窗部分 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="modalVisible"
|
||||||
|
:show-close="false"
|
||||||
|
append-to-body
|
||||||
|
:fullscreen="fullscreen"
|
||||||
|
style="z-index: revert"
|
||||||
|
>
|
||||||
|
<template #header="{ close }">
|
||||||
|
<div class="flex-x-between">
|
||||||
|
<h3>{{ currentMessage.title }}</h3>
|
||||||
|
<div class="flex-center">
|
||||||
|
<el-icon
|
||||||
|
class="ml10px"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
fullscreen = !fullscreen;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FullScreen />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon @click="close" class="icon">
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="width: auto; text-align: left">
|
||||||
|
<span class="header-item">
|
||||||
|
<el-tag v-if="currentMessage.type === 2" type="warning">
|
||||||
|
系统通知
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="currentMessage.type === 1" type="success">
|
||||||
|
通知消息
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<div v-html="currentMessage.content"></div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MessageTypeEnum, MessageTypeLabels } from "@/enums/MessageTypeEnum";
|
import { MessageTypeEnum, MessageTypeLabels } from "@/enums/MessageTypeEnum";
|
||||||
import NoticeAPI from "@/api/notice";
|
import NoticeAPI from "@/api/notice";
|
||||||
import socket from "@/api/socket";
|
import WebSocketManager from "@/utils/socket";
|
||||||
import NoticeModal from "@/components/NoticeModal/index.vue";
|
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
|
// 状态和引用
|
||||||
const activeTab = ref(MessageTypeEnum.MESSAGE);
|
const activeTab = ref(MessageTypeEnum.MESSAGE);
|
||||||
const messages = ref<any>([]);
|
const messages = ref<any[]>([]);
|
||||||
const noticeModalRef = ref(NoticeModal);
|
const offset = ref<[number, number]>([-15, 15]);
|
||||||
const offset = ref<Number[]>([-15, 15]);
|
const currentMessage = ref<any>({});
|
||||||
|
const modalVisible = ref(false);
|
||||||
|
const fullscreen = ref(false);
|
||||||
|
|
||||||
const getFilteredMessages = (type: MessageTypeEnum) => {
|
// 获取未读消息列表并连接 WebSocket
|
||||||
return messages.value.filter(
|
onMounted(() => {
|
||||||
(message: { type: MessageTypeEnum }) => message.type === type
|
NoticeAPI.getUnreadList().then((data) => {
|
||||||
);
|
messages.value = data;
|
||||||
};
|
});
|
||||||
|
|
||||||
/**'
|
WebSocketManager.getOrCreateClient("/user/queue/message", (message) => {
|
||||||
* 连接WebSocket
|
const parsedMessage = JSON.parse(message);
|
||||||
*/
|
if (parsedMessage.noticeType === MessageTypeEnum.MESSAGE) {
|
||||||
function connectWebSocket() {
|
const content = JSON.parse(parsedMessage.content);
|
||||||
socket.getWebSocketClient("/user/queue/message", (message) => {
|
|
||||||
// 这里是不是可以获取到消息之后,直接调用接口获取消息列表呢???
|
|
||||||
let parse = JSON.parse(message);
|
|
||||||
// 如果是消息类型
|
|
||||||
if (parse.noticeType === MessageTypeEnum.MESSAGE) {
|
|
||||||
let content = JSON.parse(parse.content);
|
|
||||||
//是发布消息
|
|
||||||
if (content.type === "release") {
|
if (content.type === "release") {
|
||||||
//获取到id
|
const id = content.id;
|
||||||
let id = content.id;
|
if (!messages.value.some((msg) => msg.id === id)) {
|
||||||
//确认messages里面是否有这个id
|
messages.value.unshift({
|
||||||
let index = messages.value.findIndex((item: any) => item.id === id);
|
id,
|
||||||
if (index < 0) {
|
|
||||||
let messageContent = {
|
|
||||||
id: id,
|
|
||||||
title: content.title,
|
title: content.title,
|
||||||
type: MessageTypeEnum.MESSAGE,
|
type: MessageTypeEnum.MESSAGE,
|
||||||
};
|
});
|
||||||
messages.value.unshift(messageContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
/**
|
// 阅读通知公告
|
||||||
* 获取消息列表
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function listNotice() {
|
|
||||||
NoticeAPI.listUnreadNotice().then((res) => {
|
|
||||||
messages.value = res;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 阅读通知公告
|
|
||||||
* @param id
|
|
||||||
*/
|
|
||||||
function readNotice(id: number) {
|
function readNotice(id: number) {
|
||||||
let index = messages.value.findIndex(
|
const index = messages.value.findIndex((msg) => msg.id === id);
|
||||||
(item: { id: number }) => item.id === id
|
|
||||||
);
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
messages.value.splice(index, 1);
|
currentMessage.value = messages.value[index];
|
||||||
|
modalVisible.value = true;
|
||||||
|
messages.value.splice(index, 1); // 从消息列表中移除已读消息
|
||||||
}
|
}
|
||||||
noticeModalRef.value?.open(id); // 调用 open 方法,传入 ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 查看更多
|
||||||
* 查看更多
|
function viewMore() {
|
||||||
*/
|
|
||||||
function more() {
|
|
||||||
//跳转到我的消息页面
|
|
||||||
router.push({ path: "/notice/notice" });
|
router.push({ path: "/notice/notice" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 全部已读
|
||||||
* 全部已读
|
function markAllAsRead() {
|
||||||
*/
|
NoticeAPI.readAll().then(() => {
|
||||||
function readAllNotice() {
|
|
||||||
NoticeAPI.readAllNotice().then(() => {
|
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
listNotice();
|
|
||||||
connectWebSocket();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog
|
|
||||||
v-model="visible"
|
|
||||||
:show-close="false"
|
|
||||||
append-to-body
|
|
||||||
:fullscreen="fullscreen"
|
|
||||||
style="z-index: revert"
|
|
||||||
>
|
|
||||||
<template #header="{ close }">
|
|
||||||
<div class="my-header">
|
|
||||||
<h3>{{ message.title }}</h3>
|
|
||||||
<div class="icon-content">
|
|
||||||
<el-icon
|
|
||||||
class="icon"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
fullscreen = !fullscreen;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<FullScreen />
|
|
||||||
</el-icon>
|
|
||||||
<el-icon @click="close" class="icon">
|
|
||||||
<Close />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div style="width: auto; text-align: left">
|
|
||||||
<span class="header-item">
|
|
||||||
<el-tag v-if="message.noticeType == 2" type="warning">系统通知</el-tag>
|
|
||||||
<el-tag v-if="message.noticeType == 1" type="success">通知消息</el-tag>
|
|
||||||
</span>
|
|
||||||
<span class="header-item">
|
|
||||||
<el-tag v-if="message.priority == 0" type="danger">低</el-tag>
|
|
||||||
<el-tag v-if="message.priority == 1" type="success">中</el-tag>
|
|
||||||
<el-tag v-if="message.priority == 2" type="warning">高</el-tag>
|
|
||||||
</span>
|
|
||||||
<span class="header-item">{{ message.releaseBy }}</span>
|
|
||||||
<span class="header-item">{{ message.releaseTime }}</span>
|
|
||||||
</div>
|
|
||||||
<el-divider />
|
|
||||||
<div
|
|
||||||
v-html="message.content"
|
|
||||||
style="width: auto; min-height: 400px; text-align: left"
|
|
||||||
></div>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
/**
|
|
||||||
* 这里可能存在一个问题,因为上面展示方式我是用了v-html,所以这里可能会有xss攻击,后续可以考虑使用其他方式
|
|
||||||
* 或者是用 npm install dompurify 来处理
|
|
||||||
* 示例代码
|
|
||||||
* import DOMPurify from 'dompurify';
|
|
||||||
* const rawHtmlContent = '<p>Some HTML content</p>'; // 这可能来自不可信的源
|
|
||||||
* const safeHtmlContent = DOMPurify.sanitize(rawHtmlContent);
|
|
||||||
*/
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { defineExpose } from "vue";
|
|
||||||
import NoticeAPI, { NoticeDetailVO } from "@/api/notice";
|
|
||||||
|
|
||||||
const message = ref<NoticeDetailVO>({});
|
|
||||||
const visible = ref(false);
|
|
||||||
const fullscreen = ref(false);
|
|
||||||
const open = (id: number, read: boolean) => {
|
|
||||||
fullscreen.value = false;
|
|
||||||
getNoticeDetail(id, !read);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取消息详情
|
|
||||||
* @param id 通知id
|
|
||||||
* @param read 是否是阅读,因为存在管理员查看通知管理中的消息,所以这里需要区分
|
|
||||||
*/
|
|
||||||
function getNoticeDetail(id: number, read: boolean) {
|
|
||||||
if (read) {
|
|
||||||
NoticeAPI.readNotice(id).then((res) => {
|
|
||||||
visible.value = true;
|
|
||||||
message.value = res;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
NoticeAPI.getDetail(id).then((res) => {
|
|
||||||
visible.value = true;
|
|
||||||
message.value = res;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defineExpose({ open });
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.my-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-header .icon-content {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
justify-content: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-item {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -65,11 +65,11 @@ import {
|
|||||||
UploadProgressEvent,
|
UploadProgressEvent,
|
||||||
UploadFiles,
|
UploadFiles,
|
||||||
} from "element-plus";
|
} from "element-plus";
|
||||||
import { TOKEN_KEY } from "@/enums/CacheEnum";
|
|
||||||
import FileAPI from "@/api/file";
|
import FileAPI from "@/api/file";
|
||||||
import { ref, watch } from "vue";
|
import { getToken } from "@/utils/auth";
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import { ResultEnum } from "@/enums/ResultEnum";
|
import { ResultEnum } from "@/enums/ResultEnum";
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
@@ -149,7 +149,7 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
return {
|
return {
|
||||||
Authorization: localStorage.getItem(TOKEN_KEY),
|
Authorization: getToken(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
UploadProps,
|
UploadProps,
|
||||||
} from "element-plus";
|
} from "element-plus";
|
||||||
import FileAPI from "@/api/file";
|
import FileAPI from "@/api/file";
|
||||||
import { TOKEN_KEY } from "@/enums/CacheEnum";
|
import { getToken } from "@/utils/auth";
|
||||||
import { ResultEnum } from "@/enums/ResultEnum";
|
import { ResultEnum } from "@/enums/ResultEnum";
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
@@ -79,7 +79,7 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {
|
default: () => {
|
||||||
return {
|
return {
|
||||||
Authorization: localStorage.getItem(TOKEN_KEY),
|
Authorization: getToken(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
/**
|
|
||||||
* 令牌缓存Key
|
|
||||||
*/
|
|
||||||
export const TOKEN_KEY = "accessToken";
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<lang-select class="nav-action-item" />
|
<lang-select class="nav-action-item" />
|
||||||
|
|
||||||
<!-- 消息通知 -->
|
<!-- 消息通知 -->
|
||||||
<notice />
|
<notice class="nav-action-item" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 用户头像 -->
|
<!-- 用户头像 -->
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
|
|
||||||
import NProgress from "@/utils/nprogress";
|
import NProgress from "@/utils/nprogress";
|
||||||
import { TOKEN_KEY } from "@/enums/CacheEnum";
|
import { isLogin } from "@/utils/auth";
|
||||||
|
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { usePermissionStore, useUserStore } from "@/store";
|
import { usePermissionStore, useUserStore } from "@/store";
|
||||||
|
|
||||||
@@ -15,9 +16,7 @@ export function setupPermission() {
|
|||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
const hasToken = localStorage.getItem(TOKEN_KEY);
|
if (isLogin()) {
|
||||||
|
|
||||||
if (hasToken) {
|
|
||||||
if (to.path === "/login") {
|
if (to.path === "/login") {
|
||||||
// 如果已登录,跳转到首页
|
// 如果已登录,跳转到首页
|
||||||
next({ path: "/" });
|
next({ path: "/" });
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import AuthAPI, { type LoginData } from "@/api/auth";
|
|||||||
import UserAPI, { type UserInfo } from "@/api/user";
|
import UserAPI, { type UserInfo } from "@/api/user";
|
||||||
import { resetRouter } from "@/router";
|
import { resetRouter } from "@/router";
|
||||||
import { store } from "@/store";
|
import { store } from "@/store";
|
||||||
import { TOKEN_KEY } from "@/enums/CacheEnum";
|
import { setToken, removeToken } from "@/utils/auth";
|
||||||
|
|
||||||
export const useUserStore = defineStore("user", () => {
|
export const useUserStore = defineStore("user", () => {
|
||||||
const user = ref<UserInfo>({
|
const user = ref<UserInfo>({
|
||||||
@@ -21,7 +21,7 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
AuthAPI.login(loginData)
|
AuthAPI.login(loginData)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const { tokenType, accessToken } = data;
|
const { tokenType, accessToken } = data;
|
||||||
localStorage.setItem(TOKEN_KEY, tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
setToken(tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -57,7 +57,6 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
AuthAPI.logout()
|
AuthAPI.logout()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
localStorage.setItem(TOKEN_KEY, "");
|
|
||||||
location.reload(); // 清空路由
|
location.reload(); // 清空路由
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
|
|||||||
19
src/utils/auth.ts
Normal file
19
src/utils/auth.ts
Normal file
@@ -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 };
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { ResultEnum } from "@/enums/ResultEnum";
|
import { ResultEnum } from "@/enums/ResultEnum";
|
||||||
import { TOKEN_KEY } from "@/enums/CacheEnum";
|
import { getToken } from "@/utils/auth";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
|
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
@@ -17,7 +17,7 @@ const service = axios.create({
|
|||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const accessToken = localStorage.getItem(TOKEN_KEY);
|
const accessToken = getToken();
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
config.headers.Authorization = accessToken;
|
config.headers.Authorization = accessToken;
|
||||||
}
|
}
|
||||||
|
|||||||
150
src/utils/socket.ts
Normal file
150
src/utils/socket.ts
Normal file
@@ -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<string, Client> = new Map(); // 保存所有 WebSocket 客户端
|
||||||
|
private reconnectAttempts: Map<string, number> = new Map(); // 记录各地址的重连次数
|
||||||
|
private messageHandlers: Map<string, ((message: string) => 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();
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
import { Client } from "@stomp/stompjs";
|
import { Client } from "@stomp/stompjs";
|
||||||
|
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { TOKEN_KEY } from "@/enums/CacheEnum";
|
import { getToken } from "@/utils/auth";
|
||||||
|
|
||||||
const userStore = useUserStoreHook();
|
const userStore = useUserStoreHook();
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
@@ -141,7 +141,7 @@ function connectWebSocket() {
|
|||||||
stompClient = new Client({
|
stompClient = new Client({
|
||||||
brokerURL: socketEndpoint.value,
|
brokerURL: socketEndpoint.value,
|
||||||
connectHeaders: {
|
connectHeaders: {
|
||||||
Authorization: localStorage.getItem(TOKEN_KEY) || "",
|
Authorization: getToken(),
|
||||||
},
|
},
|
||||||
debug: (str) => {
|
debug: (str) => {
|
||||||
console.log(str);
|
console.log(str);
|
||||||
|
|||||||
@@ -119,13 +119,10 @@
|
|||||||
@pagination="handleQuery()"
|
@pagination="handleQuery()"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
<NoticeModal ref="noticeModalRef" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NoticeModal from "@/components/NoticeModal/index.vue";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "MyNotice",
|
name: "MyNotice",
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
link
|
link
|
||||||
@click="recallNotice(scope.row.id)"
|
@click="revokeNotice(scope.row.id)"
|
||||||
>
|
>
|
||||||
撤回
|
撤回
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -415,8 +415,8 @@ function releaseNotice(id: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function recallNotice(id: number) {
|
function revokeNotice(id: number) {
|
||||||
NoticeAPI.recallNotice(id).then((res) => {
|
NoticeAPI.revokeNotice(id).then((res) => {
|
||||||
ElMessage.success("撤回成功");
|
ElMessage.success("撤回成功");
|
||||||
handleQuery();
|
handleQuery();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
"element-plus/es/components/descriptions/style/index",
|
"element-plus/es/components/descriptions/style/index",
|
||||||
"element-plus/es/components/descriptions-item/style/index",
|
"element-plus/es/components/descriptions-item/style/index",
|
||||||
"element-plus/es/components/checkbox-group/style/index",
|
"element-plus/es/components/checkbox-group/style/index",
|
||||||
|
"element-plus/es/components/progress/style/index",
|
||||||
|
"element-plus/es/components/image-viewer/style/index",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 构建配置
|
// 构建配置
|
||||||
|
|||||||
Reference in New Issue
Block a user