mirror of
https://github.com/Funkoala14/knowledgebase_influencer.git
synced 2025-06-08 03:08:14 +08:00
[dev]notification center
This commit is contained in:
parent
f4c1d03dc8
commit
c6368b1b4b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/node_modules
|
12784
package-lock.json
generated
12784
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,15 +6,22 @@ import {
|
|||||||
markNotificationAsRead,
|
markNotificationAsRead,
|
||||||
setWebSocketConnected,
|
setWebSocketConnected,
|
||||||
} from '../store/notificationCenter/notificationCenter.slice';
|
} from '../store/notificationCenter/notificationCenter.slice';
|
||||||
|
import {
|
||||||
|
fetchNotifications,
|
||||||
|
markNotificationRead,
|
||||||
|
markAllNotificationsRead,
|
||||||
|
} from '../store/notificationCenter/notificationCenter.thunks';
|
||||||
import RequestDetailSlideOver from '../pages/Permissions/components/RequestDetailSlideOver';
|
import RequestDetailSlideOver from '../pages/Permissions/components/RequestDetailSlideOver';
|
||||||
import { approvePermissionThunk, rejectPermissionThunk } from '../store/permissions/permissions.thunks';
|
import { approvePermissionThunk, rejectPermissionThunk } from '../store/permissions/permissions.thunks';
|
||||||
import { showNotification } from '../store/notification.slice';
|
import { showNotification } from '../store/notification.slice';
|
||||||
import { initWebSocket, acknowledgeNotification, closeWebSocket } from '../services/websocket';
|
import { initWebSocket, acknowledgeNotification, closeWebSocket } from '../services/websocket';
|
||||||
|
import { formatDate } from '../utils/dateUtils';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export default function NotificationCenter({ show, onClose }) {
|
export default function NotificationCenter({ show, onClose }) {
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter);
|
const { notifications, unreadCount, isConnected, loading } = useSelector((state) => state.notificationCenter);
|
||||||
const [selectedRequest, setSelectedRequest] = useState(null);
|
const [selectedRequest, setSelectedRequest] = useState(null);
|
||||||
const [showSlideOver, setShowSlideOver] = useState(false);
|
const [showSlideOver, setShowSlideOver] = useState(false);
|
||||||
const [showResponseInput, setShowResponseInput] = useState(false);
|
const [showResponseInput, setShowResponseInput] = useState(false);
|
||||||
@ -22,9 +29,14 @@ export default function NotificationCenter({ show, onClose }) {
|
|||||||
const [isApproving, setIsApproving] = useState(false);
|
const [isApproving, setIsApproving] = useState(false);
|
||||||
const [responseMessage, setResponseMessage] = useState('');
|
const [responseMessage, setResponseMessage] = useState('');
|
||||||
const { isAuthenticated } = useSelector((state) => state.auth);
|
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 displayedNotifications = showAll ? notifications : notifications.slice(0, 5);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 初始化WebSocket连接
|
// 初始化WebSocket连接
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只有在用户已登录的情况下才连接WebSocket
|
// 只有在用户已登录的情况下才连接WebSocket
|
||||||
@ -56,30 +68,66 @@ export default function NotificationCenter({ show, onClose }) {
|
|||||||
};
|
};
|
||||||
}, [isAuthenticated, isConnected, dispatch]);
|
}, [isAuthenticated, isConnected, dispatch]);
|
||||||
|
|
||||||
|
// 当通知中心显示时,获取最新通知
|
||||||
|
useEffect(() => {
|
||||||
|
if (show && isAuthenticated) {
|
||||||
|
dispatch(fetchNotifications());
|
||||||
|
}
|
||||||
|
}, [show, isAuthenticated, dispatch]);
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
dispatch(clearNotifications());
|
setIsClearingAll(true);
|
||||||
|
// 假设这个操作可能需要一点时间
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(clearNotifications());
|
||||||
|
setIsClearingAll(false);
|
||||||
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllAsRead = () => {
|
const handleMarkAllAsRead = () => {
|
||||||
dispatch(markAllNotificationsAsRead());
|
// 设置正在处理标志
|
||||||
|
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) => {
|
const handleMarkAsRead = (notificationId) => {
|
||||||
dispatch(markNotificationAsRead(notificationId));
|
// 设置正在加载的通知ID
|
||||||
// 同时发送确认消息到服务器
|
setLoadingNotificationId(notificationId);
|
||||||
acknowledgeNotification(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) => {
|
const handleViewDetail = (notification) => {
|
||||||
// 标记为已读
|
navigate('/permissions');
|
||||||
if (!notification.isRead) {
|
// setSelectedRequest(notification);
|
||||||
handleMarkAsRead(notification.id);
|
// setShowSlideOver(true);
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.type === 'permission') {
|
|
||||||
setSelectedRequest(notification);
|
|
||||||
setShowSlideOver(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseSlideOver = () => {
|
const handleCloseSlideOver = () => {
|
||||||
@ -138,24 +186,23 @@ export default function NotificationCenter({ show, onClose }) {
|
|||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<h6 className='mb-0 me-2'>通知中心</h6>
|
<h6 className='mb-0 me-2'>通知中心</h6>
|
||||||
{unreadCount > 0 && <span className='badge bg-danger rounded-pill'>{unreadCount}</span>}
|
{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>
|
||||||
<div className='d-flex gap-3 align-items-center'>
|
<div
|
||||||
|
className={`d-flex gap-3 align-items-center${
|
||||||
|
notifications.length === 0 ? ' d-none' : ' d-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className='btn btn-link text-decoration-none p-0 text-dark'
|
className='btn btn-link text-decoration-none p-0 text-dark'
|
||||||
onClick={handleMarkAllAsRead}
|
onClick={handleMarkAllAsRead}
|
||||||
disabled={unreadCount === 0}
|
disabled={unreadCount === 0 || isMarkingAllAsRead}
|
||||||
>
|
>
|
||||||
全部标为已读
|
全部标为已读
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className='btn btn-link text-decoration-none p-0 text-dark'
|
className='btn btn-link text-decoration-none p-0 text-dark'
|
||||||
onClick={handleClearAll}
|
onClick={handleClearAll}
|
||||||
disabled={notifications.length === 0}
|
disabled={notifications.length === 0 || isClearingAll}
|
||||||
>
|
>
|
||||||
清除所有
|
清除所有
|
||||||
</button>
|
</button>
|
||||||
@ -173,43 +220,46 @@ export default function NotificationCenter({ show, onClose }) {
|
|||||||
<div
|
<div
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`notification-item p-3 border-bottom hover-bg-light ${
|
className={`notification-item p-3 border-bottom hover-bg-light ${
|
||||||
!notification.isRead ? 'bg-light' : ''
|
!notification.is_read ? 'bg-light' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className='d-flex gap-3'>
|
<div className='flex-grow-1'>
|
||||||
<div className='notification-icon'>
|
<div className='d-flex justify-content-between align-items-start'>
|
||||||
<i
|
<h6 className={`mb-1 ${!notification.is_read ? 'fw-bold' : ''}`}>
|
||||||
className={`bi ${notification.icon} ${
|
{notification.title}
|
||||||
!notification.isRead ? 'text-primary' : 'text-secondary'
|
</h6>
|
||||||
} fs-5`}
|
<small className='text-muted'>{formatDate(notification.created_at)}</small>
|
||||||
></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-grow-1'>
|
<p className='mb-1 text-secondary'>{notification.content}</p>
|
||||||
<div className='d-flex justify-content-between align-items-start'>
|
<div className='d-flex gap-2'>
|
||||||
<h6 className={`mb-1 ${!notification.isRead ? 'fw-bold' : ''}`}>
|
{notification.type === 'permission_request' && (
|
||||||
{notification.title}
|
<button
|
||||||
</h6>
|
className='btn btn-sm btn-outline-primary'
|
||||||
<small className='text-muted'>{notification.time}</small>
|
onClick={() => handleViewDetail(notification)}
|
||||||
</div>
|
>
|
||||||
<p className='mb-1 text-secondary'>{notification.content}</p>
|
查看详情
|
||||||
<div className='d-flex gap-2'>
|
</button>
|
||||||
{notification.hasDetail && (
|
)}
|
||||||
<button
|
{!notification.is_read && (
|
||||||
className='btn btn-sm btn-dark'
|
<button
|
||||||
onClick={() => handleViewDetail(notification)}
|
className='btn btn-sm btn-outline-secondary'
|
||||||
>
|
onClick={() => handleMarkAsRead(notification.id)}
|
||||||
查看详情
|
disabled={loadingNotificationId === notification.id}
|
||||||
</button>
|
>
|
||||||
)}
|
{loadingNotificationId === notification.id ? (
|
||||||
{!notification.isRead && (
|
<>
|
||||||
<button
|
<span
|
||||||
className='btn btn-sm btn-outline-secondary'
|
className='spinner-border spinner-border-sm me-1'
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
role='status'
|
||||||
>
|
aria-hidden='true'
|
||||||
标为已读
|
></span>
|
||||||
</button>
|
处理中...
|
||||||
)}
|
</>
|
||||||
</div>
|
) : (
|
||||||
|
'标为已读'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { logoutThunk } from '../store/auth/auth.thunk';
|
import { logoutThunk } from '../store/auth/auth.thunk';
|
||||||
import UserSettingsModal from '../components/UserSettingsModal';
|
import UserSettingsModal from '../components/UserSettingsModal';
|
||||||
import NotificationCenter from '../components/NotificationCenter';
|
import NotificationCenter from '../components/NotificationCenter';
|
||||||
import SvgIcon from '../components/SvgIcon';
|
import SvgIcon from '../components/SvgIcon';
|
||||||
|
import { fetchNotifications } from '../store/notificationCenter/notificationCenter.thunks';
|
||||||
|
|
||||||
export default function HeaderWithNav() {
|
export default function HeaderWithNav() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -32,12 +33,16 @@ export default function HeaderWithNav() {
|
|||||||
// 检查用户是否有管理权限(leader 或 admin)
|
// 检查用户是否有管理权限(leader 或 admin)
|
||||||
const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
|
const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchNotifications());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<nav className='navbar navbar-expand-lg bg-white shadow-sm'>
|
<nav className='navbar navbar-expand-lg bg-white shadow-sm'>
|
||||||
<div className='container-fluid'>
|
<div className='container-fluid'>
|
||||||
<Link className='navbar-brand' to='/'>
|
<Link className='navbar-brand' to='/'>
|
||||||
OOIN 达人智能知识库
|
OOIN 智能知识库
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className='navbar-toggler'
|
className='navbar-toggler'
|
||||||
|
@ -63,7 +63,7 @@ export default function Login() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
||||||
<div className='title text-center h1'>OOIN 达人智能知识库</div>
|
<div className='title text-center h1'>OOIN 智能知识库</div>
|
||||||
<form
|
<form
|
||||||
className='auth-form login-form d-flex flex-column gap-3 align-items-center'
|
className='auth-form login-form d-flex flex-column gap-3 align-items-center'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
@ -132,7 +132,7 @@ export default function Signup() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
||||||
<div className='title text-center h1'>OOIN 达人智能知识库</div>
|
<div className='title text-center h1'>OOIN 智能知识库</div>
|
||||||
<form
|
<form
|
||||||
className='auth-form login-form d-flex flex-column gap-3 align-items-center'
|
className='auth-form login-form d-flex flex-column gap-3 align-items-center'
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
@ -521,6 +521,54 @@ const mockPermissionApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock notifications data
|
||||||
|
const mockNotifications = [
|
||||||
|
{
|
||||||
|
id: "7e383fe6-6776-4609-bfd2-76446593b3b8",
|
||||||
|
type: "permission_approved",
|
||||||
|
icon: "bi-shield-check",
|
||||||
|
title: "权限申请已通过",
|
||||||
|
content: "您对知识库 '管理员个人' 的权限申请已通过",
|
||||||
|
sender: "a9fa3c33-ca28-4ff1-b0ce-49adf0ec66f3",
|
||||||
|
receiver: "33cc280f-7bc6-4eff-b789-8434bb8c1f78",
|
||||||
|
is_read: false,
|
||||||
|
related_resource: "1",
|
||||||
|
created_at: "2025-04-12T04:49:51.724411",
|
||||||
|
hasDetail: true,
|
||||||
|
time: "1小时前"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cad476f6-1b0c-49c3-b36f-5404debf9bc2",
|
||||||
|
type: "permission_updated",
|
||||||
|
icon: "bi-shield",
|
||||||
|
title: "知识库权限更新",
|
||||||
|
content: "管理员已更新您对知识库 '测试' 的权限",
|
||||||
|
sender: "a9fa3c33-ca28-4ff1-b0ce-49adf0ec66f3",
|
||||||
|
receiver: "33cc280f-7bc6-4eff-b789-8434bb8c1f78",
|
||||||
|
is_read: false,
|
||||||
|
related_resource: "29",
|
||||||
|
created_at: "2025-04-12T04:36:43.851494",
|
||||||
|
hasDetail: true,
|
||||||
|
time: "2小时前"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: "system",
|
||||||
|
icon: "bi-info-circle",
|
||||||
|
title: "系统更新通知",
|
||||||
|
content: "系统将在今晚22:00-23:00进行维护更新,请提前保存您的工作",
|
||||||
|
sender: "system",
|
||||||
|
receiver: "all",
|
||||||
|
is_read: true,
|
||||||
|
related_resource: null,
|
||||||
|
created_at: "2025-04-11T14:30:00.000000",
|
||||||
|
hasDetail: false,
|
||||||
|
time: "1天前"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let notifications = [...mockNotifications];
|
||||||
|
|
||||||
// Mock API functions
|
// Mock API functions
|
||||||
export const mockGet = async (url, params = {}) => {
|
export const mockGet = async (url, params = {}) => {
|
||||||
console.log(`[MOCK API] GET ${url}`, params);
|
console.log(`[MOCK API] GET ${url}`, params);
|
||||||
@ -717,7 +765,15 @@ export const mockGet = async (url, params = {}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
// Notifications API
|
||||||
|
if (url === '/notifications/') {
|
||||||
|
await mockDelay();
|
||||||
|
return [...notifications];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case (if no other match)
|
||||||
|
console.warn(`Mock GET for ${url} not implemented`);
|
||||||
|
return { code: 404, message: 'Not found', data: null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockPost = async (url, data, isMultipart = false) => {
|
export const mockPost = async (url, data, isMultipart = false) => {
|
||||||
@ -942,11 +998,46 @@ export const mockPost = async (url, data, isMultipart = false) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark all notifications as read
|
||||||
|
if (url === '/notifications/mark-all-as-read/') {
|
||||||
|
// Update all notifications to be read
|
||||||
|
notifications.forEach(notification => {
|
||||||
|
notification.is_read = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'All notifications marked as read successfully',
|
||||||
|
data: { success: true }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a notification as read
|
||||||
|
if (url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)) {
|
||||||
|
const notificationId = url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)[1];
|
||||||
|
const notificationIndex = notifications.findIndex(n => n.id === notificationId);
|
||||||
|
|
||||||
|
if (notificationIndex !== -1) {
|
||||||
|
notifications[notificationIndex] = {
|
||||||
|
...notifications[notificationIndex],
|
||||||
|
is_read: true
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Notification marked as read successfully',
|
||||||
|
data: { success: true, notification: notifications[notificationIndex] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 404, message: 'Notification not found', data: null };
|
||||||
|
}
|
||||||
|
|
||||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockPut = async (url, data) => {
|
export const mockPut = async (url, data) => {
|
||||||
console.log(`[MOCK API] PUT ${url}`, data);
|
console.log('Mock PUT:', url, data);
|
||||||
|
await mockDelay();
|
||||||
|
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||||
@ -1023,7 +1114,25 @@ export const mockPut = async (url, data) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
// Mark notification as read
|
||||||
|
if (url.match(/\/notifications\/([^\/]+)\/read/)) {
|
||||||
|
const notificationId = url.match(/\/notifications\/([^\/]+)\/read/)[1];
|
||||||
|
const notificationIndex = notifications.findIndex(n => n.id === notificationId);
|
||||||
|
|
||||||
|
if (notificationIndex !== -1) {
|
||||||
|
notifications[notificationIndex] = {
|
||||||
|
...notifications[notificationIndex],
|
||||||
|
is_read: true
|
||||||
|
};
|
||||||
|
return { success: true, notification: notifications[notificationIndex] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 404, message: 'Notification not found', data: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case
|
||||||
|
console.warn(`Mock PUT for ${url} not implemented`);
|
||||||
|
return { code: 404, message: 'Not found', data: null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockDelete = async (url) => {
|
export const mockDelete = async (url) => {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { addNotification, markNotificationAsRead } from '../store/notificationCenter/notificationCenter.slice';
|
import { addNotification, markNotificationAsRead } from '../store/notificationCenter/notificationCenter.slice';
|
||||||
import store from '../store/store'; // 修改为默认导出
|
import store from '../store/store'; // 修改为默认导出
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
const secretKey = import.meta.env.VITE_SECRETKEY;
|
||||||
|
|
||||||
// 从环境变量获取 API URL
|
// 从环境变量获取 API URL
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://81.69.223.133:8008';
|
const API_URL = import.meta.env.VITE_API_URL || 'http://81.69.223.133:8008';
|
||||||
@ -32,13 +35,16 @@ export const initWebSocket = () => {
|
|||||||
try {
|
try {
|
||||||
// 从sessionStorage获取token
|
// 从sessionStorage获取token
|
||||||
const encryptedToken = sessionStorage.getItem('token');
|
const encryptedToken = sessionStorage.getItem('token');
|
||||||
|
let token = '';
|
||||||
if (!encryptedToken) {
|
if (!encryptedToken) {
|
||||||
console.error('No token found, cannot connect to notification service');
|
console.error('No token found, cannot connect to notification service');
|
||||||
reject(new Error('No token found'));
|
reject(new Error('No token found'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (encryptedToken) {
|
||||||
const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${encryptedToken}`;
|
token = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8);
|
||||||
|
}
|
||||||
|
const wsUrl = `${WS_BASE_URL}/ws/notifications/?token=${token}`;
|
||||||
console.log('WebSocket URL:', wsUrl);
|
console.log('WebSocket URL:', wsUrl);
|
||||||
socket = new WebSocket(wsUrl);
|
socket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
@ -1,60 +1,12 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { fetchNotifications, markNotificationRead, markAllNotificationsRead } from './notificationCenter.thunks';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
notifications: [
|
notifications: [],
|
||||||
{
|
unreadCount: 0,
|
||||||
id: 1,
|
|
||||||
type: 'permission',
|
|
||||||
icon: 'bi-shield',
|
|
||||||
title: '新的权限请求',
|
|
||||||
content: '张三请求访问销售数据集',
|
|
||||||
time: '10分钟前',
|
|
||||||
hasDetail: true,
|
|
||||||
isRead: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 'system',
|
|
||||||
icon: 'bi-info-circle',
|
|
||||||
title: '系统更新通知',
|
|
||||||
content: '系统将在今晚23:00进行例行维护',
|
|
||||||
time: '1小时前',
|
|
||||||
hasDetail: false,
|
|
||||||
isRead: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
type: 'permission',
|
|
||||||
icon: 'bi-shield',
|
|
||||||
title: '新的权限请求',
|
|
||||||
content: '李四请求访问用户数据集',
|
|
||||||
time: '2小时前',
|
|
||||||
hasDetail: true,
|
|
||||||
isRead: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
type: 'system',
|
|
||||||
icon: 'bi-exclamation-circle',
|
|
||||||
title: '安全提醒',
|
|
||||||
content: '检测到异常登录行为,请及时查看',
|
|
||||||
time: '3小时前',
|
|
||||||
hasDetail: true,
|
|
||||||
isRead: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
type: 'permission',
|
|
||||||
icon: 'bi-shield',
|
|
||||||
title: '权限变更通知',
|
|
||||||
content: '管理员修改了您的数据访问权限',
|
|
||||||
time: '1天前',
|
|
||||||
hasDetail: true,
|
|
||||||
isRead: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
unreadCount: 5,
|
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
|
loading: 'idle',
|
||||||
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const notificationCenterSlice = createSlice({
|
const notificationCenterSlice = createSlice({
|
||||||
@ -73,7 +25,7 @@ const notificationCenterSlice = createSlice({
|
|||||||
// 将新通知添加到列表的开头
|
// 将新通知添加到列表的开头
|
||||||
state.notifications.unshift(action.payload);
|
state.notifications.unshift(action.payload);
|
||||||
// 如果通知未读,增加未读计数
|
// 如果通知未读,增加未读计数
|
||||||
if (!action.payload.isRead) {
|
if (!action.payload.is_read) {
|
||||||
state.unreadCount += 1;
|
state.unreadCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,15 +33,15 @@ const notificationCenterSlice = createSlice({
|
|||||||
|
|
||||||
markNotificationAsRead: (state, action) => {
|
markNotificationAsRead: (state, action) => {
|
||||||
const notification = state.notifications.find((n) => n.id === action.payload);
|
const notification = state.notifications.find((n) => n.id === action.payload);
|
||||||
if (notification && !notification.isRead) {
|
if (notification && !notification.is_read) {
|
||||||
notification.isRead = true;
|
notification.is_read = true;
|
||||||
state.unreadCount = Math.max(0, state.unreadCount - 1);
|
state.unreadCount = Math.max(0, state.unreadCount - 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
markAllNotificationsAsRead: (state) => {
|
markAllNotificationsAsRead: (state) => {
|
||||||
state.notifications.forEach((notification) => {
|
state.notifications.forEach((notification) => {
|
||||||
notification.isRead = true;
|
notification.is_read = true;
|
||||||
});
|
});
|
||||||
state.unreadCount = 0;
|
state.unreadCount = 0;
|
||||||
},
|
},
|
||||||
@ -109,6 +61,35 @@ const notificationCenterSlice = createSlice({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchNotifications.pending, (state) => {
|
||||||
|
state.loading = 'loading';
|
||||||
|
})
|
||||||
|
.addCase(fetchNotifications.fulfilled, (state, action) => {
|
||||||
|
state.loading = 'succeeded';
|
||||||
|
state.notifications = action.payload;
|
||||||
|
// 计算未读通知数量
|
||||||
|
state.unreadCount = action.payload.filter((notification) => !notification.is_read).length;
|
||||||
|
})
|
||||||
|
.addCase(fetchNotifications.rejected, (state, action) => {
|
||||||
|
state.loading = 'failed';
|
||||||
|
state.error = action.payload?.message || 'Failed to fetch notifications';
|
||||||
|
})
|
||||||
|
.addCase(markNotificationRead.fulfilled, (state, action) => {
|
||||||
|
const notification = state.notifications.find((n) => n.id === action.payload.id);
|
||||||
|
if (notification && !notification.is_read) {
|
||||||
|
notification.is_read = true;
|
||||||
|
state.unreadCount = Math.max(0, state.unreadCount - 1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(markAllNotificationsRead.fulfilled, (state) => {
|
||||||
|
state.notifications.forEach((notification) => {
|
||||||
|
notification.is_read = true;
|
||||||
|
});
|
||||||
|
state.unreadCount = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
50
src/store/notificationCenter/notificationCenter.thunks.js
Normal file
50
src/store/notificationCenter/notificationCenter.thunks.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { get, post } from '../../services/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch notifications from the API
|
||||||
|
*/
|
||||||
|
export const fetchNotifications = createAsyncThunk(
|
||||||
|
'notificationCenter/fetchNotifications',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await get('/notifications/');
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || 'Failed to fetch notifications';
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a notification as read
|
||||||
|
*/
|
||||||
|
export const markNotificationRead = createAsyncThunk(
|
||||||
|
'notificationCenter/markNotificationRead',
|
||||||
|
async (notificationId, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await post(`/notifications/${notificationId}/mark-as-read/`);
|
||||||
|
return { id: notificationId, ...response };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || 'Failed to mark notification as read';
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all notifications as read
|
||||||
|
*/
|
||||||
|
export const markAllNotificationsRead = createAsyncThunk(
|
||||||
|
'notificationCenter/markAllNotificationsRead',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await post('/notifications/mark-all-as-read/');
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || 'Failed to mark all notifications as read';
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user