feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
type: 'like' | 'comment' | 'follow' | 'system';
|
||||
title: string;
|
||||
content?: string;
|
||||
link?: string;
|
||||
relatedId?: number;
|
||||
isRead: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function NotificationIcon({ type }: { type: string }) {
|
||||
const icons: Record<string, string> = {
|
||||
like: '❤️',
|
||||
comment: '💬',
|
||||
follow: '👤',
|
||||
system: '🔔',
|
||||
};
|
||||
return <span className="text-lg">{icons[type] || '🔔'}</span>;
|
||||
}
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiFetch('/notifications');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setNotifications(data.items || []);
|
||||
setUnreadCount(data.unread || 0);
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
async function markRead(id: number) {
|
||||
try {
|
||||
await apiFetch(`/notifications/${id}/read`, { method: 'PATCH' });
|
||||
setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n));
|
||||
setUnreadCount(prev => Math.max(0, prev - 1));
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function markAllRead() {
|
||||
try {
|
||||
await apiFetch('/notifications/read-all', { method: 'PATCH' });
|
||||
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
|
||||
setUnreadCount(0);
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<Skeleton className="h-4 w-32 mb-8" />
|
||||
<Skeleton className="h-64 w-full mb-4" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">通知</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{unreadCount > 0 ? `你有 ${unreadCount} 条未读通知` : '暂无未读通知'}
|
||||
</p>
|
||||
</div>
|
||||
{unreadCount > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={markAllRead}>
|
||||
全部已读
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{notifications.map(n => (
|
||||
<div key={n.id}
|
||||
className={`flex items-start gap-4 p-4 rounded-xl border transition-colors ${
|
||||
n.isRead
|
||||
? 'bg-card border-border'
|
||||
: 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800'
|
||||
}`}>
|
||||
<div className="mt-1"><NotificationIcon type={n.type} /></div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{n.link ? (
|
||||
<Link href={n.link} onClick={() => { if (!n.isRead) markRead(n.id); }}
|
||||
className="text-sm font-medium text-foreground hover:text-brand-600">
|
||||
{n.title}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="text-sm font-medium text-foreground">{n.title}</p>
|
||||
)}
|
||||
{n.content && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{n.content}</p>}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{new Date(n.createdAt).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
{!n.isRead && (
|
||||
<button onClick={() => markRead(n.id)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground shrink-0 px-2 py-1 rounded hover:bg-accent">
|
||||
已读
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{notifications.length === 0 && (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<p className="text-4xl mb-4">🔔</p>
|
||||
<p>暂无通知</p>
|
||||
<p className="text-sm mt-1">点赞、评论或关注你的人会出现在这里</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user