From 3f7d3ddf34e7955d6ad19de45c6010f7165918ce Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 00:13:11 +0800 Subject: [PATCH] Redesign the scan terminal layout for EMOS --- .../components/dashboard/Dashboard.module.css | 1188 +++++++++++++++++ .../components/dashboard/OpportunityTable.tsx | 382 +++--- .../components/dashboard/ScanFilterPanel.tsx | 169 ++- frontend/components/dashboard/ScanKPIBar.tsx | 57 +- .../dashboard/ScanTerminalDashboard.tsx | 531 +++++--- 5 files changed, 1825 insertions(+), 502 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 31c45938..d058a901 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -8964,3 +8964,1191 @@ padding: 40px; text-align: center; } + +/* ══════════════════════════════════════════════════════════════ + SCAN TERMINAL OVERRIDES — EMOS DESK + ══════════════════════════════════════════════════════════════ */ + +.root :global(.scan-terminal) { + display: grid; + grid-template-columns: 248px minmax(0, 1fr) 324px; + gap: 14px; + width: 100%; + min-height: 100vh; + height: auto; + margin-top: 0; + padding: 16px; + background: + radial-gradient(circle at top, rgba(17, 44, 78, 0.46), transparent 36%), + linear-gradient(180deg, #07111f 0%, #050c16 100%); + overflow: auto; +} + +.root :global(.scan-filter-panel), +.root :global(.scan-data-grid), +.root :global(.scan-detail-panel) { + min-height: calc(100vh - 32px); + border: 1px solid rgba(82, 114, 161, 0.18); + border-radius: 22px; + background: linear-gradient(180deg, rgba(15, 28, 47, 0.94), rgba(9, 18, 32, 0.94)); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28); + backdrop-filter: blur(14px); +} + +.root :global(.scan-filter-panel) { + width: auto; + min-width: 0; + border-right: 1px solid rgba(82, 114, 161, 0.18); + padding: 18px 14px; + gap: 18px; + overflow: hidden auto; +} + +.root :global(.scan-sidebar-brand) { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 10px 18px; + border-bottom: 1px solid rgba(118, 146, 188, 0.12); +} + +.root :global(.scan-sidebar-brand-mark) { + width: 42px; + height: 42px; + border-radius: 14px; + display: grid; + place-items: center; + color: #042113; + background: radial-gradient(circle at 30% 30%, #29f2ab, #0da46a 60%, #073c28 100%); + box-shadow: 0 0 24px rgba(23, 217, 139, 0.3); +} + +.root :global(.scan-sidebar-brand-name) { + font-size: 30px; + font-weight: 800; + letter-spacing: -0.04em; + color: #eef5ff; +} + +.root :global(.scan-filter-heading) { + display: flex; + align-items: center; + justify-content: space-between; + margin: 6px 8px 8px; + font-size: 14px; + font-weight: 600; + color: #b8c7dd; +} + +.root :global(.scan-mode-tabs) { + gap: 10px; +} + +.root :global(.scan-mode-tab) { + align-items: flex-start; + gap: 12px; + padding: 14px; + border-radius: 14px; + border: 1px solid rgba(87, 119, 166, 0.14); + background: rgba(11, 24, 42, 0.95); + color: #dbe7f8; +} + +.root :global(.scan-mode-tab:hover) { + background: rgba(13, 28, 48, 0.98); + border-color: rgba(87, 119, 166, 0.24); +} + +.root :global(.scan-mode-tab.active) { + background: linear-gradient(180deg, rgba(15, 34, 59, 1), rgba(9, 19, 34, 1)); + border-color: rgba(78, 222, 151, 0.7); + color: #f3fbff; + box-shadow: inset 0 0 0 1px rgba(78, 222, 151, 0.35), 0 0 0 2px rgba(7, 199, 119, 0.08); +} + +.root :global(.scan-mode-tab.active::before) { + display: none; +} + +.root :global(.scan-mode-icon) { + width: 28px; + height: 28px; + border-radius: 10px; + display: grid; + place-items: center; + flex-shrink: 0; + background: rgba(63, 140, 255, 0.12); + color: #6cb1ff; +} + +.root :global(.scan-mode-tab.active .scan-mode-icon) { + background: rgba(23, 217, 139, 0.14); + color: #2df3a5; +} + +.root :global(.scan-mode-copy) { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.root :global(.scan-mode-tab-label) { + font-size: 15px; + font-weight: 700; + color: inherit; +} + +.root :global(.scan-mode-tab-sub) { + font-size: 12px; + line-height: 1.4; + color: #8fa4c3; +} + +.root :global(.scan-range-card), +.root :global(.scan-filter-row) { + background: rgba(11, 24, 42, 0.92); + border: 1px solid rgba(87, 119, 166, 0.12); + border-radius: 12px; + padding: 12px 14px; +} + +.root :global(.scan-range-card) { + display: flex; + flex-direction: column; + gap: 10px; +} + +.root :global(.scan-filter-row-title), +.root :global(.scan-filter-row-label) { + font-size: 14px; + color: #d9e7f8; +} + +.root :global(.scan-filter-row) { + gap: 10px; +} + +.root :global(.scan-filter-row.inline) { + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.root :global(.scan-range-track-wrap) { + position: relative; + height: 20px; +} + +.root :global(.scan-range-track-wrap::before) { + content: ""; + position: absolute; + inset: 7px 0; + border-radius: 999px; + background: linear-gradient(90deg, #1c2f4e, #2d4f84); +} + +.root :global(.scan-range-slider) { + position: absolute; + inset: 0; + width: 100%; + height: 20px; + background: transparent; + pointer-events: none; +} + +.root :global(.scan-range-slider::-webkit-slider-thumb) { + width: 16px; + height: 16px; + border-radius: 50%; + border: none; + background: #4d93ff; + box-shadow: 0 0 0 4px rgba(77, 147, 255, 0.16); + pointer-events: auto; +} + +.root :global(.scan-range-slider::-moz-range-thumb) { + width: 16px; + height: 16px; + border-radius: 50%; + border: none; + background: #4d93ff; + box-shadow: 0 0 0 4px rgba(77, 147, 255, 0.16); + pointer-events: auto; +} + +.root :global(.scan-range-slider::-webkit-slider-runnable-track), +.root :global(.scan-range-slider::-moz-range-track) { + background: transparent; +} + +.root :global(.scan-range-labels) { + display: flex; + justify-content: space-between; + font-size: 12px; + color: #aec0d9; +} + +.root :global(.scan-select) { + border-radius: 10px; + padding: 10px 12px; + border-color: rgba(87, 119, 166, 0.12); + background: rgba(10, 22, 38, 0.95); + color: #d9e8fb; +} + +.root :global(.scan-toggle) { + width: 42px; + height: 24px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: linear-gradient(90deg, #0e2840, #12324d); +} + +.root :global(.scan-toggle.active) { + background: linear-gradient(90deg, #0e2840, #1f7f57); + border-color: rgba(39, 234, 152, 0.28); +} + +.root :global(.scan-toggle-knob) { + top: 2px; + left: 3px; + width: 18px; + height: 18px; + background: #8ca5c8; +} + +.root :global(.scan-toggle.active .scan-toggle-knob) { + left: 21px; + background: #1bf1a2; + box-shadow: 0 0 10px rgba(27, 241, 162, 0.45); +} + +.root :global(.scan-cta-button) { + border-radius: 14px; + padding: 15px; + margin-top: auto; + background: linear-gradient(180deg, #1ddb8d, #0eb96d); + box-shadow: 0 8px 22px rgba(14, 185, 109, 0.22); + font-size: 16px; + font-weight: 800; +} + +.root :global(.scan-data-grid) { + padding: 18px; + gap: 18px; + overflow: hidden; +} + +.root :global(.scan-topbar) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 6px 0 18px; + border-bottom: 1px solid rgba(118, 146, 188, 0.1); +} + +.root :global(.scan-topbar-tabs) { + display: flex; + gap: 28px; +} + +.root :global(.scan-topbar-tab) { + position: relative; + border: none; + background: transparent; + color: #b4c8e2; + font-size: 17px; + font-weight: 700; + cursor: pointer; +} + +.root :global(.scan-topbar-tab.active) { + color: #fff; +} + +.root :global(.scan-topbar-tab.active::after) { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -18px; + height: 3px; + border-radius: 999px; + background: #3f8cff; + box-shadow: 0 0 14px rgba(63, 140, 255, 0.4); +} + +.root :global(.scan-topbar-actions) { + display: flex; + align-items: center; + gap: 12px; + color: #b4c8e2; + font-size: 14px; +} + +.root :global(.scan-topbar-time) { + color: #b4c8e2; + font-variant-numeric: tabular-nums; +} + +.root :global(.scan-ghost-button), +.root :global(.scan-cta-ghost), +.root :global(.scan-sort-pill), +.root :global(.scan-icon-pill), +.root :global(.scan-detail-action-button), +.root :global(.scan-detail-icon-button), +.root :global(.scan-view-all-button) { + border: 1px solid rgba(99, 132, 180, 0.18); + background: rgba(8, 19, 34, 0.9); + color: #d7e5f7; + border-radius: 12px; +} + +.root :global(.scan-ghost-button), +.root :global(.scan-cta-ghost) { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 11px 14px; + font-weight: 700; +} + +.root :global(.scan-cta-ghost) { + color: #27ea98; + border-color: rgba(23, 217, 139, 0.3); + background: rgba(23, 217, 139, 0.12); +} + +.root :global(.scan-ghost-button .spin) { + animation: spin 1s linear infinite; +} + +.root :global(.scan-hero h1) { + margin: 0; + font-size: 44px; + line-height: 1.08; + letter-spacing: -0.04em; +} + +.root :global(.scan-hero p) { + margin: 10px 0 0; + color: #8fa4c3; + font-size: 16px; +} + +.root :global(.scan-kpi-bar) { + grid-template-columns: repeat(5, 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.purple .scan-kpi-label) { + color: #a35cff; +} + +.root :global(.scan-kpi-card.blue .scan-kpi-label) { + color: #49a3ff; +} + +.root :global(.scan-kpi-card.orange .scan-kpi-label) { + color: #ffb020; +} + +.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; + min-height: 0; +} + +.root :global(.scan-list-header) { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 10px; +} + +.root :global(.scan-list-tabs) { + display: flex; + gap: 24px; +} + +.root :global(.scan-list-tabs button) { + border: none; + background: transparent; + font-size: 16px; + font-weight: 700; + color: #91a7c4; + cursor: pointer; +} + +.root :global(.scan-list-tabs button.active) { + color: #fff; +} + +.root :global(.scan-list-controls) { + display: flex; + gap: 10px; +} + +.root :global(.scan-sort-pill) { + min-width: 188px; + padding: 12px 14px; +} + +.root :global(.scan-icon-pill) { + width: 48px; + display: grid; + place-items: center; +} + +.root :global(.scan-table-shell) { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + border: 1px solid rgba(90, 123, 166, 0.12); + border-radius: 18px; + background: rgba(8, 17, 30, 0.8); + overflow: hidden; +} + +.root :global(.scan-table-shell.empty) { + min-height: 340px; +} + +.root :global(.scan-table-header) { + display: grid; + grid-template-columns: 54px minmax(220px, 1.15fr) minmax(170px, 0.82fr) minmax(250px, 1.15fr) minmax(180px, 0.92fr) 120px 112px; + gap: 12px; + align-items: center; + padding: 14px 18px; + background: rgba(11, 23, 40, 0.98); + border-bottom: 1px solid rgba(90, 123, 166, 0.12); + color: #92a8c5; + font-size: 13px; + font-weight: 600; + text-transform: none; + letter-spacing: 0; +} + +.root :global(.scan-table-body) { + overflow-y: auto; +} + +.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; + gap: 12px; + align-items: center; + min-height: 132px; + padding: 14px 18px; + border: none; + border-bottom: 1px solid rgba(90, 123, 166, 0.08); + border-radius: 0; + background: linear-gradient(180deg, rgba(10, 20, 35, 0.92), rgba(8, 17, 29, 0.92)); + text-align: left; +} + +.root :global(.scan-table-row:hover) { + transform: none; + background: linear-gradient(180deg, rgba(11, 24, 40, 0.96), rgba(8, 18, 31, 0.96)); +} + +.root :global(.scan-table-row.selected) { + background: linear-gradient(180deg, rgba(7, 34, 35, 0.92), rgba(8, 19, 29, 0.92)); + box-shadow: inset 0 0 0 1px rgba(23, 217, 139, 0.48); +} + +.root :global(.scan-rank-cell), +.root :global(.scan-score-cell), +.root :global(.scan-edge-cell) { + display: flex; + align-items: center; + justify-content: center; +} + +.root :global(.scan-rank-circle) { + width: 34px; + height: 34px; + font-size: 16px; + font-weight: 800; + color: #06150d; + background: linear-gradient(180deg, #24e495, #11b86f); +} + +.root :global(.scan-rank-circle.yellow) { + color: #281a00; + background: linear-gradient(180deg, #ffd95f, #ffb61c); +} + +.root :global(.scan-rank-circle.gray) { + color: #08131f; + background: linear-gradient(180deg, #d6e3f7, #94a7c4); +} + +.root :global(.scan-rank-circle.red) { + color: #fff3f3; + background: linear-gradient(180deg, #ff7373, #df4747); +} + +.root :global(.scan-city-cell) { + display: flex; + align-items: center; + gap: 14px; +} + +.root :global(.scan-city-thumb) { + width: 78px; + height: 78px; + border-radius: 14px; + background: linear-gradient(135deg, #3269a9, #0e2946 55%, #09121e); + position: relative; + overflow: hidden; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.root :global(.scan-city-thumb-fill) { + position: absolute; + inset: 0; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 35%, rgba(0, 0, 0, 0.22)), + linear-gradient(135deg, #5f8fd6, #112842 60%, #09111d); +} + +.root :global(.scan-city-copy) { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.root :global(.scan-city-name) { + font-size: 18px; + font-weight: 800; +} + +.root :global(.scan-city-sub) { + font-size: 14px; + color: #a5bad5; +} + +.root :global(.scan-city-volume) { + font-size: 14px; + color: #dbe7f8; +} + +.root :global(.scan-time-cell) { + display: flex; + flex-direction: column; + gap: 10px; +} + +.root :global(.scan-time-main) { + font-size: 16px; + font-weight: 700; +} + +.root :global(.scan-time-remaining) { + font-size: 14px; + color: #9eb3ce; +} + +.root :global(.scan-phase-badge), +.root :global(.scan-status-badge) { + width: fit-content; + padding: 6px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + border: 1px solid rgba(255, 255, 255, 0.12); + text-transform: none; +} + +.root :global(.scan-phase-badge.red) { + color: #ff7f7f; + background: rgba(255, 93, 93, 0.1); +} + +.root :global(.scan-phase-badge.green) { + color: #34e49f; + background: rgba(23, 217, 139, 0.12); +} + +.root :global(.scan-phase-badge.blue) { + color: #7cb5ff; + background: rgba(63, 140, 255, 0.12); +} + +.root :global(.scan-phase-badge.amber) { + color: #ffc14b; + background: rgba(255, 176, 32, 0.12); +} + +.root :global(.scan-distribution-preview) { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} + +.root :global(.scan-distribution-card) { + text-align: center; + padding: 10px 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; +} + +.root :global(.scan-distribution-card strong) { + display: block; + margin-bottom: 8px; + color: #fff; + font-size: 15px; +} + +.root :global(.scan-distribution-card.featured strong) { + color: #22eb98; +} + +.root :global(.scan-trade-cell) { + display: flex; + flex-direction: column; + gap: 8px; +} + +.root :global(.scan-trade-main) { + font-size: 18px; + font-weight: 800; +} + +.root :global(.scan-trade-main.buy) { + color: #26e896; +} + +.root :global(.scan-trade-main.sell) { + color: #ff6969; +} + +.root :global(.scan-trade-sub), +.root :global(.scan-trade-note) { + font-size: 14px; + color: #a5bad5; +} + +.root :global(.scan-edge-cell) { + font-size: 18px; + font-weight: 800; +} + +.root :global(.scan-edge-cell.positive) { + color: #1de28f; +} + +.root :global(.scan-edge-cell.negative) { + color: #ff6868; +} + +.root :global(.scan-score-ring) { + width: 58px; + height: 58px; + border-radius: 50%; + background: conic-gradient(#20df91 0 85%, rgba(255, 255, 255, 0.06) 85% 100%); +} + +.root :global(.scan-score-ring.tone-yellow) { + background: conic-gradient(#ffbe26 0 72%, rgba(255, 255, 255, 0.06) 72% 100%); +} + +.root :global(.scan-score-ring.tone-red) { + background: conic-gradient(#ff5d5d 0 58%, rgba(255, 255, 255, 0.06) 58% 100%); +} + +.root :global(.scan-score-ring::after) { + content: ""; + position: absolute; + inset: 5px; + border-radius: 50%; + background: #0b1728; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05); +} + +.root :global(.scan-score-ring span) { + position: relative; + z-index: 1; + font-size: 18px; + font-weight: 800; + color: #ecf4ff; +} + +.root :global(.scan-row-fav) { + display: none; +} + +.root :global(.scan-view-all-wrap) { + display: flex; + justify-content: center; + padding: 16px; +} + +.root :global(.scan-view-all-button) { + padding: 12px 18px; + font-weight: 700; +} + +.root :global(.scan-detail-panel) { + width: auto; + min-width: 0; + padding: 18px 16px; + border-left: 1px solid rgba(82, 114, 161, 0.18); +} + +.root :global(.scan-detail-header) { + padding: 0 0 16px; + justify-content: space-between; + border-bottom: none; + margin-bottom: 10px; +} + +.root :global(.scan-detail-top) { + display: flex; + gap: 14px; + align-items: center; +} + +.root :global(.scan-detail-title-wrap) { + display: flex; + flex-direction: column; + gap: 6px; +} + +.root :global(.scan-detail-icon-button) { + width: 40px; + height: 40px; + display: grid; + place-items: center; +} + +.root :global(.scan-detail-hero-placeholder) { + width: 66px; + height: 66px; + border-radius: 14px; + background: linear-gradient(135deg, #314d7b, #0d2038 60%, #08131f); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.root :global(.scan-detail-city-name) { + font-size: 22px; + font-weight: 800; +} + +.root :global(.scan-detail-city-sub) { + font-size: 14px; + color: #a4bad6; +} + +.root :global(.scan-detail-volume-row) { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.root :global(.scan-detail-volume-big) { + font-size: 28px; + font-weight: 800; +} + +.root :global(.scan-detail-volume-caption) { + margin-top: 4px; + font-size: 14px; + color: #90a7c4; +} + +.root :global(.scan-detail-action-button) { + padding: 11px 14px; + font-weight: 700; +} + +.root :global(.scan-detail-section) { + padding: 16px 0; + border-bottom: 1px solid rgba(90, 123, 166, 0.12); +} + +.root :global(.scan-detail-section-title) { + font-size: 16px; + font-weight: 800; + color: #e8f2ff; + text-transform: none; + letter-spacing: 0; + margin-bottom: 12px; +} + +.root :global(.scan-kv-list) { + display: flex; + flex-direction: column; + gap: 8px; +} + +.root :global(.scan-kv) { + display: flex; + justify-content: space-between; + gap: 14px; + padding: 4px 0; + color: #dbe7f8; +} + +.root :global(.scan-kv span:first-child) { + color: #91a8c6; +} + +.root :global(.scan-kv strong) { + font-size: 15px; +} + +.root :global(.scan-kv strong.warn) { + color: #ffbf39; +} + +.root :global(.scan-kv strong.danger) { + color: #ff7f7f; +} + +.root :global(.scan-timeline-head) { + display: flex; + justify-content: space-between; + color: #a1b6d2; + font-size: 13px; +} + +.root :global(.scan-timeline-bar) { + position: relative; + height: 8px; + margin-top: 8px; + border-radius: 999px; + background: linear-gradient(90deg, #e83c3c 0 72%, rgba(255, 255, 255, 0.18) 72% 100%); +} + +.root :global(.scan-timeline-knob) { + position: absolute; + top: 50%; + width: 16px; + height: 16px; + border-radius: 50%; + background: #dfe8f6; + box-shadow: 0 0 0 4px rgba(223, 232, 246, 0.12); + transform: translate(-50%, -50%); +} + +.root :global(.scan-timeline-caption) { + margin-top: 8px; + color: #a1b6d2; + font-size: 13px; + text-align: right; +} + +.root :global(.scan-chart-legend) { + display: flex; + gap: 16px; + margin-bottom: 14px; + color: #a8bdd8; + font-size: 13px; +} + +.root :global(.scan-chart-legend .dot) { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 6px; + vertical-align: middle; +} + +.root :global(.scan-chart-legend .dot.green) { + background: #1ce393; +} + +.root :global(.scan-chart-legend .dot.blue) { + background: #4ca2ff; +} + +.root :global(.scan-chart-bars) { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 8px; + height: 170px; + padding: 8px 4px 0; +} + +.root :global(.scan-chart-group) { + flex: 1; + display: flex; + justify-content: center; + align-items: flex-end; + gap: 6px; + position: relative; +} + +.root :global(.scan-chart-col) { + width: 20px; + border-radius: 8px 8px 0 0; +} + +.root :global(.scan-chart-col.model) { + background: linear-gradient(180deg, #1ce393, #0b8d5a); +} + +.root :global(.scan-chart-col.market) { + background: linear-gradient(180deg, #4ca2ff, #2263cf); +} + +.root :global(.scan-chart-label) { + position: absolute; + bottom: -24px; + left: 50%; + transform: translateX(-50%); + color: #93a8c4; + font-size: 13px; +} + +.root :global(.scan-trade-cards) { + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.root :global(.scan-trade-card) { + padding: 16px; + border-radius: 16px; + background: rgba(11, 24, 42, 0.92); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.root :global(.scan-trade-card.buy) { + box-shadow: inset 0 0 0 1px rgba(23, 217, 139, 0.22); +} + +.root :global(.scan-trade-card.sell) { + box-shadow: inset 0 0 0 1px rgba(255, 93, 93, 0.22); +} + +.root :global(.scan-trade-card-title) { + font-size: 16px; + font-weight: 800; +} + +.root :global(.scan-trade-card.buy .scan-trade-card-title) { + color: #20e391; +} + +.root :global(.scan-trade-card.sell .scan-trade-card-title) { + color: #ff7070; +} + +.root :global(.scan-trade-card p) { + margin: 10px 0 0; + color: #d5e1f5; + font-size: 14px; +} + +.root :global(.scan-trade-card p.positive) { + color: #20e391; +} + +.root :global(.scan-trade-card p.negative) { + color: #ff7070; +} + +.root :global(.scan-detail-score-block) { + margin-top: 18px; + padding-top: 16px; +} + +.root :global(.scan-detail-score-head) { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 12px; +} + +.root :global(.scan-detail-score-label-text) { + font-size: 16px; + font-weight: 800; +} + +.root :global(.scan-detail-score-meta) { + margin-top: 6px; + color: #89a0bf; + font-size: 13px; +} + +.root :global(.scan-detail-score-value) { + font-size: 40px; + font-weight: 800; +} + +.root :global(.scan-detail-score-value.green) { + color: #23e694; +} + +.root :global(.scan-detail-score-value.yellow) { + color: #ffbe26; +} + +.root :global(.scan-detail-score-value.red) { + color: #ff6f86; +} + +.root :global(.scan-detail-score-value span) { + font-size: 22px; + color: #9fb5d2; +} + +.root :global(.scan-detail-score-line) { + height: 6px; + margin-top: 10px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); +} + +.root :global(.scan-detail-score-line span) { + display: block; + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #1be392, #12b46d); + box-shadow: 0 0 18px rgba(27, 227, 146, 0.22); +} + +.root :global(.scan-empty-state) { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 48px 32px; + text-align: center; +} + +.root :global(.scan-empty-title) { + font-size: 20px; + font-weight: 800; + color: #e8f2ff; +} + +.root :global(.scan-empty-copy) { + margin-top: 10px; + font-size: 14px; + color: #8fa4c3; + line-height: 1.6; +} + +@media (max-width: 1480px) { + .root :global(.scan-terminal) { + grid-template-columns: 240px minmax(0, 1fr); + } + + .root :global(.scan-detail-panel) { + grid-column: 1 / -1; + min-height: auto; + } +} + +@media (max-width: 1100px) { + .root :global(.scan-terminal) { + grid-template-columns: 1fr; + } + + .root :global(.scan-filter-panel), + .root :global(.scan-data-grid), + .root :global(.scan-detail-panel) { + min-height: auto; + } + + .root :global(.scan-kpi-bar) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .root :global(.scan-topbar) { + flex-direction: column; + align-items: flex-start; + } + + .root :global(.scan-table-header), + .root :global(.scan-table-row) { + grid-template-columns: 1fr; + } + + .root :global(.scan-table-header) { + display: none; + } + + .root :global(.scan-trade-cards) { + grid-template-columns: 1fr; + } +} + +@media (max-width: 820px) { + .root :global(.scan-terminal) { + padding: 10px; + } + + .root :global(.scan-kpi-bar) { + grid-template-columns: 1fr; + } + + .root :global(.scan-topbar-tabs) { + gap: 14px; + flex-wrap: wrap; + } + + .root :global(.scan-list-header) { + flex-direction: column; + align-items: flex-start; + } + + .root :global(.scan-distribution-preview) { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index 355fdbf3..1479ad9c 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -6,90 +6,125 @@ import { useI18n } from "@/hooks/useI18n"; import type { ScanOpportunityRow } from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; -function getStatusMeta( - row: ScanOpportunityRow, - locale: string, -): { label: string; tone: "green" | "amber" | "purple" | "neutral" } { - const phase = String(row.window_phase || "").toLowerCase(); - if (phase === "active_peak" || phase === "setup_today") { - return { label: locale === "en-US" ? "Tradable" : "可交易", tone: "green" }; - } - if (phase === "tomorrow" || phase === "week_ahead") { - return { label: locale === "en-US" ? "Early" : "早期机会", tone: "purple" }; - } - if (phase === "post_peak") { - return { label: locale === "en-US" ? "Post Peak" : "峰后确认", tone: "amber" }; - } - return { label: locale === "en-US" ? "Watching" : "观察中", tone: "neutral" }; -} - function formatPercent(value?: number | null, signed = false) { if (value == null || Number.isNaN(Number(value))) return "--"; const numeric = Number(value); - if (!signed) return `${numeric.toFixed(1)}%`; - return `${numeric >= 0 ? "+" : ""}${numeric.toFixed(1)}%`; + return `${signed && numeric >= 0 ? "+" : ""}${numeric.toFixed(1)}%`; } -function formatTimeBlock(row: ScanOpportunityRow, locale: string) { - const parts: string[] = []; - if (row.local_time) { - parts.push(row.local_time); +function formatVolume(value?: number | null) { + const numeric = Number(value || 0); + if (!Number.isFinite(numeric) || numeric <= 0) return "--"; + if (numeric >= 1_000_000) return `$${(numeric / 1_000_000).toFixed(1)}M`; + if (numeric >= 1_000) return `$${(numeric / 1_000).toFixed(0)}K`; + return `$${numeric.toFixed(0)}`; +} + +function formatWindowMinutes(value: number | null | undefined, locale: string) { + if (value == null || !Number.isFinite(Number(value))) return "--"; + const minutes = Math.max(0, Math.round(Number(value))); + const hours = Math.floor(minutes / 60); + const remains = minutes % 60; + if (locale === "en-US") { + if (hours <= 0) return `${remains}m left`; + return `${hours}h ${remains}m left`; } - if (row.selected_date) { - parts.push(row.selected_date); - } - return parts.join(" · ") || (locale === "en-US" ? "No time" : "暂无时间"); + if (hours <= 0) return `剩余 ${remains} 分钟`; + return `剩余 ${hours}h ${remains}m`; } function formatAction(row: ScanOpportunityRow, locale: string) { + if (row.action) return row.action; if (row.side === "yes") { - return `${locale === "en-US" ? "Buy" : "买入"} Yes ${row.target_label || ""}`.trim(); + return `${locale === "en-US" ? "Buy Yes" : "买入 Yes"} ${row.target_label || ""}`.trim(); } if (row.side === "no") { - return `${locale === "en-US" ? "Buy" : "买入"} No ${row.target_label || ""}`.trim(); + return `${locale === "en-US" ? "Buy No" : "买入 No"} ${row.target_label || ""}`.trim(); } - return row.action || "--"; + return "--"; } -function formatProbability(value?: number | null) { - if (value == null || Number.isNaN(Number(value))) return "--"; - return `${(Number(value) * 100).toFixed(0)}%`; +function getPhaseMeta( + row: ScanOpportunityRow, + locale: string, +): { label: string; tone: "green" | "amber" | "blue" | "red" } { + const mode = String(row.window_phase || "").toLowerCase(); + if (mode === "active_peak" || mode === "setup_today") { + return { + label: locale === "en-US" ? "Touch Play" : "触达博弈", + tone: "red", + }; + } + if (mode === "tomorrow" || mode === "week_ahead") { + return { + label: locale === "en-US" ? "Early" : "早期机会", + tone: "blue", + }; + } + if (row.trend_alignment) { + return { + label: locale === "en-US" ? "Trend" : "趋势确认", + tone: "amber", + }; + } + return { + label: locale === "en-US" ? "Tradable" : "可交易", + tone: "green", + }; +} + +function scoreTone(score?: number | null) { + const numeric = Number(score || 0); + if (numeric >= 85) return "green"; + if (numeric >= 70) return "yellow"; + return "gray"; +} + +function ProbabilityPreview({ + row, + locale, +}: { + row: ScanOpportunityRow; + locale: string; +}) { + const targetBase = + row.target_value ?? + row.target_threshold ?? + row.target_lower ?? + row.target_upper ?? + null; + const unit = row.target_unit || row.temp_symbol || ""; + const targetLabel = + targetBase != null + ? `${Math.round(Number(targetBase))}${unit}` + : row.target_label || "--"; + return ( +
+
+ {targetLabel} + {locale === "en-US" ? "Target" : "目标"} +
+
+ {formatPercent(row.model_event_probability != null ? row.model_event_probability * 100 : null)} + {locale === "en-US" ? "Model" : "模型"} +
+
+ {formatPercent(row.market_event_probability != null ? row.market_event_probability * 100 : null)} + {locale === "en-US" ? "Market" : "市场"} +
+
+ {formatPercent(row.distribution_bias_score)} + {row.distribution_bias_direction || (locale === "en-US" ? "Bias" : "偏移")} +
+
+ ); } function ScoreRing({ score }: { score?: number | null }) { const displayScore = Math.max(0, Math.min(100, Number(score || 0))); - const radius = 20; - const circumference = 2 * Math.PI * radius; - const progress = (displayScore / 100) * circumference; - const color = - displayScore >= 85 ? "#00E0A4" : displayScore >= 70 ? "#FFB020" : "#FF4D6A"; - return ( -
- - - - - - {displayScore.toFixed(0)} - +
+ {displayScore.toFixed(0)}
); } @@ -106,124 +141,111 @@ export function OpportunityTable({ const { locale } = useI18n(); const isEn = locale === "en-US"; - return ( -
-
- # - - {isEn ? "City / Market" : "城市 / 市场"} - - - {isEn ? "Time / Phase" : "时间 / 阶段"} - - - {isEn ? "EMOS vs Market" : "EMOS vs 市场"} - - - {isEn ? "Best Action" : "最佳动作"} - - - {isEn ? "Edge" : "边际优势"} - - - {isEn ? "Score" : "综合得分"} - - -
- - {rows.map((row, index) => { - const status = getStatusMeta(row, locale); - const localizedCityName = getLocalizedCityName( - row.city, - row.city_display_name || row.display_name || row.city, - locale, - ); - const selected = selectedRowId === row.id; - const finalScore = Number(row.final_score || 0); - - return ( - - ); - })} - - {!rows.length ? ( -
-
-
- {isEn ? "No primary signal" : "当前无主信号"} -
-

- {isEn - ? "No opportunity passed the price, spread, liquidity, and edge filters." - : "当前没有机会同时满足价格、点差、流动性和 edge 过滤。"} -

+ if (!rows.length) { + return ( +
+
+
+ {isEn ? "No main signal right now" : "当前无主信号"} +
+
+ {isEn + ? "No row passed the price, spread, liquidity, and edge thresholds." + : "当前没有机会同时满足价格、点差、流动性和 edge 过滤。"}
- ) : null} +
+ ); + } + + return ( +
+
+ + {isEn ? "City / Market" : "城市 / 市场"} + {isEn ? "Local Time / Phase" : "当前时间 / 阶段"} + {isEn ? "Model vs Market" : "模型分布 vs 市场分布"} + {isEn ? "Best Opportunity" : "最佳机会"} + {isEn ? "Edge" : "边际优势"} + {isEn ? "Score" : "综合得分"} +
+ +
+ {rows.map((row, index) => { + const phaseMeta = getPhaseMeta(row, locale); + const localizedCityName = getLocalizedCityName( + row.city, + row.city_display_name || row.display_name || row.city, + locale, + ); + const selected = selectedRowId === row.id; + const scoreClass = scoreTone(row.final_score); + return ( + + ); + })} +
); } diff --git a/frontend/components/dashboard/ScanFilterPanel.tsx b/frontend/components/dashboard/ScanFilterPanel.tsx index 79cbe298..1a5111ab 100644 --- a/frontend/components/dashboard/ScanFilterPanel.tsx +++ b/frontend/components/dashboard/ScanFilterPanel.tsx @@ -2,12 +2,12 @@ import React from "react"; import { - Crosshair, - Clock, - Zap, - TrendingUp, - SlidersHorizontal, + Bolt, + CircleDot, + Clock3, + Info, Search, + TrendingUp, } from "lucide-react"; import { useI18n } from "@/hooks/useI18n"; import type { ScanTerminalFilters } from "@/lib/dashboard-types"; @@ -17,38 +17,41 @@ export interface FilterState extends ScanTerminalFilters {} const SCAN_MODES = [ { key: "tradable" as const, - icon: Crosshair, + icon: Bolt, labelEn: "Tradable", labelZh: "可交易机会", - descEn: "Markets with immediate trading value", - descZh: "交易价值最高的市场", + descEn: "Find the best immediate trade", + descZh: "发现当前最值得交易的市场", }, { key: "early" as const, - icon: Clock, + icon: Clock3, labelEn: "Early", labelZh: "早期机会", - descEn: "Long-horizon positions", - descZh: "长周期布局", + descEn: "Long-horizon, lower-priced setups", + descZh: "长时间布局,低价市场", }, { key: "touch" as const, - icon: Zap, + icon: CircleDot, labelEn: "Touch Play", labelZh: "触达博弈", - descEn: "Approaching settle threshold", - descZh: "触达博弈最高的市场", + descEn: "Markets approaching the settle line", + descZh: "接近决策,博弈是否触达", }, { key: "trend" as const, icon: TrendingUp, labelEn: "Trend", labelZh: "趋势确认", - descEn: "Trend-confirmed opportunities", - descZh: "趋势确认、顺势加仓", + descEn: "Trend-confirmed follow-through", + descZh: "趋势明朗,顺势交易", }, ] as const; +const LIQUIDITY_OPTIONS = [500, 1000, 5000, 10000]; +const EDGE_OPTIONS = [1, 2, 3, 5, 8]; + export function ScanFilterPanel({ value, onChange, @@ -75,10 +78,19 @@ export function ScanFilterPanel({ return ( ); } function ScanTerminalScreen() { - const store = useDashboardStore(); const { locale } = useI18n(); const isEn = locale === "en-US"; const [draftFilters, setDraftFilters] = useState(DEFAULT_FILTERS); @@ -336,15 +407,11 @@ function ScanTerminalScreen() { const [selectedRowId, setSelectedRowId] = useState(null); const [detailByRowId, setDetailByRowId] = useState>({}); const [detailLoadingId, setDetailLoadingId] = useState(null); - const deferredRows = useDeferredValue(terminalData?.rows || []); + const selectedRow = useMemo(() => { if (!deferredRows.length) return null; - return ( - deferredRows.find((row) => row.id === selectedRowId) || - deferredRows[0] || - null - ); + return deferredRows.find((row) => row.id === selectedRowId) || deferredRows[0] || null; }, [deferredRows, selectedRowId]); const fetchTerminal = async (filters: FilterState, force = false) => { @@ -401,9 +468,7 @@ function ScanTerminalScreen() { const intervalId = window.setInterval(() => { void fetchTerminal(activeFilters, false); }, 30_000); - return () => { - window.clearInterval(intervalId); - }; + return () => window.clearInterval(intervalId); }, [activeFilters]); useEffect(() => { @@ -414,18 +479,7 @@ function ScanTerminalScreen() { const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null; return ( -
- fetchTerminal(activeFilters, true)} - refreshSpinning={loading || store.loadingState.refresh} - /> - +
-
-
-
- {isEn ? "Tradable Opportunities" : "可交易机会"} -
-
- {isEn - ? "REST-only EMOS scan with one main signal per city/date." - : "基于 EMOS 分布与 CLOB REST 盘口,只输出每个城市/日期的单一主信号。"} -
+
+
+ {NAV_ITEMS.map((item, index) => ( + + ))}
-
- - {isEn ? "Updated" : "数据时间"} ·{" "} - {terminalData?.generated_at - ? terminalData.generated_at.replace("T", " ").slice(0, 19) - : "--"} +
+ + {selectedRow?.local_time || terminalData?.generated_at?.replace("T", " ").slice(11, 19) || "--"} + +
+
+

{isEn ? "Tradable Opportunities" : "可交易机会"}

+

+ {isEn + ? "Use EMOS distribution, live order book, and timing windows to isolate the one actionable signal." + : "基于当前时间、实况数据和模型预测,筛选出最具交易价值的市场。"} +

+
+ -
- -
- - {error ? ( -
- {isEn ? "Scan failed." : "扫描失败。"} {error} +
+
+
+ + + +
+
+
+ {isEn ? "Sort: Score" : "排序:综合得分"} +
+ +
- ) : ( - setSelectedRowId(row.id)} - /> - )} + + {error ? ( +
+
+ {isEn ? "Scan failed" : "扫描失败"} +
+
{error}
+
+ ) : ( + <> + setSelectedRowId(row.id)} + /> + {deferredRows.length ? ( +
+ +
+ ) : null} + + )} +