feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
74
frontend/app/admin/analytics/AnalyticsShell.tsx
Normal file
74
frontend/app/admin/analytics/AnalyticsShell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user