优化公网流量来源展示
This commit is contained in:
parent
eb31e12f32
commit
8a39e6b9e8
@ -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<string, CategoryMeta> = {
|
||||
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<string, CategoryNarrative> = {
|
||||
素材库: {
|
||||
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<DatePreset, 'custom'>; 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 (
|
||||
<div className="flex min-h-[300px] items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
@ -474,63 +567,112 @@ export default function PublicTrafficTab({ showToast }: PublicTrafficTabProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 流量来源分组(核心改造)===== */}
|
||||
{/* ===== 流量来源分组(人类视角)===== */}
|
||||
<div className="card-apple overflow-hidden">
|
||||
<div className="border-b border-gray-200 px-5 py-4 dark:border-gray-700">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">流量来源分布</h3>
|
||||
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">按资源类型分组,展示每类流量消耗和请求占比</p>
|
||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">流量都花在哪</h3>
|
||||
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">按真实使用场景归类,先看主要消耗,再看技术明细</p>
|
||||
</div>
|
||||
{primaryInsight && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
最大来源:<span className="font-medium text-gray-900 dark:text-white">{primaryInsight.narrative.title}</span>
|
||||
<span className="ml-1 tabular-nums">{primaryInsight.share.toFixed(1)}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分类条形图 */}
|
||||
<div className="space-y-4 p-5">
|
||||
{categoryTotals.length === 0 && (
|
||||
<div className="p-5">
|
||||
{categoryInsights.length === 0 && (
|
||||
<div className="py-8 text-center text-sm text-gray-400 dark:text-gray-500">当前范围没有流量记录</div>
|
||||
)}
|
||||
{categoryTotals.map((cat) => {
|
||||
const meta = getCategoryMeta(cat.category)
|
||||
const pct = responseBytes > 0 ? (cat.bytes / responseBytes) * 100 : 0
|
||||
return (
|
||||
<div key={cat.category}>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`inline-flex h-7 w-7 items-center justify-center rounded-apple ${meta.badgeClass}`}>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d={meta.icon} />
|
||||
</svg>
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{meta.label}</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{formatNumber(cat.requests)} 次请求</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm tabular-nums">
|
||||
<span className="font-semibold text-gray-900 dark:text-white">{formatBytes(cat.bytes)}</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{pct.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||
<div className={`h-full rounded-full ${meta.barColor} transition-all duration-500`} style={{ width: `${Math.max(pct, 1)}%` }} />
|
||||
</div>
|
||||
|
||||
{primaryInsight && (
|
||||
<div className="mb-5 rounded-apple border border-sky-100 bg-sky-50/70 p-4 dark:border-sky-900/50 dark:bg-sky-950/20">
|
||||
<div className="text-xs font-medium text-sky-700 dark:text-sky-300">主要结论</div>
|
||||
<div className="mt-1 text-sm text-gray-700 dark:text-gray-200">
|
||||
这段时间主要消耗在
|
||||
<span className="mx-1 font-semibold text-gray-950 dark:text-white">{primaryInsight.narrative.title}</span>
|
||||
,用了 <span className="font-semibold tabular-nums text-gray-950 dark:text-white">{formatBytes(primaryInsight.bytes)}</span>
|
||||
,占响应流量 <span className="font-semibold tabular-nums text-gray-950 dark:text-white">{primaryInsight.share.toFixed(1)}%</span>
|
||||
{primaryInsight.topPurpose && (
|
||||
<>;其中最多的是 <span className="font-medium text-gray-950 dark:text-white">{primaryInsight.topPurpose.label}</span></>
|
||||
)}
|
||||
。
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{visibleCategoryInsights.map((cat) => {
|
||||
return (
|
||||
<div key={cat.category} className="rounded-apple border border-gray-100 p-4 dark:border-gray-800">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex min-w-0 gap-3">
|
||||
<span className={`mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-apple ${cat.meta.badgeClass}`}>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d={cat.meta.icon} />
|
||||
</svg>
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-white">{cat.narrative.title}</span>
|
||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] text-gray-500 dark:bg-gray-800 dark:text-gray-400">
|
||||
{cat.signal}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">{cat.narrative.note}</div>
|
||||
{cat.topPurpose && (
|
||||
<div className="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
最大细分:{cat.topPurpose.label} · {formatBytes(cat.topPurpose.responseBytes)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-left lg:text-right">
|
||||
<div className="text-base font-semibold tabular-nums text-gray-900 dark:text-white">{formatBytes(cat.bytes)}</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{cat.share.toFixed(1)}% · {formatNumber(cat.requests)} 次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<div className="h-2 flex-1 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||
<div className={`h-full rounded-full ${cat.meta.barColor} transition-all duration-500`} style={{ width: `${Math.max(cat.share, 1)}%` }} />
|
||||
</div>
|
||||
<span className="w-20 shrink-0 text-right text-xs text-gray-400 dark:text-gray-500">{cat.narrative.action}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{hiddenCategoryInsights.length > 0 && (
|
||||
<div className="mt-4 rounded-apple border border-dashed border-gray-200 px-4 py-3 text-xs text-gray-500 dark:border-gray-800 dark:text-gray-400">
|
||||
另有 {hiddenCategoryInsights.length} 类低占比来源合计 {formatBytes(hiddenCategoryBytes)},已收进下方明细。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 每类内的用途明细(展开式)*/}
|
||||
{categoryTotals.length > 0 && (
|
||||
{categoryInsights.length > 0 && (
|
||||
<div className="border-t border-gray-100 px-5 pb-5 dark:border-gray-800">
|
||||
<details>
|
||||
<summary className="cursor-pointer py-3 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
展开各类型用途明细
|
||||
查看按接口/文件拆开的明细
|
||||
</summary>
|
||||
<div className="mt-2 space-y-3">
|
||||
{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 (
|
||||
<div key={cat.category} className="rounded-apple border border-gray-100 p-3 dark:border-gray-800">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-medium" style={{ color: undefined }}>
|
||||
<span className={`inline-block h-3 w-1 rounded-full ${meta.barColor}`} />
|
||||
{meta.label}
|
||||
<span className={`inline-block h-3 w-1 rounded-full ${cat.meta.barColor}`} />
|
||||
{cat.narrative.title}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user