From d71d5979e1f2ba8e204139bf0796b9249615868d Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 26 Apr 2026 02:47:24 +0800 Subject: [PATCH] feat: add scan terminal dashboard with AI city analysis integration --- .../app/api/scan/terminal/ai-city/route.ts | 2 +- .../components/dashboard/Dashboard.module.css | 130 ++++++++++++++++++ .../dashboard/ScanTerminalDashboard.tsx | 43 ++++-- web/scan_terminal_service.py | 14 +- 4 files changed, 173 insertions(+), 16 deletions(-) diff --git a/frontend/app/api/scan/terminal/ai-city/route.ts b/frontend/app/api/scan/terminal/ai-city/route.ts index e0f905fb..67e0130a 100644 --- a/frontend/app/api/scan/terminal/ai-city/route.ts +++ b/frontend/app/api/scan/terminal/ai-city/route.ts @@ -7,7 +7,7 @@ import { const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const SCAN_AI_PROXY_TIMEOUT_MS = Math.max( 35_000, - Number(process.env.POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS || "45000") || 45_000, + Number(process.env.POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS || "55000") || 55_000, ); export const dynamic = "force-dynamic"; diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 7168371b..561642b6 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -12624,6 +12624,127 @@ line-height: 1.6; } +.root :global(.scan-loading-state) { + flex: 1; + min-height: 520px; + display: grid; + place-items: center; + align-content: center; + justify-items: center; + gap: 14px; + padding: 48px 32px; + text-align: center; +} + +.root :global(.scan-loading-orb) { + position: relative; + width: 74px; + height: 74px; + border-radius: 999px; + background: + radial-gradient(circle at center, rgba(77, 163, 255, 0.2) 0 22%, transparent 23%), + conic-gradient(from 20deg, rgba(77, 163, 255, 0.08), rgba(77, 163, 255, 0.95), rgba(34, 197, 94, 0.78), rgba(77, 163, 255, 0.08)); + box-shadow: + 0 0 0 1px rgba(77, 163, 255, 0.14), + 0 18px 42px rgba(77, 163, 255, 0.18); + animation: scan-loading-spin 1.25s linear infinite; +} + +.root :global(.scan-loading-orb::before) { + content: ""; + position: absolute; + inset: 8px; + border-radius: inherit; + background: #0b1220; +} + +.root :global(.scan-loading-orb span) { + position: absolute; + inset: 26px; + z-index: 1; + border-radius: inherit; + background: #4da3ff; + box-shadow: + 0 0 18px rgba(77, 163, 255, 0.72), + 0 0 36px rgba(34, 197, 94, 0.28); + animation: scan-loading-pulse 1.25s ease-in-out infinite; +} + +.root :global(.scan-loading-orb i) { + position: absolute; + z-index: 1; + top: 4px; + left: 50%; + width: 9px; + height: 9px; + margin-left: -4.5px; + border-radius: 999px; + background: #e6edf3; + box-shadow: 0 0 18px rgba(230, 237, 243, 0.86); +} + +.root :global(.scan-loading-title) { + color: #e6edf3; + font-size: 20px; + font-weight: 900; +} + +.root :global(.scan-loading-copy) { + color: #9fb2c7; + font-size: 13px; + font-weight: 700; +} + +.root :global(.scan-loading-steps) { + display: inline-flex; + gap: 7px; + margin-top: 2px; +} + +.root :global(.scan-loading-steps span) { + width: 32px; + height: 3px; + border-radius: 999px; + background: rgba(77, 163, 255, 0.24); + animation: scan-loading-step 1.2s ease-in-out infinite; +} + +.root :global(.scan-loading-steps span:nth-child(2)) { + animation-delay: 0.16s; +} + +.root :global(.scan-loading-steps span:nth-child(3)) { + animation-delay: 0.32s; +} + +@keyframes scan-loading-spin { + to { + transform: rotate(360deg); + } +} + +@keyframes scan-loading-pulse { + 0%, + 100% { + transform: scale(0.82); + opacity: 0.74; + } + 50% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes scan-loading-step { + 0%, + 100% { + background: rgba(77, 163, 255, 0.2); + } + 50% { + background: rgba(77, 163, 255, 0.9); + } +} + .root :global(.scan-terminal.light) { background: radial-gradient(circle at top, rgba(59, 130, 246, 0.1), transparent 34%), @@ -12678,6 +12799,7 @@ .root :global(.scan-terminal.light .scan-detail-city-sub), .root :global(.scan-terminal.light .scan-detail-volume-caption), .root :global(.scan-terminal.light .scan-empty-copy), +.root :global(.scan-terminal.light .scan-loading-copy), .root :global(.scan-terminal.light .scan-kv span:first-child), .root :global(.scan-terminal.light .scan-chart-label), .root :global(.scan-terminal.light .scan-trade-sub), @@ -12685,6 +12807,14 @@ color: #475569; } +.root :global(.scan-terminal.light .scan-loading-title) { + color: #0f172a; +} + +.root :global(.scan-terminal.light .scan-loading-orb::before) { + background: #f7f9fc; +} + .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), diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 4569e3c6..4eb82040 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -730,7 +730,11 @@ function AiPinnedCityCard({