feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user