From be41becb3019c4ee51763dc801aa9979fc2ef425 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 28 Apr 2026 11:32:03 +0800 Subject: [PATCH] Let temperature formatting ship without dashboard bulk Temperature formatting was embedded in the large dashboard utility module, so small scan-terminal views had to import the heavy utility surface for simple labels. This moves the pure temperature helpers into a lightweight module while keeping dashboard-utils re-exports for compatibility. Constraint: Preserve existing temperature text output and all dashboard-utils import compatibility. Rejected: Split chart, pace, and model helpers in the same pass | those helpers have wider coupling and should move one boundary at a time. Confidence: high Scope-risk: narrow Reversibility: clean Tested: TypeScript diagnostics for temperature-utils, dashboard-utils, and OpportunityTable Tested: npm run build Not-tested: Bundle analyzer size comparison. --- .../components/dashboard/OpportunityTable.tsx | 6 ++- .../scan-terminal/AiForecastKPIBar.tsx | 3 +- .../scan-terminal/AiPinnedCityCard.tsx | 3 +- .../dashboard/scan-terminal/CalendarView.tsx | 2 +- .../scan-terminal/OpportunityOverview.tsx | 2 +- .../scan-terminal/city-card-decision-utils.ts | 4 +- frontend/lib/dashboard-utils.ts | 47 +++++-------------- frontend/lib/temperature-utils.ts | 35 ++++++++++++++ 8 files changed, 58 insertions(+), 44 deletions(-) create mode 100644 frontend/lib/temperature-utils.ts diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index fa59b497..62f3461e 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -9,13 +9,15 @@ import type { } from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; import { - formatTemperatureValue, getModelView, getProbabilityView, getTodayPaceView, +} from "@/lib/dashboard-utils"; +import { + formatTemperatureValue, normalizeTemperatureLabel, normalizeTemperatureSymbol, -} from "@/lib/dashboard-utils"; +} from "@/lib/temperature-utils"; type PhaseMeta = { label: string; diff --git a/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx b/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx index 95ff8091..1ff8ca4a 100644 --- a/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx +++ b/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx @@ -1,5 +1,6 @@ import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types"; -import { formatTemperatureValue, getTodayPaceView } from "@/lib/dashboard-utils"; +import { getTodayPaceView } from "@/lib/dashboard-utils"; +import { formatTemperatureValue } from "@/lib/temperature-utils"; import { getPeakWindowLabel } from "@/components/dashboard/scan-terminal/decision-utils"; export function AiForecastKPIBar({ diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx index 624a0a98..cc60865a 100644 --- a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx +++ b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx @@ -21,7 +21,8 @@ import { useCityMarketScan, } from "@/components/dashboard/scan-terminal/use-ai-city-card-data"; import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types"; -import { formatTemperatureValue, getModelView, getTodayPaceView } from "@/lib/dashboard-utils"; +import { getModelView, getTodayPaceView } from "@/lib/dashboard-utils"; +import { formatTemperatureValue } from "@/lib/temperature-utils"; function toFiniteDecisionNumber(value: unknown) { if (value == null || value === "") return null; diff --git a/frontend/components/dashboard/scan-terminal/CalendarView.tsx b/frontend/components/dashboard/scan-terminal/CalendarView.tsx index af76e189..df6a0ebf 100644 --- a/frontend/components/dashboard/scan-terminal/CalendarView.tsx +++ b/frontend/components/dashboard/scan-terminal/CalendarView.tsx @@ -2,7 +2,7 @@ import { memo, useEffect, useMemo, useState } from "react"; import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable"; import type { ScanOpportunityRow } from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; -import { formatTemperatureValue } from "@/lib/dashboard-utils"; +import { formatTemperatureValue } from "@/lib/temperature-utils"; import { formatShortDate, getPeakCountdownMeta, diff --git a/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx b/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx index 3e5b567f..65d824b6 100644 --- a/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx +++ b/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx @@ -1,7 +1,7 @@ import { memo, useMemo } from "react"; import clsx from "clsx"; import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types"; -import { formatTemperatureValue } from "@/lib/dashboard-utils"; +import { formatTemperatureValue } from "@/lib/temperature-utils"; import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import { formatRowPrice, diff --git a/frontend/components/dashboard/scan-terminal/city-card-decision-utils.ts b/frontend/components/dashboard/scan-terminal/city-card-decision-utils.ts index 77440a4a..7252f972 100644 --- a/frontend/components/dashboard/scan-terminal/city-card-decision-utils.ts +++ b/frontend/components/dashboard/scan-terminal/city-card-decision-utils.ts @@ -1,9 +1,9 @@ import type { MarketScan, MarketTopBucket } from "@/lib/dashboard-types"; +import { getTodayPaceView } from "@/lib/dashboard-utils"; import { formatTemperatureValue, - getTodayPaceView, normalizeTemperatureLabel, -} from "@/lib/dashboard-utils"; +} from "@/lib/temperature-utils"; import type { AiCityForecastPayload } from "@/components/dashboard/scan-terminal/types"; export type WeatherDecisionView = { diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index b728eca9..4afaec67 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -6,6 +6,17 @@ import { NearbyStation, } from "@/lib/dashboard-types"; import { normalizeObservationSourceLabel } from "@/lib/source-labels"; +import { + formatTemperatureValue, + normalizeTemperatureLabel, + normalizeTemperatureSymbol, +} from "@/lib/temperature-utils"; + +export { + formatTemperatureValue, + normalizeTemperatureLabel, + normalizeTemperatureSymbol, +} from "@/lib/temperature-utils"; const METAR_WX_MAP: Record< string, @@ -45,42 +56,6 @@ 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, diff --git a/frontend/lib/temperature-utils.ts b/frontend/lib/temperature-utils.ts new file mode 100644 index 00000000..43536f24 --- /dev/null +++ b/frontend/lib/temperature-utils.ts @@ -0,0 +1,35 @@ +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; +}