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

@@ -6,9 +6,13 @@ import {
fetchPosts,
fetchOverview,
fetchBoardSidebar,
fetchSidebar,
type OverviewResponse,
type BoardSidebarResponse,
type SidebarPublicPayload,
} from "@/lib/api";
import { defaultSidebarConfig, sidebarScopeLists } from "@/lib/sidebarMeta";
import SidebarRenderer, { type SidebarBlockProps } from "@/components/sidebar/SidebarRenderer";
import { authCookieHeader } from "@/lib/cookies";
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
import { siteDocumentTitle } from "@/lib/brand";
@@ -17,14 +21,9 @@ import Pagination from "@/components/Pagination";
import PostRow from "@/components/PostRow";
import FeedTabs from "@/components/home/FeedTabs";
import FeedNewTip from "@/components/home/FeedNewTip";
import { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
import { WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
import { SiteDocBreadcrumb } from "@/components/site-doc";
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
import {
BoardPanels,
BoardPanelsMobile,
BoardMissingPanel,
} from "@/components/home/BoardPanels";
import { BoardMissingPanel } from "@/components/home/BoardPanels";
import { requestCanonical } from "@/lib/canonicalMeta";
import { defaultFeedSort, homeCanonicalQuery, normalizeFeedSort } from "@/lib/siteUrl";
import { listingJsonLd } from "@/lib/jsonLd";
@@ -107,13 +106,19 @@ export default async function HomePage({ searchParams }: HomePageProps) {
boardId > 0 && !isSearch
? fetchBoardSidebar(boardId, cookie).catch(() => null)
: Promise.resolve(null);
// 侧栏布局配置失败不阻塞页面,回落默认布局
const sidebarPromise: Promise<SidebarPublicPayload | null> = isSearch
? Promise.resolve(null)
: fetchSidebar(cookie).catch(() => null);
const [me, postsData, overview, boardSidebar] = await Promise.all([
const [me, postsData, overview, boardSidebar, sidebarData] = await Promise.all([
mePromise,
postsPromise,
overviewPromise,
boardSidebarPromise,
sidebarPromise,
]);
const sidebar = sidebarData ?? { config: defaultSidebarConfig(), widgets: [] };
const user = me.user;
const boards = overview?.boards ?? [];
const canManageAnnouncements = !!user && isAdminOrAbove(user.role);
@@ -124,6 +129,48 @@ export default async function HomePage({ searchParams }: HomePageProps) {
// boardId 有效但接口失败 → 显示简卡;成功则板级右栏;否则全站右栏
const boardPanelsMissing = showBoardPanels && !boardSidebar;
// 侧栏布局:板块态右栏走 board 配置(左栏跟随首页),其余走 home 配置
const scopeLists = sidebarScopeLists(sidebar.config, showBoardPanels ? "board" : "home");
const sidebarProps: SidebarBlockProps = {
allowComments,
user,
boards,
checkin: (boardSidebar ?? overview)?.checkin ?? undefined,
...(overview
? {
stats: overview.stats,
announcements: overview.announcements,
canManageAnnouncements,
ads: overview.ads ?? [],
adsPanelTitle: overview.ads_panel_title || "自助推广",
adsEnabled: overview.ads_enabled !== false,
sponsors: overview.sponsors ?? [],
sponsorsPanelTitle: overview.sponsors_panel_title || "赞助商",
sponsorsEnabled: overview.sponsors_enabled !== false,
hot: overview.hot,
newUsers: overview.new_users,
activeUsers: overview.active_users,
}
: {}),
canManagePages,
sidebarPages,
siteDescription: publicSettings.site_description,
...(boardSidebar
? {
board: boardSidebar.board,
moderators: boardSidebar.moderators,
boardStats: boardSidebar.stats,
boardHot: boardSidebar.hot,
boardActiveUsers: boardSidebar.active_users,
}
: {}),
};
// 窄屏堆叠:右栏列表 + 左栏列表(板块目录已在顶部有横向板块条,跳过)
const mobileBlocks = [
...scopeLists.right,
...scopeLists.left.filter((r) => r.key !== "board_nav"),
];
const pageName = isSearch
? `搜索“${keyword}”`
: activeBoard
@@ -189,7 +236,12 @@ export default async function HomePage({ searchParams }: HomePageProps) {
<WorkspaceNavMobile boards={boards} user={user} />
<div className={`flex items-start gap-6 xl:gap-7 ${activeBoard ? "" : "mt-5 lg:mt-0"}`}>
<WorkspaceNav boards={boards} user={user} />
{/* 左栏:按配置渲染(缺省 = 板块目录) */}
{scopeLists.left.length > 0 && (
<aside className="hidden lg:block w-56 xl:w-60 shrink-0 sticky top-[var(--rail-top)] max-h-[var(--rail-max-h)]">
<SidebarRenderer blocks={scopeLists.left} widgets={sidebar.widgets} props={sidebarProps} />
</aside>
)}
{/* 中栏:tab 栏与帖子流为同一个整体卡片(左栏切板块只替换这一列) */}
<div className="flex-1 min-w-0">
@@ -247,85 +299,44 @@ export default async function HomePage({ searchParams }: HomePageProps) {
/>
</div>
{/* 右栏:选中板块时切板级面板,否则全站社区面板 */}
{/* 右栏:选中板块时切板级面板,否则全站社区面板;均按配置渲染 */}
{showBoardPanels ? (
boardSidebar ? (
<BoardPanels
board={boardSidebar.board}
moderators={boardSidebar.moderators}
stats={boardSidebar.stats}
hot={boardSidebar.hot}
activeUsers={boardSidebar.active_users}
checkin={boardSidebar.checkin ?? undefined}
allowComments={allowComments}
/>
scopeLists.right.length > 0 ? (
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="板块面板">
<SidebarRenderer
blocks={scopeLists.right}
widgets={sidebar.widgets}
props={sidebarProps}
className="space-y-4"
/>
</aside>
) : null
) : boardPanelsMissing ? (
<BoardMissingPanel />
) : null
) : (
overview && (
<CommunityPanels
stats={overview.stats}
hot={overview.hot}
activeUsers={overview.active_users}
announcements={overview.announcements}
newUsers={overview.new_users}
checkin={overview.checkin ?? undefined}
allowComments={allowComments}
canManageAnnouncements={canManageAnnouncements}
canManagePages={canManagePages}
sidebarPages={sidebarPages}
siteDescription={publicSettings.site_description}
ads={overview.ads ?? []}
adsPanelTitle={overview.ads_panel_title || "自助推广"}
adsEnabled={overview.ads_enabled !== false}
sponsors={overview.sponsors ?? []}
sponsorsPanelTitle={overview.sponsors_panel_title || "赞助商"}
sponsorsEnabled={overview.sponsors_enabled !== false}
) : overview && scopeLists.right.length > 0 ? (
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="社区面板">
<SidebarRenderer
blocks={scopeLists.right}
widgets={sidebar.widgets}
props={sidebarProps}
className="space-y-4"
/>
)
)}
</aside>
) : null}
</div>
{/* 窄屏:面板下沉到帖子流之后 */}
{showBoardPanels ? (
boardSidebar ? (
<div className="mt-6">
<BoardPanelsMobile
board={boardSidebar.board}
moderators={boardSidebar.moderators}
stats={boardSidebar.stats}
hot={boardSidebar.hot}
activeUsers={boardSidebar.active_users}
checkin={boardSidebar.checkin ?? undefined}
allowComments={allowComments}
/>
</div>
) : null
) : (
overview && (
<div className="mt-6">
<CommunityPanelsMobile
stats={overview.stats}
hot={overview.hot}
activeUsers={overview.active_users}
announcements={overview.announcements}
newUsers={overview.new_users}
checkin={overview.checkin ?? undefined}
allowComments={allowComments}
canManageAnnouncements={canManageAnnouncements}
canManagePages={canManagePages}
sidebarPages={sidebarPages}
siteDescription={publicSettings.site_description}
ads={overview.ads ?? []}
adsPanelTitle={overview.ads_panel_title || "自助推广"}
adsEnabled={overview.ads_enabled !== false}
sponsors={overview.sponsors ?? []}
sponsorsPanelTitle={overview.sponsors_panel_title || "赞助商"}
sponsorsEnabled={overview.sponsors_enabled !== false}
/>
</div>
)
{/* 窄屏:面板下沉到帖子流之后(右栏列表 + 左栏列表,板块目录除外) */}
{mobileBlocks.length > 0 && (
<div className="mt-6 lg:hidden">
<SidebarRenderer
blocks={mobileBlocks}
widgets={sidebar.widgets}
props={sidebarProps}
className="space-y-4"
/>
</div>
)}
</div>
);