LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 1 | 'use client'; |
LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 2 | |
LaoeGaoci | 502ecae | 2025-06-04 17:54:24 +0800 | [diff] [blame] | 3 | import React, { useEffect, useRef, useState } from 'react'; |
| 4 | import axios from 'axios'; |
| 5 | import { DataTable } from 'primereact/datatable'; |
| 6 | import { Column } from 'primereact/column'; |
| 7 | import { Sidebar } from 'primereact/sidebar'; |
| 8 | import { Button } from 'primereact/button'; |
| 9 | import { Tag } from 'primereact/tag'; |
| 10 | import { Toast } from 'primereact/toast'; |
| 11 | import { Paginator, PaginatorPageChangeEvent } from 'primereact/paginator'; |
| 12 | import { Card } from 'primereact/card'; |
| 13 | import { TabView, TabPanel } from 'primereact/tabview'; // ✅ TabView 导入 |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame^] | 14 | import { useLocalStorage } from '../hook/useLocalStorage'; |
LaoeGaoci | 502ecae | 2025-06-04 17:54:24 +0800 | [diff] [blame] | 15 | import './notification.scss'; |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame^] | 16 | interface User { |
| 17 | Id: number; |
| 18 | } |
LaoeGaoci | 502ecae | 2025-06-04 17:54:24 +0800 | [diff] [blame] | 19 | interface Notification { |
| 20 | notificationId: number; |
| 21 | title: string; |
| 22 | content: string; |
| 23 | createAt: string; |
| 24 | isRead: boolean; |
| 25 | triggeredBy: number; |
| 26 | relatedId: number; |
| 27 | } |
| 28 | |
| 29 | export default function NotificationPage() { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame^] | 30 | const user = useLocalStorage<User>('user'); |
| 31 | const userId: number = user?.Id ?? -1; |
LaoeGaoci | 502ecae | 2025-06-04 17:54:24 +0800 | [diff] [blame] | 32 | const toast = useRef<Toast>(null); |
| 33 | const [notifications, setNotifications] = useState<Notification[]>([]); |
| 34 | const [selectedNotification, setSelectedNotification] = useState<Notification | null>(null); |
| 35 | const [visible, setVisible] = useState<boolean>(false); |
| 36 | const [activeTab, setActiveTab] = useState<number>(0); // ✅ 当前 Tab 下标 |
| 37 | |
| 38 | // 分页相关 |
| 39 | const [first, setFirst] = useState<number>(0); |
| 40 | const [rows, setRows] = useState<number>(5); |
| 41 | const [totalRecords, setTotalRecords] = useState<number>(0); |
| 42 | |
| 43 | // 加载数据 |
| 44 | const fetchNotifications = async () => { |
| 45 | try { |
| 46 | const pageNumber = first / rows + 1; |
| 47 | const res = await axios.get(process.env.PUBLIC_URL + '/notification', { |
LaoeGaoci | d077391 | 2025-06-09 00:38:40 +0800 | [diff] [blame^] | 48 | params: { pageNumber, rows, userId }, |
LaoeGaoci | 502ecae | 2025-06-04 17:54:24 +0800 | [diff] [blame] | 49 | }); |
| 50 | const { records, total } = res.data; |
| 51 | setNotifications(records); |
| 52 | setTotalRecords(total); |
| 53 | } catch (error) { |
| 54 | console.error('无法获取通知数据', error); |
| 55 | toast.current?.show({ severity: 'error', summary: '加载失败', detail: '无法获取通知数据' }); |
| 56 | } |
| 57 | }; |
| 58 | |
| 59 | useEffect(() => { |
| 60 | fetchNotifications(); |
| 61 | }, [first, rows]); // ✅ 加入分页依赖 |
| 62 | |
| 63 | const handleRead = async (id: number) => { |
| 64 | try { |
| 65 | await axios.post(process.env.PUBLIC_URL + '/notification/read', { notificationId: id }); |
| 66 | setNotifications(prev => |
| 67 | prev.map(n => (n.notificationId === id ? { ...n, isRead: true } : n)) |
| 68 | ); |
| 69 | } catch (error) { |
| 70 | console.error('已读失败', error); |
| 71 | toast.current?.show({ severity: 'error', summary: '设置已读失败' }); |
| 72 | } |
| 73 | }; |
| 74 | |
| 75 | const handleDelete = async (id: number) => { |
| 76 | try { |
| 77 | await axios.delete(process.env.PUBLIC_URL + '/notification', { data: { notificationId: id } }); |
| 78 | setNotifications(prev => prev.filter(n => n.notificationId !== id)); |
| 79 | toast.current?.show({ severity: 'success', summary: '删除成功' }); |
| 80 | } catch (error) { |
| 81 | console.error('删除失败', error); |
| 82 | toast.current?.show({ severity: 'error', summary: '删除失败' }); |
| 83 | } |
| 84 | }; |
| 85 | |
| 86 | const openSidebar = (notification: Notification) => { |
| 87 | if (!notification.isRead) handleRead(notification.notificationId); |
| 88 | setSelectedNotification(notification); |
| 89 | setVisible(true); |
| 90 | }; |
| 91 | |
| 92 | const readTemplate = (rowData: Notification) => ( |
| 93 | <Tag value={rowData.isRead ? '已读' : '未读'} severity={rowData.isRead ? undefined : 'warning'} /> |
| 94 | ); |
| 95 | |
| 96 | const actionTemplate = (rowData: Notification) => ( |
| 97 | <div className="actions"> |
| 98 | {!rowData.isRead && ( |
| 99 | <Button |
| 100 | icon="pi pi-check" |
| 101 | text |
| 102 | severity="success" |
| 103 | onClick={() => handleRead(rowData.notificationId)} |
| 104 | tooltip="标记为已读" |
| 105 | /> |
| 106 | )} |
| 107 | <Button icon="pi pi-times" severity="danger" text onClick={() => handleDelete(rowData.notificationId)} /> |
LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 108 | </div> |
| 109 | ); |
LaoeGaoci | 85307e6 | 2025-05-30 23:28:42 +0800 | [diff] [blame] | 110 | |
LaoeGaoci | 502ecae | 2025-06-04 17:54:24 +0800 | [diff] [blame] | 111 | const onPageChange = (e: PaginatorPageChangeEvent) => { |
| 112 | setFirst(e.first); |
| 113 | setRows(e.rows); |
| 114 | }; |
| 115 | |
| 116 | const unreadNotifications = notifications.filter(n => !n.isRead); |
| 117 | const readNotifications = notifications.filter(n => n.isRead); |
| 118 | |
| 119 | return ( |
| 120 | <div className="notification-page"> |
| 121 | <Toast ref={toast} /> |
| 122 | <h2>系统通知</h2> |
| 123 | |
| 124 | <TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}> |
| 125 | <TabPanel header="未读通知"> |
| 126 | <DataTable value={unreadNotifications} paginator={false} emptyMessage="暂无未读通知"> |
| 127 | <Column field="title" header="标题" body={(row) => ( |
| 128 | <span className="title-link" onClick={() => openSidebar(row)}>{row.title}</span> |
| 129 | )} /> |
| 130 | <Column field="createAt" header="时间" /> |
| 131 | <Column field="isRead" header="状态" body={readTemplate} /> |
| 132 | <Column header="操作" style={{ width: '120px', textAlign: 'center' }} body={actionTemplate} /> |
| 133 | </DataTable> |
| 134 | </TabPanel> |
| 135 | <TabPanel header="已读通知"> |
| 136 | <DataTable value={readNotifications} paginator={false} emptyMessage="暂无已读通知"> |
| 137 | <Column field="title" header="标题" body={(row) => ( |
| 138 | <span className="title-link" onClick={() => openSidebar(row)}>{row.title}</span> |
| 139 | )} /> |
| 140 | <Column field="createAt" header="时间" /> |
| 141 | <Column field="isRead" header="状态" body={readTemplate} /> |
| 142 | <Column header="操作" style={{ width: '120px', textAlign: 'center' }} body={actionTemplate} /> |
| 143 | </DataTable> |
| 144 | </TabPanel> |
| 145 | </TabView> |
| 146 | |
| 147 | <Paginator |
| 148 | className="Paginator" |
| 149 | first={first} |
| 150 | rows={rows} |
| 151 | totalRecords={totalRecords} |
| 152 | onPageChange={onPageChange} |
| 153 | rowsPerPageOptions={[5, 10, 20]} |
| 154 | /> |
| 155 | |
| 156 | <Sidebar visible={visible} position="right" onHide={() => setVisible(false)} className="p-sidebar-md"> |
| 157 | {selectedNotification && ( |
| 158 | <Card title={selectedNotification.title} subTitle={selectedNotification.createAt}> |
| 159 | <p className="m-0">{selectedNotification.content}</p> |
| 160 | </Card> |
| 161 | )} |
| 162 | </Sidebar> |
| 163 | </div> |
| 164 | ); |
| 165 | } |