From f47de115c8c4973b8027e33cfd1278e742f91dd2 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 10 May 2026 12:20:22 +0800 Subject: [PATCH] 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 --- frontend/components/dashboard/ScanKPIBar.tsx | 65 ------------- .../dashboard/ScanTerminal.module.css | 91 ------------------- .../dashboard/ScanTerminalBoard.module.css | 3 +- .../dashboard/ScanTerminalCard.module.css | 34 ------- .../dashboard/ScanTerminalDashboard.tsx | 22 ----- .../ScanTerminalLightTheme.module.css | 11 +-- .../dashboard/ScanTerminalList.module.css | 67 -------------- .../dashboard/ScanTerminalMobile.module.css | 14 --- .../scan-terminal/AiForecastKPIBar.tsx | 77 ---------------- .../scan-terminal/AiPinnedCityCard.tsx | 3 - .../scan-terminal/WeatherDecisionBand.tsx | 30 ------ .../scan-terminal/city-decision-state.ts | 28 +++--- 12 files changed, 16 insertions(+), 429 deletions(-) delete mode 100644 frontend/components/dashboard/ScanKPIBar.tsx delete mode 100644 frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx diff --git a/frontend/components/dashboard/ScanKPIBar.tsx b/frontend/components/dashboard/ScanKPIBar.tsx deleted file mode 100644 index d0c3bb79..00000000 --- a/frontend/components/dashboard/ScanKPIBar.tsx +++ /dev/null @@ -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 ( -
- {cards.map((card) => ( -
-
{card.label}
-
{card.value}
-
{card.note}
-
- ))} -
- ); -} diff --git a/frontend/components/dashboard/ScanTerminal.module.css b/frontend/components/dashboard/ScanTerminal.module.css index 891bef8c..9726dffa 100644 --- a/frontend/components/dashboard/ScanTerminal.module.css +++ b/frontend/components/dashboard/ScanTerminal.module.css @@ -247,97 +247,6 @@ 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 */ .root :global(.scan-view-tabs) { display: flex; diff --git a/frontend/components/dashboard/ScanTerminalBoard.module.css b/frontend/components/dashboard/ScanTerminalBoard.module.css index 6f987fbd..38d2162e 100644 --- a/frontend/components/dashboard/ScanTerminalBoard.module.css +++ b/frontend/components/dashboard/ScanTerminalBoard.module.css @@ -85,8 +85,7 @@ font-weight: 800; } -.root :global(.scan-terminal.focus-view-active .scan-upgrade-announcement), -.root :global(.scan-terminal.focus-view-active .scan-kpi-bar) { +.root :global(.scan-terminal.focus-view-active .scan-upgrade-announcement) { display: none; } diff --git a/frontend/components/dashboard/ScanTerminalCard.module.css b/frontend/components/dashboard/ScanTerminalCard.module.css index d6cb5614..acced0ef 100644 --- a/frontend/components/dashboard/ScanTerminalCard.module.css +++ b/frontend/components/dashboard/ScanTerminalCard.module.css @@ -750,40 +750,6 @@ 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) { display: none; } diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 162b2283..0ed03bf6 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -52,7 +52,6 @@ import type { } from "@/lib/dashboard-types"; import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView"; 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 { findDetailForCity } from "@/components/dashboard/scan-terminal/city-detail-utils"; import { @@ -190,19 +189,6 @@ function ScanTerminalScreen() { mapSelectedCityName || store.selectedCity, ); }, [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 rawCityName = mapSelectedCityName || store.selectedCity; const cityKey = normalizeCityKey(rawCityName); @@ -514,14 +500,6 @@ function ScanTerminalScreen() { - - {showAnnouncement ? (
diff --git a/frontend/components/dashboard/ScanTerminalLightTheme.module.css b/frontend/components/dashboard/ScanTerminalLightTheme.module.css index 1918eb85..bb95acf3 100644 --- a/frontend/components/dashboard/ScanTerminalLightTheme.module.css +++ b/frontend/components/dashboard/ScanTerminalLightTheme.module.css @@ -104,8 +104,7 @@ .root :global(.scan-terminal.light .panel-meta-chip), .root :global(.scan-terminal.light .panel-weather-chip), .root :global(.scan-terminal.light .scan-distribution-card), -.root :global(.scan-terminal.light .scan-chart-legend), -.root :global(.scan-terminal.light .scan-kpi-note) { +.root :global(.scan-terminal.light .scan-chart-legend) { color: #94A3B8; } @@ -120,7 +119,6 @@ .root :global(.scan-terminal.light .scan-mode-tab), .root :global(.scan-terminal.light .scan-table-shell), .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-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)); } -.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) { color: #14253a; } @@ -472,7 +465,6 @@ .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-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-stats b) { color: #0f172a; @@ -646,7 +638,6 @@ .root :global(.scan-terminal.light .panel-weather-chip), .root :global(.scan-terminal.light .scan-distribution-card), .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-action span), .root :global(.scan-terminal.light .scan-calendar-countdown small), diff --git a/frontend/components/dashboard/ScanTerminalList.module.css b/frontend/components/dashboard/ScanTerminalList.module.css index a0165dd9..77c58a74 100644 --- a/frontend/components/dashboard/ScanTerminalList.module.css +++ b/frontend/components/dashboard/ScanTerminalList.module.css @@ -1,73 +1,6 @@ /* 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. */ -.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) { display: flex; flex-direction: column; diff --git a/frontend/components/dashboard/ScanTerminalMobile.module.css b/frontend/components/dashboard/ScanTerminalMobile.module.css index ede3850f..fade389c 100644 --- a/frontend/components/dashboard/ScanTerminalMobile.module.css +++ b/frontend/components/dashboard/ScanTerminalMobile.module.css @@ -176,10 +176,6 @@ max-width: 100%; } - .root :global(.scan-kpi-bar) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .root :global(.scan-topbar) { flex-direction: column; align-items: flex-start; @@ -310,10 +306,6 @@ font-size: 13px; } - .root :global(.scan-kpi-bar) { - grid-template-columns: 1fr; - } - .root :global(.scan-topbar-tabs) { gap: 14px; flex-wrap: wrap; @@ -389,12 +381,6 @@ 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) { display: flex; align-items: center; diff --git a/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx b/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx deleted file mode 100644 index 39117d30..00000000 --- a/frontend/components/dashboard/scan-terminal/AiForecastKPIBar.tsx +++ /dev/null @@ -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 ( -
- {cards.map((card) => ( -
-
{card.label}
-
{card.value}
-
{card.note}
-
- ))} -
- ); -} - diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx index 71c56c98..ec08036d 100644 --- a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx +++ b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx @@ -613,15 +613,12 @@ export function AiPinnedCityCard({ {detail && !collapsed ? (
diff --git a/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx b/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx index 54b542ad..da2b622b 100644 --- a/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx +++ b/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx @@ -8,25 +8,19 @@ import type { import { MarketDecisionLine } from "@/components/dashboard/scan-terminal/MarketDecisionLine"; export function WeatherDecisionBand({ - currentTempText, decisionView, decisionWhyText, isEn, - longText, marketDecisionView, marketLineText, paceDeltaText, - peakWindow, }: { - currentTempText: string; decisionView: WeatherDecisionView; decisionWhyText: string; isEn: boolean; - longText: string; marketDecisionView: MarketDecisionView; marketLineText: string; paceDeltaText: string; - peakWindow: string; }) { return (
@@ -34,13 +28,6 @@ export function WeatherDecisionBand({ {decisionView.kicker} {decisionView.action}

{decisionWhyText}

-

{longText}

-
- {decisionView.reasons.map((reason, index) => ( - {reason} - ))} -
-

{decisionView.risk}

- - {isEn ? "Expected high" : "预计高点"} - {decisionView.expectedHigh} - {isEn ? "Weather range" : "天气区间"} {decisionView.targetRange} @@ -60,22 +43,9 @@ export function WeatherDecisionBand({ {isEn ? "Confidence" : "信心"} {decisionView.confidence} - - {isEn ? "Observed" : "实测"} - {currentTempText} - {isEn ? "Path delta" : "路径偏差"} {paceDeltaText} - - {isEn ? "Peak window" : "峰值窗口"} {peakWindow} - - - {isEn ? "Market implied" : "市场隐含"} {marketDecisionView.impliedText} - - - {isEn ? "Model prob" : "模型概率"} {marketDecisionView.modelText} - {isEn ? "Quote status" : "报价状态"}{" "} {marketDecisionView.status === "ready" ? (isEn ? "Ready" : "已同步") : marketDecisionView.status === "loading" ? (isEn ? "Loading" : "同步中") : (isEn ? "Unavailable" : "不可用")} diff --git a/frontend/components/dashboard/scan-terminal/city-decision-state.ts b/frontend/components/dashboard/scan-terminal/city-decision-state.ts index 38ccd1f0..f3a99b37 100644 --- a/frontend/components/dashboard/scan-terminal/city-decision-state.ts +++ b/frontend/components/dashboard/scan-terminal/city-decision-state.ts @@ -142,31 +142,31 @@ export function buildCityDecisionState({ : "watch"; const primaryReason = observedHighBreak ? isEn - ? "Worth watching now: observation has broken above the model range." - : "当前值得关注:实测已突破模型上沿。" + ? "Observation has broken above the model range." + : "实测已突破模型上沿。" : peakHasPassed ? 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 ? isEn - ? "Use as background only: observation is stale and needs the next report." - : "当前仅作背景:观测已过旧,需要下一报文确认。" + ? "Observation is stale and needs the next report." + : "观测已过旧,需要下一报文确认。" : marketStatus === "unavailable" ? isEn - ? "Weather evidence remains usable, but no tradable quote is available yet." - : "当前可参考天气:暂无可交易价格。" + ? "Weather evidence is usable, but no tradable quote is available yet." + : "天气证据可参考,但暂无可交易价格。" : modelHighlyConsistent ? isEn - ? "Worth watching now: models agree; wait for observation confirmation." - : "当前值得关注:模型高度一致,等待实测确认。" + ? "Models are aligned; wait for observation confirmation." + : "模型高度一致,等待实测确认。" : needsNextBulletin ? isEn - ? "Wait for confirmation: the next bulletin should decide direction." - : "当前建议等待:下一报文更适合决定方向。" + ? "The next bulletin is more likely to decide direction." + : "下一报文更可能决定方向。" : 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([ observedHighBreak