knowledgebase_influencer/src/components/NotificationCenter.jsx

361 lines
16 KiB
JavaScript
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 React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
clearNotifications,
markAllNotificationsAsRead,
markNotificationAsRead,
setWebSocketConnected,
} from '../store/notificationCenter/notificationCenter.slice';
import {
fetchNotifications,
markNotificationRead,
markAllNotificationsRead,
} from '../store/notificationCenter/notificationCenter.thunks';
import RequestDetailSlideOver from '../pages/Permissions/components/RequestDetailSlideOver';
import { approvePermissionThunk, rejectPermissionThunk } from '../store/permissions/permissions.thunks';
import { showNotification } from '../store/notification.slice';
import { initWebSocket, acknowledgeNotification, closeWebSocket } from '../services/websocket';
import { formatDate } from '../utils/dateUtils';
import { useNavigate } from 'react-router-dom';
export default function NotificationCenter({ show, onClose }) {
const [showAll, setShowAll] = useState(false);
const dispatch = useDispatch();
const { notifications, unreadCount, isConnected, loading } = useSelector((state) => state.notificationCenter);
const [selectedRequest, setSelectedRequest] = useState(null);
const [showSlideOver, setShowSlideOver] = useState(false);
const [showResponseInput, setShowResponseInput] = useState(false);
const [currentRequestId, setCurrentRequestId] = useState(null);
const [isApproving, setIsApproving] = useState(false);
const [responseMessage, setResponseMessage] = useState('');
const { isAuthenticated } = useSelector((state) => state.auth);
const [loadingNotificationId, setLoadingNotificationId] = useState(null);
const [isMarkingAllAsRead, setIsMarkingAllAsRead] = useState(false);
const [isClearingAll, setIsClearingAll] = useState(false);
const displayedNotifications = showAll ? notifications : notifications.slice(0, 5);
const navigate = useNavigate();
// 初始化WebSocket连接
useEffect(() => {
// 只有在用户已登录的情况下才连接WebSocket
if (isAuthenticated && !isConnected) {
initWebSocket()
.then(() => {
dispatch(setWebSocketConnected(true));
console.log('Successfully connected to notification WebSocket');
})
.catch((error) => {
console.error('Failed to connect to notification WebSocket:', error);
dispatch(setWebSocketConnected(false));
// 可以在这里显示连接失败的通知
dispatch(
showNotification({
message: '通知服务连接失败,部分功能可能不可用',
type: 'warning',
})
);
});
}
// 组件卸载时关闭WebSocket连接
return () => {
if (isConnected) {
closeWebSocket();
dispatch(setWebSocketConnected(false));
}
};
}, [isAuthenticated, isConnected, dispatch]);
// 当通知中心显示时,获取最新通知
useEffect(() => {
if (show && isAuthenticated) {
dispatch(fetchNotifications());
}
}, [show, isAuthenticated, dispatch]);
const handleClearAll = () => {
setIsClearingAll(true);
// 假设这个操作可能需要一点时间
setTimeout(() => {
dispatch(clearNotifications());
setIsClearingAll(false);
}, 300);
};
const handleMarkAllAsRead = () => {
// 设置正在处理标志
setIsMarkingAllAsRead(true);
// 通过API将所有通知标记为已读成功后更新本地状态
dispatch(markAllNotificationsRead())
.unwrap()
.then(() => {
dispatch(markAllNotificationsAsRead());
})
.catch((error) => {
console.error('Failed to mark all notifications as read:', error);
})
.finally(() => {
setIsMarkingAllAsRead(false);
});
};
const handleMarkAsRead = (notificationId) => {
// 设置正在加载的通知ID
setLoadingNotificationId(notificationId);
// 通过API更新已读状态成功后再更新本地状态
dispatch(markNotificationRead(notificationId))
.unwrap()
.then(() => {
// API调用成功后更新本地状态
dispatch(markNotificationAsRead(notificationId));
// 同时发送确认消息到服务器
acknowledgeNotification(notificationId);
})
.catch((error) => {
console.error('Failed to mark notification as read:', error);
})
.finally(() => {
// 清除加载状态
setLoadingNotificationId(null);
});
};
const handleViewDetail = (notification) => {
navigate('/permissions');
// setSelectedRequest(notification);
// setShowSlideOver(true);
};
const handleCloseSlideOver = () => {
setShowSlideOver(false);
setTimeout(() => {
setSelectedRequest(null);
}, 300);
};
const handleOpenResponseInput = (requestId, approving) => {
setCurrentRequestId(requestId);
setIsApproving(approving);
setShowResponseInput(true);
};
const handleCloseResponseInput = () => {
setShowResponseInput(false);
setCurrentRequestId(null);
setResponseMessage('');
};
const handleProcessRequest = () => {
if (!currentRequestId) return;
const params = {
id: currentRequestId,
responseMessage,
};
if (isApproving) {
dispatch(approvePermissionThunk(params));
} else {
dispatch(rejectPermissionThunk(params));
}
};
if (!show) return null;
return (
<>
<div
className='notification-center card shadow-lg'
style={{
position: 'fixed',
top: '60px',
right: '20px',
width: '400px',
zIndex: 1050,
backgroundColor: 'white',
maxHeight: '80vh',
display: 'flex',
flexDirection: 'column',
}}
>
<div className='card-header bg-white border-0 d-flex justify-content-between align-items-center py-3'>
<div className='d-flex align-items-center'>
<h6 className='mb-0 me-2'>通知中心</h6>
{unreadCount > 0 && <span className='badge bg-danger rounded-pill'>{unreadCount}</span>}
</div>
<div
className={`d-flex gap-3 align-items-center${
notifications.length === 0 ? ' d-none' : ' d-none'
}`}
>
<button
className='btn btn-link text-decoration-none p-0 text-dark'
onClick={handleMarkAllAsRead}
disabled={unreadCount === 0 || isMarkingAllAsRead}
>
全部标为已读
</button>
<button
className='btn btn-link text-decoration-none p-0 text-dark'
onClick={handleClearAll}
disabled={notifications.length === 0 || isClearingAll}
>
清除所有
</button>
<button type='button' className='btn-close' onClick={onClose}></button>
</div>
</div>
<div className='card-body p-0' style={{ overflowY: 'auto' }}>
{displayedNotifications.length === 0 ? (
<div className='text-center py-4 text-muted'>
<i className='bi bi-bell fs-3 d-block mb-2'></i>
<p>暂无通知</p>
</div>
) : (
displayedNotifications.map((notification) => (
<div
key={notification.id}
className={`notification-item p-3 border-bottom hover-bg-light ${
!notification.is_read ? 'bg-light' : ''
}`}
>
<div className='flex-grow-1'>
<div className='d-flex justify-content-between align-items-start'>
<h6 className={`mb-1 ${!notification.is_read ? 'fw-bold' : ''}`}>
{notification.title}
</h6>
<small className='text-muted'>{formatDate(notification.created_at)}</small>
</div>
<p className='mb-1 text-secondary'>{notification.content}</p>
<div className='d-flex gap-2'>
{notification.type === 'permission_request' && (
<button
className='btn btn-sm btn-outline-primary'
onClick={() => handleViewDetail(notification)}
>
查看详情
</button>
)}
{!notification.is_read && (
<button
className='btn btn-sm btn-outline-secondary'
onClick={() => handleMarkAsRead(notification.id)}
disabled={loadingNotificationId === notification.id}
>
{loadingNotificationId === notification.id ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : (
'标为已读'
)}
</button>
)}
</div>
</div>
</div>
))
)}
</div>
{notifications.length > 5 && (
<div className='card-footer bg-white border-0 text-center p-3 mt-auto'>
<button
className='btn btn-link text-decoration-none text-dark'
onClick={() => setShowAll(!showAll)}
>
{showAll ? '收起' : `查看全部通知 (${notifications.length})`}
</button>
</div>
)}
</div>
{/* 使用滑动面板组件 */}
<RequestDetailSlideOver
show={showSlideOver}
onClose={handleCloseSlideOver}
request={selectedRequest}
onApprove={(id) => handleOpenResponseInput(id, true)}
onReject={(id) => handleOpenResponseInput(id, false)}
processingId={currentRequestId}
approveRejectStatus={showResponseInput ? 'loading' : 'idle'}
isApproving={isApproving}
/>
{/* 回复输入弹窗 */}
{showResponseInput && (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>{isApproving ? '批准' : '拒绝'}申请</h5>
<button
type='button'
className='btn-close'
onClick={handleCloseResponseInput}
disabled={showResponseInput}
></button>
</div>
<div className='modal-body'>
<div className='mb-3'>
<label htmlFor='responseMessage' className='form-label'>
审批意见
</label>
<textarea
className='form-control'
id='responseMessage'
rows='3'
value={responseMessage}
onChange={(e) => setResponseMessage(e.target.value)}
placeholder={isApproving ? '请输入批准意见(可选)' : '请输入拒绝理由(可选)'}
></textarea>
</div>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-secondary'
onClick={handleCloseResponseInput}
disabled={showResponseInput}
>
取消
</button>
<button
type='button'
className={`btn ${isApproving ? 'btn-success' : 'btn-danger'}`}
onClick={handleProcessRequest}
disabled={showResponseInput}
>
{showResponseInput ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : isApproving ? (
'确认批准'
) : (
'确认拒绝'
)}
</button>
</div>
</div>
</div>
<div className='modal-backdrop fade show'></div>
</div>
)}
</>
);
}