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

@@ -113,7 +113,7 @@ function Announcements({ items }: { items: AnnouncementSummary[] }) {
<li key={a.id}>
<Link
href={`/announcement/${a.id}`}
className="group block -mx-1.5 rounded-lg px-1.5 py-1.5 transition-colors hover:bg-[var(--panel-2)]"
className="group block -mx-1.5 rounded-lg px-1.5 py-1.5 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
>
<span className="flex items-center justify-between gap-2">
<span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span>
@@ -194,7 +194,7 @@ function NewUsers({ users }: { users: NewUser[] }) {
<li key={u.id}>
<Link
href={`/u/${u.id}`}
className="flex flex-col items-center gap-1.5 rounded-lg px-1 py-1 transition-colors hover:bg-[var(--panel-2)]"
className="flex flex-col items-center gap-1.5 rounded-lg px-1 py-1 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
>
<Avatar name={name} src={u.avatar} size={38} />
<span

View File

@@ -2,7 +2,7 @@
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { PenSquare, Home } from "lucide-react";
import { PenSquare, Home, ChevronRight } from "lucide-react";
import type { BoardWithCount, User } from "@/lib/api";
// 首页壳内板块切换:仅替换中栏帖子流,不离开工作台
@@ -23,64 +23,62 @@ export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user:
<PenSquare size={16} /> 发起讨论
</Link>
{/* 「全部讨论」是重置筛选的动作入口,与板块目录分区展示 */}
{/* 「全部讨论」是重置筛选的动作入口,与板块目录分区展示;卡片样式与下方板块一致 */}
<div className="space-y-0.5">
<Link
href="/"
className="j13-raillink"
aria-current={activeBoard === 0 ? "page" : undefined}
>
<Home size={16} className="shrink-0" />
<span className="flex-1 truncate">全部讨论</span>
<Link href="/" className="j13-bcard" aria-current={activeBoard === 0 ? "page" : undefined}>
<span
className="j13-bcard-ico"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
aria-hidden="true"
>
<Home size={16} />
</span>
<span className="j13-bcard-name flex-1 truncate text-[13px] font-bold">
全部讨论
</span>
</Link>
</div>
{/* 板块目录:发现型入口(介绍 + 讨论量),区别于帖子页面包屑的位置指引;
点击不离开工作台,仅在中栏筛选该板块帖子流 */}
{/* 板块目录:标题可点击进入板块汇总列表页(/boards);
下方板块卡片点击不离开工作台,仅在中栏筛选该板块帖子流 */}
<div>
<p
className="px-1 mb-2 flex items-center justify-between text-[11.5px] font-bold tracking-[0.08em] uppercase"
style={{ color: "var(--ink-3)" }}
<Link
href="/boards"
className="group px-1 mb-2 flex items-center justify-between text-[11.5px] font-bold tracking-[0.08em] uppercase text-[var(--ink-3)] hover:text-[var(--accent)] transition-colors"
aria-label="进入板块目录列表页"
>
<span>板块目录</span>
<span className="flex items-center">
<span>板块目录</span>
<ChevronRight
size={12}
className="ml-0.5 opacity-0 -translate-x-0.5 group-hover:opacity-100 group-hover:translate-x-0 transition-all"
aria-hidden="true"
/>
</span>
{boards.length > 0 && (
<span className="tracking-normal font-semibold tabular-nums">{boards.length} 个</span>
)}
</p>
</Link>
<div className="space-y-2">
{boards.map((b, i) => {
const desc = b.description?.trim();
return (
<Link
key={b.id}
href={boardHref(b.id)}
className="j13-bcard"
aria-current={activeBoard === b.id ? "page" : undefined}
aria-label={`${b.name}板块,${b.post_count} 篇讨论`}
>
<span
className={`av-${i % 8} j13-bcard-ico`}
aria-hidden="true"
>
{b.name.slice(0, 1)}
{boards.map((b, i) => (
<Link
key={b.id}
href={boardHref(b.id)}
className="j13-bcard"
aria-current={activeBoard === b.id ? "page" : undefined}
aria-label={`${b.name}板块,${b.post_count} 篇讨论`}
>
<span className={`av-${i % 8} j13-bcard-ico`} aria-hidden="true">
{b.name.slice(0, 1)}
</span>
<span className="min-w-0 flex-1 flex items-center justify-between gap-2">
<span className="j13-bcard-name truncate text-[13px] font-bold">
{b.name}
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center justify-between gap-2">
<span
className="truncate text-[13px] font-bold"
style={{ color: "var(--ink)" }}
>
{b.name}
</span>
<span className="meta shrink-0 text-[11px] tabular-nums">
{b.post_count} 篇
</span>
</span>
{desc && <span className="j13-bcard-desc">{desc}</span>}
</span>
</Link>
);
})}
<span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span>
</span>
</Link>
))}
</div>
</div>
</div>