"use client"; import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { LinkOverviewData, ConversionFunnelData, VisitTrendsData, LinkPerformanceData, PlatformDistributionData, DeviceAnalysisData, PopularReferrersData, QrCodeAnalysisData, DeviceItem, ReferrerItem, } from "@/app/api/types"; import { TimeGranularity } from "@/lib/analytics"; import { PieChart, Pie, ResponsiveContainer, Tooltip, Cell, Legend, BarChart, Bar, XAxis, YAxis, CartesianGrid, LineChart, Line, LabelList } from "recharts"; interface LinkDetails { id: string; name: string; shortUrl: string; originalUrl: string; creator: string; createdAt: string; visits: number; visitChange: number; uniqueVisitors: number; uniqueVisitorsChange: number; avgTime: string; avgTimeChange: number; conversionRate: number; conversionChange: number; status: "active" | "inactive" | "expired"; tags: string[]; } export default function LinkDetailsPage({ params, }: { params: { id: string }; }) { const router = useRouter(); const [linkId, setLinkId] = useState(params.id); const [linkDetails, setLinkDetails] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState< | "overview" | "referrers" | "devices" | "locations" | "performance" | "qrCodes" >("overview"); // 添加state变量存储分析数据 const [overviewData, setOverviewData] = useState( null ); const [funnelData, setFunnelData] = useState( null ); const [trendsData, setTrendsData] = useState(null); // 添加新的state变量存储新API的数据 const [performanceData, setPerformanceData] = useState(null); const [platformData, setPlatformData] = useState(null); const [deviceData, setDeviceData] = useState(null); const [referrersData, setReferrersData] = useState(null); const [qrCodeData, setQrCodeData] = useState(null); const [timeGranularity, setTimeGranularity] = useState( TimeGranularity.DAY ); const [dateRange, setDateRange] = useState({ startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) .toISOString() .split("T")[0], // 30天前 endDate: new Date().toISOString().split("T")[0], // 今天 }); // 定义fetchAnalyticsData函数在所有useEffect前 const fetchAnalyticsData = useCallback(async (id: string) => { try { // 构建查询参数 const queryParams = new URLSearchParams({ linkId: id, startDate: dateRange.startDate, endDate: dateRange.endDate, }); // 并行获取所有数据 const [ overviewResponse, funnelResponse, trendsResponse, performanceResponse, platformResponse, deviceResponse, referrersResponse, qrCodeResponse, ] = await Promise.all([ fetch(`/api/analytics/overview?${queryParams}`), fetch(`/api/analytics/funnel?${queryParams}`), fetch( `/api/analytics/trends?${queryParams}&granularity=${timeGranularity}` ), fetch(`/api/analytics/link-performance?${queryParams}`), fetch(`/api/analytics/platform-distribution?${queryParams}`), fetch(`/api/analytics/device-analysis?${queryParams}`), fetch(`/api/analytics/popular-referrers?${queryParams}`), fetch(`/api/analytics/qr-code-analysis?${queryParams}`), ]); // 检查所有响应 if ( !overviewResponse.ok || !funnelResponse.ok || !trendsResponse.ok || !performanceResponse.ok || !platformResponse.ok || !deviceResponse.ok || !referrersResponse.ok || !qrCodeResponse.ok ) { throw new Error("Failed to fetch analytics data"); } // 解析所有响应数据 const [ overviewResult, funnelResult, trendsResult, performanceResult, platformResult, deviceResult, referrersResult, qrCodeResult, ] = await Promise.all([ overviewResponse.json(), funnelResponse.json(), trendsResponse.json(), performanceResponse.json(), platformResponse.json(), deviceResponse.json(), referrersResponse.json(), qrCodeResponse.json(), ]); // 设置状态 setOverviewData(overviewResult); setFunnelData(funnelResult); setTrendsData(trendsResult); setPerformanceData(performanceResult); setPlatformData(platformResult); setDeviceData(deviceResult); setReferrersData(referrersResult); setQrCodeData(qrCodeResult); // 更新链接详情中的统计数据 setLinkDetails((prev) => { if (!prev) return prev; return { ...prev, visits: overviewResult.totalVisits, uniqueVisitors: overviewResult.uniqueVisitors, avgTime: formatTime(overviewResult.averageTimeSpent), conversionRate: funnelResult.conversionRate, }; }); setLoading(false); } catch (error) { console.error("Failed to fetch analytics data:", error); setLoading(false); } }, [dateRange, timeGranularity]); // 获取并设置linkId useEffect(() => { const loadParams = async () => { const resolvedParams = await params; setLinkId(resolvedParams.id); }; loadParams(); }, [params]); // 获取链接详情数据 useEffect(() => { if (!linkId) return; const fetchLinkDetails = async () => { setLoading(true); try { // 获取链接详情 const response = await fetch(`/api/links/${linkId}/details`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || "Failed to fetch link details"); } const details = await response.json(); console.log("Link details:", details); // 添加日志以确认 API 响应 // 将 API 返回的数据映射到组件需要的格式 setLinkDetails({ id: details.link_id || linkId, name: details.title || "Untitled Link", shortUrl: details.short_url || window.location.hostname + "/" + details.link_id, originalUrl: details.original_url || "", creator: details.created_by || "Unknown", createdAt: details.created_at || new Date().toISOString(), visits: details.visits || 0, visitChange: details.visit_change || 0, uniqueVisitors: details.unique_visitors || 0, uniqueVisitorsChange: details.unique_visitors_change || 0, avgTime: formatTime(details.avg_time_spent || 0), avgTimeChange: details.avg_time_change || 0, conversionRate: details.conversion_rate || 0, conversionChange: details.conversion_change || 0, status: details.is_active ? "active" : "inactive", tags: details.tags || [], }); setLoading(false); // 获取分析数据 fetchAnalyticsData(linkId); } catch (error) { console.error("Failed to fetch link details:", error); setLoading(false); } }; fetchLinkDetails(); }, [linkId, fetchAnalyticsData]); // 格式化时间(秒转为分钟和秒) const formatTime = (seconds: number) => { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.round(seconds % 60); return `${minutes}m ${remainingSeconds}s`; }; // 更新时间粒度并重新获取趋势数据 const updateTimeGranularity = (granularity: TimeGranularity) => { setTimeGranularity(granularity); if (linkId) { const queryParams = new URLSearchParams({ linkId, startDate: dateRange.startDate, endDate: dateRange.endDate, granularity, }); fetch(`/api/analytics/trends?${queryParams}`) .then((res) => res.json()) .then((data) => setTrendsData(data)) .catch((err) => console.error("Failed to update trends data:", err)); } }; // 更新日期范围并重新获取所有数据 const updateDateRange = (startDate: string, endDate: string) => { setDateRange({ startDate, endDate }); if (linkId) { fetchAnalyticsData(linkId); } }; const goBack = () => { router.back(); }; // 复制链接到剪贴板 const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); // 可以添加一个复制成功的提示 }; // 添加加载和错误处理 if (loading) { return (

加载中...

正在获取链接详情数据

); } if (!linkDetails) { return (

未找到链接

无法获取该链接的详细信息

返回链接列表
); } // 只有当linkDetails存在时才渲染详情内容 return (
{/* 顶部导航栏 */}
{/* 链接基本信息卡片 */}

{linkDetails.name}

Short URL
{linkDetails.shortUrl}
Created By

{linkDetails.creator}

Created At

{linkDetails.createdAt}

Status
{linkDetails.status ? linkDetails.status.charAt(0).toUpperCase() + linkDetails.status.slice(1) : 'Unknown'}
{linkDetails.tags && linkDetails.tags.length > 0 && (
Tags
{linkDetails.tags.map((tag) => ( {tag} ))}
)}
{/* 性能指标卡片 */}

Performance Metrics

{/* Total Visits */}
Total Visits
= 0 ? "text-accent-green" : "text-accent-red"}`}> = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" > {Math.abs(linkDetails?.visitChange || 0)}%

{linkDetails?.visits !== undefined ? linkDetails.visits.toLocaleString() : '0'}

{/* Unique Visitors */}
Unique Visitors
= 0 ? "text-accent-green" : "text-accent-red"}`}> = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" > {Math.abs(linkDetails?.uniqueVisitorsChange || 0)}%

{linkDetails?.uniqueVisitors !== undefined ? linkDetails.uniqueVisitors.toLocaleString() : '0'}

{/* Average Visit Time */}
Avg. Time
= 0 ? "text-accent-green" : "text-accent-red"}`}> = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" > {Math.abs(linkDetails?.avgTimeChange || 0)}%

{linkDetails?.avgTime || '0s'}

{/* Conversion Rate */}
Conversion
= 0 ? "text-accent-green" : "text-accent-red"}`}> = 0 ? "text-accent-green" : "text-accent-red transform rotate-180"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" > {Math.abs(linkDetails?.conversionChange || 0)}%

{linkDetails?.conversionRate !== undefined ? `${linkDetails.conversionRate}%` : '0%'}

{/* 图表和详细数据部分 */}
{activeTab === "overview" && (
{/* 日期范围选择器 */}
Analytics Overview
updateDateRange(e.target.value, dateRange.endDate) } className="px-3 py-2 text-sm border rounded-md bg-card-bg border-card-border" />
to
updateDateRange( dateRange.startDate, e.target.value ) } className="px-3 py-2 text-sm border rounded-md bg-card-bg border-card-border" min={dateRange.startDate} />
{/* 设备类型分布 */} {overviewData && (

Device Types

{/* 饼图显示 */}
item.value > 0)} cx="50%" cy="50%" innerRadius={60} outerRadius={90} fill="#8884d8" dataKey="value" nameKey="name" label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`} labelLine={true} > {/* 为每种设备类型设置不同的颜色 */} {[ { key: "mobile", name: 'Mobile', value: overviewData.deviceTypes.mobile, fill: "#3498db" }, { key: "desktop", name: 'Desktop', value: overviewData.deviceTypes.desktop, fill: "#2ecc71" }, { key: "tablet", name: 'Tablet', value: overviewData.deviceTypes.tablet, fill: "#f39c12" }, { key: "other", name: 'Other', value: overviewData.deviceTypes.other, fill: "#e74c3c" } ] .filter(item => item.value > 0) .map(item => ( )) } [`${value} 访问`, '数量']} separator=": " />
Mobile
{overviewData.deviceTypes.mobile}
{overviewData.totalVisits ? Math.round((overviewData.deviceTypes.mobile / overviewData.totalVisits) * 100) : 0}%
Desktop
{overviewData.deviceTypes.desktop}
{overviewData.totalVisits ? Math.round((overviewData.deviceTypes.desktop / overviewData.totalVisits) * 100) : 0}%
Tablet
{overviewData.deviceTypes.tablet}
{overviewData.totalVisits ? Math.round((overviewData.deviceTypes.tablet / overviewData.totalVisits) * 100) : 0}%
Other
{overviewData.deviceTypes.other}
{overviewData.totalVisits ? Math.round((overviewData.deviceTypes.other / overviewData.totalVisits) * 100) : 0}%
)} {/* 转化漏斗 */} {funnelData && (

Conversion Funnel

Overall Conversion Rate:{" "} {(funnelData.conversionRate || 0).toFixed(2)}%
[ `${value} (${props.payload.percent.toFixed(1)}%)`, "Value" ]} /> `${value.toFixed(1)}%`} style={{ fill: "#666", fontSize: 12 }} />
)} {/* 访问趋势 */} {trendsData && (

Visit Trends

{Object.values(TimeGranularity).map( (granularity) => ( ) )}
Total Visits:{" "} {trendsData.totals.visits}
Unique Visitors:{" "} {trendsData.totals.uniqueVisitors}
{/* 图表展示访问趋势 */}
value.toLocaleString()} /> [ value.toLocaleString(), name === "visits" ? "访问量" : "唯一访客" ]} /> value === "visits" ? "访问量" : "唯一访客"} />
)}
)} {activeTab === "performance" && performanceData && (

Link Performance

{/* 添加图表展示 */}
{/* 柱状图展示总点击量和独立访客 */}
[value.toLocaleString(), 'Count']} /> value.toLocaleString()} />
{/* 饼图展示跳出率、转化率等比例数据 */}
`${name}: ${value}%`} labelLine={true} /> [`${value}%`, 'Percentage']} />
Total Clicks
{performanceData.totalClicks}
Unique Visitors
{performanceData.uniqueVisitors}
Bounce Rate
{performanceData.bounceRate}%
Conversion Rate
{performanceData.conversionRate}%
Avg. Time Spent
{formatTime(performanceData.averageTimeSpent)}
Active Days
{performanceData.activeDays}
Unique Referrers
{performanceData.uniqueReferrers}
Last Click
{performanceData.lastClickTime ? new Date( performanceData.lastClickTime ).toLocaleString() : "Never"}
)} {activeTab === "referrers" && referrersData && (

Popular Referrers

{/* 添加图表展示 */}
{/* 条形图展示访问量和独立访客 */}
[ value, name === "visitCount" ? "访问量" : "独立访客" ]} /> `${value}%`} style={{ fill: "#666", fontSize: 12 }} />
{/* 饼图展示来源占比 */}
`${source.substring(0, 10)}...: ${(percent * 100).toFixed(0)}%` } labelLine={false} > {referrersData.referrers.slice(0, 6).map((entry, index) => ( ))} value.length > 20 ? value.substring(0, 20) + '...' : value} />
{referrersData.referrers.map( (referrer: ReferrerItem, i: number) => ( ) )}
Source Visits Unique Visitors Conversion Rate Avg. Time Spent
{referrer.source} {referrer.visitCount} ({referrer.percent}%) {referrer.uniqueVisitors} {referrer.conversionRate}% {formatTime(referrer.averageTimeSpent)}
)} {activeTab === "devices" && deviceData && (

Device Types

{/* 添加设备类型饼图 */}
`${name}: ${(percent * 100).toFixed(0)}%` } > {deviceData.deviceTypes.map((entry, index) => ( ))} [`${value} 次访问`, '访问量']} />
{deviceData.deviceTypes.map( (device: DeviceItem, i: number) => (
{device.name}
{device.count}
{device.percent}%
) )}

Device Brands

{/* 添加设备品牌横向条形图 */}
[`${value} 次访问`, '访问量']} /> `${value}%`} />
{deviceData.deviceBrands.map( (brand: DeviceItem, i: number) => ( ) )}
Brand Count Percentage
{brand.name} {brand.count} {brand.percent}%
)} {activeTab === "locations" && platformData && (

Platform Distribution

{/* 添加图表展示 */}
{/* 操作系统分布饼图 */}

Operating Systems

`${name}: ${(percent * 100).toFixed(0)}%` } labelLine={true} > {platformData.platforms.slice(0, 6).map((entry, index) => ( ))} [`${value} 次访问`, name]} /> value.length > 25 ? value.substring(0, 25) + '...' : value} />
{/* 浏览器分布条形图 */}

Browsers

[`${value} 次访问`, '数量']} /> `${value}%`} />

Operating Systems

{platformData.platforms.map((platform, i) => ( ))}
OS Visits Percentage
{platform.name} {platform.count} {platform.percent}%

Browsers

{platformData.browsers.map((browser, i) => ( ))}
Browser Visits Percentage
{browser.name} {browser.count} {browser.percent}%
)} {activeTab === "qrCodes" && qrCodeData && (

QR Code Analysis

Total Scans
{qrCodeData.overview.totalScans}
Unique Scanners
{qrCodeData.overview.uniqueScanners}
Conversion Rate
{qrCodeData.overview.conversionRate}%
Avg. Time Spent
{formatTime(qrCodeData.overview.averageTimeSpent)}

Scan Locations

{/* 添加扫描位置饼图 */}
`${city}: ${(percent * 100).toFixed(0)}%` } > {qrCodeData.locations.slice(0, 8).map((entry, index) => ( ))} [`${value} 次扫描`, name]} /> { const location = qrCodeData.locations.find(loc => loc.city === value); return location ? `${location.city}, ${location.country}` : value; }} />
{qrCodeData.locations.map((location, i) => ( ))}
Location Scans Percentage
{location.city}, {location.country} {location.scanCount} {location.percent}%

Scan Time Distribution

{/* 添加扫描时间分布柱状图 */}
[`${value} 次扫描`, '扫描次数']} labelFormatter={(hour) => `${hour}:00 - ${hour}:59`} /> `${value.toFixed(1)}%`} />
)}
); }