598 lines
32 KiB
TypeScript
598 lines
32 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { PageInfo, SiteHeader } from "@/components/site-header";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nice/ui/components/card';
|
||
|
|
import { Badge } from '@nice/ui/components/badge';
|
||
|
|
import { Button } from '@nice/ui/components/button';
|
||
|
|
import { Checkbox } from '@nice/ui/components/checkbox';
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nice/ui/components/select';
|
||
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@nice/ui/components/dropdown-menu';
|
||
|
|
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PaginationEllipsis } from '@nice/ui/components/pagination';
|
||
|
|
import {
|
||
|
|
IconBell,
|
||
|
|
IconBellRinging,
|
||
|
|
IconHeart,
|
||
|
|
IconFileText,
|
||
|
|
IconShare,
|
||
|
|
IconSettings,
|
||
|
|
IconClock,
|
||
|
|
IconCheck,
|
||
|
|
IconChecks,
|
||
|
|
IconTrash,
|
||
|
|
IconFilter,
|
||
|
|
IconDots,
|
||
|
|
IconX
|
||
|
|
} from '@tabler/icons-react';
|
||
|
|
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
|
||
|
|
|
||
|
|
// 通知类型定义
|
||
|
|
type NotificationType = 'like' | 'comment' | 'follow' | 'system' | 'article' | 'security';
|
||
|
|
|
||
|
|
interface Notification {
|
||
|
|
id: string;
|
||
|
|
type: NotificationType;
|
||
|
|
title: string;
|
||
|
|
message: string;
|
||
|
|
time: string;
|
||
|
|
isRead: boolean;
|
||
|
|
isSelected?: boolean;
|
||
|
|
data?: {
|
||
|
|
url?: string;
|
||
|
|
articleId?: string;
|
||
|
|
userId?: string;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 模拟通知数据
|
||
|
|
const mockNotifications: Notification[] = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
type: 'like',
|
||
|
|
title: '文章获得点赞',
|
||
|
|
message: '您的文章《Next.js 最佳实践》收到了新的点赞',
|
||
|
|
time: '5分钟前',
|
||
|
|
isRead: false,
|
||
|
|
data: { articleId: '123' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
type: 'comment',
|
||
|
|
title: '新评论',
|
||
|
|
message: '用户李四评论了您的文章《TypeScript 进阶指南》:这篇文章写得非常详细,帮助很大!',
|
||
|
|
time: '1小时前',
|
||
|
|
isRead: false,
|
||
|
|
data: { articleId: '124', userId: 'user_123' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
type: 'follow',
|
||
|
|
title: '新关注者',
|
||
|
|
message: '用户王五关注了您,快去看看他的主页吧',
|
||
|
|
time: '2小时前',
|
||
|
|
isRead: true,
|
||
|
|
data: { userId: 'user_124' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '4',
|
||
|
|
type: 'system',
|
||
|
|
title: '存储空间提醒',
|
||
|
|
message: '您的存储空间使用已达到60%,建议及时清理不必要的文件',
|
||
|
|
time: '6小时前',
|
||
|
|
isRead: false
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '5',
|
||
|
|
type: 'article',
|
||
|
|
title: '文章发布成功',
|
||
|
|
message: '您的文章《React Hooks 深度解析》已成功发布',
|
||
|
|
time: '1天前',
|
||
|
|
isRead: true,
|
||
|
|
data: { articleId: '125' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '6',
|
||
|
|
type: 'security',
|
||
|
|
title: '登录提醒',
|
||
|
|
message: '检测到您的账号在新设备上登录,如非本人操作请及时修改密码',
|
||
|
|
time: '2天前',
|
||
|
|
isRead: false
|
||
|
|
},
|
||
|
|
// 更多模拟数据...
|
||
|
|
...Array.from({ length: 20 }, (_, i) => ({
|
||
|
|
id: `${i + 7}`,
|
||
|
|
type: ['like', 'comment', 'follow', 'system'][i % 4] as NotificationType,
|
||
|
|
title: `通知标题 ${i + 7}`,
|
||
|
|
message: `这是第 ${i + 7} 条通知的详细内容`,
|
||
|
|
time: `${i + 3}天前`,
|
||
|
|
isRead: Math.random() > 0.5
|
||
|
|
}))
|
||
|
|
];
|
||
|
|
|
||
|
|
|
||
|
|
useSetPageInfo({
|
||
|
|
title: '通知中心',
|
||
|
|
subtitle: '查看系统通知、消息以及系统更新等',
|
||
|
|
})
|
||
|
|
// 获取通知类型的图标
|
||
|
|
function getNotificationIcon(type: NotificationType, isRead: boolean) {
|
||
|
|
const iconClass = `h-5 w-5 ${isRead ? 'opacity-60' : ''}`;
|
||
|
|
|
||
|
|
switch (type) {
|
||
|
|
case 'like':
|
||
|
|
return <IconHeart className={`${iconClass} text-red-500`} />;
|
||
|
|
case 'comment':
|
||
|
|
return <IconFileText className={`${iconClass} text-blue-500`} />;
|
||
|
|
case 'follow':
|
||
|
|
return <IconShare className={`${iconClass} text-green-500`} />;
|
||
|
|
case 'system':
|
||
|
|
return <IconSettings className={`${iconClass} text-orange-500`} />;
|
||
|
|
case 'article':
|
||
|
|
return <IconFileText className={`${iconClass} text-purple-500`} />;
|
||
|
|
case 'security':
|
||
|
|
return <IconBellRinging className={`${iconClass} text-red-600`} />;
|
||
|
|
default:
|
||
|
|
return <IconBell className={iconClass} />;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取通知类型的标签
|
||
|
|
function getNotificationTypeLabel(type: NotificationType) {
|
||
|
|
const typeMap: Record<NotificationType, string> = {
|
||
|
|
like: '点赞',
|
||
|
|
comment: '评论',
|
||
|
|
follow: '关注',
|
||
|
|
system: '系统',
|
||
|
|
article: '文章',
|
||
|
|
security: '安全'
|
||
|
|
};
|
||
|
|
return typeMap[type];
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取通知类型的颜色
|
||
|
|
function getNotificationTypeBadgeVariant(type: NotificationType) {
|
||
|
|
const variantMap: Record<NotificationType, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||
|
|
like: 'default',
|
||
|
|
comment: 'secondary',
|
||
|
|
follow: 'outline',
|
||
|
|
system: 'secondary',
|
||
|
|
article: 'outline',
|
||
|
|
security: 'destructive'
|
||
|
|
};
|
||
|
|
return variantMap[type];
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function NotificationPage() {
|
||
|
|
const [notifications, setNotifications] = useState<Notification[]>(mockNotifications);
|
||
|
|
const [selectedNotifications, setSelectedNotifications] = useState<Set<string>>(new Set());
|
||
|
|
const [filterType, setFilterType] = useState<'all' | NotificationType>('all');
|
||
|
|
const [filterRead, setFilterRead] = useState<'all' | 'read' | 'unread'>('all');
|
||
|
|
const [currentPage, setCurrentPage] = useState(1);
|
||
|
|
const [isAllSelected, setIsAllSelected] = useState(false);
|
||
|
|
const pageSize = 7;
|
||
|
|
|
||
|
|
// 当筛选条件改变时重置选择状态
|
||
|
|
useEffect(() => {
|
||
|
|
setSelectedNotifications(new Set());
|
||
|
|
setIsAllSelected(false);
|
||
|
|
setCurrentPage(1);
|
||
|
|
}, [filterType, filterRead]);
|
||
|
|
|
||
|
|
// 过滤通知
|
||
|
|
const filteredNotifications = notifications.filter(notification => {
|
||
|
|
const typeMatch = filterType === 'all' || notification.type === filterType;
|
||
|
|
const readMatch = filterRead === 'all' ||
|
||
|
|
(filterRead === 'read' && notification.isRead) ||
|
||
|
|
(filterRead === 'unread' && !notification.isRead);
|
||
|
|
return typeMatch && readMatch;
|
||
|
|
});
|
||
|
|
|
||
|
|
// 分页
|
||
|
|
const totalPages = Math.ceil(filteredNotifications.length / pageSize);
|
||
|
|
const startIndex = (currentPage - 1) * pageSize;
|
||
|
|
const currentNotifications = filteredNotifications.slice(startIndex, startIndex + pageSize);
|
||
|
|
|
||
|
|
// 统计数据
|
||
|
|
const unreadCount = notifications.filter(n => !n.isRead).length;
|
||
|
|
const selectedCount = isAllSelected ? filteredNotifications.length : selectedNotifications.size;
|
||
|
|
|
||
|
|
// 批量操作
|
||
|
|
const handleSelectAll = () => {
|
||
|
|
if (isAllSelected || selectedCount === filteredNotifications.length) {
|
||
|
|
setSelectedNotifications(new Set());
|
||
|
|
setIsAllSelected(false);
|
||
|
|
} else {
|
||
|
|
setSelectedNotifications(new Set(filteredNotifications.map(n => n.id)));
|
||
|
|
setIsAllSelected(true);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSelectNotification = (id: string) => {
|
||
|
|
const newSelected = new Set(selectedNotifications);
|
||
|
|
if (newSelected.has(id)) {
|
||
|
|
newSelected.delete(id);
|
||
|
|
setIsAllSelected(false);
|
||
|
|
} else {
|
||
|
|
newSelected.add(id);
|
||
|
|
if (newSelected.size === filteredNotifications.length) {
|
||
|
|
setIsAllSelected(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setSelectedNotifications(newSelected);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleMarkAsRead = (ids: string[]) => {
|
||
|
|
const targetIds = isAllSelected ? filteredNotifications.map(n => n.id) : ids;
|
||
|
|
setNotifications(prev => prev.map(n =>
|
||
|
|
targetIds.includes(n.id) ? { ...n, isRead: true } : n
|
||
|
|
));
|
||
|
|
setSelectedNotifications(new Set());
|
||
|
|
setIsAllSelected(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleMarkAsUnread = (ids: string[]) => {
|
||
|
|
const targetIds = isAllSelected ? filteredNotifications.map(n => n.id) : ids;
|
||
|
|
setNotifications(prev => prev.map(n =>
|
||
|
|
targetIds.includes(n.id) ? { ...n, isRead: false } : n
|
||
|
|
));
|
||
|
|
setSelectedNotifications(new Set());
|
||
|
|
setIsAllSelected(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDelete = (ids: string[]) => {
|
||
|
|
const targetIds = isAllSelected ? filteredNotifications.map(n => n.id) : ids;
|
||
|
|
setNotifications(prev => prev.filter(n => !targetIds.includes(n.id)));
|
||
|
|
setSelectedNotifications(new Set());
|
||
|
|
setIsAllSelected(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleNotificationClick = (notification: Notification) => {
|
||
|
|
// 标记为已读
|
||
|
|
if (!notification.isRead) {
|
||
|
|
handleMarkAsRead([notification.id]);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
console.log('Navigate to:', notification.data);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-1 flex-col">
|
||
|
|
|
||
|
|
<div className="@container/main flex flex-1 flex-col gap-6 p-6">
|
||
|
|
{/* 左右布局容器 */}
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-6 gap-6">
|
||
|
|
{/* 左侧:通知统计卡片 */}
|
||
|
|
<div className="lg:col-span-1">
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* 通知统计卡片 */}
|
||
|
|
<Card className="hover:shadow-md transition-shadow">
|
||
|
|
<CardHeader className='mt-1'>
|
||
|
|
<CardTitle className="text-lg">通知统计</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-7">
|
||
|
|
{/* 总通知 */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
|
||
|
|
<IconBell className="h-5 w-5 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium text-muted-foreground">总通知</p>
|
||
|
|
<p className="text-xl font-bold">{notifications.length}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 未读通知 */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
|
||
|
|
<IconBellRinging className="h-5 w-5 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium text-muted-foreground">未读通知</p>
|
||
|
|
<p className="text-xl font-bold">{unreadCount}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 已读通知 */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
|
||
|
|
<IconCheck className="h-5 w-5 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium text-muted-foreground">已读通知</p>
|
||
|
|
<p className="text-xl font-bold">{notifications.length - unreadCount}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 右侧:通知列表 */}
|
||
|
|
<div className="lg:col-span-5">
|
||
|
|
<Card className='py-6'>
|
||
|
|
<CardHeader className="pb-3">
|
||
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<CardTitle className="text-lg">通知列表</CardTitle>
|
||
|
|
{/* 筛选器 */}
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<IconFilter className="h-4 w-4 text-muted-foreground" />
|
||
|
|
<Select value={filterType} onValueChange={(value: any) => setFilterType(value)}>
|
||
|
|
<SelectTrigger className="w-[120px]">
|
||
|
|
<SelectValue placeholder="通知类型" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="all">全部类型</SelectItem>
|
||
|
|
<SelectItem value="like">点赞</SelectItem>
|
||
|
|
<SelectItem value="comment">评论</SelectItem>
|
||
|
|
<SelectItem value="follow">关注</SelectItem>
|
||
|
|
<SelectItem value="system">系统</SelectItem>
|
||
|
|
<SelectItem value="article">文章</SelectItem>
|
||
|
|
<SelectItem value="security">安全</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
|
||
|
|
<Select value={filterRead} onValueChange={(value: any) => setFilterRead(value)}>
|
||
|
|
<SelectTrigger className="w-[120px]">
|
||
|
|
<SelectValue placeholder="阅读状态" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="all">全部状态</SelectItem>
|
||
|
|
<SelectItem value="unread">未读</SelectItem>
|
||
|
|
<SelectItem value="read">已读</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-wrap items-center gap-3">
|
||
|
|
{/* 批量操作工具栏 */}
|
||
|
|
{selectedCount > 0 && (
|
||
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-muted/50 rounded-md">
|
||
|
|
<span className="text-sm text-muted-foreground">
|
||
|
|
已选择 {selectedCount} 项
|
||
|
|
</span>
|
||
|
|
<div className="h-4 w-px bg-border" />
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="ghost"
|
||
|
|
onClick={() => handleMarkAsRead(isAllSelected ? filteredNotifications.map(n => n.id) : Array.from(selectedNotifications))}
|
||
|
|
className="h-7 px-2"
|
||
|
|
>
|
||
|
|
<IconCheck className="h-3.5 w-3.5" />
|
||
|
|
已读
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="ghost"
|
||
|
|
onClick={() => handleMarkAsUnread(isAllSelected ? filteredNotifications.map(n => n.id) : Array.from(selectedNotifications))}
|
||
|
|
className="h-7 px-2"
|
||
|
|
>
|
||
|
|
<IconX className="h-3.5 w-3.5" />
|
||
|
|
未读
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="ghost"
|
||
|
|
onClick={() => handleDelete(isAllSelected ? filteredNotifications.map(n => n.id) : Array.from(selectedNotifications))}
|
||
|
|
className="h-7 px-2 text-destructive hover:text-destructive"
|
||
|
|
>
|
||
|
|
<IconTrash className="h-3.5 w-3.5" />
|
||
|
|
删除
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 操作按钮 */}
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
onClick={handleSelectAll}
|
||
|
|
>
|
||
|
|
<IconChecks className="h-4 w-4" />
|
||
|
|
{isAllSelected ? '取消全选' : '选择全部'}
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => handleMarkAsRead(notifications.filter(n => !n.isRead).map(n => n.id))}
|
||
|
|
disabled={unreadCount === 0}
|
||
|
|
>
|
||
|
|
<IconCheck className="h-4 w-4" />
|
||
|
|
全部已读
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="p-0">
|
||
|
|
{currentNotifications.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||
|
|
<IconBell className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||
|
|
<p className="text-muted-foreground">暂无通知</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="divide-y">
|
||
|
|
{currentNotifications.map((notification) => (
|
||
|
|
<div
|
||
|
|
key={notification.id}
|
||
|
|
className={`flex items-start gap-2 p-3 hover:bg-muted/50 cursor-pointer transition-colors ${!notification.isRead ? 'bg-blue-50/50 dark:bg-blue-950/20' : ''
|
||
|
|
}`}
|
||
|
|
onClick={() => handleNotificationClick(notification)}
|
||
|
|
>
|
||
|
|
{/* 选择框 */}
|
||
|
|
<Checkbox
|
||
|
|
checked={isAllSelected || selectedNotifications.has(notification.id)}
|
||
|
|
onCheckedChange={() => handleSelectNotification(notification.id)}
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
className="mt-1"
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 通知图标 */}
|
||
|
|
<div className="flex-shrink-0 mt-0.5">
|
||
|
|
{getNotificationIcon(notification.type, notification.isRead)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 通知内容 */}
|
||
|
|
<div className="flex-1 min-w-0 space-y-1">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<h4 className={`text-sm font-medium truncate ${notification.isRead ? 'text-muted-foreground' : 'text-foreground'
|
||
|
|
}`}>
|
||
|
|
{notification.title}
|
||
|
|
</h4>
|
||
|
|
<Badge variant={getNotificationTypeBadgeVariant(notification.type)} className="text-xs">
|
||
|
|
{getNotificationTypeLabel(notification.type)}
|
||
|
|
</Badge>
|
||
|
|
{!notification.isRead && (
|
||
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<p className={`text-sm line-clamp-2 ${notification.isRead ? 'text-muted-foreground' : 'text-foreground/80'
|
||
|
|
}`}>
|
||
|
|
{notification.message}
|
||
|
|
</p>
|
||
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||
|
|
<IconClock className="h-3 w-3" />
|
||
|
|
{notification.time}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 操作菜单 */}
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||
|
|
<IconDots className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuItem
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handleMarkAsRead([notification.id]);
|
||
|
|
}}
|
||
|
|
disabled={notification.isRead}
|
||
|
|
>
|
||
|
|
<IconCheck className="h-4 w-4" />
|
||
|
|
标记已读
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handleMarkAsUnread([notification.id]);
|
||
|
|
}}
|
||
|
|
disabled={!notification.isRead}
|
||
|
|
>
|
||
|
|
<IconX className="h-4 w-4" />
|
||
|
|
标记未读
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
handleDelete([notification.id]);
|
||
|
|
}}
|
||
|
|
variant="destructive"
|
||
|
|
>
|
||
|
|
<IconTrash className="h-4 w-4" />
|
||
|
|
删除
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
{/* 分页 */}
|
||
|
|
{totalPages > 1 && (
|
||
|
|
<div className="flex justify-center mt-4">
|
||
|
|
<Pagination>
|
||
|
|
<PaginationContent>
|
||
|
|
<PaginationItem>
|
||
|
|
<PaginationPrevious
|
||
|
|
href="#"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (currentPage > 1) setCurrentPage(currentPage - 1);
|
||
|
|
}}
|
||
|
|
aria-disabled={currentPage === 1}
|
||
|
|
className={currentPage === 1 ? 'pointer-events-none opacity-50' : ''}
|
||
|
|
/>
|
||
|
|
</PaginationItem>
|
||
|
|
|
||
|
|
{/* 页码 */}
|
||
|
|
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
|
||
|
|
let pageNum;
|
||
|
|
if (totalPages <= 7) {
|
||
|
|
pageNum = i + 1;
|
||
|
|
} else if (currentPage <= 4) {
|
||
|
|
pageNum = i + 1;
|
||
|
|
} else if (currentPage >= totalPages - 3) {
|
||
|
|
pageNum = totalPages - 6 + i;
|
||
|
|
} else {
|
||
|
|
pageNum = currentPage - 3 + i;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pageNum === currentPage - 2 && currentPage > 4 && totalPages > 7) {
|
||
|
|
return (
|
||
|
|
<PaginationItem key="ellipsis-1">
|
||
|
|
<PaginationEllipsis />
|
||
|
|
</PaginationItem>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pageNum === currentPage + 2 && currentPage < totalPages - 3 && totalPages > 7) {
|
||
|
|
return (
|
||
|
|
<PaginationItem key="ellipsis-2">
|
||
|
|
<PaginationEllipsis />
|
||
|
|
</PaginationItem>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<PaginationItem key={pageNum}>
|
||
|
|
<PaginationLink
|
||
|
|
href="#"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
setCurrentPage(pageNum);
|
||
|
|
}}
|
||
|
|
isActive={pageNum === currentPage}
|
||
|
|
>
|
||
|
|
{pageNum}
|
||
|
|
</PaginationLink>
|
||
|
|
</PaginationItem>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
|
||
|
|
<PaginationItem>
|
||
|
|
<PaginationNext
|
||
|
|
href="#"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
|
||
|
|
}}
|
||
|
|
aria-disabled={currentPage === totalPages}
|
||
|
|
className={currentPage === totalPages ? 'pointer-events-none opacity-50' : ''}
|
||
|
|
/>
|
||
|
|
</PaginationItem>
|
||
|
|
</PaginationContent>
|
||
|
|
</Pagination>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|