- 新增 urlStyle 工具与测试:postPath/userPath/sitePagePath 统一生成链接 - 管理端新增 UrlStylePanel 设置面板,settings 服务与 handler 适配 - 全站路由链接与 JSON-LD/sitemap 切换为统一 URL 生成 - middleware 与相关组件适配
336 lines
11 KiB
TypeScript
336 lines
11 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";
|
|
import { getPublicSettingsCached } from "@/lib/serverData";
|
|
import { postPath, userPath } from "@/lib/urlStyle";
|
|
|
|
/* ---------- 1. 板块管理员 ---------- */
|
|
|
|
export async function Moderators({ items }: { items: BoardModerator[] }) {
|
|
const urlStyle = (await getPublicSettingsCached()).url_style;
|
|
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={userPath(urlStyle, 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 async function BoardHot({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
|
|
const urlStyle = (await getPublicSettingsCached()).url_style;
|
|
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={postPath(urlStyle, 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 async function BoardActive({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
|
|
const urlStyle = (await getPublicSettingsCached()).url_style;
|
|
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={userPath(urlStyle, 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>
|
|
);
|
|
}
|