This commit is contained in:
2025-03-25 21:12:03 +08:00
parent ecf21a812f
commit e0ac87fb25
2 changed files with 105 additions and 34 deletions

View File

@@ -1,29 +1,87 @@
// Event Types // Event Types
export interface Event { export interface Event {
id: string; // 核心事件信息
time: string; event_id: string;
type: string; event_time: string;
linkInfo: { event_type: string;
event_attributes: string;
// 链接信息
link_id: string;
link_slug: string;
link_label: string;
link_title: string;
link_original_url: string;
link_attributes: string;
link_created_at: string;
link_expires_at: string | null;
link_tags: string;
// 用户信息
user_id: string;
user_name: string;
user_email: string;
user_attributes: string;
// 团队信息
team_id: string;
team_name: string;
team_attributes: string;
// 项目信息
project_id: string;
project_name: string;
project_attributes: string;
// 二维码信息
qr_code_id: string;
qr_code_name: string;
qr_code_attributes: string;
// 访问者信息
visitor_id: string;
session_id: string;
ip_address: string;
country: string;
city: string;
device_type: string;
browser: string;
os: string;
user_agent: string;
// 来源信息
referrer: string;
utm_source: string;
utm_medium: string;
utm_campaign: string;
// 交互信息
time_spent_sec: number;
is_bounce: boolean;
is_qr_scan: boolean;
conversion_type: string;
conversion_value: number;
// 旧接口兼容字段
id?: string;
time?: string;
type?: string;
linkInfo?: {
id: string; id: string;
shortUrl: string; shortUrl: string;
originalUrl: string; originalUrl: string;
}; };
visitor: { visitor?: {
id: string; id: string;
browser: string; browser: string;
os: string; os: string;
device: string; device: string;
}; };
location: { location?: {
country: string; country: string;
region: string; region: string;
city: string; city: string;
}; };
referrer: string;
conversion?: {
type: string;
value: number;
};
} }
// Analytics Types // Analytics Types

View File

@@ -1,14 +1,14 @@
"use client"; "use client";
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect } from 'react';
import { addDays, format } from 'date-fns'; import { format } from 'date-fns';
import { DateRangePicker } from '../components/ui/DateRangePicker'; import { DateRangePicker } from '../components/ui/DateRangePicker';
import { Event, EventFilters } from '../api/types'; import { Event } from '../api/types';
export default function EventsPage() { export default function EventsPage() {
const [dateRange, setDateRange] = useState({ const [dateRange, setDateRange] = useState({
from: addDays(new Date(), -7), from: new Date('2024-02-01'),
to: new Date() to: new Date('2025-03-05')
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -22,7 +22,7 @@ export default function EventsPage() {
linkSlug: '' linkSlug: ''
}); });
const [filters, setFilters] = useState<EventFilters>({ const [filters, setFilters] = useState({
startTime: format(new Date('2024-02-01'), "yyyy-MM-dd'T'HH:mm:ss'Z'"), startTime: format(new Date('2024-02-01'), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
endTime: format(new Date('2025-03-05'), "yyyy-MM-dd'T'HH:mm:ss'Z'"), endTime: format(new Date('2025-03-05'), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
page: 1, page: 1,
@@ -30,8 +30,6 @@ export default function EventsPage() {
}); });
const [summary, setSummary] = useState<any>(null); const [summary, setSummary] = useState<any>(null);
const observerRef = useRef<IntersectionObserver | null>(null);
const lastEventRef = useRef<HTMLDivElement | null>(null);
const fetchEvents = async (pageNum: number) => { const fetchEvents = async (pageNum: number) => {
try { try {
@@ -56,15 +54,19 @@ export default function EventsPage() {
throw new Error(data.error || 'Failed to fetch events'); throw new Error(data.error || 'Failed to fetch events');
} }
const eventsData = data.data || data.events || [];
if (pageNum === 1) { if (pageNum === 1) {
setEvents(data.events); setEvents(eventsData);
} else { } else {
setEvents(prev => [...prev, ...data.events]); setEvents(prev => [...prev, ...eventsData]);
} }
setHasMore(data.events.length === 50); setHasMore(Array.isArray(eventsData) && eventsData.length === 50);
} catch (err) { } catch (err) {
console.error("Error fetching events:", err);
setError(err instanceof Error ? err.message : 'An error occurred while fetching events'); setError(err instanceof Error ? err.message : 'An error occurred while fetching events');
setEvents([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -174,42 +176,53 @@ export default function EventsPage() {
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Referrer Referrer
</th> </th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Conversion
</th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{events.map((event, index) => ( {Array.isArray(events) && events.map((event, index) => (
<tr key={event.id} className={index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-900'}> <tr key={event.event_id || index} className={index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-900'}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{formatDate(event.time)} {event.event_time && formatDate(event.event_time)}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm"> <td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
event.type === 'conversion' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100' event.event_type === 'conversion' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100'
}`}> }`}>
{event.type} {event.event_type || 'unknown'}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<div> <div>
<div className="font-medium">{event.linkInfo.shortUrl}</div> <div className="font-medium">{event.link_slug || '-'}</div>
<div className="text-gray-500 dark:text-gray-400 text-xs">{event.linkInfo.originalUrl}</div> <div className="text-gray-500 dark:text-gray-400 text-xs">{event.link_original_url || '-'}</div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<div> <div>
<div>{event.visitor.browser}</div> <div>{event.browser || '-'}</div>
<div className="text-gray-500 dark:text-gray-400 text-xs">{event.visitor.os} / {event.visitor.device}</div> <div className="text-gray-500 dark:text-gray-400 text-xs">{event.os || '-'} / {event.device_type || '-'}</div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<div> <div>
<div>{event.location.city}</div> <div>{event.city || '-'}</div>
<div className="text-gray-500 dark:text-gray-400 text-xs">{event.location.region}, {event.location.country}</div> <div className="text-gray-500 dark:text-gray-400 text-xs">{event.country || '-'}</div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{event.referrer || '-'} {event.referrer || '-'}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
<div>
<div>{event.conversion_type || '-'}</div>
{event.conversion_value > 0 && (
<div className="text-gray-500 dark:text-gray-400 text-xs">Value: {event.conversion_value}</div>
)}
</div>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -233,7 +246,7 @@ export default function EventsPage() {
</div> </div>
)} )}
{!loading && events.length === 0 && ( {!loading && Array.isArray(events) && events.length === 0 && (
<div className="flex justify-center p-8 text-gray-500 dark:text-gray-400"> <div className="flex justify-center p-8 text-gray-500 dark:text-gray-400">
No events found No events found
</div> </div>