feat: 新增大量社区功能与基础组件,优化站点整体体验

- 新增事件总线式toast通知系统
- 新增头像、阅读进度、404/500错误页等基础组件
- 重构页脚、布局、分页、点赞按钮等现有组件
- 新增用户个人页、通知中心、帖子编辑/发布/置顶/推荐管理功能
- 优化登录注册页面样式与交互
- 修复路径类型文件引用,更新站点元信息与字体配置
This commit is contained in:
2026-09-12 04:30:02 +08:00
parent a8f6ce4e4f
commit 1eaf3903d4
31 changed files with 2395 additions and 740 deletions

View File

@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import Link from "next/link";
import { Pencil, Trash2, Pin, Star } from "lucide-react";
import { apiMe, apiDeletePost, apiTogglePin, apiToggleRecommend, type User } from "@/lib/api";
import { toast } from "@/lib/toast";
interface PostActionsProps {
postId: string;
@@ -19,7 +20,7 @@ export default function PostActions({ postId, authorId, pinned, recommended }: P
const [deleting, setDeleting] = useState(false);
const [isPinned, setIsPinned] = useState(pinned > 0);
const [isRecommended, setIsRecommended] = useState(recommended);
const [acting, setActing] = useState("");
const [acting, setActing] = useState(false);
useEffect(() => {
apiMe()
@@ -27,7 +28,6 @@ export default function PostActions({ postId, authorId, pinned, recommended }: P
.catch(() => setUser(null));
}, []);
// 仅作者本人或管理员可编辑/删除;置顶/推荐仅管理员
const canManage = user && (user.id === authorId || user.role === "admin");
const isAdmin = user?.role === "admin";
if (!canManage) return null;
@@ -39,78 +39,76 @@ export default function PostActions({ postId, authorId, pinned, recommended }: P
const res = await apiDeletePost(postId);
if (res.message) {
router.push("/");
toast("帖子已删除", "ok");
} else {
alert(res.error || "删除失败");
toast(res.error || "删除失败");
}
} catch {
toast("网络错误,删除失败");
} finally {
setDeleting(false);
}
};
const handlePin = async () => {
setActing("pin");
setActing(true);
try {
const res = await apiTogglePin(postId);
setIsPinned(res.pinned > 0);
toast(res.pinned > 0 ? "已置顶" : "已取消置顶", "ok");
router.refresh();
} catch {
toast("操作失败,请稍后重试");
} finally {
setActing("");
setActing(false);
}
};
const handleRecommend = async () => {
setActing("recommend");
setActing(true);
try {
const res = await apiToggleRecommend(postId);
setIsRecommended(res.recommended);
toast(res.recommended ? "已设为推荐" : "已取消推荐", "ok");
router.refresh();
} catch {
toast("操作失败,请稍后重试");
} finally {
setActing("");
setActing(false);
}
};
return (
<div className="flex items-center gap-2 flex-wrap justify-end">
<div className="flex items-center gap-1.5 flex-wrap">
{isAdmin && (
<>
<button
onClick={handlePin}
disabled={acting !== ""}
className="flex items-center gap-1 text-sm px-3 py-1.5 rounded-lg hover:opacity-80 disabled:opacity-50"
style={{
background: isPinned ? "var(--primary)" : "var(--bg-secondary)",
color: isPinned ? "white" : "var(--text)",
}}
disabled={acting}
className="btn btn-line btn-sm"
style={isPinned ? { borderColor: "var(--accent)", color: "var(--accent)" } : undefined}
>
<Pin size={14} /> {isPinned ? "取消置顶" : "置顶"}
<Pin size={13} /> {isPinned ? "取消置顶" : "置顶"}
</button>
<button
onClick={handleRecommend}
disabled={acting !== ""}
className="flex items-center gap-1 text-sm px-3 py-1.5 rounded-lg hover:opacity-80 disabled:opacity-50"
style={{
background: isRecommended ? "#e11d48" : "var(--bg-secondary)",
color: isRecommended ? "white" : "var(--text)",
}}
disabled={acting}
className="btn btn-line btn-sm"
style={
isRecommended
? { borderColor: "var(--gold)", color: "var(--gold)", background: "var(--gold-soft)" }
: undefined
}
>
<Star size={14} /> {isRecommended ? "取消推荐" : "推荐"}
<Star size={13} /> {isRecommended ? "取消推荐" : "推荐"}
</button>
</>
)}
<Link
href={`/post/${postId}/edit`}
className="flex items-center gap-1 text-sm px-3 py-1.5 rounded-lg hover:opacity-80"
style={{ background: "var(--bg-secondary)", color: "var(--text)" }}
>
<Pencil size={14} /> 编辑
<Link href={`/post/${postId}/edit`} className="btn btn-line btn-sm">
<Pencil size={13} /> 编辑
</Link>
<button
onClick={handleDelete}
disabled={deleting}
className="flex items-center gap-1 text-sm px-3 py-1.5 rounded-lg text-red-600 hover:bg-red-50 disabled:opacity-50"
style={{ background: "var(--bg-secondary)" }}
>
<Trash2 size={14} /> {deleting ? "删除中..." : "删除"}
<button onClick={handleDelete} disabled={deleting} className="btn btn-danger btn-sm">
<Trash2 size={13} /> {deleting ? "删除中..." : "删除"}
</button>
</div>
);