Files
jiang13-bbs/frontend/lib/sidebarMeta.ts
freefire 7b327bc8cc feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
2026-09-24 18:53:45 +08:00

180 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 侧栏管理:内置块元数据与默认布局(管理页与前台注册表共用,勿在此引入组件)
export type SidebarScope = "home" | "board" | "post";
export interface SidebarBlockRef {
key?: string;
widget?: number;
}
export interface SidebarScopeConfig {
left?: SidebarBlockRef[] | null;
right?: SidebarBlockRef[] | null;
}
export interface SidebarConfig {
version: number;
scopes: Partial<Record<SidebarScope, SidebarScopeConfig>>;
}
export interface SidebarWidgetPublic {
id: number;
name: string;
html: string;
css: string;
js: string;
}
export interface SidebarPublicPayload {
config: SidebarConfig;
widgets: SidebarWidgetPublic[];
}
export interface SidebarWidgetRecord {
id: number;
name: string;
html: string;
css: string;
js: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
// 每侧最多放置块数(与后端 SidebarMaxBlocksPerSide 一致)
export const SIDEBAR_MAX_BLOCKS_PER_SIDE = 24;
export const SIDEBAR_SCOPE_LABELS: Record<SidebarScope, string> = {
home: "首页",
board: "板块页",
post: "帖子详情",
};
export const SIDEBAR_SIDE_LABELS: Record<"left" | "right", string> = {
left: "左栏",
right: "右栏",
};
// 各页面范围可用的内置块(key → 展示名),与后端 sidebarBuiltinKeys 保持一致
export const SIDEBAR_WIDGET_META: Record<SidebarScope, Record<string, string>> = {
home: {
board_nav: "板块目录",
pulse: "站点数据",
announcements: "站点公告",
ads: "自助推广",
sponsors: "赞助商",
hot: "热门讨论",
new_users: "最新注册",
active_users: "活跃成员",
friend_links: "友情链接",
about_site: "关于本站",
},
board: {
board_moderators: "板块管理员",
board_intro: "板块概况",
board_pulse: "板块数据",
board_hot: "本板热门",
board_active: "活跃成员",
},
post: {
post_board: "所属板块",
post_author: "作者名片",
post_author_posts: "作者其他帖",
post_toc: "文章目录",
},
};
// 内置块 icon(字符串引用,渲染方 import lucide-react 时映射)
// 保持轻量:不在 sidebarMeta 里引入组件
export const SIDEBAR_BLOCK_ICON: Record<string, string> = {
board_nav: "layout-grid",
pulse: "bar-chart-3",
announcements: "megaphone",
ads: "circle-dollar-sign",
sponsors: "handshake",
hot: "flame",
new_users: "user-plus",
active_users: "users",
friend_links: "link2",
about_site: "info",
board_moderators: "shield-check",
board_intro: "file-text",
board_pulse: "bar-chart-3",
board_hot: "flame",
board_active: "users",
post_board: "layers",
post_author: "user",
post_author_posts: "pen-line",
post_toc: "list",
};
// 默认布局(= 历史硬编码渲染;后端 DefaultSidebarConfig 的前端镜像)
export function defaultSidebarConfig(): SidebarConfig {
const refs = (keys: string[]): SidebarBlockRef[] => keys.map((key) => ({ key }));
return {
version: 1,
scopes: {
home: {
left: refs(["board_nav"]),
right: refs([
"pulse",
"announcements",
"ads",
"sponsors",
"hot",
"new_users",
"active_users",
"friend_links",
"about_site",
]),
},
board: {
left: [],
right: refs(["board_moderators", "board_intro", "board_pulse", "board_hot", "board_active"]),
},
post: {
left: refs(["post_board", "post_author", "post_author_posts"]),
right: refs(["post_toc"]),
},
},
};
}
// 按页面范围解析左右两列的块列表:
// - 未配置(nil)的侧回落默认布局,显式空数组 = 站长清空该侧;
// - board 范围左栏跟随 home.left(板块态仅管理右栏)。
export function sidebarScopeLists(
config: SidebarConfig,
scope: SidebarScope
): { left: SidebarBlockRef[]; right: SidebarBlockRef[] } {
const def = defaultSidebarConfig().scopes;
if (scope === "board") {
return {
left: config.scopes.home?.left ?? def.home?.left ?? [],
right: config.scopes.board?.right ?? def.board?.right ?? [],
};
}
return {
left: config.scopes[scope]?.left ?? def[scope]?.left ?? [],
right: config.scopes[scope]?.right ?? def[scope]?.right ?? [],
};
}
// 块的展示名:内置取 META,自定义取备注名;未知返回 null(渲染时跳过)
export function sidebarBlockLabel(
ref: SidebarBlockRef,
widgets: { id: number; name: string }[]
): { label: string; custom: boolean } | null {
if (ref.widget) {
const w = widgets.find((x) => x.id === ref.widget);
return w ? { label: w.name, custom: true } : null;
}
if (ref.key) {
for (const scope of Object.keys(SIDEBAR_WIDGET_META) as SidebarScope[]) {
const label = SIDEBAR_WIDGET_META[scope][ref.key];
if (label) return { label, custom: false };
}
}
return null;
}