From 63f434fd93cd6be927202ec2d428a38fc123bf26 Mon Sep 17 00:00:00 2001 From: William Tso Date: Wed, 2 Apr 2025 21:36:13 +0800 Subject: [PATCH] geo tab --- app/api/events/geo/route.ts | 5 +- app/api/types.ts | 1 + app/components/analytics/GeoAnalytics.tsx | 150 ++++++++++++++-------- app/page.tsx | 35 ++++- lib/analytics.ts | 12 +- 5 files changed, 149 insertions(+), 54 deletions(-) diff --git a/app/api/events/geo/route.ts b/app/api/events/geo/route.ts index 6f4b22b..e9bafdf 100644 --- a/app/api/events/geo/route.ts +++ b/app/api/events/geo/route.ts @@ -11,11 +11,14 @@ export async function GET(request: NextRequest) { const projectIds = searchParams.getAll('projectId'); const tagIds = searchParams.getAll('tagId'); + // Get the groupBy parameter + const groupBy = searchParams.get('groupBy') as 'country' | 'city' | 'region' | 'continent' | null; + const data = await getGeoAnalytics({ startTime: searchParams.get('startTime') || undefined, endTime: searchParams.get('endTime') || undefined, linkId: searchParams.get('linkId') || undefined, - groupBy: (searchParams.get('groupBy') || 'country') as 'country' | 'city', + groupBy: groupBy || undefined, // 添加团队、项目和标签筛选 teamIds: teamIds.length > 0 ? teamIds : undefined, projectIds: projectIds.length > 0 ? projectIds : undefined, diff --git a/app/api/types.ts b/app/api/types.ts index 76a8970..881b24a 100644 --- a/app/api/types.ts +++ b/app/api/types.ts @@ -94,6 +94,7 @@ export interface TimeSeriesData { export interface GeoData { location: string; + area: string; visits: number; visitors: number; percentage: number; diff --git a/app/components/analytics/GeoAnalytics.tsx b/app/components/analytics/GeoAnalytics.tsx index ae58ab3..ffa093a 100644 --- a/app/components/analytics/GeoAnalytics.tsx +++ b/app/components/analytics/GeoAnalytics.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from 'react'; import { GeoData } from '@/app/api/types'; interface GeoAnalyticsProps { @@ -7,6 +8,8 @@ interface GeoAnalyticsProps { } export default function GeoAnalytics({ data }: GeoAnalyticsProps) { + const [viewMode, setViewMode] = useState<'country' | 'city' | 'region' | 'continent'>('country'); + // 安全地格式化数字 const formatNumber = (value: number | undefined | null): string => { if (value === undefined || value === null) return '0'; @@ -21,60 +24,107 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) { const sortedData = [...data].sort((a, b) => (b.visits || 0) - (a.visits || 0)); + // Handle tab selection - only change local view mode + const handleViewModeChange = (mode: 'country' | 'city' | 'region' | 'continent') => { + setViewMode(mode); + // Only change the local view mode, no callback to parent + }; + return ( -
- - - - - - - - - - - {sortedData.length > 0 ? ( - sortedData.map((item, index) => ( - - - - - +
- IP Address - - Visits - - Unique Visitors - - % of Total -
- {item.location || 'Unknown'} - - {formatNumber(item.visits)} - - {formatNumber(item.visitors)} - -
-
-
+
+ {/* Tabs for geographic levels */} +
+ + + + +
+ + {/* Table with added area column */} +
+ + + + + + + + + + + + {sortedData.length > 0 ? ( + sortedData.map((item, index) => ( + + + + + + + + )) + ) : ( + + - )) - ) : ( - - - - )} - -
+ {viewMode === 'country' ? 'Country' : + viewMode === 'city' ? 'City' : + viewMode === 'region' ? 'Region' : 'Continent'} + + {viewMode === 'country' ? 'Countries' : + viewMode === 'city' ? 'Cities' : + viewMode === 'region' ? 'Regions' : 'Continents'} + + Visits + + Unique Visitors + + % of Total +
+ {item.location || 'Unknown'} + + {item.area || ''} + + {formatNumber(item.visits)} + + {formatNumber(item.visitors)} + +
+
+
+
+ {formatPercent(item.percentage)}%
- {formatPercent(item.percentage)}% -
+
+ No location data available
- No IP data available -
+ )} +
+
); } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 0ced34d..e2bb31e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -762,7 +762,40 @@ export default function HomePage() {

Geographic Distribution

- + { + // 构建查询参数 + const params = new URLSearchParams({ + startTime: format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'"), + endTime: format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'"), + groupBy: mode + }); + + // 添加其他筛选参数 + if (selectedTeamIds.length > 0) { + selectedTeamIds.forEach(id => params.append('teamId', id)); + } + + if (selectedProjectIds.length > 0) { + selectedProjectIds.forEach(id => params.append('projectId', id)); + } + + if (selectedTagIds.length > 0) { + selectedTagIds.forEach(id => params.append('tagId', id)); + } + + // 刷新地理位置数据 + fetch(`/api/events/geo?${params}`) + .then(res => res.json()) + .then(data => { + if (data.success) { + setGeoData(data.data); + } + }) + .catch(error => console.error('Failed to fetch geo data:', error)); + }} + />
diff --git a/lib/analytics.ts b/lib/analytics.ts index e451723..bc24e9c 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -215,16 +215,24 @@ export async function getGeoAnalytics(params: { startTime?: string; endTime?: string; linkId?: string; + groupBy?: 'country' | 'city' | 'region' | 'continent'; teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; }): Promise { const filter = buildFilter(params); - // Use IP address as the grouping field + // Choose grouping field based on selected view + let groupByField = 'country'; + if (params.groupBy === 'city') groupByField = 'city'; + else if (params.groupBy === 'region') groupByField = 'region'; + else if (params.groupBy === 'continent') groupByField = 'continent'; + else if (!params.groupBy) groupByField = 'ip_address'; // Default to IP address if no groupBy is specified + const query = ` SELECT - ip_address as location, + COALESCE(${groupByField}, 'Unknown') as location, + '' as area, /* Area column - empty for now */ count() as visits, uniq(ip_address) as visitors, count() * 100.0 / sum(count()) OVER () as percentage