mirror of
https://github.com/Funkoala14/knowledgebase_influencer.git
synced 2025-06-07 18:58:14 +08:00
361 lines
16 KiB
JavaScript
361 lines
16 KiB
JavaScript
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>
|
||
)}
|
||
</>
|
||
);
|
||
}
|