update 完成消息盒子功能前后端联动(已读未读在前端浏览器存储)

This commit is contained in:
疯狂的狮子Li
2026-03-27 14:34:38 +08:00
parent 5907e4778b
commit ebc7760de2
10 changed files with 200 additions and 30 deletions
+1 -4
View File
@@ -53,10 +53,7 @@ export function register(data: any) {
*/
export function logout() {
closePush();
if (
import.meta.env.VITE_APP_MESSAGE_ENABLED === 'true'
&& import.meta.env.VITE_APP_MESSAGE_TRANSPORT.toLowerCase() !== 'websocket'
) {
if (import.meta.env.VITE_APP_MESSAGE_ENABLED === 'true' && import.meta.env.VITE_APP_MESSAGE_TRANSPORT.toLowerCase() === 'sse') {
request({
url: import.meta.env.VITE_APP_MESSAGE_PATH + '/close',
method: 'get'
+10
View File
@@ -0,0 +1,10 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { MessageBoxVO } from './types';
export function getMessageBox(): AxiosPromise<MessageBoxVO> {
return request({
url: '/system/message/box',
method: 'get'
});
}
+17
View File
@@ -0,0 +1,17 @@
export interface MessageVO extends BaseEntity {
messageId: number | string;
category: string;
type: string;
source: string;
title: string;
message: string;
content?: string;
data?: Record<string, any> | null;
path?: string;
}
export interface MessageBoxVO {
systemList: MessageVO[];
noticeList: MessageVO[];
workflowList: MessageVO[];
}
+1 -10
View File
@@ -30,7 +30,7 @@
<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">
<el-badge :value="noticeStore.unreadCount.value > 0 ? noticeStore.unreadCount.value : ''" :max="99">
<div class="right-menu-item hover-effect message-trigger"><svg-icon icon-class="message" /></div>
</el-badge>
</template>
@@ -107,7 +107,6 @@ const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const noticeStore = storeToRefs(useNoticeStore());
const newNotice = ref(<number>0);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -158,14 +157,6 @@ const handleCommand = (command: string) => {
commandMap[command]();
}
};
//用深度监听 消息
watch(
() => noticeStore.state.value.notices,
(newVal) => {
newNotice.value = newVal.filter((item: any) => !item.read).length;
},
{ deep: true }
);
</script>
<style lang="scss" scoped>
+15 -2
View File
@@ -31,10 +31,12 @@
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { useNoticeStore } from '@/store/modules/notice';
import router from '@/router';
import { markMessageRead, markMessageReadBatch } from '@/utils/message-read';
import { useUserStore } from '@/store/modules/user';
import { NOTICE_GROUP } from '@/utils/push-message';
const noticeStore = useNoticeStore();
const { readAll } = useNoticeStore();
const userStore = useUserStore();
// 定义变量内容
const state = reactive({
@@ -67,12 +69,23 @@ const emptyDescription = computed(() => {
//点击消息,写入已读
const onNewsClick = async (item: any) => {
currentNewsList.value[item].read = true;
const current = currentNewsList.value[item];
if (current?.messageId) {
markMessageRead(userStore.userId, current.messageId);
noticeStore.markRead(current.messageId);
}
if (current?.path) {
await router.push(current.path);
}
};
const readAll = () => {
const ids = newsList.value
.map((item: any) => item.messageId)
.filter((item: string | number | undefined) => item !== undefined && item !== null);
markMessageReadBatch(userStore.userId, ids);
noticeStore.markReadBatch(ids);
};
</script>
<style lang="scss" scoped>
+6 -2
View File
@@ -19,7 +19,7 @@ import { AppMain, Navbar, Settings, TagsView } from './components';
import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings';
import { NavTypeEnum } from '@/enums/NavTypeEnum';
import { initPush } from '@/utils/push';
import { initMessageBox, initPush } from '@/utils/push';
const settingsStore = useSettingsStore();
const theme = computed(() => settingsStore.theme);
@@ -59,8 +59,12 @@ watchEffect(() => {
const settingRef = ref<InstanceType<typeof Settings>>();
onMounted(() => {
onMounted(async () => {
try {
await initMessageBox();
} finally {
initPush();
}
});
const handleClickOutside = () => {
+57 -8
View File
@@ -1,7 +1,8 @@
import { defineStore } from 'pinia';
import { reactive } from 'vue';
import { computed, reactive } from 'vue';
export interface NoticeItem {
messageId?: string | number;
title?: string;
category?: string;
type?: string;
@@ -11,6 +12,7 @@ export interface NoticeItem {
content?: string;
data?: Record<string, any> | null;
path?: string;
timestamp?: number;
time: string;
}
@@ -19,28 +21,75 @@ export const useNoticeStore = defineStore('notice', () => {
notices: [] as NoticeItem[]
});
const unreadCount = computed(() => state.notices.filter((item) => !item.read).length);
const buildNoticeKey = (notice: NoticeItem) => {
if (notice.messageId !== undefined && notice.messageId !== null) {
return String(notice.messageId);
}
return [notice.type, notice.source, notice.timestamp, notice.message].join(':');
};
const sortNotices = () => {
state.notices.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0));
};
const setNotices = (notices: NoticeItem[]) => {
state.notices = [...notices];
sortNotices();
};
const addNotice = (notice: NoticeItem) => {
state.notices.push(notice);
const key = buildNoticeKey(notice);
const index = state.notices.findIndex((item) => buildNoticeKey(item) === key);
if (index > -1) {
state.notices[index] = {
...state.notices[index],
...notice
};
} else {
state.notices.unshift(notice);
}
sortNotices();
};
const removeNotice = (notice: NoticeItem) => {
state.notices.splice(state.notices.indexOf(notice), 1);
const markRead = (messageId?: string | number) => {
if (messageId === undefined || messageId === null) {
return;
}
const target = state.notices.find((item) => String(item.messageId) === String(messageId));
if (target) {
target.read = true;
}
};
//实现全部已读
const readAll = () => {
state.notices.forEach((item: any) => {
const markReadBatch = (messageIds: Array<string | number>) => {
const idSet = new Set(messageIds.map((item) => String(item)));
state.notices.forEach((item) => {
if (item.messageId !== undefined && item.messageId !== null && idSet.has(String(item.messageId))) {
item.read = true;
}
});
};
const readAll = () => {
markReadBatch(
state.notices
.map((item) => item.messageId)
.filter((item): item is string | number => item !== undefined && item !== null)
);
};
const clearNotice = () => {
state.notices = [];
};
return {
state,
unreadCount,
setNotices,
addNotice,
removeNotice,
markRead,
markReadBatch,
readAll,
clearNotice
};
+45
View File
@@ -0,0 +1,45 @@
import cache from '@/plugins/cache';
const MESSAGE_READ_KEY = 'message_read_ids';
const MAX_READ_IDS = 300;
const buildCacheKey = (userId: string | number) => `${MESSAGE_READ_KEY}:${userId}`;
const normalizeIds = (ids: Array<string | number>) => {
const values = ids
.map((item) => String(item))
.filter((item) => !!item);
return Array.from(new Set(values)).slice(0, MAX_READ_IDS);
};
export const getMessageReadIds = (userId: string | number) => {
if (!userId) {
return [] as string[];
}
const ids = cache.local.getJSON(buildCacheKey(userId));
return Array.isArray(ids) ? normalizeIds(ids) : [];
};
export const isMessageRead = (userId: string | number, messageId?: string | number) => {
if (!userId || messageId === undefined || messageId === null) {
return false;
}
return getMessageReadIds(userId).includes(String(messageId));
};
export const markMessageRead = (userId: string | number, messageId?: string | number) => {
if (!userId || messageId === undefined || messageId === null) {
return [];
}
return markMessageReadBatch(userId, [messageId]);
};
export const markMessageReadBatch = (userId: string | number, messageIds: Array<string | number>) => {
if (!userId) {
return [] as string[];
}
const current = getMessageReadIds(userId);
const next = normalizeIds([...messageIds, ...current]);
cache.local.setJSON(buildCacheKey(userId), next);
return next;
};
+3
View File
@@ -20,6 +20,7 @@ export const NOTICE_GROUP = {
} as const;
export interface PushMessagePayload {
messageId?: string | number;
type?: string;
source?: string;
message?: string;
@@ -36,6 +37,7 @@ export const parsePushMessage = (raw: string): PushMessagePayload => {
return {
type: payload.type ?? PUSH_MESSAGE_TYPE.MESSAGE,
source: payload.source ?? 'backend',
messageId: payload.messageId,
message: payload.message ?? '',
data: payload.data ?? null,
path: payload.path,
@@ -45,6 +47,7 @@ export const parsePushMessage = (raw: string): PushMessagePayload => {
return {
type: PUSH_MESSAGE_TYPE.MESSAGE,
source: 'backend',
messageId: undefined,
message: raw,
data: null,
path: undefined,
+43 -2
View File
@@ -1,17 +1,28 @@
import { getMessageBox } from '@/api/system/message';
import type { MessageVO } from '@/api/system/message/types';
import { useUserStore } from '@/store/modules/user';
import { getToken } from '@/utils/auth';
import { ElNotification } from 'element-plus';
import { useNoticeStore } from '@/store/modules/notice';
import { isMessageRead } from '@/utils/message-read';
import { parsePushMessage, resolveNoticeGroup, resolveNoticeTitle, shouldAppendNotice } from '@/utils/push-message';
let closePushConnection: (() => void) | undefined;
const formatNoticeTime = (timestamp?: number | string) => {
const time = timestamp ? new Date(timestamp) : new Date();
return time.toLocaleString();
};
const appendNotice = (raw: string) => {
const payload = parsePushMessage(raw);
if (!shouldAppendNotice(payload)) {
return;
}
const userId = useUserStore().userId;
const title = resolveNoticeTitle(payload);
useNoticeStore().addNotice({
messageId: payload.messageId,
title,
category: resolveNoticeGroup(payload),
type: payload.type,
@@ -20,8 +31,9 @@ const appendNotice = (raw: string) => {
content: payload.data?.noticeContent,
data: payload.data,
path: payload.path,
read: false,
time: new Date(payload.timestamp ?? Date.now()).toLocaleString()
read: isMessageRead(userId, payload.messageId),
timestamp: payload.timestamp ?? Date.now(),
time: formatNoticeTime(payload.timestamp)
});
ElNotification({
title,
@@ -31,6 +43,25 @@ const appendNotice = (raw: string) => {
});
};
const toNoticeItem = (item: MessageVO) => {
const userId = useUserStore().userId;
const timestamp = item.createTime ? new Date(item.createTime).getTime() : Date.now();
return {
messageId: item.messageId,
title: item.title,
category: resolveNoticeGroup(item),
type: item.type,
source: item.source,
message: item.message ?? '',
content: item.content,
data: item.data ?? null,
path: item.path,
read: isMessageRead(userId, item.messageId),
timestamp,
time: formatNoticeTime(timestamp)
};
};
const buildSseUrl = (path: string) => {
return `${import.meta.env.VITE_APP_BASE_API}${path}?Authorization=Bearer ${getToken()}&clientid=${import.meta.env.VITE_APP_CLIENT_ID}`;
};
@@ -108,6 +139,16 @@ export const initPush = () => {
initSsePush(buildSseUrl(path));
};
export const initMessageBox = async () => {
if (import.meta.env.VITE_APP_MESSAGE_ENABLED === 'false') {
useNoticeStore().clearNotice();
return;
}
const { data } = await getMessageBox();
const notices = [...(data?.systemList ?? []), ...(data?.noticeList ?? []), ...(data?.workflowList ?? [])].map(toNoticeItem);
useNoticeStore().setNotices(notices);
};
export const closePush = () => {
closePushConnection?.();
closePushConnection = undefined;