Speed up intraday modal loading and add sync status

This commit is contained in:
2569718930@qq.com
2026-04-13 11:19:24 +08:00
parent f0940b9c8b
commit 1d54f44189
3 changed files with 194 additions and 45 deletions
@@ -2856,6 +2856,75 @@
padding-right: 14px;
}
.root :global(.future-v2-sync-strip) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.root :global(.future-v2-sync-chip) {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 11px 12px;
border-radius: 12px;
border: 1px solid rgba(52, 211, 153, 0.16);
background: rgba(16, 185, 129, 0.06);
}
.root :global(.future-v2-sync-chip.syncing) {
border-color: rgba(34, 211, 238, 0.2);
background: rgba(34, 211, 238, 0.07);
}
.root :global(.future-v2-sync-dot) {
width: 9px;
height: 9px;
border-radius: 999px;
background: #34d399;
margin-top: 5px;
flex: 0 0 auto;
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.12);
}
.root :global(.future-v2-sync-chip.syncing .future-v2-sync-dot) {
background: #22d3ee;
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.12);
animation: pulseGlow 1.25s ease-in-out infinite;
}
.root :global(.future-v2-sync-copy) {
display: grid;
gap: 4px;
min-width: 0;
}
.root :global(.future-v2-sync-copy strong) {
color: var(--text-primary);
font-size: 12px;
font-weight: 700;
line-height: 1.3;
}
.root :global(.future-v2-sync-copy span) {
color: var(--text-muted);
font-size: 11px;
line-height: 1.45;
}
@keyframes pulseGlow {
0%,
100% {
opacity: 0.9;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.08);
}
}
.root :global(.future-v2-layout) {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
@@ -592,6 +592,11 @@ export function FutureForecastModal() {
if (!detail || !dateStr) return null;
const isToday = dateStr === detail.local_date;
const detailDepth = detail.detail_depth || "full";
const isFullDetailReady = detailDepth === "full";
const isStructureSyncing = store.loadingState.refresh || !isFullDetailReady;
const isMarketSyncing = store.loadingState.marketScan;
const isAnyLayerSyncing = isStructureSyncing || isMarketSyncing;
const view = getFutureModalView(detail, dateStr, locale);
const scorePosition = `${50 + view.front.score / 2}%`;
const barStyle = {
@@ -1060,6 +1065,58 @@ export function FutureForecastModal() {
}
return lines.slice(0, 3);
}, [boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView]);
const syncStatusItems = [
{
key: "base",
state: "ready",
label:
locale === "en-US" ? "Base analysis ready" : "基础分析已加载",
note:
locale === "en-US"
? "Forecast curve, anchor state, and current structure are available."
: "预测曲线、锚点状态和当前结构已经可用。",
},
{
key: "market",
state: isMarketSyncing ? "syncing" : "ready",
label:
locale === "en-US"
? isMarketSyncing
? "Syncing market ladder"
: "Market ladder ready"
: isMarketSyncing
? "市场挂单同步中"
: "市场挂单已加载",
note:
locale === "en-US"
? isMarketSyncing
? "Polymarket buckets and edge are updating in the background."
: "Probability buckets and edge are in sync."
: isMarketSyncing
? "Polymarket 概率桶和 edge 正在后台更新。"
: "概率桶和 edge 已同步完成。",
},
{
key: "structure",
state: isStructureSyncing ? "syncing" : "ready",
label:
locale === "en-US"
? isStructureSyncing
? "Backfilling deep structure"
: "Deep structure ready"
: isStructureSyncing
? "深度结构补齐中"
: "深度结构已加载",
note:
locale === "en-US"
? isStructureSyncing
? "Upper-air, nearby network, and deeper fusion signals are still coming in."
: "Upper-air, nearby network, and deeper fusion signals are ready."
: isStructureSyncing
? "高空、周边站网和更深层融合信号还在补齐。"
: "高空、周边站网和更深层融合信号已可用。",
},
] as const;
return (
<div
@@ -1107,7 +1164,7 @@ export function FutureForecastModal() {
<button
className={clsx(
"future-refresh-btn",
store.loadingState.marketScan && "spinning",
isAnyLayerSyncing && "spinning",
)}
disabled={!isPro || isProLoading}
onClick={() => {
@@ -1154,6 +1211,23 @@ export function FutureForecastModal() {
</button>
</div>
<div className="modal-body future-modal-body">
<section className="future-v2-sync-strip" aria-live="polite">
{syncStatusItems.map((item) => (
<div
key={item.key}
className={clsx(
"future-v2-sync-chip",
item.state === "syncing" && "syncing",
)}
>
<span className="future-v2-sync-dot" aria-hidden="true" />
<div className="future-v2-sync-copy">
<strong>{item.label}</strong>
<span>{item.note}</span>
</div>
</div>
))}
</section>
{isNoaaSettlement && (
<div
style={{
+50 -44
View File
@@ -977,38 +977,39 @@ export function DashboardStoreProvider({
openFutureModal: async (dateStr: string, forceRefresh = false) => {
mapStopMotionRef.current();
if (!selectedCity || !proAccess.subscriptionActive) return;
const cityName = selectedCity;
const cachedDetail = cityDetailsByName[selectedCity];
const hasFullCachedDetail =
detailSatisfiesDepth(cachedDetail, "full") &&
!hasSparseDetailCoverage(cachedDetail, dateStr);
setFutureModalDate(dateStr);
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
setLoadingState((current) => ({ ...current, marketScan: true }));
if (!hasFullCachedDetail || forceRefresh) {
setLoadingState((current) => ({
...current,
refresh: true,
}));
try {
await ensureCityDetail(selectedCity, true, "full");
} catch {
} finally {
setLoadingState((current) => ({
...current,
refresh: false,
}));
}
void ensureCityDetail(cityName, true, "full")
.catch(() => {})
.finally(() => {
if (selectedCityRef.current !== cityName) return;
setLoadingState((current) => ({
...current,
refresh: false,
}));
});
}
setFutureModalDate(dateStr);
const cacheKey = getMarketScanCacheKey(selectedCity, dateStr);
setLoadingState((current) => ({ ...current, marketScan: true }));
void ensureCityMarketScan(
selectedCity,
cityName,
forceRefresh || !marketScanByCityName[cacheKey],
null,
dateStr,
)
.catch(() => {})
.finally(() => {
if (selectedCityRef.current !== cityName) return;
setLoadingState((current) => ({ ...current, marketScan: false }));
});
},
@@ -1019,13 +1020,16 @@ export function DashboardStoreProvider({
}
mapStopMotionRef.current();
const cachedDetail = cityDetailsByName[selectedCity];
const cityName = selectedCity;
const cachedDetail = cityDetailsByName[cityName];
const hasFullCachedDetail =
detailSatisfiesDepth(cachedDetail, "full") &&
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
if (hasFullCachedDetail && cachedDetail?.local_date) {
setSelectedForecastDate(cachedDetail.local_date);
setFutureModalDate(cachedDetail.local_date);
const targetDate =
cachedDetail?.local_date || selectedForecastDate || null;
if (targetDate) {
setSelectedForecastDate(targetDate);
setFutureModalDate(targetDate);
}
if (!proAccess.subscriptionActive) return;
const needsDetailRefresh =
@@ -1038,37 +1042,39 @@ export function DashboardStoreProvider({
refresh: needsDetailRefresh,
marketScan: true,
}));
void ensureCityDetail(
cityName,
needsDetailRefresh,
"full",
)
.then((detail) => {
if (selectedCityRef.current !== cityName) return;
setSelectedForecastDate(detail.local_date);
setFutureModalDate(detail.local_date);
try {
const detail = await ensureCityDetail(
selectedCity,
needsDetailRefresh,
"full",
);
setSelectedForecastDate(detail.local_date);
setFutureModalDate(detail.local_date);
const marketKey = getMarketScanCacheKey(selectedCity, detail.local_date);
try {
await ensureCityMarketScan(
selectedCity,
const marketKey = getMarketScanCacheKey(cityName, detail.local_date);
return ensureCityMarketScan(
cityName,
forceRefresh || !marketScanByCityName[marketKey],
null,
detail.local_date,
);
} catch {}
} catch {
if (cachedDetail?.local_date) {
setSelectedForecastDate(cachedDetail.local_date);
setFutureModalDate(cachedDetail.local_date);
}
} finally {
setLoadingState((current) => ({
...current,
refresh: false,
marketScan: false,
}));
}
})
.catch(() => {
if (selectedCityRef.current !== cityName) return;
if (cachedDetail?.local_date) {
setSelectedForecastDate(cachedDetail.local_date);
setFutureModalDate(cachedDetail.local_date);
}
})
.finally(() => {
if (selectedCityRef.current !== cityName) return;
setLoadingState((current) => ({
...current,
refresh: false,
marketScan: false,
}));
});
},
registerMapStopMotion: (stopMotion: () => void) => {
mapStopMotionRef.current = stopMotion;