From b0dbd088e7ef42b89496758007a43ee34a0ea09e Mon Sep 17 00:00:00 2001 From: William Tso Date: Wed, 2 Apr 2025 17:56:15 +0800 Subject: [PATCH] rm no used page --- app/(app)/geo-analytics/page.tsx | 1 - app/(app)/links/[id]/page.tsx | 1688 ------------------------------ app/(app)/links/page.tsx | 574 ---------- 3 files changed, 2263 deletions(-) delete mode 100644 app/(app)/geo-analytics/page.tsx delete mode 100644 app/(app)/links/[id]/page.tsx delete mode 100644 app/(app)/links/page.tsx diff --git a/app/(app)/geo-analytics/page.tsx b/app/(app)/geo-analytics/page.tsx deleted file mode 100644 index 0519ecb..0000000 --- a/app/(app)/geo-analytics/page.tsx +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/(app)/links/[id]/page.tsx b/app/(app)/links/[id]/page.tsx deleted file mode 100644 index 1dbfb63..0000000 --- a/app/(app)/links/[id]/page.tsx +++ /dev/null @@ -1,1688 +0,0 @@ -"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(""); - - 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} - - -
-
- -
- - Original URL - - -
-
-
- -
-
-
- - 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)}%`} - /> - - - -
-
- -
-
-
-
- )} -
-
-
-
- ); -} diff --git a/app/(app)/links/page.tsx b/app/(app)/links/page.tsx deleted file mode 100644 index 2e2c6e8..0000000 --- a/app/(app)/links/page.tsx +++ /dev/null @@ -1,574 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useRef } from 'react'; -import CreateLinkModal from '@/app/components/ui/CreateLinkModal'; - -// 自定义类型定义,替换原来的导入 -interface Link { - link_id: string; - title?: string; - original_url: string; - visits: number; - unique_visits: number; - created_by: string; - created_at: string; - is_active: boolean; - tags?: string[]; -} - -interface StatsOverview { - totalLinks: number; - activeLinks: number; - totalVisits: number; - conversionRate: number; -} - -interface Tag { - tag: string; - id: string; - name: string; - count: number; -} - -// Define type for link data -interface LinkData { - name: string; - originalUrl: string; - customSlug: string; - expiresAt: string; - tags: string[]; -} - -// 映射API数据到UI所需格式 -interface UILink { - 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: string; - tags: string[]; -} - -export default function LinksPage() { - const [links, setLinks] = useState([]); - const [allTags, setAllTags] = useState([]); - const [stats, setStats] = useState({ - totalLinks: 0, - activeLinks: 0, - totalVisits: 0, - conversionRate: 0 - }); - - const [searchQuery, setSearchQuery] = useState(''); - const [showCreateModal, setShowCreateModal] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // 无限加载相关状态 - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const observer = useRef(null); - const lastLinkElementRef = useRef(null); - - // 映射API数据到UI所需格式的函数 - const mapApiLinkToUiLink = (apiLink: Link): UILink => { - // 生成短URL显示 - 因为数据库中没有short_url字段 - const shortUrlDisplay = generateShortUrlDisplay(apiLink.link_id, apiLink.original_url); - - return { - id: apiLink.link_id, - name: apiLink.title || 'Untitled Link', - shortUrl: shortUrlDisplay, - originalUrl: apiLink.original_url, - creator: apiLink.created_by, - createdAt: new Date(apiLink.created_at).toLocaleDateString(), - visits: apiLink.visits, - visitChange: 0, // API doesn't provide change data yet - uniqueVisitors: apiLink.unique_visits, - uniqueVisitorsChange: 0, - avgTime: '0m 0s', // API doesn't provide average time yet - avgTimeChange: 0, - conversionRate: 0, // API doesn't provide conversion rate yet - conversionChange: 0, - status: apiLink.is_active ? 'active' : 'inactive', - tags: apiLink.tags || [] - }; - }; - - // 从link_id和原始URL生成短URL显示 - const generateShortUrlDisplay = (linkId: string, originalUrl: string): string => { - try { - // 尝试从原始URL提取域名 - const urlObj = new URL(originalUrl); - const domain = urlObj.hostname.replace('www.', ''); - - // 使用link_id的前8个字符作为短代码 - const shortCode = linkId.substring(0, 8); - - return `${domain}/${shortCode}`; - } catch { - // 如果URL解析失败,返回一个基于linkId的默认值 - return `short.link/${linkId.substring(0, 8)}`; - } - }; - - // 获取链接数据 - const fetchLinks = useCallback(async (pageNum: number, isInitialLoad: boolean = false) => { - try { - if (isInitialLoad) { - setIsLoading(true); - } else { - setIsLoadingMore(true); - } - setError(null); - - // 获取链接列表 - const linksResponse = await fetch(`/api/links?page=${pageNum}&limit=20${searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : ''}`); - if (!linksResponse.ok) { - throw new Error(`Failed to fetch links: ${linksResponse.statusText}`); - } - const linksData = await linksResponse.json(); - - const uiLinks = linksData.data.map(mapApiLinkToUiLink); - - if (isInitialLoad) { - setLinks(uiLinks); - } else { - setLinks(prevLinks => [...prevLinks, ...uiLinks]); - } - - // 检查是否还有更多数据可加载 - const { pagination } = linksData; - setHasMore(pagination.page < pagination.totalPages); - - if (isInitialLoad) { - // 只在初始加载时获取标签和统计数据 - const tagsResponse = await fetch('/api/tags'); - if (!tagsResponse.ok) { - throw new Error(`Failed to fetch tags: ${tagsResponse.statusText}`); - } - const tagsData = await tagsResponse.json(); - - const statsResponse = await fetch('/api/stats'); - if (!statsResponse.ok) { - throw new Error(`Failed to fetch stats: ${statsResponse.statusText}`); - } - const statsData = await statsResponse.json(); - - setAllTags(tagsData); - setStats(statsData); - } - - } catch (err) { - console.error('Data loading failed:', err); - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - if (isInitialLoad) { - setIsLoading(false); - } else { - setIsLoadingMore(false); - } - } - }, [searchQuery]); - - // 初始加载 - useEffect(() => { - setPage(1); - fetchLinks(1, true); - }, [fetchLinks]); - - // 搜索过滤变化时重新加载数据 - useEffect(() => { - // 当搜索关键词变化时,重置页码和链接列表,然后重新获取数据 - setLinks([]); - setPage(1); - fetchLinks(1, true); - }, [searchQuery, fetchLinks]); - - // 设置Intersection Observer来检测滚动并加载更多数据 - useEffect(() => { - // 如果正在加载或没有更多数据,则不设置observer - if (isLoading || isLoadingMore || !hasMore) return; - - // 断开之前的observer连接 - if (observer.current) { - observer.current.disconnect(); - } - - observer.current = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && hasMore) { - // 当最后一个元素可见且有更多数据时,加载下一页 - setPage(prevPage => prevPage + 1); - } - }, { - root: null, - rootMargin: '0px', - threshold: 0.5 - }); - - if (lastLinkElementRef.current) { - observer.current.observe(lastLinkElementRef.current); - } - - return () => { - if (observer.current) { - observer.current.disconnect(); - } - }; - }, [isLoading, isLoadingMore, hasMore, links]); - - // 当页码变化时加载更多数据 - useEffect(() => { - if (page > 1) { - fetchLinks(page, false); - } - }, [page, fetchLinks]); - - const filteredLinks = links.filter(link => - link.name.toLowerCase().includes(searchQuery.toLowerCase()) || - link.shortUrl.toLowerCase().includes(searchQuery.toLowerCase()) || - link.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) - ); - - const handleOpenLinkDetails = (id: string) => { - window.location.href = `/links/${id}`; - }; - - const handleCreateLink = async (linkData: LinkData) => { - try { - setIsLoading(true); - // 在实际应用中,这里会发送 POST 请求到 API - console.log('创建链接:', linkData); - - // 刷新链接列表 - setPage(1); - fetchLinks(1, true); - - setShowCreateModal(false); - } catch (err) { - console.error('创建链接失败:', err); - setError(err instanceof Error ? err.message : '未知错误'); - } finally { - setIsLoading(false); - } - }; - - // 加载状态 - if (isLoading && links.length === 0) { - return ( -
-
-
-

Loading data...

-
-
- ); - } - - // 错误状态 - if (error && links.length === 0) { - return ( -
-
- - - -

Loading Failed

-

{error}

- -
-
- ); - } - - return ( -
-
- {/* Header */} -
-
-

Link Management

-

- View and manage all your shortened links -

-
- -
-
-
- - - -
- setSearchQuery(e.target.value)} - /> -
- - -
-
- - {/* Stats Summary */} -
-
-
-
- - - - -
-
-

Total Links

-

{stats.totalLinks}

-
-
-
- -
-
-
- - - -
-
-

Active Links

-

{stats.activeLinks}

-
-
-
- -
-
-
- - - - -
-
-

Total Visits

-

{stats.totalVisits.toLocaleString()}

-
-
-
- -
-
-
- - - -
-
-

Conversion Rate

-

{(stats.conversionRate * 100).toFixed(1)}%

-
-
-
-
- - {/* Links Table */} -
-
- - - - - - - - - - - - - - {filteredLinks.length === 0 ? ( - - - - ) : ( - filteredLinks.map((link, index) => ( - handleOpenLinkDetails(link.id)} - className="transition-colors cursor-pointer hover:bg-card-bg-secondary" - ref={index === filteredLinks.length - 1 ? lastLinkElementRef : null} - > - - - - - - - - - )) - )} - -
Link InfoVisitsUnique VisitorsAvg TimeConversionStatus - Actions -
- No links found. Create one to get started. -
-
{link.name}
-
{link.shortUrl}
-
-
{link.visits.toLocaleString()}
-
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> - = 0 ? '' : 'transform rotate-180'}`} - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - - - {Math.abs(link.visitChange)}% -
-
-
{link.uniqueVisitors.toLocaleString()}
-
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> - = 0 ? '' : 'transform rotate-180'}`} - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - - - {Math.abs(link.uniqueVisitorsChange)}% -
-
-
{link.avgTime}
-
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> - = 0 ? '' : 'transform rotate-180'}`} - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - - - {Math.abs(link.avgTimeChange)}% -
-
-
{link.conversionRate}%
-
= 0 ? 'text-accent-green' : 'text-accent-red'}`}> - = 0 ? '' : 'transform rotate-180'}`} - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - - - {Math.abs(link.conversionChange)}% -
-
- - {link.status === 'active' ? 'Active' : link.status === 'inactive' ? 'Inactive' : 'Expired'} - - - -
-
- - {/* Loading more indicator */} - {isLoadingMore && ( -
-
-

Loading more links...

-
- )} - - {/* End of results message */} - {!hasMore && links.length > 0 && ( -
- No more links to load. -
- )} -
- - {/* Tags Section */} - {allTags.length > 0 && ( -
-

Tags

-
- {allTags.map(tagItem => ( - setSearchQuery(tagItem.tag)} - style={{ cursor: 'pointer' }} - > - {tagItem.tag} - - {tagItem.count} - - - ))} -
-
- )} -
- - {/* Create Link Modal */} - {showCreateModal && ( - setShowCreateModal(false)} - onSubmit={handleCreateLink} - /> - )} -
- ); -} \ No newline at end of file