feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
179
frontend/lib/sidebarMeta.ts
Normal file
179
frontend/lib/sidebarMeta.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
// 侧栏管理:内置块元数据与默认布局(管理页与前台注册表共用,勿在此引入组件)
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user