utm
This commit is contained in:
185
app/components/analytics/UtmAnalytics.tsx
Normal file
185
app/components/analytics/UtmAnalytics.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"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;
|
||||
}
|
||||
|
||||
export default function UtmAnalytics({ startTime, endTime, linkId }: 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);
|
||||
params.append('utmType', activeTab);
|
||||
|
||||
// 发送请求
|
||||
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]);
|
||||
|
||||
// 安全地格式化数字
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user