diff --git a/app/(app)/events/page.tsx b/app/(app)/events/page.tsx index d2cd00c..7aeec2e 100644 --- a/app/(app)/events/page.tsx +++ b/app/(app)/events/page.tsx @@ -1,9 +1,9 @@ "use client"; import { useState, useEffect } from 'react'; -import { addDays, format } from 'date-fns'; +import { format } from 'date-fns'; import { DateRangePicker } from '@/app/components/ui/DateRangePicker'; -import { Event } from '@/app/api/types'; +import { Event, EventType } from '@/lib/types'; export default function EventsPage() { const [dateRange, setDateRange] = useState({ @@ -16,36 +16,62 @@ export default function EventsPage() { const [events, setEvents] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); - const [filter, setFilter] = useState({ + const [totalEvents, setTotalEvents] = useState(0); + const [tags, setTags] = useState([]); + + // 过滤条件状态 + const [filters, setFilters] = useState({ eventType: '', linkId: '', - linkSlug: '' + linkSlug: '', + userId: '', + teamId: '', + projectId: '', + tags: [] as string[], + searchSlug: '', + sortBy: 'event_time', + sortOrder: 'desc' as 'asc' | 'desc' }); - const [filters, setFilters] = useState({ - 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'"), - page: 1, - pageSize: 20 - }); - - const [summary, setSummary] = useState(null); + // 加载标签列表 + const fetchTags = async () => { + try { + const response = await fetch('/api/events/tags'); + const data = await response.json(); + if (data.success && Array.isArray(data.data)) { + setTags(data.data.map((tag: { tag_name: string }) => tag.tag_name)); + } + } catch (err) { + console.error('Error fetching tags:', err); + } + }; + // 获取事件列表 const fetchEvents = async (pageNum: number) => { 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 params = new URLSearchParams({ - startTime, - endTime, page: pageNum.toString(), - pageSize: '50' + pageSize: '20' }); - if (filter.eventType) params.append('eventType', filter.eventType); - if (filter.linkId) params.append('linkId', filter.linkId); - if (filter.linkSlug) params.append('linkSlug', filter.linkSlug); + // 添加时间范围参数(如果有) + if (startTime) params.append('startTime', startTime); + if (endTime) params.append('endTime', endTime); + + // 添加其他过滤参数 + if (filters.eventType) params.append('eventType', filters.eventType); + if (filters.linkId) params.append('linkId', filters.linkId); + if (filters.linkSlug) params.append('linkSlug', filters.linkSlug); + if (filters.userId) params.append('userId', filters.userId); + if (filters.teamId) params.append('teamId', filters.teamId); + if (filters.projectId) params.append('projectId', filters.projectId); + if (filters.tags.length > 0) params.append('tags', filters.tags.join(',')); + if (filters.searchSlug) params.append('searchSlug', filters.searchSlug); + if (filters.sortBy) params.append('sortBy', filters.sortBy); + if (filters.sortOrder) params.append('sortOrder', filters.sortOrder); const response = await fetch(`/api/events?${params.toString()}`); const data = await response.json(); @@ -54,15 +80,14 @@ export default function EventsPage() { throw new Error(data.error || 'Failed to fetch events'); } - const eventsData = data.data || data.events || []; - if (pageNum === 1) { - setEvents(eventsData); + setEvents(data.data || []); } else { - setEvents(prev => [...prev, ...eventsData]); + setEvents(prev => [...prev, ...(data.data || [])]); } - setHasMore(Array.isArray(eventsData) && eventsData.length === 50); + setTotalEvents(data.meta?.total || 0); + setHasMore(data.data && data.data.length === 20); } catch (err) { console.error("Error fetching events:", err); setError(err instanceof Error ? err.message : 'An error occurred while fetching events'); @@ -72,13 +97,20 @@ export default function EventsPage() { } }; + // 初始化加载 + useEffect(() => { + fetchTags(); + }, []); + + // 当过滤条件改变时重新加载数据 useEffect(() => { setPage(1); setEvents([]); setLoading(true); fetchEvents(1); - }, [dateRange, filter]); + }, [dateRange, filters]); + // 加载更多数据 const loadMore = () => { if (!loading && hasMore) { const nextPage = page + 1; @@ -87,6 +119,23 @@ export default function EventsPage() { } }; + // 重置过滤条件 + const resetFilters = () => { + setFilters({ + eventType: '', + linkId: '', + linkSlug: '', + userId: '', + teamId: '', + projectId: '', + tags: [], + searchSlug: '', + sortBy: 'event_time', + sortOrder: 'desc' + }); + }; + + // 格式化日期 const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString(); @@ -102,153 +151,213 @@ export default function EventsPage() { return (
-
-

Events

- -
- -
-
+ {/* 过滤器部分 */} +
+

过滤器

+
+
+ +
-
- setFilter(prev => ({ ...prev, linkId: e.target.value }))} - placeholder="Enter Link ID" - className="block w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md text-sm text-gray-900 dark:text-gray-100" + placeholder="链接 ID" + value={filters.linkId} + onChange={(e) => setFilters(prev => ({ ...prev, linkId: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" />
- setFilter(prev => ({ ...prev, linkSlug: e.target.value }))} - placeholder="Enter Link Slug" - className="block w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md text-sm text-gray-900 dark:text-gray-100" + placeholder="链接短码" + value={filters.linkSlug} + onChange={(e) => setFilters(prev => ({ ...prev, linkSlug: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" />
+
+ setFilters(prev => ({ ...prev, userId: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ setFilters(prev => ({ ...prev, teamId: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ setFilters(prev => ({ ...prev, projectId: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ setFilters(prev => ({ ...prev, searchSlug: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ +
+
+ +
+
+ + {/* 标签选择 */} +
+

标签

+
+ {tags.map(tag => ( + + ))} +
+
+ +
+
-
-
- - - - - - - - - - - - - - {Array.isArray(events) && events.map((event, index) => ( - - - - - - - - - - ))} - -
- Time - - Type - - Link - - Visitor - - Location - - Referrer - - Conversion -
- {event.event_time && formatDate(event.event_time)} - - - {event.event_type || 'unknown'} + {/* 事件列表 */} +
+
+

事件列表 ({totalEvents})

+
+ +
+ {events.map((event) => ( +
+
+
+ {event.event_type} + {formatDate(event.event_time)} +
+ + {event.device_type} + +
+ +
+
+

链接: {event.link_slug}

+

用户: {event.user_name || event.user_id}

+
+
+

IP: {event.ip_address}

+

位置: {event.country} {event.city}

+
+
+ + {event.link_tags && ( +
+ {(typeof event.link_tags === 'string' ? + (() => { + try { + return JSON.parse(event.link_tags); + } catch (e) { + console.error('Error parsing link_tags:', e); + return []; + } + })() : + event.link_tags + ).map((tag: string) => ( + + {tag} -
-
-
{event.link_slug || '-'}
-
{event.link_original_url || '-'}
-
-
-
-
{event.browser || '-'}
-
{event.os || '-'} / {event.device_type || '-'}
-
-
-
-
{event.city || '-'}
-
{event.country || '-'}
-
-
- {event.referrer || '-'} - -
-
{event.conversion_type || '-'}
- {event.conversion_value > 0 && ( -
Value: {event.conversion_value}
- )} -
-
+ ))} +
+ )} +
+ ))}
- {loading && ( -
-
-
- )} - - {!loading && hasMore && ( -
+ {hasMore && ( +
)} - {!loading && Array.isArray(events) && events.length === 0 && ( -
- No events found + {!loading && events.length === 0 && ( +
+ 没有找到符合条件的事件
)}