feat: 站点访问统计与可配置侧边栏组件

- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
2026-09-24 18:53:45 +08:00
parent 3e55b5d230
commit 7b327bc8cc
60 changed files with 6539 additions and 572 deletions

View File

@@ -25,7 +25,7 @@ import { formatCompact } from "@/lib/format";
/* ---------- 1. 板块管理员 ---------- */
function Moderators({ items }: { items: BoardModerator[] }) {
export function Moderators({ items }: { items: BoardModerator[] }) {
return (
<section className="j13-module" aria-label="板块管理员">
<p className="j13-module-title mb-2">
@@ -56,7 +56,7 @@ function Moderators({ items }: { items: BoardModerator[] }) {
/* ---------- 2. 板块概况 ---------- */
function BoardIntro({ board }: { board: BoardWithCount }) {
export function BoardIntro({ board }: { board: BoardWithCount }) {
return (
<section className="j13-module" aria-label="板块概况">
<p className="j13-module-title mb-2">
@@ -82,7 +82,7 @@ function BoardIntro({ board }: { board: BoardWithCount }) {
/* ---------- 3. 板块数据 + 签到 ---------- */
function BoardPulse({
export function BoardPulse({
stats,
checkin,
allowComments,
@@ -156,7 +156,7 @@ function BoardPulse({
/* ---------- 4. 本板热门 ---------- */
function BoardHot({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
export function BoardHot({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
return (
<section className="j13-module" aria-label="本板热门">
<p className="j13-module-title mb-1.5">
@@ -196,7 +196,7 @@ function BoardHot({ posts, allowComments }: { posts: PostListItem[]; allowCommen
/* ---------- 5. 本板活跃 ---------- */
function BoardActive({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
export function BoardActive({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
return (
<section className="j13-module" aria-label="本板活跃成员">
<p className="j13-module-title mb-2">

View File

@@ -38,7 +38,7 @@ import { AD_IMAGE_HEIGHT, AD_IMAGE_SIZE_LABEL, AD_IMAGE_WIDTH } from "@/lib/ads"
/* ---------- 站点数据(2×2 bento 统计瓦片) ---------- */
function PulseCard({
export function PulseCard({
stats,
checkin,
allowComments,
@@ -130,7 +130,7 @@ function PulseCard({
/* ---------- 关于本站(侧栏页目录,置于右栏最底) ---------- */
function AboutSite({
export function AboutSite({
pages,
siteDescription,
canManagePages,
@@ -175,7 +175,7 @@ function AboutSite({
/* ---------- 站点公告 ---------- */
function Announcements({
export function Announcements({
items,
canManageAnnouncements,
}: {
@@ -248,7 +248,7 @@ function Announcements({
/* ---------- 热门榜 ---------- */
function HotRanking({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
export function HotRanking({ posts, allowComments }: { posts: PostListItem[]; allowComments: boolean }) {
return (
<section className="j13-module" aria-label="热门讨论">
<p className="j13-module-title mb-1.5">
@@ -288,7 +288,7 @@ function HotRanking({ posts, allowComments }: { posts: PostListItem[]; allowComm
/* ---------- 最新注册 ---------- */
function NewUsers({ users }: { users: NewUser[] }) {
export function NewUsers({ users }: { users: NewUser[] }) {
if (users.length === 0) return null;
return (
<section className="j13-module" aria-label="最新注册">
@@ -323,7 +323,7 @@ function NewUsers({ users }: { users: NewUser[] }) {
/* ---------- 活跃成员(本周发帖 / 回复) ---------- */
function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
export function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; allowComments: boolean }) {
return (
<section className="j13-module" aria-label="本周活跃成员">
<p className="j13-module-title mb-2">
@@ -383,7 +383,7 @@ function ActiveUsers({ users, allowComments }: { users: ActiveUser[]; allowComme
/* ---------- 友情链接 ---------- */
function FriendLinks() {
export function FriendLinks() {
if (FRIEND_LINKS.length === 0) return null;
return (
<section className="j13-module" aria-label="友情链接">
@@ -441,7 +441,7 @@ type PanelsProps = {
sponsorsEnabled?: boolean;
};
function AdsPanel({
export function AdsPanel({
items,
title,
enabled,
@@ -529,7 +529,7 @@ function AdsPanel({
);
}
function SponsorsPanel({
export function SponsorsPanel({
items,
title,
enabled,

View File

@@ -12,81 +12,95 @@ function boardHref(id: number) {
return `/?board=${id}`;
}
// 桌面左栏:整块模块,与右栏 j13-module 对齐
export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user?: User | null }) {
// 板块目录裸面板:只有 j13-module 本体,不含外层 nav / 宽度 / sticky 类,
// 供侧栏注册表按管理端配置在任意侧栏渲染。
export function WorkspaceNavPanel({
boards,
user,
}: {
boards: BoardWithCount[];
user?: User | null;
}) {
const params = useSearchParams();
const activeBoard = Number(params.get("board")) || 0;
const showAdmin = !!user && isStaff(user.role);
return (
<section className="j13-module flex max-h-[var(--rail-max-h)] flex-col overflow-hidden" aria-label="板块目录">
<p className="j13-module-title shrink-0">
<LayoutGrid size={15} style={{ color: "var(--accent)" }} aria-hidden />
板块目录
{boards.length > 0 ? (
<span className="ml-auto meta text-[11.5px] font-medium tracking-normal tabular-nums">
{boards.length} 个
</span>
) : null}
</p>
<div className="j13-rail-scroll j13-board-list mt-2.5 min-h-0 flex-1">
<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={15} />
</span>
<span className="j13-bcard-name flex-1 truncate">全部讨论</span>
</Link>
{boards.length > 0 ? <div className="j13-board-split" role="separator" /> : null}
{boards.map((b) => (
<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-${boardColor(b)} j13-bcard-ico`} aria-hidden="true">
<BoardIcon icon={b.icon} name={b.name} size={15} />
</span>
<span className="min-w-0 flex-1 flex items-center justify-between gap-2">
<span className="j13-bcard-name truncate">{b.name}</span>
<span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span>
</span>
</Link>
))}
</div>
{showAdmin ? (
<div className="shrink-0 mt-1.5 pt-1.5" style={{ borderTop: "1px solid var(--line)" }}>
<Link href="/admin" className="j13-bcard">
<span
className="j13-bcard-ico"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
aria-hidden="true"
>
<ShieldCheck size={15} />
</span>
<span className="j13-bcard-name flex-1 truncate">管理面板</span>
</Link>
</div>
) : null}
</section>
);
}
// 桌面左栏:整块模块,与右栏 j13-module 对齐
export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user?: User | null }) {
return (
// sticky 必须挂在 flex 子项上:父级 items-start 会把内层缩成内容高,失去粘滞行程
<nav
className="hidden lg:block w-56 xl:w-60 shrink-0 sticky top-[var(--rail-top)] max-h-[var(--rail-max-h)]"
aria-label="工作台导航"
>
<section className="j13-module flex max-h-[var(--rail-max-h)] flex-col overflow-hidden">
<p className="j13-module-title shrink-0">
<LayoutGrid size={15} style={{ color: "var(--accent)" }} aria-hidden />
板块目录
{boards.length > 0 ? (
<span className="ml-auto meta text-[11.5px] font-medium tracking-normal tabular-nums">
{boards.length} 个
</span>
) : null}
</p>
<div className="j13-rail-scroll j13-board-list mt-2.5 min-h-0 flex-1">
<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={15} />
</span>
<span className="j13-bcard-name flex-1 truncate">全部讨论</span>
</Link>
{boards.length > 0 ? <div className="j13-board-split" role="separator" /> : null}
{boards.map((b) => (
<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-${boardColor(b)} j13-bcard-ico`} aria-hidden="true">
<BoardIcon icon={b.icon} name={b.name} size={15} />
</span>
<span className="min-w-0 flex-1 flex items-center justify-between gap-2">
<span className="j13-bcard-name truncate">{b.name}</span>
<span className="meta shrink-0 text-[11px] tabular-nums">{b.post_count} 篇</span>
</span>
</Link>
))}
</div>
{showAdmin ? (
<div className="shrink-0 mt-1.5 pt-1.5" style={{ borderTop: "1px solid var(--line)" }}>
<Link href="/admin" className="j13-bcard">
<span
className="j13-bcard-ico"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
aria-hidden="true"
>
<ShieldCheck size={15} />
</span>
<span className="j13-bcard-name flex-1 truncate">管理面板</span>
</Link>
</div>
) : null}
</section>
<WorkspaceNavPanel boards={boards} user={user} />
</nav>
);
}