Files
PolyWeather/frontend/lib/weather-summary-utils.ts
T
2569718930@qq.com a77d613c2b Move weather summaries out of the dashboard utility bulk
Weather labels, METAR translation, risk badges, and hero meta chips were still embedded in dashboard-utils even though panel, modal, and detail views use them as focused presentation helpers. This moves them into weather-summary-utils while keeping dashboard-utils re-export compatibility.

Constraint: Preserve existing weather/METAR/risk/hero meta wording.

Rejected: Move airport narrative in the same pass | it has broader AI narrative coupling and should be separated independently.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Tested: TypeScript diagnostics for weather-summary-utils, dashboard-utils, PanelSections, DetailPanel, and FutureForecastModal

Tested: npm run build

Not-tested: Visual snapshot of every weather icon/label combination.
2026-04-28 12:17:42 +08:00

194 lines
6.4 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 } from "@/lib/dashboard-types";
import type { Locale } from "@/lib/i18n";
import {
getRealtimeObservationTag,
isTurkishMgmCity,
} from "@/lib/observation-source-utils";
const METAR_WX_MAP: Record<
string,
{ en: string; icon: string; zh: string }
> = {
VCSH: { en: "Showers nearby", icon: "🌦️", zh: "附近有阵雨" },
SHRA: { en: "Rain showers", icon: "🌦️", zh: "阵雨" },
"-SHRA": { en: "Light rain showers", icon: "🌦️", zh: "小阵雨" },
"+SHRA": { en: "Heavy rain showers", icon: "⛈️", zh: "强阵雨" },
VCRA: { en: "Rain nearby", icon: "🌧️", zh: "附近有降雨" },
TSRA: { en: "Thunderstorms with rain", icon: "⛈️", zh: "雷雨" },
"-TSRA": { en: "Light thunderstorms with rain", icon: "⛈️", zh: "小雷雨" },
"+TSRA": { en: "Heavy thunderstorms with rain", icon: "⛈️", zh: "强雷雨" },
RA: { en: "Rain", icon: "🌧️", zh: "降雨" },
"-RA": { en: "Light rain", icon: "🌦️", zh: "小雨" },
"+RA": { en: "Heavy rain", icon: "⛈️", zh: "强降雨" },
SN: { en: "Snow", icon: "❄️", zh: "降雪" },
"-SN": { en: "Light snow", icon: "🌨️", zh: "小雪" },
"+SN": { en: "Heavy snow", icon: "🌨️", zh: "大雪" },
DZ: { en: "Drizzle", icon: "🌦️", zh: "毛毛雨" },
FG: { en: "Fog", icon: "🌫️", zh: "雾" },
VCFG: { en: "Fog nearby", icon: "🌫️", zh: "附近有雾" },
MIFG: { en: "Shallow fog", icon: "🌫️", zh: "浅雾" },
BR: { en: "Mist", icon: "🌫️", zh: "薄雾" },
HZ: { en: "Haze", icon: "🌫️", zh: "霾" },
TS: { en: "Thunderstorm", icon: "⛈️", zh: "雷暴" },
VCTS: { en: "Nearby thunderstorm", icon: "⛈️", zh: "附近雷暴" },
SQ: { en: "Squall", icon: "💨", zh: "飑线" },
GS: { en: "Hail", icon: "🌨️", zh: "冰雹" },
};
function isEnglish(locale: Locale) {
return locale === "en-US";
}
function normalizeCloudSummary(
cloudDesc: string | null | undefined,
locale: Locale,
): { icon: string; text: string } {
const raw = String(cloudDesc || "").trim();
if (!raw) {
return { icon: "🔍", text: isEnglish(locale) ? "Unknown" : "未知" };
}
const lower = raw.toLowerCase();
if (
raw.includes("晴") ||
raw.includes("晴朗") ||
lower.includes("clear") ||
lower.includes("sunny")
) {
return { icon: "☀️", text: isEnglish(locale) ? "Clear" : "晴朗" };
}
if (raw.includes("阴") || lower.includes("overcast")) {
return { icon: "☁️", text: isEnglish(locale) ? "Overcast" : "阴天" };
}
if (raw.includes("多云") || lower.includes("cloud")) {
return { icon: "☁️", text: isEnglish(locale) ? "Cloudy" : "多云" };
}
if (raw.includes("少云") || lower.includes("few")) {
return { icon: "🌤️", text: isEnglish(locale) ? "Mostly clear" : "少云" };
}
if (raw.includes("散云") || lower.includes("scattered")) {
return { icon: "⛅", text: isEnglish(locale) ? "Partly cloudy" : "散云" };
}
return { icon: "🔍", text: raw };
}
export function translateMetar(code?: string | null, locale: Locale = "zh-CN") {
if (!code) return null;
const metarCode = String(code);
for (const [key, value] of Object.entries(METAR_WX_MAP)) {
if (metarCode.includes(key)) {
return {
icon: value.icon,
label: isEnglish(locale) ? value.en : value.zh,
};
}
}
return { icon: "🔍", label: metarCode };
}
export function getRiskBadgeLabel(
level?: string | null,
locale: Locale = "zh-CN",
) {
if (isEnglish(locale)) {
return (
{
high: "🔴 High Risk",
low: "🟢 Low Risk",
medium: "🟠 Medium Risk",
}[String(level || "low")] || "Unknown Risk"
);
}
return (
{
high: "🔴 高风险",
low: "🟢 低风险",
medium: "🟠 中风险",
}[String(level || "low")] || "未知风险"
);
}
export function getWeatherSummary(detail: CityDetail, locale: Locale = "zh-CN") {
const current = detail.current || {};
const cloud = normalizeCloudSummary(current.cloud_desc, locale);
let weatherText = cloud.text;
let weatherIcon = cloud.icon;
if (current.wx_desc) {
const translated = translateMetar(current.wx_desc, locale);
if (translated) {
weatherText = translated.label;
weatherIcon = translated.icon;
}
}
return { weatherIcon, weatherText };
}
export function getHeroMetaItems(detail: CityDetail, locale: Locale = "zh-CN") {
const current = detail.current || {};
const parts: string[] = [];
const sourceTag = getRealtimeObservationTag(detail);
const suppressAnkaraMgmObservation = isTurkishMgmCity(detail);
if (current.obs_time) {
const ageText =
current.obs_age_min != null && current.obs_age_min >= 30
? isEnglish(locale)
? ` (${current.obs_age_min} min ago)`
: `${current.obs_age_min} 分钟前)`
: "";
parts.push(`✈️ ${sourceTag} ${current.obs_time}${ageText}`);
}
if (current.wx_desc) {
const translated = translateMetar(current.wx_desc, locale);
if (translated) {
parts.push(`${translated.icon} ${translated.label}`);
}
} else if (current.cloud_desc) {
const cloud = normalizeCloudSummary(current.cloud_desc, locale);
parts.push(`${cloud.icon} ${cloud.text}`);
}
if (current.wind_speed_kt != null) {
parts.push(`💨 ${current.wind_speed_kt}kt`);
}
if (current.visibility_mi != null) {
parts.push(`👁️ ${current.visibility_mi}mi`);
}
if (!suppressAnkaraMgmObservation && detail.mgm?.temp != null) {
const timeMatch = detail.mgm.time?.match(/T?(\d{2}:\d{2})/);
const timeText = timeMatch ? ` @${timeMatch[1]}` : "";
parts.push(
isEnglish(locale)
? `🛰 MGM Obs: ${detail.mgm.temp}${detail.temp_symbol}${timeText}`
: `🛰 MGM 实测: ${detail.mgm.temp}${detail.temp_symbol}${timeText}`,
);
}
const trend = detail.trend || {};
if (trend.is_dead_market) {
parts.push(isEnglish(locale) ? "☠️ Flat market" : "☠️ 死盘");
} else if (trend.direction && trend.direction !== "unknown") {
const labels: Record<string, string> = isEnglish(locale)
? {
falling: "📉 Cooling",
mixed: "📊 Choppy",
rising: "📈 Warming",
stagnant: "⏸ Flat",
}
: {
falling: "📉 降温中",
mixed: "📊 波动中",
rising: "📈 升温中",
stagnant: "⏸ 持平",
};
parts.push(labels[trend.direction] || trend.direction);
}
return parts;
}