This version of Antigravity is no longer supported. Please upgrade to receive the latest features.

This commit is contained in:
2569718930@qq.com
2026-04-30 08:05:10 +08:00
parent f0436f498a
commit a984c22b62
4 changed files with 138 additions and 52 deletions
@@ -29,23 +29,27 @@
background: rgba(8, 17, 30, 0.8); background: rgba(8, 17, 30, 0.8);
} }
.root :global(.scan-terminal.map-view-active .scan-upgrade-announcement), .root :global(.scan-terminal.focus-view-active .scan-upgrade-announcement),
.root :global(.scan-terminal.map-view-active .scan-kpi-bar) { .root :global(.scan-terminal.focus-view-active .scan-kpi-bar) {
display: none; display: none;
} }
.root :global(.scan-terminal.map-view-active .scan-data-grid) { .root :global(.scan-terminal.focus-view-active .scan-data-grid) {
gap: 12px; gap: 12px;
} }
.root :global(.scan-terminal.map-view-active .scan-list-section) { .root :global(.scan-terminal.focus-view-active .scan-list-section) {
min-height: min(760px, calc(100dvh - 164px)); min-height: min(780px, calc(100dvh - 136px));
} }
.root :global(.scan-terminal.map-view-active .scan-list-header) { .root :global(.scan-terminal.focus-view-active .scan-list-header) {
margin-bottom: 8px; margin-bottom: 8px;
} }
.root :global(.scan-terminal.focus-view-active .scan-topbar) {
padding-bottom: 12px;
}
@media (max-width: 1680px), (max-height: 900px) { @media (max-width: 1680px), (max-height: 900px) {
.root :global(.scan-terminal.map-view-active > .detail-panel.scan-city-detail-rail) { .root :global(.scan-terminal.map-view-active > .detail-panel.scan-city-detail-rail) {
display: none; display: none;
@@ -465,6 +465,8 @@ function ScanTerminalScreen() {
className={clsx( className={clsx(
"scan-terminal", "scan-terminal",
resolvedView === "map" && "map-view-active", resolvedView === "map" && "map-view-active",
resolvedView !== "opportunities" && "focus-view-active",
resolvedView === "analysis" && "analysis-view-active",
themeMode === "light" && "light", themeMode === "light" && "light",
)} )}
> >
@@ -489,10 +489,11 @@
max-height: 100%; max-height: 100%;
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; overscroll-behavior: contain;
scroll-padding-top: 64px;
border: 1px solid rgba(77, 163, 255, 0.16); border: 1px solid rgba(77, 163, 255, 0.16);
border-radius: 18px; border-radius: 18px;
background: #0b1220; background: #0b1220;
padding: 18px; padding: 14px;
} }
.root :global(.scan-ai-workspace::-webkit-scrollbar) { .root :global(.scan-ai-workspace::-webkit-scrollbar) {
@@ -514,7 +515,7 @@
justify-content: space-between; justify-content: space-between;
gap: 18px; gap: 18px;
align-items: flex-end; align-items: flex-end;
margin-bottom: 18px; margin-bottom: 10px;
padding: 0 2px; padding: 0 2px;
} }
@@ -529,7 +530,7 @@
.root :global(.scan-ai-workspace-head strong) { .root :global(.scan-ai-workspace-head strong) {
display: block; display: block;
color: #e6edf3; color: #e6edf3;
font-size: 22px; font-size: 20px;
line-height: 1.2; line-height: 1.2;
margin-top: 4px; margin-top: 4px;
} }
@@ -542,3 +543,57 @@
line-height: 1.55; line-height: 1.55;
text-align: right; text-align: right;
} }
.root :global(.scan-ai-city-jumpbar) {
position: sticky;
top: 0;
z-index: 8;
display: flex;
gap: 8px;
margin: 0 0 12px;
padding: 8px;
overflow-x: auto;
border: 1px solid rgba(77, 163, 255, 0.14);
border-radius: 14px;
background: rgba(11, 18, 32, 0.92);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
scrollbar-width: none;
}
.root :global(.scan-ai-city-jumpbar::-webkit-scrollbar) {
display: none;
}
.root :global(.scan-ai-city-jumpbar button) {
flex: 0 0 auto;
min-height: 32px;
padding: 0 12px;
border: 1px solid rgba(77, 163, 255, 0.26);
border-radius: 999px;
background: rgba(77, 163, 255, 0.1);
color: #cfe5ff;
font-size: 12px;
font-weight: 900;
cursor: pointer;
white-space: nowrap;
}
.root :global(.scan-ai-city-jumpbar button:hover) {
border-color: rgba(111, 183, 255, 0.62);
background: rgba(77, 163, 255, 0.18);
}
.root :global(.scan-ai-city-anchor) {
scroll-margin-top: 66px;
}
.root :global(.scan-ai-city-card:not(.collapsed)) {
overflow: visible;
}
.root :global(.scan-ai-city-card:not(.collapsed) .scan-ai-city-hero) {
position: sticky;
top: 54px;
z-index: 6;
border-radius: 18px 18px 0 0;
}
@@ -23,38 +23,37 @@ export function AiPinnedForecastView({
onRemoveCity: (cityName: string) => void; onRemoveCity: (cityName: string) => void;
}) { }) {
const isEn = locale === "en-US"; const isEn = locale === "en-US";
const [collapsedCities, setCollapsedCities] = useState<Set<string>>( const [expandedCityKey, setExpandedCityKey] = useState<string | null>(null);
() => new Set(),
);
const [removingCities, setRemovingCities] = useState<Set<string>>( const [removingCities, setRemovingCities] = useState<Set<string>>(
() => new Set(), () => new Set(),
); );
const cityCardRefs = useRef<Record<string, HTMLDivElement | null>>({});
const knownCityKeysRef = useRef<Set<string>>(new Set()); const knownCityKeysRef = useRef<Set<string>>(new Set());
const previousFirstCityKeyRef = useRef<string | null>(null);
const removeTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map()); const removeTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
useEffect(() => { useEffect(() => {
const activeKeys = new Set( const activeKeyList = items.map(
items.map((item) => normalizeCityKey(item.cityName) || item.cityName), (item) => normalizeCityKey(item.cityName) || item.cityName,
); );
setCollapsedCities((current) => { const activeKeys = new Set(activeKeyList);
const next = new Set<string>(); const firstCityKey = activeKeyList[0] ?? null;
let changed = false; const hasNewCity = activeKeyList.some((key) => !knownCityKeysRef.current.has(key));
current.forEach((key) => { const firstCityChanged = firstCityKey !== previousFirstCityKeyRef.current;
if (activeKeys.has(key)) {
next.add(key); setExpandedCityKey((current) => {
} else { if (!firstCityKey) return null;
changed = true; if (hasNewCity || firstCityChanged) return firstCityKey;
} if (current && activeKeys.has(current)) return current;
}); return firstCityKey;
items.forEach((item) => { });
const stableKey = normalizeCityKey(item.cityName) || item.cityName; Object.keys(cityCardRefs.current).forEach((key) => {
if (!knownCityKeysRef.current.has(stableKey)) { if (!activeKeys.has(key)) {
changed = true; delete cityCardRefs.current[key];
} }
});
return changed ? next : current;
}); });
knownCityKeysRef.current = activeKeys; knownCityKeysRef.current = activeKeys;
previousFirstCityKeyRef.current = firstCityKey;
}, [items]); }, [items]);
useEffect(() => { useEffect(() => {
@@ -86,6 +85,18 @@ export function AiPinnedForecastView({
[onRemoveCity], [onRemoveCity],
); );
const scrollToCity = useCallback((stableKey: string) => {
cityCardRefs.current[stableKey]?.scrollIntoView({
behavior: "smooth",
block: "start",
});
}, []);
const focusCity = useCallback((stableKey: string) => {
setExpandedCityKey(stableKey);
window.requestAnimationFrame(() => scrollToCity(stableKey));
}, [scrollToCity]);
if (!items.length) { if (!items.length) {
return ( return (
<div className="scan-ai-workspace empty"> <div className="scan-ai-workspace empty">
@@ -120,36 +131,50 @@ export function AiPinnedForecastView({
: "地图点击会把城市加入这里;城市分析会保留,直到你手动移除。"} : "地图点击会把城市加入这里;城市分析会保留,直到你手动移除。"}
</p> </p>
</div> </div>
<div className="scan-ai-city-jumpbar" aria-label={isEn ? "Selected city shortcuts" : "已选城市快捷跳转"}>
{items.map((item) => {
const stableKey = normalizeCityKey(item.cityName) || item.cityName;
return (
<button
key={stableKey}
type="button"
onClick={() => focusCity(stableKey)}
>
{item.cityName}
</button>
);
})}
</div>
<div className="scan-ai-city-stack"> <div className="scan-ai-city-stack">
{items.map((item) => { {items.map((item) => {
const detail = findDetailForCity(detailsByName, item.cityName); const detail = findDetailForCity(detailsByName, item.cityName);
const row = findRowForCity(rows, item.cityName); const row = findRowForCity(rows, item.cityName);
const key = normalizeCityKey(item.cityName); const key = normalizeCityKey(item.cityName);
const stableKey = key || item.cityName; const stableKey = key || item.cityName;
const isKnownCity = knownCityKeysRef.current.has(stableKey);
return ( return (
<AiPinnedCityCard <div
key={stableKey} key={stableKey}
item={item} ref={(node) => {
detail={detail} cityCardRefs.current[stableKey] = node;
row={row}
locale={locale}
collapsed={!isKnownCity || collapsedCities.has(stableKey)}
removing={removingCities.has(stableKey)}
onRefreshCityDetail={onRefreshCityDetail}
onRemove={() => removeCityWithMotion(item, stableKey)}
onToggleCollapsed={() => {
setCollapsedCities((current) => {
const next = new Set(current);
if (next.has(stableKey)) {
next.delete(stableKey);
} else {
next.add(stableKey);
}
return next;
});
}} }}
/> className="scan-ai-city-anchor"
>
<AiPinnedCityCard
item={item}
detail={detail}
row={row}
locale={locale}
collapsed={expandedCityKey !== stableKey}
removing={removingCities.has(stableKey)}
onRefreshCityDetail={onRefreshCityDetail}
onRemove={() => removeCityWithMotion(item, stableKey)}
onToggleCollapsed={() => {
setExpandedCityKey((current) =>
current === stableKey ? null : stableKey,
);
}}
/>
</div>
); );
})} })}
</div> </div>