feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
const res = await fetch(`${base}/api/v1/users`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
const users = data.items || [];
|
||||
if (users.length === 0) return [{ id: '1' }];
|
||||
return users.map((u: any) => ({ id: String(u.id) }));
|
||||
} catch {
|
||||
return [{ id: '1' }];
|
||||
}
|
||||
}
|
||||
|
||||
import UserProfilePage from './user-profile';
|
||||
|
||||
export default function Page() {
|
||||
return <UserProfilePage />;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface UserProfile {
|
||||
id: number; nickname: string; avatar?: string; bio?: string;
|
||||
followerCount: number; followingCount: number; postCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Post {
|
||||
id: number; title: string; content: string; tags?: string;
|
||||
viewCount: number; likeCount: number; commentCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function UserProfilePage() {
|
||||
const params = useParams();
|
||||
const userId = Number(params.id);
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [posts, setPosts] = useState<Post[]>([]);
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'posts' | 'followers' | 'following'>('posts');
|
||||
const [followers, setFollowers] = useState<any[]>([]);
|
||||
const [following, setFollowing] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => { loadData(); }, [userId]);
|
||||
|
||||
function getToken() { return localStorage.getItem('token'); }
|
||||
function apiHeaders() {
|
||||
const h: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
const t = getToken();
|
||||
if (t) h['Authorization'] = `Bearer ${t}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
const [profileRes, postsRes] = await Promise.all([
|
||||
fetch(`${base}/api/v1/community/users/${userId}/profile`),
|
||||
fetch(`${base}/api/v1/community/posts?userId=${userId}`),
|
||||
]);
|
||||
if (profileRes.ok) setProfile(await profileRes.json());
|
||||
if (postsRes.ok) {
|
||||
const d = await postsRes.json();
|
||||
setPosts(d.items || []);
|
||||
}
|
||||
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
const followRes = await fetch(`${base}/api/v1/community/users/${userId}/follow`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (followRes.ok) {
|
||||
const d = await followRes.json();
|
||||
setIsFollowing(d.followed);
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function toggleFollow() {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
const res = await fetch(`${base}/api/v1/community/users/${userId}/follow`, {
|
||||
method: isFollowing ? 'DELETE' : 'POST',
|
||||
headers: apiHeaders(),
|
||||
});
|
||||
if (res.ok) {
|
||||
setIsFollowing(!isFollowing);
|
||||
setProfile(prev => prev ? {
|
||||
...prev,
|
||||
followerCount: prev.followerCount + (isFollowing ? -1 : 1),
|
||||
} : prev);
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function loadFollowers() {
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
const res = await fetch(`${base}/api/v1/community/users/${userId}/followers`);
|
||||
if (res.ok) {
|
||||
const d = await res.json();
|
||||
setFollowers(d.items || []);
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function loadFollowing() {
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
const res = await fetch(`${base}/api/v1/community/users/${userId}/following`);
|
||||
if (res.ok) {
|
||||
const d = await res.json();
|
||||
setFollowing(d.items || []);
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'followers') loadFollowers();
|
||||
if (activeTab === 'following') loadFollowing();
|
||||
}, [activeTab]);
|
||||
|
||||
if (!profile) 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-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-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="bg-card rounded-xl border border-border p-8 mb-8">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-20 h-20 rounded-full bg-brand-100 flex items-center justify-center text-brand-600 text-2xl font-bold">
|
||||
{profile.nickname?.[0] || 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{profile.nickname || '用户'}</h1>
|
||||
{profile.bio && <p className="text-muted-foreground mt-1">{profile.bio}</p>}
|
||||
<div className="flex gap-6 mt-3 text-sm text-muted-foreground">
|
||||
<span><strong className="text-foreground">{profile.postCount}</strong> 帖子</span>
|
||||
<span><strong className="text-foreground">{profile.followerCount}</strong> 粉丝</span>
|
||||
<span><strong className="text-foreground">{profile.followingCount}</strong> 关注</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={toggleFollow}
|
||||
className={`px-6 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
isFollowing ? 'bg-muted text-muted-foreground hover:bg-accent' : 'bg-brand-600 text-white hover:bg-brand-700'
|
||||
}`}>
|
||||
{isFollowing ? '已关注' : '关注'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 mb-6 bg-muted rounded-lg p-1">
|
||||
{(['posts', 'followers', 'following'] as const).map(tab => (
|
||||
<button key={tab} onClick={() => setActiveTab(tab)}
|
||||
className={`flex-1 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === tab ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-muted-foreground'
|
||||
}`}>
|
||||
{tab === 'posts' ? '帖子' : tab === 'followers' ? '粉丝' : '关注'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === 'posts' && (
|
||||
<div className="space-y-4">
|
||||
{posts.map(post => (
|
||||
<Link key={post.id} href={`/community/${post.id}`}
|
||||
className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<h3 className="font-semibold text-foreground mb-2">{post.title}</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">{post.content}</p>
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
<span>👁 {post.viewCount}</span>
|
||||
<span>❤️ {post.likeCount}</span>
|
||||
<span>💬 {post.commentCount}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{posts.length === 0 && <p className="text-center text-muted-foreground py-12">暂无帖子</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'followers' && (
|
||||
<div className="bg-card rounded-xl border border-border divide-y">
|
||||
{followers.map((u: any) => (
|
||||
<Link key={u.id} href={`/users/${u.id}`}
|
||||
className="flex items-center gap-3 px-6 py-4 hover:bg-muted/50">
|
||||
<div className="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center text-brand-600 text-sm font-bold">
|
||||
{u.nickname?.[0] || 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{u.nickname || '用户'}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.followerCount} 粉丝</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{followers.length === 0 && <p className="text-center text-muted-foreground py-8">暂无粉丝</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'following' && (
|
||||
<div className="bg-card rounded-xl border border-border divide-y">
|
||||
{following.map((u: any) => (
|
||||
<Link key={u.id} href={`/users/${u.id}`}
|
||||
className="flex items-center gap-3 px-6 py-4 hover:bg-muted/50">
|
||||
<div className="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center text-brand-600 text-sm font-bold">
|
||||
{u.nickname?.[0] || 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{u.nickname || '用户'}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.followerCount} 粉丝</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{following.length === 0 && <p className="text-center text-muted-foreground py-8">暂未关注</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user