feat: 完成头像上传、站点主题配置与UI优化

- 新增附件模型与上传服务,拆分头像上传/管理逻辑
- 重构用户资料接口,移除直接修改头像字段,改用专用上传接口
- 添加站点公开设置API与管理员外观配置页
- 全局替换hover背景为accent-soft,优化交互一致性
- 完善头像组件,支持图片加载失败回退与src更新重置
- 新增用户状态事件总线,解决同页头像更新不及时问题
- 调整依赖包顺序与CI依赖版本
- 重构板块列表接口,新增帖子计数
- 迁移账号设置到用户主页tab,合并重复代码
This commit is contained in:
2026-09-15 01:06:12 +08:00
parent f58b2af993
commit 24c6dea38c
48 changed files with 2513 additions and 432 deletions

View File

@@ -19,9 +19,11 @@ import {
User as UserIcon,
ChevronDown,
Megaphone,
Palette,
} from "lucide-react";
import { apiLogout, apiMe, type User } from "@/lib/api";
import { hasAuthCookieHint } from "@/lib/cookies";
import { onUserUpdate } from "@/lib/userEvents";
import NotificationBell from "./NotificationBell";
import Avatar from "./Avatar";
@@ -53,6 +55,11 @@ export default function Header({
setUnread(initialUnread);
}, [initialUnread]);
// 同页内资料变更(如上传/删除头像)立即生效,不依赖 router.refresh 时序
useEffect(() => onUserUpdate((patch) => {
setUser((prev) => (prev ? { ...prev, ...patch } : prev));
}), []);
// 挂载后静默校正一次(仅一次):覆盖 SSR 期间后端不可用、旧 Path refresh
// 半登录等边缘情况。正常 F5 下结果与 SSR 一致,不产生可见闪动;
// 无可读 CSRF cookie 说明必然是游客,跳过请求
@@ -274,9 +281,14 @@ export default function Header({
)}
{user && <MobileLink href="/settings">账号设置</MobileLink>}
{user?.role === "admin" && (
<MobileLink href="/admin/announcements">
<span className="inline-flex items-center gap-2"><Megaphone size={15} /> 公告管理</span>
</MobileLink>
<>
<MobileLink href="/admin/announcements">
<span className="inline-flex items-center gap-2"><Megaphone size={15} /> 公告管理</span>
</MobileLink>
<MobileLink href="/admin/appearance">
<span className="inline-flex items-center gap-2"><Palette size={15} /> 外观设置</span>
</MobileLink>
</>
)}
{!user && <MobileLink href="/login">登录</MobileLink>}
{!user && <MobileLink href="/register">注册</MobileLink>}
@@ -373,7 +385,7 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
}, [open]);
const itemCls =
"w-full flex items-center gap-2.5 px-4 py-2.5 text-[13.5px] font-semibold transition-colors hover:bg-[var(--panel-2)]";
"w-full flex items-center gap-2.5 px-4 py-2.5 text-[13.5px] font-semibold transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]";
return (
<div className="relative hidden lg:block" ref={menuRef}>
@@ -387,7 +399,7 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
background: open ? "var(--accent-soft)" : "var(--panel)",
}}
>
<Avatar name={user.nickname || user.username} size={28} />
<Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={28} />
<span
className="text-[13px] font-semibold max-w-[90px] truncate"
style={{ color: open ? "var(--accent)" : "var(--ink)" }}
@@ -419,7 +431,7 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
className="flex items-center gap-3 px-4 py-3.5"
style={{ borderBottom: "1px solid var(--line)" }}
>
<Avatar name={user.nickname || user.username} size={38} />
<Avatar name={user.nickname || user.username} src={user.avatar || undefined} size={38} />
<div className="min-w-0">
<p className="text-[14px] font-bold truncate" style={{ color: "var(--ink)" }}>
{user.nickname}
@@ -440,6 +452,9 @@ function UserMenu({ user, onLogout }: { user: User; onLogout: () => void }) {
<Link href="/admin/announcements" onClick={() => setOpen(false)} className={itemCls} role="menuitem">
<Megaphone size={15} style={{ color: "var(--accent)" }} /> 公告管理
</Link>
<Link href="/admin/appearance" onClick={() => setOpen(false)} className={itemCls} role="menuitem">
<Palette size={15} style={{ color: "var(--accent)" }} /> 外观设置
</Link>
</div>
)}
<div className="p-1.5" style={{ borderTop: "1px solid var(--line)" }}>