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

@@ -0,0 +1,40 @@
import { Fragment } from "react";
import type { SidebarBlockRef, SidebarWidgetPublic } from "@/lib/sidebarMeta";
import { renderSidebarBlock, type SidebarBlockProps } from "./registry";
// 页面从本文件导入该类型(registry 中定义)
export type { SidebarBlockProps };
import CustomWidgetBlock from "./CustomWidgetBlock";
// 按管理端配置渲染一列侧栏块:
// - 内置块经注册表解析,未知 key / 数据缺失静默跳过;
// - 自定义工具引用已删除或停用(公开载荷不含)时静默跳过;
// - className 缺省时输出 Fragment,布局容器(gap/space-y、sticky、限高)由页面自行决定。
export default function SidebarRenderer({
blocks,
widgets,
props,
className,
}: {
blocks: SidebarBlockRef[];
widgets: SidebarWidgetPublic[];
props: SidebarBlockProps;
className?: string;
}) {
const items = blocks
.map((ref, i) => {
if (ref.widget) {
const widget = widgets.find((x) => x.id === ref.widget);
if (!widget) return null;
return <CustomWidgetBlock key={`w-${widget.id}-${i}`} widget={widget} viewer={props.user} />;
}
if (!ref.key) return null;
const node = renderSidebarBlock(ref.key, props);
if (node === null) return null;
return <Fragment key={`k-${ref.key}-${i}`}>{node}</Fragment>;
})
.filter((x) => x !== null);
if (items.length === 0) return null;
return className ? <div className={className}>{items}</div> : <>{items}</>;
}