Reduce duplicate scan terminal decision copy and chrome
Removed the scan KPI bar, trimmed redundant decision-card content, and shortened the primary reason text so the city analysis cards read like concise signals instead of repeating the same state across sections. Constraint: Keep existing city decision logic and data flow unchanged while simplifying the UI Rejected: Rework the full card layout or mobile card flow | larger surface area than requested Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep hero, decision band, and market line responsibilities separate to avoid duplicate copy returning Tested: frontend npm run build Not-tested: Manual visual QA in browser across desktop/mobile breakpoints
This commit is contained in:
@@ -1,65 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { useI18n } from "@/hooks/useI18n";
|
|
||||||
import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types";
|
|
||||||
|
|
||||||
function formatTemperature(value?: number | null, unit?: string | null): string {
|
|
||||||
if (value == null || Number.isNaN(Number(value))) return "--";
|
|
||||||
return `${Number(value).toFixed(1)}${unit || "°C"}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScanKPIBar({
|
|
||||||
response,
|
|
||||||
rows,
|
|
||||||
totalCities,
|
|
||||||
loading: _loading,
|
|
||||||
}: {
|
|
||||||
response: ScanTerminalResponse | null;
|
|
||||||
rows: ScanOpportunityRow[];
|
|
||||||
totalCities: number;
|
|
||||||
loading: boolean;
|
|
||||||
}) {
|
|
||||||
const { locale } = useI18n();
|
|
||||||
const isEn = locale === "en-US";
|
|
||||||
const bestRow = rows[0] || null;
|
|
||||||
|
|
||||||
const cards = [
|
|
||||||
{
|
|
||||||
label: isEn ? "AI Workspace" : "AI 工作区",
|
|
||||||
value: `${rows.length}/${totalCities || 0}`,
|
|
||||||
note: isEn
|
|
||||||
? "No automatic scan. Cities are added from map clicks."
|
|
||||||
: "不主动扫描;地图点选后加入城市分析。",
|
|
||||||
tone: response?.status === "failed" ? "red" : response?.status === "stale" ? "amber" : "cyan",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isEn ? "City Pool" : "城市池",
|
|
||||||
value: `${totalCities || 0}`,
|
|
||||||
note: isEn ? "Available cities for forecast review" : "可点击进入 AI 预测的城市范围",
|
|
||||||
tone: "blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isEn ? "Forecast Center" : "预测中枢",
|
|
||||||
value: bestRow ? formatTemperature(bestRow.deb_prediction, bestRow.temp_symbol || bestRow.target_unit) : "--",
|
|
||||||
note: bestRow
|
|
||||||
? `${isEn ? "Focus" : "焦点"} ${bestRow.city_display_name || bestRow.display_name || bestRow.city} · DEB / ${isEn ? "models" : "模型"} / METAR`
|
|
||||||
: isEn
|
|
||||||
? "Select a city to load DEB and observations"
|
|
||||||
: "选择城市后展示 DEB 与实测路径",
|
|
||||||
tone: "green",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="scan-kpi-bar">
|
|
||||||
{cards.map((card) => (
|
|
||||||
<article key={card.label} className={`scan-kpi-card ${card.tone}`}>
|
|
||||||
<div className="scan-kpi-label">{card.label}</div>
|
|
||||||
<div className="scan-kpi-value">{card.value}</div>
|
|
||||||
<div className="scan-kpi-note">{card.note}</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -247,97 +247,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KPI Bar */
|
|
||||||
.root :global(.scan-kpi-bar) {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card) {
|
|
||||||
background: rgba(13, 17, 23, 0.6);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card:hover) {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border-color: rgba(0, 224, 164, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card::after) {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent,
|
|
||||||
rgba(0, 224, 164, 0.5),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card:hover::after) {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-green) {
|
|
||||||
border-top: 2px solid rgba(0, 224, 164, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-purple) {
|
|
||||||
border-top: 2px solid rgba(123, 97, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-header) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-icon) {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-label) {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-value) {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-green .scan-kpi-value) {
|
|
||||||
color: #4DA3FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-delta) {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View Tabs */
|
/* View Tabs */
|
||||||
.root :global(.scan-view-tabs) {
|
.root :global(.scan-view-tabs) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -85,8 +85,7 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-terminal.focus-view-active .scan-upgrade-announcement),
|
.root :global(.scan-terminal.focus-view-active .scan-upgrade-announcement) {
|
||||||
.root :global(.scan-terminal.focus-view-active .scan-kpi-bar) {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -750,40 +750,6 @@
|
|||||||
padding: 9px 11px;
|
padding: 9px 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-decision-reasons) {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-ai-decision-reasons small) {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
padding-left: 16px;
|
|
||||||
color: #c9d7e8;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 760;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-ai-decision-reasons small::before) {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.58em;
|
|
||||||
left: 0;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #4da3ff;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-ai-decision-risk) {
|
|
||||||
margin-top: 12px !important;
|
|
||||||
color: #fbbf24 !important;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-ai-market-mobile-line) {
|
.root :global(.scan-ai-market-mobile-line) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import type {
|
|||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView";
|
import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView";
|
||||||
import { CalendarView } from "@/components/dashboard/scan-terminal/CalendarView";
|
import { CalendarView } from "@/components/dashboard/scan-terminal/CalendarView";
|
||||||
import { AiForecastKPIBar } from "@/components/dashboard/scan-terminal/AiForecastKPIBar";
|
|
||||||
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
||||||
import { findDetailForCity } from "@/components/dashboard/scan-terminal/city-detail-utils";
|
import { findDetailForCity } from "@/components/dashboard/scan-terminal/city-detail-utils";
|
||||||
import {
|
import {
|
||||||
@@ -190,19 +189,6 @@ function ScanTerminalScreen() {
|
|||||||
mapSelectedCityName || store.selectedCity,
|
mapSelectedCityName || store.selectedCity,
|
||||||
);
|
);
|
||||||
}, [mapSelectedCityName, store.selectedCity, timeSortedRows]);
|
}, [mapSelectedCityName, store.selectedCity, timeSortedRows]);
|
||||||
const kpiCityName =
|
|
||||||
mapSelectedCityName ||
|
|
||||||
store.selectedCity ||
|
|
||||||
aiPinnedCities[0]?.cityName ||
|
|
||||||
null;
|
|
||||||
const kpiDetail =
|
|
||||||
findDetailForCity(store.cityDetailsByName, kpiCityName) ||
|
|
||||||
(store.selectedDetail &&
|
|
||||||
normalizeCityKey(store.selectedDetail.name) === normalizeCityKey(kpiCityName)
|
|
||||||
? store.selectedDetail
|
|
||||||
: null);
|
|
||||||
const kpiRow = findRowForCity(timeSortedRows, kpiCityName);
|
|
||||||
|
|
||||||
const mapFallbackRow = useMemo(() => {
|
const mapFallbackRow = useMemo(() => {
|
||||||
const rawCityName = mapSelectedCityName || store.selectedCity;
|
const rawCityName = mapSelectedCityName || store.selectedCity;
|
||||||
const cityKey = normalizeCityKey(rawCityName);
|
const cityKey = normalizeCityKey(rawCityName);
|
||||||
@@ -514,14 +500,6 @@ function ScanTerminalScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AiForecastKPIBar
|
|
||||||
pinnedCount={aiPinnedCities.length}
|
|
||||||
activeCityName={kpiCityName}
|
|
||||||
activeDetail={kpiDetail}
|
|
||||||
activeRow={kpiRow}
|
|
||||||
locale={locale}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showAnnouncement ? (
|
{showAnnouncement ? (
|
||||||
<section className="scan-upgrade-announcement" aria-label={isEn ? "Upgrade announcement" : "升级公告"}>
|
<section className="scan-upgrade-announcement" aria-label={isEn ? "Upgrade announcement" : "升级公告"}>
|
||||||
<div className="scan-upgrade-announcement-copy">
|
<div className="scan-upgrade-announcement-copy">
|
||||||
|
|||||||
@@ -104,8 +104,7 @@
|
|||||||
.root :global(.scan-terminal.light .panel-meta-chip),
|
.root :global(.scan-terminal.light .panel-meta-chip),
|
||||||
.root :global(.scan-terminal.light .panel-weather-chip),
|
.root :global(.scan-terminal.light .panel-weather-chip),
|
||||||
.root :global(.scan-terminal.light .scan-distribution-card),
|
.root :global(.scan-terminal.light .scan-distribution-card),
|
||||||
.root :global(.scan-terminal.light .scan-chart-legend),
|
.root :global(.scan-terminal.light .scan-chart-legend) {
|
||||||
.root :global(.scan-terminal.light .scan-kpi-note) {
|
|
||||||
color: #94A3B8;
|
color: #94A3B8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +119,6 @@
|
|||||||
.root :global(.scan-terminal.light .scan-mode-tab),
|
.root :global(.scan-terminal.light .scan-mode-tab),
|
||||||
.root :global(.scan-terminal.light .scan-table-shell),
|
.root :global(.scan-terminal.light .scan-table-shell),
|
||||||
.root :global(.scan-terminal.light .scan-table-header),
|
.root :global(.scan-terminal.light .scan-table-header),
|
||||||
.root :global(.scan-terminal.light .scan-kpi-card),
|
|
||||||
.root :global(.scan-terminal.light .scan-calendar-group),
|
.root :global(.scan-terminal.light .scan-calendar-group),
|
||||||
.root :global(.scan-terminal.light .scan-calendar-card),
|
.root :global(.scan-terminal.light .scan-calendar-card),
|
||||||
.root :global(.scan-terminal.light .scan-summary-card),
|
.root :global(.scan-terminal.light .scan-summary-card),
|
||||||
@@ -195,11 +193,6 @@
|
|||||||
background: linear-gradient(180deg, rgba(248, 252, 255, 0.96), rgba(238, 247, 255, 0.96));
|
background: linear-gradient(180deg, rgba(248, 252, 255, 0.96), rgba(238, 247, 255, 0.96));
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-terminal.light .scan-kpi-card) {
|
|
||||||
box-shadow: 0 10px 24px rgba(40, 70, 110, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-terminal.light .scan-kpi-value),
|
|
||||||
.root :global(.scan-terminal.light .scan-opportunity-phase > span) {
|
.root :global(.scan-terminal.light .scan-opportunity-phase > span) {
|
||||||
color: #14253a;
|
color: #14253a;
|
||||||
}
|
}
|
||||||
@@ -472,7 +465,6 @@
|
|||||||
.root :global(.scan-terminal.light .scan-ai-decision-metrics b),
|
.root :global(.scan-terminal.light .scan-ai-decision-metrics b),
|
||||||
.root :global(.scan-terminal.light .scan-ai-market-bucket strong),
|
.root :global(.scan-terminal.light .scan-ai-market-bucket strong),
|
||||||
.root :global(.scan-terminal.light .scan-ai-weather-summary),
|
.root :global(.scan-terminal.light .scan-ai-weather-summary),
|
||||||
.root :global(.scan-terminal.light .scan-ai-decision-reasons small),
|
|
||||||
.root :global(.scan-terminal.light .scan-ai-market-decision strong),
|
.root :global(.scan-terminal.light .scan-ai-market-decision strong),
|
||||||
.root :global(.scan-terminal.light .scan-ai-market-decision-stats b) {
|
.root :global(.scan-terminal.light .scan-ai-market-decision-stats b) {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -646,7 +638,6 @@
|
|||||||
.root :global(.scan-terminal.light .panel-weather-chip),
|
.root :global(.scan-terminal.light .panel-weather-chip),
|
||||||
.root :global(.scan-terminal.light .scan-distribution-card),
|
.root :global(.scan-terminal.light .scan-distribution-card),
|
||||||
.root :global(.scan-terminal.light .scan-chart-legend),
|
.root :global(.scan-terminal.light .scan-chart-legend),
|
||||||
.root :global(.scan-terminal.light .scan-kpi-note),
|
|
||||||
.root :global(.scan-terminal.light .scan-calendar-subtitle),
|
.root :global(.scan-terminal.light .scan-calendar-subtitle),
|
||||||
.root :global(.scan-terminal.light .scan-calendar-action span),
|
.root :global(.scan-terminal.light .scan-calendar-action span),
|
||||||
.root :global(.scan-terminal.light .scan-calendar-countdown small),
|
.root :global(.scan-terminal.light .scan-calendar-countdown small),
|
||||||
|
|||||||
@@ -1,73 +1,6 @@
|
|||||||
/* Scan terminal KPI, list tabs, table rows, v4 analysis, and AI workspace.
|
/* Scan terminal KPI, list tabs, table rows, v4 analysis, and AI workspace.
|
||||||
Kept separate from the shell/filter CSS so dense decision-board styles can evolve independently. */
|
Kept separate from the shell/filter CSS so dense decision-board styles can evolve independently. */
|
||||||
|
|
||||||
.root :global(.scan-kpi-bar) {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card) {
|
|
||||||
min-height: 124px;
|
|
||||||
padding: 18px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(92, 124, 170, 0.12);
|
|
||||||
background: linear-gradient(180deg, rgba(14, 28, 48, 0.92), rgba(11, 21, 37, 0.94));
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card:hover) {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card::after) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.green .scan-kpi-label) {
|
|
||||||
color: #17d98b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.cyan .scan-kpi-label) {
|
|
||||||
color: #37d6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.purple .scan-kpi-label) {
|
|
||||||
color: #a35cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.blue .scan-kpi-label) {
|
|
||||||
color: #49a3ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.red .scan-kpi-label) {
|
|
||||||
color: #ff647c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.amber .scan-kpi-label) {
|
|
||||||
color: #ffb84f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-card.orange .scan-kpi-label) {
|
|
||||||
color: #F59E0B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-label) {
|
|
||||||
font-size: 13px;
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-value) {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-kpi-note) {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #a8bedb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-list-section) {
|
.root :global(.scan-list-section) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -176,10 +176,6 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-kpi-bar) {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-topbar) {
|
.root :global(.scan-topbar) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -310,10 +306,6 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-kpi-bar) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-topbar-tabs) {
|
.root :global(.scan-topbar-tabs) {
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -389,12 +381,6 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-decision-long),
|
|
||||||
.root :global(.scan-ai-decision-reasons),
|
|
||||||
.root :global(.scan-ai-decision-risk) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root :global(.scan-ai-market-mobile-line) {
|
.root :global(.scan-ai-market-mobile-line) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
|
||||||
import { getTodayPaceView } from "@/lib/pace-utils";
|
|
||||||
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
|
||||||
import { getPeakWindowLabel } from "@/components/dashboard/scan-terminal/decision-utils";
|
|
||||||
|
|
||||||
export function AiForecastKPIBar({
|
|
||||||
pinnedCount,
|
|
||||||
activeCityName,
|
|
||||||
activeDetail,
|
|
||||||
activeRow,
|
|
||||||
locale,
|
|
||||||
}: {
|
|
||||||
pinnedCount: number;
|
|
||||||
activeCityName?: string | null;
|
|
||||||
activeDetail?: CityDetail | null;
|
|
||||||
activeRow?: ScanOpportunityRow | null;
|
|
||||||
locale: string;
|
|
||||||
}) {
|
|
||||||
const isEn = locale === "en-US";
|
|
||||||
const tempSymbol = activeDetail?.temp_symbol || activeRow?.temp_symbol || "°C";
|
|
||||||
const displayName =
|
|
||||||
activeDetail?.display_name ||
|
|
||||||
activeRow?.city_display_name ||
|
|
||||||
activeRow?.display_name ||
|
|
||||||
activeCityName ||
|
|
||||||
"--";
|
|
||||||
const deb = activeDetail?.deb?.prediction ?? activeRow?.deb_prediction ?? null;
|
|
||||||
const paceView = activeDetail
|
|
||||||
? getTodayPaceView(activeDetail, locale as "zh-CN" | "en-US")
|
|
||||||
: null;
|
|
||||||
const peakWindow =
|
|
||||||
paceView?.peakWindowText ||
|
|
||||||
(activeRow ? getPeakWindowLabel(activeRow) : null) ||
|
|
||||||
"--";
|
|
||||||
const cards = [
|
|
||||||
{
|
|
||||||
label: isEn ? "Decision Cards" : "决策卡",
|
|
||||||
value: String(pinnedCount),
|
|
||||||
note: isEn ? "Cities opened from opportunities or map" : "从机会榜或地图加入",
|
|
||||||
tone: "green",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isEn ? "Current City" : "当前城市",
|
|
||||||
value: displayName,
|
|
||||||
note: isEn ? "City briefing stays in the right rail" : "右侧城市简报同步显示,不自动切页",
|
|
||||||
tone: "blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isEn ? "Forecast Center" : "预测中枢",
|
|
||||||
value:
|
|
||||||
deb != null
|
|
||||||
? formatTemperatureValue(deb, tempSymbol, { digits: 1 })
|
|
||||||
: "--",
|
|
||||||
note: isEn ? "Weather center before price mapping" : "先定天气中枢,再映射价格",
|
|
||||||
tone: "cyan",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isEn ? "Peak Window" : "峰值窗口",
|
|
||||||
value: peakWindow,
|
|
||||||
note: isEn ? "Key window for daily high judgment" : "用于判断是否接近今日最高温",
|
|
||||||
tone: "amber",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="scan-kpi-bar">
|
|
||||||
{cards.map((card) => (
|
|
||||||
<article key={card.label} className={`scan-kpi-card ${card.tone}`}>
|
|
||||||
<div className="scan-kpi-label">{card.label}</div>
|
|
||||||
<div className="scan-kpi-value">{card.value}</div>
|
|
||||||
<div className="scan-kpi-note">{card.note}</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -613,15 +613,12 @@ export function AiPinnedCityCard({
|
|||||||
{detail && !collapsed ? (
|
{detail && !collapsed ? (
|
||||||
<div className="scan-ai-city-body" id={collapseId}>
|
<div className="scan-ai-city-body" id={collapseId}>
|
||||||
<WeatherDecisionBand
|
<WeatherDecisionBand
|
||||||
currentTempText={currentTempText}
|
|
||||||
decisionView={decisionView}
|
decisionView={decisionView}
|
||||||
decisionWhyText={decisionState.primaryReason}
|
decisionWhyText={decisionState.primaryReason}
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
longText={localizedFinalJudgment || paceText}
|
|
||||||
marketDecisionView={marketDecisionView}
|
marketDecisionView={marketDecisionView}
|
||||||
marketLineText={marketLineText}
|
marketLineText={marketLineText}
|
||||||
paceDeltaText={paceView?.deltaText || "--"}
|
paceDeltaText={paceView?.deltaText || "--"}
|
||||||
peakWindow={peakWindow}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="scan-ai-city-analysis-grid">
|
<div className="scan-ai-city-analysis-grid">
|
||||||
|
|||||||
@@ -8,25 +8,19 @@ import type {
|
|||||||
import { MarketDecisionLine } from "@/components/dashboard/scan-terminal/MarketDecisionLine";
|
import { MarketDecisionLine } from "@/components/dashboard/scan-terminal/MarketDecisionLine";
|
||||||
|
|
||||||
export function WeatherDecisionBand({
|
export function WeatherDecisionBand({
|
||||||
currentTempText,
|
|
||||||
decisionView,
|
decisionView,
|
||||||
decisionWhyText,
|
decisionWhyText,
|
||||||
isEn,
|
isEn,
|
||||||
longText,
|
|
||||||
marketDecisionView,
|
marketDecisionView,
|
||||||
marketLineText,
|
marketLineText,
|
||||||
paceDeltaText,
|
paceDeltaText,
|
||||||
peakWindow,
|
|
||||||
}: {
|
}: {
|
||||||
currentTempText: string;
|
|
||||||
decisionView: WeatherDecisionView;
|
decisionView: WeatherDecisionView;
|
||||||
decisionWhyText: string;
|
decisionWhyText: string;
|
||||||
isEn: boolean;
|
isEn: boolean;
|
||||||
longText: string;
|
|
||||||
marketDecisionView: MarketDecisionView;
|
marketDecisionView: MarketDecisionView;
|
||||||
marketLineText: string;
|
marketLineText: string;
|
||||||
paceDeltaText: string;
|
paceDeltaText: string;
|
||||||
peakWindow: string;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className={clsx("scan-ai-decision-band", decisionView.tone)}>
|
<section className={clsx("scan-ai-decision-band", decisionView.tone)}>
|
||||||
@@ -34,13 +28,6 @@ export function WeatherDecisionBand({
|
|||||||
<span>{decisionView.kicker}</span>
|
<span>{decisionView.kicker}</span>
|
||||||
<strong>{decisionView.action}</strong>
|
<strong>{decisionView.action}</strong>
|
||||||
<p className="scan-ai-decision-why">{decisionWhyText}</p>
|
<p className="scan-ai-decision-why">{decisionWhyText}</p>
|
||||||
<p className="scan-ai-decision-long">{longText}</p>
|
|
||||||
<div className="scan-ai-decision-reasons">
|
|
||||||
{decisionView.reasons.map((reason, index) => (
|
|
||||||
<small key={`${reason}-${index}`}>{reason}</small>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="scan-ai-decision-risk">{decisionView.risk}</p>
|
|
||||||
<MarketDecisionLine
|
<MarketDecisionLine
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
marketDecisionView={marketDecisionView}
|
marketDecisionView={marketDecisionView}
|
||||||
@@ -48,10 +35,6 @@ export function WeatherDecisionBand({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-ai-decision-metrics">
|
<div className="scan-ai-decision-metrics">
|
||||||
<span>
|
|
||||||
{isEn ? "Expected high" : "预计高点"}
|
|
||||||
<b>{decisionView.expectedHigh}</b>
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
{isEn ? "Weather range" : "天气区间"}
|
{isEn ? "Weather range" : "天气区间"}
|
||||||
<b>{decisionView.targetRange}</b>
|
<b>{decisionView.targetRange}</b>
|
||||||
@@ -60,22 +43,9 @@ export function WeatherDecisionBand({
|
|||||||
{isEn ? "Confidence" : "信心"}
|
{isEn ? "Confidence" : "信心"}
|
||||||
<b>{decisionView.confidence}</b>
|
<b>{decisionView.confidence}</b>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
|
||||||
{isEn ? "Observed" : "实测"}
|
|
||||||
<b>{currentTempText}</b>
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
{isEn ? "Path delta" : "路径偏差"} <b>{paceDeltaText}</b>
|
{isEn ? "Path delta" : "路径偏差"} <b>{paceDeltaText}</b>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
|
||||||
{isEn ? "Peak window" : "峰值窗口"} <b>{peakWindow}</b>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{isEn ? "Market implied" : "市场隐含"} <b>{marketDecisionView.impliedText}</b>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{isEn ? "Model prob" : "模型概率"} <b>{marketDecisionView.modelText}</b>
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
{isEn ? "Quote status" : "报价状态"}{" "}
|
{isEn ? "Quote status" : "报价状态"}{" "}
|
||||||
<b>{marketDecisionView.status === "ready" ? (isEn ? "Ready" : "已同步") : marketDecisionView.status === "loading" ? (isEn ? "Loading" : "同步中") : (isEn ? "Unavailable" : "不可用")}</b>
|
<b>{marketDecisionView.status === "ready" ? (isEn ? "Ready" : "已同步") : marketDecisionView.status === "loading" ? (isEn ? "Loading" : "同步中") : (isEn ? "Unavailable" : "不可用")}</b>
|
||||||
|
|||||||
@@ -142,31 +142,31 @@ export function buildCityDecisionState({
|
|||||||
: "watch";
|
: "watch";
|
||||||
const primaryReason = observedHighBreak
|
const primaryReason = observedHighBreak
|
||||||
? isEn
|
? isEn
|
||||||
? "Worth watching now: observation has broken above the model range."
|
? "Observation has broken above the model range."
|
||||||
: "当前值得关注:实测已突破模型上沿。"
|
: "实测已突破模型上沿。"
|
||||||
: peakHasPassed
|
: peakHasPassed
|
||||||
? isEn
|
? isEn
|
||||||
? "Avoid chasing now: peak window has passed; wait to confirm no new high."
|
? "Peak window has passed; confirm whether a new high can still form."
|
||||||
: "当前不宜追高:峰值窗口已过,等待确认是否还有新高。"
|
: "峰值窗口已过,确认是否还会出现新高。"
|
||||||
: observationStale
|
: observationStale
|
||||||
? isEn
|
? isEn
|
||||||
? "Use as background only: observation is stale and needs the next report."
|
? "Observation is stale and needs the next report."
|
||||||
: "当前仅作背景:观测已过旧,需要下一报文确认。"
|
: "观测已过旧,需要下一报文确认。"
|
||||||
: marketStatus === "unavailable"
|
: marketStatus === "unavailable"
|
||||||
? isEn
|
? isEn
|
||||||
? "Weather evidence remains usable, but no tradable quote is available yet."
|
? "Weather evidence is usable, but no tradable quote is available yet."
|
||||||
: "当前可参考天气:暂无可交易价格。"
|
: "天气证据可参考,但暂无可交易价格。"
|
||||||
: modelHighlyConsistent
|
: modelHighlyConsistent
|
||||||
? isEn
|
? isEn
|
||||||
? "Worth watching now: models agree; wait for observation confirmation."
|
? "Models are aligned; wait for observation confirmation."
|
||||||
: "当前值得关注:模型高度一致,等待实测确认。"
|
: "模型高度一致,等待实测确认。"
|
||||||
: needsNextBulletin
|
: needsNextBulletin
|
||||||
? isEn
|
? isEn
|
||||||
? "Wait for confirmation: the next bulletin should decide direction."
|
? "The next bulletin is more likely to decide direction."
|
||||||
: "当前建议等待:下一报文更适合决定方向。"
|
: "下一报文更可能决定方向。"
|
||||||
: isEn
|
: isEn
|
||||||
? "Watch the peak window and compare observations against the expected high."
|
? "Compare new observations with the expected high through the peak window."
|
||||||
: "当前重点:盯住峰值窗口,把实测与预计高点对照。";
|
: "在峰值窗口内继续对照实测与预计高点。";
|
||||||
|
|
||||||
const badges = uniqueStatusBadges([
|
const badges = uniqueStatusBadges([
|
||||||
observedHighBreak
|
observedHighBreak
|
||||||
|
|||||||
Reference in New Issue
Block a user