dashboard data

This commit is contained in:
2025-03-31 19:46:31 +08:00
parent a2c6de8474
commit b425f5b987

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { addDays, format } from 'date-fns'; import { format } from 'date-fns';
import { DateRangePicker } from '@/app/components/ui/DateRangePicker'; import { DateRangePicker } from '@/app/components/ui/DateRangePicker';
import TimeSeriesChart from '@/app/components/charts/TimeSeriesChart'; import TimeSeriesChart from '@/app/components/charts/TimeSeriesChart';
import GeoAnalytics from '@/app/components/analytics/GeoAnalytics'; import GeoAnalytics from '@/app/components/analytics/GeoAnalytics';
@@ -21,57 +21,117 @@ export default function DashboardPage() {
const [geoData, setGeoData] = useState<GeoData[]>([]); const [geoData, setGeoData] = useState<GeoData[]>([]);
const [deviceData, setDeviceData] = useState<DeviceAnalyticsType | null>(null); const [deviceData, setDeviceData] = useState<DeviceAnalyticsType | null>(null);
useEffect(() => { // 获取统计数据
const fetchData = async () => { const fetchSummary = async () => {
setLoading(true); try {
setError(null); const startTime = format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const endTime = format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'");
try {
const startTime = format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'"); const response = await fetch(
const endTime = format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'"); `/api/events/summary?startTime=${startTime}&endTime=${endTime}`
);
// 并行获取所有数据 const data = await response.json();
const [summaryRes, timeSeriesRes, geoRes, deviceRes] = await Promise.all([
fetch(`/api/events/summary?startTime=${startTime}&endTime=${endTime}`), if (!response.ok) {
fetch(`/api/events/time-series?startTime=${startTime}&endTime=${endTime}`), throw new Error(data.error || 'Failed to fetch summary');
fetch(`/api/events/geo?startTime=${startTime}&endTime=${endTime}`),
fetch(`/api/events/devices?startTime=${startTime}&endTime=${endTime}`)
]);
const [summaryData, timeSeriesData, geoData, deviceData] = await Promise.all([
summaryRes.json(),
timeSeriesRes.json(),
geoRes.json(),
deviceRes.json()
]);
if (!summaryRes.ok) throw new Error(summaryData.error || 'Failed to fetch summary data');
if (!timeSeriesRes.ok) throw new Error(timeSeriesData.error || 'Failed to fetch time series data');
if (!geoRes.ok) throw new Error(geoData.error || 'Failed to fetch geo data');
if (!deviceRes.ok) throw new Error(deviceData.error || 'Failed to fetch device data');
setSummary(summaryData);
setTimeSeriesData(timeSeriesData.data);
setGeoData(geoData.data);
setDeviceData(deviceData.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred while fetching data');
} finally {
setLoading(false);
} }
};
fetchData(); if (data.success && data.data) {
console.log('Summary data:', data.data); // 添加日志
setSummary(data.data);
}
} catch (err) {
console.error('Error fetching summary:', err);
setError(err instanceof Error ? err.message : 'Failed to load summary');
}
};
// 获取时间序列数据
const fetchTimeSeriesData = async () => {
try {
const startTime = format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const endTime = format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const response = await fetch(
`/api/events/time-series?startTime=${startTime}&endTime=${endTime}&granularity=day`
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch time series data');
}
if (data.success && Array.isArray(data.data)) {
setTimeSeriesData(data.data);
}
} catch (err) {
console.error('Error fetching time series:', err);
}
};
// 获取地理位置数据
const fetchGeoData = async () => {
try {
const startTime = format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const endTime = format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const response = await fetch(
`/api/events/geo?startTime=${startTime}&endTime=${endTime}`
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch geo data');
}
if (data.success && Array.isArray(data.data)) {
setGeoData(data.data);
}
} catch (err) {
console.error('Error fetching geo data:', err);
}
};
// 获取设备数据
const fetchDeviceData = async () => {
try {
const startTime = format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const endTime = format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'");
const response = await fetch(
`/api/events/devices?startTime=${startTime}&endTime=${endTime}`
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch device data');
}
if (data.success && data.data) {
setDeviceData(data.data);
}
} catch (err) {
console.error('Error fetching device data:', err);
}
};
// 加载所有数据
const loadAllData = async () => {
setLoading(true);
await Promise.all([
fetchSummary(),
fetchTimeSeriesData(),
fetchGeoData(),
fetchDeviceData()
]);
setLoading(false);
};
// 当日期范围改变时重新加载数据
useEffect(() => {
loadAllData();
}, [dateRange]); }, [dateRange]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
</div>
);
}
if (error) { if (error) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
@@ -90,50 +150,82 @@ export default function DashboardPage() {
/> />
</div> </div>
{summary && ( <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Total Events</h3>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Total Events</h3> <p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100"> {loading ? (
{typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents} <span className="text-gray-400">Loading...</span>
</p> ) : (
</div> summary?.totalEvents?.toLocaleString() || '0'
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> )}
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Unique Visitors</h3> </p>
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Total Conversions</h3>
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Avg. Time Spent</h3>
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{summary.averageTimeSpent?.toFixed(1) || '0'}s
</p>
</div>
</div> </div>
)} <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Unique Visitors</h3>
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
summary?.uniqueVisitors?.toLocaleString() || '0'
)}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Total Conversions</h3>
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
summary?.totalConversions?.toLocaleString() || '0'
)}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">Avg. Time Spent</h3>
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
`${summary?.averageTimeSpent?.toFixed(1) || '0'}s`
)}
</p>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Event Trends</h2> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Event Trends</h2>
<div className="h-96"> <div className="h-96">
<TimeSeriesChart data={timeSeriesData} /> {loading ? (
<div className="flex items-center justify-center h-full">
<span className="text-gray-400">Loading...</span>
</div>
) : (
<TimeSeriesChart data={timeSeriesData} />
)}
</div> </div>
</div> </div>
<div className="mb-8"> <div className="mb-8">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Device Analytics</h2> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Device Analytics</h2>
{deviceData && <DeviceAnalytics data={deviceData} />} {loading ? (
<div className="flex items-center justify-center h-48">
<span className="text-gray-400">Loading...</span>
</div>
) : (
deviceData && <DeviceAnalytics data={deviceData} />
)}
</div> </div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Geographic Distribution</h2> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Geographic Distribution</h2>
<GeoAnalytics data={geoData} /> {loading ? (
<div className="flex items-center justify-center h-48">
<span className="text-gray-400">Loading...</span>
</div>
) : (
<GeoAnalytics data={geoData} />
)}
</div> </div>
</div> </div>
); );