- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
119 lines
4.4 KiB
TypeScript
119 lines
4.4 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useState } from "react";
|
||
import { Activity, Zap, Users, Clock } from "lucide-react";
|
||
import { AdminStatCard } from "@/components/admin";
|
||
import { AdminChartCard, SingleAreaChart } from "@/components/admin/charts";
|
||
import { apiStatsRealtime, type VisitRealtime } from "@/lib/api";
|
||
import { CardShell, StatsState, TableEmpty, deviceLabel, formatInt, kindLabel, usePolling } from "../ui";
|
||
|
||
export default function RealtimePage() {
|
||
const [data, setData] = useState<VisitRealtime | null>(null);
|
||
const [error, setError] = useState("");
|
||
|
||
const load = useCallback(() => {
|
||
apiStatsRealtime()
|
||
.then((d) => {
|
||
setData(d);
|
||
setError("");
|
||
})
|
||
.catch(() => setError("加载失败,请稍后重试"));
|
||
}, []);
|
||
usePolling(load, 15_000);
|
||
|
||
if (!data) return <StatsState loading={!error} error={error || undefined} />;
|
||
|
||
// 兜底:旧后端可能返回 null(Go nil 切片)
|
||
const minutes = (data.minutes ?? []).map((m) => ({
|
||
// SingleAreaChart 取 date 字段画 X 轴:只留 HH:MM
|
||
date: m.minute.length >= 16 ? m.minute.slice(11) : m.minute,
|
||
pv: m.pv,
|
||
}));
|
||
const topPages = data.top_pages ?? [];
|
||
const recent = data.recent ?? [];
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
<p className="meta text-[12.5px]">每 15 秒自动刷新;切回前台标签页时立即刷新。</p>
|
||
{error ? <StatsState error={error} /> : null}
|
||
|
||
<div className="grid gap-3 grid-cols-2 xl:grid-cols-4">
|
||
<AdminStatCard label="当前在线" value={formatInt(data.online)} hint="近 5 分钟活跃用户" icon={<Users size={17} />} />
|
||
<AdminStatCard
|
||
label="近 5 分钟 PV"
|
||
value={formatInt(data.pv_5min)}
|
||
icon={<Zap size={17} />}
|
||
color="var(--ok)"
|
||
soft="var(--ok-soft, var(--panel-2))"
|
||
/>
|
||
<AdminStatCard
|
||
label="近 30 分钟 PV"
|
||
value={formatInt(data.pv_30min)}
|
||
icon={<Activity size={17} />}
|
||
color="var(--gold)"
|
||
soft="var(--gold-soft)"
|
||
/>
|
||
<AdminStatCard
|
||
label="数据延迟"
|
||
value="< 3s"
|
||
hint="事件攒批落库周期"
|
||
icon={<Clock size={17} />}
|
||
color="var(--ink-2)"
|
||
soft="var(--panel-2)"
|
||
/>
|
||
</div>
|
||
|
||
<AdminChartCard title="近 60 分钟浏览" hint="分钟级 PV">
|
||
{minutes.length ? (
|
||
<SingleAreaChart data={minutes} dataKey="pv" name="PV" />
|
||
) : (
|
||
<TableEmpty text="暂无浏览数据" />
|
||
)}
|
||
</AdminChartCard>
|
||
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<CardShell title="近 5 分钟热门路径">
|
||
{topPages.length ? (
|
||
<ul className="flex flex-col divide-y" style={{ borderColor: "var(--line)" }}>
|
||
{topPages.map((p) => (
|
||
<li key={p.key} className="py-2 flex items-center gap-3 text-[13px]">
|
||
<span className="flex-1 min-w-0 truncate" style={{ color: "var(--ink)" }}>
|
||
{p.key || "/"}
|
||
</span>
|
||
<span className="tabular-nums shrink-0" style={{ color: "var(--ink-2)" }}>
|
||
{formatInt(p.count)}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<TableEmpty />
|
||
)}
|
||
</CardShell>
|
||
|
||
<CardShell title="最新浏览" hint="最多 20 条">
|
||
{recent.length ? (
|
||
<ul className="flex flex-col divide-y max-h-[420px] overflow-y-auto" style={{ borderColor: "var(--line)" }}>
|
||
{recent.map((e) => (
|
||
<li key={e.id} className="py-2 flex items-center gap-2.5 text-[13px]">
|
||
<span className="w-[4.5rem] shrink-0 tabular-nums text-[11.5px]" style={{ color: "var(--ink-3)" }}>
|
||
{new Date(e.created_at).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })}
|
||
</span>
|
||
<span className="flex-1 min-w-0 truncate" style={{ color: "var(--ink)" }} title={e.path}>
|
||
{e.path || "/"}
|
||
</span>
|
||
<span className="shrink-0 text-[11.5px]" style={{ color: "var(--ink-3)" }}>
|
||
{deviceLabel(e.device)} · {kindLabel(e.kind)}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<TableEmpty />
|
||
)}
|
||
</CardShell>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|