Clarify METAR anchor labeling in weather forecasts

This commit is contained in:
2569718930@qq.com
2026-04-17 00:19:47 +08:00
parent 910a4aa0a0
commit 8db933a47c
4 changed files with 230 additions and 28 deletions
@@ -2949,10 +2949,33 @@
.root :global(.future-v2-meteorology-copy) {
display: grid;
gap: 9px;
min-width: 0;
}
.root :global(.future-v2-anchor-row) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 0;
}
.root :global(.future-v2-anchor-source) {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 4px 8px;
border: 1px solid rgba(34, 211, 238, 0.28);
border-radius: 8px;
background: rgba(8, 47, 73, 0.36);
color: #67e8f9;
font-size: 11px;
font-weight: 800;
line-height: 1.25;
white-space: nowrap;
}
.root :global(.future-v2-meteorology-copy h3) {
margin: 0;
color: #f8fafc;
@@ -2962,6 +2985,14 @@
letter-spacing: 0;
}
.root :global(.future-v2-anchor-rule) {
margin: 0;
max-width: 860px;
color: var(--text-muted);
font-size: 12px;
line-height: 1.55;
}
.root :global(.future-v2-meteorology-meta) {
display: flex;
flex-wrap: wrap;
@@ -2981,6 +3012,73 @@
line-height: 1.35;
}
.root :global(.future-v2-decision-rail) {
display: grid;
grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.35fr);
gap: 10px;
min-width: 0;
}
.root :global(.future-v2-decision-anchor) {
min-width: 0;
padding: 12px;
border: 1px solid rgba(96, 165, 250, 0.2);
border-radius: 8px;
background: rgba(2, 6, 23, 0.5);
}
.root :global(.future-v2-decision-anchor span),
.root :global(.future-v2-decision-grid span) {
display: block;
color: var(--text-muted);
font-size: 11px;
line-height: 1.3;
}
.root :global(.future-v2-decision-anchor strong) {
display: block;
margin-top: 8px;
color: #f8fafc;
font-size: 14px;
font-weight: 800;
line-height: 1.35;
letter-spacing: 0;
}
.root :global(.future-v2-decision-anchor small) {
display: block;
margin-top: 6px;
color: #67e8f9;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
}
.root :global(.future-v2-decision-grid) {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
min-width: 0;
}
.root :global(.future-v2-decision-grid div) {
min-width: 0;
padding: 10px;
border: 1px solid rgba(34, 211, 238, 0.18);
border-radius: 8px;
background: rgba(2, 6, 23, 0.5);
}
.root :global(.future-v2-decision-grid strong) {
display: block;
margin-top: 7px;
color: #67e8f9;
font-size: 18px;
font-weight: 800;
line-height: 1;
letter-spacing: 0;
}
.root :global(.future-v2-meteorology-paths) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -4127,6 +4225,14 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.root :global(.future-v2-decision-rail) {
grid-template-columns: 1fr;
}
.root :global(.future-v2-decision-grid) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.root :global(.future-v2-layout) {
grid-template-columns: 1fr;
}
@@ -4160,6 +4266,15 @@
grid-template-columns: 1fr;
}
.root :global(.future-v2-anchor-row) {
align-items: flex-start;
flex-direction: column;
}
.root :global(.future-v2-decision-grid) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.root :global(.future-v2-meteorology-copy h3) {
font-size: 16px;
}
@@ -1197,6 +1197,47 @@ export function FutureForecastModal() {
String(intradayMeteorology.peak_window || "").trim() ||
paceView?.peakWindowText ||
"--";
const settlementSourceCode = String(
detail.current?.settlement_source || "",
).trim().toLowerCase();
const settlementStationCode = String(
detail.current?.station_code || detail.risk?.icao || "",
)
.trim()
.toUpperCase();
const settlementStationName =
String(detail.current?.station_name || detail.risk?.airport || "").trim() ||
settlementStationCode ||
(locale === "en-US" ? "Anchor station" : "锚点站");
const airportMetarAnchor =
settlementSourceCode === "metar" ||
settlementSourceCode === "wunderground" ||
Boolean(settlementStationCode && /^[A-Z]{4}$/.test(settlementStationCode));
const anchorSourceLabel = airportMetarAnchor
? settlementStationCode
? `${settlementStationCode} METAR`
: "METAR"
: detail.current?.settlement_source_label ||
detail.current?.settlement_source ||
(locale === "en-US" ? "Official observation" : "官方观测");
const anchorRuleText = airportMetarAnchor
? locale === "en-US"
? `Airport contract anchor: use the ${anchorSourceLabel} reports. Wunderground is only a history display page when present.`
: `机场合约锚点:以 ${anchorSourceLabel} 报文为准;若出现 Wunderground,它只是历史展示页面。`
: locale === "en-US"
? `Official anchor: use ${anchorSourceLabel} observations for this contract.`
: `官方锚点:该合约按 ${anchorSourceLabel} 观测口径判断。`;
const nextObservationLabel = airportMetarAnchor
? locale === "en-US"
? "Next METAR watch"
: "下一次 METAR 观察"
: locale === "en-US"
? "Next anchor watch"
: "下一次锚点观察";
const gapToBaseText =
gapToBaseBucket == null
? "--"
: `${gapToBaseBucket.toFixed(1)}${detail.temp_symbol || "°C"}`;
const syncStatusItems = [
{
key: "base",
@@ -1334,35 +1375,50 @@ export function FutureForecastModal() {
{isToday && (
<section className="future-v2-meteorology-brief">
<div className="future-v2-meteorology-copy">
<div className="modal-section-kicker">
{locale === "en-US" ? "Professional meteorology read" : "专业气象判断"}
<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(intradayMeteorology.confidence, locale)}
</span>
<span>
{locale === "en-US" ? "Peak window" : "峰值窗口"} · {peakWindowText}
{locale === "en-US" ? "Path state" : "路径状态"} · {pathStatus}
</span>
<span>
{locale === "en-US" ? "Next observation" : "下一观测"} · {nextObservationTime}
{nextObservationLabel} · {nextObservationTime}
</span>
</div>
</div>
<div className="future-v2-meteorology-paths">
<div>
<span>{locale === "en-US" ? "Base case" : "基准路径"}</span>
<strong>{baseCaseBucket || "--"}</strong>
<div className="future-v2-decision-rail">
<div className="future-v2-decision-anchor">
<span>{locale === "en-US" ? "Anchor" : "锚点"}</span>
<strong>{settlementStationName}</strong>
<small>{anchorSourceLabel}</small>
</div>
<div>
<span>{locale === "en-US" ? "Upside" : "上修路径"}</span>
<strong>{intradayMeteorology.upside_bucket || "--"}</strong>
</div>
<div>
<span>{locale === "en-US" ? "Downside" : "下修路径"}</span>
<strong>{intradayMeteorology.downside_bucket || "--"}</strong>
<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>{intradayMeteorology.upside_bucket || "--"}</strong>
</div>
<div>
<span>{locale === "en-US" ? "Downside" : "下修"}</span>
<strong>{intradayMeteorology.downside_bucket || "--"}</strong>
</div>
<div>
<span>{locale === "en-US" ? "Gap to base" : "距基准还差"}</span>
<strong>{gapToBaseText}</strong>
</div>
</div>
</div>
</section>
@@ -1653,8 +1709,8 @@ export function FutureForecastModal() {
</div>
<h3>
{locale === "en-US"
? "Today's temperature forecast (obs + market)"
: "今日气温预测(观测 + 市场"}
? "Today's temperature path (anchor obs + models)"
: "今日气温路径(锚点观测 + 模型"}
</h3>
</div>
<DailyTemperatureChart dateStr={dateStr} />
@@ -1740,8 +1796,12 @@ export function FutureForecastModal() {
? confirmationRules
: [
locale === "en-US"
? "Keep watching the next settlement-source observation."
: "继续观察下一次结算源报文。",
? airportMetarAnchor
? "Keep watching the next anchor METAR report."
: "Keep watching the next official anchor observation."
: airportMetarAnchor
? "继续观察下一次锚点 METAR 报文。"
: "继续观察下一次官方锚点观测。",
]
).map((rule, index) => (
<li key={`${rule}-${index}`}>{rule}</li>
@@ -158,7 +158,22 @@ export function HeroSummary() {
const { weatherIcon, weatherText } = getWeatherSummary(data, locale);
const metaItems = getHeroMetaItems(data, locale);
const current = data.current || {};
const settlementSource = String(current.settlement_source || "metar").toUpperCase();
const settlementSourceCode = String(current.settlement_source || "metar")
.trim()
.toLowerCase();
const settlementIcao = String(
current.station_code || data.risk?.icao || "",
)
.trim()
.toUpperCase();
const settlementSource =
settlementSourceCode === "wunderground"
? settlementIcao
? `${settlementIcao} METAR`
: "METAR"
: String(current.settlement_source_label || current.settlement_source || "METAR")
.trim()
.toUpperCase();
const isMax =
current.max_so_far != null &&
current.temp != null &&
@@ -198,8 +213,8 @@ export function HeroSummary() {
<div className="hero-item">
<span className="label">
{locale === "en-US"
? `${settlementSource} Settlement Ref`
: `${settlementSource} 结算参考`}
? `${settlementSource} Anchor`
: `${settlementSource} 锚点`}
</span>
<span className="value highlight">
{current.wu_settlement != null
+18 -6
View File
@@ -106,7 +106,12 @@ function getObservationSourceTag(detail: CityDetail): string {
if (code === "hko") return "HKO";
if (code === "cwa") return "CWA";
if (code === "noaa") return "NOAA";
if (code === "wunderground") return "WUNDERGROUND";
if (code === "wunderground") {
const icao = String(detail.risk?.icao || detail.current?.station_code || "")
.trim()
.toUpperCase();
return icao ? `${icao} METAR` : "METAR";
}
if (code === "mgm") return "MGM";
return "METAR";
}
@@ -819,7 +824,7 @@ export function getTemperatureChartData(
: observationCode === "wunderground" && usingMetarObservationSource
? metarFallbackTag
: observationCode === "wunderground"
? `WU ${String(detail.risk?.icao || "").trim().toUpperCase() || "STATION"}`
? metarFallbackTag
: useSettlementObservationSource && shouldUseMetarFallback
? metarFallbackTag
: observationCode === "noaa"
@@ -3190,8 +3195,7 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
const isOfficialSource =
sourceCode === "hko" ||
sourceCode === "cwa" ||
sourceCode === "noaa" ||
sourceCode === "wunderground";
sourceCode === "noaa";
const sourceDisplay = (() => {
if (sourceCode === "hko") {
@@ -3211,6 +3215,15 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
? `${noaaName}${noaaCode ? ` (${noaaCode})` : ""}`
: `${noaaName}${noaaCode ? `${noaaCode}` : ""}`;
}
if (sourceCode === "wunderground") {
const icao = String(detail.risk?.icao || detail.current?.station_code || "")
.trim()
.toUpperCase();
const stationName = String(
detail.current?.station_name || detail.risk?.airport || "",
).trim();
return `${stationName || icao || "Airport"}${icao ? ` (${icao} METAR)` : " METAR"}`;
}
const stationName = String(
detail.current?.station_name || detail.risk?.airport || "",
).trim();
@@ -3343,8 +3356,7 @@ export function getSettlementRiskNarrative(
const stationTerm =
sourceCode === "hko" ||
sourceCode === "cwa" ||
sourceCode === "noaa" ||
sourceCode === "wunderground"
sourceCode === "noaa"
? isEnglish(locale)
? "settlement reference station"
: "结算参考站"