fix: 对齐监控概览双通道口径文案与诊断抽样
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import china from '@svg-maps/china';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buildChinaRegionCountMap, heatFill, emptyFill } from './monitorMapUtils';
|
||||
import { buildChinaRegionCountMap, heatFill } from './monitorMapUtils';
|
||||
|
||||
type Loc = { id: string; name: string; path: string };
|
||||
|
||||
@@ -15,10 +15,19 @@ type RegionStat = {
|
||||
type Props = {
|
||||
regions: RegionStat[];
|
||||
className?: string;
|
||||
/** 近 30 日是否已有国家级浏览量 */
|
||||
hasCountryHits?: boolean;
|
||||
/** 近 30 日是否已有省级浏览量 */
|
||||
hasRegionHits?: boolean;
|
||||
};
|
||||
|
||||
/** 中国省区地图:按 BIN 解析出的省/区填色 */
|
||||
export default function MonitorChinaMap({ regions, className }: Props) {
|
||||
export default function MonitorChinaMap({
|
||||
regions,
|
||||
className,
|
||||
hasCountryHits = false,
|
||||
hasRegionHits = false,
|
||||
}: Props) {
|
||||
const [hover, setHover] = useState<{ name: string; count: number } | null>(null);
|
||||
const counts = useMemo(() => buildChinaRegionCountMap(regions), [regions]);
|
||||
const max = useMemo(() => Math.max(0, ...Object.values(counts)), [counts]);
|
||||
@@ -26,6 +35,16 @@ export default function MonitorChinaMap({ regions, className }: Props) {
|
||||
const locations = (china as { locations: Loc[]; viewBox: string }).locations;
|
||||
const viewBox = (china as { viewBox: string }).viewBox;
|
||||
|
||||
let emptyTitle = '暂无省级访问数据';
|
||||
let emptyHint = '放置 IP2LOCATION-LITE-DB3.BIN 后按省/区填色';
|
||||
if (hasCountryHits && !hasRegionHits) {
|
||||
emptyTitle = '有国家浏览量,尚无省级数据';
|
||||
emptyHint = '需前台路由产生带省字段的浏览量;仅请求日志不会点亮中国地图。请确认已放置 IP2Location DB3 BIN。';
|
||||
} else if (!hasCountryHits) {
|
||||
emptyTitle = '暂无浏览量地理数据';
|
||||
emptyHint = '需前台路由产生浏览量后才会按省填色;仅请求日志不会点亮此处。';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('admin-monitor-svg-wrap', className)}>
|
||||
<svg
|
||||
@@ -63,8 +82,8 @@ export default function MonitorChinaMap({ regions, className }: Props) {
|
||||
)}
|
||||
{!hasData && (
|
||||
<div className="admin-monitor-map-empty-overlay">
|
||||
<p>暂无省级访问数据</p>
|
||||
<p>放置 IP2LOCATION-LITE-DB3.BIN 后按省/区填色</p>
|
||||
<p>{emptyTitle}</p>
|
||||
<p>{emptyHint}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -79,23 +79,29 @@ function GeoRankTable({
|
||||
cities,
|
||||
asns,
|
||||
mode,
|
||||
mapMode,
|
||||
rankMode,
|
||||
onRankMode,
|
||||
hasCountryHits,
|
||||
}: {
|
||||
cities: MonitorCityItem[];
|
||||
asns: MonitorASNItem[];
|
||||
mode: MapMode;
|
||||
rankMode: RankMode;
|
||||
onRankMode: (m: RankMode) => void;
|
||||
/** 近 30 日是否已有国家级浏览量(用于区分「无 PV 地理」与缺库) */
|
||||
hasCountryHits: boolean;
|
||||
}) {
|
||||
const cityRows = mode === 'china'
|
||||
? cities.filter((i) => CHINA_CODES.has((i.country || '').toUpperCase()))
|
||||
: cities;
|
||||
const rows = rankMode === 'city' ? cityRows : asns;
|
||||
const emptyHint = rankMode === 'city'
|
||||
? '放置 IP2Location DB3 BIN 后可见城市排行'
|
||||
: '放置 GeoLite2-ASN.mmdb 后可见运营商排行';
|
||||
? (hasCountryHits
|
||||
? '需前台路由产生带城市的浏览量;仅请求日志不会进入此排行。请确认已放置 IP2Location DB3 BIN。'
|
||||
: '需前台路由产生浏览量后才会有城市排行;仅请求日志不会点亮此处。')
|
||||
: (hasCountryHits
|
||||
? '需浏览量写入 ASN;请确认已放置 GeoLite2-ASN.mmdb。仅请求日志不会进入此排行。'
|
||||
: '需前台路由产生浏览量后才会有运营商排行;仅请求日志不会点亮此处。');
|
||||
|
||||
return (
|
||||
<div className="admin-monitor-rank">
|
||||
@@ -117,7 +123,7 @@ function GeoRankTable({
|
||||
</div>
|
||||
{rows.length === 0 ? (
|
||||
<div className="admin-monitor-geo-empty">
|
||||
<p>暂无来源数据</p>
|
||||
<p>暂无浏览量来源数据</p>
|
||||
<p className="admin-monitor-muted">{emptyHint}</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -282,18 +288,24 @@ export default function AdminMonitorPage() {
|
||||
|
||||
const metrics = useMemo(() => {
|
||||
if (!overview) return [];
|
||||
const pvHint = '前台浏览';
|
||||
const accessHintShort = '服务端请求';
|
||||
const accessHintFull = '服务端请求(含 API,重启后重计)';
|
||||
return [
|
||||
{ label: '浏览量', value: formatNum(overview.pageviews) },
|
||||
{ label: '访客数', value: formatNum(overview.visitors) },
|
||||
{ label: '独立 IP', value: formatNum(overview.unique_ips) },
|
||||
{ label: '流量', value: formatBytes(overview.traffic) },
|
||||
{ label: '蜘蛛', value: formatNum(overview.bots) },
|
||||
{ label: '请求数', value: formatNum(overview.requests) },
|
||||
{ label: '4xx', value: formatNum(overview.status_4xx) },
|
||||
{ label: '5xx', value: formatNum(overview.status_5xx) },
|
||||
{ label: '浏览量', hint: pvHint, title: pvHint, value: formatNum(overview.pageviews) },
|
||||
{ label: '访客数', hint: pvHint, title: pvHint, value: formatNum(overview.visitors) },
|
||||
{ label: '独立 IP', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.unique_ips) },
|
||||
{ label: '流量', hint: accessHintShort, title: accessHintFull, value: formatBytes(overview.traffic) },
|
||||
{ label: '蜘蛛', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.bots) },
|
||||
{ label: '请求数', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.requests) },
|
||||
{ label: '4xx', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.status_4xx) },
|
||||
{ label: '5xx', hint: accessHintShort, title: accessHintFull, value: formatNum(overview.status_5xx) },
|
||||
];
|
||||
}, [overview]);
|
||||
|
||||
const hasCountryHits = (geo?.countries?.length || 0) > 0;
|
||||
const hasRegionHits = (geo?.regions?.length || 0) > 0;
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (!settings) return;
|
||||
setSaving(true);
|
||||
@@ -352,7 +364,7 @@ export default function AdminMonitorPage() {
|
||||
网站监控
|
||||
</h1>
|
||||
<p className="admin-page-desc">
|
||||
浏览量/访客来自前台路由 pageview;请求数与日志来自服务端访问采集(含 /api)
|
||||
浏览量/访客与访客地图来自前台路由 pageview;请求数、流量与请求日志来自服务端访问采集(含 /api,重启后请求类今日指标重计)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -391,8 +403,11 @@ export default function AdminMonitorPage() {
|
||||
</div>
|
||||
<div className="admin-monitor-today-grid">
|
||||
{metrics.map((m) => (
|
||||
<div key={m.label} className="admin-monitor-today-item">
|
||||
<div className="admin-monitor-today-label">{m.label}</div>
|
||||
<div key={m.label} className="admin-monitor-today-item" title={m.title}>
|
||||
<div className="admin-monitor-today-label">
|
||||
{m.label}
|
||||
<span className="admin-monitor-today-hint">{m.hint}</span>
|
||||
</div>
|
||||
<div className="admin-monitor-today-value">{m.value}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -404,7 +419,13 @@ export default function AdminMonitorPage() {
|
||||
<div className="admin-card-head admin-monitor-map-head">
|
||||
<div className="admin-monitor-today-head" style={{ marginBottom: 0 }}>
|
||||
<span className="admin-monitor-section-bar" aria-hidden />
|
||||
<h2 className="admin-monitor-section-title">访客地图(30 日)</h2>
|
||||
<div>
|
||||
<h2 className="admin-monitor-section-title">访客地图</h2>
|
||||
<p className="admin-monitor-map-sub">
|
||||
基于前台浏览 · 近 30 日
|
||||
{mapMode === 'china' ? ' · 按省/区填色' : ' · 按国家填色'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-monitor-seg" role="group" aria-label="地图范围">
|
||||
<button
|
||||
@@ -428,7 +449,11 @@ export default function AdminMonitorPage() {
|
||||
{mapMode === 'world' ? (
|
||||
<MonitorWorldMap items={geoCountries(geo)} />
|
||||
) : (
|
||||
<MonitorChinaMap regions={geo?.regions || []} />
|
||||
<MonitorChinaMap
|
||||
regions={geo?.regions || []}
|
||||
hasCountryHits={hasCountryHits}
|
||||
hasRegionHits={hasRegionHits}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="admin-monitor-map-side">
|
||||
@@ -438,6 +463,7 @@ export default function AdminMonitorPage() {
|
||||
mode={mapMode}
|
||||
rankMode={rankMode}
|
||||
onRankMode={setRankMode}
|
||||
hasCountryHits={hasCountryHits}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -554,10 +580,10 @@ export default function AdminMonitorPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map((row) => {
|
||||
const placeBits = [row.region, row.city, row.as_org].filter(Boolean);
|
||||
const geoBits = [
|
||||
row.country ? countryLabel(row.country) : '',
|
||||
row.city || '',
|
||||
row.as_org || '',
|
||||
...placeBits,
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
@@ -568,9 +594,9 @@ export default function AdminMonitorPage() {
|
||||
<td className="admin-table-mono">
|
||||
{row.ip || '—'}
|
||||
{row.is_bot ? ' · bot' : ''}
|
||||
{(row.city || row.as_org) ? (
|
||||
{placeBits.length > 0 ? (
|
||||
<div className="admin-monitor-muted" style={{ fontSize: 12 }}>
|
||||
{[row.city, row.as_org].filter(Boolean).join(' · ')}
|
||||
{placeBits.join(' · ')}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
|
||||
@@ -15356,6 +15356,18 @@ button.post-poll__option,
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.admin-monitor-today-hint {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
opacity: 0.85;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.admin-monitor-today-value {
|
||||
font-size: 22px;
|
||||
@@ -15365,6 +15377,13 @@ button.post-poll__option,
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
.admin-monitor-map-sub {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: hsl(var(--muted-foreground));
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-monitor-main {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user