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 (
| - {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 |