mirror of
https://github.com/Funkoala14/knowledgebase_influencer.git
synced 2025-06-08 03:08:14 +08:00
311 lines
14 KiB
React
311 lines
14 KiB
React
|
import React, { useState, useEffect } from 'react';
|
||
|
import { useDispatch, useSelector } from 'react-redux';
|
||
|
import {
|
||
|
clearNotifications,
|
||
|
markAllNotificationsAsRead,
|
||
|
markNotificationAsRead,
|
||
|
setWebSocketConnected,
|
||
|
} from '../store/notificationCenter/notificationCenter.slice';
|
||
|
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';
|
||
|
|
||
|
export default function NotificationCenter({ show, onClose }) {
|
||
|
const [showAll, setShowAll] = useState(false);
|
||
|
const dispatch = useDispatch();
|
||
|
const { notifications, unreadCount, isConnected } = 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 displayedNotifications = showAll ? notifications : notifications.slice(0, 5);
|
||
|
|
||
|
// 初始化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]);
|
||
|
|
||
|
const handleClearAll = () => {
|
||
|
dispatch(clearNotifications());
|
||
|
};
|
||
|
|
||
|
const handleMarkAllAsRead = () => {
|
||
|
dispatch(markAllNotificationsAsRead());
|
||
|
};
|
||
|
|
||
|
const handleMarkAsRead = (notificationId) => {
|
||
|
dispatch(markNotificationAsRead(notificationId));
|
||
|
// 同时发送确认消息到服务器
|
||
|
acknowledgeNotification(notificationId);
|
||
|
};
|
||
|
|
||
|
const handleViewDetail = (notification) => {
|
||
|
// 标记为已读
|
||
|
if (!notification.isRead) {
|
||
|
handleMarkAsRead(notification.id);
|
||
|
}
|
||
|
|
||
|
if (notification.type === 'permission') {
|
||
|
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>}
|
||
|
{isConnected ? (
|
||
|
<span className='ms-2 badge bg-success rounded-pill'>已连接</span>
|
||
|
) : (
|
||
|
<span className='ms-2 badge bg-secondary rounded-pill'>未连接</span>
|
||
|
)}
|
||
|
</div>
|
||
|
<div className='d-flex gap-3 align-items-center'>
|
||
|
<button
|
||
|
className='btn btn-link text-decoration-none p-0 text-dark'
|
||
|
onClick={handleMarkAllAsRead}
|
||
|
disabled={unreadCount === 0}
|
||
|
>
|
||
|
全部标为已读
|
||
|
</button>
|
||
|
<button
|
||
|
className='btn btn-link text-decoration-none p-0 text-dark'
|
||
|
onClick={handleClearAll}
|
||
|
disabled={notifications.length === 0}
|
||
|
>
|
||
|
清除所有
|
||
|
</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.isRead ? 'bg-light' : ''
|
||
|
}`}
|
||
|
>
|
||
|
<div className='d-flex gap-3'>
|
||
|
<div className='notification-icon'>
|
||
|
<i
|
||
|
className={`bi ${notification.icon} ${
|
||
|
!notification.isRead ? 'text-primary' : 'text-secondary'
|
||
|
} fs-5`}
|
||
|
></i>
|
||
|
</div>
|
||
|
<div className='flex-grow-1'>
|
||
|
<div className='d-flex justify-content-between align-items-start'>
|
||
|
<h6 className={`mb-1 ${!notification.isRead ? 'fw-bold' : ''}`}>
|
||
|
{notification.title}
|
||
|
</h6>
|
||
|
<small className='text-muted'>{notification.time}</small>
|
||
|
</div>
|
||
|
<p className='mb-1 text-secondary'>{notification.content}</p>
|
||
|
<div className='d-flex gap-2'>
|
||
|
{notification.hasDetail && (
|
||
|
<button
|
||
|
className='btn btn-sm btn-dark'
|
||
|
onClick={() => handleViewDetail(notification)}
|
||
|
>
|
||
|
查看详情
|
||
|
</button>
|
||
|
)}
|
||
|
{!notification.isRead && (
|
||
|
<button
|
||
|
className='btn btn-sm btn-outline-secondary'
|
||
|
onClick={() => handleMarkAsRead(notification.id)}
|
||
|
>
|
||
|
标为已读
|
||
|
</button>
|
||
|
)}
|
||
|
</div>
|
||
|
</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>
|
||
|
)}
|
||
|
</>
|
||
|
);
|
||
|
}
|