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/sources/page.tsx
Normal file
111
frontend/app/admin/analytics/sources/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AdminChartCard, NamedBarChart, NamedPieChart } from "@/components/admin/charts";
|
||||
import { apiStatsSources, type VisitSources } from "@/lib/api";
|
||||
import { RangeSegmented, StatsState, TableEmpty, refKindLabel } from "../ui";
|
||||
|
||||
/** 来源按日表:固定展示四类主要来源列 */
|
||||
const TREND_KINDS = ["direct", "search", "social", "external"] as const;
|
||||
|
||||
export default function SourcesPage() {
|
||||
const [range, setRange] = useState<"7d" | "30d">("7d");
|
||||
const [data, setData] = useState<VisitSources | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
apiStatsSources(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 kinds = data.kinds.map((k) => ({ label: refKindLabel(k.key), count: k.count }));
|
||||
|
||||
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]">按浏览来源(Referer)自动分类;爬虫与探测请求不计入。</p>
|
||||
<RangeSegmented value={range} onChange={setRange} />
|
||||
</div>
|
||||
|
||||
{error ? <StatsState error={error} /> : null}
|
||||
|
||||
<AdminChartCard title="来源类别占比" hint={`近 ${data.range_days} 日浏览`}>
|
||||
{kinds.length ? <NamedPieChart data={kinds} /> : <TableEmpty />}
|
||||
</AdminChartCard>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<AdminChartCard title="搜索引擎明细" hint="来自搜索结果页的浏览 Top 10">
|
||||
{data.search_hosts.length ? (
|
||||
<NamedBarChart data={data.search_hosts.map((h) => ({ label: h.key, count: h.count }))} />
|
||||
) : (
|
||||
<TableEmpty text="暂无搜索引擎来源" />
|
||||
)}
|
||||
</AdminChartCard>
|
||||
<AdminChartCard title="外部链接明细" hint="其他站点跳转来源 Top 10">
|
||||
{data.external.length ? (
|
||||
<NamedBarChart data={data.external.map((h) => ({ label: h.key, count: h.count }))} />
|
||||
) : (
|
||||
<TableEmpty text="暂无外链来源" />
|
||||
)}
|
||||
</AdminChartCard>
|
||||
</div>
|
||||
|
||||
<AdminChartCard title="来源按日分布" hint="各来源类别每日浏览量">
|
||||
{data.trend.length ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-[13px]">
|
||||
<thead>
|
||||
<tr className="text-left" style={{ color: "var(--ink-3)" }}>
|
||||
<th className="py-2 pr-3 font-medium">日期</th>
|
||||
{TREND_KINDS.map((k) => (
|
||||
<th key={k} className="py-2 pr-3 font-medium text-right">
|
||||
{refKindLabel(k)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.trend.map((row) => {
|
||||
const byKey = new Map(row.items.map((i) => [i.key, i.count]));
|
||||
return (
|
||||
<tr key={row.date} style={{ borderTop: "1px solid var(--line)" }}>
|
||||
<td className="py-2 pr-3 tabular-nums" style={{ color: "var(--ink-2)" }}>
|
||||
{row.date.slice(0, 10)}
|
||||
</td>
|
||||
{TREND_KINDS.map((k) => (
|
||||
<td key={k} className="py-2 pr-3 text-right tabular-nums" style={{ color: "var(--ink)" }}>
|
||||
{(byKey.get(k) ?? 0) === 0 ? (
|
||||
<span style={{ color: "var(--ink-3)" }}>—</span>
|
||||
) : (
|
||||
(byKey.get(k) ?? 0).toLocaleString("zh-CN")
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<TableEmpty />
|
||||
)}
|
||||
</AdminChartCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user