feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
|
||||
interface PostDetail {
|
||||
id: number; title: string; content: string; tags?: string;
|
||||
viewCount: number; likeCount: number; commentCount: number;
|
||||
createdAt: string;
|
||||
user: { id: number; nickname: string; avatar: string | null };
|
||||
liked: boolean;
|
||||
comments: Comment[];
|
||||
}
|
||||
|
||||
interface Comment {
|
||||
id: number; content: string; createdAt: string;
|
||||
user: { id: number; nickname: string; avatar: string | null };
|
||||
}
|
||||
|
||||
export default function CommunityPostDetail() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [post, setPost] = useState<PostDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [commentText, setCommentText] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => { loadPost(); }, [params.id]);
|
||||
|
||||
async function loadPost() {
|
||||
try {
|
||||
const res = await apiFetch(`/community/posts/${params.id}`);
|
||||
if (res.ok) setPost(await res.json());
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleComment(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!commentText.trim()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await apiFetch(`/community/posts/${params.id}/comments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content: commentText }),
|
||||
});
|
||||
if (res.ok) { setCommentText(''); loadPost(); }
|
||||
} catch (e) { console.error(e) }
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function handleLike() {
|
||||
try {
|
||||
await apiFetch(`/community/posts/${params.id}/like`, { method: 'POST' });
|
||||
loadPost();
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl 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-32 w-full mb-4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!post) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 text-center">
|
||||
<p className="text-muted-foreground mb-4">帖子不存在或已被删除</p>
|
||||
<Link href="/community"><Button variant="outline">返回社区</Button></Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Link href="/community" className="text-sm text-muted-foreground hover:text-foreground mb-6 inline-block">← 返回社区</Link>
|
||||
|
||||
<Card className="p-6 sm:p-8 mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Link href={`/users/${post.user.id}`} className="flex items-center gap-3 group">
|
||||
<Avatar className="w-9 h-9">
|
||||
<AvatarFallback className="bg-brand-100 dark:bg-brand-900/40 text-brand-600 dark:text-brand-400 text-sm font-bold">{post.user.nickname?.[0] || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground group-hover:text-brand-600">{post.user.nickname}</div>
|
||||
<div className="text-xs text-muted-foreground">{new Date(post.createdAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold text-foreground mb-4">{post.title}</h1>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none mb-6 whitespace-pre-wrap text-foreground leading-relaxed">{post.content}</div>
|
||||
|
||||
{post.tags && (
|
||||
<div className="flex gap-2 mb-6">
|
||||
{post.tags.split(',').map(tag => (
|
||||
<span key={tag} className="text-xs px-2 py-0.5 bg-brand-50 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 rounded">{tag.trim()}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground border-t border-border pt-4">
|
||||
<button onClick={handleLike} className="flex items-center gap-1.5 hover:text-red-500 transition-colors">
|
||||
{post.liked ? '❤️' : '🤍'} {post.likeCount}
|
||||
</button>
|
||||
<span className="flex items-center gap-1.5">💬 {post.commentCount}</span>
|
||||
<span className="flex items-center gap-1.5">👁 {post.viewCount}</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">评论 ({post.comments?.length || 0})</h2>
|
||||
|
||||
<form onSubmit={handleComment} className="mb-6">
|
||||
<div className="flex gap-2">
|
||||
<input value={commentText} onChange={e => setCommentText(e.target.value)}
|
||||
placeholder="写下你的评论..." disabled={submitting}
|
||||
className="flex-1 px-4 py-2.5 bg-background border border-input rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<Button type="submit" disabled={submitting || !commentText.trim()}>
|
||||
{submitting ? '发送中...' : '评论'}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
评论需审核后展示。请遵守法律法规,不得发布违法或侵权内容。
|
||||
提交即视为同意
|
||||
<Link href="/ai-agreement" className="text-brand-600 hover:underline ml-1">AI 服务协议</Link>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div className="space-y-4">
|
||||
{post.comments?.map(c => (
|
||||
<Card key={c.id} className="p-4">
|
||||
<div className="flex gap-3">
|
||||
<Avatar className="w-7 h-7">
|
||||
<AvatarFallback className="text-xs">{c.user.nickname?.[0] || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-foreground">{c.user.nickname}</span>
|
||||
<span className="text-xs text-muted-foreground">{new Date(c.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{c.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{(!post.comments || post.comments.length === 0) && (
|
||||
<p className="text-center text-muted-foreground py-8">暂无评论,来抢沙发吧!</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
const res = await fetch(`${base}/api/v1/community/posts`);
|
||||
const data = await res.json();
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) return [{ id: '1' }];
|
||||
return items.map((p: any) => ({ id: String(p.id) }));
|
||||
} catch {
|
||||
return [{ id: '1' }];
|
||||
}
|
||||
}
|
||||
|
||||
import ClientPage from './client';
|
||||
|
||||
export default function Page() {
|
||||
return <ClientPage />;
|
||||
}
|
||||
Reference in New Issue
Block a user