Show forecast completion state in dashboard panels
This commit is contained in:
@@ -1245,9 +1245,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.forecast-inline-note) {
|
.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;
|
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) {
|
.root :global(.forecast-day) {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
@@ -1287,6 +1308,28 @@
|
|||||||
color: var(--accent-cyan);
|
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) {
|
@media (max-width: 720px) {
|
||||||
.root :global(.forecast-table) {
|
.root :global(.forecast-table) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -190,6 +190,12 @@ export function DetailPanel() {
|
|||||||
const heroSettlementLabel =
|
const heroSettlementLabel =
|
||||||
detail?.current?.settlement_source_label || basicSettlementLabel;
|
detail?.current?.settlement_source_label || basicSettlementLabel;
|
||||||
const heroAirportLabel = detail?.risk?.airport || basicAirportLabel;
|
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 = () => {
|
const blurActiveElement = () => {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
@@ -309,13 +315,17 @@ export function DetailPanel() {
|
|||||||
</div>
|
</div>
|
||||||
<h2>{panelDisplayName}</h2>
|
<h2>{panelDisplayName}</h2>
|
||||||
</div>
|
</div>
|
||||||
{store.loadingState.cityDetail && (
|
{isPanelSyncing && (
|
||||||
<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" />
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? `Syncing ${panelDisplayName}...`
|
? isDetailCompleting
|
||||||
: `正在同步 ${panelDisplayName}...`}
|
? `Completing ${panelDisplayName} cards...`
|
||||||
|
: `Syncing ${panelDisplayName}...`
|
||||||
|
: isDetailCompleting
|
||||||
|
? `正在补齐 ${panelDisplayName} 卡片...`
|
||||||
|
: `正在同步 ${panelDisplayName}...`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -993,11 +993,15 @@ export function ModelForecast({
|
|||||||
export function ForecastTable() {
|
export function ForecastTable() {
|
||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { data } = useCityData();
|
const { data } = useCityData();
|
||||||
const { t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const daily = data.forecast?.daily || [];
|
const daily = data.forecast?.daily || [];
|
||||||
const isSparseDaily = daily.length <= 1;
|
const isSparseDaily = daily.length <= 1;
|
||||||
|
const isForecastCompleting =
|
||||||
|
store.loadingState.cityDetail ||
|
||||||
|
data.detail_depth !== "full" ||
|
||||||
|
isSparseDaily;
|
||||||
const resolveForecastTemp = (date: string, fallback: number | null | undefined) => {
|
const resolveForecastTemp = (date: string, fallback: number | null | undefined) => {
|
||||||
const debPrediction = data.multi_model_daily?.[date]?.deb?.prediction;
|
const debPrediction = data.multi_model_daily?.[date]?.deb?.prediction;
|
||||||
return debPrediction ?? fallback ?? null;
|
return debPrediction ?? fallback ?? null;
|
||||||
@@ -1006,17 +1010,14 @@ export function ForecastTable() {
|
|||||||
<section className="forecast-section">
|
<section className="forecast-section">
|
||||||
<h3>{t("forecast.title")}</h3>
|
<h3>{t("forecast.title")}</h3>
|
||||||
{isSparseDaily && (
|
{isSparseDaily && (
|
||||||
<div
|
<div className="forecast-inline-note">
|
||||||
className="forecast-inline-note"
|
{isForecastCompleting
|
||||||
style={{
|
? locale === "en-US"
|
||||||
color: "var(--text-secondary)",
|
? "Multi-day forecast is syncing. Only the current-day card has arrived."
|
||||||
fontSize: "12px",
|
: "多日预报同步中,当前只到达当日卡片。"
|
||||||
marginBottom: "10px",
|
: locale === "en-US"
|
||||||
}}
|
? "Only the current-day forecast is available right now."
|
||||||
>
|
: "当前只收到当日预报,其他日期结果暂未回传。"}
|
||||||
{store.loadingState.cityDetail
|
|
||||||
? "多日预报同步中,正在刷新完整日序列。"
|
|
||||||
: "当前只收到当日预报,其他日期结果暂未回传。"}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="forecast-table">
|
<div className="forecast-table">
|
||||||
@@ -1062,7 +1063,23 @@ export function ForecastTable() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user