feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
111
frontend/app/admin/analytics/visitors/page.tsx
Normal file
111
frontend/app/admin/analytics/visitors/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Users, UserPlus, Repeat, LogIn } from "lucide-react";
|
||||
import { AdminStatCard } from "@/components/admin";
|
||||
import { AdminChartCard, NamedBarChart, NamedPieChart } from "@/components/admin/charts";
|
||||
import { apiStatsVisitors, type VisitVisitors } from "@/lib/api";
|
||||
import { RangeSegmented, StatsState, TableEmpty, formatInt } from "../ui";
|
||||
|
||||
export default function VisitorsPage() {
|
||||
const [range, setRange] = useState<"7d" | "30d">("7d");
|
||||
const [data, setData] = useState<VisitVisitors | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
apiStatsVisitors(range)
|
||||
.then((d) => {
|
||||
if (alive) {
|
||||
setData(d);
|
||||
setError("");
|
||||
}
|
||||
})
|
||||
.catch(() => alive && setError("加载失败,请稍后重试"))
|
||||
.finally(() => alive && setLoading(false));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [range]);
|
||||
|
||||
if (!data) return <StatsState loading={loading} error={error || undefined} />;
|
||||
|
||||
const devices = data.devices.map((d) => ({ label: d.label || "未知", count: d.count }));
|
||||
const oses = data.oses.map((d) => ({ label: d.label || "未知", count: d.count }));
|
||||
const browsers = data.browsers.map((d) => ({ label: d.label || "未知", count: d.count }));
|
||||
const hourly = data.hourly.map((h) => ({ label: `${h.key}时`, count: h.count }));
|
||||
const totalPv = data.logged_in_pv + data.guest_pv;
|
||||
const totalVisitors = data.new_visitors + data.returning;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<p className="meta text-[12.5px]">基于匿名访客标识(仅存哈希)与登录用户统计。</p>
|
||||
<RangeSegmented value={range} onChange={setRange} />
|
||||
</div>
|
||||
|
||||
{error ? <StatsState error={error} /> : null}
|
||||
|
||||
<div className="grid gap-3 grid-cols-2 xl:grid-cols-4">
|
||||
<AdminStatCard label="登录用户浏览" value={formatInt(data.logged_in_pv)} icon={<LogIn size={17} />} />
|
||||
<AdminStatCard
|
||||
label="游客浏览"
|
||||
value={formatInt(data.guest_pv)}
|
||||
icon={<Users size={17} />}
|
||||
color="var(--ok)"
|
||||
soft="var(--ok-soft, var(--panel-2))"
|
||||
/>
|
||||
<AdminStatCard
|
||||
label="新访客"
|
||||
value={formatInt(data.new_visitors)}
|
||||
sub={totalVisitors > 0 ? `${Math.round((data.new_visitors / totalVisitors) * 100)}%` : undefined}
|
||||
hint="统计期内首次出现"
|
||||
icon={<UserPlus size={17} />}
|
||||
color="var(--gold)"
|
||||
soft="var(--gold-soft)"
|
||||
/>
|
||||
<AdminStatCard
|
||||
label="回访"
|
||||
value={formatInt(data.returning)}
|
||||
sub={totalVisitors > 0 ? `${Math.round((data.returning / totalVisitors) * 100)}%` : undefined}
|
||||
icon={<Repeat size={17} />}
|
||||
color="var(--ink-2)"
|
||||
soft="var(--panel-2)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{totalPv > 0 ? (
|
||||
<AdminChartCard title="登录 vs 游客" hint={`近 ${data.range_days} 日浏览占比`}>
|
||||
<NamedPieChart
|
||||
data={[
|
||||
{ label: "登录用户", count: data.logged_in_pv },
|
||||
{ label: "游客", count: data.guest_pv },
|
||||
]}
|
||||
/>
|
||||
</AdminChartCard>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<AdminChartCard title="设备分布">
|
||||
{devices.length ? <NamedPieChart data={devices} /> : <TableEmpty />}
|
||||
</AdminChartCard>
|
||||
<AdminChartCard title="操作系统 Top 8">
|
||||
{oses.length ? <NamedPieChart data={oses} /> : <TableEmpty />}
|
||||
</AdminChartCard>
|
||||
<AdminChartCard title="浏览器 Top 8">
|
||||
{browsers.length ? <NamedPieChart data={browsers} /> : <TableEmpty />}
|
||||
</AdminChartCard>
|
||||
</div>
|
||||
|
||||
<AdminChartCard title="24 小时活跃分布" hint="各小时浏览量(本地时区)">
|
||||
{hourly.length ? (
|
||||
<NamedBarChart data={hourly} />
|
||||
) : (
|
||||
<TableEmpty />
|
||||
)}
|
||||
</AdminChartCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user