From 8a39e6b9e84c8d85a16b883ba3d412a7a1359de7 Mon Sep 17 00:00:00 2001 From: chenpei Date: Wed, 8 Jul 2026 15:26:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=85=AC=E7=BD=91=E6=B5=81?= =?UTF-8?q?=E9=87=8F=E6=9D=A5=E6=BA=90=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/PublicTrafficTab.tsx | 222 +++++++++++++++++++++----- 1 file changed, 182 insertions(+), 40 deletions(-) diff --git a/components/admin/PublicTrafficTab.tsx b/components/admin/PublicTrafficTab.tsx index 26a2cca9..8c49ea08 100644 --- a/components/admin/PublicTrafficTab.tsx +++ b/components/admin/PublicTrafficTab.tsx @@ -118,6 +118,7 @@ function formatTime(value: string | null) { // 类别中文映射 + 图标 + 颜色。后端目前返回的是中文 category,这里兼容旧英文分类。 type CategoryMeta = { label: string; icon: string; barColor: string; badgeClass: string } +type CategoryNarrative = { title: string; note: string; action: string } const CATEGORY_META: Record = { image_direct: { @@ -226,9 +227,80 @@ function getCategoryMeta(category: string) { return { ...CATEGORY_META.other, label: key } } -// 简化类别名(用于分组标题) -function shortCategoryLabel(category: string) { - return getCategoryMeta(category).label +function getCategoryNarrative(category: string): CategoryNarrative { + const key = normalizeCategoryKey(category) + const narratives: Record = { + 素材库: { + title: '素材库内浏览/下载', + note: '来自素材列表、详情、下载、考核调研等站内使用。', + action: '通常是正常业务流量', + }, + 历史上传资源: { + title: '历史上传图片/文件', + note: '旧上传目录里的图片、视频、压缩包被直接打开。', + action: '适合看 Top 资源', + }, + 资源直链: { + title: '图片/文件直链访问', + note: '用户或外部页面直接打开素材文件。', + action: '适合排查大文件', + }, + 直传资源: { + title: '直传资源访问', + note: '新直传目录里的图片、视频、压缩包被访问。', + action: '关注是否集中', + }, + 产品设计平台: { + title: '产品设计平台调用', + note: '产品设计平台访问素材库接口或取图。', + action: '看对接频率', + }, + 外部接口: { + title: '外部平台接口', + note: '外部系统调用素材库接口产生的流量。', + action: '看调用来源', + }, + 后台: { + title: '后台管理操作', + note: '管理员在后台查看、管理、导出等操作。', + action: '低频为正常', + }, + 账号: { + title: '登录与认证', + note: '账号登录、钉钉认证相关请求。', + action: '通常很小', + }, + 页面加载: { + title: '页面打开资源', + note: '前端页面脚本、样式等静态资源。', + action: '可看缓存效果', + }, + 资源文件: { + title: '页面静态文件', + note: '站点公共静态文件访问。', + action: '通常很小', + }, + 其他: { + title: '未归类流量', + note: '暂时没有归入明确业务场景的访问。', + action: '占比高时再拆', + }, + other: { + title: '未归类流量', + note: '暂时没有归入明确业务场景的访问。', + action: '占比高时再拆', + }, + } + + return narratives[key] || { title: getCategoryMeta(key).label, note: '按当前规则识别出的访问来源。', action: '查看明细' } +} + +function getCategorySignal(category: string, share: number, rank: number) { + const key = normalizeCategoryKey(category) + if (rank === 0 && share >= 50) return '主要来源' + if (['历史上传资源', '资源直链', '直传资源'].includes(key) && share >= 10) return '建议查看' + if (share < 1) return '低占比' + return '正常' } const PRESET_OPTIONS: Array<{ key: Exclude; label: string }> = [ @@ -366,6 +438,27 @@ export default function PublicTrafficTab({ showToast }: PublicTrafficTabProps) { return Array.from(map.values()).sort((a, b) => b.bytes - a.bytes) }, [visibleStats]) + const categoryInsights = useMemo(() => { + return categoryTotals.map((cat, index) => { + const items: PublicTrafficStats['byPurpose'][number][] = purposeByCategory.get(cat.category) || [] + const share = responseBytes > 0 ? (cat.bytes / responseBytes) * 100 : 0 + return { + ...cat, + rank: index + 1, + share, + meta: getCategoryMeta(cat.category), + narrative: getCategoryNarrative(cat.category), + signal: getCategorySignal(cat.category, share, index), + topPurpose: items[0] || null, + } + }) + }, [categoryTotals, purposeByCategory, responseBytes]) + + const primaryInsight = categoryInsights[0] + const visibleCategoryInsights = categoryInsights.filter((cat) => cat.rank <= 5 || cat.share >= 1) + const hiddenCategoryInsights = categoryInsights.filter((cat) => !visibleCategoryInsights.includes(cat)) + const hiddenCategoryBytes = hiddenCategoryInsights.reduce((total, cat) => total + cat.bytes, 0) + if (!visibleStats) { return (
@@ -474,63 +567,112 @@ export default function PublicTrafficTab({ showToast }: PublicTrafficTabProps) {
- {/* ===== 流量来源分组(核心改造)===== */} + {/* ===== 流量来源分组(人类视角)===== */}
-

流量来源分布

-

按资源类型分组,展示每类流量消耗和请求占比

+
+
+

流量都花在哪

+

按真实使用场景归类,先看主要消耗,再看技术明细

+
+ {primaryInsight && ( +
+ 最大来源:{primaryInsight.narrative.title} + {primaryInsight.share.toFixed(1)}% +
+ )} +
- {/* 分类条形图 */} -
- {categoryTotals.length === 0 && ( +
+ {categoryInsights.length === 0 && (
当前范围没有流量记录
)} - {categoryTotals.map((cat) => { - const meta = getCategoryMeta(cat.category) - const pct = responseBytes > 0 ? (cat.bytes / responseBytes) * 100 : 0 - return ( -
-
-
- - - - - - {meta.label} - {formatNumber(cat.requests)} 次请求 -
-
- {formatBytes(cat.bytes)} - {pct.toFixed(1)}% -
-
-
-
-
+ + {primaryInsight && ( +
+
主要结论
+
+ 这段时间主要消耗在 + {primaryInsight.narrative.title} + ,用了 {formatBytes(primaryInsight.bytes)} + ,占响应流量 {primaryInsight.share.toFixed(1)}% + {primaryInsight.topPurpose && ( + <>;其中最多的是 {primaryInsight.topPurpose.label} + )} + 。
- ) - })} +
+ )} + +
+ {visibleCategoryInsights.map((cat) => { + return ( +
+
+
+ + + + + +
+
+ {cat.narrative.title} + + {cat.signal} + +
+
{cat.narrative.note}
+ {cat.topPurpose && ( +
+ 最大细分:{cat.topPurpose.label} · {formatBytes(cat.topPurpose.responseBytes)} +
+ )} +
+
+
+
{formatBytes(cat.bytes)}
+
+ {cat.share.toFixed(1)}% · {formatNumber(cat.requests)} 次 +
+
+
+ +
+
+
+
+ {cat.narrative.action} +
+
+ ) + })} +
+ + {hiddenCategoryInsights.length > 0 && ( +
+ 另有 {hiddenCategoryInsights.length} 类低占比来源合计 {formatBytes(hiddenCategoryBytes)},已收进下方明细。 +
+ )}
{/* 每类内的用途明细(展开式)*/} - {categoryTotals.length > 0 && ( + {categoryInsights.length > 0 && (
- 展开各类型用途明细 + 查看按接口/文件拆开的明细
- {categoryTotals.map((cat) => { - const meta = getCategoryMeta(cat.category) + {categoryInsights.map((cat) => { const items: PublicTrafficStats['byPurpose'][number][] = purposeByCategory.get(cat.category) || [] if (items.length === 0) return null return (
- - {meta.label} + + {cat.narrative.title}
{items.map((item) => {