205 lines
7.6 KiB
TypeScript
205 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
interface UtmData {
|
|
utm_value: string;
|
|
clicks: number;
|
|
visitors: number;
|
|
avg_time_spent: number;
|
|
bounces: number;
|
|
conversions: number;
|
|
}
|
|
|
|
interface UtmAnalyticsProps {
|
|
startTime?: string;
|
|
endTime?: string;
|
|
linkId?: string;
|
|
teamIds?: string[];
|
|
projectIds?: string[];
|
|
tagIds?: string[];
|
|
subpath?: string;
|
|
}
|
|
|
|
export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath }: UtmAnalyticsProps) {
|
|
const [activeTab, setActiveTab] = useState<string>('source');
|
|
const [utmData, setUtmData] = useState<UtmData[]>([]);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// 加载UTM数据
|
|
useEffect(() => {
|
|
const fetchUtmData = async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// 构建URL参数
|
|
const params = new URLSearchParams();
|
|
if (startTime) params.append('startTime', startTime);
|
|
if (endTime) params.append('endTime', endTime);
|
|
if (linkId) params.append('linkId', linkId);
|
|
if (subpath) params.append('subpath', subpath);
|
|
params.append('utmType', activeTab);
|
|
|
|
// 添加团队ID参数
|
|
if (teamIds && teamIds.length > 0) {
|
|
teamIds.forEach(id => params.append('teamId', id));
|
|
}
|
|
|
|
// 添加项目ID参数
|
|
if (projectIds && projectIds.length > 0) {
|
|
projectIds.forEach(id => params.append('projectId', id));
|
|
}
|
|
|
|
// 添加标签名称参数
|
|
if (tagIds && tagIds.length > 0) {
|
|
tagIds.forEach(tagName => params.append('tagName', tagName));
|
|
}
|
|
|
|
// 发送请求
|
|
const response = await fetch(`/api/events/utm?${params}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch UTM data');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
setUtmData(result.data || []);
|
|
} else {
|
|
throw new Error(result.error || 'Failed to fetch UTM data');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unknown error occurred');
|
|
console.error('Error fetching UTM data:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchUtmData();
|
|
}, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath]);
|
|
|
|
// 安全地格式化数字
|
|
const formatNumber = (value: number | undefined | null): string => {
|
|
if (value === undefined || value === null) return '0';
|
|
return value.toLocaleString();
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">UTM Parameters</h2>
|
|
|
|
<div className="mb-4 border-b">
|
|
<div className="flex">
|
|
<button
|
|
onClick={() => setActiveTab('source')}
|
|
className={`px-4 py-2 ${activeTab === 'source' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
|
>
|
|
Source
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('medium')}
|
|
className={`px-4 py-2 ${activeTab === 'medium' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
|
>
|
|
Medium
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('campaign')}
|
|
className={`px-4 py-2 ${activeTab === 'campaign' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
|
>
|
|
Campaign
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('term')}
|
|
className={`px-4 py-2 ${activeTab === 'term' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
|
>
|
|
Term
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('content')}
|
|
className={`px-4 py-2 ${activeTab === 'content' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
|
>
|
|
Content
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex justify-center items-center py-8">
|
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
|
<span className="ml-2 text-gray-500">Loading...</span>
|
|
</div>
|
|
) : error ? (
|
|
<div className="text-red-500 text-center py-8">
|
|
Error: {error}
|
|
</div>
|
|
) : utmData.length === 0 ? (
|
|
<div className="text-gray-500 text-center py-8">
|
|
No data available
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{activeTab === 'source' ? 'Source' :
|
|
activeTab === 'medium' ? 'Medium' :
|
|
activeTab === 'campaign' ? 'Campaign' :
|
|
activeTab === 'term' ? 'Term' : 'Content'}
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Clicks
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Visitors
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Avg. Time
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Bounce Rate
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Conversions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{utmData.map((item, index) => {
|
|
const bounceRate = item.clicks > 0 ? (item.bounces / item.clicks) * 100 : 0;
|
|
const conversionRate = item.clicks > 0 ? (item.conversions / item.clicks) * 100 : 0;
|
|
|
|
return (
|
|
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{item.utm_value || 'Unknown'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatNumber(item.clicks)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatNumber(item.visitors)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{item.avg_time_spent.toFixed(1)}s
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{bounceRate.toFixed(1)}%
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatNumber(item.conversions)} ({conversionRate.toFixed(1)}%)
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|