feat: implement scan terminal dashboard system and supporting services

This commit is contained in:
2569718930@qq.com
2026-05-28 08:24:50 +08:00
parent 725727d763
commit 5d784f78b9
27 changed files with 263 additions and 961 deletions
+1 -2
View File
@@ -37,8 +37,7 @@ PolyWeather Pro 的生产前端工程。
- 右侧详情面板在多日预报仍未补齐时会显示同步占位卡,不再把“只有今天一张卡”的中间态伪装成完整数据
- 日内分析弹窗在 full detail / market scan 同步时会锁住旧内容并显示刷新状态,避免用户短暂看到旧城市或旧日期的数据
- 城市决策卡支持从地图点击城市进入;机会榜和日历仍按 Pro 权限控制,地图探索和城市简报可作为轻量入口
- 城市决策卡的 AI 机场报文解读包括最终判断、METAR 解读、推理说明、模型集群备注、风险提示和原始 METAR
- AI 机场报文解读按 `city + local_date + locale + METAR signature` 做页面内存缓存和 `localStorage` 最终结果缓存;切换选项卡返回时会优先恢复已有内容
- 城市决策卡展示结构化实况、模型区间、市场温度桶和模型-市场差,不再请求 AI 解读
- 市场价格层使用完整 `all_buckets` 匹配温度桶,并把 `模型-市场差` 解释为 `模型概率 - 市场隐含概率`
- 概率区展示当前生产概率引擎输出(legacy 高斯或 EMOS),模型共识只作为辅助参考
- 缓存桶状态与 summary cache hit/miss
@@ -1,75 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyAuthResponseCookies,
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const SCAN_AI_PROXY_TIMEOUT_MS = Math.max(
35_000,
Number(process.env.POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS || "45000") || 45_000,
);
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export async function POST(req: NextRequest) {
if (!API_BASE) {
return NextResponse.json(
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
{ status: 500 },
);
}
let body: unknown = {};
try {
body = await req.json();
} catch {
body = {};
}
let auth: Awaited<ReturnType<typeof buildBackendRequestHeaders>> | null = null;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), SCAN_AI_PROXY_TIMEOUT_MS);
try {
auth = await buildBackendRequestHeaders(req);
const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json");
headers.set("Accept", "application/json");
const res = await fetch(`${API_BASE}/api/scan/terminal/ai`, {
method: "POST",
headers,
cache: "no-store",
signal: controller.signal,
body: JSON.stringify(body || {}),
});
if (!res.ok) {
const raw = await res.text();
const response = buildUpstreamErrorResponse(res.status, raw);
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
const response = NextResponse.json(data, {
headers: {
"Cache-Control": "no-store",
},
});
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const timedOut = controller.signal.aborted;
const response = buildProxyExceptionResponse(error, {
publicMessage: timedOut
? "Scan AI request timed out"
: "Failed to fetch scan AI data",
status: timedOut ? 504 : 500,
});
return auth ? applyAuthResponseCookies(response, auth.response) : response;
} finally {
clearTimeout(timeoutId);
}
}
@@ -1026,41 +1026,41 @@
color: #64748b;
}
.root :global(.scan-terminal.light .scan-ai-city-card),
.root :global(.scan-terminal.light .scan-ai-city-hero) {
.root :global(.scan-terminal.light .scan-city-card),
.root :global(.scan-terminal.light .scan-city-hero) {
background: var(--bg-card);
border-color: var(--border-glass);
}
.root :global(.scan-terminal.light .scan-ai-city-section),
.root :global(.scan-terminal.light .scan-city-section),
.root :global(.scan-terminal.light .scan-ai-decision-band),
.root :global(.scan-terminal.light .scan-ai-market-decision),
.root :global(.scan-terminal.light .scan-ai-market-decision-stats small),
.root :global(.scan-terminal.light .scan-ai-decision-metrics span),
.root :global(.scan-terminal.light .scan-ai-market-bucket),
.root :global(.scan-terminal.light .scan-ai-city-pills span),
.root :global(.scan-terminal.light .scan-ai-city-freshness span),
.root :global(.scan-terminal.light .scan-ai-city-metrics > span) {
.root :global(.scan-terminal.light .scan-city-pills span),
.root :global(.scan-terminal.light .scan-city-freshness span),
.root :global(.scan-terminal.light .scan-city-metrics > span) {
background: var(--bg-secondary);
border-color: var(--border-glass);
}
.root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) {
.root :global(.scan-terminal.light .scan-city-metrics > span.primary) {
background: #dbeafe;
border-color: rgba(59, 130, 246, 0.28);
}
.root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) {
.root :global(.scan-terminal.light .scan-city-metrics > span.primary b) {
color: #1d4ed8;
}
.root :global(.scan-terminal.light .scan-ai-city-metrics small) {
.root :global(.scan-terminal.light .scan-city-metrics small) {
color: #64748b;
}
.root :global(.scan-terminal.light .scan-ai-workspace-head strong),
.root :global(.scan-terminal.light .scan-ai-city-hero h3),
.root :global(.scan-terminal.light .scan-ai-city-metrics b),
.root :global(.scan-terminal.light .scan-city-hero h3),
.root :global(.scan-terminal.light .scan-city-metrics b),
.root :global(.scan-terminal.light .scan-ai-decision-band strong),
.root :global(.scan-terminal.light .scan-ai-decision-metrics b),
.root :global(.scan-terminal.light .scan-ai-market-bucket strong),
@@ -1071,18 +1071,18 @@
}
.root :global(.scan-terminal.light .scan-ai-workspace-head p),
.root :global(.scan-terminal.light .scan-ai-city-section p),
.root :global(.scan-terminal.light .scan-city-section p),
.root :global(.scan-terminal.light .scan-ai-decision-band p),
.root :global(.scan-terminal.light .scan-ai-city-pills span),
.root :global(.scan-terminal.light .scan-ai-city-freshness),
.root :global(.scan-terminal.light .scan-city-pills span),
.root :global(.scan-terminal.light .scan-city-freshness),
.root :global(.scan-terminal.light .scan-ai-decision-metrics span),
.root :global(.scan-terminal.light .scan-ai-market-bucket span),
.root :global(.scan-terminal.light .scan-ai-market-decision span),
.root :global(.scan-terminal.light .scan-ai-market-decision p),
.root :global(.scan-terminal.light .scan-ai-market-decision-stats small),
.root :global(.scan-terminal.light .scan-ai-city-muted),
.root :global(.scan-terminal.light .scan-ai-city-loading),
.root :global(.scan-terminal.light .scan-ai-city-chart-legend),
.root :global(.scan-terminal.light .scan-city-muted),
.root :global(.scan-terminal.light .scan-city-loading),
.root :global(.scan-terminal.light .scan-city-chart-legend),
.root :global(.scan-terminal.light .scan-ai-weather-bullets) {
color: var(--text-muted);
}
@@ -1092,7 +1092,7 @@
color: #64748b;
}
.root :global(.scan-terminal.light .scan-ai-city-chart-placeholder) {
.root :global(.scan-terminal.light .scan-city-chart-placeholder) {
border-color: #cbd5e1;
background: #f8fafc;
color: #64748b;
@@ -1152,25 +1152,25 @@
}
.root :global(.scan-terminal.light .scan-ai-summary-card),
.root :global(.scan-terminal.light .scan-ai-city-card) {
.root :global(.scan-terminal.light .scan-city-card) {
background: rgba(255, 255, 255, 0.92);
border-color: rgba(148, 163, 184, 0.28);
}
.root :global(.scan-terminal.light .scan-ai-summary-card strong),
.root :global(.scan-terminal.light .scan-ai-city-head strong),
.root :global(.scan-terminal.light .scan-city-head strong),
.root :global(.scan-terminal.light .scan-ai-contract b) {
color: var(--text-primary);
}
.root :global(.scan-terminal.light .scan-ai-summary-card p),
.root :global(.scan-terminal.light .scan-ai-city-head p),
.root :global(.scan-terminal.light .scan-city-head p),
.root :global(.scan-terminal.light .scan-ai-contract p),
.root :global(.scan-terminal.light .scan-ai-contract small) {
color: var(--text-muted);
}
.root :global(.scan-terminal.light .scan-ai-city-head) {
.root :global(.scan-terminal.light .scan-city-head) {
background: #f8fafc;
}
@@ -1334,57 +1334,57 @@
}
/* ── AI city card light overrides ── */
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-section),
:global(html.light) .root :global(.scan-terminal.light .scan-city-card),
:global(html.light) .root :global(.scan-terminal.light .scan-city-section),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-band),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-decision),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-metrics span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-pills span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-freshness span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics > span),
:global(html.light) .root :global(.scan-terminal.light .scan-city-pills span),
:global(html.light) .root :global(.scan-terminal.light .scan-city-freshness span),
:global(html.light) .root :global(.scan-terminal.light .scan-city-metrics > span),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-metrics span),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-reason) {
background: #eef7ff;
border-color: rgba(37, 99, 235, 0.16);
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-metrics > span.primary) {
background: #dbeafe;
border-color: rgba(59, 130, 246, 0.34);
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics b) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-metrics b) {
color: var(--text-primary);
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-metrics > span.primary b) {
color: #1d4ed8;
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics small) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-metrics small) {
color: #64748b;
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card p),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card li),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card small),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card span:not(.scan-ai-city-kicker)) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-card p),
:global(html.light) .root :global(.scan-terminal.light .scan-city-card li),
:global(html.light) .root :global(.scan-terminal.light .scan-city-card small),
:global(html.light) .root :global(.scan-terminal.light .scan-city-card span:not(.scan-city-kicker)) {
color: var(--text-secondary);
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.green) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-status-tag.green) {
color: #047857;
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.blue) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-status-tag.blue) {
color: #1d4ed8;
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.amber) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-status-tag.amber) {
color: #b45309;
}
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-status-tag.red) {
color: #b91c1c;
}
@@ -1398,7 +1398,7 @@
}
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
:global(html.light) .root :global(.scan-terminal.light .scan-city-mobile-priority b),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-metrics b),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-reason),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-head h3),
@@ -1408,7 +1408,7 @@
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement li),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
:global(html.light) .root :global(.scan-terminal.light .scan-city-mobile-priority small),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-metrics small),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
color: var(--text-secondary);
@@ -1419,7 +1419,7 @@
}
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement li),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
:global(html.light) .root :global(.scan-terminal.light .scan-city-mobile-priority span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-why),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-mobile-line) {
background: #eef7ff;
@@ -1431,7 +1431,7 @@
}
/* ── Section titles ── */
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-section-title) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-section-title) {
color: #1d4ed8;
}
@@ -1448,11 +1448,11 @@
}
/* ── Scrollbar light overrides ── */
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-body) {
:global(html.light) .root :global(.scan-terminal.light .scan-city-body) {
scrollbar-color: rgba(37, 99, 235, 0.18) transparent;
}
:global(html.light) .root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) {
:global(html.light) .root :global(.scan-city-body::-webkit-scrollbar-thumb) {
background: rgba(37, 99, 235, 0.22);
}
@@ -517,20 +517,20 @@
margin: 0;
}
.root :global(.scan-ai-city-list) {
.root :global(.scan-city-list) {
display: flex;
flex-direction: column;
gap: 12px;
}
.root :global(.scan-ai-city-card) {
.root :global(.scan-city-card) {
border: 1px solid rgba(68, 100, 150, 0.2);
border-radius: 18px;
overflow: hidden;
background: rgba(10, 24, 42, 0.82);
}
.root :global(.scan-ai-city-head) {
.root :global(.scan-city-head) {
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -539,13 +539,13 @@
background: rgba(15, 34, 57, 0.9);
}
.root :global(.scan-ai-city-head strong) {
.root :global(.scan-city-head strong) {
color: #f2f8ff;
font-size: 17px;
font-weight: 900;
}
.root :global(.scan-ai-city-head p) {
.root :global(.scan-city-head p) {
margin: 6px 0 0;
color: #9fb4d2;
font-size: 13px;
@@ -553,7 +553,7 @@
line-height: 1.45;
}
.root :global(.scan-ai-city-head span) {
.root :global(.scan-city-head span) {
flex: 0 0 auto;
padding: 5px 9px;
border-radius: 999px;
@@ -370,12 +370,12 @@
white-space: normal;
}
.root :global(.scan-ai-city-stack) {
.root :global(.scan-city-stack) {
display: grid;
gap: 18px;
}
.root :global(.scan-ai-city-card) {
.root :global(.scan-city-card) {
overflow: hidden;
border: 1px solid rgba(77, 163, 255, 0.35);
border-radius: 18px;
@@ -390,13 +390,13 @@
background 0.18s ease;
}
.root :global(.scan-ai-city-card.removing) {
.root :global(.scan-city-card.removing) {
pointer-events: none;
opacity: 0;
transform: translateX(44px);
}
.root :global(.scan-ai-city-hero) {
.root :global(.scan-city-hero) {
display: flex;
justify-content: space-between;
align-items: flex-start;
@@ -408,35 +408,35 @@
border-bottom: 1px solid rgba(159, 178, 199, 0.12);
}
.root :global(.scan-ai-city-hero-left) {
.root :global(.scan-city-hero-left) {
min-width: 0;
flex: 1;
}
.root :global(.scan-ai-city-kicker) {
.root :global(.scan-city-kicker) {
color: var(--color-accent-primary);
font-size: 12px;
font-weight: 800;
}
.root :global(.scan-ai-city-hero h3) {
.root :global(.scan-city-hero h3) {
margin: 4px 0 8px;
color: var(--color-text-primary);
font-size: 22px;
line-height: 1.15;
}
.root :global(.scan-ai-city-mobile-priority) {
.root :global(.scan-city-mobile-priority) {
display: none;
}
.root :global(.scan-ai-city-pills) {
.root :global(.scan-city-pills) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.root :global(.scan-ai-city-pills span) {
.root :global(.scan-city-pills span) {
border: 1px solid rgba(159, 178, 199, 0.14);
border-radius: 10px;
background: rgba(11, 18, 32, 0.58);
@@ -446,14 +446,14 @@
padding: 7px 10px;
}
.root :global(.scan-ai-city-status-tags) {
.root :global(.scan-city-status-tags) {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: -2px 0 10px;
}
.root :global(.scan-ai-city-status-tag) {
.root :global(.scan-city-status-tag) {
border: 1px solid rgba(159, 178, 199, 0.16);
border-radius: 999px;
background: rgba(15, 23, 42, 0.62);
@@ -464,35 +464,35 @@
padding: 5px 9px;
}
.root :global(.scan-ai-city-status-tag.green) {
.root :global(.scan-city-status-tag.green) {
border-color: rgba(34, 197, 94, 0.34);
background: rgba(34, 197, 94, 0.12);
color: #86efac;
}
.root :global(.scan-ai-city-status-tag.blue) {
.root :global(.scan-city-status-tag.blue) {
border-color: rgba(77, 163, 255, 0.36);
background: rgba(77, 163, 255, 0.13);
color: #9ecbff;
}
.root :global(.scan-ai-city-status-tag.amber) {
.root :global(.scan-city-status-tag.amber) {
border-color: rgba(245, 158, 11, 0.38);
background: rgba(245, 158, 11, 0.13);
color: #fcd34d;
}
.root :global(.scan-ai-city-status-tag.red) {
.root :global(.scan-city-status-tag.red) {
border-color: rgba(248, 113, 113, 0.4);
background: rgba(239, 68, 68, 0.13);
color: #fca5a5;
}
.root :global(.scan-ai-city-status-tag.muted) {
.root :global(.scan-city-status-tag.muted) {
color: #94a3b8;
}
.root :global(.scan-ai-city-freshness) {
.root :global(.scan-city-freshness) {
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -503,7 +503,7 @@
font-weight: 700;
}
.root :global(.scan-ai-city-freshness strong) {
.root :global(.scan-city-freshness strong) {
color: #7d92b2;
font-size: 10px;
font-weight: 800;
@@ -512,7 +512,7 @@
margin-right: 2px;
}
.root :global(.scan-ai-city-freshness span) {
.root :global(.scan-city-freshness span) {
display: inline-flex;
align-items: center;
gap: 2px;
@@ -522,37 +522,37 @@
font-size: 11px;
}
.root :global(.scan-ai-city-freshness b) {
.root :global(.scan-city-freshness b) {
color: #d6e2f0;
font-style: normal;
font-weight: 900;
}
.root :global(.scan-ai-city-freshness em) {
.root :global(.scan-city-freshness em) {
color: var(--color-text-secondary);
font-style: normal;
}
.root :global(.scan-ai-city-freshness span.fresh em),
.root :global(.scan-ai-city-freshness span.green em) {
.root :global(.scan-city-freshness span.fresh em),
.root :global(.scan-city-freshness span.green em) {
color: #86efac;
}
.root :global(.scan-ai-city-freshness span.loading em),
.root :global(.scan-ai-city-freshness span.blue em) {
.root :global(.scan-city-freshness span.loading em),
.root :global(.scan-city-freshness span.blue em) {
color: #9ecbff;
}
.root :global(.scan-ai-city-freshness span.stale em),
.root :global(.scan-ai-city-freshness span.amber em) {
.root :global(.scan-city-freshness span.stale em),
.root :global(.scan-city-freshness span.amber em) {
color: #fcd34d;
}
.root :global(.scan-ai-city-freshness span.red em) {
.root :global(.scan-city-freshness span.red em) {
color: #fca5a5;
}
.root :global(.scan-ai-city-hero-side) {
.root :global(.scan-city-hero-side) {
display: flex;
flex-direction: column;
align-items: flex-end;
@@ -560,13 +560,13 @@
flex-shrink: 0;
}
.root :global(.scan-ai-city-metrics) {
.root :global(.scan-city-metrics) {
display: grid;
grid-template-columns: repeat(3, auto);
gap: 10px;
}
.root :global(.scan-ai-city-metrics > span) {
.root :global(.scan-city-metrics > span) {
display: grid;
gap: 3px;
min-width: 88px;
@@ -577,12 +577,12 @@
background: rgba(11, 18, 32, 0.42);
}
.root :global(.scan-ai-city-metrics > span.primary) {
.root :global(.scan-city-metrics > span.primary) {
border-color: rgba(77, 163, 255, 0.28);
background: rgba(77, 163, 255, 0.09);
}
.root :global(.scan-ai-city-metrics small) {
.root :global(.scan-city-metrics small) {
color: var(--color-text-muted);
font-size: 10px;
font-weight: 800;
@@ -590,26 +590,26 @@
text-transform: uppercase;
}
.root :global(.scan-ai-city-metrics b) {
.root :global(.scan-city-metrics b) {
color: var(--color-text-primary);
font-size: 20px;
font-weight: 900;
line-height: 1.1;
}
.root :global(.scan-ai-city-metrics > span.primary b) {
.root :global(.scan-city-metrics > span.primary b) {
color: #9ecbff;
}
.root :global(.scan-ai-city-actions) {
.root :global(.scan-city-actions) {
display: inline-flex;
justify-content: flex-end;
gap: 8px;
}
.root :global(.scan-ai-city-icon-button),
.root :global(.scan-ai-city-collapse),
.root :global(.scan-ai-city-price-button) {
.root :global(.scan-city-icon-button),
.root :global(.scan-city-collapse),
.root :global(.scan-city-price-button) {
min-height: 36px;
display: inline-flex;
align-items: center;
@@ -624,70 +624,70 @@
transition: background 0.18s ease, border-color 0.18s ease;
}
.root :global(.scan-ai-city-icon-button) {
.root :global(.scan-city-icon-button) {
width: 36px;
justify-content: center;
padding: 0;
}
.root :global(.scan-ai-city-icon-button.danger) {
.root :global(.scan-city-icon-button.danger) {
border-color: rgba(239, 68, 68, 0.34);
background: rgba(239, 68, 68, 0.1);
color: #fca5a5;
}
.root :global(.scan-ai-city-icon-button.danger:hover) {
.root :global(.scan-city-icon-button.danger:hover) {
border-color: rgba(239, 68, 68, 0.52);
background: rgba(239, 68, 68, 0.14);
color: #ff9aa4;
}
.root :global(.scan-ai-city-icon-button:disabled) {
.root :global(.scan-city-icon-button:disabled) {
cursor: wait;
opacity: 0.68;
}
.root :global(.scan-ai-city-icon-button .spin) {
.root :global(.scan-city-icon-button .spin) {
animation: spin 1s linear infinite;
}
.root :global(.scan-ai-city-collapse) {
.root :global(.scan-city-collapse) {
padding: 8px 10px;
}
.root :global(.scan-ai-city-card.collapsed .scan-ai-city-collapse svg) {
.root :global(.scan-city-card.collapsed .scan-city-collapse svg) {
transform: rotate(-90deg);
}
.root :global(.scan-ai-city-price-button) {
.root :global(.scan-city-price-button) {
padding: 9px 12px;
}
.root :global(.scan-ai-city-icon-button:hover),
.root :global(.scan-ai-city-collapse:hover),
.root :global(.scan-ai-city-price-button:hover) {
.root :global(.scan-city-icon-button:hover),
.root :global(.scan-city-collapse:hover),
.root :global(.scan-city-price-button:hover) {
background: rgba(77, 163, 255, 0.18);
border-color: rgba(111, 183, 255, 0.58);
}
.root :global(.scan-ai-city-price-button:disabled) {
.root :global(.scan-city-price-button:disabled) {
cursor: wait;
opacity: 0.62;
}
.root :global(.scan-ai-city-body) {
.root :global(.scan-city-body) {
padding: 18px 18px 36px;
max-height: none;
overflow: visible;
overscroll-behavior: contain;
}
.root :global(.scan-ai-city-body::-webkit-scrollbar) {
.root :global(.scan-city-body::-webkit-scrollbar) {
width: 8px;
}
.root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) {
.root :global(.scan-city-body::-webkit-scrollbar-thumb) {
border-radius: 999px;
background: rgba(77, 163, 255, 0.28);
}
@@ -859,14 +859,14 @@
font-size: 15px;
}
.root :global(.scan-ai-city-analysis-grid) {
.root :global(.scan-city-analysis-grid) {
display: grid;
grid-template-columns: minmax(420px, 1.4fr) minmax(280px, 0.8fr);
gap: 14px;
margin-top: 14px;
}
.root :global(.scan-ai-city-section) {
.root :global(.scan-city-section) {
border: 1px solid rgba(159, 178, 199, 0.12);
border-radius: 16px;
background: rgba(11, 18, 32, 0.38);
@@ -911,7 +911,7 @@
line-height: 1.4;
}
.root :global(.scan-ai-city-section-title) {
.root :global(.scan-city-section-title) {
display: inline-flex;
align-items: center;
gap: 8px;
@@ -921,16 +921,16 @@
margin-bottom: 12px;
}
.root :global(.scan-ai-city-ai-read) {
.root :global(.scan-city-ai-read) {
overflow: hidden;
}
.root :global(.scan-ai-city-ai-read summary::-webkit-details-marker) {
.root :global(.scan-city-ai-read summary::-webkit-details-marker) {
display: none;
}
.root :global(.scan-ai-city-ai-read summary::after) {
.root :global(.scan-city-ai-read summary::after) {
width: 18px;
height: 18px;
display: inline-grid;
@@ -944,15 +944,15 @@
transition: transform 0.18s ease;
}
.root :global(.scan-ai-city-ai-read[open] summary::after) {
.root :global(.scan-city-ai-read[open] summary::after) {
transform: rotate(180deg);
}
.root :global(.scan-ai-city-section-body) {
.root :global(.scan-city-section-body) {
margin-top: 2px;
}
.root :global(.scan-ai-city-section p) {
.root :global(.scan-city-section p) {
margin: 0 0 10px;
color: var(--color-text-secondary);
font-size: 13px;
@@ -1101,17 +1101,17 @@
font-size: 12px;
}
.root :global(.scan-ai-city-chart) {
.root :global(.scan-city-chart) {
height: 260px;
}
.root :global(.scan-ai-city-chart canvas) {
.root :global(.scan-city-chart canvas) {
display: block;
width: 100%;
height: 100%;
}
.root :global(.scan-ai-city-chart-placeholder) {
.root :global(.scan-city-chart-placeholder) {
display: grid;
height: 100%;
place-items: center;
@@ -1123,7 +1123,7 @@
font-weight: 800;
}
.root :global(.scan-ai-city-chart-legend) {
.root :global(.scan-city-chart-legend) {
display: flex;
gap: 14px;
margin-top: 10px;
@@ -1132,20 +1132,20 @@
font-weight: 800;
}
.root :global(.scan-ai-city-chart-legend span) {
.root :global(.scan-city-chart-legend span) {
display: inline-flex;
align-items: center;
gap: 7px;
}
.root :global(.scan-ai-city-chart-legend i) {
.root :global(.scan-city-chart-legend i) {
width: 18px;
height: 3px;
border-radius: 999px;
background: rgba(100, 116, 139, 0.72);
}
.root :global(.scan-ai-city-chart-legend i.forecast) {
.root :global(.scan-city-chart-legend i.forecast) {
background: repeating-linear-gradient(
90deg,
rgba(100, 116, 139, 0.72) 0 6px,
@@ -1153,27 +1153,27 @@
);
}
.root :global(.scan-ai-city-chart-legend i.calibrated) {
.root :global(.scan-city-chart-legend i.calibrated) {
background: #38bdf8;
}
.root :global(.scan-ai-city-chart-legend i.observation) {
.root :global(.scan-city-chart-legend i.observation) {
width: 8px;
height: 8px;
background: #22c55e;
}
.root :global(.scan-ai-city-section.models) {
.root :global(.scan-city-section.models) {
margin-top: 14px;
}
.root :global(.scan-ai-city-section.models .models-section) {
.root :global(.scan-city-section.models .models-section) {
padding: 0;
border: 0;
background: transparent;
}
.root :global(.scan-ai-city-section-head) {
.root :global(.scan-city-section-head) {
display: flex;
justify-content: space-between;
gap: 14px;
@@ -1181,7 +1181,7 @@
margin-bottom: 12px;
}
.root :global(.scan-ai-city-section.market) {
.root :global(.scan-city-section.market) {
margin-top: 14px;
}
@@ -1206,14 +1206,14 @@
}
.root :global(.scan-ai-market-bucket span),
.root :global(.scan-ai-city-muted),
.root :global(.scan-ai-city-loading) {
.root :global(.scan-city-muted),
.root :global(.scan-city-loading) {
color: var(--color-text-secondary);
font-size: 12px;
font-weight: 700;
}
.root :global(.scan-ai-city-loading) {
.root :global(.scan-city-loading) {
padding: 28px;
}
@@ -477,7 +477,7 @@
text-align: right;
}
.root :global(.scan-ai-city-jumpbar) {
.root :global(.scan-city-jumpbar) {
display: flex;
gap: 8px;
margin: 0 0 12px;
@@ -490,11 +490,11 @@
scrollbar-width: none;
}
.root :global(.scan-ai-city-jumpbar::-webkit-scrollbar) {
.root :global(.scan-city-jumpbar::-webkit-scrollbar) {
display: none;
}
.root :global(.scan-ai-city-jumpbar button) {
.root :global(.scan-city-jumpbar button) {
flex: 0 0 auto;
min-height: 32px;
padding: 0 12px;
@@ -508,13 +508,13 @@
white-space: nowrap;
}
.root :global(.scan-ai-city-jumpbar button:hover) {
.root :global(.scan-city-jumpbar button:hover) {
border-color: rgba(111, 183, 255, 0.62);
background: rgba(77, 163, 255, 0.18);
}
.root :global(.scan-ai-city-card:not(.collapsed)) {
.root :global(.scan-city-card:not(.collapsed)) {
overflow: visible;
}
@@ -522,19 +522,19 @@
@keyframes spin { to { transform: rotate(360deg); } }
/* ── City Jumpbar Light Mode Overrides ── */
:global(html.light) .root :global(.scan-ai-city-jumpbar) {
:global(html.light) .root :global(.scan-city-jumpbar) {
background: #ffffff;
border-color: #d8e0ec;
box-shadow: var(--shadow-elevation-1);
}
:global(html.light) .root :global(.scan-ai-city-jumpbar button) {
:global(html.light) .root :global(.scan-city-jumpbar button) {
background: var(--color-bg-input);
border-color: var(--color-border-default);
color: var(--color-text-secondary);
}
:global(html.light) .root :global(.scan-ai-city-jumpbar button:hover) {
:global(html.light) .root :global(.scan-city-jumpbar button:hover) {
background: #e2e8f0;
border-color: var(--color-border-hover);
color: var(--color-text-primary);
@@ -155,15 +155,15 @@
padding: 11px 12px;
}
.root :global(.scan-mobile-decision-card .scan-ai-city-status-tags),
.root :global(.scan-mobile-decision-card .scan-ai-city-freshness),
.root :global(.scan-mobile-decision-card .scan-city-status-tags),
.root :global(.scan-mobile-decision-card .scan-city-freshness),
.root :global(.scan-mobile-decision-card .scan-ai-market-mobile-line),
.root :global(.scan-mobile-decision-folds) {
margin-right: 14px;
margin-left: 14px;
}
.root :global(.scan-mobile-decision-card .scan-ai-city-freshness) {
.root :global(.scan-mobile-decision-card .scan-city-freshness) {
grid-template-columns: 1fr;
max-width: none;
margin-top: 0;
@@ -204,13 +204,13 @@
margin-bottom: 10px;
}
.root :global(.scan-mobile-fold .scan-ai-city-section.models) {
.root :global(.scan-mobile-fold .scan-city-section.models) {
border: 0;
background: transparent;
padding: 0;
}
.root :global(.scan-ai-city-collapse svg) {
.root :global(.scan-city-collapse svg) {
transition: transform 0.18s ease;
}
@@ -300,7 +300,7 @@
.root :global(.scan-forecast-city-head),
.root :global(.scan-forecast-row-main),
.root :global(.scan-ai-brief-grid),
.root :global(.scan-ai-city-analysis-grid),
.root :global(.scan-city-analysis-grid),
.root :global(.scan-ai-decision-band),
.root :global(.scan-ai-evidence-line) {
grid-template-columns: 1fr;
@@ -309,8 +309,8 @@
.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) {
.root :global(.scan-city-hero),
.root :global(.scan-city-section-head) {
flex-direction: column;
align-items: flex-start;
}
@@ -321,7 +321,7 @@
.root :global(.scan-ai-workspace-head p),
.root :global(.scan-mobile-city-list-head p),
.root :global(.scan-ai-city-hero-side) {
.root :global(.scan-city-hero-side) {
text-align: left;
justify-items: start;
}
@@ -424,23 +424,23 @@
padding: 6px 8px;
}
.root :global(.scan-ai-city-hero) {
.root :global(.scan-city-hero) {
padding: 16px;
}
.root :global(.scan-ai-city-hero h3) {
.root :global(.scan-city-hero h3) {
margin-bottom: 10px;
font-size: 24px;
}
.root :global(.scan-ai-city-mobile-priority) {
.root :global(.scan-city-mobile-priority) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin: 0 0 12px;
}
.root :global(.scan-ai-city-mobile-priority span) {
.root :global(.scan-city-mobile-priority span) {
display: grid;
gap: 4px;
border: 1px solid rgba(77, 163, 255, 0.18);
@@ -449,23 +449,23 @@
padding: 9px;
}
.root :global(.scan-ai-city-mobile-priority small) {
.root :global(.scan-city-mobile-priority small) {
color: var(--color-text-secondary);
font-size: 10px;
font-weight: 900;
}
.root :global(.scan-ai-city-mobile-priority b) {
.root :global(.scan-city-mobile-priority b) {
color: #f3f8ff;
font-size: 13px;
line-height: 1.15;
}
.root :global(.scan-ai-city-pills) {
.root :global(.scan-city-pills) {
display: none;
}
.root :global(.scan-ai-city-freshness) {
.root :global(.scan-city-freshness) {
grid-template-columns: 1fr;
}
@@ -505,7 +505,7 @@
margin-top: 10px;
}
.root :global(.scan-ai-city-ai-read:not([open])) {
.root :global(.scan-city-ai-read:not([open])) {
padding-bottom: 4px;
}
+17 -17
View File
@@ -55,14 +55,14 @@ export const DOCS_PAGES: DocsPage[] = [
id: "core-modules",
title: "你会在页面上看到什么",
blocks: [
{ type: "bullets", items: ["锚点状态:先确认当前机场主站实测、日内已见高点和结算时钟。", "当前节奏:把“此刻应到温度”和“机场实测”放在一张卡里,判断今天跑得快还是慢。", "专业气象结论条:先给今日主判断、置信度、基准/上修/下修路径和下一观测点。", "城市决策卡:从地图进入城市简报,读取 AI 机场报文解读、最高温中枢、市场温度桶和模型-市场差。", "校准模型概率 / 模型区间与分歧:概率层看当前生产概率引擎输出;EMOS / LGBM 只有在评估通过或 shadow 对照时进入解释层,模型区间用于解释分歧。", "气象证据链 / 失效条件 / 确认条件:解释为什么这么判断,以及什么情况会让判断降级。"] },
{ type: "bullets", items: ["锚点状态:先确认当前机场主站实测、日内已见高点和结算时钟。", "当前节奏:把“此刻应到温度”和“机场实测”放在一张卡里,判断今天跑得快还是慢。", "专业气象结论条:先给今日主判断、置信度、基准/上修/下修路径和下一观测点。", "城市决策卡:从地图进入城市简报,读取结构化实况、最高温中枢、市场温度桶和模型-市场差。", "校准模型概率 / 模型区间与分歧:概率层看当前生产概率引擎输出;EMOS / LGBM 只有在评估通过或 shadow 对照时进入解释层,模型区间用于解释分歧。", "气象证据链 / 失效条件 / 确认条件:解释为什么这么判断,以及什么情况会让判断降级。"] },
],
},
{
id: "how-to-read",
title: "如何快速读懂主站",
blocks: [
{ type: "steps", items: ["先看专业气象结论条或城市决策卡,确认今日主判断、最高温中枢和下一观测点。", "再看锚点状态和今日气温预测图,确认机场实测、DEB、峰值窗口和关键档位线。", "接着看 AI 机场报文解读、气象证据链、失效条件和确认条件,判断这个路径有没有被新观测破坏。", "最后看校准模型概率、模型区间、市场温度桶和模型-市场差,判断概率是否已经被市场充分计价。"] },
{ type: "steps", items: ["先看专业气象结论条或城市决策卡,确认今日主判断、最高温中枢和下一观测点。", "再看锚点状态和今日气温预测图,确认机场实测、DEB、峰值窗口和关键档位线。", "接着看气象证据链、失效条件和确认条件,判断这个路径有没有被新观测破坏。", "最后看校准模型概率、模型区间、市场温度桶和模型-市场差,判断概率是否已经被市场充分计价。"] },
],
},
],
@@ -183,21 +183,21 @@ export const DOCS_PAGES: DocsPage[] = [
content: {
"zh-CN": {
title: "城市决策卡",
description: "这页解释地图城市决策卡如何把 AI 机场报文解读、最高温中枢、市场温度桶和模型-市场差组合成可验证判断。",
description: "这页解释地图城市决策卡如何把结构化实况、最高温中枢、市场温度桶和模型-市场差组合成可验证判断。",
sections: [
{
id: "entry-and-permission",
title: "从地图进入决策卡",
blocks: [
{ type: "paragraph", text: "用户可以从地图点击城市进入城市决策卡。机会榜和日历属于 Pro 能力;地图探索和城市简报仍可作为轻量入口使用。" },
{ type: "callout", tone: "info", title: "先天气、后市场", text: "决策卡顶部的天气判断层不读取市场价格,先用 METAR、DEB多模型集合和 AI 解读确定最高温中枢,再把该中枢映射到市场温度桶。" },
{ type: "callout", tone: "info", title: "先天气、后市场", text: "决策卡顶部的天气判断层不读取市场价格,先用结构化实况、DEB多模型集合确定最高温中枢,再把该中枢映射到市场温度桶。" },
],
},
{
id: "ai-airport-read",
title: "AI 机场报文解读包括什么",
id: "structured-observations",
title: "结构化实况包括什么",
blocks: [
{ type: "bullets", items: ["最终判断:预计最高温中枢、上修/下修空间和当前操作口径。", "METAR 解读:报文时间、实测温度、露点/湿度、风向风速、能见度、云量、气压和 NOSIG / TAF 等机场侧信号。", "推理说明:把最新实测、DEB、多模型集群和午后对流/云雨/风向风险合并成日内节奏判断。", "模型集群备注:展示模型数量、模型区间,以及是否集中在 DEB ±2°C 内。", "风险提示:后续 METAR 或路径明显偏离时,说明应如何上调或下修。", "原始 METAR:保留原始报文,便于人工复核。"] },
{ type: "bullets", items: ["实测锚点:当前温度、当日已见高点、观测时间和数据新鲜度。", "模型区间:DEB、多模型范围,以及模型是否明显分散。", "日内节奏:把实测路径、峰值窗口和目标温度桶放在一起对比。", "市场映射:把最高温中枢映射到 YES/NO 温度桶,并计算模型-市场差。"] },
],
},
{
@@ -213,29 +213,29 @@ export const DOCS_PAGES: DocsPage[] = [
id: "cache-behavior",
title: "为什么切换选项卡后不应重新空白加载",
blocks: [
{ type: "paragraph", text: "城市决策卡会用 city + local_date + locale + METAR signature 作为 AI 解读缓存键。METAR signature 优先使用原始报文,缺失时回退到报文时间、观测时间和温度。" },
{ type: "bullets", items: ["页面内存缓存:保存 loading 状态、流式进度、机场报文解读片段和最终结果;从其他选项卡切回时优先恢复旧内容。", "浏览器 localStorage:保存最终成功的 AI payload,默认 TTL 为 1 小时。", "后端 AI 缓存:不再把 local_time 放入缓存键,避免同一报文因当前时间变化反复失效。", "市场扫描缓存:完整 all_buckets 结果按城市和日期缓存,默认 TTL 为 10 分钟。"] },
{ type: "paragraph", text: "城市决策卡复用城市详情、市场扫描和图表数据缓存,不再单独请求 AI 解读。" },
{ type: "bullets", items: ["城市详情缓存:保存实况、模型、概率和结算上下文。", "市场扫描缓存:完整 all_buckets 结果按城市和日期缓存,默认 TTL 为 10 分钟。", "前端图表缓存:切换城市或选项卡时优先复用已加载的结构化数据。"] },
],
},
],
},
"en-US": {
title: "City Decision Cards",
description: "How the city card combines the AI airport read, expected-high center, market bucket mapping, and model-market difference into a verifiable decision.",
description: "How the city card combines structured observations, expected-high center, market bucket mapping, and model-market difference into a verifiable decision.",
sections: [
{
id: "entry-and-permission",
title: "Opening a card from the map",
blocks: [
{ type: "paragraph", text: "Users can click a city on the map to open its city decision card. The opportunity board and calendar are Pro surfaces; map exploration and city briefs remain the lightweight entry point." },
{ type: "callout", tone: "info", title: "Weather first, market second", text: "The weather decision layer does not use market price input. It first sets the expected-high center from METAR, DEB, the model cluster, and AI reasoning, then maps that center to the relevant market bucket." },
{ type: "callout", tone: "info", title: "Weather first, market second", text: "The weather decision layer does not use market price input. It first sets the expected-high center from structured observations, DEB, and the model cluster, then maps that center to the relevant market bucket." },
],
},
{
id: "ai-airport-read",
title: "What the AI airport read contains",
id: "structured-observations",
title: "What structured observations contain",
blocks: [
{ type: "bullets", items: ["Final judgment: expected-high center, upside/downside room, and the working decision.", "METAR read: report time, observed temperature, dew point / humidity, wind, visibility, clouds, pressure, and NOSIG / TAF airport-side signals.", "Reasoning: combines live observations, DEB, the model cluster, and convective / cloud / wind risks into an intraday pace read.", "Model-cluster note: model count, model range, and whether the cluster sits within DEB ±2°C.", "Risk notes: how later METAR/path breaks should raise or lower the high center.", "Raw METAR: preserved so the read can be manually audited."] },
{ type: "bullets", items: ["Observation anchor: current temperature, daily high so far, observation time, and freshness.", "Model range: DEB, multi-model range, and whether the model cluster is dispersed.", "Intraday pace: live path, peak window, and target temperature bucket in one comparison.", "Market mapping: maps the expected-high center to YES/NO buckets and calculates the model-market difference."] },
],
},
{
@@ -249,10 +249,10 @@ export const DOCS_PAGES: DocsPage[] = [
},
{
id: "cache-behavior",
title: "Why tab switching should not blank the AI read",
title: "Why tab switching should not blank the card",
blocks: [
{ type: "paragraph", text: "The card keys AI reads by city + local_date + locale + METAR signature. The signature prefers the raw report and falls back to report time, observation time, and temperature." },
{ type: "bullets", items: ["In-page memory cache: stores loading state, stream progress, airport-read snippets, and final results so returning from another tab restores prior content first.", "Browser localStorage: stores final successful AI payloads for one hour by default.", "Backend AI cache: excludes local_time from the key so the same report does not expire merely because the current clock changed.", "Market-scan cache: stores full all_buckets results by city and date for 10 minutes by default."] },
{ type: "paragraph", text: "The card reuses city detail, market scan, and chart-data caches. It no longer makes a separate AI-read request." },
{ type: "bullets", items: ["City detail cache: stores observations, models, probabilities, and settlement context.", "Market-scan cache: stores full all_buckets results by city and date for 10 minutes by default.", "Frontend chart cache: reuses already loaded structured data when switching cities or tabs."] },
],
},
],
-63
View File
@@ -640,27 +640,6 @@ export interface ScanOpportunityRow {
ai_confidence?: string | null;
ai_reason_zh?: string | null;
ai_reason_en?: string | null;
ai_model_cluster_note?: string | null;
ai_city_thesis_zh?: string | null;
ai_city_thesis_en?: string | null;
ai_city_confidence?: string | null;
ai_city_model_cluster_note?: string | null;
ai_predicted_max?: number | null;
ai_predicted_low?: number | null;
ai_predicted_high?: number | null;
ai_forecast_unit?: string | null;
ai_forecast_confidence?: string | null;
ai_peak_window_zh?: string | null;
ai_peak_window_en?: string | null;
ai_airport_metar_read_zh?: string | null;
ai_airport_metar_read_en?: string | null;
ai_forecast_reason_zh?: string | null;
ai_forecast_reason_en?: string | null;
ai_forecast_match?: "core" | "edge" | "outside" | "watch" | string | null;
ai_forecast_match_reason_zh?: string | null;
ai_forecast_match_reason_en?: string | null;
ai_watchlist_reason_zh?: string | null;
ai_watchlist_reason_en?: string | null;
v4_metar_decision?: "approve" | "veto" | "downgrade" | "watchlist" | string | null;
v4_metar_reason_zh?: string | null;
v4_metar_reason_en?: string | null;
@@ -668,47 +647,6 @@ export interface ScanOpportunityRow {
export interface PrimarySignal extends ScanOpportunityRow {}
export interface ScanAiWatchlistItem {
row_id: string;
reason?: string | null;
reason_zh?: string | null;
reason_en?: string | null;
}
export interface ScanAiReview {
status?: "ready" | "disabled" | "missing_key" | "failed" | "no_rows" | "no_snapshot" | "snapshot_mismatch" | string;
stage?: "completed" | "fallback" | string | null;
model?: string | null;
cached?: boolean;
generated_at?: string | null;
snapshot_id?: string | null;
input_rows?: number | null;
sent_rows?: number | null;
sent_cities?: number | null;
sent_contracts?: number | null;
duration_ms?: number | null;
timeout_sec?: number | null;
cache_ttl_sec?: number | null;
provider?: string | null;
base_url?: string | null;
finish_reason?: string | null;
usage?: {
prompt_tokens?: number | null;
completion_tokens?: number | null;
total_tokens?: number | null;
prompt_cache_hit_tokens?: number | null;
prompt_cache_miss_tokens?: number | null;
} | null;
reason?: string | null;
summary_zh?: string | null;
summary_en?: string | null;
watchlist?: ScanAiWatchlistItem[] | null;
recommended_count?: number | null;
vetoed_count?: number | null;
downgraded_count?: number | null;
watchlist_count?: number | null;
}
export interface ScanTerminalResponse {
generated_at: string;
snapshot_id?: string | null;
@@ -733,7 +671,6 @@ export interface ScanTerminalResponse {
};
top_signal?: PrimarySignal | null;
rows: ScanOpportunityRow[];
ai_scan?: ScanAiReview | null;
}
export interface IntradayMeteorologySignal {
+10 -10
View File
@@ -96,12 +96,12 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.judgement": "判断",
"future.confidence": "置信度",
"future.maxPrecip": "最大降水概率",
"future.ai": "机场报文解读",
"future.noAi": "暂无机场报文解读,当前以结构化气象与模型数据为主。",
"future.ai": "结构化实况",
"future.noAi": "暂无结构化实况,当前以模型数据为主。",
"future.weatherGov": "weather.gov 文本",
"future.risk": "结算与偏差风险",
"future.climate": "当地气候主要受什么影响",
"future.chartLegendEmpty": "暂无机场报文或小时级实测数据",
"future.chartLegendEmpty": "暂无小时级实测数据",
"confidence.high": "高",
"confidence.medium": "中",
@@ -114,8 +114,8 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"section.noProb": "暂无概率数据",
"section.models": "多模型预报",
"section.noModels": "暂无多模型预报",
"section.ai": "机场报文解读",
"section.aiEmpty": "暂无机场报文解读,当前以结构化气象与模型数据为主。",
"section.ai": "结构化实况",
"section.aiEmpty": "暂无结构化实况,当前以模型数据为主。",
"section.risk": "数据偏差风险",
"section.noRiskProfile": "暂无风险档案",
"section.airport": "机场",
@@ -278,14 +278,14 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.judgement": "Judgement",
"future.confidence": "Confidence",
"future.maxPrecip": "Max Precip Probability",
"future.ai": "Airport METAR Narrative",
"future.ai": "Structured Observations",
"future.noAi":
"No airport bulletin narrative is available. Structured meteorological and model data are used as baseline.",
"No structured observations are available. Model data is used as baseline.",
"future.weatherGov": "weather.gov text",
"future.risk": "Settlement & Deviation Risk",
"future.climate": "What Mainly Drives Local Climate",
"future.chartLegendEmpty":
"No METAR bulletin or hourly observations available",
"No hourly observations available",
"confidence.high": "High",
"confidence.medium": "Medium",
@@ -298,9 +298,9 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"section.noProb": "No probability data available",
"section.models": "Multi-model Forecast",
"section.noModels": "No multi-model forecast available",
"section.ai": "Airport METAR Narrative",
"section.ai": "Structured Observations",
"section.aiEmpty":
"No airport bulletin narrative is available. Structured meteorological data are currently used.",
"No structured observations are available. Model data is currently used.",
"section.risk": "Data Deviation Risk",
"section.noRiskProfile": "No risk profile available",
"section.airport": "Airport",
-1
View File
@@ -169,6 +169,5 @@ export const config = {
"/api/payments/:path*",
"/api/system/:path*",
"/api/city/:path*/detail:path*",
"/api/scan/terminal/ai:path*",
],
};