geo main
This commit is contained in:
@@ -1,14 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { GeoData } from '@/app/api/types';
|
||||
import { getLocationsFromIPs } from '@/app/utils/ipLocation';
|
||||
|
||||
interface GeoAnalyticsProps {
|
||||
data: GeoData[];
|
||||
}
|
||||
|
||||
// Interface for IP location data in our cache
|
||||
interface IpLocationDetail {
|
||||
country: string;
|
||||
city: string;
|
||||
region: string;
|
||||
continent: string;
|
||||
}
|
||||
|
||||
// Cache for IP location data
|
||||
interface LocationCache {
|
||||
[key: string]: IpLocationDetail;
|
||||
}
|
||||
|
||||
export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
const [viewMode, setViewMode] = useState<'country' | 'city' | 'region' | 'continent'>('country');
|
||||
const [locationCache, setLocationCache] = useState<LocationCache>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// Track IPs that failed to resolve
|
||||
const [failedIPs, setFailedIPs] = useState<Set<string>>(new Set());
|
||||
|
||||
// 安全地格式化数字
|
||||
const formatNumber = (value: number | undefined | null): string => {
|
||||
@@ -27,7 +45,122 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
// 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
|
||||
};
|
||||
|
||||
// Load location data for all IPs when the data changes
|
||||
useEffect(() => {
|
||||
const fetchLocations = async () => {
|
||||
if (sortedData.length === 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const tempCache: LocationCache = {...locationCache};
|
||||
const tempFailedIPs = new Set(failedIPs);
|
||||
|
||||
// Get all unique IPs that aren't already in the cache and haven't failed
|
||||
const uniqueIPs = [...new Set(sortedData.map(item => item.location))].filter(ip =>
|
||||
ip &&
|
||||
ip !== 'Unknown' &&
|
||||
!tempCache[ip] &&
|
||||
!tempFailedIPs.has(ip)
|
||||
);
|
||||
|
||||
if (uniqueIPs.length === 0) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use batch lookup for better performance
|
||||
const batchResults = await getLocationsFromIPs(uniqueIPs);
|
||||
|
||||
// Convert results to our cache format
|
||||
for (const [ip, data] of Object.entries(batchResults)) {
|
||||
if (data) {
|
||||
tempCache[ip] = {
|
||||
country: data.country_name,
|
||||
city: data.city,
|
||||
region: data.region,
|
||||
continent: data.continent_name
|
||||
};
|
||||
} else {
|
||||
// Mark as failed
|
||||
tempFailedIPs.add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
setLocationCache(tempCache);
|
||||
setFailedIPs(tempFailedIPs);
|
||||
} catch (error) {
|
||||
console.error('Error fetching location data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLocations();
|
||||
}, [data]);
|
||||
|
||||
// Get the appropriate location value based on the current view mode
|
||||
const getLocationValue = (item: GeoData): string => {
|
||||
const ip = item.location || '';
|
||||
|
||||
// If there's no IP or it's "Unknown", return that value
|
||||
if (!ip || ip === 'Unknown') return 'Unknown';
|
||||
|
||||
// If IP failed to resolve, return Unknown
|
||||
if (failedIPs.has(ip)) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
// Return from cache if available
|
||||
if (locationCache[ip]) {
|
||||
switch (viewMode) {
|
||||
case 'country':
|
||||
return locationCache[ip].country || 'Unknown';
|
||||
case 'city':
|
||||
return locationCache[ip].city || 'Unknown';
|
||||
case 'region':
|
||||
return locationCache[ip].region || 'Unknown';
|
||||
case 'continent':
|
||||
return locationCache[ip].continent || 'Unknown';
|
||||
default:
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
// Return placeholder if not in cache yet
|
||||
return `Loading...`;
|
||||
};
|
||||
|
||||
// Get the appropriate area value based on the current view mode
|
||||
const getAreaValue = (item: GeoData): string => {
|
||||
const ip = item.location || '';
|
||||
|
||||
// If there's no IP or it's "Unknown", return empty string
|
||||
if (!ip || ip === 'Unknown' || failedIPs.has(ip)) return '';
|
||||
|
||||
// Return from cache if available
|
||||
if (locationCache[ip]) {
|
||||
switch (viewMode) {
|
||||
case 'country':
|
||||
// For country view, show the continent as area
|
||||
return locationCache[ip].continent || '';
|
||||
case 'city':
|
||||
// For city view, show the country and region
|
||||
return `${locationCache[ip].country}, ${locationCache[ip].region}`;
|
||||
case 'region':
|
||||
// For region view, show the country
|
||||
return locationCache[ip].country || '';
|
||||
case 'continent':
|
||||
// For continent view, no additional area needed
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty if not in cache yet
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -60,6 +193,14 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center py-2 mb-4">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500 mr-2"></div>
|
||||
<span className="text-sm text-gray-500">Loading location data...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table with added area column */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
@@ -71,9 +212,9 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
viewMode === 'region' ? 'Region' : 'Continent'}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
{viewMode === 'country' ? 'Countries' :
|
||||
viewMode === 'city' ? 'Cities' :
|
||||
viewMode === 'region' ? 'Regions' : 'Continents'}
|
||||
{viewMode === 'country' ? 'Continent' :
|
||||
viewMode === 'city' ? 'Location' :
|
||||
viewMode === 'region' ? 'Country' : 'Area'}
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Visits
|
||||
@@ -91,10 +232,13 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) {
|
||||
sortedData.map((item, index) => (
|
||||
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{item.location || 'Unknown'}
|
||||
{getLocationValue(item)}
|
||||
{item.location && (
|
||||
<div className="text-xs text-gray-500 mt-1">{item.location}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{item.area || ''}
|
||||
{getAreaValue(item)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatNumber(item.visits)}
|
||||
|
||||
100
app/components/ipLocationTest.tsx
Normal file
100
app/components/ipLocationTest.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getLocationFromIP } from '@/app/utils/ipLocation';
|
||||
|
||||
interface LocationData {
|
||||
ip: string;
|
||||
country_name: string;
|
||||
country_code: string;
|
||||
city: string;
|
||||
region: string;
|
||||
continent_code: string;
|
||||
continent_name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export default function IpLocationTest() {
|
||||
const [locationData, setLocationData] = useState<LocationData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const testIp = "120.244.39.90";
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchLocation() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getLocationFromIP(testIp);
|
||||
setLocationData(data);
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchLocation();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white rounded-lg shadow">
|
||||
<h2 className="text-lg font-semibold mb-4">IP Location Test: {testIp}</h2>
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center text-gray-500">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500 mr-2"></div>
|
||||
Loading location data...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && locationData && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-medium">Location Data:</h3>
|
||||
<pre className="mt-2 p-4 bg-gray-100 rounded overflow-auto">
|
||||
{JSON.stringify(locationData, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="border p-3 rounded">
|
||||
<h4 className="font-medium">Country</h4>
|
||||
<div>{locationData.country_name} ({locationData.country_code})</div>
|
||||
</div>
|
||||
|
||||
<div className="border p-3 rounded">
|
||||
<h4 className="font-medium">City</h4>
|
||||
<div>{locationData.city || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div className="border p-3 rounded">
|
||||
<h4 className="font-medium">Region</h4>
|
||||
<div>{locationData.region || 'N/A'}</div>
|
||||
</div>
|
||||
|
||||
<div className="border p-3 rounded">
|
||||
<h4 className="font-medium">Continent</h4>
|
||||
<div>{locationData.continent_name} ({locationData.continent_code})</div>
|
||||
</div>
|
||||
|
||||
<div className="border p-3 rounded col-span-2">
|
||||
<h4 className="font-medium">Coordinates</h4>
|
||||
<div>Latitude: {locationData.latitude}, Longitude: {locationData.longitude}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user