feat: implement scan terminal mobile dashboard components and data management logic
This commit is contained in:
@@ -865,6 +865,44 @@
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.root :global(.scan-airport-evidence-grid) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-airport-evidence-card) {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.16);
|
||||
border-radius: 14px;
|
||||
background: rgba(77, 163, 255, 0.08);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-airport-evidence-card.runway) {
|
||||
border-color: rgba(255, 184, 77, 0.24);
|
||||
background: rgba(255, 184, 77, 0.08);
|
||||
}
|
||||
|
||||
.root :global(.scan-airport-evidence-card span) {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.root :global(.scan-airport-evidence-card b) {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.root :global(.scan-airport-evidence-card small) {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-section-title) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,100 @@
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-list-view) {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-list-head) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
border: 1px solid rgba(77, 163, 255, 0.16);
|
||||
border-radius: 20px;
|
||||
background: rgba(5, 12, 24, 0.72);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-list-head span) {
|
||||
display: block;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-list-head strong) {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-list-head p) {
|
||||
max-width: 360px;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-list) {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-row) {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(118, 150, 192, 0.16);
|
||||
border-radius: 18px;
|
||||
background: rgba(8, 18, 35, 0.76);
|
||||
color: inherit;
|
||||
padding: 13px 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-rank) {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
background: rgba(77, 163, 255, 0.12);
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-main),
|
||||
.root :global(.scan-mobile-city-metrics) {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-main b),
|
||||
.root :global(.scan-mobile-city-metrics b) {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-main small),
|
||||
.root :global(.scan-mobile-city-metrics small) {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-city-metrics) {
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-decision-head) {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -213,6 +307,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-workspace-head),
|
||||
.root :global(.scan-mobile-city-list-head),
|
||||
.root :global(.scan-opportunity-hero),
|
||||
.root :global(.scan-ai-city-hero),
|
||||
.root :global(.scan-ai-city-section-head) {
|
||||
@@ -225,6 +320,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-workspace-head p),
|
||||
.root :global(.scan-mobile-city-list-head p),
|
||||
.root :global(.scan-ai-city-hero-side) {
|
||||
text-align: left;
|
||||
justify-items: start;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { MouseEvent } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart";
|
||||
import { AiEvidencePanel } from "@/components/dashboard/scan-terminal/AiEvidencePanel";
|
||||
import { AirportEvidencePanel } from "@/components/dashboard/scan-terminal/AirportEvidencePanel";
|
||||
import { CityCardHeader } from "@/components/dashboard/scan-terminal/CityCardHeader";
|
||||
import { MobileDecisionCard } from "@/components/dashboard/scan-terminal/MobileDecisionCard";
|
||||
import { ModelEvidencePanel } from "@/components/dashboard/scan-terminal/ModelEvidencePanel";
|
||||
@@ -654,6 +655,7 @@ export function AiPinnedCityCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AirportEvidencePanel detail={detail} isEn={isEn} />
|
||||
<ModelEvidencePanel detail={detail} isEn={isEn} />
|
||||
</div>
|
||||
) : !detail ? (
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import type { CityDetail } from "@/lib/dashboard-types";
|
||||
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
||||
|
||||
const FOCUS_RUNWAY_PAIRS: Record<string, Array<[string, string]>> = {
|
||||
chongqing: [["02L", "20R"]],
|
||||
shanghai: [["17L", "35R"]],
|
||||
wuhan: [["04", "22"]],
|
||||
beijing: [["01", "19"]],
|
||||
guangzhou: [["02L", "20R"]],
|
||||
chengdu: [["02L", "20R"]],
|
||||
seoul: [["15R", "33L"]],
|
||||
};
|
||||
|
||||
function normalizeRunwayLabel(value?: string | null) {
|
||||
return String(value || "").trim().toUpperCase().replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
function normalizeCityKey(value?: string | null) {
|
||||
return String(value || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
||||
}
|
||||
|
||||
function pairKey(pair: [string, string]) {
|
||||
return pair.map(normalizeRunwayLabel).sort().join("/");
|
||||
}
|
||||
|
||||
function toFiniteNumber(value: unknown) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : null;
|
||||
}
|
||||
|
||||
function formatObsTime(value: unknown) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return "";
|
||||
if (raw.includes("T")) {
|
||||
const parsed = new Date(raw);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return `${String(parsed.getUTCHours()).padStart(2, "0")}:${String(
|
||||
parsed.getUTCMinutes(),
|
||||
).padStart(2, "0")}Z`;
|
||||
}
|
||||
}
|
||||
return raw.length >= 16 && raw[10] === " " ? raw.slice(11, 16) : raw;
|
||||
}
|
||||
|
||||
function buildFocusedRunwayEvidence(detail: CityDetail | null) {
|
||||
if (!detail) return null;
|
||||
const cityKey = normalizeCityKey(detail.name) || normalizeCityKey(detail.display_name);
|
||||
const focusPairs = FOCUS_RUNWAY_PAIRS[cityKey];
|
||||
if (!focusPairs?.length) return null;
|
||||
const focusKeys = new Set(focusPairs.map(pairKey));
|
||||
const runwayObs = detail.amos?.runway_obs || {};
|
||||
const runwayPairs = runwayObs.runway_pairs || [];
|
||||
const runwayTemps = runwayObs.temperatures || [];
|
||||
const pointTemps = runwayObs.point_temperatures || [];
|
||||
const rows: Array<{
|
||||
label: string;
|
||||
maxTemp: number;
|
||||
values: number[];
|
||||
}> = [];
|
||||
|
||||
runwayPairs.forEach((rawPair, index) => {
|
||||
const pair = rawPair as [string, string];
|
||||
if (!Array.isArray(pair) || pair.length < 2) return;
|
||||
if (!focusKeys.has(pairKey(pair))) return;
|
||||
const values = [
|
||||
...(Array.isArray(runwayTemps[index]) ? runwayTemps[index] : []),
|
||||
toFiniteNumber(pointTemps[index]?.tdz_temp),
|
||||
toFiniteNumber(pointTemps[index]?.mid_temp),
|
||||
toFiniteNumber(pointTemps[index]?.end_temp),
|
||||
].filter((value): value is number => Number.isFinite(value));
|
||||
if (!values.length) return;
|
||||
rows.push({
|
||||
label: `${normalizeRunwayLabel(pair[0])}/${normalizeRunwayLabel(pair[1])}`,
|
||||
maxTemp: Math.max(...values),
|
||||
values,
|
||||
});
|
||||
});
|
||||
|
||||
if (!rows.length) return null;
|
||||
return {
|
||||
observedAt:
|
||||
formatObsTime(detail.amos?.observation_time_local) ||
|
||||
formatObsTime(detail.amos?.observation_time),
|
||||
rows,
|
||||
sourceLabel: detail.amos?.source_label || detail.amos?.source || "AMOS",
|
||||
};
|
||||
}
|
||||
|
||||
export function AirportEvidencePanel({
|
||||
detail,
|
||||
isEn,
|
||||
}: {
|
||||
detail: CityDetail | null;
|
||||
isEn: boolean;
|
||||
}) {
|
||||
const airportPrimary = getDisplayAirportPrimary(detail);
|
||||
const airportCurrent = detail?.airport_current;
|
||||
const station = airportPrimary || airportCurrent || null;
|
||||
const runwayEvidence = buildFocusedRunwayEvidence(detail);
|
||||
const tempSymbol = detail?.temp_symbol || "°C";
|
||||
if (!station && !runwayEvidence) return null;
|
||||
|
||||
return (
|
||||
<section className="scan-ai-city-section scan-airport-evidence">
|
||||
<div className="scan-ai-city-section-head">
|
||||
<div>
|
||||
<span className="scan-ai-city-kicker">
|
||||
{isEn ? "Airport live evidence" : "机场实时证据"}
|
||||
</span>
|
||||
<h4>{isEn ? "Airport / focused runway" : "机场主站 / 重点跑道"}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="scan-airport-evidence-grid">
|
||||
{station ? (
|
||||
<div className="scan-airport-evidence-card">
|
||||
<span>{isEn ? "Airport station" : "机场主站"}</span>
|
||||
<b>
|
||||
{station.temp != null && Number.isFinite(Number(station.temp))
|
||||
? formatTemperatureValue(Number(station.temp), tempSymbol, { digits: 1 })
|
||||
: "--"}
|
||||
</b>
|
||||
<small>
|
||||
{[station.station_label || station.station_code, station.source_label || "METAR", formatObsTime(station.obs_time || station.report_time)]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
{runwayEvidence?.rows.map((row) => (
|
||||
<div className="scan-airport-evidence-card runway" key={row.label}>
|
||||
<span>{isEn ? "Focused runway" : "重点跑道"}</span>
|
||||
<b>{formatTemperatureValue(row.maxTemp, tempSymbol, { digits: 1 })}</b>
|
||||
<small>
|
||||
{[row.label, runwayEvidence.sourceLabel, runwayEvidence.observedAt]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { MouseEvent } from "react";
|
||||
import { useState } from "react";
|
||||
import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart";
|
||||
import { AiEvidencePanel } from "@/components/dashboard/scan-terminal/AiEvidencePanel";
|
||||
import { AirportEvidencePanel } from "@/components/dashboard/scan-terminal/AirportEvidencePanel";
|
||||
import {
|
||||
CityStatusTags,
|
||||
type CityStatusTag,
|
||||
@@ -102,6 +103,7 @@ export function MobileDecisionCard({
|
||||
const loadingCopy = getCityLoadingCopy({ isEn, isHkoObservation });
|
||||
const [modelOpen, setModelOpen] = useState(false);
|
||||
const [chartOpen, setChartOpen] = useState(false);
|
||||
const [airportOpen, setAirportOpen] = useState(false);
|
||||
const statusTags: CityStatusTag[] = decisionState.badges.length
|
||||
? decisionState.badges
|
||||
: [{ label: decisionState.aiStatusLabel, tone: decisionState.aiStatusTone as StatusTone }];
|
||||
@@ -201,6 +203,17 @@ export function MobileDecisionCard({
|
||||
tempSymbol={tempSymbol}
|
||||
/>
|
||||
|
||||
<details
|
||||
className="scan-ai-city-section scan-mobile-fold"
|
||||
open={airportOpen}
|
||||
onToggle={(event) => setAirportOpen(event.currentTarget.open)}
|
||||
>
|
||||
<summary className="scan-ai-city-section-title">
|
||||
{isEn ? "Airport live evidence" : "机场实时证据"}
|
||||
</summary>
|
||||
{airportOpen ? <AirportEvidencePanel detail={detail} isEn={isEn} /> : null}
|
||||
</details>
|
||||
|
||||
<details
|
||||
className="scan-ai-city-section scan-mobile-fold"
|
||||
open={modelOpen}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall";
|
||||
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
|
||||
export type ScanTerminalContentView = "analysis" | "map";
|
||||
export type ScanTerminalContentView = "city-list" | "analysis" | "map";
|
||||
|
||||
type ThemeMode = "dark" | "light";
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ export function runTests() {
|
||||
!shellPartsSource.includes('"monitor"') && !shellPartsSource.includes('"runway"'),
|
||||
"scan terminal content views must not include market monitor or runway tabs",
|
||||
);
|
||||
assert(
|
||||
shellPartsSource.includes('"city-list"') &&
|
||||
dashboardSource.includes("scan-mobile-city-list-view") &&
|
||||
dashboardSource.includes('setActiveView("city-list")'),
|
||||
"mobile web should expose the lightweight city-list entry view",
|
||||
);
|
||||
assert(
|
||||
!dashboardSource.includes('setActiveView("monitor")') &&
|
||||
!dashboardSource.includes('setActiveView("runway")') &&
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const projectRoot = process.cwd();
|
||||
const queryPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"use-scan-terminal-query.ts",
|
||||
);
|
||||
const dashboardPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"ScanTerminalDashboard.tsx",
|
||||
);
|
||||
const airportEvidencePath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"AirportEvidencePanel.tsx",
|
||||
);
|
||||
|
||||
const querySource = fs.readFileSync(queryPath, "utf8");
|
||||
const dashboardSource = fs.readFileSync(dashboardPath, "utf8");
|
||||
const airportEvidenceSource = fs.readFileSync(airportEvidencePath, "utf8");
|
||||
|
||||
assert(
|
||||
querySource.includes("void fetchScanTerminal({ forceRefresh: false, showLoading: false })"),
|
||||
"web auto refresh must read cached scan data instead of forcing a full server scan",
|
||||
);
|
||||
assert(
|
||||
dashboardSource.includes("scan-mobile-city-list-view") &&
|
||||
dashboardSource.includes("MapCanvas") &&
|
||||
dashboardSource.indexOf("scan-mobile-city-list-view") < dashboardSource.indexOf("scan-map-view"),
|
||||
"mobile city list should be the lightweight entry before the optional map view",
|
||||
);
|
||||
assert(
|
||||
airportEvidenceSource.includes("FOCUS_RUNWAY_PAIRS") &&
|
||||
airportEvidenceSource.includes("chongqing") &&
|
||||
airportEvidenceSource.includes("seoul") &&
|
||||
!airportEvidenceSource.includes("busan:"),
|
||||
"airport evidence must only expose configured focused runways, not all runway observations",
|
||||
);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export function useScanTerminalQuery({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void fetchScanTerminal({ forceRefresh: true, showLoading: false });
|
||||
void fetchScanTerminal({ forceRefresh: false, showLoading: false });
|
||||
}, scanTerminalQueryPolicy.autoRefreshMs);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [fetchScanTerminal, isLoading, isPro, proAccessLoading]);
|
||||
|
||||
@@ -96,6 +96,12 @@ SCAN_TERMINAL_BUILD_TIMEOUT_SEC = max(
|
||||
8,
|
||||
int(os.getenv("POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC", "22")),
|
||||
)
|
||||
SCAN_TERMINAL_MAX_WORKERS = _env_int(
|
||||
"POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS",
|
||||
2,
|
||||
min_value=1,
|
||||
max_value=4,
|
||||
)
|
||||
DEFAULT_SCAN_AI_MODEL = "mimo-v2.5-pro"
|
||||
DEFAULT_SCAN_AI_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1"
|
||||
SCAN_AI_API_KEY_ENV_HINT = (
|
||||
@@ -1126,7 +1132,7 @@ def _build_scan_terminal_payload_uncached(
|
||||
|
||||
try:
|
||||
city_names = list(CITIES.keys())
|
||||
max_workers = max(1, min(4, len(city_names)))
|
||||
max_workers = max(1, min(SCAN_TERMINAL_MAX_WORKERS, len(city_names)))
|
||||
city_results: List[Dict[str, Any]] = []
|
||||
failed_cities: List[str] = []
|
||||
failed_reasons: List[str] = []
|
||||
|
||||
Reference in New Issue
Block a user