Files
jiang13-bbs/frontend/components/home/BoardPanels.tsx
freefire 1d862a0dd3 feat: 站点媒体库与移动端底部导航,服务测试补强
- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面
- 移动端底部导航 MobileTabBar 替换 MobilePostBar
- 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强
- site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
2026-09-27 02:36:11 +08:00

331 lines
10 KiB
TypeScript

import Link from "next/link";
import {
Flame,
Users,
MessageCircle,
Heart,
Shield,
LayoutGrid,
PenLine,
PenSquare,
BarChart3,
TrendingUp,
} from "lucide-react";
import type {
PostListItem,
ActiveUser,
BoardWithCount,
BoardModerator,
BoardSidebarStats,
CheckinStatus,
} from "@/lib/api";
import Avatar from "@/components/Avatar";
import LevelBadge from "@/components/LevelBadge";
import BadgeChip from "@/components/BadgeChip";
import { PulseCheckin } from "./CheckinCard";
import { formatCompact } from "@/lib/format";
/* ---------- 1. 板块管理员 ---------- */
export function Moderators({ items }: { items: BoardModerator[] }) {
return (
<section className="j13-module" aria-label="板块管理员">
<p className="j13-module-title mb-2">
<Shield size={15} style={{ color: "var(--accent)" }} /> 板块管理员
</p>
{items.length === 0 ? (
<p className="meta px-0.5 py-1.5">暂无板块管理员</p>
) : (
<ul className="space-y-1.5">
{items.map((u) => {
const name = u.nickname || u.username;
return (
<li key={u.id}>
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
<Avatar name={name} src={u.avatar || undefined} size={30} />
<span className="min-w-0 truncate text-[13px] font-medium" style={{ color: "var(--ink)" }}>
{name}
</span>
<LevelBadge level={u.level} className="shrink-0" />
{u.badges?.slice(0, 2).map((ub) => (
<BadgeChip key={ub.id} badge={ub.badge} className="shrink-0" />
))}
<span className="flex-1" />
</Link>
</li>
);
})}
</ul>
)}
</section>
);
}
/* ---------- 2. 板块概况 ---------- */
export function BoardIntro({ board }: { board: BoardWithCount }) {
return (
<section className="j13-module" aria-label="板块概况">
<p className="j13-module-title mb-2">
<LayoutGrid size={15} style={{ color: "var(--accent)" }} /> 板块概况
</p>
<p className="text-[15px] font-medium leading-snug" style={{ color: "var(--ink)" }}>
{board.name}
</p>
{board.description ? (
<p className="meta mt-1.5 text-[12.5px] leading-relaxed line-clamp-3">{board.description}</p>
) : (
<p className="meta mt-1.5">暂无板块介绍</p>
)}
<p className="meta mt-2 tabular-nums">讨论 {formatCompact(board.post_count)} 篇</p>
<div className="mt-3">
<Link href={`/compose?board=${board.id}`} className="btn btn-primary btn-sm w-full justify-center">
<PenSquare size={14} /> 在此发帖
</Link>
</div>
</section>
);
}
/* ---------- 3. 板块数据 + 签到 ---------- */
export function BoardPulse({
stats,
checkin,
allowComments,
}: {
stats: BoardSidebarStats;
checkin?: CheckinStatus | null;
allowComments: boolean;
}) {
const cells = [
{
icon: <PenLine size={12.5} />,
label: "话题总数",
value: formatCompact(stats.post_count),
foot: `+${formatCompact(stats.today_posts)} 今日`,
up: true,
},
{
icon: <TrendingUp size={12.5} />,
label: "今日新帖",
value: formatCompact(stats.today_posts),
foot: "本日新增",
up: stats.today_posts > 0,
},
...(allowComments
? [
{
icon: <MessageCircle size={12.5} />,
label: "回复总数",
value: formatCompact(stats.comment_count),
foot: `+${formatCompact(stats.today_comments)} 今日`,
up: true,
},
{
icon: <Heart size={12.5} />,
label: "今日回复",
value: formatCompact(stats.today_comments),
foot: "本日新增",
up: stats.today_comments > 0,
},
]
: []),
];
return (
<section className="j13-module" aria-label="板块数据">
<p className="j13-module-title">
<BarChart3 size={15} style={{ color: "var(--accent)" }} /> 板块数据
</p>
<div className="j13-pulse">
{cells.map((c) => (
<div key={c.label} className="j13-pulse-cell">
<p className="j13-pulse-lab">
<span style={{ color: "var(--accent)" }} aria-hidden="true">
{c.icon}
</span>
{c.label}
</p>
<p className="j13-pulse-num">{c.value}</p>
<p className={`j13-pulse-foot${c.up ? " is-up" : ""}`}>
{c.up && <TrendingUp size={10.5} aria-hidden="true" />}
{c.foot}
</p>
</div>
))}
</div>
<div className="j13-pulse-checkin">
<PulseCheckin initial={checkin} />
</div>
</section>
);
}
/* ---------- 4. 本板热门 ---------- */
export function BoardHot({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
return (
<section className="j13-module" aria-label="本板热门">
<p className="j13-module-title mb-1.5">
<Flame size={15} style={{ color: "var(--clay)" }} /> 本板热门
</p>
{posts.length === 0 ? (
<p className="meta px-1.5 py-3">本板还没有热门讨论</p>
) : (
<ol className="j13-ranklist">
{posts.map((p, i) => (
<li key={p.id}>
<Link href={`/post/${p.id}`} className="j13-rank" data-tip={p.title}>
<span className="j13-rank-no">{i + 1}</span>
<span className="min-w-0 flex-1">
<span className="j13-rank-title truncate">
{p.title}
</span>
<span className="mt-1 flex items-center gap-2.5 meta text-[11.5px]">
{allowComments ? (
<span className="inline-flex items-center gap-1">
<MessageCircle size={11.5} /> {p.comment_count}
</span>
) : null}
<span className="inline-flex items-center gap-1">
<Heart size={11.5} /> {p.like_count}
</span>
</span>
</span>
</Link>
</li>
))}
</ol>
)}
</section>
);
}
/* ---------- 5. 本板活跃 ---------- */
export function BoardActive({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
return (
<section className="j13-module" aria-label="本板活跃成员">
<p className="j13-module-title mb-2">
<Users size={15} style={{ color: "var(--accent)" }} /> 本板活跃
<span
className="ml-auto font-normal normal-case tracking-normal text-[11px]"
style={{ color: "var(--ink-3)" }}
>
本周
</span>
</p>
{users.length === 0 ? (
<p className="meta px-1.5 py-2">本周本板还没有人互动</p>
) : (
<ul className="space-y-0.5">
{users.map((u) => {
const name = u.nickname || u.username;
return (
<li key={u.id}>
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
<Avatar name={name} src={u.avatar || undefined} size={30} />
<span className="flex-1 min-w-0">
<span
className="block truncate text-[13px] font-medium leading-tight"
style={{ color: "var(--ink)" }}
>
{name}
</span>
<span className="mt-px flex items-center gap-2.5 text-[11px] meta">
<span className="inline-flex items-center gap-1">
<PenLine size={10.5} /> 发帖 {u.post_count}
</span>
{allowComments ? (
<span className="inline-flex items-center gap-1">
<MessageCircle size={10.5} /> 回复 {u.comment_count}
</span>
) : null}
</span>
</span>
<span
className="shrink-0 text-[13px] tabular-nums"
style={{ color: "var(--accent)" }}
aria-label={
allowComments
? `本周共 ${u.post_count + u.comment_count} 次互动`
: `本周发帖 ${u.post_count}`
}
>
{allowComments ? u.post_count + u.comment_count : u.post_count}
</span>
</Link>
</li>
);
})}
</ul>
)}
</section>
);
}
/* ---------- 装配 ---------- */
type BoardPanelsProps = {
board: BoardWithCount;
moderators: BoardModerator[];
stats: BoardSidebarStats;
hot: PostListItem[];
activeUsers: ActiveUser[];
checkin?: CheckinStatus | null;
allowComments: boolean;
};
function BoardPanelsBody(props: BoardPanelsProps) {
return (
<>
<Moderators items={props.moderators} />
<BoardIntro board={props.board} />
<BoardPulse stats={props.stats} checkin={props.checkin} allowComments={props.allowComments} />
<BoardHot posts={props.hot} allowComments={props.allowComments} />
<BoardActive users={props.activeUsers} allowComments={props.allowComments} />
</>
);
}
export function BoardPanels(props: BoardPanelsProps) {
return (
<aside
className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]"
aria-label="板块面板"
>
<div className="space-y-4">
<BoardPanelsBody {...props} />
</div>
</aside>
);
}
export function BoardPanelsMobile(props: BoardPanelsProps) {
return (
<div className="space-y-4 lg:hidden" aria-label="板块面板">
<BoardPanelsBody {...props} />
</div>
);
}
/** 板块不存在时的简卡 */
export function BoardMissingPanel() {
return (
<aside
className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]"
aria-label="板块面板"
>
<section className="j13-module">
<p className="j13-module-title mb-2">
<LayoutGrid size={15} style={{ color: "var(--ink-3)" }} /> 板块
</p>
<p className="meta">板块不存在或已删除</p>
<Link href="/" className="btn btn-line btn-sm mt-3 w-full justify-center">
返回全部讨论
</Link>
</section>
</aside>
);
}