From 4950e31c69b753739499dec79dda66a7fcbc8d33 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 03:30:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A9=E5=B1=95=E6=89=AB=E6=8F=8F=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E8=A7=86=E5=9B=BE=E5=B9=B6=E4=BC=98=E5=8C=96=E6=B8=A9?= =?UTF-8?q?=E5=BA=A6=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard/Dashboard.module.css | 163 +++++++++ .../components/dashboard/OpportunityTable.tsx | 30 +- .../dashboard/ScanTerminalDashboard.tsx | 325 ++++++++++++++++-- 3 files changed, 486 insertions(+), 32 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 35dcfeb3..9e5459d4 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -9238,6 +9238,8 @@ padding: 18px; gap: 18px; overflow: hidden; + min-height: calc(100vh - 32px); + max-height: calc(100vh - 32px); } .root :global(.scan-topbar) { @@ -9445,6 +9447,7 @@ .root :global(.scan-list-section) { display: flex; flex-direction: column; + flex: 1; min-height: 0; } @@ -9521,9 +9524,169 @@ } .root :global(.scan-table-body) { + flex: 1; + min-height: 0; overflow-y: auto; } +.root :global(.scan-map-view), +.root :global(.scan-calendar-view), +.root :global(.scan-settings-view) { + flex: 1; + min-height: 0; +} + +.root :global(.scan-map-view) { + display: flex; + flex-direction: column; + gap: 12px; +} + +.root :global(.scan-map-shell) { + position: relative; + flex: 1; + min-height: 520px; + border: 1px solid rgba(90, 123, 166, 0.12); + border-radius: 18px; + overflow: hidden; + background: rgba(8, 17, 30, 0.8); +} + +.root :global(.scan-map-caption) { + color: #8fa4c3; + font-size: 13px; +} + +.root :global(.scan-calendar-view) { + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 14px; + padding-right: 4px; +} + +.root :global(.scan-calendar-group) { + border: 1px solid rgba(90, 123, 166, 0.12); + border-radius: 18px; + background: rgba(8, 17, 30, 0.8); + overflow: hidden; +} + +.root :global(.scan-calendar-group-head) { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 18px; + border-bottom: 1px solid rgba(90, 123, 166, 0.1); +} + +.root :global(.scan-calendar-date) { + font-size: 16px; + font-weight: 800; + color: #eef7ff; +} + +.root :global(.scan-calendar-count) { + font-size: 13px; + color: #8fa4c3; +} + +.root :global(.scan-calendar-grid) { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + padding: 14px; +} + +.root :global(.scan-calendar-card), +.root :global(.scan-summary-card), +.root :global(.scan-settings-card) { + border: 1px solid rgba(90, 123, 166, 0.12); + border-radius: 16px; + background: linear-gradient(180deg, rgba(10, 20, 35, 0.92), rgba(8, 17, 29, 0.92)); +} + +.root :global(.scan-calendar-card) { + text-align: left; + padding: 14px; +} + +.root :global(.scan-calendar-card.selected) { + box-shadow: inset 0 0 0 1px rgba(23, 217, 139, 0.42); +} + +.root :global(.scan-calendar-city) { + font-size: 16px; + font-weight: 800; + color: #eef7ff; +} + +.root :global(.scan-calendar-action) { + margin-top: 8px; + font-size: 15px; + font-weight: 800; +} + +.root :global(.scan-calendar-action.buy) { + color: #21e391; +} + +.root :global(.scan-calendar-action.sell) { + color: #ff6e6e; +} + +.root :global(.scan-calendar-meta) { + margin-top: 10px; + display: flex; + justify-content: space-between; + color: #8fa4c3; + font-size: 13px; +} + +.root :global(.scan-summary-grid) { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.root :global(.scan-summary-grid.monitor) { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); +} + +.root :global(.scan-summary-card), +.root :global(.scan-settings-card) { + padding: 18px; +} + +.root :global(.scan-summary-card.wide) { + grid-column: span 1; +} + +.root :global(.scan-summary-label) { + font-size: 13px; + color: #8fa4c3; +} + +.root :global(.scan-summary-value) { + margin-top: 10px; + font-size: 28px; + font-weight: 800; + color: #eef7ff; +} + +.root :global(.scan-settings-view) { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.root :global(.scan-settings-copy) { + margin-top: 10px; + color: #c8d8ee; + font-size: 14px; + line-height: 1.6; +} + .root :global(.scan-table-row) { display: grid; grid-template-columns: 54px minmax(220px, 1.15fr) minmax(170px, 0.82fr) minmax(250px, 1.15fr) minmax(180px, 0.92fr) 120px 112px; diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index 79e377e1..c1c4d0e4 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -46,13 +46,22 @@ function formatWindowMinutes(value: number | null | undefined, locale: string) { return `剩余 ${hours}h ${remains}m`; } -function formatAction(row: ScanOpportunityRow, locale: string) { - if (row.action) return row.action; +function formatAction( + row: ScanOpportunityRow, + locale: string, + tempSymbol?: string | null, +) { + const formattedTarget = normalizeTemperatureLabel(row.target_label, tempSymbol); + if (row.action) { + return row.target_label + ? row.action.replace(String(row.target_label), formattedTarget || String(row.target_label)) + : row.action; + } if (row.side === "yes") { - return `${locale === "en-US" ? "Buy Yes" : "买入 Yes"} ${row.target_label || ""}`.trim(); + return `${locale === "en-US" ? "Buy Yes" : "买入 Yes"} ${formattedTarget || ""}`.trim(); } if (row.side === "no") { - return `${locale === "en-US" ? "Buy No" : "买入 No"} ${row.target_label || ""}`.trim(); + return `${locale === "en-US" ? "Buy No" : "买入 No"} ${formattedTarget || ""}`.trim(); } return "--"; } @@ -223,6 +232,7 @@ export function OpportunityTable({
{rows.map((row, index) => { const phaseMeta = getWindowPhaseMeta(row, locale); + const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol); const localizedCityName = getLocalizedCityName( row.city, row.city_display_name || row.display_name || row.city, @@ -265,16 +275,18 @@ export function OpportunityTable({ -
-
- {formatAction(row, locale)} -
+
+
+ {formatAction(row, locale, tempSymbol)} +
{formatPercent(row.ask != null ? row.ask * 100 : null)} →{" "} {formatPercent(row.model_probability != null ? row.model_probability * 100 : null)}
- {row.target_label || row.market_direction || "--"} + {normalizeTemperatureLabel(row.target_label, tempSymbol) || + row.market_direction || + "--"}
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index ff98e640..d982cd73 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -23,6 +23,7 @@ import { FilterState, ScanFilterPanel, } from "@/components/dashboard/ScanFilterPanel"; +import { MapCanvas } from "@/components/dashboard/MapCanvas"; import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable"; import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar"; import { OpportunityTable } from "@/components/dashboard/OpportunityTable"; @@ -69,6 +70,17 @@ const NAV_ITEMS = [ { zh: "设置", en: "Settings" }, ]; +type TopSection = "terminal" | "markets" | "analysis" | "portfolio" | "monitor" | "settings"; +type ContentView = "list" | "map" | "calendar"; +const TOP_SECTION_ORDER: TopSection[] = [ + "terminal", + "markets", + "analysis", + "portfolio", + "monitor", + "settings", +]; + function formatPercent(value?: number | null, signed = false) { if (value == null || Number.isNaN(Number(value))) return "--"; const numeric = Number(value); @@ -125,6 +137,16 @@ function confidenceLabel(score?: number | null, locale = "zh-CN") { return "观察"; } +function formatShortDate(value?: string | null, locale = "zh-CN") { + const text = String(value || "").trim(); + if (!text) return "--"; + const date = new Date(`${text}T00:00:00`); + if (Number.isNaN(date.getTime())) return text; + return locale === "en-US" + ? date.toLocaleDateString("en-US", { month: "short", day: "numeric" }) + : date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); +} + function getSideRow( marketScan: MarketScan | null | undefined, selectedRow: ScanOpportunityRow, @@ -421,7 +443,8 @@ function DetailPanel({
- {isEn ? "Buy Yes" : "买入 Yes"} {displayRow.target_label || ""} + {isEn ? "Buy Yes" : "买入 Yes"}{" "} + {normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || ""}

{formatPrice(getDetailSideAsk(yesRow, marketScan, "yes"))} →{" "} @@ -435,7 +458,8 @@ function DetailPanel({

- {isEn ? "Buy No" : "买入 No"} {displayRow.target_label || ""} + {isEn ? "Buy No" : "买入 No"}{" "} + {normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || ""}

{formatPrice(getDetailSideAsk(noRow, marketScan, "no"))} →{" "} @@ -475,6 +499,187 @@ function DetailPanel({ ); } +function CalendarView({ + rows, + locale, + selectedRowId, + onSelectRow, +}: { + rows: ScanOpportunityRow[]; + locale: string; + selectedRowId: string | null; + onSelectRow: (row: ScanOpportunityRow) => void; +}) { + const groups = useMemo(() => { + const byDate = new Map(); + rows.forEach((row) => { + const key = String(row.selected_date || row.local_date || "unknown"); + const list = byDate.get(key) || []; + list.push(row); + byDate.set(key, list); + }); + return Array.from(byDate.entries()).sort(([left], [right]) => left.localeCompare(right)); + }, [rows]); + + if (!groups.length) { + return ( +

+
+ {locale === "en-US" ? "No dated opportunities" : "当前没有日期机会"} +
+
+ ); + } + + return ( +
+ {groups.map(([date, items]) => ( +
+
+
{formatShortDate(date, locale)}
+
+ {locale === "en-US" ? `${items.length} rows` : `${items.length} 条`} +
+
+
+ {items.map((row) => ( + + ))} +
+
+ ))} +
+ ); +} + +function OverviewMapView({ locale }: { locale: string }) { + const store = useDashboardStore(); + + return ( +
+
+ +
+
+ {locale === "en-US" + ? `Monitoring ${store.cities.length} cities on the original map canvas.` + : `正在用原地图画布监控 ${store.cities.length} 个城市。`} +
+
+ ); +} + +function PortfolioView({ + rows, + locale, +}: { + rows: ScanOpportunityRow[]; + locale: string; +}) { + const yesCount = rows.filter((row) => row.side === "yes").length; + const noCount = rows.filter((row) => row.side === "no").length; + const avgScore = + rows.length > 0 + ? rows.reduce((sum, row) => sum + Number(row.final_score || 0), 0) / rows.length + : null; + + return ( +
+
+
{locale === "en-US" ? "YES Ideas" : "YES 机会"}
+
{yesCount}
+
+
+
{locale === "en-US" ? "NO Ideas" : "NO 机会"}
+
{noCount}
+
+
+
{locale === "en-US" ? "Avg Score" : "平均评分"}
+
+ {avgScore != null ? `${avgScore.toFixed(0)}/100` : "--"} +
+
+
+ ); +} + +function MonitorView({ + rows, + locale, +}: { + rows: ScanOpportunityRow[]; + locale: string; +}) { + const groups = useMemo(() => { + const result = new Map(); + rows.forEach((row) => { + const label = getWindowPhaseMeta(row, locale).label; + result.set(label, (result.get(label) || 0) + 1); + }); + return Array.from(result.entries()); + }, [rows, locale]); + + return ( +
+ {groups.map(([label, count]) => ( +
+
{label}
+
{count}
+
+ ))} +
+ ); +} + +function SettingsView({ locale }: { locale: string }) { + return ( +
+
+
{locale === "en-US" ? "Market Discovery" : "市场发现"}
+
+ {locale === "en-US" ? "Gamma REST refreshes every 60s." : "Gamma REST 每 60 秒刷新一次。"} +
+
+
+
{locale === "en-US" ? "Price Layer" : "价格层"}
+
+ {locale === "en-US" + ? "CLOB REST prices and books refresh every 30s." + : "CLOB REST 价格和盘口每 30 秒刷新一次。"} +
+
+
+
{locale === "en-US" ? "Server Profile" : "服务器配置"}
+
+ {locale === "en-US" + ? "2GB profile: one polling loop, cached REST, no websocket fan-out." + : "2G 配置:单轮询、REST 缓存优先、不做 websocket 扇出。"} +
+
+
+ ); +} + function ScanTerminalScreen() { const store = useDashboardStore(); const { locale, toggleLocale } = useI18n(); @@ -490,6 +695,8 @@ function ScanTerminalScreen() { const [selectedRowId, setSelectedRowId] = useState(null); const [detailByRowId, setDetailByRowId] = useState>({}); const [detailLoadingId, setDetailLoadingId] = useState(null); + const [activeSection, setActiveSection] = useState("terminal"); + const [activeView, setActiveView] = useState("list"); const deferredRows = useDeferredValue(terminalData?.rows || []); const selectedRow = useMemo(() => { @@ -560,6 +767,55 @@ function ScanTerminalScreen() { }, [selectedRow, detailByRowId]); const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null; + const resolvedView: ContentView = + activeSection === "markets" + ? "map" + : activeSection === "analysis" + ? "calendar" + : activeView; + + const renderMainView = () => { + if (activeSection === "portfolio") { + return ; + } + if (activeSection === "monitor") { + return ; + } + if (activeSection === "settings") { + return ; + } + if (resolvedView === "map") { + return ; + } + if (resolvedView === "calendar") { + return ( + setSelectedRowId(row.id)} + /> + ); + } + return ( + <> + setSelectedRowId(row.id)} + /> + {deferredRows.length ? ( +
+ +
+ ) : null} + + ); + }; return (
@@ -581,7 +837,10 @@ function ScanTerminalScreen() { @@ -647,15 +906,50 @@ function ScanTerminalScreen() {
- - - + +
- {isEn ? "Sort: Score" : "排序:综合得分"} + {isEn + ? resolvedView === "calendar" + ? "Sort: Date" + : resolvedView === "map" + ? "Sort: City Map" + : "Sort: Score" + : resolvedView === "calendar" + ? "排序:日期" + : resolvedView === "map" + ? "排序:地图" + : "排序:综合得分"}
) : ( - <> - setSelectedRowId(row.id)} - /> - {deferredRows.length ? ( -
- -
- ) : null} - + renderMainView() )}