diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx new file mode 100644 index 0000000..aaefe59 --- /dev/null +++ b/app/analytics/page.tsx @@ -0,0 +1,812 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { format, subDays } 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 DevicePieCharts from '@/app/components/charts/DevicePieCharts'; +import UtmAnalytics from '@/app/components/analytics/UtmAnalytics'; +import { EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics as DeviceAnalyticsType } from '@/app/api/types'; +import { TeamSelector } from '@/app/components/ui/TeamSelector'; +import { ProjectSelector } from '@/app/components/ui/ProjectSelector'; +import { TagSelector } from '@/app/components/ui/TagSelector'; + +// 事件类型定义 +interface Event { + event_id?: string; + url_id: string; + url: string; + event_type: string; + visitor_id: string; + created_at: string; + event_time?: string; + referrer?: string; + browser?: string; + os?: string; + device_type?: string; + country?: string; + city?: string; + event_attributes?: string; + link_attributes?: string; + user_attributes?: string; + link_label?: string; + link_original_url?: string; + team_name?: string; + project_name?: string; + link_id?: string; + link_slug?: string; + link_tags?: string; + ip_address?: string; +} + +// 格式化日期函数 +const formatDate = (dateString: string | undefined) => { + if (!dateString) return ''; + try { + return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss'); + } catch { + return dateString; + } +}; + +// 解析JSON字符串 +const parseJsonSafely = (jsonString: string) => { + if (!jsonString) return null; + try { + return JSON.parse(jsonString); + } catch { + return null; + } +}; + +// 获取用户可读名称 +const getUserDisplayName = (user: Record | null) => { + if (!user) return '-'; + if (typeof user.full_name === 'string') return user.full_name; + if (typeof user.name === 'string') return user.name; + if (typeof user.email === 'string') return user.email; + return '-'; +}; + +// 提取链接和事件的重要信息 +const extractEventInfo = (event: Event) => { + // 解析事件属性 + const eventAttrs = parseJsonSafely(event.event_attributes || '{}'); + + // 解析链接属性 + const linkAttrs = parseJsonSafely(event.link_attributes || '{}'); + + // 解析用户属性 + const userAttrs = parseJsonSafely(event.user_attributes || '{}'); + + // 解析标签信息 + let tags: string[] = []; + try { + if (event.link_tags) { + const parsedTags = JSON.parse(event.link_tags); + if (Array.isArray(parsedTags)) { + tags = parsedTags; + } + } + } catch { + // 解析失败则保持空数组 + } + + return { + eventTime: event.created_at || event.event_time, + linkName: event.link_label || linkAttrs?.name || eventAttrs?.link_name || event.link_slug || '-', + originalUrl: event.link_original_url || eventAttrs?.origin_url || '-', + fullUrl: eventAttrs?.full_url || '-', + eventType: event.event_type || '-', + visitorId: event.visitor_id?.substring(0, 8) || '-', + referrer: eventAttrs?.referrer || '-', + ipAddress: event.ip_address || '-', + location: event.country ? (event.city ? `${event.city}, ${event.country}` : event.country) : '-', + device: event.device_type || '-', + browser: event.browser || '-', + os: event.os || '-', + userInfo: getUserDisplayName(userAttrs), + teamName: event.team_name || '-', + projectName: event.project_name || '-', + tags: tags + }; +}; + +export default function AnalyticsPage() { + // 默认日期范围为最近7天 + const today = new Date(); + const [dateRange, setDateRange] = useState({ + from: subDays(today, 7), // 7天前 + to: today // 今天 + }); + + // 添加团队选择状态 - 使用数组支持多选 + const [selectedTeamIds, setSelectedTeamIds] = useState([]); + // 添加项目选择状态 - 使用数组支持多选 + const [selectedProjectIds, setSelectedProjectIds] = useState([]); + // 添加标签名称状态 - 用于在UI中显示和API请求 + const [selectedTagNames, setSelectedTagNames] = useState([]); + // 添加分页状态 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [totalEvents, setTotalEvents] = useState(0); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [summary, setSummary] = useState(null); + const [timeSeriesData, setTimeSeriesData] = useState([]); + const [geoData, setGeoData] = useState([]); + const [deviceData, setDeviceData] = useState(null); + const [events, setEvents] = useState([]); + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + setError(null); + + 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'"); + + // 构建基础URL和查询参数 + const baseUrl = '/api/events'; + const params = new URLSearchParams({ + startTime, + endTime, + page: currentPage.toString(), + pageSize: pageSize.toString() + }); + + // 添加团队ID参数 - 支持多个团队 + if (selectedTeamIds.length > 0) { + selectedTeamIds.forEach(teamId => { + params.append('teamId', teamId); + }); + } + + // 添加项目ID参数 - 支持多个项目 + if (selectedProjectIds.length > 0) { + selectedProjectIds.forEach(projectId => { + params.append('projectId', projectId); + }); + } + + // 添加标签名称参数 - 支持多个标签 + if (selectedTagNames.length > 0) { + selectedTagNames.forEach(tagName => { + params.append('tagName', tagName); + }); + } + + // 并行获取所有数据 + const [summaryRes, timeSeriesRes, geoRes, deviceRes, eventsRes] = await Promise.all([ + fetch(`${baseUrl}/summary?${params.toString()}`), + fetch(`${baseUrl}/time-series?${params.toString()}`), + fetch(`${baseUrl}/geo?${params.toString()}`), + fetch(`${baseUrl}/devices?${params.toString()}`), + fetch(`${baseUrl}?${params.toString()}`) + ]); + + const [summaryData, timeSeriesData, geoData, deviceData, eventsData] = await Promise.all([ + summaryRes.json(), + timeSeriesRes.json(), + geoRes.json(), + deviceRes.json(), + eventsRes.json() + ]); + + if (!summaryRes.ok) throw new Error(summaryData.error || 'Failed to fetch summary data'); + if (!timeSeriesRes.ok) throw new Error(timeSeriesData.error || 'Failed to fetch time series data'); + if (!geoRes.ok) throw new Error(geoData.error || 'Failed to fetch geo data'); + if (!deviceRes.ok) throw new Error(deviceData.error || 'Failed to fetch device data'); + if (!eventsRes.ok) throw new Error(eventsData.error || 'Failed to fetch events data'); + + setSummary(summaryData.data); + setTimeSeriesData(timeSeriesData.data); + setGeoData(geoData.data); + setDeviceData(deviceData.data); + setEvents(eventsData.data || []); + + // 设置总事件数量用于分页 + if (eventsData.meta) { + // 确保将total转换为数字,无论它是字符串还是数字 + const totalCount = parseInt(String(eventsData.meta.total), 10); + if (!isNaN(totalCount)) { + setTotalEvents(totalCount); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+
+

Analytics Dashboard

+
+ { + const newTeamIds = Array.isArray(value) ? value : [value]; + + // Check if team selection has changed + if (JSON.stringify(newTeamIds) !== JSON.stringify(selectedTeamIds)) { + // Clear project selection when team changes + setSelectedProjectIds([]); + + // Update team selection + setSelectedTeamIds(newTeamIds); + } + }} + className="w-[250px]" + multiple={true} + /> + setSelectedProjectIds(Array.isArray(value) ? value : [value])} + className="w-[250px]" + multiple={true} + teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} + /> + { + // TagSelector返回的是标签名称 + if (Array.isArray(value)) { + setSelectedTagNames(value); + } else { + setSelectedTagNames(value ? [value] : []); + } + // 我们需要将标签名称映射回ID,但由于TagSelector内部已经做了处理 + // 这里不需要额外的映射代码,selectedTagNames存储名称即可 + }} + className="w-[250px]" + multiple={true} + teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} + /> + +
+
+ + {/* 显示团队选择信息 */} + {selectedTeamIds.length > 0 && ( +
+ + {selectedTeamIds.length === 1 ? 'Team filter:' : 'Teams filter:'} + +
+ {selectedTeamIds.map(teamId => ( + + {teamId} + + + ))} + {selectedTeamIds.length > 0 && ( + + )} +
+
+ )} + + {/* 显示项目选择信息 */} + {selectedProjectIds.length > 0 && ( +
+ + {selectedProjectIds.length === 1 ? 'Project filter:' : 'Projects filter:'} + +
+ {selectedProjectIds.map(projectId => ( + + {projectId} + + + ))} + {selectedProjectIds.length > 0 && ( + + )} +
+
+ )} + + {/* 显示标签选择信息 */} + {selectedTagNames.length > 0 && ( +
+ + {selectedTagNames.length === 1 ? 'Tag filter:' : 'Tags filter:'} + +
+ {selectedTagNames.map(tagName => ( + + {tagName} + + + ))} + {selectedTagNames.length > 0 && ( + + )} +
+
+ )} + + {/* 仪表板内容 - 现在放在事件列表之后 */} + <> + {summary && ( +
+
+

Total Events

+

+ {typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents} +

+
+
+

Unique Visitors

+

+ {typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors} +

+
+
+

Total Conversions

+

+ {typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions} +

+
+
+

Avg. Time Spent

+

+ {summary.averageTimeSpent?.toFixed(1) || '0'}s +

+
+
+ )} + + {/* 时间序列图表 */} + {timeSeriesData && timeSeriesData.length > 0 && ( +
+

Traffic Over Time

+
+ +
+
+ )} + + {/* 设备分析图表 */} + {deviceData && ( +
+

Device Analytics

+ +
+ )} + + {/* 地理分析 */} + {geoData && geoData.length > 0 && ( +
+

Geographic Distribution

+ +
+ )} + + {/* UTM 参数分析 */} +
+

UTM Parameters Analysis

+ +
+ +
+
+

Recent Events

+
+ +
+ + + + + + + + + + + + + + + + + {events.map((event, index) => { + const info = extractEventInfo(event); + return ( + + + + + + + + + + + + + ); + })} + +
+ Time + + Link Name + + Original URL + + Full URL + + Event Type + + Tags + + User + + Team/Project + + IP/Location + + Device Info +
+ {formatDate(info.eventTime)} + + {info.linkName} +
+ ID: {event.link_id?.substring(0, 8) || '-'} +
+
+ + {info.originalUrl} + + + + {info.fullUrl} + + + + {info.eventType} + + +
+ {info.tags && info.tags.length > 0 ? ( + info.tags.map((tag, idx) => ( + + {tag} + + )) + ) : ( + - + )} +
+
+
{info.userInfo}
+
{info.visitorId}...
+
+
{info.teamName}
+
{info.projectName}
+
+
+ + IP: + {info.ipAddress} + + + Location: + {info.location} + +
+
+
+ + Device: + {info.device} + + + Browser: + {info.browser} + + + OS: + {info.os} + +
+
+
+ + {/* 表格为空状态 */} + {!loading && events.length === 0 && ( +
+ No events found +
+ )} + + {/* 分页控件 */} + {!loading && events.length > 0 && ( +
+
+ + +
+
+
+

+ Showing {events.length > 0 ? ((currentPage - 1) * pageSize) + 1 : 0} to {events.length > 0 ? ((currentPage - 1) * pageSize) + events.length : 0} of{' '} + {totalEvents} results +

+
+
+
+ +
+ + {/* 添加直接跳转到指定页的输入框 */} +
+ Go to: + { + const page = parseInt(e.target.value); + if (!isNaN(page) && page >= 1 && page <= Math.ceil(totalEvents / pageSize)) { + setCurrentPage(page); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const input = e.target as HTMLInputElement; + const page = parseInt(input.value); + if (!isNaN(page) && page >= 1 && page <= Math.ceil(totalEvents / pageSize)) { + setCurrentPage(page); + } + } + }} + className="w-16 px-3 py-1 border border-gray-300 rounded-md text-sm" + /> + + of {Math.max(1, Math.ceil(totalEvents / pageSize))} + +
+ + +
+
+
+ )} +
+ +
+ ); +} \ No newline at end of file diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 3bceb9b..8c844ad 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -14,7 +14,7 @@ export default function Header() {
- +
  • - + Dashboard
  • diff --git a/app/page.tsx b/app/page.tsx index d8b35af..87264c6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,800 +1,5 @@ -"use client"; +import { redirect } from 'next/navigation'; -import { useState, useEffect } from 'react'; -import { format, subDays } 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 DevicePieCharts from '@/app/components/charts/DevicePieCharts'; -import UtmAnalytics from '@/app/components/analytics/UtmAnalytics'; -import { EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics as DeviceAnalyticsType } from '@/app/api/types'; -import { TeamSelector } from '@/app/components/ui/TeamSelector'; -import { ProjectSelector } from '@/app/components/ui/ProjectSelector'; -import { TagSelector } from '@/app/components/ui/TagSelector'; - -// 事件类型定义 -interface Event { - event_id?: string; - url_id: string; - url: string; - event_type: string; - visitor_id: string; - created_at: string; - event_time?: string; - referrer?: string; - browser?: string; - os?: string; - device_type?: string; - country?: string; - city?: string; - event_attributes?: string; - link_attributes?: string; - user_attributes?: string; - link_label?: string; - link_original_url?: string; - team_name?: string; - project_name?: string; - link_id?: string; - link_slug?: string; - link_tags?: string; - ip_address?: string; -} - -// 格式化日期函数 -const formatDate = (dateString: string | undefined) => { - if (!dateString) return ''; - try { - return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss'); - } catch { - return dateString; - } -}; - -// 解析JSON字符串 -const parseJsonSafely = (jsonString: string) => { - if (!jsonString) return null; - try { - return JSON.parse(jsonString); - } catch { - return null; - } -}; - -// 获取用户可读名称 -const getUserDisplayName = (user: Record | null) => { - if (!user) return '-'; - if (typeof user.full_name === 'string') return user.full_name; - if (typeof user.name === 'string') return user.name; - if (typeof user.email === 'string') return user.email; - return '-'; -}; - -// 提取链接和事件的重要信息 -const extractEventInfo = (event: Event) => { - // 解析事件属性 - const eventAttrs = parseJsonSafely(event.event_attributes || '{}'); - - // 解析链接属性 - const linkAttrs = parseJsonSafely(event.link_attributes || '{}'); - - // 解析用户属性 - const userAttrs = parseJsonSafely(event.user_attributes || '{}'); - - // 解析标签信息 - let tags: string[] = []; - try { - if (event.link_tags) { - const parsedTags = JSON.parse(event.link_tags); - if (Array.isArray(parsedTags)) { - tags = parsedTags; - } - } - } catch { - // 解析失败则保持空数组 - } - - return { - eventTime: event.created_at || event.event_time, - linkName: event.link_label || linkAttrs?.name || eventAttrs?.link_name || event.link_slug || '-', - originalUrl: event.link_original_url || eventAttrs?.origin_url || '-', - fullUrl: eventAttrs?.full_url || '-', - eventType: event.event_type || '-', - visitorId: event.visitor_id?.substring(0, 8) || '-', - referrer: eventAttrs?.referrer || '-', - ipAddress: event.ip_address || '-', - location: event.country ? (event.city ? `${event.city}, ${event.country}` : event.country) : '-', - device: event.device_type || '-', - browser: event.browser || '-', - os: event.os || '-', - userInfo: getUserDisplayName(userAttrs), - teamName: event.team_name || '-', - projectName: event.project_name || '-', - tags: tags - }; -}; - -export default function HomePage() { - // 默认日期范围为最近7天 - const today = new Date(); - const [dateRange, setDateRange] = useState({ - from: subDays(today, 7), // 7天前 - to: today // 今天 - }); - - // 添加团队选择状态 - 使用数组支持多选 - const [selectedTeamIds, setSelectedTeamIds] = useState([]); - // 添加项目选择状态 - 使用数组支持多选 - const [selectedProjectIds, setSelectedProjectIds] = useState([]); - // 添加标签名称状态 - 用于在UI中显示和API请求 - const [selectedTagNames, setSelectedTagNames] = useState([]); - // 添加分页状态 - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [totalEvents, setTotalEvents] = useState(0); - - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [summary, setSummary] = useState(null); - const [timeSeriesData, setTimeSeriesData] = useState([]); - const [geoData, setGeoData] = useState([]); - const [deviceData, setDeviceData] = useState(null); - const [events, setEvents] = useState([]); - - useEffect(() => { - const fetchData = async () => { - setLoading(true); - setError(null); - - 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'"); - - // 构建基础URL和查询参数 - const baseUrl = '/api/events'; - const params = new URLSearchParams({ - startTime, - endTime, - page: currentPage.toString(), - pageSize: pageSize.toString() - }); - - // 添加团队ID参数 - 支持多个团队 - if (selectedTeamIds.length > 0) { - selectedTeamIds.forEach(teamId => { - params.append('teamId', teamId); - }); - } - - // 添加项目ID参数 - 支持多个项目 - if (selectedProjectIds.length > 0) { - selectedProjectIds.forEach(projectId => { - params.append('projectId', projectId); - }); - } - - // 添加标签名称参数 - 支持多个标签 - if (selectedTagNames.length > 0) { - selectedTagNames.forEach(tagName => { - params.append('tagName', tagName); - }); - } - - // 并行获取所有数据 - const [summaryRes, timeSeriesRes, geoRes, deviceRes, eventsRes] = await Promise.all([ - fetch(`${baseUrl}/summary?${params.toString()}`), - fetch(`${baseUrl}/time-series?${params.toString()}`), - fetch(`${baseUrl}/geo?${params.toString()}`), - fetch(`${baseUrl}/devices?${params.toString()}`), - fetch(`${baseUrl}?${params.toString()}`) - ]); - - const [summaryData, timeSeriesData, geoData, deviceData, eventsData] = await Promise.all([ - summaryRes.json(), - timeSeriesRes.json(), - geoRes.json(), - deviceRes.json(), - eventsRes.json() - ]); - - if (!summaryRes.ok) throw new Error(summaryData.error || 'Failed to fetch summary data'); - if (!timeSeriesRes.ok) throw new Error(timeSeriesData.error || 'Failed to fetch time series data'); - if (!geoRes.ok) throw new Error(geoData.error || 'Failed to fetch geo data'); - if (!deviceRes.ok) throw new Error(deviceData.error || 'Failed to fetch device data'); - if (!eventsRes.ok) throw new Error(eventsData.error || 'Failed to fetch events data'); - - setSummary(summaryData.data); - setTimeSeriesData(timeSeriesData.data); - setGeoData(geoData.data); - setDeviceData(deviceData.data); - setEvents(eventsData.data || []); - - // 设置总事件数量用于分页 - if (eventsData.meta) { - // 确保将total转换为数字,无论它是字符串还是数字 - const totalCount = parseInt(String(eventsData.meta.total), 10); - if (!isNaN(totalCount)) { - setTotalEvents(totalCount); - } - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize]); - - if (loading) { - return ( -
    -
    -
    - ); - } - - if (error) { - return ( -
    -
    {error}
    -
    - ); - } - - return ( -
    -
    -

    Analytics Dashboard

    -
    - { - const newTeamIds = Array.isArray(value) ? value : [value]; - - // Check if team selection has changed - if (JSON.stringify(newTeamIds) !== JSON.stringify(selectedTeamIds)) { - // Clear project selection when team changes - setSelectedProjectIds([]); - - // Update team selection - setSelectedTeamIds(newTeamIds); - } - }} - className="w-[250px]" - multiple={true} - /> - setSelectedProjectIds(Array.isArray(value) ? value : [value])} - className="w-[250px]" - multiple={true} - teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} - /> - { - // TagSelector返回的是标签名称 - if (Array.isArray(value)) { - setSelectedTagNames(value); - } else { - setSelectedTagNames(value ? [value] : []); - } - // 我们需要将标签名称映射回ID,但由于TagSelector内部已经做了处理 - // 这里不需要额外的映射代码,selectedTagNames存储名称即可 - }} - className="w-[250px]" - multiple={true} - teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} - /> - -
    -
    - - {/* 显示团队选择信息 */} - {selectedTeamIds.length > 0 && ( -
    - - {selectedTeamIds.length === 1 ? 'Team filter:' : 'Teams filter:'} - -
    - {selectedTeamIds.map(teamId => ( - - {teamId} - - - ))} - {selectedTeamIds.length > 0 && ( - - )} -
    -
    - )} - - {/* 显示项目选择信息 */} - {selectedProjectIds.length > 0 && ( -
    - - {selectedProjectIds.length === 1 ? 'Project filter:' : 'Projects filter:'} - -
    - {selectedProjectIds.map(projectId => ( - - {projectId} - - - ))} - {selectedProjectIds.length > 0 && ( - - )} -
    -
    - )} - - {/* 显示标签选择信息 */} - {selectedTagNames.length > 0 && ( -
    - - {selectedTagNames.length === 1 ? 'Tag filter:' : 'Tags filter:'} - -
    - {selectedTagNames.map(tagName => ( - - {tagName} - - - ))} - {selectedTagNames.length > 0 && ( - - )} -
    -
    - )} - - {/* 仪表板内容 - 现在放在事件列表之后 */} - <> - {summary && ( -
    -
    -

    Total Events

    -

    - {typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents} -

    -
    -
    -

    Unique Visitors

    -

    - {typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors} -

    -
    -
    -

    Total Conversions

    -

    - {typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions} -

    -
    -
    -

    Avg. Time Spent

    -

    - {summary.averageTimeSpent?.toFixed(1) || '0'}s -

    -
    -
    - )} - -
    -
    -

    Recent Events

    -
    - -
    - - - - - - - - - - - - - - - - - {events.map((event, index) => { - const info = extractEventInfo(event); - return ( - - - - - - - - - - - - - ); - })} - -
    - Time - - Link Name - - Original URL - - Full URL - - Event Type - - Tags - - User - - Team/Project - - IP/Location - - Device Info -
    - {formatDate(info.eventTime)} - - {info.linkName} -
    - ID: {event.link_id?.substring(0, 8) || '-'} -
    -
    - - {info.originalUrl} - - - - {info.fullUrl} - - - - {info.eventType} - - -
    - {info.tags && info.tags.length > 0 ? ( - info.tags.map((tag, idx) => ( - - {tag} - - )) - ) : ( - - - )} -
    -
    -
    {info.userInfo}
    -
    {info.visitorId}...
    -
    -
    {info.teamName}
    -
    {info.projectName}
    -
    -
    - - IP: - {info.ipAddress} - - - Location: - {info.location} - -
    -
    -
    - - Device: - {info.device} - - - Browser: - {info.browser} - - - OS: - {info.os} - -
    -
    -
    - - {/* 表格为空状态 */} - {!loading && events.length === 0 && ( -
    - No events found -
    - )} - - {/* 分页控件 - 删除totalEvents > 0条件,改为events.length > 0 */} - {!loading && events.length > 0 && ( -
    -
    - - -
    -
    -
    -

    - Showing {events.length > 0 ? ((currentPage - 1) * pageSize) + 1 : 0} to {events.length > 0 ? ((currentPage - 1) * pageSize) + events.length : 0} of{' '} - {totalEvents} results -

    -
    -
    -
    - -
    - - {/* 添加直接跳转到指定页的输入框 */} -
    - Go to: - { - const page = parseInt(e.target.value); - if (!isNaN(page) && page >= 1 && page <= Math.ceil(totalEvents / pageSize)) { - setCurrentPage(page); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - const input = e.target as HTMLInputElement; - const page = parseInt(input.value); - if (!isNaN(page) && page >= 1 && page <= Math.ceil(totalEvents / pageSize)) { - setCurrentPage(page); - } - } - }} - className="w-16 px-3 py-1 border border-gray-300 rounded-md text-sm" - /> - - of {Math.max(1, Math.ceil(totalEvents / pageSize))} - -
    - - -
    -
    -
    - )} -
    - -
    -

    Event Trends

    -
    - -
    -
    - -
    -

    Device Analytics

    - {deviceData && } -
    - -
    -

    Geographic Distribution

    - -
    - - {/* 添加UTM分析组件 */} - - -
    - ); +export default function Home() { + redirect('/analytics'); } \ No newline at end of file