update 完成消息盒子功能前后端联动(已读未读在前端浏览器存储)
This commit is contained in:
+1
-4
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user