232 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
}
|