Files
ai-agent-admin/web/apps/web-ele/src/composables/useNotification.ts
T

405 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { WebSocketManager } from '#/api/core/websocket';
/**
* 消息通知 Composable
* 集成 WebSocket 实时推送、消息和公告 API
*/
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAccessStore } from '@vben/stores';
import {
getUnreadAnnouncementCountApi,
getUserAnnouncementListApi,
markAnnouncementReadApi,
} from '#/api/core/announcement';
import {
clearReadMessagesApi,
getMessageListApi,
getUnreadCountApi,
markAllAsReadApi,
markAsReadApi,
} from '#/api/core/message';
import { createNotificationWebSocket } from '#/api/core/websocket';
import { $t } from '#/locales';
// 通知项类型
export interface NotificationItem {
id: string;
avatar: string;
title: string;
message: string;
date: string;
isRead: boolean;
linkType?: string;
linkId?: string;
priority?: number;
isTop?: boolean;
senderId?: string;
senderName?: string;
}
// 公告项类型
export interface AnnouncementItem {
id: string;
title: string;
summary: string;
content: string;
date: string;
isRead: boolean;
priority: number;
isTop: boolean;
publisherName: string;
}
// WebSocket 连接状态
const wsConnected = ref(false);
let wsManagerInstance: null | WebSocketManager = null;
// 消息数据
const notifications = ref<NotificationItem[]>([]);
const messageUnreadCount = ref(0);
const unreadByType = ref<Record<string, number>>({});
// 公告数据
const announcements = ref<AnnouncementItem[]>([]);
const announcementUnreadCount = ref(0);
// 当前激活的 Tab
const activeTab = ref<'announcement' | 'chat' | 'message'>('message');
// 消息类型图标映射
const typeAvatarMap: Record<string, string> = {
system: 'https://avatar.vercel.sh/system?text=SYS',
workflow: 'https://avatar.vercel.sh/workflow?text=WF',
todo: 'https://avatar.vercel.sh/todo?text=TD',
announcement: 'https://avatar.vercel.sh/announcement?text=AN',
};
export function useNotification() {
const router = useRouter();
const accessStore = useAccessStore();
// 总未读数量
const totalUnreadCount = computed(
() => messageUnreadCount.value + announcementUnreadCount.value,
);
// 是否显示红点
const showDot = computed(() => totalUnreadCount.value > 0);
// 加载消息列表
async function loadMessages() {
try {
const res = await getMessageListApi({ page: 1, pageSize: 10 });
notifications.value = (res.items || []).map((msg) => ({
id: msg.id,
avatar: typeAvatarMap[msg.msg_type] ?? typeAvatarMap.system!,
title: msg.title,
message: msg.content,
date: formatDate(msg.created_at),
isRead: msg.status === 'read',
linkType: msg.link_type,
linkId: msg.link_id,
senderId: msg.sender_id,
senderName: msg.sender_name,
}));
} catch (error) {
console.error('加载消息失败:', error);
}
}
// 加载未读数量
async function loadUnreadCount() {
try {
const res = await getUnreadCountApi();
messageUnreadCount.value = res.total;
unreadByType.value = res.by_type;
} catch (error) {
console.error('加载未读数量失败:', error);
}
}
// 加载公告列表
async function loadAnnouncements() {
try {
const res = await getUserAnnouncementListApi({ page: 1, pageSize: 10 });
announcements.value = (res.items || []).map((item) => ({
id: item.id,
title: item.title,
summary: item.summary,
content: item.content,
date: formatDate(item.publish_time || ''),
isRead: item.is_read,
priority: item.priority,
isTop: item.is_top,
publisherName: item.publisher_name,
}));
} catch (error) {
console.error('加载公告失败:', error);
}
}
// 加载公告未读数量
async function loadAnnouncementUnreadCount() {
try {
const res = await getUnreadAnnouncementCountApi();
announcementUnreadCount.value = res.count;
} catch (error) {
console.error('加载公告未读数量失败:', error);
}
}
// 标记消息已读并从列表中移除
async function markAsRead(item: NotificationItem) {
// 先跳转
handleNavigate(item);
// 从列表中移除
const index = notifications.value.findIndex((n) => n.id === item.id);
if (index !== -1) {
notifications.value.splice(index, 1);
}
// 如果未读,调用API标记已读
if (!item.isRead) {
try {
await markAsReadApi(item.id as string);
messageUnreadCount.value = Math.max(0, messageUnreadCount.value - 1);
} catch (error) {
console.error('标记已读失败:', error);
}
}
}
// 标记公告已读并从列表中移除
async function markAnnouncementAsRead(item: AnnouncementItem) {
// 先跳转
viewAnnouncementDetail(item);
// 从列表中移除
const index = announcements.value.findIndex((a) => a.id === item.id);
if (index !== -1) {
announcements.value.splice(index, 1);
}
// 如果未读,调用API标记已读
if (!item.isRead) {
try {
await markAnnouncementReadApi(item.id);
announcementUnreadCount.value = Math.max(
0,
announcementUnreadCount.value - 1,
);
} catch (error) {
console.error('标记公告已读失败:', error);
}
}
}
// 标记全部消息已读
async function markAllAsRead() {
try {
await markAllAsReadApi();
notifications.value.forEach((item) => (item.isRead = true));
messageUnreadCount.value = 0;
unreadByType.value = {};
} catch (error) {
console.error('标记全部已读失败:', error);
}
}
// 清空已读消息
async function clearReadMessages() {
try {
await clearReadMessagesApi();
// 重新加载消息列表和未读数量
await loadMessages();
await loadUnreadCount();
} catch (error) {
console.error('清空已读消息失败:', error);
}
}
// 跳转到消息关联页面
function handleNavigate(item: NotificationItem) {
const linkType = item.linkType;
const linkId = item.linkId;
if (!linkType || !linkId) return;
// 根据关联类型跳转(新开tab
let path = '';
switch (linkType) {
case 'announcement': {
path = `/message/announcement-list`;
break;
}
case 'workflow_instance': {
path = `/ai-platform/workflow-runs?id=${linkId}`;
break;
}
case 'workflow_task': {
path = `/ai-platform/workflow-runs?id=${linkId}`;
break;
}
// No default
}
if (path) {
const url = router.resolve(path).href;
window.open(url, '_blank');
}
}
// 查看公告详情
function viewAnnouncementDetail(_item: AnnouncementItem) {
// 跳转到公告列表页(新开tab
const url = router.resolve(`/message/announcement-list`).href;
window.open(url, '_blank');
}
// 查看全部(根据当前 Tab 跳转)
function viewAllMessages() {
if (activeTab.value === 'announcement') {
router.push('/message/announcement-list');
} else {
router.push('/message/list');
}
}
// 连接 WebSocket
function connectWebSocket() {
const token = accessStore.accessToken;
if (!token) return;
// 使用统一的 WebSocketManager
wsManagerInstance = createNotificationWebSocket({
onOpen: () => {
wsConnected.value = true;
// 发送订阅消息
wsManagerInstance?.send({ type: 'subscribe' });
},
onMessage: (message) => {
handleWebSocketMessage(message);
},
onClose: () => {
wsConnected.value = false;
},
onError: () => {
wsConnected.value = false;
},
});
wsManagerInstance.connect().catch((error) => {
console.error('WebSocket 连接失败:', error);
});
}
// 处理 WebSocket 消息
function handleWebSocketMessage(data: any) {
if (data.type === 'notification') {
// 收到新通知
const msgData = data.data;
const newNotification: NotificationItem = {
id: msgData.id,
avatar: typeAvatarMap[msgData.msg_type] ?? typeAvatarMap.system!,
title: msgData.title,
message: msgData.content,
date: $t('message.drawer.justNow'),
isRead: false,
linkType: msgData.link_type,
linkId: msgData.link_id,
senderId: msgData.sender_id,
senderName: msgData.sender_name,
};
// 添加到列表头部
notifications.value.unshift(newNotification);
// 只保留最近10条
if (notifications.value.length > 10) {
notifications.value.pop();
}
// 更新未读数量
messageUnreadCount.value += 1;
} else if (data.type === 'announcement') {
// 收到新公告推送,刷新公告列表和未读数
loadAnnouncements();
loadAnnouncementUnreadCount();
}
}
// 断开 WebSocket
function disconnectWebSocket() {
if (wsManagerInstance) {
wsManagerInstance.close();
wsManagerInstance = null;
}
}
// 格式化日期
function formatDate(dateStr: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60_000) return $t('message.drawer.justNow');
if (diff < 3_600_000)
return $t('message.drawer.minutesAgo', {
count: Math.floor(diff / 60_000),
});
if (diff < 86_400_000)
return $t('message.drawer.hoursAgo', {
count: Math.floor(diff / 3_600_000),
});
if (diff < 604_800_000)
return $t('message.drawer.daysAgo', {
count: Math.floor(diff / 86_400_000),
});
return date.toLocaleDateString();
}
// 初始化
function init() {
loadMessages();
loadUnreadCount();
loadAnnouncements();
loadAnnouncementUnreadCount();
connectWebSocket();
}
// 清理
function cleanup() {
disconnectWebSocket();
}
return {
// 消息
notifications,
messageUnreadCount,
unreadByType,
// 公告
announcements,
announcementUnreadCount,
// 通用
activeTab,
totalUnreadCount,
showDot,
wsConnected,
// 方法
loadMessages,
loadUnreadCount,
loadAnnouncements,
loadAnnouncementUnreadCount,
markAsRead,
markAnnouncementAsRead,
markAllAsRead,
clearReadMessages,
viewAllMessages,
init,
cleanup,
};
}