feat: implement scan terminal mobile dashboard components and data management logic

This commit is contained in:
2569718930@qq.com
2026-05-17 15:32:47 +08:00
parent 04e0369255
commit 2d9aa576dd
11 changed files with 450 additions and 4 deletions
@@ -98,7 +98,12 @@ function ScanTerminalScreen() {
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const media = window.matchMedia("(max-width: 768px)");
const syncMobileViewport = () => setIsMobileViewport(media.matches);
const syncMobileViewport = () => {
setIsMobileViewport(media.matches);
if (media.matches) {
setActiveView((current) => (current === "map" ? "city-list" : current));
}
};
syncMobileViewport();
media.addEventListener("change", syncMobileViewport);
return () => media.removeEventListener("change", syncMobileViewport);
@@ -336,6 +341,76 @@ function ScanTerminalScreen() {
}, []);
const renderMainView = () => {
if (resolvedView === "city-list") {
return (
<div className="scan-mobile-city-list-view">
<div className="scan-mobile-city-list-head">
<div>
<span>{isEn ? "Lightweight mobile entry" : "移动端轻量入口"}</span>
<strong>{isEn ? "Pick one city to inspect" : "选择一个城市进入决策卡"}</strong>
</div>
<p>
{isEn
? "Cached scan rows are used first. The map and heavier evidence load only when opened."
: "优先使用缓存扫描结果;地图和重证据只在打开后加载。"}
</p>
</div>
<div className="scan-mobile-city-list">
{timeSortedRows.slice(0, 24).map((row, index) => {
const cityName = row.city || row.city_display_name || row.display_name || "";
const display = row.city_display_name || row.display_name || cityName;
const currentTemp = row.current_temp ?? row.current_max_so_far ?? null;
const deb = row.deb_prediction ?? null;
return (
<button
key={row.id || `${cityName}-${index}`}
type="button"
className="scan-mobile-city-row"
onClick={() => handleOpenDecisionRow(row)}
>
<span className="scan-mobile-city-rank">{String(index + 1).padStart(2, "0")}</span>
<span className="scan-mobile-city-main">
<b>{display}</b>
<small>{row.market_question || row.target_label || row.local_time || "--"}</small>
</span>
<span className="scan-mobile-city-metrics">
<b>
{currentTemp != null && Number.isFinite(Number(currentTemp))
? `${Number(currentTemp).toFixed(1)}${row.temp_symbol || "°C"}`
: "--"}
</b>
<small>
DEB{" "}
{deb != null && Number.isFinite(Number(deb))
? `${Number(deb).toFixed(1)}${row.temp_symbol || "°C"}`
: "--"}
</small>
</span>
</button>
);
})}
</div>
{!timeSortedRows.length ? (
<div className="scan-empty-state">
<div className="scan-empty-title">
{scanLoading
? isEn
? "Loading cached scan"
: "正在读取缓存扫描"
: isEn
? "No city rows yet"
: "暂无城市列表"}
</div>
<div className="scan-empty-copy">
{isEn
? "Use refresh only when you need a new server-side scan."
: "只有需要新的服务端扫描时再手动刷新。"}
</div>
</div>
) : null}
</div>
);
}
if (resolvedView === "map") {
return (
<div className="scan-map-view">
@@ -396,6 +471,7 @@ function ScanTerminalScreen() {
<div
className={clsx(
"scan-terminal",
resolvedView === "city-list" && "city-list-view-active",
resolvedView === "map" && "map-view-active",
resolvedView !== "map" && "focus-view-active",
resolvedView === "analysis" && "analysis-view-active",
@@ -432,6 +508,17 @@ function ScanTerminalScreen() {
<section className="scan-list-section">
<div className="scan-list-header">
<div className="scan-list-tabs" role="tablist" aria-label={isEn ? "Content view" : "内容视图"}>
<button
type="button"
role="tab"
aria-selected={resolvedView === "city-list"}
className={resolvedView === "city-list" ? "active" : ""}
onClick={() => {
setActiveView("city-list");
}}
>
{isEn ? "City List" : "城市列表"}
</button>
<button
type="button"
role="tab"