|
|
@@ -0,0 +1,279 @@
|
|
|
+import React, { useState, useEffect, useRef } from 'react';
|
|
|
+import { Bell, X, Eye } from 'lucide-react';
|
|
|
+import { Popover } from 'antd';
|
|
|
+import { in_site, type NotifyMessageVO } from '../api/notification/in_site';
|
|
|
+import { getUserId } from '../utils/auth';
|
|
|
+import { systemAttributeApi } from '../api/systemAttribute';
|
|
|
+import { connectWebsocket, closeWebsocket } from '../utils/webSocket';
|
|
|
+import { dateFormatter } from '../utils/formatTime';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import { notification } from 'antd';
|
|
|
+
|
|
|
+export default function MessageNotification() {
|
|
|
+ const [unreadCount, setUnreadCount] = useState(0);
|
|
|
+ const [messageList, setMessageList] = useState<NotifyMessageVO[]>([]);
|
|
|
+ const [popoverVisible, setPopoverVisible] = useState(false);
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const wsInitialized = useRef(false);
|
|
|
+
|
|
|
+ // 获取未读消息数量
|
|
|
+ const getUnreadCount = async () => {
|
|
|
+ try {
|
|
|
+ const count = await in_site.getUnreadCount();
|
|
|
+ setUnreadCount(count || 0);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取未读消息数量失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取未读消息列表
|
|
|
+ const getUnreadList = async () => {
|
|
|
+ try {
|
|
|
+ const list = await in_site.getUnreadList();
|
|
|
+ setMessageList(list || []);
|
|
|
+ // 同时更新未读数量
|
|
|
+ setUnreadCount(list?.length || 0);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取未读消息列表失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 跳转到站内信页面
|
|
|
+ const goToMessagePage = () => {
|
|
|
+ setPopoverVisible(false);
|
|
|
+ // 通过 sessionStorage 传递参数,让 Dashboard 自动切换到站内信
|
|
|
+ sessionStorage.setItem('navigateToMenu', JSON.stringify({
|
|
|
+ menu: 'notificationManagement',
|
|
|
+ subMenu: 'webmail'
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 如果当前已经在 dashboard 页面,触发自定义事件来切换菜单
|
|
|
+ if (window.location.pathname === '/dashboard') {
|
|
|
+ // 触发自定义事件,通知 Dashboard 切换菜单
|
|
|
+ window.dispatchEvent(new CustomEvent('switchToMenu', {
|
|
|
+ detail: { menu: 'notificationManagement', subMenu: 'webmail' }
|
|
|
+ }));
|
|
|
+ } else {
|
|
|
+ // 如果不在 dashboard 页面,导航过去
|
|
|
+ navigate('/dashboard');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 初始化 WebSocket
|
|
|
+ const initWebSocket = async () => {
|
|
|
+ if (wsInitialized.current) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const userId = getUserId();
|
|
|
+ if (!userId) {
|
|
|
+ console.warn('未获取到 userId,不建立 WebSocket 连接');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 获取 WebSocket 地址配置
|
|
|
+ const addressData = await systemAttributeApi.getIsSystemAttributeByKey('sys.websocket.address');
|
|
|
+ const url = addressData.sysAttrValue;
|
|
|
+
|
|
|
+ // 判断是否为本地开发环境
|
|
|
+ const isLocalDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
|
+ const baseAddress = isLocalDev ? 'ws://192.168.0.10:48080' : url;
|
|
|
+
|
|
|
+ const wsUrl = `${baseAddress}/websocket/messSend/${userId}`;
|
|
|
+
|
|
|
+ connectWebsocket(
|
|
|
+ wsUrl,
|
|
|
+ { w: 'S' },
|
|
|
+ (msg) => {
|
|
|
+ console.log('接收消息:', msg);
|
|
|
+
|
|
|
+ if (msg !== 'heartbeat' && msg !== 'pong') {
|
|
|
+ try {
|
|
|
+ const parsedMsg = JSON.parse(msg);
|
|
|
+ const content = parsedMsg.templateContent || parsedMsg.content || '';
|
|
|
+
|
|
|
+ // 显示通知
|
|
|
+ notification.info({
|
|
|
+ message: '新消息提醒',
|
|
|
+ description: (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ maxHeight: '130px',
|
|
|
+ overflow: 'hidden',
|
|
|
+ textOverflow: 'ellipsis',
|
|
|
+ display: '-webkit-box',
|
|
|
+ WebkitLineClamp: 2,
|
|
|
+ WebkitBoxOrient: 'vertical',
|
|
|
+ wordBreak: 'break-word'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {content.replace(/<[^>]*>/g, '')}
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ duration: 8,
|
|
|
+ onClick: () => {
|
|
|
+ goToMessagePage();
|
|
|
+ },
|
|
|
+ placement: 'bottomRight',
|
|
|
+ });
|
|
|
+
|
|
|
+ // 立即刷新未读消息数量(延迟一小段时间确保后端已处理)
|
|
|
+ setTimeout(() => {
|
|
|
+ getUnreadCount();
|
|
|
+ }, 500);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('消息解析失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ (err) => {
|
|
|
+ console.error('WebSocket 错误:', err);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ wsInitialized.current = true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('初始化 WebSocket 失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 组件挂载时初始化
|
|
|
+ useEffect(() => {
|
|
|
+ // 首次加载未读消息数量
|
|
|
+ getUnreadCount();
|
|
|
+
|
|
|
+ // 初始化 WebSocket
|
|
|
+ initWebSocket();
|
|
|
+
|
|
|
+ // 监听消息已读事件,立即更新未读数量
|
|
|
+ const handleMessageRead = () => {
|
|
|
+ console.log('收到消息已读事件,更新未读数量');
|
|
|
+ getUnreadCount();
|
|
|
+ };
|
|
|
+
|
|
|
+ window.addEventListener('messageRead', handleMessageRead);
|
|
|
+
|
|
|
+ // 轮询刷新未读消息数量(每30秒)
|
|
|
+ const pollInterval = setInterval(() => {
|
|
|
+ const userId = getUserId();
|
|
|
+ if (userId) {
|
|
|
+ getUnreadCount();
|
|
|
+ } else {
|
|
|
+ setUnreadCount(0);
|
|
|
+ }
|
|
|
+ }, 1000 * 30);
|
|
|
+
|
|
|
+ // 组件卸载时清理
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener('messageRead', handleMessageRead);
|
|
|
+ clearInterval(pollInterval);
|
|
|
+ closeWebsocket();
|
|
|
+ wsInitialized.current = false;
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 当点击消息图标时,获取消息列表
|
|
|
+ const handleIconClick = () => {
|
|
|
+ if (!popoverVisible) {
|
|
|
+ // 打开弹窗时,同时更新未读列表和数量
|
|
|
+ getUnreadList();
|
|
|
+ getUnreadCount();
|
|
|
+ }
|
|
|
+ setPopoverVisible(!popoverVisible);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 消息列表内容
|
|
|
+ const messageContent = (
|
|
|
+ <div className="w-96" style={{ maxHeight: 'calc(100vh - 100px)', display: 'flex', flexDirection: 'column' }}>
|
|
|
+ <div className="p-4 border-b border-gray-200 flex-shrink-0">
|
|
|
+ <div className="flex items-center justify-between gap-4">
|
|
|
+ <h3 className="text-base font-semibold text-gray-900 flex-1">通知消息</h3>
|
|
|
+ <button
|
|
|
+ onClick={() => setPopoverVisible(false)}
|
|
|
+ className="p-1 hover:bg-gray-100 rounded transition-colors flex-shrink-0"
|
|
|
+ >
|
|
|
+ <X className="w-4 h-4 text-gray-500" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex-1 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
|
|
|
+ {messageList.length === 0 ? (
|
|
|
+ <div className="flex flex-col items-center justify-center py-12 text-gray-400">
|
|
|
+ <Bell className="w-12 h-12 mb-2 opacity-50" />
|
|
|
+ <p className="text-sm">暂无未读消息</p>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="divide-y divide-gray-100">
|
|
|
+ {messageList.map((item) => (
|
|
|
+ <div
|
|
|
+ key={item.id}
|
|
|
+ className="p-4 hover:bg-gray-50 transition-colors cursor-pointer"
|
|
|
+ onClick={() => {
|
|
|
+ goToMessagePage();
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div className="flex items-start gap-3">
|
|
|
+ <div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
|
|
+ <Bell className="w-5 h-5 text-blue-600" />
|
|
|
+ </div>
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
+ <div className="flex items-start justify-between gap-2 mb-1">
|
|
|
+ <p className="text-sm font-medium text-gray-900 line-clamp-2">
|
|
|
+ {item.templateNickname || '系统'}:{item.templateContent || item.title}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <p className="text-xs text-gray-500">
|
|
|
+ {item.createTime ? dateFormatter(item.createTime) : ''}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {messageList.length > 0 && (
|
|
|
+ <div className="p-4 border-t border-gray-200 flex-shrink-0">
|
|
|
+ <button
|
|
|
+ onClick={goToMessagePage}
|
|
|
+ className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
|
|
+ >
|
|
|
+ <Eye className="w-4 h-4 text-white" />
|
|
|
+ <span className="text-white">查看全部</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Popover
|
|
|
+ content={messageContent}
|
|
|
+ trigger="click"
|
|
|
+ open={popoverVisible}
|
|
|
+ onOpenChange={setPopoverVisible}
|
|
|
+ placement="bottomRight"
|
|
|
+ overlayClassName="message-notification-popover"
|
|
|
+ overlayStyle={{ maxHeight: 'calc(100vh - 100px)', overflow: 'hidden' }}
|
|
|
+ >
|
|
|
+ <div className="relative">
|
|
|
+ <button
|
|
|
+ onClick={handleIconClick}
|
|
|
+ className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
|
|
|
+ title="消息通知"
|
|
|
+ >
|
|
|
+ <Bell className="w-5 h-5 text-gray-600" />
|
|
|
+ {unreadCount > 0 && (
|
|
|
+ <span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-semibold">
|
|
|
+ {unreadCount > 99 ? '99+' : unreadCount}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </Popover>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|