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 ( +
- {isEn - ? "No opportunity passed the price, spread, liquidity, and edge filters." - : "当前没有机会同时满足价格、点差、流动性和 edge 过滤。"} -
+ if (!rows.length) { + return ( +