"use client"; import { ChartConfiguration } from "chart.js/auto"; import clsx from "clsx"; import { useChart } from "@/hooks/useChart"; import { useCityData, useDashboardStore } from "@/hooks/useDashboardStore"; import { CityDetail } from "@/lib/dashboard-types"; import { getHeroMetaItems, getModelView, getProbabilityView, getTemperatureChartData, getWeatherSummary, parseAiAnalysis, } from "@/lib/dashboard-utils"; function EmptyState({ text }: { text: string }) { return
{text}
; } export function HeroSummary() { const { data } = useCityData(); if (!data) return null; const { weatherIcon, weatherText } = getWeatherSummary(data); const metaItems = getHeroMetaItems(data); const current = data.current || {}; const isMax = current.max_so_far != null && current.temp != null && current.max_so_far <= current.temp; return (
{weatherIcon} {weatherText}
{current.temp != null ? current.temp.toFixed(1) : "--"} {data.temp_symbol || "°C"}
{isMax && current.max_temp_time ? `该城市今日最高温出现在当地时间 ${current.max_temp_time}` : ""}
当前实测 {current.temp != null ? `${current.temp}${data.temp_symbol} @${current.obs_time || "--"}` : "--"}
WU 结算参考 {current.wu_settlement != null ? `${current.wu_settlement}${data.temp_symbol}` : "--"}
DEB 预测 {data.deb?.prediction != null ? `${data.deb.prediction}${data.temp_symbol}` : "--"}
{metaItems.map((item) => ( {item} ))}
); } export function TemperatureChart() { const { data } = useCityData(); const chartData = data ? getTemperatureChartData(data) : null; const canvasRef = useChart( () => { if (!data || !chartData) { return { data: { datasets: [], labels: [] }, type: "line", } satisfies ChartConfiguration<"line">; } const datasets: NonNullable["data"]>["datasets"] = []; if (chartData.datasets.hasMgmHourly) { datasets.push({ backgroundColor: "rgba(234, 179, 8, 0.05)", borderColor: "rgba(234, 179, 8, 0.8)", borderWidth: 2, data: chartData.datasets.mgmHourlyPoints, fill: false, label: "MGM 预报", pointHoverRadius: 6, pointRadius: 3, spanGaps: true, tension: 0.3, }); } else { datasets.push({ backgroundColor: "rgba(52, 211, 153, 0.05)", borderColor: "rgba(52, 211, 153, 0.6)", borderWidth: 1.5, data: chartData.datasets.debPast, fill: true, label: "DEB 预报", pointHoverRadius: 3, pointRadius: 0, tension: 0.3, }); datasets.push({ borderColor: "rgba(52, 211, 153, 0.35)", borderDash: [5, 3], borderWidth: 1.5, data: chartData.datasets.debFuture, fill: false, label: "DEB 预报", pointRadius: 0, tension: 0.3, }); } datasets.push({ backgroundColor: "#22d3ee", borderColor: "#22d3ee", borderWidth: 0, data: chartData.datasets.metarPoints, fill: false, label: "METAR 实测", order: 0, pointHoverRadius: 7, pointRadius: 5, }); if (chartData.datasets.mgmPoints.some((value) => value != null)) { datasets.push({ backgroundColor: "#facc15", borderColor: "#facc15", borderWidth: 0, data: chartData.datasets.mgmPoints, fill: false, label: "MGM 实测", order: -1, pointHoverRadius: 9, pointRadius: 7, showLine: false, }); } if ( !chartData.datasets.hasMgmHourly && Math.abs(chartData.datasets.offset) > 0.3 ) { datasets.push({ borderColor: "rgba(99, 102, 241, 0.2)", borderDash: [2, 4], borderWidth: 1, data: chartData.datasets.temps, fill: false, label: "OM 原始", pointRadius: 0, tension: 0.3, }); } return { data: { datasets, labels: chartData.times, }, options: { interaction: { intersect: false, mode: "index" }, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: "rgba(15, 23, 42, 0.9)", borderColor: "rgba(52, 211, 153, 0.3)", borderWidth: 1, }, }, responsive: true, scales: { x: { grid: { color: "rgba(255,255,255,0.04)" }, ticks: { callback: (_value, index) => typeof index === "number" && index % 3 === 0 ? chartData.times[index] : "", color: "#64748b", maxRotation: 0, }, }, y: { grid: { color: "rgba(255,255,255,0.04)" }, max: chartData.max, min: chartData.min, ticks: { callback: (value) => `${value}${data.temp_symbol || "°C"}`, color: "#64748b", }, }, }, }, type: "line", } satisfies ChartConfiguration<"line">; }, [data, chartData], ); return (

今日温度走势

{chartData?.legendText || "暂无小时级数据"}
); } export function ProbabilityDistribution({ detail, hideTitle = false, targetDate, }: { detail: CityDetail; hideTitle?: boolean; targetDate?: string | null; }) { const view = getProbabilityView(detail, targetDate); return (
{!hideTitle &&

结算概率分布

}
{view.mu != null && (
动态分布中心 μ = {view.mu.toFixed(1)} {detail.temp_symbol}
)} {view.probabilities.length === 0 ? ( ) : ( view.probabilities.slice(0, 6).map((bucket, index) => { const probability = Math.round(Number(bucket.probability || 0) * 100); return (
{bucket.label || `${bucket.value}${detail.temp_symbol}`}
{probability}%
); }) )}
); } export function ModelForecast({ detail, hideTitle = false, targetDate, }: { detail: CityDetail; hideTitle?: boolean; targetDate?: string | null; }) { const view = getModelView(detail, targetDate); const modelEntries = Object.entries(view.models).filter(([, value]) => Number.isFinite(Number(value)), ); const numericValues = modelEntries.map(([, value]) => Number(value)); const comparisonValues = view.deb != null ? [...numericValues, Number(view.deb)] : numericValues; const minValue = comparisonValues.length ? Math.min(...comparisonValues) - 1 : 0; const maxValue = comparisonValues.length ? Math.max(...comparisonValues) + 1 : 1; const range = Math.max(maxValue - minValue, 1); return (
{!hideTitle &&

多模型预报

}
{!modelEntries.length ? ( ) : ( <> {modelEntries .sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0)) .map(([name, value]) => { const numeric = Number(value); const width = ((numeric - minValue) / range) * 100; const debLine = view.deb != null ? ((Number(view.deb) - minValue) / range) * 100 : null; return (
{name}
{numeric} {detail.temp_symbol}
{debLine != null && (
)}
); })} {view.deb != null && (
DEB
{Number(view.deb)} {detail.temp_symbol}
)} )}
); } export function ForecastTable() { const store = useDashboardStore(); const { data } = useCityData(); if (!data) return null; const daily = data.forecast?.daily || []; return (

多日预报

{daily.length === 0 ? ( ) : ( daily.map((day, index) => { const isToday = day.date === data.local_date || index === 0; const isSelected = store.futureModalDate === day.date || store.selectedForecastDate === day.date; return ( ); }) )}
); } export function AiAnalysis() { const { data } = useCityData(); if (!data) return null; const ai = parseAiAnalysis(data.ai_analysis); return (

AI 深度分析

{!ai.summary && ai.bullets.length === 0 ? ( 暂无 AI 分析,当前以结构化气象与模型数据为主。 ) : ( <> {ai.summary &&
{ai.summary}
} {ai.bullets.length > 0 && (
    {ai.bullets.map((item) => (
  • {item}
  • ))}
)} )}
); } export function RiskInfo() { const { data } = useCityData(); if (!data) return null; const risk = data.risk || {}; return (

数据偏差风险

{!risk.airport ? ( 暂无风险档案 ) : ( <>
机场 {risk.airport} ({risk.icao})
距离 {risk.distance_km}km
{risk.warning && (
注意 {risk.warning}
)} )}
); }