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.
This commit is contained in:
@@ -13,11 +13,9 @@ import { CityDetail } from "@/lib/dashboard-types";
|
|||||||
import { trackAppEvent } from "@/lib/app-analytics";
|
import { trackAppEvent } from "@/lib/app-analytics";
|
||||||
import { getTodayPolymarketUrl } from "@/lib/polymarket-market-links";
|
import { getTodayPolymarketUrl } from "@/lib/polymarket-market-links";
|
||||||
import { getTemperatureChartData } from "@/lib/chart-utils";
|
import { getTemperatureChartData } from "@/lib/chart-utils";
|
||||||
import {
|
import { getCityProfileStats } from "@/lib/dashboard-utils";
|
||||||
getCityProfileStats,
|
|
||||||
getRiskBadgeLabel,
|
|
||||||
} from "@/lib/dashboard-utils";
|
|
||||||
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
|
import { normalizeObservationSourceLabel } from "@/lib/source-labels";
|
||||||
|
import { getRiskBadgeLabel } from "@/lib/weather-summary-utils";
|
||||||
|
|
||||||
function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ import {
|
|||||||
import { getTemperatureChartData } from "@/lib/chart-utils";
|
import { getTemperatureChartData } from "@/lib/chart-utils";
|
||||||
import {
|
import {
|
||||||
getFutureModalView,
|
getFutureModalView,
|
||||||
getWeatherSummary,
|
|
||||||
} from "@/lib/dashboard-utils";
|
} from "@/lib/dashboard-utils";
|
||||||
import { getModelView, getProbabilityView } from "@/lib/model-utils";
|
import { getModelView, getProbabilityView } from "@/lib/model-utils";
|
||||||
import { getTodayPaceView } from "@/lib/pace-utils";
|
import { getTodayPaceView } from "@/lib/pace-utils";
|
||||||
import { dashboardClient } from "@/lib/dashboard-client";
|
import { dashboardClient } from "@/lib/dashboard-client";
|
||||||
|
import { getWeatherSummary } from "@/lib/weather-summary-utils";
|
||||||
import {
|
import {
|
||||||
normalizeObservationSourceCode,
|
normalizeObservationSourceCode,
|
||||||
normalizeObservationSourceLabel,
|
normalizeObservationSourceLabel,
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import {
|
|||||||
ProbabilityBucket,
|
ProbabilityBucket,
|
||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
import { getTemperatureChartData } from "@/lib/chart-utils";
|
import { getTemperatureChartData } from "@/lib/chart-utils";
|
||||||
import {
|
|
||||||
getHeroMetaItems,
|
|
||||||
getRiskBadgeLabel,
|
|
||||||
getWeatherSummary,
|
|
||||||
} from "@/lib/dashboard-utils";
|
|
||||||
import { getModelView, getProbabilityView } from "@/lib/model-utils";
|
import { getModelView, getProbabilityView } from "@/lib/model-utils";
|
||||||
import {
|
import {
|
||||||
normalizeObservationSourceCode,
|
normalizeObservationSourceCode,
|
||||||
normalizeObservationSourceLabel,
|
normalizeObservationSourceLabel,
|
||||||
} from "@/lib/source-labels";
|
} from "@/lib/source-labels";
|
||||||
|
import {
|
||||||
|
getHeroMetaItems,
|
||||||
|
getRiskBadgeLabel,
|
||||||
|
getWeatherSummary,
|
||||||
|
} from "@/lib/weather-summary-utils";
|
||||||
|
|
||||||
function EmptyState({ text }: { text: string }) {
|
function EmptyState({ text }: { text: string }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,46 +20,23 @@ import {
|
|||||||
} from "@/lib/temperature-utils";
|
} from "@/lib/temperature-utils";
|
||||||
import { formatTafMarkerType } from "@/lib/taf-utils";
|
import { formatTafMarkerType } from "@/lib/taf-utils";
|
||||||
import { normalizeHm } from "@/lib/time-utils";
|
import { normalizeHm } from "@/lib/time-utils";
|
||||||
|
import { getWeatherSummary } from "@/lib/weather-summary-utils";
|
||||||
|
|
||||||
export { getTemperatureChartData } from "@/lib/chart-utils";
|
export { getTemperatureChartData } from "@/lib/chart-utils";
|
||||||
export { getModelView, getProbabilityView } from "@/lib/model-utils";
|
export { getModelView, getProbabilityView } from "@/lib/model-utils";
|
||||||
export { getTodayPaceView } from "@/lib/pace-utils";
|
export { getTodayPaceView } from "@/lib/pace-utils";
|
||||||
|
export {
|
||||||
|
getHeroMetaItems,
|
||||||
|
getRiskBadgeLabel,
|
||||||
|
getWeatherSummary,
|
||||||
|
translateMetar,
|
||||||
|
} from "@/lib/weather-summary-utils";
|
||||||
export {
|
export {
|
||||||
formatTemperatureValue,
|
formatTemperatureValue,
|
||||||
normalizeTemperatureLabel,
|
normalizeTemperatureLabel,
|
||||||
normalizeTemperatureSymbol,
|
normalizeTemperatureSymbol,
|
||||||
} from "@/lib/temperature-utils";
|
} from "@/lib/temperature-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) {
|
function isEnglish(locale: Locale) {
|
||||||
return locale === "en-US";
|
return locale === "en-US";
|
||||||
}
|
}
|
||||||
@@ -94,159 +71,6 @@ function getLocalizedDynamicCommentary(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseAiAnalysis(analysis: CityDetail["ai_analysis"]) {
|
export function parseAiAnalysis(analysis: CityDetail["ai_analysis"]) {
|
||||||
const fallback = {
|
const fallback = {
|
||||||
bullets: [] as string[],
|
bullets: [] as string[],
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user