Surface city-card decision reasons before the paragraph
City decision cards already had enough evidence, but the first screen still forced users to read the longer explanation before seeing why a card mattered. The header now caps status chips at three high-priority signals and uses product-facing labels for observed breakouts, stale METARs, AI loading, missing market prices, strong model agreement, and next-report waits. Constraint: The card should stay lightweight and avoid another explanatory section. Rejected: Keep six mixed freshness chips in the header | too much noise for first-glance scanning. Confidence: high Scope-risk: narrow Reversibility: clean Tested: npm run build Not-tested: Browser visual QA across all city states.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## 1.5.5 - 2026-04-27
|
## 1.5.5 - 2026-04-27
|
||||||
|
|
||||||
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
|
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
|
||||||
|
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
|
||||||
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
|
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
|
||||||
- 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常”
|
- 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常”
|
||||||
- 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间
|
- 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间
|
||||||
|
|||||||
@@ -11334,7 +11334,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
margin-top: 10px;
|
margin: -2px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-city-status-tag) {
|
.root :global(.scan-ai-city-status-tag) {
|
||||||
|
|||||||
@@ -353,6 +353,15 @@ function AiPinnedCityCard({
|
|||||||
String((row as { window_phase?: string | null } | null)?.window_phase || "").toLowerCase(),
|
String((row as { window_phase?: string | null } | null)?.window_phase || "").toLowerCase(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const modelSpread = modelMax != null && modelMin != null ? modelMax - modelMin : null;
|
||||||
|
const modelHighlyConsistent =
|
||||||
|
modelEntries.length >= 4 && modelSpread != null && modelSpread <= 2;
|
||||||
|
const needsNextBulletin =
|
||||||
|
!observationStale &&
|
||||||
|
!observedHighBreak &&
|
||||||
|
!observedLowBreak &&
|
||||||
|
!peakHasPassed &&
|
||||||
|
(observedLowLag || paceTone === "neutral" || aiForecast.status === "loading");
|
||||||
const observationFreshnessLabel = buildObservationFreshnessLabel({
|
const observationFreshnessLabel = buildObservationFreshnessLabel({
|
||||||
detail,
|
detail,
|
||||||
displayTime: metarReportTimeDisplay,
|
displayTime: metarReportTimeDisplay,
|
||||||
@@ -410,43 +419,59 @@ function AiPinnedCityCard({
|
|||||||
const statusTags = uniqueStatusTags([
|
const statusTags = uniqueStatusTags([
|
||||||
observedHighBreak
|
observedHighBreak
|
||||||
? {
|
? {
|
||||||
label: isEn ? "Observed above models" : "实测突破模型",
|
label: isEn ? "Observed breakout" : "实测突破",
|
||||||
tone: "red",
|
tone: "red",
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
observedLowBreak
|
|
||||||
? {
|
|
||||||
label: isEn ? "Peak revised down" : "峰值下修",
|
|
||||||
tone: "blue",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
observedLowLag
|
|
||||||
? {
|
|
||||||
label: isEn ? "Warm-up needs proof" : "等待升温确认",
|
|
||||||
tone: "amber",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
observationStale
|
|
||||||
? {
|
|
||||||
label: isEn ? "Stale observation" : "观测过旧",
|
|
||||||
tone: "amber",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
peakHasPassed
|
peakHasPassed
|
||||||
? {
|
? {
|
||||||
label: isEn ? "Peak window passed" : "峰值窗口已过",
|
label: isEn ? "Peak window passed" : "峰值窗口已过",
|
||||||
tone: "muted",
|
tone: "muted",
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
{
|
observationStale
|
||||||
label: aiStatusLabel,
|
? {
|
||||||
tone: aiStatusTone,
|
label: isEn
|
||||||
},
|
? isHkoObservation
|
||||||
{
|
? "HKO stale"
|
||||||
label: marketFreshnessLabel,
|
: "METAR stale"
|
||||||
tone: marketStatusTone,
|
: isHkoObservation
|
||||||
},
|
? "观测过旧"
|
||||||
]).slice(0, 6);
|
: "METAR 过旧",
|
||||||
|
tone: "amber",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
observedLowBreak
|
||||||
|
? {
|
||||||
|
label: isEn ? "Peak revised down" : "峰值下修",
|
||||||
|
tone: "blue",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
aiForecast.status === "loading"
|
||||||
|
? {
|
||||||
|
label: isEn ? "AI reading" : "AI 解读中",
|
||||||
|
tone: aiStatusTone,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
marketDecisionView.status === "unavailable"
|
||||||
|
? {
|
||||||
|
label: isEn ? "Market missing" : "市场价格缺失",
|
||||||
|
tone: marketStatusTone,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
modelHighlyConsistent
|
||||||
|
? {
|
||||||
|
label: isEn ? "Models agree" : "模型高度一致",
|
||||||
|
tone: "green",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
observedLowLag || needsNextBulletin
|
||||||
|
? {
|
||||||
|
label: isEn ? "Wait next report" : "需要等待下一报文",
|
||||||
|
tone: "amber",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
]).slice(0, 3);
|
||||||
const localizedRisksRaw =
|
const localizedRisksRaw =
|
||||||
(isEn ? aiCityForecast?.risks_en : aiCityForecast?.risks_zh) || [];
|
(isEn ? aiCityForecast?.risks_en : aiCityForecast?.risks_zh) || [];
|
||||||
const localizedRisks = Array.isArray(localizedRisksRaw)
|
const localizedRisks = Array.isArray(localizedRisksRaw)
|
||||||
@@ -475,6 +500,16 @@ function AiPinnedCityCard({
|
|||||||
{isEn ? "Deep analysis" : "城市深度分析"}
|
{isEn ? "Deep analysis" : "城市深度分析"}
|
||||||
</span>
|
</span>
|
||||||
<h3>{displayName}</h3>
|
<h3>{displayName}</h3>
|
||||||
|
<div className="scan-ai-city-status-tags">
|
||||||
|
{statusTags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag.label}
|
||||||
|
className={clsx("scan-ai-city-status-tag", tag.tone)}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<div className="scan-ai-city-pills">
|
<div className="scan-ai-city-pills">
|
||||||
<span>{detail?.local_time || row?.local_time || "--"}</span>
|
<span>{detail?.local_time || row?.local_time || "--"}</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -486,16 +521,6 @@ function AiPinnedCityCard({
|
|||||||
<span>{isEn ? "Model" : "模型"} {modelRange}</span>
|
<span>{isEn ? "Model" : "模型"} {modelRange}</span>
|
||||||
<span>{isEn ? "Peak" : "峰值"} {peakWindow}</span>
|
<span>{isEn ? "Peak" : "峰值"} {peakWindow}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-ai-city-status-tags">
|
|
||||||
{statusTags.map((tag) => (
|
|
||||||
<span
|
|
||||||
key={tag.label}
|
|
||||||
className={clsx("scan-ai-city-status-tag", tag.tone)}
|
|
||||||
>
|
|
||||||
{tag.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="scan-ai-city-freshness">
|
<div className="scan-ai-city-freshness">
|
||||||
<span>{observationFreshnessLabel}</span>
|
<span>{observationFreshnessLabel}</span>
|
||||||
<span>{marketFreshnessLabel}</span>
|
<span>{marketFreshnessLabel}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user