Files
shorturl-analytics/app/components/analytics/PathAnalytics.tsx
2025-04-10 17:19:40 +08:00

156 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
interface PathAnalyticsProps {
startTime: string;
endTime: string;
linkId?: string;
onPathClick?: (path: string) => void;
}
interface PathData {
path: string;
count: number;
percentage: number;
}
const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkId, onPathClick }) => {
const [loading, setLoading] = useState(true);
const [pathData, setPathData] = useState<PathData[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!linkId) {
setLoading(false);
return;
}
const fetchPathData = async () => {
try {
const params = new URLSearchParams({
startTime,
endTime,
linkId
});
const response = await fetch(`/api/events/path-analytics?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch path analytics data');
}
const result = await response.json();
if (result.success && result.data) {
// 自定义处理路径数据,根据是否有子路径来分组
const rawData = result.data;
const pathMap = new Map<string, number>();
let totalClicks = 0;
rawData.forEach((item: PathData) => {
const urlPath = item.path.split('?')[0];
totalClicks += item.count;
// 解析路径,检查是否有子路径
const pathParts = urlPath.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 || 'Failed to load path analytics');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchPathData();
}, [startTime, endTime, linkId]);
const handlePathClick = (path: string, e: React.MouseEvent) => {
e.preventDefault();
if (onPathClick) {
onPathClick(path);
}
};
if (loading) {
return <div className="py-8 flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" />
</div>;
}
if (error) {
return <div className="py-4 text-red-500">{error}</div>;
}
if (!linkId) {
return <div className="py-4 text-gray-500">Select a specific link to view path analytics.</div>;
}
if (pathData.length === 0) {
return <div className="py-4 text-gray-500">No path data available for this link.</div>;
}
return (
<div>
<div className="text-sm text-gray-500 mb-4">
Note: Paths are grouped by subpath. URLs with different query parameters but the same base path (without subpath) are grouped together.
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Path</th>
<th className="px-6 py-3 bg-gray-50 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Clicks</th>
<th className="px-6 py-3 bg-gray-50 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Percentage</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{pathData.map((item, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<a
href="#"
className="hover:text-blue-600 hover:underline cursor-pointer"
onClick={(e) => handlePathClick(item.path, e)}
>
{item.path}
</a>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{item.count}</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<div className="flex items-center justify-end">
<span className="text-sm text-gray-500 mr-2">{(item.percentage * 100).toFixed(1)}%</span>
<div className="w-32 bg-gray-200 rounded-full h-2.5">
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${item.percentage * 100}%` }}></div>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default PathAnalytics;