Add bilingual docs center and replace the guide modal

This commit is contained in:
2569718930@qq.com
2026-03-25 15:55:55 +08:00
parent b866ebf6e3
commit 4e0ddfa771
16 changed files with 1229 additions and 137 deletions
@@ -136,8 +136,7 @@ export function DetailPanel() {
const [heavyContentReady, setHeavyContentReady] = useState(false);
const isOverlayOpen =
Boolean(store.futureModalDate) ||
store.historyState.isOpen ||
store.isGuideOpen;
store.historyState.isOpen;
const isVisible =
store.isPanelOpen &&
Boolean(store.selectedCity) &&
@@ -1,105 +0,0 @@
"use client";
import { useDashboardStore } from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
const GUIDE_CARDS = {
"zh-CN": [
{
body: "Dynamic Ensemble Blending 是系统的核心预测层。它不是对 ECMWF、GFS、ICON、GEM、JMA 等模型的简单平均,而是结合近期样本表现、当前实况与城市偏置后得到的动态加权结果。",
title: "DEB 动态融合预测",
},
{
body: "右侧的结算概率分布基于 DEB 预测值与多模型离散度动态计算。μ 代表当前分布中心,会随着模型、实况和时间变化而变化,不是固定结算值。",
title: "结算概率引擎",
},
{
body: "结算源按城市市场定义:米兰(LIMC)、华沙(EPWA)、马德里(LEMD)使用机场 METAR;香港市场使用香港天文台(HKO);台北市场使用交通部中央气象署(CWA)。系统仍会保留 METAR/MGM 作为临近结构参考,并区分观测时间与接收时间。",
title: "结算点与主观测源",
},
{
body: "Ankara 不走通用城市逻辑。结算主站以 LTAC / Esenboğa 为准,周边领先信号优先参考 Turkish MGM 站网,其中 Ankara (Bölge/Center) 是重点监控站,不用 Etimesgut 代替。",
title: "Ankara 专属增强",
},
{
body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主;0-2 小时临近判断优先看 METAR 与周边站。",
title: "未来日期分析",
},
{
body: "历史准确率对账只统计已结算样本。网页端采用近 15 天滚动视图,不把当天尚未结算的样本算入胜率和 MAE。",
title: "历史对账规则",
},
],
"en-US": [
{
body: "Dynamic Ensemble Blending (DEB) is the core prediction layer. It is not a simple average across ECMWF/GFS/ICON/GEM/JMA, but a dynamically weighted blend adjusted by recent model performance, current observations, and city bias.",
title: "DEB Dynamic Fusion",
},
{
body: "Settlement probability distribution is dynamically computed from DEB forecast and model spread. μ is the current distribution center and shifts with model updates, observations, and time.",
title: "Settlement Probability Engine",
},
{
body: "Settlement source follows market rule by city: Milan (LIMC), Warsaw (EPWA), and Madrid (LEMD) settle on airport METAR, Hong Kong settles on HKO, and Taipei settles on CWA. METAR/MGM are still kept for intraday structure tracking with observation time vs receipt time separated.",
title: "Settlement Source Logic",
},
{
body: "Ankara does not follow the generic city path. LTAC / Esenboğa is the settlement station, with Turkish MGM network for leading signals. Ankara (Bölge/Center) is a key station and is not replaced by Etimesgut.",
title: "Ankara-specific Enhancement",
},
{
body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo; 0-2h nowcast prioritizes METAR and nearby stations.",
title: "Future-date Analysis",
},
{
body: "History reconciliation only uses settled samples. The web dashboard uses a rolling 15-day window and excludes same-day unsettled samples from hit-rate and MAE.",
title: "History Rules",
},
],
} as const;
export function GuideModal() {
const store = useDashboardStore();
const { locale, t } = useI18n();
if (!store.isGuideOpen) return null;
return (
<div
className="modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="guide-modal-title"
onClick={(event) => {
if (event.target === event.currentTarget) {
store.closeGuide();
}
}}
>
<div className="modal-content large">
<div className="modal-header">
<h2 id="guide-modal-title">{t("guide.title")}</h2>
<button
type="button"
className="modal-close"
aria-label={t("guide.closeAria")}
onClick={store.closeGuide}
>
×
</button>
</div>
<div className="modal-body">
<div className="guide-grid">
{GUIDE_CARDS[locale].map((card) => (
<div key={card.title} className="guide-card">
<h3>{card.title}</h3>
<p>{card.body}</p>
</div>
))}
</div>
<div className="guide-footer">{t("guide.footer")}</div>
</div>
</div>
</div>
);
}
+17 -5
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import clsx from "clsx";
import { LogIn, UserRound } from "lucide-react";
import { useDashboardStore } from "@/hooks/useDashboardStore";
@@ -14,8 +15,11 @@ import {
export function HeaderBar() {
const store = useDashboardStore();
const { locale, setLocale, t } = useI18n();
const pathname = usePathname();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const supabaseReady = hasSupabasePublicEnv();
const docsHref = "/docs/intro";
const docsActive = pathname?.startsWith("/docs");
useEffect(() => {
let mounted = true;
@@ -78,6 +82,15 @@ export function HeaderBar() {
</button>
</div>
<Link
href={docsHref}
className={clsx("info-btn", docsActive && "active")}
title={t("header.docsAria")}
aria-label={t("header.docsAria")}
>
{t("header.docs")}
</Link>
<Link
href={accountHref}
className="account-btn"
@@ -88,15 +101,14 @@ export function HeaderBar() {
<span>{accountLabel}</span>
</Link>
<button
type="button"
<Link
href={docsHref}
className="info-btn"
title={t("header.infoAria")}
aria-label={t("header.infoAria")}
onClick={store.openGuide}
>
{t("header.info")}
</button>
</Link>
<div className="live-badge" id="liveBadge">
<span className="pulse-dot" />
@@ -115,4 +127,4 @@ export function HeaderBar() {
</div>
</header>
);
}
}
+1 -2
View File
@@ -21,8 +21,7 @@ export function MapCanvas() {
selectedDetail: store.selectedDetail,
suspendMotion:
Boolean(store.futureModalDate) ||
store.historyState.isOpen ||
store.isGuideOpen,
store.historyState.isOpen,
isLoadingDetail: store.loadingState.cityDetail,
});
@@ -21,15 +21,6 @@ const MapCanvas = dynamic(
},
);
const GuideModal = dynamic(
() =>
import("@/components/dashboard/GuideModal").then((module) => module.GuideModal),
{
ssr: false,
loading: () => null,
},
);
const HistoryModal = dynamic(
() =>
import("@/components/dashboard/HistoryModal").then(
@@ -57,7 +48,6 @@ function DashboardScreen() {
const { t } = useI18n();
useEffect(() => {
void import("@/components/dashboard/GuideModal");
void import("@/components/dashboard/HistoryModal");
void import("@/components/dashboard/FutureForecastModal");
}, []);
@@ -73,10 +63,6 @@ function DashboardScreen() {
store.closeHistory();
return;
}
if (store.isGuideOpen) {
store.closeGuide();
return;
}
if (store.isPanelOpen) {
store.closePanel();
}
@@ -100,7 +86,6 @@ function DashboardScreen() {
<HeaderBar />
<CitySidebar />
<DetailPanel />
{store.isGuideOpen && <GuideModal />}
{store.historyState.isOpen && <HistoryModal />}
{store.futureModalDate && <FutureForecastModal />}
{showLoading && (