Refine dashboard detail panel layout
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
getCityProfileStats,
|
getCityProfileStats,
|
||||||
getRiskBadgeLabel,
|
getRiskBadgeLabel,
|
||||||
getTemperatureChartData,
|
getTemperatureChartData,
|
||||||
|
getWeatherSummary,
|
||||||
} from "@/lib/dashboard-utils";
|
} from "@/lib/dashboard-utils";
|
||||||
|
|
||||||
function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||||
@@ -154,16 +155,9 @@ export function DetailPanel() {
|
|||||||
const isProStateLoading = store.proAccess.loading;
|
const isProStateLoading = store.proAccess.loading;
|
||||||
const panelRef = useRef<HTMLElement | null>(null);
|
const panelRef = useRef<HTMLElement | null>(null);
|
||||||
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
||||||
const isOverlayOpen =
|
const isOverlayOpen = Boolean(store.futureModalDate) || store.historyState.isOpen;
|
||||||
Boolean(store.futureModalDate) ||
|
const isVisible = store.isPanelOpen && Boolean(store.selectedCity) && !isOverlayOpen;
|
||||||
store.historyState.isOpen;
|
const hasBasicPanelContent = Boolean(detail || selectedSummary || selectedCityItem);
|
||||||
const isVisible =
|
|
||||||
store.isPanelOpen &&
|
|
||||||
Boolean(store.selectedCity) &&
|
|
||||||
!isOverlayOpen;
|
|
||||||
const hasBasicPanelContent = Boolean(
|
|
||||||
detail || selectedSummary || selectedCityItem,
|
|
||||||
);
|
|
||||||
const panelDisplayName =
|
const panelDisplayName =
|
||||||
detail?.display_name ||
|
detail?.display_name ||
|
||||||
selectedSummary?.display_name ||
|
selectedSummary?.display_name ||
|
||||||
@@ -188,6 +182,9 @@ export function DetailPanel() {
|
|||||||
[detail, locale],
|
[detail, locale],
|
||||||
);
|
);
|
||||||
const scenery = getCityScenery(detail?.name);
|
const scenery = getCityScenery(detail?.name);
|
||||||
|
const weatherSummary = detail
|
||||||
|
? getWeatherSummary(detail, locale)
|
||||||
|
: { weatherIcon: "", weatherText: "" };
|
||||||
const basicCurrentTemp = selectedSummary?.current?.temp;
|
const basicCurrentTemp = selectedSummary?.current?.temp;
|
||||||
const basicObsTime = selectedSummary?.current?.obs_time;
|
const basicObsTime = selectedSummary?.current?.obs_time;
|
||||||
const basicDeb = selectedSummary?.deb?.prediction;
|
const basicDeb = selectedSummary?.deb?.prediction;
|
||||||
@@ -200,11 +197,35 @@ export function DetailPanel() {
|
|||||||
selectedCityItem?.airport ||
|
selectedCityItem?.airport ||
|
||||||
selectedSummary?.icao ||
|
selectedSummary?.icao ||
|
||||||
(locale === "en-US" ? "Airport pending" : "机场待确认");
|
(locale === "en-US" ? "Airport pending" : "机场待确认");
|
||||||
const isBasicSummaryLoading =
|
const activeTempSymbol = detail?.temp_symbol || selectedSummary?.temp_symbol || "°C";
|
||||||
!detail && !selectedSummary && store.loadingState.cityDetail;
|
const heroCurrentTemp = detail?.current?.temp ?? basicCurrentTemp;
|
||||||
|
const heroCurrentTempText =
|
||||||
|
heroCurrentTemp != null ? `${heroCurrentTemp}${activeTempSymbol}` : "--";
|
||||||
|
const heroObsTimeLabel =
|
||||||
|
detail?.current?.obs_time ||
|
||||||
|
basicObsTime ||
|
||||||
|
(locale === "en-US" ? "Observation pending" : "观测时间待更新");
|
||||||
|
const heroDebValue = detail?.deb?.prediction ?? basicDeb;
|
||||||
|
const heroDebText =
|
||||||
|
heroDebValue != null
|
||||||
|
? `${heroDebValue}${activeTempSymbol}`
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Pending"
|
||||||
|
: "待更新";
|
||||||
|
const heroSettlementLabel =
|
||||||
|
detail?.current?.settlement_source_label || basicSettlementLabel;
|
||||||
|
const heroAirportLabel = detail?.risk?.airport || basicAirportLabel;
|
||||||
|
const heroWeatherText = detail
|
||||||
|
? weatherSummary.weatherText
|
||||||
|
: locale === "en-US"
|
||||||
|
? "City snapshot"
|
||||||
|
: "城市快照";
|
||||||
|
const heroWeatherIcon = detail ? weatherSummary.weatherIcon : "";
|
||||||
|
const isBasicSummaryLoading = !detail && !selectedSummary && store.loadingState.cityDetail;
|
||||||
const shouldShowSyncCard =
|
const shouldShowSyncCard =
|
||||||
!detail &&
|
!detail &&
|
||||||
(store.loadingState.cityDetail || isProStateLoading || isAuthenticated);
|
(store.loadingState.cityDetail || isProStateLoading || isAuthenticated);
|
||||||
|
|
||||||
const blurActiveElement = () => {
|
const blurActiveElement = () => {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
@@ -212,6 +233,7 @@ export function DetailPanel() {
|
|||||||
active.blur();
|
active.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFeatureAccess = (feature: "today" | "history") => {
|
const handleFeatureAccess = (feature: "today" | "history") => {
|
||||||
blurActiveElement();
|
blurActiveElement();
|
||||||
|
|
||||||
@@ -251,10 +273,7 @@ export function DetailPanel() {
|
|||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
panel.setAttribute("inert", "");
|
panel.setAttribute("inert", "");
|
||||||
if (
|
if (typeof document !== "undefined" && panel.contains(document.activeElement)) {
|
||||||
typeof document !== "undefined" &&
|
|
||||||
panel.contains(document.activeElement)
|
|
||||||
) {
|
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
if (active instanceof HTMLElement) {
|
if (active instanceof HTMLElement) {
|
||||||
active.blur();
|
active.blur();
|
||||||
@@ -303,10 +322,7 @@ export function DetailPanel() {
|
|||||||
}, [detail, isVisible]);
|
}, [detail, isVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside ref={panelRef} className={clsx("detail-panel", isVisible && "visible")}>
|
||||||
ref={panelRef}
|
|
||||||
className={clsx("detail-panel", isVisible && "visible")}
|
|
||||||
>
|
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -320,7 +336,23 @@ export function DetailPanel() {
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
<div className="panel-title-area">
|
<div className="panel-title-area">
|
||||||
<h2>{panelDisplayName.toUpperCase()}</h2>
|
<div className="panel-title-stack">
|
||||||
|
<div className="panel-overline">
|
||||||
|
<span>{locale === "en-US" ? "City briefing" : "城市简报"}</span>
|
||||||
|
<span className="panel-overline-sep">•</span>
|
||||||
|
<span>{panelDisplayName.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
<h2>{panelDisplayName}</h2>
|
||||||
|
<div className="panel-subtitle">
|
||||||
|
{detail
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Primary settlement posture and local evidence."
|
||||||
|
: "先看当前口径,再补结算证据与周边对照。"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Summary first. Deep detail fills in automatically."
|
||||||
|
: "基础快照先到,深度内容随后补齐。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{store.loadingState.cityDetail && (
|
{store.loadingState.cityDetail && (
|
||||||
<div className="panel-loading-hint" role="status" aria-live="polite">
|
<div className="panel-loading-hint" role="status" aria-live="polite">
|
||||||
<span className="panel-loading-spinner" aria-hidden="true" />
|
<span className="panel-loading-spinner" aria-hidden="true" />
|
||||||
@@ -335,161 +367,207 @@ export function DetailPanel() {
|
|||||||
<span className={clsx("risk-badge", panelRiskLevel)}>
|
<span className={clsx("risk-badge", panelRiskLevel)}>
|
||||||
{getRiskBadgeLabel(panelRiskLevel, locale)}
|
{getRiskBadgeLabel(panelRiskLevel, locale)}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative group">
|
<span className="panel-meta-chip panel-meta-chip-strong">{heroSettlementLabel}</span>
|
||||||
{marketUrl ? (
|
<span className="panel-meta-chip panel-meta-chip-muted">{heroAirportLabel}</span>
|
||||||
<a
|
</div>
|
||||||
className="history-btn"
|
<div className="panel-actions">
|
||||||
href={marketUrl}
|
{marketUrl ? (
|
||||||
target="_blank"
|
<a
|
||||||
rel="noreferrer"
|
className="panel-action-button panel-action-button-ghost"
|
||||||
title={
|
href={marketUrl}
|
||||||
locale === "en-US"
|
target="_blank"
|
||||||
? "Open today's Polymarket market"
|
rel="noreferrer"
|
||||||
: "打开今日 Polymarket 题目页"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{locale === "en-US" ? "Open Market" : "打开 Polymarket"}
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={clsx("history-btn", !isPro && "pro-locked")}
|
|
||||||
title={
|
title={
|
||||||
isPro
|
locale === "en-US"
|
||||||
? t("detail.todayAnalysis")
|
? "Open today's Polymarket market"
|
||||||
: `${t("detail.todayAnalysis")} (Pro)`
|
: "打开今日 Polymarket 题目页"
|
||||||
}
|
}
|
||||||
onClick={() => handleFeatureAccess("today")}
|
|
||||||
disabled={!store.selectedCity}
|
|
||||||
>
|
>
|
||||||
{isPro
|
{locale === "en-US" ? "Market" : "市场页"}
|
||||||
? t("detail.todayAnalysis")
|
</a>
|
||||||
: `${t("detail.todayAnalysis")} · Pro`}
|
) : null}
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={clsx(
|
||||||
className={clsx("history-btn", !isPro && "pro-locked")}
|
"panel-action-button",
|
||||||
title={
|
"panel-action-button-primary",
|
||||||
isPro ? t("detail.history") : `${t("detail.history")} (Pro)`
|
!isPro && "pro-locked",
|
||||||
}
|
)}
|
||||||
onClick={() => handleFeatureAccess("history")}
|
title={
|
||||||
disabled={!store.selectedCity}
|
isPro ? t("detail.todayAnalysis") : `${t("detail.todayAnalysis")} (Pro)`
|
||||||
>
|
}
|
||||||
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
|
onClick={() => handleFeatureAccess("today")}
|
||||||
</button>
|
disabled={!store.selectedCity}
|
||||||
</div>
|
>
|
||||||
|
{isPro ? t("detail.todayAnalysis") : `${t("detail.todayAnalysis")} · Pro`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clsx(
|
||||||
|
"panel-action-button",
|
||||||
|
"panel-action-button-secondary",
|
||||||
|
!isPro && "pro-locked",
|
||||||
|
)}
|
||||||
|
title={isPro ? t("detail.history") : `${t("detail.history")} (Pro)`}
|
||||||
|
onClick={() => handleFeatureAccess("history")}
|
||||||
|
disabled={!store.selectedCity}
|
||||||
|
>
|
||||||
|
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
{!hasBasicPanelContent ? (
|
{!hasBasicPanelContent ? (
|
||||||
<section>
|
<section className="detail-summary-shell detail-empty-state">
|
||||||
|
<div className="detail-section-head">
|
||||||
|
<div>
|
||||||
|
<div className="detail-section-kicker">
|
||||||
|
{locale === "en-US" ? "No city selected" : "尚未选择城市"}
|
||||||
|
</div>
|
||||||
|
<h3>{locale === "en-US" ? "Pick a city to start." : "先选择一个城市。"}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{ color: "var(--text-muted)", fontSize: "13px" }}>
|
<div style={{ color: "var(--text-muted)", fontSize: "13px" }}>
|
||||||
{store.loadingState.cityDetail
|
{store.loadingState.cityDetail ? t("detail.loading") : t("detail.emptyHint")}
|
||||||
? t("detail.loading")
|
|
||||||
: t("detail.emptyHint")}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : !detail ? (
|
) : !detail ? (
|
||||||
<>
|
<>
|
||||||
<section className="detail-section">
|
<section className="detail-summary-shell">
|
||||||
<h3>{locale === "en-US" ? "City Snapshot" : "城市概览"}</h3>
|
<div className="detail-section-head">
|
||||||
|
<div>
|
||||||
|
<div className="detail-section-kicker">
|
||||||
|
{locale === "en-US" ? "Quick read" : "基础快照"}
|
||||||
|
</div>
|
||||||
|
<h3>{locale === "en-US" ? "Summary before deep sync." : "先看基础结论,再等深度详情。"}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{isBasicSummaryLoading ? (
|
{isBasicSummaryLoading ? (
|
||||||
<div className="detail-mini-meta">
|
<div className="detail-mini-meta">
|
||||||
{locale === "en-US"
|
{locale === "en-US" ? "Syncing public city snapshot..." : "正在同步城市基础信息..."}
|
||||||
? "Syncing public city snapshot..."
|
|
||||||
: "正在同步城市基础信息..."}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="detail-grid">
|
<div className="detail-summary-main">
|
||||||
<div className="detail-card">
|
<article className="detail-card detail-card-hero">
|
||||||
<span className="detail-label">
|
<span className="detail-label">{locale === "en-US" ? "Current posture" : "当前态势"}</span>
|
||||||
{locale === "en-US" ? "Current temp" : "当前温度"}
|
<span className="detail-summary-temp">{heroCurrentTempText}</span>
|
||||||
</span>
|
<span className="detail-summary-supporting">@{heroObsTimeLabel}</span>
|
||||||
<span className="detail-value">
|
<span className="detail-summary-supporting">{heroWeatherText}</span>
|
||||||
{basicCurrentTemp != null
|
</article>
|
||||||
? `${basicCurrentTemp}${selectedSummary?.temp_symbol || ""}`
|
<div className="detail-summary-grid">
|
||||||
: locale === "en-US"
|
<article className="detail-card">
|
||||||
? "--"
|
<span className="detail-label">DEB</span>
|
||||||
: "--"}
|
<span className="detail-value">{heroDebText}</span>
|
||||||
</span>
|
</article>
|
||||||
</div>
|
<article className="detail-card">
|
||||||
<div className="detail-card">
|
<span className="detail-label">{locale === "en-US" ? "Settlement" : "结算口径"}</span>
|
||||||
<span className="detail-label">
|
<span className="detail-value">{heroSettlementLabel}</span>
|
||||||
{locale === "en-US" ? "Observed" : "观测时间"}
|
</article>
|
||||||
</span>
|
<article className="detail-card">
|
||||||
<span className="detail-value">
|
<span className="detail-label">{locale === "en-US" ? "Airport" : "结算机场"}</span>
|
||||||
{basicObsTime || (locale === "en-US" ? "Pending" : "待更新")}
|
<span className="detail-value">{heroAirportLabel}</span>
|
||||||
</span>
|
</article>
|
||||||
</div>
|
<article className="detail-card">
|
||||||
<div className="detail-card">
|
<span className="detail-label">{locale === "en-US" ? "Sync status" : "同步状态"}</span>
|
||||||
<span className="detail-label">DEB</span>
|
<span className="detail-value-muted">
|
||||||
<span className="detail-value">
|
{locale === "en-US" ? "Deep detail pending" : "深度详情补齐中"}
|
||||||
{basicDeb != null
|
</span>
|
||||||
? `${basicDeb}${selectedSummary?.temp_symbol || ""}`
|
</article>
|
||||||
: locale === "en-US"
|
|
||||||
? isBasicSummaryLoading
|
|
||||||
? "Syncing..."
|
|
||||||
: "Pending"
|
|
||||||
: isBasicSummaryLoading
|
|
||||||
? "同步中..."
|
|
||||||
: "待更新"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">
|
|
||||||
{locale === "en-US" ? "Settlement" : "结算口径"}
|
|
||||||
</span>
|
|
||||||
<span className="detail-value">{basicSettlementLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">
|
|
||||||
{locale === "en-US" ? "Airport" : "结算机场"}
|
|
||||||
</span>
|
|
||||||
<span className="detail-value">{basicAirportLabel}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="detail-section">
|
<section className="detail-summary-shell detail-summary-shell-live">
|
||||||
<div className="detail-card">
|
<div className="detail-section-head">
|
||||||
<span className="detail-label">
|
<div>
|
||||||
|
<div className="detail-section-kicker">
|
||||||
|
{shouldShowSyncCard
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Background sync"
|
||||||
|
: "后台同步"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Pro workflow"
|
||||||
|
: "Pro 工作流"}
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
{shouldShowSyncCard
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Deep evidence will appear automatically."
|
||||||
|
: "更深层的结算证据会自动补齐。"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Intraday and history live behind the analysis flow."
|
||||||
|
: "今日日内分析和历史对账会在分析流中继续展开。"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="detail-callout-card">
|
||||||
|
<span className="detail-value-muted">
|
||||||
{shouldShowSyncCard
|
{shouldShowSyncCard
|
||||||
? locale === "en-US"
|
? locale === "en-US"
|
||||||
? "Detail sync"
|
? "Full city detail is still syncing. Nearby stations, source links, and mini trend will land here once ready."
|
||||||
: "详情同步"
|
: "完整城市详情仍在同步中。周边站点、官方参考和温度微趋势会在完成后自动出现在这里。"
|
||||||
: locale === "en-US"
|
: locale === "en-US"
|
||||||
? "Pro features"
|
? "Intraday analysis, history reconciliation, and deeper structure signals stay available from the action bar."
|
||||||
: "Pro 功能"}
|
: "今日日内分析、历史对账和更深层结构信号会从上方操作区继续展开。"}
|
||||||
</span>
|
|
||||||
<span className="detail-value" style={{ fontSize: "15px" }}>
|
|
||||||
{shouldShowSyncCard
|
|
||||||
? locale === "en-US"
|
|
||||||
? "Full city detail is still syncing. The deeper panel will appear automatically."
|
|
||||||
: "完整城市详情仍在同步中,深度面板会自动补齐。"
|
|
||||||
: locale === "en-US"
|
|
||||||
? "Intraday analysis, history reconciliation, and deeper structure signals require Pro."
|
|
||||||
: "今日日内分析、历史对账和更深入的结构信号需要 Pro。"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<section className="detail-summary-shell">
|
||||||
|
<div className="detail-section-head">
|
||||||
|
<div>
|
||||||
|
<div className="detail-section-kicker">
|
||||||
|
{locale === "en-US" ? "Settlement snapshot" : "结算快照"}
|
||||||
|
</div>
|
||||||
|
<h3>{locale === "en-US" ? "Start from the active settlement posture." : "先看当前结算姿态,再判断偏差和风险。"}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="detail-summary-main">
|
||||||
|
<article className="detail-card detail-card-hero">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Current reading" : "当前读数"}</span>
|
||||||
|
<span className="detail-summary-temp">{heroCurrentTempText}</span>
|
||||||
|
<span className="detail-summary-supporting">@{heroObsTimeLabel}</span>
|
||||||
|
<span className="detail-summary-supporting">
|
||||||
|
{heroWeatherIcon ? `${heroWeatherIcon} ` : ""}
|
||||||
|
{heroWeatherText}
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
<div className="detail-summary-grid">
|
||||||
|
<article className="detail-card">
|
||||||
|
<span className="detail-label">DEB</span>
|
||||||
|
<span className="detail-value">{heroDebText}</span>
|
||||||
|
</article>
|
||||||
|
<article className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Settlement" : "结算口径"}</span>
|
||||||
|
<span className="detail-value">{heroSettlementLabel}</span>
|
||||||
|
</article>
|
||||||
|
<article className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Airport" : "结算机场"}</span>
|
||||||
|
<span className="detail-value">{heroAirportLabel}</span>
|
||||||
|
</article>
|
||||||
|
<article className="detail-card">
|
||||||
|
<span className="detail-label">{locale === "en-US" ? "Risk level" : "风险等级"}</span>
|
||||||
|
<span className="detail-value">{getRiskBadgeLabel(panelRiskLevel, locale)}</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="detail-scenery-card">
|
<section className="detail-scenery-card">
|
||||||
{scenery ? (
|
{scenery ? (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
className="detail-scenery-image"
|
className="detail-scenery-image"
|
||||||
src={scenery.imageUrl}
|
src={scenery.imageUrl}
|
||||||
alt={t("detail.sceneryAlt", { city: detail?.display_name || "" })}
|
alt={t("detail.sceneryAlt", { city: detail.display_name || "" })}
|
||||||
/>
|
/>
|
||||||
<div className="detail-scenery-overlay">
|
<div className="detail-scenery-overlay">
|
||||||
<div className="detail-scenery-copy">
|
<div className="detail-scenery-copy">
|
||||||
<span className="detail-scenery-kicker">
|
<span className="detail-scenery-kicker">{detail.display_name}</span>
|
||||||
{detail?.display_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
className="detail-scenery-credit"
|
className="detail-scenery-credit"
|
||||||
@@ -503,34 +581,40 @@ export function DetailPanel() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="detail-scenery-fallback">
|
<div className="detail-scenery-fallback">
|
||||||
<span className="detail-scenery-kicker">
|
<span className="detail-scenery-kicker">{detail.display_name}</span>
|
||||||
{detail?.display_name}
|
<strong className="detail-scenery-title">{t("detail.sceneryTitle")}</strong>
|
||||||
</span>
|
<span className="detail-scenery-subtitle">{t("detail.sceneryFallback")}</span>
|
||||||
<strong className="detail-scenery-title">
|
|
||||||
{t("detail.sceneryTitle")}
|
|
||||||
</strong>
|
|
||||||
<span className="detail-scenery-subtitle">
|
|
||||||
{t("detail.sceneryFallback")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="detail-section">
|
<section className="detail-structured-section">
|
||||||
<h3>{t("detail.profile")}</h3>
|
<div className="detail-section-head">
|
||||||
|
<div>
|
||||||
|
<div className="detail-section-kicker">{locale === "en-US" ? "Profile" : "城市画像"}</div>
|
||||||
|
<h3>{t("detail.profile")}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="detail-grid">
|
<div className="detail-grid">
|
||||||
{profileStats.map((item) => (
|
{profileStats.map((item) => (
|
||||||
<div key={item.label} className="detail-card">
|
<article key={item.label} className="detail-card">
|
||||||
<span className="detail-label">{item.label}</span>
|
<span className="detail-label">{item.label}</span>
|
||||||
<span className="detail-value">{item.value}</span>
|
<span className="detail-value">{item.value}</span>
|
||||||
</div>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{officialLinks.length > 0 ? (
|
{officialLinks.length > 0 ? (
|
||||||
<section className="detail-section">
|
<section className="detail-structured-section">
|
||||||
<h3>{locale === "en-US" ? "Official Sources" : "官方参考"}</h3>
|
<div className="detail-section-head">
|
||||||
|
<div>
|
||||||
|
<div className="detail-section-kicker">
|
||||||
|
{locale === "en-US" ? "Primary references" : "官方参考"}
|
||||||
|
</div>
|
||||||
|
<h3>{locale === "en-US" ? "Settlement and observation references" : "结算与观测参考来源"}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p className="detail-source-note">
|
<p className="detail-source-note">
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? "AGENCY = national meteorological service, METAR = airport observation, AIRPORT = airport official page."
|
? "AGENCY = national meteorological service, METAR = airport observation, AIRPORT = airport official page."
|
||||||
@@ -545,9 +629,7 @@ export function DetailPanel() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<span className="detail-source-kind">
|
<span className="detail-source-kind">{link.kind.toUpperCase()}</span>
|
||||||
{link.kind.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
<span className="detail-source-label">{link.label}</span>
|
<span className="detail-source-label">{link.label}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
@@ -555,10 +637,17 @@ export function DetailPanel() {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section className="detail-section rounded-2xl">
|
<section className="detail-structured-section rounded-2xl">
|
||||||
<h3>{t("detail.todayMiniTrend")}</h3>
|
<div className="detail-section-head">
|
||||||
|
<div>
|
||||||
|
<div className="detail-section-kicker">
|
||||||
|
{locale === "en-US" ? "Mini trend" : "温度微趋势"}
|
||||||
|
</div>
|
||||||
|
<h3>{t("detail.todayMiniTrend")}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{heavyContentReady ? (
|
{heavyContentReady ? (
|
||||||
<DetailMiniTemperatureChart detail={detail!} />
|
<DetailMiniTemperatureChart detail={detail} />
|
||||||
) : (
|
) : (
|
||||||
<div className="detail-mini-meta">{t("detail.loading")}</div>
|
<div className="detail-mini-meta">{t("detail.loading")}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
.root :global(.panel-title-stack) {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-overline) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-overline-sep) {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-subtitle) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-weather-chip) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-meta-chip) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-meta-chip-strong) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: rgba(34, 211, 238, 0.1);
|
||||||
|
border-color: rgba(34, 211, 238, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-meta-chip-muted) {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-actions) {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-action-button) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease, color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-action-button:hover) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-action-button-primary) {
|
||||||
|
color: #05111f;
|
||||||
|
background: linear-gradient(135deg, #67e8f9, #38bdf8);
|
||||||
|
border-color: rgba(103, 232, 249, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-action-button-secondary) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-action-button-ghost) {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
background: rgba(34, 211, 238, 0.08);
|
||||||
|
border-color: rgba(34, 211, 238, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-shell) {
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,0.035), rgba(255,255,255,0.018));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 18px 42px rgba(2, 6, 23, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-body section.detail-summary-shell) {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 18px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-shell-live) {
|
||||||
|
background: linear-gradient(180deg, rgba(22, 78, 99, 0.18), rgba(15, 23, 42, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-section-head) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-section-kicker) {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-main) {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-hero) {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-temp) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-supporting) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-summary-grid) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-card) {
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-card-hero) {
|
||||||
|
background: rgba(34, 211, 238, 0.08);
|
||||||
|
border-color: rgba(34, 211, 238, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-card-wide) {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-callout-card) {
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-value-muted) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-structured-section) {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.detail-empty-state) {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1155,58 +1155,74 @@ export function FutureForecastModal() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="modal-content large future-modal">
|
<div className="modal-content large future-modal">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h2
|
<div className="modal-title-stack">
|
||||||
id="future-modal-title"
|
<div className="modal-overline">
|
||||||
className="future-modal-title-with-actions"
|
<span>{locale === "en-US" ? "Analysis workspace" : "分析工作台"}</span>
|
||||||
>
|
<span className="modal-overline-sep">•</span>
|
||||||
<span>
|
<span>{detail.display_name.toUpperCase()}</span>
|
||||||
{isToday
|
</div>
|
||||||
? t("future.todayTitle", {
|
<h2
|
||||||
city: detail.display_name.toUpperCase(),
|
id="future-modal-title"
|
||||||
})
|
className="future-modal-title-with-actions"
|
||||||
: t("future.dateTitle", {
|
|
||||||
city: detail.display_name.toUpperCase(),
|
|
||||||
date: dateStr,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"future-refresh-btn",
|
|
||||||
isAnyLayerSyncing && "spinning",
|
|
||||||
)}
|
|
||||||
disabled={!isPro || isProLoading}
|
|
||||||
onClick={() => {
|
|
||||||
if (isToday) {
|
|
||||||
void store.openTodayModal(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
store.openFutureModal(dateStr, true);
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
!isPro
|
|
||||||
? locale === "en-US"
|
|
||||||
? "Pro subscription required"
|
|
||||||
: "需要 Pro 订阅"
|
|
||||||
: locale === "en-US"
|
|
||||||
? "Refresh Data"
|
|
||||||
: "刷新数据"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<svg
|
<span>
|
||||||
width="14"
|
{isToday
|
||||||
height="14"
|
? t("future.todayTitle", {
|
||||||
viewBox="0 0 24 24"
|
city: detail.display_name.toUpperCase(),
|
||||||
fill="none"
|
})
|
||||||
stroke="currentColor"
|
: t("future.dateTitle", {
|
||||||
strokeWidth="2.5"
|
city: detail.display_name.toUpperCase(),
|
||||||
strokeLinecap="round"
|
date: dateStr,
|
||||||
strokeLinejoin="round"
|
})}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"future-refresh-btn",
|
||||||
|
isAnyLayerSyncing && "spinning",
|
||||||
|
)}
|
||||||
|
disabled={!isPro || isProLoading}
|
||||||
|
onClick={() => {
|
||||||
|
if (isToday) {
|
||||||
|
void store.openTodayModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.openFutureModal(dateStr, true);
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
!isPro
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Pro subscription required"
|
||||||
|
: "需要 Pro 订阅"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Refresh Data"
|
||||||
|
: "刷新数据"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
<svg
|
||||||
<path d="M3 3v5h5" />
|
width="14"
|
||||||
</svg>
|
height="14"
|
||||||
</button>
|
viewBox="0 0 24 24"
|
||||||
</h2>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||||
|
<path d="M3 3v5h5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div className="modal-subtitle">
|
||||||
|
{isToday
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Base signal first, market and deep structure follow."
|
||||||
|
: "先看基础信号,市场层和深度结构随后补齐。"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Forward date view with phased model and structure sync."
|
||||||
|
: "未来日期视图,模型层与结构层分阶段补齐。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="modal-close"
|
className="modal-close"
|
||||||
@@ -1237,18 +1253,7 @@ export function FutureForecastModal() {
|
|||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
{isNoaaSettlement && (
|
{isNoaaSettlement && (
|
||||||
<div
|
<div className="modal-callout modal-callout-info">
|
||||||
style={{
|
|
||||||
marginBottom: "16px",
|
|
||||||
padding: "12px 14px",
|
|
||||||
border: "1px solid rgba(56, 189, 248, 0.24)",
|
|
||||||
borderRadius: "12px",
|
|
||||||
background: "rgba(14, 165, 233, 0.08)",
|
|
||||||
color: "var(--text-secondary)",
|
|
||||||
fontSize: "13px",
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? `${detail.display_name} now settles against NOAA ${noaaStationCode} (${noaaStationName}). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized.`
|
? `${detail.display_name} now settles against NOAA ${noaaStationCode} (${noaaStationName}). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized.`
|
||||||
: `${detail.display_name} 当前按 NOAA ${noaaStationCode}(${noaaStationName})结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。`}
|
: `${detail.display_name} 当前按 NOAA ${noaaStationCode}(${noaaStationName})结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。`}
|
||||||
@@ -1491,19 +1496,29 @@ export function FutureForecastModal() {
|
|||||||
|
|
||||||
<main className="future-v2-right">
|
<main className="future-v2-right">
|
||||||
<section className="future-modal-section future-v2-main-chart">
|
<section className="future-modal-section future-v2-main-chart">
|
||||||
<h3>
|
<div className="modal-section-heading">
|
||||||
{locale === "en-US"
|
<div className="modal-section-kicker">
|
||||||
? "Today's temperature forecast (obs + market)"
|
{locale === "en-US" ? "Primary view" : "主视图"}
|
||||||
: "今日气温预测(观测 + 市场)"}
|
</div>
|
||||||
</h3>
|
<h3>
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Today's temperature forecast (obs + market)"
|
||||||
|
: "今日气温预测(观测 + 市场)"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<DailyTemperatureChart dateStr={dateStr} />
|
<DailyTemperatureChart dateStr={dateStr} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="future-modal-grid">
|
<div className="future-modal-grid">
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
<h3>
|
<div className="modal-section-heading">
|
||||||
{locale === "en-US" ? "Current Hit Odds" : "当前命中胜率"}
|
<div className="modal-section-kicker">
|
||||||
</h3>
|
{locale === "en-US" ? "Probability layer" : "概率层"}
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
{locale === "en-US" ? "Current Hit Odds" : "当前命中胜率"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<div className="future-text-block" style={{ marginBottom: "12px" }}>
|
<div className="future-text-block" style={{ marginBottom: "12px" }}>
|
||||||
{probabilitySummary}
|
{probabilitySummary}
|
||||||
</div>
|
</div>
|
||||||
@@ -1516,9 +1531,14 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
<h3>
|
<div className="modal-section-heading">
|
||||||
{locale === "en-US" ? "Model Range & Spread" : "模型区间与分歧"}
|
<div className="modal-section-kicker">
|
||||||
</h3>
|
{locale === "en-US" ? "Model layer" : "模型层"}
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
{locale === "en-US" ? "Model Range & Spread" : "模型区间与分歧"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<div className="future-text-block" style={{ marginBottom: "12px" }}>
|
<div className="future-text-block" style={{ marginBottom: "12px" }}>
|
||||||
{modelSummary}
|
{modelSummary}
|
||||||
</div>
|
</div>
|
||||||
@@ -1532,7 +1552,12 @@ export function FutureForecastModal() {
|
|||||||
|
|
||||||
{showDeferredTodaySections ? (
|
{showDeferredTodaySections ? (
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
<h3>{t("future.structureToday")}</h3>
|
<div className="modal-section-heading">
|
||||||
|
<div className="modal-section-kicker">
|
||||||
|
{locale === "en-US" ? "Structure layer" : "结构层"}
|
||||||
|
</div>
|
||||||
|
<h3>{t("future.structureToday")}</h3>
|
||||||
|
</div>
|
||||||
<div className="future-front-score">
|
<div className="future-front-score">
|
||||||
<div className="future-front-bar" style={barStyle}>
|
<div className="future-front-bar" style={barStyle}>
|
||||||
<div
|
<div
|
||||||
@@ -1691,7 +1716,12 @@ export function FutureForecastModal() {
|
|||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
<h3>{t("future.structureToday")}</h3>
|
<div className="modal-section-heading">
|
||||||
|
<div className="modal-section-kicker">
|
||||||
|
{locale === "en-US" ? "Structure layer" : "结构层"}
|
||||||
|
</div>
|
||||||
|
<h3>{t("future.structureToday")}</h3>
|
||||||
|
</div>
|
||||||
<div className="future-trend-summary future-trend-summary-muted">
|
<div className="future-trend-summary future-trend-summary-muted">
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? "Surface structure, upper-air diagnostics, and trade commentary are loading after the primary chart."
|
? "Surface structure, upper-air diagnostics, and trade commentary are loading after the primary chart."
|
||||||
|
|||||||
@@ -228,30 +228,40 @@ export function HistoryModal() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="modal-content history-modal">
|
<div className="modal-content history-modal">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h2 id="history-modal-title">
|
<div className="modal-title-stack">
|
||||||
{t("history.title", {
|
<div className="modal-overline">
|
||||||
city: store.selectedCity?.toUpperCase() || "",
|
<span>{locale === "en-US" ? "Audit workspace" : "对账工作台"}</span>
|
||||||
})}
|
<span className="modal-overline-sep">•</span>
|
||||||
</h2>
|
<span>{store.selectedCity?.toUpperCase() || ""}</span>
|
||||||
{meta?.mode === "preview" ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
fontSize: "12px",
|
|
||||||
marginLeft: "12px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isRecordsLoading
|
|
||||||
? locale === "en-US"
|
|
||||||
? "Loading full records in background..."
|
|
||||||
: "完整历史正在后台补齐..."
|
|
||||||
: meta.hasMore
|
|
||||||
? locale === "en-US"
|
|
||||||
? `Preview ${meta.previewCount}/${meta.fullCount}`
|
|
||||||
: `预览 ${meta.previewCount}/${meta.fullCount}`
|
|
||||||
: null}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<h2 id="history-modal-title">
|
||||||
|
{t("history.title", {
|
||||||
|
city: store.selectedCity?.toUpperCase() || "",
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
<div className="modal-subtitle">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Observed highs, DEB path, and historical baseline consistency."
|
||||||
|
: "查看实测最高温、DEB 路径与历史基线是否一致。"}
|
||||||
|
</div>
|
||||||
|
{meta?.mode === "preview" ? (
|
||||||
|
<div className="modal-header-meta">
|
||||||
|
<span className="modal-meta-pill">
|
||||||
|
{isRecordsLoading
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Full records syncing"
|
||||||
|
: "完整记录补齐中"
|
||||||
|
: meta.hasMore
|
||||||
|
? locale === "en-US"
|
||||||
|
? `Preview ${meta.previewCount}/${meta.fullCount}`
|
||||||
|
: `预览 ${meta.previewCount}/${meta.fullCount}`
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Full set loaded"
|
||||||
|
: "完整记录已到齐"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="modal-close"
|
className="modal-close"
|
||||||
@@ -263,18 +273,7 @@ export function HistoryModal() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
{isNoaaSettlement && (
|
{isNoaaSettlement && (
|
||||||
<div
|
<div className="modal-callout modal-callout-info">
|
||||||
style={{
|
|
||||||
marginBottom: "16px",
|
|
||||||
padding: "12px 14px",
|
|
||||||
border: "1px solid rgba(56, 189, 248, 0.24)",
|
|
||||||
borderRadius: "12px",
|
|
||||||
background: "rgba(14, 165, 233, 0.08)",
|
|
||||||
color: "var(--text-secondary)",
|
|
||||||
fontSize: "13px",
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("lang") === "en-US"
|
{t("lang") === "en-US"
|
||||||
? `${store.selectedDetail?.display_name || store.selectedCity || "This city"} historical actuals are aligned to NOAA ${noaaStationCode} (${noaaStationName}) settlement rules: use the highest rounded whole-degree Celsius reading after the date is finalized.`
|
? `${store.selectedDetail?.display_name || store.selectedCity || "This city"} historical actuals are aligned to NOAA ${noaaStationCode} (${noaaStationName}) settlement rules: use the highest rounded whole-degree Celsius reading after the date is finalized.`
|
||||||
: `${store.selectedDetail?.display_name || store.selectedCity || "该城市"}历史对账已按 NOAA ${noaaStationCode}(${noaaStationName})结算口径对齐:采用该日最终完成质控后的最高整度摄氏值。`}
|
: `${store.selectedDetail?.display_name || store.selectedCity || "该城市"}历史对账已按 NOAA ${noaaStationCode}(${noaaStationName})结算口径对齐:采用该日最终完成质控后的最高整度摄氏值。`}
|
||||||
@@ -316,6 +315,21 @@ export function HistoryModal() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<div className="modal-section-heading">
|
||||||
|
<div className="modal-section-kicker">
|
||||||
|
{locale === "en-US" ? "Performance snapshot" : "表现快照"}
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Recent settlement performance"
|
||||||
|
: "近期结算表现"}
|
||||||
|
</h3>
|
||||||
|
<div className="modal-section-note">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Hit rate, MAE, and baseline comparison across recent settled days."
|
||||||
|
: "查看近期已结算样本里的命中率、误差与基线对照。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="history-stats">
|
<div className="history-stats">
|
||||||
{error ? (
|
{error ? (
|
||||||
<span style={{ color: "var(--accent-red)" }}>
|
<span style={{ color: "var(--accent-red)" }}>
|
||||||
@@ -371,10 +385,20 @@ export function HistoryModal() {
|
|||||||
{!error && <HistoryChart />}
|
{!error && <HistoryChart />}
|
||||||
{!error && settledPeakRows.length > 0 && (
|
{!error && settledPeakRows.length > 0 && (
|
||||||
<div className="history-peak-reference">
|
<div className="history-peak-reference">
|
||||||
<div className="history-peak-reference-title">
|
<div className="modal-section-heading">
|
||||||
{locale === "en-US"
|
<div className="modal-section-kicker">
|
||||||
? "Peak-12h DEB Reference (Approx.)"
|
{locale === "en-US" ? "Reference table" : "参考表"}
|
||||||
: "峰值前 12 小时 DEB 参考(近似)"}
|
</div>
|
||||||
|
<div className="history-peak-reference-title">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Peak-12h DEB Reference (Approx.)"
|
||||||
|
: "峰值前 12 小时 DEB 参考(近似)"}
|
||||||
|
</div>
|
||||||
|
<div className="modal-section-note">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Use peak-minus-12h DEB as a fast sanity check for settled highs."
|
||||||
|
: "用峰值前 12 小时的 DEB 作为历史结算高温的快速校验。"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="history-peak-reference-scroll">
|
<div className="history-peak-reference-scroll">
|
||||||
{settledPeakRows.map((row) => (
|
{settledPeakRows.map((row) => (
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
.root :global(.modal-title-stack) {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-overline) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-overline-sep) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-subtitle) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-header-meta) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-meta-pill) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(34, 211, 238, 0.08);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-callout) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-callout-info) {
|
||||||
|
border-color: rgba(56, 189, 248, 0.24);
|
||||||
|
background: rgba(14, 165, 233, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-section-heading) {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-section-kicker) {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-section-note) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import clsx from "clsx";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import styles from "./Dashboard.module.css";
|
import styles from "./Dashboard.module.css";
|
||||||
|
import detailChromeStyles from "./DetailPanelChrome.module.css";
|
||||||
|
import modalChromeStyles from "./ModalChrome.module.css";
|
||||||
import {
|
import {
|
||||||
DashboardStoreProvider,
|
DashboardStoreProvider,
|
||||||
useDashboardStore,
|
useDashboardStore,
|
||||||
@@ -92,7 +95,13 @@ function DashboardScreen() {
|
|||||||
!activeSummary;
|
!activeSummary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.root,
|
||||||
|
detailChromeStyles.root,
|
||||||
|
modalChromeStyles.root,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<MapCanvas />
|
<MapCanvas />
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<CitySidebar />
|
<CitySidebar />
|
||||||
|
|||||||
Reference in New Issue
Block a user