This version of Antigravity is no longer supported. Please upgrade to receive the latest features.
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user