From f41a6b6e5b72d6902f0ddf82cb3994cb1a8bd319 Mon Sep 17 00:00:00 2001 From: William Tso Date: Mon, 24 Mar 2025 18:25:05 +0800 Subject: [PATCH] chart --- app/links/[id]/page.tsx | 684 ++++++++++++++++----- windmill/sync_shorturl_event_from_mongo.ts | 19 +- 2 files changed, 549 insertions(+), 154 deletions(-) diff --git a/app/links/[id]/page.tsx b/app/links/[id]/page.tsx index 4165a8b..f1b8222 100644 --- a/app/links/[id]/page.tsx +++ b/app/links/[id]/page.tsx @@ -16,6 +16,11 @@ import { 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; @@ -206,7 +211,28 @@ export default function LinkDetailsPage({ } const details = await response.json(); - setLinkDetails(details); + 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); // 获取分析数据 @@ -689,77 +715,78 @@ export default function LinkDetailsPage({

Device Types

-
-
-
- Mobile -
-
- {overviewData.deviceTypes.mobile} -
-
- {overviewData.totalVisits - ? Math.round( - (overviewData.deviceTypes.mobile / - overviewData.totalVisits) * - 100 - ) - : 0} - % -
+ + {/* 饼图显示 */} +
+
+ + + 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=": " + /> + + +
-
-
- Desktop +
+
+
Mobile
+
{overviewData.deviceTypes.mobile}
+
+ {overviewData.totalVisits ? Math.round((overviewData.deviceTypes.mobile / overviewData.totalVisits) * 100) : 0}% +
-
- {overviewData.deviceTypes.desktop} +
+
Desktop
+
{overviewData.deviceTypes.desktop}
+
+ {overviewData.totalVisits ? Math.round((overviewData.deviceTypes.desktop / overviewData.totalVisits) * 100) : 0}% +
-
- {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}% +
-
-
-
- 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} - % +
+
Other
+
{overviewData.deviceTypes.other}
+
+ {overviewData.totalVisits ? Math.round((overviewData.deviceTypes.other / overviewData.totalVisits) * 100) : 0}% +
@@ -781,26 +808,41 @@ export default function LinkDetailsPage({
-
- {funnelData.steps.map((step) => ( -
-
- - {step.name} - - - {step.value} ({(step.percent || 0).toFixed(1)} - %) - -
-
-
-
-
- ))} +
+ + + + + + [ + `${value} (${props.payload.percent.toFixed(1)}%)`, + "Value" + ]} + /> + + `${value.toFixed(1)}%`} + style={{ fill: "#666", fontSize: 12 }} + /> + + +
)} @@ -850,38 +892,51 @@ export default function LinkDetailsPage({
- {/* 简单趋势表格 */} -
- - - - - - - - - - {trendsData.trends.map((trend, i) => ( - - - - - - ))} - -
- Time - - Visits - - Unique Visitors -
- {trend.timestamp} - - {trend.visits} - - {trend.uniqueVisitors} -
+ {/* 图表展示访问趋势 */} +
+ + + + + value.toLocaleString()} + /> + [ + value.toLocaleString(), + name === "visits" ? "访问量" : "唯一访客" + ]} + /> + value === "visits" ? "访问量" : "唯一访客"} + /> + + + +
@@ -895,6 +950,63 @@ export default function LinkDetailsPage({

Link Performance

+ + {/* 添加图表展示 */} +
+
+ {/* 柱状图展示总点击量和独立访客 */} +
+ + + + + + [value.toLocaleString(), 'Count']} /> + + + value.toLocaleString()} /> + + + +
+ + {/* 饼图展示跳出率、转化率等比例数据 */} +
+ + + `${name}: ${value}%`} + labelLine={true} + /> + [`${value}%`, 'Percentage']} /> + + + +
+
+
+
@@ -975,6 +1087,89 @@ export default function LinkDetailsPage({

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} + /> + + +
+
+
+
@@ -1031,6 +1226,36 @@ export default function LinkDetailsPage({

Device Types

+ + {/* 添加设备类型饼图 */} +
+
+ + + + `${name}: ${(percent * 100).toFixed(0)}%` + } + > + {deviceData.deviceTypes.map((entry, index) => ( + + ))} + + [`${value} 次访问`, '访问量']} /> + + + +
+
+
{deviceData.deviceTypes.map( (device: DeviceItem, i: number) => ( @@ -1054,6 +1279,44 @@ export default function LinkDetailsPage({

Device Brands

+ + {/* 添加设备品牌横向条形图 */} +
+
+ + + + + + [`${value} 次访问`, '访问量']} + /> + + `${value}%`} + /> + + + +
+
+
@@ -1098,6 +1361,81 @@ export default function LinkDetailsPage({

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}%`} + /> + + + +
+
+
+

@@ -1225,6 +1563,42 @@ export default function LinkDetailsPage({

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; + }} + /> + + +
+
+
@@ -1263,40 +1637,46 @@ export default function LinkDetailsPage({

Scan Time Distribution

-
-
-
- - - - - - - - - {qrCodeData.hourlyDistribution.map((hour) => ( - - - - - - ))} - -
- Hour - - Scans - - Percentage -
- {hour.hour}:00 - {hour.hour}:59 - - {hour.scanCount} - - {hour.percent.toFixed(1)}% -
+ + {/* 添加扫描时间分布柱状图 */} +
+
+ + + + + + [`${value} 次扫描`, '扫描次数']} + labelFormatter={(hour) => `${hour}:00 - ${hour}:59`} + /> + + `${value.toFixed(1)}%`} + /> + + +
+ +
+
)} diff --git a/windmill/sync_shorturl_event_from_mongo.ts b/windmill/sync_shorturl_event_from_mongo.ts index 3618310..67bbfb9 100644 --- a/windmill/sync_shorturl_event_from_mongo.ts +++ b/windmill/sync_shorturl_event_from_mongo.ts @@ -479,8 +479,23 @@ export async function main( .toArray(); if (records.length === 0) { - logWithTimestamp(`第 ${page+1} 批次没有找到数据,结束处理`); - break; + logWithTimestamp(`第 ${page+1} 批次没有找到数据,但将继续尝试后续批次...`); + + // 更新查询条件,以确保能找到后续数据 + // 更新查询时间,增加一定的时间窗口以尝试找到后续数据 + const timeGap = 3600 * 1000; // 1小时的毫秒数 + lastCreateTime += timeGap; + + query.createTime = { $gt: lastCreateTime }; + if (lastId) { + // 移除ID条件,因为我们已经跳过了时间 + delete query._id; + } + + logWithTimestamp(`调整查询条件向前搜索: 创建时间 > ${new Date(lastCreateTime).toISOString()}`); + + // 继续下一批次,不中断循环 + continue; } logWithTimestamp(`获取到 ${records.length} 条记录,开始处理...`);