Files
shorturl-analytics/app/(app)/dashboard/page.tsx
2025-03-31 19:51:02 +08:00

232 lines
7.6 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
import { format } from 'date-fns';
import { DateRangePicker } from '@/app/components/ui/DateRangePicker';
import TimeSeriesChart from '@/app/components/charts/TimeSeriesChart';
import GeoAnalytics from '@/app/components/analytics/GeoAnalytics';
import DeviceAnalytics from '@/app/components/analytics/DeviceAnalytics';
import { EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics as DeviceAnalyticsType } from '@/app/api/types';
export default function DashboardPage() {
const [dateRange, setDateRange] = useState({
from: new Date('2024-02-01'),
to: new Date('2025-03-05')
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [summary, setSummary] = useState<EventsSummary | null>(null);
const [timeSeriesData, setTimeSeriesData] = useState<TimeSeriesData[]>([]);
const [geoData, setGeoData] = useState<GeoData[]>([]);
const [deviceData, setDeviceData] = useState<DeviceAnalyticsType | null>(null);
// 获取统计数据
const fetchSummary = 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/summary?startTime=${startTime}&endTime=${endTime}`
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch summary');
}
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]);
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-red-500">{error}</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">Analytics Dashboard</h1>
<DateRangePicker
value={dateRange}
onChange={setDateRange}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500">Total Events</h3>
<p className="text-2xl font-semibold text-gray-900">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
summary?.totalEvents?.toLocaleString() || '0'
)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500">Unique Visitors</h3>
<p className="text-2xl font-semibold text-gray-900">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
summary?.uniqueVisitors?.toLocaleString() || '0'
)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500">Total Conversions</h3>
<p className="text-2xl font-semibold text-gray-900">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
summary?.totalConversions?.toLocaleString() || '0'
)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-500">Avg. Time Spent</h3>
<p className="text-2xl font-semibold text-gray-900">
{loading ? (
<span className="text-gray-400">Loading...</span>
) : (
`${summary?.averageTimeSpent?.toFixed(1) || '0'}s`
)}
</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Event Trends</h2>
<div className="h-96">
{loading ? (
<div className="flex items-center justify-center h-full">
<span className="text-gray-400">Loading...</span>
</div>
) : (
<TimeSeriesChart data={timeSeriesData} />
)}
</div>
</div>
<div className="mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Device Analytics</h2>
{loading ? (
<div className="flex items-center justify-center h-48">
<span className="text-gray-400">Loading...</span>
</div>
) : (
deviceData && <DeviceAnalytics data={deviceData} />
)}
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Geographic Distribution</h2>
{loading ? (
<div className="flex items-center justify-center h-48">
<span className="text-gray-400">Loading...</span>
</div>
) : (
<GeoAnalytics data={geoData} />
)}
</div>
</div>
);
}