Show forecast completion state in dashboard panels
This commit is contained in:
@@ -1245,9 +1245,30 @@
|
||||
}
|
||||
|
||||
.root :global(.forecast-inline-note) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(34, 211, 238, 0.18);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(34, 211, 238, 0.06);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.root :global(.forecast-inline-note::before) {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
background: #22d3ee;
|
||||
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.12);
|
||||
animation: pulseGlow 1.25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.root :global(.forecast-day) {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border-subtle);
|
||||
@@ -1287,6 +1308,28 @@
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.root :global(.forecast-day-sync) {
|
||||
cursor: wait;
|
||||
opacity: 0.76;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(15, 23, 42, 0.86), rgba(30, 41, 59, 0.72), rgba(15, 23, 42, 0.86));
|
||||
background-size: 220% 100%;
|
||||
animation: panelSkeletonSweep 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.root :global(.forecast-day-sync .f-temp) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@keyframes panelSkeletonSweep {
|
||||
0% {
|
||||
background-position: 120% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -120% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.root :global(.forecast-table) {
|
||||
display: flex;
|
||||
|
||||
@@ -190,6 +190,12 @@ export function DetailPanel() {
|
||||
const heroSettlementLabel =
|
||||
detail?.current?.settlement_source_label || basicSettlementLabel;
|
||||
const heroAirportLabel = detail?.risk?.airport || basicAirportLabel;
|
||||
const isDetailCompleting = Boolean(
|
||||
detail &&
|
||||
(detail.detail_depth !== "full" ||
|
||||
(detail.forecast?.daily?.length ?? 0) <= 1),
|
||||
);
|
||||
const isPanelSyncing = store.loadingState.cityDetail || isDetailCompleting;
|
||||
|
||||
const blurActiveElement = () => {
|
||||
if (typeof document === "undefined") return;
|
||||
@@ -309,13 +315,17 @@ export function DetailPanel() {
|
||||
</div>
|
||||
<h2>{panelDisplayName}</h2>
|
||||
</div>
|
||||
{store.loadingState.cityDetail && (
|
||||
{isPanelSyncing && (
|
||||
<div className="panel-loading-hint" role="status" aria-live="polite">
|
||||
<span className="panel-loading-spinner" aria-hidden="true" />
|
||||
<span>
|
||||
{locale === "en-US"
|
||||
? `Syncing ${panelDisplayName}...`
|
||||
: `正在同步 ${panelDisplayName}...`}
|
||||
? isDetailCompleting
|
||||
? `Completing ${panelDisplayName} cards...`
|
||||
: `Syncing ${panelDisplayName}...`
|
||||
: isDetailCompleting
|
||||
? `正在补齐 ${panelDisplayName} 卡片...`
|
||||
: `正在同步 ${panelDisplayName}...`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -993,11 +993,15 @@ export function ModelForecast({
|
||||
export function ForecastTable() {
|
||||
const store = useDashboardStore();
|
||||
const { data } = useCityData();
|
||||
const { t } = useI18n();
|
||||
const { locale, t } = useI18n();
|
||||
if (!data) return null;
|
||||
|
||||
const daily = data.forecast?.daily || [];
|
||||
const isSparseDaily = daily.length <= 1;
|
||||
const isForecastCompleting =
|
||||
store.loadingState.cityDetail ||
|
||||
data.detail_depth !== "full" ||
|
||||
isSparseDaily;
|
||||
const resolveForecastTemp = (date: string, fallback: number | null | undefined) => {
|
||||
const debPrediction = data.multi_model_daily?.[date]?.deb?.prediction;
|
||||
return debPrediction ?? fallback ?? null;
|
||||
@@ -1006,17 +1010,14 @@ export function ForecastTable() {
|
||||
<section className="forecast-section">
|
||||
<h3>{t("forecast.title")}</h3>
|
||||
{isSparseDaily && (
|
||||
<div
|
||||
className="forecast-inline-note"
|
||||
style={{
|
||||
color: "var(--text-secondary)",
|
||||
fontSize: "12px",
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
{store.loadingState.cityDetail
|
||||
? "多日预报同步中,正在刷新完整日序列。"
|
||||
: "当前只收到当日预报,其他日期结果暂未回传。"}
|
||||
<div className="forecast-inline-note">
|
||||
{isForecastCompleting
|
||||
? locale === "en-US"
|
||||
? "Multi-day forecast is syncing. Only the current-day card has arrived."
|
||||
: "多日预报同步中,当前只到达当日卡片。"
|
||||
: locale === "en-US"
|
||||
? "Only the current-day forecast is available right now."
|
||||
: "当前只收到当日预报,其他日期结果暂未回传。"}
|
||||
</div>
|
||||
)}
|
||||
<div className="forecast-table">
|
||||
@@ -1062,7 +1063,23 @@ export function ForecastTable() {
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
}).concat(
|
||||
isForecastCompleting
|
||||
? Array.from({ length: Math.max(0, 5 - daily.length) }).map((_, index) => (
|
||||
<button
|
||||
key={`forecast-sync-${index}`}
|
||||
type="button"
|
||||
className="forecast-day forecast-day-sync"
|
||||
disabled
|
||||
>
|
||||
<div className="f-date">
|
||||
{locale === "en-US" ? "Syncing" : "同步中"}
|
||||
</div>
|
||||
<div className="f-temp">--</div>
|
||||
</button>
|
||||
))
|
||||
: [],
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user