- 新增事件总线式toast通知系统 - 新增头像、阅读进度、404/500错误页等基础组件 - 重构页脚、布局、分页、点赞按钮等现有组件 - 新增用户个人页、通知中心、帖子编辑/发布/置顶/推荐管理功能 - 优化登录注册页面样式与交互 - 修复路径类型文件引用,更新站点元信息与字体配置
181 lines
6.8 KiB
TypeScript
181 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { FileText, MessageCircle } from "lucide-react";
|
|
import { fetchUserComments, type UserProfile, type UserCommentsResponse } from "@/lib/api";
|
|
import Pagination from "@/components/Pagination";
|
|
import { IconComment, IconEye, IconHeart } from "@/components/Icons";
|
|
|
|
interface UserProfileClientProps {
|
|
userId: string;
|
|
initialPosts: UserProfile;
|
|
initialPage: number;
|
|
}
|
|
|
|
type Tab = "posts" | "comments";
|
|
|
|
export default function UserProfileClient({ userId, initialPosts, initialPage }: UserProfileClientProps) {
|
|
const [tab, setTab] = useState<Tab>("posts");
|
|
const [commentData, setCommentData] = useState<UserCommentsResponse | null>(null);
|
|
const [commentLoading, setCommentLoading] = useState(false);
|
|
const [commentPage, setCommentPage] = useState(1);
|
|
|
|
const loadComments = async (page: number) => {
|
|
setCommentLoading(true);
|
|
try {
|
|
const data = await fetchUserComments(userId, page);
|
|
setCommentData({ ...data, comments: data.comments || [] });
|
|
setCommentPage(page);
|
|
} catch {
|
|
setCommentData({ comments: [], total: 0, page, size: 20 });
|
|
}
|
|
setCommentLoading(false);
|
|
};
|
|
|
|
const handleTabChange = (t: Tab) => {
|
|
setTab(t);
|
|
if (t === "comments" && commentData === null) {
|
|
loadComments(1);
|
|
}
|
|
};
|
|
|
|
const tabBtn = (t: Tab, label: string, count: number, icon: React.ReactNode) => {
|
|
const active = tab === t;
|
|
return (
|
|
<button
|
|
onClick={() => handleTabChange(t)}
|
|
className="flex-1 sm:flex-none inline-flex items-center justify-center gap-1.5 px-4 py-2 rounded-full text-sm font-semibold transition-all"
|
|
style={{
|
|
background: active ? "var(--panel)" : "transparent",
|
|
color: active ? "var(--accent)" : "var(--ink-3)",
|
|
boxShadow: active ? "var(--shadow-sm)" : "none",
|
|
}}
|
|
>
|
|
{icon}
|
|
{label}
|
|
<span
|
|
className="text-[11px] font-bold px-1.5 py-px rounded-full"
|
|
style={{
|
|
background: active ? "var(--accent-soft)" : "var(--tint)",
|
|
color: active ? "var(--accent)" : "var(--ink-3)",
|
|
}}
|
|
>
|
|
{count}
|
|
</span>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className="inline-flex w-full sm:w-auto items-center gap-1 mb-6 p-1 rounded-full"
|
|
style={{ background: "var(--panel-2)" }}
|
|
>
|
|
{tabBtn("posts", "发帖", initialPosts.stats.post_count, <FileText size={15} />)}
|
|
{tabBtn("comments", "评论", initialPosts.stats.comment_count, <MessageCircle size={15} />)}
|
|
</div>
|
|
|
|
{tab === "posts" ? (
|
|
<div>
|
|
{initialPosts.posts.length > 0 ? (
|
|
<div className="panel overflow-hidden p-0">
|
|
{initialPosts.posts.map((post, i) => (
|
|
<Link
|
|
key={post.id}
|
|
href={`/post/${post.id}`}
|
|
className="block px-4 sm:px-5 py-4 transition-colors hover:bg-[var(--panel-2)]"
|
|
style={{ borderTop: i === 0 ? "none" : "1px solid var(--line)" }}
|
|
>
|
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
|
<span className="chip">{post.board.name}</span>
|
|
{post.pinned > 0 && <span className="badge-pin">置顶</span>}
|
|
{post.recommended && <span className="badge-rec">推荐</span>}
|
|
</div>
|
|
<h2
|
|
className="font-semibold mb-2 leading-snug group-hover:text-[var(--accent)]"
|
|
style={{ color: "var(--ink)" }}
|
|
>
|
|
{post.title}
|
|
</h2>
|
|
<div className="flex items-center gap-4 meta text-[12px]">
|
|
<span>{new Date(post.created_at).toLocaleDateString("zh-CN")}</span>
|
|
<span className="inline-flex items-center gap-1">
|
|
<IconComment size={12} /> {post.comment_count}
|
|
</span>
|
|
<span className="inline-flex items-center gap-1">
|
|
<IconEye size={12} /> {post.view_count}
|
|
</span>
|
|
<span className="inline-flex items-center gap-1">
|
|
<IconHeart size={12} /> {post.like_count}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="panel text-center py-16 px-6">
|
|
<FileText size={26} className="mx-auto mb-3" style={{ color: "var(--ink-3)" }} />
|
|
<p className="text-sm font-semibold" style={{ color: "var(--ink-2)" }}>暂无发帖</p>
|
|
</div>
|
|
)}
|
|
<Pagination
|
|
total={initialPosts.posts_total}
|
|
page={initialPage}
|
|
size={20}
|
|
basePath={`/u/${userId}`}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{commentLoading ? (
|
|
<div className="panel text-center py-16">
|
|
<p className="meta">正在加载评论…</p>
|
|
</div>
|
|
) : commentData && commentData.comments.length > 0 ? (
|
|
<>
|
|
<div className="panel overflow-hidden p-0">
|
|
{commentData.comments.map((c, i) => (
|
|
<Link
|
|
key={c.id}
|
|
href={`/post/${c.post_id}`}
|
|
className="block px-4 sm:px-5 py-4 transition-colors hover:bg-[var(--panel-2)]"
|
|
style={{ borderTop: i === 0 ? "none" : "1px solid var(--line)" }}
|
|
>
|
|
<div className="meta text-xs mb-2">
|
|
评论于《<span style={{ color: "var(--accent)" }}>{c.post_title}</span>》
|
|
</div>
|
|
<p
|
|
className="text-[15px] leading-relaxed line-clamp-2"
|
|
style={{ color: "var(--ink)" }}
|
|
>
|
|
{c.content}
|
|
</p>
|
|
<div className="meta text-xs mt-2">
|
|
{new Date(c.created_at).toLocaleString("zh-CN")}
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
<Pagination
|
|
total={commentData.total}
|
|
page={commentPage}
|
|
size={20}
|
|
basePath={`/u/${userId}`}
|
|
query={{ tab: "comments" }}
|
|
onChange={loadComments}
|
|
/>
|
|
</>
|
|
) : (
|
|
<div className="panel text-center py-16 px-6">
|
|
<MessageCircle size={26} className="mx-auto mb-3" style={{ color: "var(--ink-3)" }} />
|
|
<p className="text-sm font-semibold" style={{ color: "var(--ink-2)" }}>暂无评论</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|