feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user