Show stale detail blocker while refreshing city data
This commit is contained in:
@@ -774,8 +774,39 @@
|
|||||||
.root :global(.panel-body) {
|
.root :global(.panel-body) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
padding: 0 20px 24px;
|
padding: 0 20px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-sync-blocker) {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid rgba(34, 211, 238, 0.22);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(8, 16, 30, 0.96);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: 0 14px 34px rgba(2, 6, 23, 0.38);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-content-stale) {
|
||||||
|
opacity: 0.28;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-content-stale canvas) {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
.root :global(.panel-body::-webkit-scrollbar) {
|
.root :global(.panel-body::-webkit-scrollbar) {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export function DetailPanel() {
|
|||||||
(detail.forecast?.daily?.length ?? 0) <= 1),
|
(detail.forecast?.daily?.length ?? 0) <= 1),
|
||||||
);
|
);
|
||||||
const isPanelSyncing = store.loadingState.cityDetail;
|
const isPanelSyncing = store.loadingState.cityDetail;
|
||||||
|
const isShowingCachedDetailDuringSync = Boolean(detail && isPanelSyncing);
|
||||||
|
|
||||||
const blurActiveElement = () => {
|
const blurActiveElement = () => {
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
@@ -384,7 +385,18 @@ export function DetailPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel-body">
|
<div className={clsx("panel-body", isShowingCachedDetailDuringSync && "is-syncing")}>
|
||||||
|
{isShowingCachedDetailDuringSync ? (
|
||||||
|
<div className="panel-sync-blocker" role="status" aria-live="polite">
|
||||||
|
<span className="panel-loading-spinner" aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
{locale === "en-US"
|
||||||
|
? `Refreshing ${panelDisplayName}. Current cards are paused until the latest data arrives.`
|
||||||
|
: `正在刷新 ${panelDisplayName},最新数据返回前暂停展示旧卡片。`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={clsx(isShowingCachedDetailDuringSync && "panel-content-stale")}>
|
||||||
{!hasBasicPanelContent ? (
|
{!hasBasicPanelContent ? (
|
||||||
<section className="detail-summary-shell detail-empty-state">
|
<section className="detail-summary-shell detail-empty-state">
|
||||||
<div className="detail-section-head">
|
<div className="detail-section-head">
|
||||||
@@ -479,6 +491,7 @@ export function DetailPanel() {
|
|||||||
{heavyContentReady ? <ForecastTable /> : null}
|
{heavyContentReady ? <ForecastTable /> : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -788,11 +788,7 @@ export function DashboardStoreProvider({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedDetail = cityDetailsByName[cityName];
|
const needsDetailRefresh = true;
|
||||||
const needsDetailRefresh = hasSparseDetailCoverage(
|
|
||||||
cachedDetail,
|
|
||||||
cachedDetail?.local_date,
|
|
||||||
);
|
|
||||||
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
||||||
const detailPromise = ensureCityDetail(cityName, needsDetailRefresh, "panel");
|
const detailPromise = ensureCityDetail(cityName, needsDetailRefresh, "panel");
|
||||||
void Promise.allSettled([summaryPromise, detailPromise])
|
void Promise.allSettled([summaryPromise, detailPromise])
|
||||||
|
|||||||
@@ -593,6 +593,13 @@ export function useLeafletMap({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function maybeAutoShowNearbyStations() {
|
async function maybeAutoShowNearbyStations() {
|
||||||
|
if (selectedCity && isLoadingDetail) {
|
||||||
|
autoNearbyCityRef.current = selectedCity;
|
||||||
|
clearNearbyRefreshTimer();
|
||||||
|
layer.clearLayers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (handlingAutoNearbyRef.current) {
|
if (handlingAutoNearbyRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -700,7 +707,7 @@ export function useLeafletMap({
|
|||||||
map.off("zoomend", syncVisibility);
|
map.off("zoomend", syncVisibility);
|
||||||
map.off("moveend", maybeAutoShowNearbyStations);
|
map.off("moveend", maybeAutoShowNearbyStations);
|
||||||
};
|
};
|
||||||
}, [cityDetailsByName, selectedCity, selectedDetail, suspendMotion]);
|
}, [cityDetailsByName, selectedCity, selectedDetail, suspendMotion, isLoadingDetail]);
|
||||||
|
|
||||||
// Centralized City Selection Zoom Effect
|
// Centralized City Selection Zoom Effect
|
||||||
// Higher level than selection: we only flyTo once the data is loaded (selectedDetail)
|
// Higher level than selection: we only flyTo once the data is loaded (selectedDetail)
|
||||||
|
|||||||
Reference in New Issue
Block a user