Refine scan terminal focus and stale snapshot handling

This commit is contained in:
2569718930@qq.com
2026-04-26 00:54:39 +08:00
parent 92e505d804
commit 90fcb96fa7
2 changed files with 77 additions and 14 deletions
@@ -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;
});
}}
/> />
); );
})} })}