Refine scan terminal focus and stale snapshot handling
This commit is contained in:
@@ -11042,7 +11042,15 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-city-remove),
|
.root :global(.scan-ai-city-actions) {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-city-pin),
|
||||||
|
.root :global(.scan-ai-city-collapse),
|
||||||
.root :global(.scan-ai-city-price-button) {
|
.root :global(.scan-ai-city-price-button) {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -11058,15 +11066,30 @@
|
|||||||
transition: background 0.18s ease, border-color 0.18s ease;
|
transition: background 0.18s ease, border-color 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-city-remove) {
|
.root :global(.scan-ai-city-pin) {
|
||||||
|
width: 36px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-city-collapse) {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-city-card.collapsed .scan-ai-city-collapse svg) {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-city-collapse svg) {
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-city-price-button) {
|
.root :global(.scan-ai-city-price-button) {
|
||||||
padding: 9px 12px;
|
padding: 9px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-city-remove:hover),
|
.root :global(.scan-ai-city-pin:hover),
|
||||||
|
.root :global(.scan-ai-city-collapse:hover),
|
||||||
.root :global(.scan-ai-city-price-button:hover) {
|
.root :global(.scan-ai-city-price-button:hover) {
|
||||||
background: rgba(77, 163, 255, 0.18);
|
background: rgba(77, 163, 255, 0.18);
|
||||||
border-color: rgba(111, 183, 255, 0.58);
|
border-color: rgba(111, 183, 255, 0.58);
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import clsx from "clsx";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
|
ChevronDown,
|
||||||
LogIn,
|
LogIn,
|
||||||
X,
|
|
||||||
Moon,
|
Moon,
|
||||||
|
Pin,
|
||||||
Sun,
|
Sun,
|
||||||
UserRound,
|
UserRound,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -531,13 +532,17 @@ function AiPinnedCityCard({
|
|||||||
detail,
|
detail,
|
||||||
row,
|
row,
|
||||||
locale,
|
locale,
|
||||||
|
collapsed,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onToggleCollapsed,
|
||||||
}: {
|
}: {
|
||||||
item: AiPinnedCity;
|
item: AiPinnedCity;
|
||||||
detail: CityDetail | null;
|
detail: CityDetail | null;
|
||||||
row: ScanOpportunityRow | null;
|
row: ScanOpportunityRow | null;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
collapsed: boolean;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
|
onToggleCollapsed: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isEn = locale === "en-US";
|
const isEn = locale === "en-US";
|
||||||
const displayName =
|
const displayName =
|
||||||
@@ -584,8 +589,10 @@ function AiPinnedCityCard({
|
|||||||
detail?.airport_primary?.wx_desc ||
|
detail?.airport_primary?.wx_desc ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
|
const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="scan-ai-city-card">
|
<article className={clsx("scan-ai-city-card", collapsed && "collapsed")}>
|
||||||
<header className="scan-ai-city-hero">
|
<header className="scan-ai-city-hero">
|
||||||
<div>
|
<div>
|
||||||
<span className="scan-ai-city-kicker">
|
<span className="scan-ai-city-kicker">
|
||||||
@@ -613,15 +620,32 @@ function AiPinnedCityCard({
|
|||||||
? formatTemperatureValue(deb, tempSymbol, { digits: 1 })
|
? formatTemperatureValue(deb, tempSymbol, { digits: 1 })
|
||||||
: "--"}
|
: "--"}
|
||||||
</strong>
|
</strong>
|
||||||
<button type="button" className="scan-ai-city-remove" onClick={onRemove}>
|
<div className="scan-ai-city-actions">
|
||||||
<X size={14} />
|
<button
|
||||||
{isEn ? "Unpin" : "取消固定"}
|
type="button"
|
||||||
</button>
|
className="scan-ai-city-pin"
|
||||||
|
onClick={onRemove}
|
||||||
|
aria-label={isEn ? `Unpin ${displayName}` : `取消固定 ${displayName}`}
|
||||||
|
title={isEn ? "Unpin city" : "取消固定"}
|
||||||
|
>
|
||||||
|
<Pin size={15} fill="currentColor" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="scan-ai-city-collapse"
|
||||||
|
onClick={onToggleCollapsed}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
aria-controls={collapseId}
|
||||||
|
>
|
||||||
|
<ChevronDown size={15} />
|
||||||
|
{collapsed ? (isEn ? "Expand" : "展开") : (isEn ? "Collapse" : "收起")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{detail ? (
|
{detail && !collapsed ? (
|
||||||
<div className="scan-ai-city-body">
|
<div className="scan-ai-city-body" id={collapseId}>
|
||||||
<section className={clsx("scan-ai-decision-band", paceTone)}>
|
<section className={clsx("scan-ai-decision-band", paceTone)}>
|
||||||
<div>
|
<div>
|
||||||
<span>{isEn ? "Final read" : "最终判断"}</span>
|
<span>{isEn ? "Final read" : "最终判断"}</span>
|
||||||
@@ -684,11 +708,11 @@ function AiPinnedCityCard({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : !detail ? (
|
||||||
<div className="scan-ai-city-loading">
|
<div className="scan-ai-city-loading">
|
||||||
{isEn ? "Loading today analysis for this city..." : "正在加载该城市的今日日内分析..."}
|
{isEn ? "Loading today analysis for this city..." : "正在加载该城市的今日日内分析..."}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -707,6 +731,9 @@ 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>>(
|
||||||
|
() => new Set(),
|
||||||
|
);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
<div className="scan-ai-workspace empty">
|
<div className="scan-ai-workspace empty">
|
||||||
@@ -746,14 +773,27 @@ function AiPinnedForecastView({
|
|||||||
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;
|
||||||
return (
|
return (
|
||||||
<AiPinnedCityCard
|
<AiPinnedCityCard
|
||||||
key={key || item.cityName}
|
key={stableKey}
|
||||||
item={item}
|
item={item}
|
||||||
detail={detail}
|
detail={detail}
|
||||||
row={row}
|
row={row}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
collapsed={collapsedCities.has(stableKey)}
|
||||||
onRemove={() => onRemoveCity(item.cityName)}
|
onRemove={() => onRemoveCity(item.cityName)}
|
||||||
|
onToggleCollapsed={() => {
|
||||||
|
setCollapsedCities((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
if (next.has(stableKey)) {
|
||||||
|
next.delete(stableKey);
|
||||||
|
} else {
|
||||||
|
next.add(stableKey);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user