246 lines
7.4 KiB
TypeScript
246 lines
7.4 KiB
TypeScript
import clsx from "clsx";
|
|
import type { CSSProperties } from "react";
|
|
import type { getTodayPaceView } from "@/lib/pace-utils";
|
|
import { WeatherIcon } from "./FutureForecastModalWeatherIcon";
|
|
|
|
type Locale = string;
|
|
type TodayPaceView = NonNullable<ReturnType<typeof getTodayPaceView>>;
|
|
|
|
type WeatherSummaryView = {
|
|
weatherIcon: string;
|
|
weatherText: string;
|
|
};
|
|
|
|
type DaylightProgressView = {
|
|
phase: string;
|
|
percent: number;
|
|
};
|
|
|
|
export type FuturePaceSignalItem = {
|
|
label: string;
|
|
tone: "cyan" | "blue" | "amber";
|
|
status: string;
|
|
value: string;
|
|
note: string;
|
|
};
|
|
|
|
export function FutureAnchorStatusCard({
|
|
locale,
|
|
currentTempText,
|
|
weatherSummary,
|
|
obsTime,
|
|
topObservedTemp,
|
|
tempSymbol,
|
|
gapToBaseBucket,
|
|
pathStatus,
|
|
}: {
|
|
locale: Locale;
|
|
currentTempText: string;
|
|
weatherSummary: WeatherSummaryView;
|
|
obsTime?: string | null;
|
|
topObservedTemp: number | string | null | undefined;
|
|
tempSymbol: string;
|
|
gapToBaseBucket: number | null;
|
|
pathStatus: string;
|
|
}) {
|
|
const observedHigh = topObservedTemp ?? "--";
|
|
|
|
return (
|
|
<section className="future-v2-card future-v2-hero-card">
|
|
<div className="future-v2-card-head">
|
|
<h3 className="future-v2-hero-title">
|
|
{locale === "en-US" ? "Anchor Status" : "锚点状态"}
|
|
</h3>
|
|
<div className="future-v2-card-kicker">
|
|
{locale === "en-US"
|
|
? "Settlement anchor and current clock"
|
|
: "结算锚点与当前时钟"}
|
|
</div>
|
|
</div>
|
|
<div className="future-v2-hero-main">
|
|
<div className="future-v2-hero-temp">{currentTempText}</div>
|
|
<div className="future-v2-hero-divider" />
|
|
<div className="future-v2-hero-weather">
|
|
<span className="future-v2-hero-icon">
|
|
<WeatherIcon emoji={weatherSummary.weatherIcon} size={42} />
|
|
</span>
|
|
<span>{weatherSummary.weatherText}</span>
|
|
</div>
|
|
</div>
|
|
<div className="future-v2-hero-obs">@{obsTime || "--"}</div>
|
|
<div className="future-v2-mini-grid">
|
|
<FutureMiniMetric
|
|
label={locale === "en-US" ? "High so far" : "日内已见高点"}
|
|
value={`${observedHigh}${tempSymbol}`}
|
|
/>
|
|
<FutureMiniMetric
|
|
label={locale === "en-US" ? "Anchor clock" : "锚点时钟"}
|
|
value={obsTime || "--"}
|
|
/>
|
|
<FutureMiniMetric
|
|
label={locale === "en-US" ? "Gap to base" : "距基准档"}
|
|
value={
|
|
gapToBaseBucket != null
|
|
? `${gapToBaseBucket.toFixed(1)}${tempSymbol}`
|
|
: "--"
|
|
}
|
|
/>
|
|
<FutureMiniMetric
|
|
label={locale === "en-US" ? "Path state" : "路径状态"}
|
|
value={pathStatus}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function FuturePaceCard({
|
|
locale,
|
|
paceView,
|
|
tempSymbol,
|
|
signalItems,
|
|
}: {
|
|
locale: Locale;
|
|
paceView: TodayPaceView;
|
|
tempSymbol: string;
|
|
signalItems: FuturePaceSignalItem[];
|
|
}) {
|
|
return (
|
|
<section className="future-v2-card future-v2-pace-card future-v2-focus-card">
|
|
<div className="future-v2-card-head">
|
|
<h4 className="future-v2-card-title">
|
|
{locale === "en-US" ? "Current Pace" : "当前节奏"}
|
|
</h4>
|
|
<div className="future-v2-card-kicker">
|
|
{locale === "en-US"
|
|
? "Expected now vs airport anchor"
|
|
: "此刻应到 vs 机场锚点"}
|
|
</div>
|
|
</div>
|
|
<div className="future-v2-pace-head">
|
|
<span className="future-v2-pace-kicker">{paceView.kicker}</span>
|
|
<FutureSignalTag tone={paceView.biasTone}>{paceView.badge}</FutureSignalTag>
|
|
</div>
|
|
<div
|
|
className={clsx(
|
|
"future-v2-pace-delta",
|
|
paceView.biasTone === "cold" && "cold",
|
|
paceView.biasTone === "neutral" && "neutral",
|
|
paceView.biasTone === "warm" && "warm",
|
|
)}
|
|
>
|
|
{paceView.deltaText}
|
|
</div>
|
|
<div className="future-v2-pace-summary">{paceView.summary}</div>
|
|
<div className="future-v2-pace-meter">
|
|
<span className="future-v2-pace-meter-midline" />
|
|
<span
|
|
className={clsx(
|
|
"future-v2-pace-meter-fill",
|
|
paceView.biasTone === "cold" && "cold",
|
|
paceView.biasTone === "neutral" && "neutral",
|
|
paceView.biasTone === "warm" && "warm",
|
|
)}
|
|
style={
|
|
{
|
|
"--pace-left": `${paceView.meterLeft}%`,
|
|
"--pace-width": `${paceView.meterWidth}%`,
|
|
} as CSSProperties & {
|
|
"--pace-left": string;
|
|
"--pace-width": string;
|
|
}
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="future-v2-mini-grid future-v2-mini-grid-tight">
|
|
<FutureMiniMetric
|
|
label={locale === "en-US" ? "Expected now" : "预期此刻"}
|
|
value={`${paceView.expectedNow.toFixed(1)}${tempSymbol}`}
|
|
/>
|
|
<FutureMiniMetric
|
|
label={paceView.observedLabel}
|
|
value={`${paceView.observedNow.toFixed(1)}${tempSymbol}`}
|
|
/>
|
|
<FutureMiniMetric
|
|
label={paceView.paceAdjustedLabel}
|
|
value={
|
|
paceView.paceAdjustedHigh != null
|
|
? `${paceView.paceAdjustedHigh.toFixed(1)}${tempSymbol}`
|
|
: "--"
|
|
}
|
|
/>
|
|
<FutureMiniMetric
|
|
label={locale === "en-US" ? "Peak window" : "峰值窗口"}
|
|
value={paceView.peakWindowText}
|
|
/>
|
|
</div>
|
|
{signalItems.length ? (
|
|
<div className="future-v2-pace-signal-grid">
|
|
{signalItems.map((item) => (
|
|
<div key={item.label} className="future-v2-pace-signal-card">
|
|
<div className="future-v2-signal-head">
|
|
<span>{item.label}</span>
|
|
<FutureSignalTag tone={item.tone}>{item.status}</FutureSignalTag>
|
|
</div>
|
|
<strong>{item.value}</strong>
|
|
<div className="future-v2-pace-signal-note">{item.note}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function FuturePaceLoadingCard({ locale }: { locale: Locale }) {
|
|
return (
|
|
<section className="future-v2-card future-v2-support-card">
|
|
<div className="future-v2-card-head">
|
|
<h4 className="future-v2-card-title">
|
|
{locale === "en-US" ? "Current Pace" : "当前节奏"}
|
|
</h4>
|
|
<div className="future-v2-card-kicker">
|
|
{locale === "en-US"
|
|
? "Backfilling intraday pace context"
|
|
: "正在补齐日内节奏上下文"}
|
|
</div>
|
|
</div>
|
|
<div className="future-trend-summary future-trend-summary-muted">
|
|
{locale === "en-US"
|
|
? "Expected-now pace, boundary risk, and airport-vs-network cues are loading in the background."
|
|
: "预期此刻节奏、边界风险和机场对比站网信号正在后台补齐。"}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function FutureMiniMetric({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="future-v2-mini-item">
|
|
<span>{label}</span>
|
|
<strong>{value}</strong>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FutureSignalTag({
|
|
tone,
|
|
children,
|
|
}: {
|
|
tone: string;
|
|
children: string;
|
|
}) {
|
|
return (
|
|
<em
|
|
className={clsx(
|
|
"future-v2-signal-tag",
|
|
(tone === "cold" || tone === "cyan") && "cyan",
|
|
(tone === "neutral" || tone === "blue") && "blue",
|
|
(tone === "warm" || tone === "amber") && "amber",
|
|
)}
|
|
>
|
|
{children}
|
|
</em>
|
|
);
|
|
}
|