From 69f24ddc083f1187d1f85f7afd51ea0d6540d507 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 18 Mar 2026 20:38:43 +0800 Subject: [PATCH] Optimize frontend performance and add route-level web vitals tracking --- ankara_debug.json | 0 ankara_test.json | 0 frontend/app/account/page.tsx | 5 +- frontend/app/api/vitals/route.ts | 73 ++ frontend/app/layout.tsx | 6 +- frontend/components/account/AccountCenter.tsx | 140 ++- frontend/components/account/AccountEntry.tsx | 29 + frontend/components/dashboard/CitySidebar.tsx | 33 +- frontend/components/dashboard/DetailPanel.tsx | 59 +- .../dashboard/FutureForecastModal.tsx | 10 +- .../components/dashboard/HistoryModal.tsx | 2 +- frontend/components/dashboard/MapCanvas.tsx | 2 + .../components/dashboard/PanelSections.tsx | 16 +- .../observability/WebVitalsReporter.tsx | 43 + frontend/hooks/useChart.ts | 25 +- frontend/hooks/useLeafletMap.ts | 110 +- frontend/lib/vitals-store.ts | 88 ++ src/payments/contract_checkout.py.bak | 1118 ----------------- tmp.json | 0 tmp2.json | 0 tmp_fix_legacy.js | 130 -- 21 files changed, 503 insertions(+), 1386 deletions(-) delete mode 100644 ankara_debug.json delete mode 100644 ankara_test.json create mode 100644 frontend/app/api/vitals/route.ts create mode 100644 frontend/components/account/AccountEntry.tsx create mode 100644 frontend/components/observability/WebVitalsReporter.tsx create mode 100644 frontend/lib/vitals-store.ts delete mode 100644 src/payments/contract_checkout.py.bak delete mode 100644 tmp.json delete mode 100644 tmp2.json delete mode 100644 tmp_fix_legacy.js diff --git a/ankara_debug.json b/ankara_debug.json deleted file mode 100644 index e69de29b..00000000 diff --git a/ankara_test.json b/ankara_test.json deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/app/account/page.tsx b/frontend/app/account/page.tsx index 1a35dc17..8a9abd4f 100644 --- a/frontend/app/account/page.tsx +++ b/frontend/app/account/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next"; import { I18nProvider } from "@/hooks/useI18n"; -import { AccountCenter } from "@/components/account/AccountCenter"; +import { AccountEntry } from "@/components/account/AccountEntry"; export const metadata: Metadata = { title: "PolyWeather | Account Center", @@ -10,8 +10,7 @@ export const metadata: Metadata = { export default function AccountPage() { return ( - + ); } - diff --git a/frontend/app/api/vitals/route.ts b/frontend/app/api/vitals/route.ts new file mode 100644 index 00000000..95f4f284 --- /dev/null +++ b/frontend/app/api/vitals/route.ts @@ -0,0 +1,73 @@ +import { NextResponse } from "next/server"; +import { + getVitalsSummary, + normalizeMetricName, + recordVitalsSample, +} from "@/lib/vitals-store"; + +type VitalsPayload = { + id?: string; + metric?: string; + navigationType?: string; + pathname?: string; + rating?: string; + value?: number; +}; + +export async function POST(request: Request) { + try { + const payload = (await request.json()) as VitalsPayload; + const metric = normalizeMetricName(payload.metric); + if (!metric) { + return NextResponse.json({ ok: false, error: "metric is required" }, { status: 400 }); + } + + const pathname = String(payload.pathname || "/"); + const rating = String(payload.rating || "unknown"); + const value = Number(payload.value); + const navigationType = String(payload.navigationType || "unknown"); + const id = String(payload.id || ""); + const ts = Date.now(); + + if (!Number.isFinite(value)) { + return NextResponse.json({ ok: false, error: "value must be finite" }, { status: 400 }); + } + + recordVitalsSample({ + id, + metric, + navigationType, + pathname, + rating, + timestamp: ts, + value, + }); + + // Keep this lightweight: log for now, can be wired to a persistent sink later. + console.info( + `[vitals] metric=${metric} path=${pathname} value=${Number.isFinite(value) ? value : "NaN"} rating=${rating} nav=${navigationType} id=${id}`, + ); + + return NextResponse.json({ ok: true }); + } catch (error) { + console.warn("[vitals] failed to parse payload", error); + return NextResponse.json({ ok: false }, { status: 400 }); + } +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const targetRoute = String(searchParams.get("route") || "").trim(); + const summary = getVitalsSummary(); + + if (!targetRoute) { + return NextResponse.json({ ok: true, ...summary }); + } + + return NextResponse.json({ + ok: true, + generatedAt: summary.generatedAt, + route: targetRoute, + metrics: summary.routes[targetRoute] || {}, + }); +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 7cf46ffa..26a2a493 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Analytics } from "@vercel/analytics/react"; import { SpeedInsights } from "@vercel/speed-insights/next"; +import { WebVitalsReporter } from "@/components/observability/WebVitalsReporter"; import "./globals.css"; export const metadata: Metadata = { @@ -25,10 +26,6 @@ export default function RootLayout({ return ( - {children} + diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 798442c3..79f39ae2 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -612,10 +612,42 @@ export function AccountCenter() { const [paymentError, setPaymentError] = useState(""); const [lastIntentId, setLastIntentId] = useState(""); const [lastTxHash, setLastTxHash] = useState(""); + const [showSecondarySections, setShowSecondarySections] = useState(false); const supabaseReady = hasSupabasePublicEnv(); const walletConnectEnabled = Boolean(WALLETCONNECT_PROJECT_ID); + useEffect(() => { + let canceled = false; + let timeoutId: number | null = null; + let idleId: number | null = null; + const win = typeof window !== "undefined" ? (window as any) : null; + + const reveal = () => { + if (!canceled) { + setShowSecondarySections(true); + } + }; + + if (win && typeof win.requestIdleCallback === "function") { + idleId = win.requestIdleCallback(reveal, { timeout: 320 }); + } else if (typeof window !== "undefined") { + timeoutId = window.setTimeout(reveal, 140); + } else { + setShowSecondarySections(true); + } + + return () => { + canceled = true; + if (win && idleId != null && typeof win.cancelIdleCallback === "function") { + win.cancelIdleCallback(idleId); + } + if (timeoutId != null && typeof window !== "undefined") { + window.clearTimeout(timeoutId); + } + }; + }, []); + /** * Returns a valid access token, refreshing the session if the stored one * is missing or close to expiry. Throws if the user is not authenticated. @@ -1826,55 +1858,66 @@ export function AccountCenter() { {/* Weekly Ranking Motivation */} -
-
-

- {" "} - {copy.weeklyRewards} -

-
-
- -
- 1 -
{" "} - Top 1 -
- - +500 积分 & 7天Pro - -
-
- -
- 2 -
{" "} - Top 2-3 -
- - +300 积分 & 3天Pro - -
-
- -
- 4 -
{" "} - Top 4-10 -
- - +150 积分 - + {showSecondarySections ? ( +
+
+

+ {" "} + {copy.weeklyRewards} +

+
+
+ +
+ 1 +
{" "} + Top 1 +
+ + +500 积分 & 7天Pro + +
+
+ +
+ 2 +
{" "} + Top 2-3 +
+ + +300 积分 & 3天Pro + +
+
+ +
+ 4 +
{" "} + Top 4-10 +
+ + +150 积分 + +
+
+ +

+ 积分规则:群内有效发言(自动防刷检测)。每周一零点结算并重置周积分榜。 +

+
-
- -

- 积分规则:群内有效发言(自动防刷检测)。每周一零点结算并重置周积分榜。 -

+ ) : ( +
+
+
+
+
+
+
-
+ )} {/* Subscription Info & Paywall */}
@@ -1966,6 +2009,7 @@ export function AccountCenter() {
{/* Telegram Bot Section */} + {showSecondarySections ? (
+ ) : ( +
+
+
+
+ )}