feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { Skeleton } from '@/components/ui/skeleton';
interface PendingComment {
id: number;
content: string;
status: string;
createdAt: string;
user: { id: number; nickname: string; avatar: string | null };
post: { id: number; title: string };
}
export default function AdminCommentsPage() {
const [comments, setComments] = useState<PendingComment[]>([]);
const [loading, setLoading] = useState(true);
const [tab, setTab] = useState<'pending' | 'approved' | 'rejected'>('pending');
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api/v1';
function headers() {
const t = localStorage.getItem('adminToken');
return { 'Content-Type': 'application/json', ...(t ? { Authorization: `Bearer ${t}` } : {}) };
}
const load = useCallback(async () => {
setLoading(true);
try {
const url = tab === 'pending'
? '/admin/comments/pending'
: tab === 'approved'
? '/admin/comments?status=PUBLISHED'
: '/admin/comments?status=REJECTED';
const res = await fetch(`${base}${url}`, { headers: headers() });
if (res.ok) {
const data = await res.json();
setComments(data.items || []);
}
} catch (e) { console.error(e) }
setLoading(false);
}, [tab, base]);
useEffect(() => { load(); }, [load]);
async function approve(id: number) {
try {
const res = await fetch(`${base}/admin/comments/${id}/approve`, {
method: 'PUT', headers: headers(),
});
if (res.ok) setComments(prev => prev.filter(c => c.id !== id));
} catch (e) { console.error(e) }
}
async function reject(id: number) {
const reason = prompt('请输入拒绝原因:');
if (!reason) return;
try {
const res = await fetch(`${base}/admin/comments/${id}/reject`, {
method: 'PUT', headers: headers(), body: JSON.stringify({ reason }),
});
if (res.ok) setComments(prev => prev.filter(c => c.id !== id));
} catch (e) { console.error(e) }
}
return (
<>
<div className="border-b border-border bg-card px-4 py-4">
<h1 className="text-2xl font-bold text-foreground"></h1>
</div>
<div className="max-w-4xl mx-auto p-6">
<div className="flex gap-1 mb-6 bg-muted rounded-lg p-1">
{(['pending', 'approved', 'rejected'] as const).map(t => (
<button key={t} onClick={() => setTab(t)}
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${
tab === t ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
}`}>
{t === 'pending' ? '待审核' : t === 'approved' ? '已通过' : '已拒绝'}
</button>
))}
</div>
{loading ? (
<div className="space-y-3">
{[1,2,3].map(i => (
<div key={i} className="bg-card rounded-xl border border-border p-4">
<Skeleton className="h-4 w-32 mb-2" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</div>
))}
</div>
) : comments.length === 0 ? (
<div className="text-center py-20 text-muted-foreground">
<p className="text-4xl mb-4">{tab === 'pending' ? '✅' : '📝'}</p>
<p>{tab === 'pending' ? '暂无待审核评论' : tab === 'approved' ? '暂无已通过评论' : '暂无已拒绝评论'}</p>
</div>
) : (
<div className="space-y-3">
{comments.map(c => (
<div key={c.id} className="bg-card rounded-xl border border-border p-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-7 h-7 rounded-full bg-brand-100 flex items-center justify-center text-brand-600 text-xs font-bold">
{c.user.nickname?.[0] || 'U'}
</div>
<span className="text-sm font-medium text-foreground">{c.user.nickname || '用户'}</span>
<span className="text-xs text-muted-foreground">· {new Date(c.createdAt).toLocaleString('zh-CN')}</span>
</div>
<p className="text-sm text-foreground mb-2">{c.content}</p>
<div className="flex items-center justify-between">
<a href={`/community/${c.post.id}`} target="_blank"
className="text-xs text-brand-600 hover:underline">
: {c.post.title}
</a>
{tab === 'pending' && (
<div className="flex gap-2">
<button onClick={() => approve(c.id)}
className="px-3 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700">
</button>
<button onClick={() => reject(c.id)}
className="px-3 py-1 text-xs bg-red-500 text-white rounded-lg hover:bg-red-600">
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</>
);
}