"use client"; import { ChartConfiguration } from "chart.js/auto"; import clsx from "clsx"; import { CSSProperties } from "react"; import { useChart } from "@/hooks/useChart"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { ModelForecast, ProbabilityDistribution, } from "@/components/dashboard/PanelSections"; import { getFutureModalView, getShortTermNowcastLines, getTemperatureChartData, getWeatherSummary, parseAiAnalysis, } from "@/lib/dashboard-utils"; function getConfidenceLabel(confidence: string) { return ( { high: "高", medium: "中", low: "低", }[confidence] || confidence ); } function DailyTemperatureChart({ dateStr }: { dateStr: string }) { const store = useDashboardStore(); const detail = store.selectedDetail; const view = detail ? getFutureModalView(detail, dateStr) : null; const isToday = detail ? dateStr === detail.local_date : false; const todayChartData = detail && isToday ? getTemperatureChartData(detail) : null; const canvasRef = useChart( () => { if (!detail || !view) { return { data: { datasets: [], labels: [] }, type: "line", } satisfies ChartConfiguration<"line">; } if (isToday && todayChartData) { const datasets: NonNullable["data"]>["datasets"] = []; if (todayChartData.datasets.hasMgmHourly) { datasets.push({ backgroundColor: "rgba(234, 179, 8, 0.05)", borderColor: "rgba(234, 179, 8, 0.8)", borderWidth: 2, data: todayChartData.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: todayChartData.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: todayChartData.datasets.debFuture, fill: false, label: "DEB 预报", pointRadius: 0, tension: 0.3, }); } datasets.push({ backgroundColor: "#22d3ee", borderColor: "#22d3ee", borderWidth: 0, data: todayChartData.datasets.metarPoints, fill: false, label: "METAR 实测", order: 0, pointHoverRadius: 7, pointRadius: 5, }); if (todayChartData.datasets.mgmPoints.some((value) => value != null)) { datasets.push({ backgroundColor: "#facc15", borderColor: "#facc15", borderWidth: 0, data: todayChartData.datasets.mgmPoints, fill: false, label: "MGM 实测", order: -1, pointHoverRadius: 9, pointRadius: 7, showLine: false, }); } if ( !todayChartData.datasets.hasMgmHourly && Math.abs(todayChartData.datasets.offset) > 0.3 ) { datasets.push({ borderColor: "rgba(99, 102, 241, 0.2)", borderDash: [2, 4], borderWidth: 1, data: todayChartData.datasets.temps, fill: false, label: "OM 原始", pointRadius: 0, tension: 0.3, }); } return { data: { datasets, labels: todayChartData.times, }, options: { interaction: { intersect: false, mode: "index" }, maintainAspectRatio: false, plugins: { legend: { labels: { color: "#94a3b8", filter: (legendItem, chartData) => { const text = String(legendItem.text || ""); if (!text) return false; if (!text.includes("DEB")) return true; const firstDebIndex = (chartData.datasets || []).findIndex((dataset) => String(dataset.label || "").includes("DEB"), ); return legendItem.datasetIndex === firstDebIndex; }, font: { family: "Inter", size: 11 }, }, }, tooltip: { backgroundColor: "rgba(15, 23, 42, 0.96)", borderColor: "rgba(34, 211, 238, 0.2)", borderWidth: 1, }, }, responsive: true, scales: { x: { grid: { color: "rgba(255,255,255,0.04)" }, ticks: { callback: (_value, index) => typeof index === "number" && index % 3 === 0 ? todayChartData.times[index] : "", color: "#64748b", font: { family: "Inter", size: 10 }, maxRotation: 0, }, }, y: { grid: { color: "rgba(255,255,255,0.04)" }, max: todayChartData.max, min: todayChartData.min, ticks: { callback: (value) => `${value}${detail.temp_symbol || "°C"}`, color: "#64748b", font: { family: "Inter", size: 10 }, }, }, }, }, type: "line", } satisfies ChartConfiguration<"line">; } const labels = view.slice.map((point) => point.label); const unit = detail.temp_symbol || "°C"; return { data: { datasets: [ { backgroundColor: "rgba(34, 211, 238, 0.08)", borderColor: "#22d3ee", data: view.slice.map((point) => point.temp), fill: false, label: "Open-Meteo 温度", pointRadius: 2, tension: 0.28, }, { backgroundColor: "transparent", borderColor: "#a78bfa", borderDash: [5, 4], data: view.slice.map((point) => point.dewPoint), fill: false, label: "露点", pointRadius: 0, tension: 0.24, }, ], labels, }, options: { interaction: { intersect: false, mode: "index" }, maintainAspectRatio: false, plugins: { legend: { labels: { color: "#94a3b8", font: { family: "Inter", size: 11 }, }, }, tooltip: { backgroundColor: "rgba(15, 23, 42, 0.96)", borderColor: "rgba(34, 211, 238, 0.2)", borderWidth: 1, callbacks: { label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y?.toFixed(1)}${unit}`, }, }, }, responsive: true, scales: { x: { grid: { color: "rgba(255,255,255,0.04)" }, ticks: { color: "#64748b", font: { family: "Inter", size: 10 }, maxRotation: 0, }, }, y: { grid: { color: "rgba(255,255,255,0.04)" }, ticks: { callback: (value) => `${value}${unit}`, color: "#64748b", font: { family: "Inter", size: 10 }, }, }, }, }, type: "line", } satisfies ChartConfiguration<"line">; }, [detail, isToday, todayChartData, view], ); return ( <>
{isToday && (
{todayChartData?.legendText || "暂无机场报文或小时级实测数据"}
)} ); } export function FutureForecastModal() { const store = useDashboardStore(); const detail = store.selectedDetail; const dateStr = store.futureModalDate; if (!detail || !dateStr) return null; const isToday = dateStr === detail.local_date; const view = getFutureModalView(detail, dateStr); const nowcastRows = getShortTermNowcastLines(detail, dateStr); const ai = parseAiAnalysis(detail.ai_analysis); const scorePosition = `${50 + view.front.score / 2}%`; const barStyle = { "--score-position": scorePosition, } as CSSProperties & { "--score-position": string }; const weatherSummary = getWeatherSummary(detail); return (
{ if (event.target === event.currentTarget) { store.closeFutureModal(); } }} >

{isToday ? `${detail.display_name.toUpperCase()} · 今日日内分析` : `${detail.display_name.toUpperCase()} · ${dateStr} 未来日期分析`}

{isToday && ( <>
当前实测 {detail.current?.temp ?? "--"} {detail.temp_symbol} @{detail.current?.obs_time || "--"}
当前天气 {weatherSummary.weatherIcon} {weatherSummary.weatherText}
WU 结算参考 {detail.current?.wu_settlement ?? "--"} {detail.temp_symbol}
日出时间 {detail.forecast?.sunrise || "--"}
日落时间 {detail.forecast?.sunset || "--"}
日照时长 {detail.forecast?.sunshine_hours != null ? `${detail.forecast.sunshine_hours}h` : "--"}
)}
{isToday ? "今日预报高温" : "目标日预报"} {view.forecastEntry?.max_temp ?? "--"} {detail.temp_symbol}
DEB 预测 {view.deb ?? "--"} {detail.temp_symbol}
动态分布中心 {view.mu != null ? `${view.mu.toFixed(1)}${detail.temp_symbol}` : "--"}
趋势评分 {view.front.score > 0 ? "+" : ""} {view.front.score}

{isToday ? "今日温度走势" : "目标日小时走势"}

结算概率分布

多模型预报

{isToday ? "今日日内结构信号" : "未来 6-48 小时趋势"}

判断: {view.front.label} 置信度: {getConfidenceLabel(view.front.confidence)} 最大降水概率: {Math.round(view.front.precipMax)}%
{view.front.summary}
{view.front.metrics.map((metric) => (
{metric.label}
{metric.value}
{metric.note}
))}

AI 深度分析

{ai.summary ?
{ai.summary}
: null} {ai.bullets.length > 0 && (
{ai.bullets.map((item) => (
{item}
))}
)} {!ai.summary && ai.bullets.length === 0 && (
暂无 AI 分析,当前以结构化气象与模型数据为主。
)}
{nowcastRows.map(([label, value]) => (
{label}: {value}
))}
{view.front.weatherGovPeriods.length > 0 && (
weather.gov 文本: {view.front.weatherGovPeriods .map((period) => period.short_forecast || period.detailed_forecast) .filter(Boolean) .join(" / ")}
)}
); }