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,74 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useRef, type KeyboardEvent, type ReactNode } from "react";
import { BarChart3 } from "lucide-react";
import { AdminPageHeader } from "@/components/admin";
export const ANALYTICS_TABS: { href: string; label: string }[] = [
{ href: "/admin/analytics", label: "概览" },
{ href: "/admin/analytics/realtime", label: "实时" },
{ href: "/admin/analytics/sources", label: "来源分析" },
{ href: "/admin/analytics/pages", label: "受访页面" },
{ href: "/admin/analytics/visitors", label: "访客画像" },
{ href: "/admin/analytics/security", label: "爬虫与安全" },
{ href: "/admin/analytics/details", label: "访问明细" },
{ href: "/admin/analytics/settings", label: "设置" },
];
export default function AnalyticsShell({ children }: { children: ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const navRefs = useRef<(HTMLAnchorElement | null)[]>([]);
// ←→/Home/End 键盘导航,范式同 SettingsShell(纵向改横向)
const onNavKeyDown = (e: KeyboardEvent<HTMLAnchorElement>, i: number) => {
if (
e.key !== "ArrowRight" &&
e.key !== "ArrowLeft" &&
e.key !== "Home" &&
e.key !== "End"
) {
return;
}
e.preventDefault();
let next = i;
if (e.key === "ArrowRight") next = (i + 1) % ANALYTICS_TABS.length;
else if (e.key === "ArrowLeft") next = (i + ANALYTICS_TABS.length - 1) % ANALYTICS_TABS.length;
else if (e.key === "Home") next = 0;
else next = ANALYTICS_TABS.length - 1;
navRefs.current[next]?.focus();
router.push(ANALYTICS_TABS[next].href);
};
return (
<div className="admin-analytics-page">
<AdminPageHeader
icon={BarChart3}
title="访问统计"
description="站点流量、来源、访客与安全事件的采集与分析"
/>
<nav className="admin-analytics-tabs" aria-label="访问统计分类">
{ANALYTICS_TABS.map((item, i) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
ref={(el) => {
navRefs.current[i] = el;
}}
href={item.href}
className="admin-analytics-tab"
aria-current={active ? "page" : undefined}
onKeyDown={(e) => onNavKeyDown(e, i)}
>
{item.label}
</Link>
);
})}
</nav>
{children}
</div>
);
}