Files
jiang13-bbs/frontend/app/admin/analytics/sources/page.tsx
freefire 3e0689fe98 fix: 管理会话自动恢复与访问来源统计优化
- 新增 AdminSessionRecover:后台离线/后端重启时自动重试恢复会话,明确失效才回跳登录
- 认证 cookie 契约调整(SameSite=Lax 与刷新轮转适配),路由与守卫适配
- 访问来源统计查询优化与测试、管理端 analytics sources 页面适配
- 移动端布局修正:覆盖 body min-h-screen 避免内容区高度异常
2026-09-28 03:33:52 +08:00

126 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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, formatInt, 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="站内搜索词 Top 10" hint="搜索结果页(/?q=)浏览量 Top 10">
{data.search_terms.length ? (
<ul className="flex flex-col divide-y max-h-[360px] overflow-y-auto" style={{ borderColor: "var(--line)" }}>
{data.search_terms.map((t, i) => (
<li key={`${i}-${t.key}`} className="py-2 flex items-center gap-3 text-[13px]">
<span className="w-5 text-right tabular-nums shrink-0" style={{ color: "var(--ink-3)" }}>
{i + 1}
</span>
<span className="flex-1 min-w-0 truncate" style={{ color: "var(--ink)" }} title={t.key}>
{t.key}
</span>
<span className="tabular-nums shrink-0" style={{ color: "var(--ink-2)" }}>
{formatInt(t.count)}
</span>
</li>
))}
</ul>
) : (
<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>
);
}