From ecf21a812f2a62e0a9d20c6aa49599877a918fdf Mon Sep 17 00:00:00 2001 From: William Tso Date: Tue, 25 Mar 2025 21:02:17 +0800 Subject: [PATCH] dashboard page good --- app/api/types.ts | 34 +++++++++++++++++--- app/components/analytics/DeviceAnalytics.tsx | 26 +++++++++++---- app/components/analytics/GeoAnalytics.tsx | 22 ++++++++++--- app/components/charts/TimeSeriesChart.tsx | 21 ++++++++++-- app/events/page.tsx | 8 +++-- 5 files changed, 90 insertions(+), 21 deletions(-) diff --git a/app/api/types.ts b/app/api/types.ts index 96929c5..25c6223 100644 --- a/app/api/types.ts +++ b/app/api/types.ts @@ -35,14 +35,18 @@ export interface TimeSeriesData { } export interface GeoData { - country: string; - region: string; - city: string; + location?: string; + country?: string; + region?: string; + city?: string; visits: number; - uniqueVisitors: number; + uniqueVisitors?: number; + visitors?: number; percentage: number; } +export type DeviceType = 'mobile' | 'desktop' | 'tablet' | 'other'; + export interface DeviceAnalytics { deviceTypes: { type: string; @@ -82,4 +86,26 @@ export interface EventsSummary { count: number; percentage: number; }[]; +} + +export interface ConversionStats { + totalConversions: number; + conversionRate: number; + averageValue: number; + byType: { + type: string; + count: number; + percentage: number; + value: number; + }[]; +} + +export interface EventFilters { + startTime?: string; + endTime?: string; + eventType?: string; + linkId?: string; + linkSlug?: string; + page?: number; + pageSize?: number; } \ No newline at end of file diff --git a/app/components/analytics/DeviceAnalytics.tsx b/app/components/analytics/DeviceAnalytics.tsx index a3d2e2c..14b3194 100644 --- a/app/components/analytics/DeviceAnalytics.tsx +++ b/app/components/analytics/DeviceAnalytics.tsx @@ -7,6 +7,18 @@ interface DeviceAnalyticsProps { } function StatCard({ title, items }: { title: string; items: { name: string; count: number; percentage: number }[] }) { + // 安全地格式化数字 + const formatNumber = (value: number | string | undefined | null): string => { + if (value === undefined || value === null) return '0'; + return typeof value === 'number' ? value.toLocaleString() : String(value); + }; + + // 安全地格式化百分比 + const formatPercent = (value: number | undefined | null): string => { + if (value === undefined || value === null) return '0'; + return value.toFixed(1); + }; + return (

{title}

@@ -14,13 +26,13 @@ function StatCard({ title, items }: { title: string; items: { name: string; coun {items.map((item, index) => (
- {item.name} - {item.count.toLocaleString()} ({item.percentage.toFixed(1)}%) + {item.name || 'Unknown'} + {formatNumber(item.count)} ({formatPercent(item.percentage)}%)
@@ -35,19 +47,19 @@ export default function DeviceAnalytics({ data }: DeviceAnalyticsProps) {
({ - name: item.type.charAt(0).toUpperCase() + item.type.slice(1), + items={(data.deviceTypes || []).map(item => ({ + name: item.type ? (item.type.charAt(0).toUpperCase() + item.type.slice(1)) : 'Unknown', count: item.count, percentage: item.percentage }))} />
); diff --git a/app/components/analytics/GeoAnalytics.tsx b/app/components/analytics/GeoAnalytics.tsx index d330276..41d737a 100644 --- a/app/components/analytics/GeoAnalytics.tsx +++ b/app/components/analytics/GeoAnalytics.tsx @@ -7,6 +7,18 @@ interface GeoAnalyticsProps { } export default function GeoAnalytics({ data }: GeoAnalyticsProps) { + // 安全地格式化数字 + const formatNumber = (value: any): string => { + if (value === undefined || value === null) return '0'; + return typeof value === 'number' ? value.toLocaleString() : String(value); + }; + + // 安全地格式化百分比 + const formatPercent = (value: any): string => { + if (value === undefined || value === null) return '0'; + return typeof value === 'number' ? value.toFixed(2) : String(value); + }; + return (
@@ -30,21 +42,21 @@ export default function GeoAnalytics({ data }: GeoAnalyticsProps) { {data.map((item, index) => (
- {item.city ? `${item.city}, ${item.region}, ${item.country}` : item.region ? `${item.region}, ${item.country}` : item.country} + {item.city ? `${item.city}, ${item.region}, ${item.country}` : item.region ? `${item.region}, ${item.country}` : item.country || item.location || 'Unknown'} - {item.visits.toLocaleString()} + {formatNumber(item.visits)} - {item.uniqueVisitors.toLocaleString()} + {formatNumber(item.uniqueVisitors || item.visitors)}
- {item.percentage.toFixed(2)}% + {formatPercent(item.percentage)}%
diff --git a/app/components/charts/TimeSeriesChart.tsx b/app/components/charts/TimeSeriesChart.tsx index f43c7fe..3753c45 100644 --- a/app/components/charts/TimeSeriesChart.tsx +++ b/app/components/charts/TimeSeriesChart.tsx @@ -7,6 +7,7 @@ import { LinearScale, PointElement, LineElement, + LineController, Title, Tooltip, Legend, @@ -23,6 +24,7 @@ ChartJS.register( LinearScale, PointElement, LineElement, + LineController, Title, Tooltip, Legend, @@ -50,13 +52,25 @@ export default function TimeSeriesChart({ data }: TimeSeriesChartProps) { // 准备数据 const labels = data.map(item => { + if (!item || !item.timestamp) return ''; const date = new Date(item.timestamp); return date.toLocaleDateString(); }); - const eventsData = data.map(item => Number(item.events)); - const visitorsData = data.map(item => Number(item.visitors)); - const conversionsData = data.map(item => Number(item.conversions)); + const eventsData = data.map(item => { + if (!item || item.events === undefined || item.events === null) return 0; + return Number(item.events); + }); + + const visitorsData = data.map(item => { + if (!item || item.visitors === undefined || item.visitors === null) return 0; + return Number(item.visitors); + }); + + const conversionsData = data.map(item => { + if (!item || item.conversions === undefined || item.conversions === null) return 0; + return Number(item.conversions); + }); // 创建新的图表实例 chartInstance.current = new ChartJS(ctx, { @@ -144,6 +158,7 @@ export default function TimeSeriesChart({ data }: TimeSeriesChartProps) { ticks: { color: 'rgb(156, 163, 175)', // gray-400 callback: (value: number) => { + if (!value && value !== 0) return ''; if (value >= 1000) { return `${(value / 1000).toFixed(1)}k`; } diff --git a/app/events/page.tsx b/app/events/page.tsx index bbd164e..be07dea 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -1,9 +1,9 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { addDays, format } from 'date-fns'; import { DateRangePicker } from '../components/ui/DateRangePicker'; -import { Event } from '../api/types'; +import { Event, EventFilters } from '../api/types'; export default function EventsPage() { const [dateRange, setDateRange] = useState({ @@ -29,6 +29,10 @@ export default function EventsPage() { pageSize: 20 }); + const [summary, setSummary] = useState(null); + const observerRef = useRef(null); + const lastEventRef = useRef(null); + const fetchEvents = async (pageNum: number) => { try { const startTime = format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'");