update 完成消息盒子功能前后端联动(已读未读在前端浏览器存储)
This commit is contained in:
+1
-4
@@ -53,10 +53,7 @@ export function register(data: any) {
|
|||||||
*/
|
*/
|
||||||
export function logout() {
|
export function logout() {
|
||||||
closePush();
|
closePush();
|
||||||
if (
|
if (import.meta.env.VITE_APP_MESSAGE_ENABLED === 'true' && import.meta.env.VITE_APP_MESSAGE_TRANSPORT.toLowerCase() === 'sse') {
|
||||||
import.meta.env.VITE_APP_MESSAGE_ENABLED === 'true'
|
|
||||||
&& import.meta.env.VITE_APP_MESSAGE_TRANSPORT.toLowerCase() !== 'websocket'
|
|
||||||
) {
|
|
||||||
request({
|
request({
|
||||||
url: import.meta.env.VITE_APP_MESSAGE_PATH + '/close',
|
url: import.meta.env.VITE_APP_MESSAGE_PATH + '/close',
|
||||||
method: 'get'
|
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>
|
<div>
|
||||||
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
|
||||||
<template #reference>
|
<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>
|
<div class="right-menu-item hover-effect message-trigger"><svg-icon icon-class="message" /></div>
|
||||||
</el-badge>
|
</el-badge>
|
||||||
</template>
|
</template>
|
||||||
@@ -107,7 +107,6 @@ const appStore = useAppStore();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const noticeStore = storeToRefs(useNoticeStore());
|
const noticeStore = storeToRefs(useNoticeStore());
|
||||||
const newNotice = ref(<number>0);
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
|
|
||||||
@@ -158,14 +157,6 @@ const handleCommand = (command: string) => {
|
|||||||
commandMap[command]();
|
commandMap[command]();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//用深度监听 消息
|
|
||||||
watch(
|
|
||||||
() => noticeStore.state.value.notices,
|
|
||||||
(newVal) => {
|
|
||||||
newNotice.value = newVal.filter((item: any) => !item.read).length;
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -31,10 +31,12 @@
|
|||||||
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
<script setup lang="ts" name="layoutBreadcrumbUserNews">
|
||||||
import { useNoticeStore } from '@/store/modules/notice';
|
import { useNoticeStore } from '@/store/modules/notice';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
|
import { markMessageRead, markMessageReadBatch } from '@/utils/message-read';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
import { NOTICE_GROUP } from '@/utils/push-message';
|
import { NOTICE_GROUP } from '@/utils/push-message';
|
||||||
|
|
||||||
const noticeStore = useNoticeStore();
|
const noticeStore = useNoticeStore();
|
||||||
const { readAll } = useNoticeStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// 定义变量内容
|
// 定义变量内容
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -67,12 +69,23 @@ const emptyDescription = computed(() => {
|
|||||||
|
|
||||||
//点击消息,写入已读
|
//点击消息,写入已读
|
||||||
const onNewsClick = async (item: any) => {
|
const onNewsClick = async (item: any) => {
|
||||||
currentNewsList.value[item].read = true;
|
|
||||||
const current = currentNewsList.value[item];
|
const current = currentNewsList.value[item];
|
||||||
|
if (current?.messageId) {
|
||||||
|
markMessageRead(userStore.userId, current.messageId);
|
||||||
|
noticeStore.markRead(current.messageId);
|
||||||
|
}
|
||||||
if (current?.path) {
|
if (current?.path) {
|
||||||
await router.push(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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { AppMain, Navbar, Settings, TagsView } from './components';
|
|||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||||
import { initPush } from '@/utils/push';
|
import { initMessageBox, initPush } from '@/utils/push';
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const theme = computed(() => settingsStore.theme);
|
const theme = computed(() => settingsStore.theme);
|
||||||
@@ -59,8 +59,12 @@ watchEffect(() => {
|
|||||||
|
|
||||||
const settingRef = ref<InstanceType<typeof Settings>>();
|
const settingRef = ref<InstanceType<typeof Settings>>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
initPush();
|
try {
|
||||||
|
await initMessageBox();
|
||||||
|
} finally {
|
||||||
|
initPush();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
|
|
||||||
export interface NoticeItem {
|
export interface NoticeItem {
|
||||||
|
messageId?: string | number;
|
||||||
title?: string;
|
title?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -11,6 +12,7 @@ export interface NoticeItem {
|
|||||||
content?: string;
|
content?: string;
|
||||||
data?: Record<string, any> | null;
|
data?: Record<string, any> | null;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
timestamp?: number;
|
||||||
time: string;
|
time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,28 +21,75 @@ export const useNoticeStore = defineStore('notice', () => {
|
|||||||
notices: [] as NoticeItem[]
|
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) => {
|
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) => {
|
const markRead = (messageId?: string | number) => {
|
||||||
state.notices.splice(state.notices.indexOf(notice), 1);
|
if (messageId === undefined || messageId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = state.notices.find((item) => String(item.messageId) === String(messageId));
|
||||||
|
if (target) {
|
||||||
|
target.read = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//实现全部已读
|
const markReadBatch = (messageIds: Array<string | number>) => {
|
||||||
const readAll = () => {
|
const idSet = new Set(messageIds.map((item) => String(item)));
|
||||||
state.notices.forEach((item: any) => {
|
state.notices.forEach((item) => {
|
||||||
item.read = true;
|
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 = () => {
|
const clearNotice = () => {
|
||||||
state.notices = [];
|
state.notices = [];
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
unreadCount,
|
||||||
|
setNotices,
|
||||||
addNotice,
|
addNotice,
|
||||||
removeNotice,
|
markRead,
|
||||||
|
markReadBatch,
|
||||||
readAll,
|
readAll,
|
||||||
clearNotice
|
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;
|
} as const;
|
||||||
|
|
||||||
export interface PushMessagePayload {
|
export interface PushMessagePayload {
|
||||||
|
messageId?: string | number;
|
||||||
type?: string;
|
type?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -36,6 +37,7 @@ export const parsePushMessage = (raw: string): PushMessagePayload => {
|
|||||||
return {
|
return {
|
||||||
type: payload.type ?? PUSH_MESSAGE_TYPE.MESSAGE,
|
type: payload.type ?? PUSH_MESSAGE_TYPE.MESSAGE,
|
||||||
source: payload.source ?? 'backend',
|
source: payload.source ?? 'backend',
|
||||||
|
messageId: payload.messageId,
|
||||||
message: payload.message ?? '',
|
message: payload.message ?? '',
|
||||||
data: payload.data ?? null,
|
data: payload.data ?? null,
|
||||||
path: payload.path,
|
path: payload.path,
|
||||||
@@ -45,6 +47,7 @@ export const parsePushMessage = (raw: string): PushMessagePayload => {
|
|||||||
return {
|
return {
|
||||||
type: PUSH_MESSAGE_TYPE.MESSAGE,
|
type: PUSH_MESSAGE_TYPE.MESSAGE,
|
||||||
source: 'backend',
|
source: 'backend',
|
||||||
|
messageId: undefined,
|
||||||
message: raw,
|
message: raw,
|
||||||
data: null,
|
data: null,
|
||||||
path: undefined,
|
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 { getToken } from '@/utils/auth';
|
||||||
import { ElNotification } from 'element-plus';
|
import { ElNotification } from 'element-plus';
|
||||||
import { useNoticeStore } from '@/store/modules/notice';
|
import { useNoticeStore } from '@/store/modules/notice';
|
||||||
|
import { isMessageRead } from '@/utils/message-read';
|
||||||
import { parsePushMessage, resolveNoticeGroup, resolveNoticeTitle, shouldAppendNotice } from '@/utils/push-message';
|
import { parsePushMessage, resolveNoticeGroup, resolveNoticeTitle, shouldAppendNotice } from '@/utils/push-message';
|
||||||
|
|
||||||
let closePushConnection: (() => void) | undefined;
|
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 appendNotice = (raw: string) => {
|
||||||
const payload = parsePushMessage(raw);
|
const payload = parsePushMessage(raw);
|
||||||
if (!shouldAppendNotice(payload)) {
|
if (!shouldAppendNotice(payload)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const userId = useUserStore().userId;
|
||||||
const title = resolveNoticeTitle(payload);
|
const title = resolveNoticeTitle(payload);
|
||||||
useNoticeStore().addNotice({
|
useNoticeStore().addNotice({
|
||||||
|
messageId: payload.messageId,
|
||||||
title,
|
title,
|
||||||
category: resolveNoticeGroup(payload),
|
category: resolveNoticeGroup(payload),
|
||||||
type: payload.type,
|
type: payload.type,
|
||||||
@@ -20,8 +31,9 @@ const appendNotice = (raw: string) => {
|
|||||||
content: payload.data?.noticeContent,
|
content: payload.data?.noticeContent,
|
||||||
data: payload.data,
|
data: payload.data,
|
||||||
path: payload.path,
|
path: payload.path,
|
||||||
read: false,
|
read: isMessageRead(userId, payload.messageId),
|
||||||
time: new Date(payload.timestamp ?? Date.now()).toLocaleString()
|
timestamp: payload.timestamp ?? Date.now(),
|
||||||
|
time: formatNoticeTime(payload.timestamp)
|
||||||
});
|
});
|
||||||
ElNotification({
|
ElNotification({
|
||||||
title,
|
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) => {
|
const buildSseUrl = (path: string) => {
|
||||||
return `${import.meta.env.VITE_APP_BASE_API}${path}?Authorization=Bearer ${getToken()}&clientid=${import.meta.env.VITE_APP_CLIENT_ID}`;
|
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));
|
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 = () => {
|
export const closePush = () => {
|
||||||
closePushConnection?.();
|
closePushConnection?.();
|
||||||
closePushConnection = undefined;
|
closePushConnection = undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user