feat: 帖子收藏与用户关注

- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件
- 新增关注功能:follow 服务与 handler、FollowButton 组件
- 用户主页适配关注/粉丝与收藏展示
- flash 消息工具、广告与侧边栏组件微调
This commit is contained in:
2026-09-26 03:02:40 +08:00
parent 31f5360c26
commit d7c6b3a961
40 changed files with 1528 additions and 268 deletions

View File

@@ -5,6 +5,7 @@ import BoardIcon, { boardColor } from "@/components/BoardIcon";
import Avatar from "@/components/Avatar";
import RoleBadge from "@/components/RoleBadge";
import LevelBadge from "@/components/LevelBadge";
import BadgeChip from "@/components/BadgeChip";
// formatYMD 帖子详情页左栏「作者其他帖」日期
function formatYMD(dateStr: string) {
@@ -56,9 +57,14 @@ export function BoardCard({ board }: { board: Board }) {
export function AuthorRailCard({
profile,
allowComments,
boardNames,
boardColors,
}: {
profile: UserProfile;
allowComments: boolean;
/** 板块 id → 名称/配色:板块管理员徽章展示「{板块名}板块管理员」并对齐板块色 */
boardNames?: Record<number, string>;
boardColors?: Record<number, number>;
}) {
const u = profile.user;
const signature = (u.signature || "").trim();
@@ -89,13 +95,23 @@ export function AuthorRailCard({
<LevelBadge level={u.level} className="shrink-0" />
</div>
<p className="meta mt-0.5 truncate">@{u.username}</p>
{u.role !== "user" && (
<div className="mt-1">
<RoleBadge role={u.role} boardIds={u.board_ids} />
</div>
)}
</div>
</div>
{/* 徽章行:站点角色 + 管理员颁布徽章统一收拢一处,悬浮显示颁布时间;窄栏最多 4 枚 */}
{(u.role !== "user" || (u.badges ?? []).length > 0) && (
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
{u.role !== "user" && (
<RoleBadge role={u.role} boardIds={u.board_ids} boardNames={boardNames} boardColors={boardColors} />
)}
{(u.badges ?? []).slice(0, 4).map((ub) => (
<BadgeChip
key={ub.id}
badge={ub.badge}
tip={`${ub.badge.name} · ${new Date(ub.awarded_at).toLocaleDateString("zh-CN")} 颁布`}
/>
))}
</div>
)}
{/* 有签名则展示;无则留空,不放占位文案 */}
{signature ? (
<p
@@ -106,18 +122,21 @@ export function AuthorRailCard({
{signature}
</p>
) : null}
{/* 数据面板:圆角浅底 + 竖线分隔(参考作者卡布局,配色走站点令牌) */}
<div
className={`mt-3 grid border-t ${stats.length === 3 ? "grid-cols-3" : "grid-cols-2"}`}
style={{ borderColor: "var(--line)" }}
className={`mt-3 grid overflow-hidden rounded-[10px] ${
stats.length === 3 ? "grid-cols-3" : "grid-cols-2"
}`}
style={{ background: "var(--panel-2)" }}
>
{stats.map((s, i) => (
<div
key={s.label}
className="px-1.5 py-2.5 text-center"
className="py-2.5 text-center"
style={i > 0 ? { borderLeft: "1px solid var(--line)" } : undefined}
>
<p
className="text-[17px] font-semibold leading-none tabular-nums"
className="text-[16px] font-semibold leading-none tabular-nums"
style={{ color: "var(--ink)" }}
>
{s.value}
@@ -128,12 +147,17 @@ export function AuthorRailCard({
</div>
))}
</div>
{/* 连签胶囊:金色调软底,与站点 gold 令牌一致 */}
{streak > 0 && (
<p
className="mt-2 flex items-center justify-center gap-1 text-[11.5px] font-medium"
style={{ color: "var(--gold)" }}
className="mt-2.5 flex h-9 items-center justify-center gap-1.5 rounded-full text-[12px] font-semibold"
style={{
background: "var(--gold-soft)",
color: "var(--gold)",
border: "1px solid color-mix(in srgb, var(--gold) 22%, transparent)",
}}
>
<Flame size={11.5} /> 连签 {streak} 天
<Flame size={12.5} /> 连签 {streak} 天
</p>
)}
<Link href={`/u/${u.id}`} className="btn btn-line btn-sm w-full justify-center mt-3">