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

@@ -0,0 +1,155 @@
import type {
ActiveUser,
AnnouncementSummary,
Board,
BoardModerator,
BoardSidebarStats,
BoardWithCount,
CheckinStatus,
NewUser,
OverviewStats,
PostListItem,
PublicAdItem,
PublicSponsorItem,
SidebarPageItem,
User,
UserProfile,
} from "@/lib/api";
import PostToc, { type TocItem } from "@/components/PostToc";
import { WorkspaceNavPanel } from "@/components/home/WorkspaceNav";
import {
PulseCard,
Announcements,
AdsPanel,
SponsorsPanel,
HotRanking,
NewUsers,
ActiveUsers,
FriendLinks,
AboutSite,
} from "@/components/home/CommunityPanels";
import {
Moderators,
BoardIntro,
BoardPulse,
BoardHot,
BoardActive,
} from "@/components/home/BoardPanels";
import {
BoardCard as PostBoardCard,
AuthorRailCard as PostAuthorCard,
AuthorPosts as PostAuthorPosts,
} from "@/components/post/PostRailBlocks";
// 侧栏块 props 汇总:页面把已有数据整包传入,块按需取用;
// 某块所需数据缺失(如板块态没传 stats)时对应块渲染为空,安全跳过。
export interface SidebarBlockProps {
allowComments: boolean;
user?: User | null;
checkin?: CheckinStatus | null;
// home 范围
boards?: BoardWithCount[];
stats?: OverviewStats;
announcements?: AnnouncementSummary[];
canManageAnnouncements?: boolean;
ads?: PublicAdItem[];
adsPanelTitle?: string;
adsEnabled?: boolean;
sponsors?: PublicSponsorItem[];
sponsorsPanelTitle?: string;
sponsorsEnabled?: boolean;
hot?: PostListItem[];
newUsers?: NewUser[];
activeUsers?: ActiveUser[];
sidebarPages?: SidebarPageItem[];
siteDescription?: string;
canManagePages?: boolean;
// board 范围
board?: BoardWithCount;
moderators?: BoardModerator[];
boardStats?: BoardSidebarStats;
boardHot?: PostListItem[];
boardActiveUsers?: ActiveUser[];
// post 范围
postBoard?: Board;
authorProfile?: UserProfile;
authorPosts?: PostListItem[];
toc?: TocItem[];
}
// 内置块注册表:key → 组件。数据未就绪或未知 key 返回 null(渲染层静默跳过)。
export function renderSidebarBlock(key: string, p: SidebarBlockProps) {
switch (key) {
case "board_nav":
return p.boards ? <WorkspaceNavPanel boards={p.boards} user={p.user} /> : null;
case "pulse":
return p.stats ? (
<PulseCard stats={p.stats} checkin={p.checkin} allowComments={p.allowComments} />
) : null;
case "announcements":
return p.announcements ? (
<Announcements items={p.announcements} canManageAnnouncements={p.canManageAnnouncements} />
) : null;
case "ads":
return p.ads && p.adsEnabled !== false ? (
<AdsPanel items={p.ads} title={p.adsPanelTitle || "自助推广"} enabled />
) : null;
case "sponsors":
return p.sponsors && p.sponsorsEnabled !== false ? (
<SponsorsPanel
items={p.sponsors}
title={p.sponsorsPanelTitle || "赞助商"}
enabled
/>
) : null;
case "hot":
return p.hot ? <HotRanking posts={p.hot} allowComments={p.allowComments} /> : null;
case "new_users":
return p.newUsers ? <NewUsers users={p.newUsers} /> : null;
case "active_users":
return p.activeUsers ? (
<ActiveUsers users={p.activeUsers} allowComments={p.allowComments} />
) : null;
case "friend_links":
return <FriendLinks />;
case "about_site":
return p.sidebarPages || p.canManagePages ? (
<AboutSite
pages={p.sidebarPages ?? []}
siteDescription={p.siteDescription}
canManagePages={p.canManagePages}
/>
) : null;
case "board_moderators":
return p.moderators ? <Moderators items={p.moderators} /> : null;
case "board_intro":
return p.board ? <BoardIntro board={p.board} /> : null;
case "board_pulse":
return p.boardStats ? (
<BoardPulse stats={p.boardStats} checkin={p.checkin} allowComments={p.allowComments} />
) : null;
case "board_hot":
return p.boardHot ? (
<BoardHot posts={p.boardHot} allowComments={p.allowComments} />
) : null;
case "board_active":
return p.boardActiveUsers ? (
<BoardActive users={p.boardActiveUsers} allowComments={p.allowComments} />
) : null;
case "post_board":
return p.postBoard ? <PostBoardCard board={p.postBoard} /> : null;
case "post_author":
return p.authorProfile ? (
<PostAuthorCard profile={p.authorProfile} allowComments={p.allowComments} />
) : null;
case "post_author_posts":
return p.authorPosts ? <PostAuthorPosts posts={p.authorPosts} /> : null;
case "post_toc":
return p.toc && p.toc.length > 0 ? <PostToc items={p.toc} /> : null;
default:
return null;
}
}