From c05cdd17c6c38aa939f3c0ed6bd2899de9742c18 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 06:31:29 +0800 Subject: [PATCH] feat: implement dashboard store and scan terminal components for city market analysis --- .../components/dashboard/Dashboard.module.css | 96 ++++++++++++------- .../components/dashboard/OpportunityTable.tsx | 57 ++++++----- .../dashboard/ScanTerminalDashboard.tsx | 73 ++++---------- frontend/hooks/useDashboardStore.tsx | 5 +- 4 files changed, 113 insertions(+), 118 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index d736bef6..a52d9001 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -9879,20 +9879,24 @@ .root :global(.scan-distribution-card) { min-width: 0; - text-align: center; - padding: 9px 5px; + display: flex; + flex-direction: column; + gap: 5px; + padding: 8px 6px; border-radius: 10px; border: 1px solid rgba(87, 119, 166, 0.1); background: rgba(15, 28, 46, 0.72); color: #b4c7df; - font-size: 11px; + font-size: 10px; } .root :global(.scan-distribution-card strong) { display: block; - margin-bottom: 8px; + margin-bottom: 1px; color: #fff; - font-size: 14px; + font-size: 13px; + line-height: 1.1; + text-align: center; overflow-wrap: anywhere; } @@ -9900,6 +9904,47 @@ color: #22eb98; } +.root :global(.scan-distribution-line) { + display: grid; + grid-template-columns: 12px minmax(22px, 1fr) auto; + gap: 4px; + align-items: center; + min-width: 0; + color: rgba(180, 199, 223, 0.9); +} + +.root :global(.scan-distribution-line b) { + color: rgba(148, 163, 184, 0.78); + font-size: 9px; + font-weight: 800; +} + +.root :global(.scan-distribution-line i) { + display: block; + max-width: 100%; + height: 4px; + border-radius: 999px; +} + +.root :global(.scan-distribution-line.model i) { + background: linear-gradient(90deg, #17d98b, #20e3d2); +} + +.root :global(.scan-distribution-line.market i) { + background: linear-gradient(90deg, #3f8cff, #57a8ff); +} + +.root :global(.scan-distribution-line em) { + overflow: hidden; + color: rgba(226, 237, 252, 0.92); + font-size: 10px; + font-style: normal; + font-weight: 760; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + .root :global(.scan-trade-cell) { display: flex; flex-direction: column; @@ -10083,35 +10128,6 @@ border-color: rgba(23, 217, 139, 0.4); } -.root :global(.scan-detail-inline-analysis) { - display: flex; - flex-direction: column; - gap: 12px; -} - -.root :global(.scan-detail-inline-analysis .models-section) { - margin: 0; - padding: 0; - border: 0; - background: transparent; -} - -.root :global(.scan-detail-inline-analysis .model-bars) { - padding: 14px; - border-radius: 16px; - border: 1px solid rgba(90, 123, 166, 0.14); - background: rgba(10, 20, 35, 0.72); -} - -.root :global(.scan-inline-analysis-state) { - padding: 14px; - border-radius: 14px; - border: 1px solid rgba(90, 123, 166, 0.12); - color: #9bb0cb; - font-size: 13px; - background: rgba(10, 20, 35, 0.52); -} - .root :global(.scan-detail-section) { padding: 16px 0; border-bottom: 1px solid rgba(90, 123, 166, 0.12); @@ -10447,6 +10463,14 @@ color: #647a98; } +.root :global(.scan-terminal.light .scan-distribution-line em) { + color: #243b5a; +} + +.root :global(.scan-terminal.light .scan-distribution-line b) { + color: #6f86a4; +} + .root :global(.scan-terminal.light .scan-mode-tab), .root :global(.scan-terminal.light .scan-table-shell), .root :global(.scan-terminal.light .scan-table-header), @@ -10455,9 +10479,7 @@ .root :global(.scan-terminal.light .scan-summary-card), .root :global(.scan-terminal.light .scan-settings-card), .root :global(.scan-terminal.light .scan-distribution-card), -.root :global(.scan-terminal.light .scan-trade-card), -.root :global(.scan-terminal.light .scan-detail-inline-analysis .model-bars), -.root :global(.scan-terminal.light .scan-inline-analysis-state) { +.root :global(.scan-terminal.light .scan-trade-card) { border-color: rgba(35, 72, 118, 0.12); background: rgba(255, 255, 255, 0.72); } diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index 39255cc1..b3220684 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -153,33 +153,40 @@ function ProbabilityPreview({ }); } + const modelLabel = locale === "en-US" ? "M" : "模"; + const marketLabel = locale === "en-US" ? "P" : "市"; + return (
- {preview.slice(0, 6).map((item) => ( -
- - {normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"} - - - {locale === "en-US" ? "Model" : "模型"} -
- {formatPercent( - item.model_probability != null ? item.model_probability * 100 : null, - )} -
-
- - {locale === "en-US" ? "Market" : "市场"} -
- {formatPercent( - item.market_probability != null ? item.market_probability * 100 : null, - )} -
-
- ))} + {preview.slice(0, 6).map((item) => { + const modelPercent = + item.model_probability != null ? item.model_probability * 100 : null; + const marketPercent = + item.market_probability != null ? item.market_probability * 100 : null; + const modelWidth = Math.max(3, Math.min(100, Number(modelPercent || 0))); + const marketWidth = Math.max(3, Math.min(100, Number(marketPercent || 0))); + + return ( +
+ + {normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"} + + + {modelLabel} + + {formatPercent(modelPercent)} + + + {marketLabel} + + {formatPercent(marketPercent)} + +
+ ); + })}
); } diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 9172d84b..3b9d3b15 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -25,9 +25,9 @@ import { FilterState, ScanFilterPanel, } from "@/components/dashboard/ScanFilterPanel"; +import { FutureForecastModal } from "@/components/dashboard/FutureForecastModal"; import { MapCanvas } from "@/components/dashboard/MapCanvas"; import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable"; -import { ModelForecast } from "@/components/dashboard/PanelSections"; import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar"; import { OpportunityTable } from "@/components/dashboard/OpportunityTable"; import { @@ -151,12 +151,11 @@ function formatShortDate(value?: string | null, locale = "zh-CN") { : date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); } -function formatUserLocalTime(locale = "zh-CN") { - return new Intl.DateTimeFormat(locale === "en-US" ? "en-US" : "zh-CN", { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }).format(new Date()); +function formatUserLocalTime() { + const now = new Date(); + return `${String(now.getHours()).padStart(2, "0")}:${String( + now.getMinutes(), + ).padStart(2, "0")}`; } function getLocalDateIndex(value?: string | null) { @@ -345,30 +344,15 @@ function DetailPanel({ ); const scoreClass = scoreTone(displayRow.final_score); const phaseMeta = getWindowPhaseMeta(displayRow, locale); - const analysisDetail = + const cityDetail = store.selectedDetail?.name?.toLowerCase() === row.city.toLowerCase() ? store.selectedDetail : store.cityDetailsByName[row.city] || null; - const [showInlineAnalysis, setShowInlineAnalysis] = useState(false); - const analysisLoading = Boolean( - showInlineAnalysis && - row.city && - !analysisDetail && - store.loadingState.cityDetail, - ); - - useEffect(() => { - setShowInlineAnalysis(false); - }, [row.id]); const openTodayAnalysis = async () => { if (!row.city) return; - if (showInlineAnalysis && analysisDetail) { - setShowInlineAnalysis(false); - return; - } - setShowInlineAnalysis(true); await store.selectCity(row.city); + await store.openTodayModal(); }; return ( @@ -416,29 +400,6 @@ function DetailPanel({ - {showInlineAnalysis && ( -
-
- {isEn ? "Today's Intraday Analysis" : "今日日内分析"} -
- {analysisLoading ? ( -
- {isEn ? "Loading model spread card..." : "正在加载模型分歧卡片..."} -
- ) : analysisDetail ? ( - - ) : ( -
- {isEn ? "No intraday model card available yet." : "当前还没有可展示的日内模型卡片。"} -
- )} -
- )} -
{isEn ? "Current Context" : "当前概况"} @@ -503,8 +464,8 @@ function DetailPanel({
- 00:00 - 23:59 + {cityDetail?.forecast?.sunrise || "--"} + {cityDetail?.forecast?.sunset || "--"}
- {phaseMeta.label} + {isEn ? "Sunrise / Sunset" : "日出 / 日落"} · {phaseMeta.label}
@@ -901,12 +862,12 @@ function ScanTerminalScreen() { }, [activeFilters]); useEffect(() => { - setUserLocalTime(formatUserLocalTime(locale)); + setUserLocalTime(formatUserLocalTime()); const intervalId = window.setInterval(() => { - setUserLocalTime(formatUserLocalTime(locale)); - }, 30_000); + setUserLocalTime(formatUserLocalTime()); + }, 10_000); return () => window.clearInterval(intervalId); - }, [locale]); + }, []); useEffect(() => { const stored = window.localStorage.getItem("polyweather_scan_theme"); @@ -932,6 +893,9 @@ function ScanTerminalScreen() { useEffect(() => { if (!activeDetailRow) return; void fetchDetail(activeDetailRow); + if (!store.cityDetailsByName[activeDetailRow.city]) { + void store.ensureCityDetail(activeDetailRow.city, false, "panel").catch(() => {}); + } }, [activeDetailRow, detailByRowId]); const handleMapCitySelect = useCallback((cityName: string) => { @@ -1171,6 +1135,7 @@ export function ScanTerminalDashboard() { + ); diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx index 85374d58..d8670482 100644 --- a/frontend/hooks/useDashboardStore.tsx +++ b/frontend/hooks/useDashboardStore.tsx @@ -1237,12 +1237,13 @@ export function DashboardStoreProvider({ }, openHistory, openTodayModal: async (forceRefresh?: boolean) => { - if (!selectedCity) { + const activeCity = selectedCityRef.current || selectedCity; + if (!activeCity) { return; } mapStopMotionRef.current(); - const cityName = selectedCity; + const cityName = activeCity; const modalSeq = (modalOpenSeqRef.current += 1); const isLatestModalRequest = () => modalOpenSeqRef.current === modalSeq &&