清理旧 FutureForecast 组件链和 scan-root-styles 死引用

This commit is contained in:
2569718930@qq.com
2026-05-25 05:28:36 +08:00
parent d808d89fdd
commit 945c2c4e20
12 changed files with 0 additions and 4014 deletions
@@ -1,80 +0,0 @@
"use client";
import type { useDashboardStore } from "@/hooks/useDashboardStore";
import type { useI18n } from "@/hooks/useI18n";
import type { getFutureModalView } from "@/lib/dashboard-utils";
import {
FutureModelForecastPanel,
FutureProbabilityPanel,
FutureTemperaturePathChart,
} from "./FutureForecastModalPanels";
type DashboardDetail = NonNullable<
ReturnType<typeof useDashboardStore>["selectedDetail"]
>;
type FutureModalView = ReturnType<typeof getFutureModalView>;
type TranslationFn = ReturnType<typeof useI18n>["t"];
export function FutureForecastForwardView({
dateStr,
detail,
t,
view,
}: {
dateStr: string;
detail: DashboardDetail;
t: TranslationFn;
view: FutureModalView;
}) {
return (
<>
<div className="future-forward-stats">
<div className="future-forward-stat-card">
<span className="label">{t("future.targetForecast")}</span>
<span className="val">
{view.forecastEntry?.max_temp ?? "--"}
{detail.temp_symbol}
</span>
</div>
<div className="future-forward-stat-card">
<span className="label">{t("future.deb")}</span>
<span className="val">
{view.deb ?? "--"}
{detail.temp_symbol}
</span>
</div>
<div className="future-forward-stat-card">
<span className="label">{t("future.mu")}</span>
<span className="val">
{view.mu != null
? `${view.mu.toFixed(1)}${detail.temp_symbol}`
: "--"}
</span>
</div>
<div className="future-forward-stat-card">
<span className="label">{t("future.score")}</span>
<span className="val">
{view.front.score > 0 ? "+" : ""}
{view.front.score}
</span>
</div>
</div>
<section className="future-modal-section">
<h3>{t("future.targetTempTrend")}</h3>
<FutureTemperaturePathChart dateStr={dateStr} forceToday={false} />
</section>
<div className="future-modal-grid">
<section className="future-modal-section">
<h3>{t("future.probability")}</h3>
<FutureProbabilityPanel detail={detail} targetDate={dateStr} hideTitle />
</section>
<section className="future-modal-section">
<h3>{t("future.models")}</h3>
<FutureModelForecastPanel detail={detail} targetDate={dateStr} hideTitle />
</section>
</div>
</>
);
}
File diff suppressed because it is too large Load Diff
@@ -1,31 +0,0 @@
"use client";
import {
useDashboardModal,
useDashboardSelection,
useProAccess,
} from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
import { FutureForecastModalContent } from "./FutureForecastModalContent";
export function FutureForecastModal() {
const modal = useDashboardModal();
const selection = useDashboardSelection();
const proAccess = useProAccess();
const { locale, t } = useI18n();
const detail = selection.selectedDetail;
const dateStr = modal.futureModalDate;
if (!detail || !dateStr) return null;
return (
<FutureForecastModalContent
modal={modal}
proAccess={proAccess.proAccess}
locale={locale}
t={t}
detail={detail}
dateStr={dateStr}
/>
);
}
@@ -1,252 +0,0 @@
import type { IntradayMeteorologySignal } from "@/lib/dashboard-types";
export const TODAY_MARKET_SCAN_AUTO_REFRESH_MS = 5 * 60 * 1000;
export function normalizeMarketValue(value?: number | null) {
if (value == null) return null;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
return Math.max(0, Math.min(1, numeric));
}
export function formatMinuteAxisLabel(value: number) {
if (!Number.isFinite(value)) return "";
const total = Math.max(0, Math.round(value));
const hour = Math.floor(total / 60) % 24;
const minute = total % 60;
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
}
export function formatMarketPercent(value?: number | null) {
const normalized = normalizeMarketValue(value);
if (normalized == null) return "--";
return `${(normalized * 100).toFixed(1)}%`;
}
export function formatBucketLabel(
bucket?: {
label?: string | null;
bucket?: string | null;
range?: string | null;
value?: number | null;
temp?: number | null;
} | null,
) {
if (!bucket) return "--";
const direct =
String(bucket.label || "").trim() ||
String(bucket.bucket || "").trim() ||
String(bucket.range || "").trim();
if (direct) {
let str = direct.toUpperCase().replace(/\s+/g, "");
str = str.replace(/°?C($|\+|-)/g, "℃$1");
if (!str.includes("℃") && /[0-9]/.test(str)) {
str += "℃";
}
return str;
}
const temp = Number(bucket.value ?? bucket.temp);
if (Number.isFinite(temp)) {
return `${Math.round(temp)}`;
}
return "--";
}
export function parseBucketBoundaries(
bucket?: {
label?: string | null;
bucket?: string | null;
range?: string | null;
value?: number | null;
temp?: number | null;
} | null,
) {
if (!bucket) return null;
const raw =
String(bucket.label || "").trim() ||
String(bucket.bucket || "").trim() ||
String(bucket.range || "").trim();
if (!raw) return null;
const numbers = Array.from(raw.matchAll(/-?\d+(?:\.\d+)?/g)).map((match) =>
Number(match[0]),
);
if (!numbers.length) return null;
if (raw.includes("+")) {
return {
lower: numbers[0] ?? null,
upper: null as number | null,
boundaryLabel: `${numbers[0]}°C`,
};
}
if (numbers.length >= 2) {
return {
lower: numbers[0],
upper: numbers[1],
boundaryLabel: null as string | null,
};
}
return {
lower: numbers[0],
upper: null as number | null,
boundaryLabel: `${numbers[0]}°C`,
};
}
export function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
export function parseClockMinutes(value?: string | null) {
const text = String(value || "").trim();
const match = text.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const hours = Number(match[1]);
const minutes = Number(match[2]);
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
return hours * 60 + minutes;
}
export function parseLeadingNumber(value?: string | number | null) {
if (typeof value === "number" && Number.isFinite(value)) return value;
const text = String(value || "").trim();
const match = text.match(/[-+]?\d+(?:\.\d+)?/);
if (!match) return null;
const numeric = Number(match[0]);
return Number.isFinite(numeric) ? numeric : null;
}
export function parsePercentFromText(value?: string | number | null) {
if (typeof value === "number" && Number.isFinite(value)) {
return clamp(value, 0, 100);
}
const text = String(value || "").trim();
const percentMatch = text.match(/([-+]?\d+(?:\.\d+)?)\s*%/);
if (percentMatch) {
const numeric = Number(percentMatch[1]);
return Number.isFinite(numeric) ? clamp(numeric, 0, 100) : null;
}
return parseLeadingNumber(text);
}
export function formatConfidenceLabel(value?: string | null, locale = "zh-CN") {
const normalized = String(value || "")
.trim()
.toLowerCase();
if (normalized === "high") return locale === "en-US" ? "High" : "高";
if (normalized === "medium") return locale === "en-US" ? "Medium" : "中";
if (normalized === "low") return locale === "en-US" ? "Low" : "低";
return locale === "en-US" ? "Pending" : "待确认";
}
export function formatSignalDirection(value?: string | null, locale = "zh-CN") {
const normalized = String(value || "")
.trim()
.toLowerCase();
if (normalized === "support")
return locale === "en-US" ? "Support" : "支持升温";
if (normalized === "suppress")
return locale === "en-US" ? "Suppress" : "压制峰值";
return locale === "en-US" ? "Neutral" : "中性";
}
export function formatSignalStrength(value?: string | null, locale = "zh-CN") {
const normalized = String(value || "")
.trim()
.toLowerCase();
if (normalized === "strong") return locale === "en-US" ? "Strong" : "强";
if (normalized === "medium") return locale === "en-US" ? "Medium" : "中";
return locale === "en-US" ? "Weak" : "弱";
}
export function signalTone(signal?: IntradayMeteorologySignal | null) {
const direction = String(signal?.direction || "")
.trim()
.toLowerCase();
if (direction === "support") return "cyan";
if (direction === "suppress") return "amber";
return "blue";
}
export function localizedText(
locale: string,
primary?: string | null,
english?: string | null,
) {
const en = String(english || "").trim();
const value = String(primary || "").trim();
if (locale === "en-US" && en) return en;
return value || en;
}
export function localizedList(
locale: string,
primary?: string[] | null,
english?: string[] | null,
) {
const en = Array.isArray(english)
? english.filter((item) => String(item || "").trim())
: [];
const value = Array.isArray(primary)
? primary.filter((item) => String(item || "").trim())
: [];
if (locale === "en-US" && en.length) return en;
return value.length ? value : en;
}
export function getTrendMetricVisual(metric: {
label?: string;
value?: string;
tone?: string;
}) {
const label = String(metric.label || "").toLowerCase();
const value = String(metric.value || "");
const numeric = parseLeadingNumber(value);
if (label.includes("降水") || label.includes("precip")) {
const precipPercent = parsePercentFromText(value);
if (precipPercent == null) return null;
return {
mode: "fill" as const,
percent: precipPercent,
tone: "cold" as const,
};
}
if (numeric == null) return null;
if (label.includes("温度") || label.includes("temp")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 4) * 50, 0, 100),
tone: numeric >= 0 ? ("warm" as const) : ("cold" as const),
};
}
if (label.includes("露点") || label.includes("dew")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 3) * 50, 0, 100),
tone: numeric >= 0 ? ("warm" as const) : ("cold" as const),
};
}
if (label.includes("气压") || label.includes("pressure")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 4) * 50, 0, 100),
tone: numeric >= 0 ? ("warm" as const) : ("cold" as const),
};
}
if (label.includes("云量") || label.includes("cloud")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 40) * 50, 0, 100),
tone: numeric >= 0 ? ("cold" as const) : ("warm" as const),
};
}
return null;
}
File diff suppressed because it is too large Load Diff
@@ -1,100 +0,0 @@
"use client";
import clsx from "clsx";
import type { Locale } from "@/lib/i18n";
type FutureForecastModalHeaderProps = {
cityDisplayName: string;
dateStr: string;
isAnyLayerSyncing: boolean;
isPro: boolean;
isProLoading: boolean;
isToday: boolean;
locale: Locale;
onClose: () => void;
onRefresh: () => void;
t: (key: string, params?: Record<string, string | number>) => string;
};
export function FutureForecastModalHeader({
cityDisplayName,
dateStr,
isAnyLayerSyncing,
isPro,
isProLoading,
isToday,
locale,
onClose,
onRefresh,
t,
}: FutureForecastModalHeaderProps) {
const cityLabel = cityDisplayName.toUpperCase();
return (
<div className="modal-header">
<div className="modal-title-stack">
<div className="modal-overline">
<span>{locale === "en-US" ? "Analysis workspace" : "分析工作台"}</span>
<span className="modal-overline-sep"></span>
<span>{cityLabel}</span>
</div>
<h2 id="future-modal-title" className="future-modal-title-with-actions">
<span>
{isToday
? t("future.todayTitle", {
city: cityLabel,
})
: t("future.dateTitle", {
city: cityLabel,
date: dateStr,
})}
</span>
<button
className={clsx("future-refresh-btn", isAnyLayerSyncing && "spinning")}
disabled={!isPro || isProLoading}
onClick={onRefresh}
title={
!isPro
? locale === "en-US"
? "Pro subscription required"
: "需要 Pro 订阅"
: locale === "en-US"
? "Refresh Data"
: "刷新数据"
}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
</svg>
</button>
</h2>
<div className="modal-subtitle">
{isToday
? locale === "en-US"
? "Base signal first, then probability and model layers."
: "先看基础信号,再看概率层和模型层。"
: locale === "en-US"
? "Forward date view with phased model and structure sync."
: "未来日期视图,模型层与结构层分阶段补齐。"}
</div>
</div>
<button
type="button"
className="modal-close"
aria-label={isToday ? t("future.closeTodayAria") : t("future.closeDateAria")}
onClick={onClose}
>
×
</button>
</div>
);
}
@@ -1,87 +0,0 @@
"use client";
import dynamic from "next/dynamic";
import type { useDashboardStore } from "@/hooks/useDashboardStore";
import type { MarketScan } from "@/lib/dashboard-types";
const DailyTemperatureChart = dynamic(
() =>
import("./FutureForecastModalChart").then(
(module) => module.DailyTemperatureChart,
),
{
loading: () => <div className="history-chart-wrapper future-chart-wrapper" />,
ssr: false,
},
);
const ProbabilityDistribution = dynamic(
() =>
import("@/components/dashboard/PanelSections").then(
(module) => module.ProbabilityDistribution,
),
{
loading: () => <div className="future-v2-panel-loading" />,
ssr: false,
},
);
const ModelForecast = dynamic(
() =>
import("@/components/dashboard/PanelSections").then(
(module) => module.ModelForecast,
),
{
loading: () => <div className="future-v2-panel-loading" />,
ssr: false,
},
);
type DashboardDetail = NonNullable<
ReturnType<typeof useDashboardStore>["selectedDetail"]
>;
export function FutureTemperaturePathChart({
dateStr,
forceToday,
}: {
dateStr: string;
forceToday: boolean;
}) {
return <DailyTemperatureChart dateStr={dateStr} forceToday={forceToday} />;
}
export function FutureProbabilityPanel({
detail,
targetDate,
marketScan,
hideTitle = true,
}: {
detail: DashboardDetail;
targetDate: string;
marketScan?: MarketScan | null;
hideTitle?: boolean;
}) {
return (
<ProbabilityDistribution
detail={detail}
targetDate={targetDate}
marketScan={marketScan}
hideTitle={hideTitle}
/>
);
}
export function FutureModelForecastPanel({
detail,
targetDate,
hideTitle = true,
}: {
detail: DashboardDetail;
targetDate: string;
hideTitle?: boolean;
}) {
return (
<ModelForecast detail={detail} targetDate={targetDate} hideTitle={hideTitle} />
);
}
@@ -1,245 +0,0 @@
import clsx from "clsx";
import type { CSSProperties } from "react";
import type { getTodayPaceView } from "@/lib/pace-utils";
import { WeatherIcon } from "./FutureForecastModalWeatherIcon";
type Locale = string;
type TodayPaceView = NonNullable<ReturnType<typeof getTodayPaceView>>;
type WeatherSummaryView = {
weatherIcon: string;
weatherText: string;
};
type DaylightProgressView = {
phase: string;
percent: number;
};
export type FuturePaceSignalItem = {
label: string;
tone: "cyan" | "blue" | "amber";
status: string;
value: string;
note: string;
};
export function FutureAnchorStatusCard({
locale,
currentTempText,
weatherSummary,
obsTime,
topObservedTemp,
tempSymbol,
gapToBaseBucket,
pathStatus,
}: {
locale: Locale;
currentTempText: string;
weatherSummary: WeatherSummaryView;
obsTime?: string | null;
topObservedTemp: number | string | null | undefined;
tempSymbol: string;
gapToBaseBucket: number | null;
pathStatus: string;
}) {
const observedHigh = topObservedTemp ?? "--";
return (
<section className="future-v2-card future-v2-hero-card">
<div className="future-v2-card-head">
<h3 className="future-v2-hero-title">
{locale === "en-US" ? "Anchor Status" : "锚点状态"}
</h3>
<div className="future-v2-card-kicker">
{locale === "en-US"
? "Settlement anchor and current clock"
: "结算锚点与当前时钟"}
</div>
</div>
<div className="future-v2-hero-main">
<div className="future-v2-hero-temp">{currentTempText}</div>
<div className="future-v2-hero-divider" />
<div className="future-v2-hero-weather">
<span className="future-v2-hero-icon">
<WeatherIcon emoji={weatherSummary.weatherIcon} size={42} />
</span>
<span>{weatherSummary.weatherText}</span>
</div>
</div>
<div className="future-v2-hero-obs">@{obsTime || "--"}</div>
<div className="future-v2-mini-grid">
<FutureMiniMetric
label={locale === "en-US" ? "High so far" : "日内已见高点"}
value={`${observedHigh}${tempSymbol}`}
/>
<FutureMiniMetric
label={locale === "en-US" ? "Anchor clock" : "锚点时钟"}
value={obsTime || "--"}
/>
<FutureMiniMetric
label={locale === "en-US" ? "Gap to base" : "距基准档"}
value={
gapToBaseBucket != null
? `${gapToBaseBucket.toFixed(1)}${tempSymbol}`
: "--"
}
/>
<FutureMiniMetric
label={locale === "en-US" ? "Path state" : "路径状态"}
value={pathStatus}
/>
</div>
</section>
);
}
export function FuturePaceCard({
locale,
paceView,
tempSymbol,
signalItems,
}: {
locale: Locale;
paceView: TodayPaceView;
tempSymbol: string;
signalItems: FuturePaceSignalItem[];
}) {
return (
<section className="future-v2-card future-v2-pace-card future-v2-focus-card">
<div className="future-v2-card-head">
<h4 className="future-v2-card-title">
{locale === "en-US" ? "Current Pace" : "当前节奏"}
</h4>
<div className="future-v2-card-kicker">
{locale === "en-US"
? "Expected now vs airport anchor"
: "此刻应到 vs 机场锚点"}
</div>
</div>
<div className="future-v2-pace-head">
<span className="future-v2-pace-kicker">{paceView.kicker}</span>
<FutureSignalTag tone={paceView.biasTone}>{paceView.badge}</FutureSignalTag>
</div>
<div
className={clsx(
"future-v2-pace-delta",
paceView.biasTone === "cold" && "cold",
paceView.biasTone === "neutral" && "neutral",
paceView.biasTone === "warm" && "warm",
)}
>
{paceView.deltaText}
</div>
<div className="future-v2-pace-summary">{paceView.summary}</div>
<div className="future-v2-pace-meter">
<span className="future-v2-pace-meter-midline" />
<span
className={clsx(
"future-v2-pace-meter-fill",
paceView.biasTone === "cold" && "cold",
paceView.biasTone === "neutral" && "neutral",
paceView.biasTone === "warm" && "warm",
)}
style={
{
"--pace-left": `${paceView.meterLeft}%`,
"--pace-width": `${paceView.meterWidth}%`,
} as CSSProperties & {
"--pace-left": string;
"--pace-width": string;
}
}
/>
</div>
<div className="future-v2-mini-grid future-v2-mini-grid-tight">
<FutureMiniMetric
label={locale === "en-US" ? "Expected now" : "预期此刻"}
value={`${paceView.expectedNow.toFixed(1)}${tempSymbol}`}
/>
<FutureMiniMetric
label={paceView.observedLabel}
value={`${paceView.observedNow.toFixed(1)}${tempSymbol}`}
/>
<FutureMiniMetric
label={paceView.paceAdjustedLabel}
value={
paceView.paceAdjustedHigh != null
? `${paceView.paceAdjustedHigh.toFixed(1)}${tempSymbol}`
: "--"
}
/>
<FutureMiniMetric
label={locale === "en-US" ? "Peak window" : "峰值窗口"}
value={paceView.peakWindowText}
/>
</div>
{signalItems.length ? (
<div className="future-v2-pace-signal-grid">
{signalItems.map((item) => (
<div key={item.label} className="future-v2-pace-signal-card">
<div className="future-v2-signal-head">
<span>{item.label}</span>
<FutureSignalTag tone={item.tone}>{item.status}</FutureSignalTag>
</div>
<strong>{item.value}</strong>
<div className="future-v2-pace-signal-note">{item.note}</div>
</div>
))}
</div>
) : null}
</section>
);
}
export function FuturePaceLoadingCard({ locale }: { locale: Locale }) {
return (
<section className="future-v2-card future-v2-support-card">
<div className="future-v2-card-head">
<h4 className="future-v2-card-title">
{locale === "en-US" ? "Current Pace" : "当前节奏"}
</h4>
<div className="future-v2-card-kicker">
{locale === "en-US"
? "Backfilling intraday pace context"
: "正在补齐日内节奏上下文"}
</div>
</div>
<div className="future-trend-summary future-trend-summary-muted">
{locale === "en-US"
? "Expected-now pace, boundary risk, and airport-vs-network cues are loading in the background."
: "预期此刻节奏、边界风险和机场对比站网信号正在后台补齐。"}
</div>
</section>
);
}
function FutureMiniMetric({ label, value }: { label: string; value: string }) {
return (
<div className="future-v2-mini-item">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function FutureSignalTag({
tone,
children,
}: {
tone: string;
children: string;
}) {
return (
<em
className={clsx(
"future-v2-signal-tag",
(tone === "cold" || tone === "cyan") && "cyan",
(tone === "neutral" || tone === "blue") && "blue",
(tone === "warm" || tone === "amber") && "amber",
)}
>
{children}
</em>
);
}
@@ -1,87 +0,0 @@
"use client";
import { formatConfidenceLabel } from "./FutureForecastModal.utils";
export function FutureForecastTodayDecisionBrief({
anchorRuleText,
anchorSourceLabel,
baseCaseBucket,
confidence,
displayName,
downsideBucket,
gapToBaseText,
locale,
meteorologyHeadline,
nextObservationLabel,
nextObservationTime,
pathStatus,
settlementStationName,
upsideBucket,
}: {
anchorRuleText: string;
anchorSourceLabel: string;
baseCaseBucket: string;
confidence: string | null | undefined;
displayName: string;
downsideBucket: string | null | undefined;
gapToBaseText: string;
locale: string;
meteorologyHeadline: string;
nextObservationLabel: string;
nextObservationTime: string;
pathStatus: string;
settlementStationName: string;
upsideBucket: string | null | undefined;
}) {
return (
<section className="future-v2-meteorology-brief">
<div className="future-v2-meteorology-copy">
<div className="future-v2-anchor-row">
<div className="modal-section-kicker">
{locale === "en-US" ? "Professional meteorology read" : "专业气象判断"}
</div>
<span className="future-v2-anchor-source">{anchorSourceLabel}</span>
</div>
<h3>{meteorologyHeadline}</h3>
<p className="future-v2-anchor-rule">{anchorRuleText}</p>
<div className="future-v2-meteorology-meta">
<span>
{locale === "en-US" ? "Confidence" : "置信度"} ·{" "}
{formatConfidenceLabel(confidence, locale)}
</span>
<span>
{locale === "en-US" ? "Path state" : "路径状态"} · {pathStatus}
</span>
<span>
{nextObservationLabel} · {nextObservationTime}
</span>
</div>
</div>
<div className="future-v2-decision-rail" aria-label={displayName}>
<div className="future-v2-decision-anchor">
<span>{locale === "en-US" ? "Anchor" : "锚点"}</span>
<strong>{settlementStationName}</strong>
<small>{anchorSourceLabel}</small>
</div>
<div className="future-v2-decision-grid">
<div>
<span>{locale === "en-US" ? "Base" : "基准"}</span>
<strong>{baseCaseBucket || "--"}</strong>
</div>
<div>
<span>{locale === "en-US" ? "Upside" : "上修"}</span>
<strong>{upsideBucket || "--"}</strong>
</div>
<div>
<span>{locale === "en-US" ? "Downside" : "下修"}</span>
<strong>{downsideBucket || "--"}</strong>
</div>
<div>
<span>{locale === "en-US" ? "Gap to base" : "距基准还差"}</span>
<strong>{gapToBaseText}</strong>
</div>
</div>
</div>
</section>
);
}
@@ -1,129 +0,0 @@
"use client";
import clsx from "clsx";
import {
formatSignalDirection,
formatSignalStrength,
localizedText,
signalTone,
} from "./FutureForecastModal.utils";
type MeteorologySignal = {
direction?: string | null;
label?: string | null;
label_en?: string | null;
strength?: string | null;
summary?: string | null;
summary_en?: string | null;
};
export function FutureForecastTodayEvidenceGrid({
airportMetarAnchor,
confirmationRules,
invalidationRules,
locale,
meteorologySignals,
modelSummary,
}: {
airportMetarAnchor: boolean;
confirmationRules: string[];
invalidationRules: string[];
locale: string;
meteorologySignals: MeteorologySignal[];
modelSummary: string;
}) {
const fallbackInvalidationRule =
locale === "en-US"
? "If observations stop tracking the expected curve, wait for the next refresh."
: "若实测不再贴近预期曲线,等待下一次刷新确认。";
const fallbackConfirmationRule =
locale === "en-US"
? airportMetarAnchor
? "Keep watching the next anchor METAR report."
: "Keep watching the next official anchor observation."
: airportMetarAnchor
? "继续观察下一次锚点 METAR 报文。"
: "继续观察下一次官方锚点观测。";
return (
<div className="future-v2-meteorology-grid">
<section className="future-modal-section future-v2-evidence-panel">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Evidence chain" : "气象证据链"}
</div>
<h3>{locale === "en-US" ? "Signal Contributions" : "信号贡献"}</h3>
</div>
<div className="future-v2-evidence-list">
{meteorologySignals.length > 0 ? (
meteorologySignals.map((signal, index) => (
<div
key={`${signal.label || "signal"}-${index}`}
className={clsx("future-v2-evidence-row", signalTone(signal))}
>
<div className="future-v2-evidence-head">
<strong>
{localizedText(locale, signal.label, signal.label_en) || "--"}
</strong>
<span>
{formatSignalDirection(signal.direction, locale)} ·{" "}
{formatSignalStrength(signal.strength, locale)}
</span>
</div>
<p>
{localizedText(locale, signal.summary, signal.summary_en) ||
"--"}
</p>
</div>
))
) : (
<div className="future-text-block">
{locale === "en-US"
? "Meteorology signals are still loading."
: "气象信号仍在加载。"}
</div>
)}
</div>
</section>
<section className="future-modal-section future-v2-rule-panel">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Failure modes" : "失效条件"}
</div>
<h3>
{locale === "en-US" ? "What Downgrades the Read" : "什么会让判断降级"}
</h3>
</div>
<ul className="future-v2-rule-list">
{(invalidationRules.length > 0
? invalidationRules
: [fallbackInvalidationRule]
).map((rule, index) => (
<li key={`${rule}-${index}`}>{rule}</li>
))}
</ul>
</section>
<section className="future-modal-section future-v2-rule-panel">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Confirmation" : "确认条件"}
</div>
<h3>
{locale === "en-US" ? "What Confirms the Path" : "什么会确认主路径"}
</h3>
</div>
<ul className="future-v2-rule-list">
{(confirmationRules.length > 0
? confirmationRules
: [fallbackConfirmationRule]
).map((rule, index) => (
<li key={`${rule}-${index}`}>{rule}</li>
))}
</ul>
<div className="future-v2-model-note">{modelSummary}</div>
</section>
</div>
);
}
@@ -1,193 +0,0 @@
"use client";
import type { MarketScan, CityDetail } from "@/lib/dashboard-types";
import type { FuturePaceSignalItem } from "./FutureForecastTodayCards";
import { FutureForecastTodayEvidenceGrid } from "./FutureForecastTodayEvidenceGrid";
import {
FutureModelForecastPanel,
FutureProbabilityPanel,
FutureTemperaturePathChart,
} from "./FutureForecastModalPanels";
import {
FutureAnchorStatusCard,
FuturePaceCard,
FuturePaceLoadingCard,
} from "./FutureForecastTodayCards";
type Locale = string;
interface WeatherSummaryView {
weatherIcon: string;
weatherText: string;
}
interface DaylightProgressView {
phase: string;
percent: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PaceView = any;
export interface FutureForecastTodayLayoutProps {
locale: Locale;
isToday: boolean;
dateStr: string;
detail: CityDetail;
currentTempText: string;
weatherSummary: WeatherSummaryView;
daylightProgress: DaylightProgressView | null;
topObservedTemp: number | string | null | undefined;
gapToBaseBucket: number | null;
pathStatus: string;
showDeferredTodaySections: boolean;
paceView: PaceView | null;
paceSignalItems: FuturePaceSignalItem[];
baseCaseBucket: string | undefined;
upsideBucket: string | null | undefined;
nextObservationTime: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
airportMetarAnchor: any;
confirmationRules: string[];
invalidationRules: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
meteorologySignals: any[];
modelSummary: string;
probabilityTitle: string;
probabilitySummary: string;
activeMarketScan: MarketScan | null | undefined;
}
export function FutureForecastTodayLayout(props: FutureForecastTodayLayoutProps) {
const {
locale,
isToday,
dateStr,
detail,
currentTempText,
weatherSummary,
daylightProgress,
topObservedTemp,
gapToBaseBucket,
pathStatus,
showDeferredTodaySections,
paceView,
paceSignalItems,
baseCaseBucket,
upsideBucket,
nextObservationTime,
airportMetarAnchor,
confirmationRules,
invalidationRules,
meteorologySignals,
modelSummary,
probabilityTitle,
probabilitySummary,
activeMarketScan,
} = props;
return (
<div className="future-v2-layout">
<aside className="future-v2-left">
<FutureAnchorStatusCard
locale={locale}
currentTempText={currentTempText}
weatherSummary={weatherSummary}
obsTime={detail.current?.obs_time}
topObservedTemp={topObservedTemp}
tempSymbol={detail.temp_symbol}
gapToBaseBucket={gapToBaseBucket}
pathStatus={pathStatus}
/>
{showDeferredTodaySections && paceView ? (
<FuturePaceCard
locale={locale}
paceView={paceView}
tempSymbol={detail.temp_symbol}
signalItems={paceSignalItems}
/>
) : isToday ? (
<FuturePaceLoadingCard locale={locale} />
) : null}
</aside>
<main className="future-v2-right">
<section className="future-modal-section future-v2-main-chart">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Primary view" : "主视图"}
</div>
<h3>
{locale === "en-US"
? "Today's temperature path (anchor obs + models)"
: "今日气温路径(锚点观测 + 模型)"}
</h3>
</div>
<FutureTemperaturePathChart dateStr={dateStr} forceToday={isToday} />
<div className="future-v2-chart-thresholds">
<span>
{locale === "en-US" ? "Base" : "基准"} ·{" "}
{baseCaseBucket || "--"}
</span>
<span>
{locale === "en-US" ? "Upside" : "上修"} ·{" "}
{upsideBucket || "--"}
</span>
<span>
{locale === "en-US" ? "Invalidates at" : "失效观察"} ·{" "}
{nextObservationTime}
</span>
</div>
</section>
<FutureForecastTodayEvidenceGrid
airportMetarAnchor={airportMetarAnchor}
confirmationRules={confirmationRules}
invalidationRules={invalidationRules}
locale={locale}
meteorologySignals={meteorologySignals}
modelSummary={modelSummary}
/>
<div className="future-modal-grid">
<section className="future-modal-section">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Probability read" : "概率判断"}
</div>
<h3>{probabilityTitle}</h3>
</div>
<div className="future-text-block" style={{ marginBottom: "12px" }}>
{probabilitySummary}
</div>
<div style={{ position: "relative", minHeight: "120px" }}>
<FutureProbabilityPanel
detail={detail}
targetDate={dateStr}
marketScan={activeMarketScan}
hideTitle
/>
</div>
</section>
<section className="future-modal-section">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Model layer" : "模型层"}
</div>
<h3>
{locale === "en-US"
? "Model Range & Spread"
: "模型区间与分歧"}
</h3>
</div>
<div className="future-text-block" style={{ marginBottom: "12px" }}>
{modelSummary}
</div>
<FutureModelForecastPanel detail={detail} targetDate={dateStr} hideTitle />
</section>
</div>
</main>
</div>
);
}
@@ -9,7 +9,6 @@ import dashboardShellStyles from "./DashboardShell.module.css";
import detailChromeStyles from "./DetailPanelChrome.module.css";
import detailContentStyles from "./DetailPanelContent.module.css";
import detailSectionsStyles from "./DetailPanelSections.module.css";
import futureForecastModalStyles from "./FutureForecastModal.module.css";
import modalChromeStyles from "./ModalChrome.module.css";
import scanTerminalStyles from "./ScanTerminal.module.css";
import scanTerminalBoardStyles from "./ScanTerminalBoard.module.css";
@@ -41,5 +40,4 @@ export const scanRootClass = clsx(
detailContentStyles.root,
detailSectionsStyles.root,
modalChromeStyles.root,
futureForecastModalStyles.root,
);