Files
PolyWeather/frontend/components/dashboard/opportunity-v4-risk.ts
T
2569718930@qq.com 2b1d7c0b65 Improve dashboard maintainability before the next release
The dashboard had several oversized orchestration, component, and CSS files that made product-copy changes and mobile/performance work risky. This refactor preserves behavior while splitting scan terminal CSS, opportunity helpers, future forecast panels, history/detail charts, and probability/model sections into smaller ownership boundaries.

Constraint: No user-visible version bump because this batch is architecture and performance cleanup, not a release announcement.

Rejected: Rewrite dashboard state management in the same batch | too broad for a safe upload after CSS and component splitting.

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Keep new component/CSS boundaries instead of moving product copy back into the large dashboard files.

Tested: npm run build; npm run test:business; git diff --check

Not-tested: Browser visual smoke test after push
2026-04-28 20:19:17 +08:00

105 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
import {
formatTemperatureValue,
normalizeTemperatureSymbol,
} from "@/lib/temperature-utils";
import { formatModelClusterRange } from "./opportunity-model-summary";
import { getMetarObservationContext } from "./opportunity-observation";
import type { V4CityForecast } from "./opportunity-v4-types";
export function getForecastRiskItems(
row: ScanOpportunityRow,
detail: CityDetail | null,
forecast: V4CityForecast,
locale: string,
tempSymbol?: string | null,
) {
const isEn = locale === "en-US";
const obs = getMetarObservationContext(row, detail);
const risks: string[] = [];
const phase = String(row.window_phase || "").toLowerCase();
if (
phase === "early_today" ||
phase === "setup_today" ||
(row.minutes_until_peak_start != null && Number(row.minutes_until_peak_start) > 0)
) {
risks.push(
isEn
? "Peak window has not arrived; current METAR is only path evidence."
: "峰值窗口尚未到达,当前 METAR 只能作为路径证据。",
);
}
if (row.trend_alignment === false) {
risks.push(
isEn
? "Intraday observation path does not fully support the forecast center."
: "日内实况路径还没有完全支持预测中枢。",
);
}
if (forecast.paceRead && forecast.paceTone !== "neutral") {
risks.push(
isEn
? `Observed pace is deviating from the DEB curve: ${forecast.paceRead}`
: `实测节奏正在偏离 DEB 曲线:${forecast.paceRead}`,
);
}
if (obs.stale || obs.lastTemp == null) {
risks.push(
isEn
? "Same-day METAR confirmation is still weak."
: "同日 METAR 确认仍然偏弱。",
);
}
if (forecast.low != null && forecast.high != null) {
const spread = Math.abs(forecast.high - forecast.low);
const wide = String(normalizeTemperatureSymbol(tempSymbol)).toUpperCase().includes("F")
? spread > 2
: spread > 1;
if (wide) {
risks.push(
isEn
? "Model range is wide; treat boundary buckets conservatively."
: "模型区间偏宽,边界温度桶需要保守处理。",
);
}
}
if (!risks.length) {
risks.push(
isEn
? "Residual risk is late METAR revision or a shifted afternoon peak."
: "残余风险主要是后续 METAR 修订或峰值窗口漂移。",
);
}
return Array.from(new Set(risks)).slice(0, 3);
}
export function getDecisionReasonItems(
row: ScanOpportunityRow,
forecast: V4CityForecast,
modelSupportText: string,
locale: string,
tempSymbol?: string | null,
) {
const isEn = locale === "en-US";
const modelRange = formatModelClusterRange(row.model_cluster_sources, tempSymbol);
const reasons: string[] = [];
if (modelRange !== "--") {
reasons.push(
isEn
? `Model cluster sits around ${modelRange}; ${modelSupportText}.`
: `模型区间集中在 ${modelRange}${modelSupportText}。`,
);
}
if (forecast.predicted != null) {
reasons.push(
isEn
? `AI high-temperature center is ${formatTemperatureValue(forecast.predicted, tempSymbol, { digits: 1 })}.`
: `AI 最高温中枢约 ${formatTemperatureValue(forecast.predicted, tempSymbol, { digits: 1 })}。`,
);
}
if (forecast.paceRead) reasons.push(forecast.paceRead);
if (forecast.peakWindow) reasons.push(forecast.peakWindow);
if (forecast.weatherRead) reasons.push(forecast.weatherRead);
return Array.from(new Set(reasons)).slice(0, 3);
}