From a1faacb302421c76199dc0935a0e3dbe049ebe2d Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 02:14:20 +0800 Subject: [PATCH] feat: add scan dashboard components and local development configuration --- .claude/launch.json | 11 ++++++ .claude/settings.local.json | 8 +++++ .../components/dashboard/OpportunityTable.tsx | 15 +++++--- .../dashboard/ScanTerminalDashboard.tsx | 20 ++++++++--- frontend/lib/dashboard-utils.ts | 36 +++++++++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 .claude/launch.json create mode 100644 .claude/settings.local.json diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 00000000..6c59049f --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "frontend-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev", "--", "--hostname", "127.0.0.1", "--port", "3001"], + "port": 3001 + } + ] +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..d3d26911 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm --prefix \"/e/web/PolyWeather/frontend\" run build)", + "mcp__Claude_Preview__preview_start" + ] + } +} diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index 5acbd6f8..79e377e1 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -8,6 +8,11 @@ import type { ScanOpportunityRow, } from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; +import { + formatTemperatureValue, + normalizeTemperatureLabel, + normalizeTemperatureSymbol, +} from "@/lib/dashboard-utils"; type PhaseMeta = { label: string; @@ -113,6 +118,7 @@ function ProbabilityPreview({ Boolean(item && (item.label || item.value != null)), ) : []; + const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol); if (!preview.length) { const targetBase = @@ -121,11 +127,10 @@ function ProbabilityPreview({ row.target_lower ?? row.target_upper ?? null; - const unit = row.target_unit || row.temp_symbol || ""; const targetLabel = targetBase != null - ? `${Math.round(Number(targetBase))}${unit}` - : row.target_label || "--"; + ? formatTemperatureValue(Number(targetBase), tempSymbol) + : normalizeTemperatureLabel(row.target_label, tempSymbol) || "--"; preview.push({ label: targetLabel, model_probability: row.model_event_probability, @@ -141,7 +146,9 @@ function ProbabilityPreview({ key={`${item.label}-${item.value ?? ""}`} className={`scan-distribution-card ${item.highlighted ? "featured" : ""}`} > - {item.label || "--"} + + {normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"} + {locale === "en-US" ? "Model" : "模型"}
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 8b0da846..759694dc 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -43,6 +43,10 @@ import { getLocalizedAirportName, getLocalizedCityName, } from "@/lib/dashboard-home-copy"; +import { + formatTemperatureValue, + normalizeTemperatureLabel, +} from "@/lib/dashboard-utils"; const DEFAULT_FILTERS: FilterState = { scan_mode: "tradable", @@ -222,6 +226,7 @@ function DetailPanel({ detailSignal && detailSignal.market_slug === row.market_slug ? detailSignal : row; + const tempSymbol = row.temp_symbol || displayRow.temp_symbol || "°C"; const yesRow = getSideRow(marketScan, row, "yes"); const noRow = getSideRow(marketScan, row, "no"); const comparisonBuckets = buildComparisonBuckets(marketScan, row); @@ -276,26 +281,33 @@ function DetailPanel({
{isEn ? "Current Temp" : "当前温度"} - {row.current_temp != null ? `${row.current_temp}${row.temp_symbol || ""}` : "--"} + {row.current_temp != null + ? formatTemperatureValue(row.current_temp, tempSymbol) + : "--"}
{isEn ? "Day High (So Far)" : "今日最高(至今)"} {row.current_max_so_far != null - ? `${row.current_max_so_far}${row.temp_symbol || ""}` + ? formatTemperatureValue(row.current_max_so_far, tempSymbol) : "--"}
{isEn ? "Target" : "目标温度"} - {displayRow.target_label || "--"} + + {normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || "--"} +
{isEn ? "Gap To Target" : "距离目标"} {displayRow.gap_to_target != null - ? `${displayRow.gap_to_target >= 0 ? "+" : ""}${displayRow.gap_to_target.toFixed(1)}${row.temp_symbol || ""}` + ? formatTemperatureValue(displayRow.gap_to_target, tempSymbol, { + signed: true, + digits: 1, + }) : "--"}
diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index 8848b95e..535730ce 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -45,6 +45,42 @@ function containsCjk(text: string) { return /[\u3400-\u9fff]/.test(text); } +export function normalizeTemperatureSymbol(value?: string | null) { + return String(value || "").toUpperCase().includes("F") ? "°F" : "°C"; +} + +export function formatTemperatureValue( + value: number | null | undefined, + symbol?: string | null, + options?: { signed?: boolean; digits?: number }, +) { + const numeric = Number(value); + if (!Number.isFinite(numeric)) return "--"; + const digits = options?.digits ?? 0; + const sign = options?.signed && numeric >= 0 ? "+" : ""; + return `${sign}${numeric.toFixed(digits)}${normalizeTemperatureSymbol(symbol)}`; +} + +export function normalizeTemperatureLabel( + label?: string | null, + fallbackSymbol?: string | null, +) { + const raw = String(label || "").trim(); + if (!raw) return ""; + const normalizedSymbol = normalizeTemperatureSymbol(fallbackSymbol); + let next = raw.replace(/℃/gi, "°C"); + next = next.replace(/°?([CF])\b/gi, (_, unit: string) => `°${unit.toUpperCase()}`); + if (!/[°][CF]/.test(next) && /\d/.test(next)) { + next = `${next}${normalizedSymbol}`; + } + if (normalizedSymbol === "°F") { + next = next.replace(/°C/g, "°F"); + } else { + next = next.replace(/°F/g, "°C"); + } + return next; +} + function getLocalizedDynamicCommentary( detail: CityDetail, locale: Locale,