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

@@ -3,7 +3,7 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Heart, MessageCircle, CheckCheck } from "lucide-react";
import { Heart, MessageCircle, CheckCheck, Bell, ChevronRight } from "lucide-react";
import {
apiFetchNotifications,
apiMarkRead,
@@ -60,68 +60,84 @@ export default function NotificationsPage() {
return (
<div className="max-w-2xl mx-auto">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold" style={{ color: "var(--text)" }}>通知中心</h1>
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
<ChevronRight size={13} />
<span style={{ color: "var(--accent)" }}>通知中心</span>
</nav>
<div className="flex items-end justify-between mb-6 gap-4">
<h1 className="text-[26px] sm:text-[30px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
通知中心
</h1>
{hasUnread && (
<button
onClick={handleMarkAll}
className="flex items-center gap-1 text-sm hover:opacity-80"
style={{ color: "var(--primary)" }}
>
<CheckCheck size={16} /> 全部标记已读
<button onClick={handleMarkAll} className="btn btn-line btn-sm">
<CheckCheck size={14} /> 全部已读
</button>
)}
</div>
{loading ? (
<p className="text-center py-12" style={{ color: "var(--text-secondary)" }}>加载中...</p>
<div className="panel text-center py-16">
<p className="meta">正在加载通知…</p>
</div>
) : notifications.length === 0 ? (
<p className="text-center py-12" style={{ color: "var(--text-secondary)" }}>暂无通知</p>
<div className="panel text-center py-20 px-6">
<span
className="inline-flex w-14 h-14 rounded-full items-center justify-center mb-4"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
<Bell size={26} />
</span>
<p className="font-bold text-lg" style={{ color: "var(--ink)" }}>还没有通知</p>
<p className="meta mt-1.5">当有人评论或点赞你的帖子时,会在这里提醒你</p>
</div>
) : (
<div className="space-y-2">
{notifications.map((n) => (
<div className="panel overflow-hidden p-0">
{notifications.map((n, i) => (
<button
key={n.id}
onClick={() => handleClick(n)}
className="w-full text-left p-4 rounded-lg border hover:shadow-sm transition-shadow"
className="w-full text-left px-4 sm:px-5 py-4 flex items-start gap-3.5 transition-colors hover:bg-[var(--panel-2)]"
style={{
borderColor: "var(--border)",
background: n.is_read ? "var(--bg)" : "var(--bg-secondary)",
borderTop: i === 0 ? "none" : "1px solid var(--line)",
background: n.is_read ? "transparent" : "var(--panel-2)",
}}
>
<div className="flex items-start gap-3">
<span style={{ color: n.type === "like" ? "#ef4444" : "var(--primary)" }}>
{n.type === "like" ? <Heart size={18} /> : <MessageCircle size={18} />}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm" style={{ color: "var(--text)" }}>
<Link
href={`/u/${n.actor.id}`}
onClick={(e) => e.stopPropagation()}
className="font-medium hover:underline"
>
{n.actor.nickname}
</Link>{" "}
<span style={{ color: "var(--text-secondary)" }}>
{n.type === "comment" ? "评论了你的帖子" : "点赞了你的帖子"}
</span>
<span
className="w-10 h-10 shrink-0 rounded-full flex items-center justify-center"
style={{
color: n.type === "like" ? "var(--clay)" : "var(--accent)",
background:
n.type === "like" ? "var(--clay-soft)" : "var(--accent-soft)",
}}
>
{n.type === "like" ? <Heart size={16} /> : <MessageCircle size={16} />}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm leading-snug" style={{ color: "var(--ink)" }}>
<Link
href={`/u/${n.actor.id}`}
onClick={(e) => e.stopPropagation()}
className="font-semibold hover:text-[var(--accent)] transition-colors"
>
{n.actor.nickname}
</Link>{" "}
<span className="meta">{n.type === "comment" ? "评论了你的帖子" : "点赞了你的帖子"}</span>
</p>
<p className="text-sm mt-1 truncate" style={{ color: "var(--ink-2)" }}>
《{n.post.title}》
</p>
{n.content && (
<p className="text-[13px] mt-1.5 line-clamp-2" style={{ color: "var(--ink-3)" }}>
{n.content}
</p>
<p className="text-sm mt-1 font-medium truncate" style={{ color: "var(--text)" }}>
《{n.post.title}》
</p>
{n.content && (
<p className="text-sm mt-1 line-clamp-2" style={{ color: "var(--text-secondary)" }}>
{n.content}
</p>
)}
<p className="text-xs mt-2" style={{ color: "var(--text-secondary)" }}>
{formatTime(n.created_at)}
</p>
</div>
{!n.is_read && (
<span className="w-2.5 h-2.5 rounded-full shrink-0 mt-1.5" style={{ background: "var(--primary)" }} />
)}
<p className="meta text-xs mt-2">{formatTime(n.created_at)}</p>
</div>
{!n.is_read && (
<span className="w-2.5 h-2.5 shrink-0 mt-2 rounded-full" style={{ background: "var(--clay)" }} />
)}
</button>
))}
</div>