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,54 @@
"use client";
import { useEffect, useRef } from "react";
import type { SidebarWidgetPublic, User } from "@/lib/api";
import { scopeWidgetCss } from "@/lib/widgetCode";
import { ensureWidgetApi } from "@/lib/widgetApi";
// 站长自定义工具块:HTML/CSS 随 SSR 直出(dangerouslySetInnerHTML),
// JS 在客户端挂载时注入执行、卸载时移除。站长可信内容,不做消毒/沙箱。
// CSS 经 scopeWidgetCss 限定在 `.j13-widget-{id}` 容器内,不污染页面其他区域;
// 工具内可直接用站点主题变量(var(--accent) 等)与 window.J13 接口。
export default function CustomWidgetBlock({
widget,
viewer,
}: {
widget: SidebarWidgetPublic;
viewer?: User | null;
}) {
const hostRef = useRef<HTMLDivElement>(null);
// 先于站长 JS 建立站点接口(声明顺序保证本 effect 先执行)
useEffect(() => {
ensureWidgetApi(viewer);
}, [viewer]);
useEffect(() => {
const host = hostRef.current;
if (!host || !widget.js) return;
const script = document.createElement("script");
script.dataset.widgetScript = String(widget.id);
script.textContent = widget.js;
host.appendChild(script);
return () => {
script.remove();
};
}, [widget.id, widget.js]);
return (
<section
className={`j13-widget-body j13-widget-${widget.id}`}
data-widget-id={widget.id}
data-widget-name={widget.name}
>
{widget.css ? (
<style dangerouslySetInnerHTML={{ __html: scopeWidgetCss(widget.css, widget.id) }} />
) : null}
{widget.html ? (
<div className="j13-widget-html" dangerouslySetInnerHTML={{ __html: widget.html }} />
) : null}
{/* 自定义 JS 的宿主节点:脚本由客户端创建后挂到此处执行 */}
<div ref={hostRef} />
</section>
);
}