- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
180 lines
4.9 KiB
TypeScript
180 lines
4.9 KiB
TypeScript
// 侧栏管理:内置块元数据与默认布局(管理页与前台注册表共用,勿在此引入组件)
|
||
|
||
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;
|
||
}
|