From f331673c8d1b2e110ba36bddabd72df7f53dc054 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 15:00:02 +0800 Subject: [PATCH] Refine scan terminal opportunity layout --- .gitignore | 1 + .../components/dashboard/Dashboard.module.css | 207 ++++++++++++++---- .../components/dashboard/OpportunityTable.tsx | 51 +++-- .../dashboard/ScanTerminalDashboard.tsx | 34 +-- 4 files changed, 226 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 8b6369b3..c12f04da 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ frontend/node_modules/ frontend/.next/ frontend/.vercel/ frontend/*.tsbuildinfo +frontend/.codex-next-dev*.log .npm-cache/ .codex-tmp/ diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index b8f568c3..1285e067 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -8998,7 +8998,7 @@ .root :global(.scan-terminal) { display: grid; - grid-template-columns: 248px minmax(0, 1fr) 324px; + grid-template-columns: minmax(0, 1fr) 324px; gap: 14px; width: 100%; min-height: 100vh; @@ -9302,6 +9302,8 @@ .root :global(.scan-topbar-actions) { display: flex; align-items: center; + flex-wrap: wrap; + justify-content: flex-end; gap: 12px; color: #b4c8e2; font-size: 14px; @@ -9364,6 +9366,31 @@ font-weight: 700; } +.root :global(.scan-primary-button) { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 42px; + padding: 11px 16px; + border: 1px solid rgba(23, 217, 139, 0.38); + border-radius: 12px; + background: linear-gradient(180deg, #1ddb8d, #0eb96d); + color: #031411; + font-size: 14px; + font-weight: 900; + box-shadow: 0 10px 24px rgba(14, 185, 109, 0.18); + cursor: pointer; +} + +.root :global(.scan-primary-button:hover:not(:disabled)) { + filter: brightness(1.06); +} + +.root :global(.scan-primary-button:disabled) { + cursor: not-allowed; + opacity: 0.68; +} + .root :global(.scan-cta-ghost) { color: #27ea98; border-color: rgba(23, 217, 139, 0.3); @@ -9618,7 +9645,7 @@ grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; - padding: 16px 18px; + padding: 16px 18px 14px; border-bottom: 1px solid rgba(90, 123, 166, 0.1); background: rgba(13, 28, 48, 0.62); } @@ -9636,15 +9663,43 @@ font-weight: 900; } -.root :global(.scan-opportunity-city span) { - overflow: hidden; +.root :global(.scan-opportunity-models) { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-width: 0; +} + +.root :global(.scan-opportunity-models span) { + display: inline-flex; + gap: 5px; + align-items: baseline; + min-width: 0; + padding: 5px 8px; + border: 1px solid rgba(122, 154, 196, 0.14); + border-radius: 8px; + background: rgba(4, 12, 22, 0.28); color: #9fb4cf; - font-size: 14px; - font-weight: 700; - text-overflow: ellipsis; + font-size: 12px; + font-weight: 800; + line-height: 1.2; white-space: nowrap; } +.root :global(.scan-opportunity-models em) { + color: #6f88aa; + font-style: normal; + font-weight: 800; +} + +.root :global(.scan-opportunity-models b) { + overflow: hidden; + color: #dceaff; + font-size: 13px; + font-weight: 900; + text-overflow: ellipsis; +} + .root :global(.scan-opportunity-phase) { display: flex; gap: 10px; @@ -9665,42 +9720,74 @@ } .root :global(.scan-opportunity-items) { - display: flex; - flex-direction: column; + display: grid; + gap: 8px; + padding: 10px 12px 12px; } .root :global(.scan-opportunity-item) { display: grid; - grid-template-columns: 24px minmax(190px, 1.1fr) minmax(92px, 0.45fr) minmax(92px, 0.45fr) minmax(90px, 0.38fr); - gap: 12px; + grid-template-columns: 18px minmax(190px, 1fr) minmax(96px, 0.42fr) minmax(88px, 0.36fr) minmax(98px, 0.38fr); + gap: 14px; align-items: center; width: 100%; - min-height: 48px; - padding: 10px 18px; - border: 0; - border-bottom: 1px solid rgba(90, 123, 166, 0.08); - background: transparent; + min-height: 56px; + padding: 10px 14px; + border: 1px solid rgba(90, 123, 166, 0.1); + border-radius: 12px; + background: rgba(5, 14, 25, 0.42); color: #b8c9e1; text-align: left; + transition: + background 0.18s ease, + border-color 0.18s ease, + transform 0.18s ease; } .root :global(.scan-opportunity-item:last-child) { - border-bottom: 0; + border-bottom: 1px solid rgba(90, 123, 166, 0.1); } .root :global(.scan-opportunity-item:hover) { - background: rgba(30, 70, 110, 0.18); + background: rgba(22, 48, 78, 0.5); + border-color: rgba(109, 145, 190, 0.2); } .root :global(.scan-opportunity-item.selected) { - background: rgba(10, 74, 65, 0.38); - box-shadow: inset 3px 0 0 rgba(23, 217, 139, 0.78); + background: linear-gradient(180deg, rgba(6, 50, 47, 0.72), rgba(7, 35, 39, 0.72)); + border-color: rgba(23, 217, 139, 0.42); + box-shadow: inset 3px 0 0 rgba(23, 217, 139, 0.82); } .root :global(.scan-opportunity-branch) { - color: rgba(159, 180, 207, 0.72); - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 16px; + position: relative; + align-self: stretch; + width: 18px; +} + +.root :global(.scan-opportunity-branch::before) { + position: absolute; + top: -18px; + bottom: 50%; + left: 8px; + width: 1px; + background: rgba(122, 154, 196, 0.28); + content: ""; +} + +.root :global(.scan-opportunity-branch i) { + position: absolute; + top: 50%; + left: 8px; + width: 10px; + height: 1px; + background: rgba(122, 154, 196, 0.32); +} + +.root :global(.scan-opportunity-trade) { + display: flex; + min-width: 0; + align-items: center; } .root :global(.scan-opportunity-action) { @@ -9719,27 +9806,47 @@ color: #ff7474; } -.root :global(.scan-opportunity-item span:not(.scan-opportunity-branch)) { +.root :global(.scan-opportunity-stat) { + display: grid; + min-width: 0; + gap: 2px; + justify-items: start; + color: #d0ddf0; +} + +.root :global(.scan-opportunity-stat small) { overflow: hidden; + max-width: 100%; + color: #718bac; + font-size: 10px; + font-weight: 900; + line-height: 1.1; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} + +.root :global(.scan-opportunity-stat b) { + overflow: hidden; + max-width: 100%; + color: #dceaff; font-size: 14px; - font-weight: 700; + font-weight: 900; + line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; } -.root :global(.scan-opportunity-item em) { +.root :global(.scan-opportunity-stat.edge) { justify-self: end; - font-size: 15px; - font-style: normal; - font-weight: 900; - white-space: nowrap; + justify-items: end; } -.root :global(.scan-opportunity-item em.positive) { +.root :global(.scan-opportunity-stat b.positive) { color: #1de28f; } -.root :global(.scan-opportunity-item em.negative) { +.root :global(.scan-opportunity-stat b.negative) { color: #ff6868; } @@ -10588,7 +10695,6 @@ .root :global(.scan-terminal.light .scan-hero p), .root :global(.scan-terminal.light .scan-topbar-title span), .root :global(.scan-terminal.light .scan-city-sub), -.root :global(.scan-terminal.light .scan-opportunity-city span), .root :global(.scan-terminal.light .scan-opportunity-phase), .root :global(.scan-terminal.light .scan-opportunity-phase em), .root :global(.scan-terminal.light .panel-overline), @@ -10608,6 +10714,8 @@ .root :global(.scan-terminal.light .scan-list-tabs button), .root :global(.scan-terminal.light .scan-mode-tab-sub), +.root :global(.scan-terminal.light .scan-opportunity-models em), +.root :global(.scan-terminal.light .scan-opportunity-stat small), .root :global(.scan-terminal.light .panel-meta-chip), .root :global(.scan-terminal.light .panel-weather-chip), .root :global(.scan-terminal.light .scan-distribution-card), @@ -10692,8 +10800,10 @@ } .root :global(.scan-terminal.light .scan-opportunity-item) { + background: rgba(255, 255, 255, 0.62); color: #405977; border-bottom-color: rgba(35, 72, 118, 0.08); + border-color: rgba(35, 72, 118, 0.1); } .root :global(.scan-terminal.light .scan-opportunity-item:hover) { @@ -10701,7 +10811,23 @@ } .root :global(.scan-terminal.light .scan-opportunity-item.selected) { - background: rgba(222, 250, 238, 0.82); + background: linear-gradient(180deg, rgba(222, 250, 238, 0.9), rgba(241, 250, 247, 0.96)); + border-color: rgba(10, 160, 100, 0.34); +} + +.root :global(.scan-terminal.light .scan-opportunity-models span) { + border-color: rgba(35, 72, 118, 0.1); + background: rgba(255, 255, 255, 0.68); +} + +.root :global(.scan-terminal.light .scan-opportunity-models b), +.root :global(.scan-terminal.light .scan-opportunity-stat b) { + color: #14253a; +} + +.root :global(.scan-terminal.light .scan-opportunity-branch::before), +.root :global(.scan-terminal.light .scan-opportunity-branch i) { + background: rgba(56, 86, 126, 0.24); } .root :global(.scan-terminal.light .panel-header), @@ -10731,6 +10857,12 @@ color: #1f3654; } +.root :global(.scan-terminal.light .scan-primary-button) { + color: #032019; + border-color: rgba(10, 160, 100, 0.34); + background: linear-gradient(180deg, #25d990, #10b870); +} + .root :global(.scan-terminal.light .scan-cta-ghost), .root :global(.scan-terminal.light .scan-detail-analysis-button) { border-color: rgba(7, 160, 100, 0.24); @@ -10756,7 +10888,7 @@ @media (max-width: 1480px) { .root :global(.scan-terminal) { - grid-template-columns: 240px minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); } .root :global(.scan-terminal > .detail-panel.scan-city-detail-rail) { @@ -10813,8 +10945,9 @@ display: none; } - .root :global(.scan-opportunity-item em) { + .root :global(.scan-opportunity-stat.edge) { justify-self: start; + justify-items: start; } .root :global(.scan-trade-cards) { @@ -10845,7 +10978,7 @@ padding: 12px; } - .root :global(.scan-opportunity-city span) { + .root :global(.scan-opportunity-models span) { white-space: normal; } } diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index a507347f..4e57ee35 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -305,10 +305,22 @@ export const OpportunityTable = React.memo(function OpportunityTable({
{group.cityName} - - DEB {group.debLabel} · EMOS peak {group.peakLabel} - {group.peakProbability != null ? ` · ${formatPercent(group.peakProbability)}` : ""} - +
+ + DEB + {group.debLabel} + + + EMOS peak + {group.peakLabel} + + {group.peakProbability != null ? ( + + {isEn ? "Peak prob" : "峰值概率"} + {formatPercent(group.peakProbability)} + + ) : null} +
{group.localTime || "--"} @@ -339,19 +351,28 @@ export const OpportunityTable = React.memo(function OpportunityTable({ className={`scan-opportunity-item ${selected ? "selected" : ""}`} onClick={() => onSelectRow?.(row)} > - - {rowIndex === group.rows.length - 1 ? "└" : "├"} + - - {formatAction(row, locale, tempSymbol)} - - EMOS {formatPercent(modelProbability)} - - {priceLabel} {formatQuoteCents(row.ask)} + + + {formatAction(row, locale, tempSymbol)} + + + + EMOS + {formatPercent(modelProbability)} + + + {priceLabel} + {formatQuoteCents(row.ask)} + + + edge + + {formatPercent(row.edge_percent, true)} + - - edge {formatPercent(row.edge_percent, true)} - ); })} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index e2066579..1b35646c 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { RefreshCw, Moon, + Search, Sun, UserRound, } from "lucide-react"; @@ -19,10 +20,6 @@ import { import styles from "./Dashboard.module.css"; import detailChromeStyles from "./DetailPanelChrome.module.css"; import modalChromeStyles from "./ModalChrome.module.css"; -import { - FilterState, - ScanFilterPanel, -} from "@/components/dashboard/ScanFilterPanel"; import { DetailPanel as CityDetailPanel } from "@/components/dashboard/DetailPanel"; import { FutureForecastModal } from "@/components/dashboard/FutureForecastModal"; import { MapCanvas } from "@/components/dashboard/MapCanvas"; @@ -37,6 +34,7 @@ import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { dashboardClient } from "@/lib/dashboard-client"; import type { ScanOpportunityRow, + ScanTerminalFilters, ScanTerminalResponse, } from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; @@ -56,6 +54,8 @@ const DEFAULT_FILTERS: FilterState = { const SCAN_AUTO_REFRESH_MS = 5 * 60 * 1000; +interface FilterState extends ScanTerminalFilters {} + type ContentView = "list" | "map" | "calendar"; type ThemeMode = "dark" | "light"; function formatShortDate(value?: string | null, locale = "zh-CN") { @@ -249,7 +249,6 @@ function ScanTerminalScreen() { const accountHref = store.proAccess.authenticated ? "/account" : "/auth/login?next=%2Faccount"; - const [draftFilters, setDraftFilters] = useState(DEFAULT_FILTERS); const [activeFilters, setActiveFilters] = useState(DEFAULT_FILTERS); const [terminalData, setTerminalData] = useState(null); const [loading, setLoading] = useState(false); @@ -491,16 +490,6 @@ function ScanTerminalScreen() { return (
- { - setDraftFilters(filters); - void fetchTerminal(filters, true); - }} - isScanning={loading} - /> -
@@ -533,6 +522,21 @@ function ScanTerminalScreen() { {userLocalTime} +