diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index 85e0fe8..350120f 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -673,10 +673,10 @@ function AnalyticsContent() { /> - {/* 路径分析 - 仅在选中特定链接时显示 */} + {/* Path Analysis - 仅在选中特定链接时显示 */} {selectedShortUrl && selectedShortUrl.externalId && (
-

路径分析

+

Path Analysis

)} + + {/* Recent Events Table */} +
+
+

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 || '-'} +
+
+ + {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))} + +
+
+
+
+ )} +
); } diff --git a/app/api/events/path-analytics/route.ts b/app/api/events/path-analytics/route.ts index 19a6921..032714e 100644 --- a/app/api/events/path-analytics/route.ts +++ b/app/api/events/path-analytics/route.ts @@ -13,7 +13,7 @@ export async function GET(request: NextRequest) { if (!startTime || !endTime || !linkId) { return NextResponse.json({ success: false, - error: '缺少必要参数' + error: 'Missing required parameters' }, { status: 400 }); } @@ -70,10 +70,10 @@ export async function GET(request: NextRequest) { return NextResponse.json(response); } catch (error) { - console.error('获取路径分析数据错误:', error); + console.error('Error fetching path analytics data:', error); const response: ApiResponse = { success: false, - error: error instanceof Error ? error.message : '服务器内部错误' + error: error instanceof Error ? error.message : 'Internal server error' }; return NextResponse.json(response, { status: 500 }); } diff --git a/app/components/analytics/PathAnalytics.tsx b/app/components/analytics/PathAnalytics.tsx index 50c1298..e9e6d1e 100644 --- a/app/components/analytics/PathAnalytics.tsx +++ b/app/components/analytics/PathAnalytics.tsx @@ -34,18 +34,47 @@ const PathAnalytics: React.FC = ({ startTime, endTime, linkI const response = await fetch(`/api/events/path-analytics?${params.toString()}`); if (!response.ok) { - throw new Error('获取路径分析数据失败'); + throw new Error('Failed to fetch path analytics data'); } const result = await response.json(); if (result.success && result.data) { - setPathData(result.data); + // 自定义处理路径数据,根据是否有子路径来分组 + const rawData = result.data; + const pathMap = new Map(); + let totalClicks = 0; + + rawData.forEach((item: PathData) => { + const urlPath = item.path; + totalClicks += item.count; + + // 解析路径,检查是否有子路径 + const pathParts = urlPath.split('?')[0].split('/').filter(Boolean); + + // 基础路径(例如/5seaii)或者带有查询参数但没有子路径的路径视为同一个路径 + // 子路径(例如/5seaii/bbbbb)单独统计 + const groupKey = pathParts.length > 1 ? urlPath : `/${pathParts[0]}`; + + const currentCount = pathMap.get(groupKey) || 0; + pathMap.set(groupKey, currentCount + item.count); + }); + + // 转换回数组并排序 + const groupedPathData = Array.from(pathMap.entries()) + .map(([path, count]) => ({ + path, + count, + percentage: totalClicks > 0 ? count / totalClicks : 0, + })) + .sort((a, b) => b.count - a.count); + + setPathData(groupedPathData); } else { - setError(result.error || '加载路径分析失败'); + setError(result.error || 'Failed to load path analytics'); } } catch (err) { - setError(err instanceof Error ? err.message : '发生错误'); + setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setLoading(false); } @@ -65,25 +94,25 @@ const PathAnalytics: React.FC = ({ startTime, endTime, linkI } if (!linkId) { - return
选择一个特定链接查看路径分析。
; + return
Select a specific link to view path analytics.
; } if (pathData.length === 0) { - return
该链接暂无路径数据。
; + return
No path data available for this link.
; } return (
- 注意:不同的URL参数组合会被视为不同的路径(例如 /abc?p=1 和 /abc?p=2 属于不同路径) + Note: Paths are grouped by subpath. URLs with different query parameters but the same base path (without subpath) are grouped together.
- - - + + +
路径点击数百分比PathClicksPercentage