From d808d89fdd08a26ee958070a46fa3d30e16a8fc9 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 25 May 2026 05:27:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=97=A7=20dashboard=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=92=8C=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/FutureForecastModalChart.tsx | 416 ---------------- .../dashboard/FutureForecastModalStatus.tsx | 63 --- .../FutureForecastModalWeatherIcon.tsx | 26 - frontend/components/dashboard/HeaderBar.tsx | 194 -------- .../dashboard/IntradaySignalScene.tsx | 272 ----------- frontend/components/dashboard/MapCanvas.tsx | 39 -- .../dashboard/PolyWeatherDashboard.tsx | 7 - .../components/dashboard/ScanFilterPanel.tsx | 116 ----- .../dashboard/ScanTerminalDashboard.tsx | 8 +- .../components/dashboard/WeatherAuraLayer.tsx | 451 ------------------ .../dashboard/opportunity-ai-meta.ts | 213 --------- .../dashboard/opportunity-evidence-summary.ts | 127 ----- .../dashboard/opportunity-v4-decision.ts | 293 ------------ .../dashboard/opportunity-v4-risk.ts | 104 ---- 14 files changed, 2 insertions(+), 2327 deletions(-) delete mode 100644 frontend/components/dashboard/FutureForecastModalChart.tsx delete mode 100644 frontend/components/dashboard/FutureForecastModalStatus.tsx delete mode 100644 frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx delete mode 100644 frontend/components/dashboard/HeaderBar.tsx delete mode 100644 frontend/components/dashboard/IntradaySignalScene.tsx delete mode 100644 frontend/components/dashboard/MapCanvas.tsx delete mode 100644 frontend/components/dashboard/PolyWeatherDashboard.tsx delete mode 100644 frontend/components/dashboard/ScanFilterPanel.tsx delete mode 100644 frontend/components/dashboard/WeatherAuraLayer.tsx delete mode 100644 frontend/components/dashboard/opportunity-ai-meta.ts delete mode 100644 frontend/components/dashboard/opportunity-evidence-summary.ts delete mode 100644 frontend/components/dashboard/opportunity-v4-decision.ts delete mode 100644 frontend/components/dashboard/opportunity-v4-risk.ts diff --git a/frontend/components/dashboard/FutureForecastModalChart.tsx b/frontend/components/dashboard/FutureForecastModalChart.tsx deleted file mode 100644 index 59aae99d..00000000 --- a/frontend/components/dashboard/FutureForecastModalChart.tsx +++ /dev/null @@ -1,416 +0,0 @@ -"use client"; - -import type { ChartConfiguration } from "chart.js"; -import { useMemo } from "react"; -import { useChart } from "@/hooks/useChart"; -import { useDashboardSelection } from "@/hooks/useDashboardStore"; -import { useI18n } from "@/hooks/useI18n"; -import { getTemperatureChartData } from "@/lib/chart-utils"; -import { getFutureModalView } from "@/lib/dashboard-utils"; -import { formatMinuteAxisLabel } from "./FutureForecastModal.utils"; - -export function DailyTemperatureChart({ - dateStr, - forceToday = false, -}: { - dateStr: string; - forceToday?: boolean; -}) { - const { selectedDetail: detail } = useDashboardSelection(); - const { locale, t } = useI18n(); - const view = detail ? getFutureModalView(detail, dateStr, locale) : null; - const isToday = - forceToday || (detail ? dateStr === detail.local_date : false); - const todayChartData = useMemo( - () => (detail && isToday ? getTemperatureChartData(detail, locale) : null), - [detail, isToday, locale], - ); - - const canvasRef = useChart(() => { - if (!detail || !view) { - return { - data: { datasets: [], labels: [] }, - type: "line", - } satisfies ChartConfiguration<"line">; - } - - if (isToday && todayChartData) { - const datasets: NonNullable< - ChartConfiguration<"line">["data"] - >["datasets"] = []; - - datasets.push({ - borderColor: "rgba(100, 116, 139, 0.72)", - borderDash: [6, 4], - borderWidth: 1.6, - data: todayChartData.datasets.debSeries, - fill: false, - label: locale === "en-US" ? "DEB baseline" : "DEB 原始路径", - parsing: false, - pointRadius: 0, - tension: 0.1, - }); - - if (todayChartData.datasets.calibratedFutureSeries.length > 0) { - datasets.push({ - borderColor: "#38bdf8", - borderWidth: 2.4, - data: todayChartData.datasets.calibratedFutureSeries, - fill: false, - label: - locale === "en-US" - ? "METAR-calibrated path" - : "METAR 修正路径", - parsing: false, - pointHoverRadius: 5, - pointRadius: 0, - tension: 0.12, - }); - } - - if (todayChartData.datasets.hasMgmHourly) { - datasets.push({ - backgroundColor: "rgba(234, 179, 8, 0.05)", - borderColor: "rgba(234, 179, 8, 0.8)", - borderDash: [3, 3], - borderWidth: 1.2, - data: todayChartData.datasets.mgmHourlySeries, - fill: false, - label: locale === "en-US" ? "MGM Forecast" : "MGM 预测", - parsing: false, - pointHoverRadius: 6, - pointRadius: 0, - spanGaps: true, - tension: 0.1, - }); - } - - datasets.push({ - backgroundColor: "#22c55e", - borderColor: "#22c55e", - borderWidth: 0, - data: todayChartData.datasets.metarSeries, - fill: false, - label: - todayChartData.observationLabel || - (locale === "en-US" ? "Observation" : "观测实况"), - order: 0, - parsing: false, - pointHoverRadius: 7, - pointRadius: 5, - showLine: false, - }); - - if (todayChartData.datasets.airportMetarSeries?.length > 0) { - datasets.push({ - backgroundColor: "#86efac", - borderColor: "#86efac", - borderWidth: 1, - data: todayChartData.datasets.airportMetarSeries, - fill: false, - label: locale === "en-US" ? "Airport METAR" : "机场 METAR", - order: 0, - parsing: false, - pointHoverRadius: 6, - pointRadius: 4, - showLine: false, - }); - } - - if (todayChartData.datasets.mgmSeries?.length > 0) { - datasets.push({ - backgroundColor: "#facc15", - borderColor: "#facc15", - borderWidth: 0, - data: todayChartData.datasets.mgmSeries, - fill: false, - label: locale === "en-US" ? "MGM Observation" : "MGM 实测", - order: -1, - parsing: false, - pointHoverRadius: 9, - pointRadius: 7, - showLine: false, - }); - } - - if ( - !todayChartData.datasets.hasMgmHourly && - Math.abs(todayChartData.datasets.offset) > 0.3 - ) { - datasets.push({ - borderColor: "rgba(77, 163, 255, 0.22)", - borderDash: [2, 4], - borderWidth: 1, - data: todayChartData.datasets.tempsSeries, - fill: false, - label: locale === "en-US" ? "OM Raw" : "OM 原始", - parsing: false, - pointRadius: 0, - tension: 0.1, - }); - } - if ((todayChartData.tafMarkers || []).length > 0) { - datasets.push({ - backgroundColor: "#f59e0b", - borderColor: "#f59e0b", - borderWidth: 0, - data: todayChartData.datasets.tafCurrentMarkerSeries, - fill: false, - label: locale === "en-US" ? "Current TAF" : "当前 TAF", - order: -3, - parsing: false, - pointHoverRadius: 8, - pointRadius: 6, - showLine: false, - }); - datasets.push({ - backgroundColor: "rgba(250, 204, 21, 0.72)", - borderColor: "rgba(250, 204, 21, 0.72)", - borderWidth: 0, - data: todayChartData.datasets.tafPeakWindowMarkerSeries, - fill: false, - label: locale === "en-US" ? "Peak-window TAF" : "峰值窗口 TAF", - order: -2, - parsing: false, - pointHoverRadius: 7, - pointRadius: 4, - showLine: false, - }); - datasets.push({ - backgroundColor: "#f59e0b", - borderColor: "#f59e0b", - borderWidth: 0, - data: todayChartData.datasets.tafMarkerSeries, - fill: false, - label: locale === "en-US" ? "TAF Timing" : "TAF 时段", - order: -4, - parsing: false, - pointHoverRadius: 0, - pointRadius: 0, - showLine: false, - }); - } - - return { - data: { - datasets, - labels: [], - }, - options: { - interaction: { intersect: false, mode: "nearest" }, - maintainAspectRatio: false, - plugins: { - legend: { - labels: { - color: "#9FB2C7", - filter: (legendItem, chartData) => { - const text = String(legendItem.text || ""); - if (!text) return false; - if (text === "TAF Timing" || text === "TAF 时段") - return false; - if (!text.includes("DEB")) return true; - - const firstDebIndex = (chartData.datasets || []).findIndex( - (dataset) => String(dataset.label || "").includes("DEB"), - ); - return legendItem.datasetIndex === firstDebIndex; - }, - font: { family: "Inter", size: 11 }, - }, - }, - tooltip: { - backgroundColor: "rgba(15, 23, 42, 0.96)", - borderColor: "rgba(77, 163, 255, 0.24)", - borderWidth: 1, - callbacks: { - title: (items) => { - const rawX = items?.[0]?.parsed?.x; - return rawX != null - ? formatMinuteAxisLabel(Number(rawX)) - : ""; - }, - label: (ctx) => { - const label = String(ctx.dataset.label || ""); - const raw = ctx.raw as - | { - marker?: { - summary?: string; - markerType?: string; - displayType?: string; - isCurrent?: boolean; - isPeakWindow?: boolean; - }; - } - | undefined; - if ( - label === "TAF Timing" || - label === "TAF 时段" || - label === "Current TAF" || - label === "当前 TAF" || - label === "Peak-window TAF" || - label === "峰值窗口 TAF" - ) { - const marker = raw?.marker; - if (!marker) return label; - const markerType = String(marker.markerType || ""); - const displayType = String( - marker.displayType || marker.markerType || "", - ); - const summary = String(marker.summary || ""); - const prefix = - marker.isCurrent && marker.isPeakWindow - ? locale === "en-US" - ? "Current / peak-window TAF" - : "当前 / 峰值窗口 TAF" - : marker.isCurrent - ? locale === "en-US" - ? "Current TAF" - : "当前 TAF" - : marker.isPeakWindow - ? locale === "en-US" - ? "Peak-window TAF" - : "峰值窗口 TAF" - : label; - return `${prefix}: ${ - markerType - ? summary.replace(markerType, displayType) - : summary - }`; - } - const value = ctx.parsed.y; - if (value == null) return label; - return `${label}: ${value.toFixed(1)}${detail.temp_symbol || "°C"}`; - }, - }, - }, - }, - responsive: true, - scales: { - x: { - max: todayChartData.xMax, - min: todayChartData.xMin, - grid: { color: "rgba(255,255,255,0.04)" }, - type: "linear", - ticks: { - callback: (value) => { - const num = Number(value); - if (!Number.isFinite(num)) return ""; - const minutes = Math.round(num); - if ( - minutes !== todayChartData.xMin && - minutes !== todayChartData.xMax && - minutes % 120 !== 0 - ) { - return ""; - } - return formatMinuteAxisLabel(minutes); - }, - color: "#6B7A90", - font: { family: "Inter", size: 10 }, - maxRotation: 0, - }, - }, - y: { - grid: { color: "rgba(255,255,255,0.04)" }, - max: todayChartData.max, - min: todayChartData.min, - ticks: { - callback: (value) => - `${Number(value).toFixed(todayChartData.yTickStep < 1 ? 1 : 0)}${detail.temp_symbol || "°C"}`, - color: "#6B7A90", - font: { family: "Inter", size: 10 }, - stepSize: todayChartData.yTickStep, - }, - }, - }, - }, - type: "line", - } satisfies ChartConfiguration<"line">; - } - - const labels = view.slice.map((point) => point.label); - const unit = detail.temp_symbol || "°C"; - - return { - data: { - datasets: [ - { - backgroundColor: "rgba(77, 163, 255, 0.08)", - borderColor: "#4DA3FF", - data: view.slice.map((point) => point.temp), - fill: false, - label: - locale === "en-US" ? "Open-Meteo Temperature" : "Open-Meteo 温度", - pointRadius: 2, - tension: 0.1, - }, - { - backgroundColor: "transparent", - borderColor: "#93C5FD", - borderDash: [5, 4], - data: view.slice.map((point) => point.dewPoint), - fill: false, - label: locale === "en-US" ? "Dew Point" : "露点", - pointRadius: 0, - tension: 0.1, - }, - ], - labels, - }, - options: { - interaction: { intersect: false, mode: "index" }, - maintainAspectRatio: false, - plugins: { - legend: { - labels: { - color: "#9FB2C7", - font: { family: "Inter", size: 11 }, - }, - }, - tooltip: { - backgroundColor: "rgba(15, 23, 42, 0.96)", - borderColor: "rgba(77, 163, 255, 0.24)", - borderWidth: 1, - callbacks: { - label: (ctx) => - `${ctx.dataset.label}: ${ctx.parsed.y?.toFixed(1)}${unit}`, - }, - }, - }, - responsive: true, - scales: { - x: { - grid: { color: "rgba(255,255,255,0.04)" }, - ticks: { - color: "#6B7A90", - font: { family: "Inter", size: 10 }, - maxRotation: 0, - }, - }, - y: { - grid: { color: "rgba(255,255,255,0.04)" }, - ticks: { - callback: (value) => `${value}${unit}`, - color: "#6B7A90", - font: { family: "Inter", size: 10 }, - }, - }, - }, - }, - type: "line", - } satisfies ChartConfiguration<"line">; - }, [detail, isToday, locale, todayChartData, view]); - - return ( - <> -
- -
- {isToday && ( -
- {todayChartData?.legendText || t("future.chartLegendEmpty")} -
- )} - - ); -} diff --git a/frontend/components/dashboard/FutureForecastModalStatus.tsx b/frontend/components/dashboard/FutureForecastModalStatus.tsx deleted file mode 100644 index 85ff77a3..00000000 --- a/frontend/components/dashboard/FutureForecastModalStatus.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import clsx from "clsx"; - -export type FutureSyncStatusItem = { - key: string; - state: "ready" | "syncing"; - label: string; - note: string; -}; - -export function FutureRefreshLock({ locale }: { locale: string }) { - return ( -
-
- ); -} - -export function FutureSyncStatusStrip({ - items, - compact = false, -}: { - items: readonly FutureSyncStatusItem[]; - compact?: boolean; -}) { - return ( -
- {items.map((item) => ( -
-
- ))} -
- ); -} diff --git a/frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx b/frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx deleted file mode 100644 index ae80b950..00000000 --- a/frontend/components/dashboard/FutureForecastModalWeatherIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - Cloud, - CloudFog, - CloudLightning, - CloudRain, - CloudSnow, - CloudSun, - Search, - Sun, - Wind, -} from "lucide-react"; - -export function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) { - if (emoji === "☀️") return ; - if (emoji === "⛅" || emoji === "🌤️") - return ; - if (emoji === "☁️") return ; - if (emoji === "🌧️" || emoji === "🌦️") - return ; - if (emoji === "⛈️") return ; - if (emoji === "❄️" || emoji === "🌨️") - return ; - if (emoji === "🌫️") return ; - if (emoji === "💨") return ; - return ; -} diff --git a/frontend/components/dashboard/HeaderBar.tsx b/frontend/components/dashboard/HeaderBar.tsx deleted file mode 100644 index 692a648b..00000000 --- a/frontend/components/dashboard/HeaderBar.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; - -import Link from "next/link"; -import Image from "next/image"; -import { usePathname } from "next/navigation"; -import clsx from "clsx"; -import { - LogIn, - UserRound, - RotateCw, - BookOpen, - MoreHorizontal, - CloudSun, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useDashboardStore, useProAccess } from "@/hooks/useDashboardStore"; -import { useI18n } from "@/hooks/useI18n"; - -function parseExpiryInfo(raw?: string | null) { - const text = String(raw || "").trim(); - if (!text) return null; - const dt = new Date(text); - if (Number.isNaN(dt.getTime())) return null; - const diffMs = dt.getTime() - Date.now(); - const daysLeft = Math.ceil(diffMs / 86_400_000); - return { - date: dt, - daysLeft, - expired: diffMs <= 0, - }; -} - -export function HeaderBar({ - refreshAction, - refreshSpinning, -}: { - refreshAction?: () => void | Promise; - refreshSpinning?: boolean; -}) { - const store = useDashboardStore(); - const { proAccess } = useProAccess(); - const { locale, t, toggleLocale } = useI18n(); - const pathname = usePathname(); - const isAuthenticated = proAccess.authenticated; - const docsHref = "/docs/intro"; - const docsActive = pathname?.startsWith("/docs"); - const navItems = [ - { - href: "/", - label: locale === "en-US" ? "Dashboard" : "总览", - active: pathname === "/", - }, - ]; - const isRefreshing = refreshSpinning ?? store.loadingState.refresh; - const handleRefresh = () => { - if (refreshAction) { - return void refreshAction(); - } - return void store.refreshAll(); - }; - - const accountHref = isAuthenticated - ? "/account" - : "/auth/login?next=%2Faccount"; - const accountAria = isAuthenticated - ? t("header.accountAria") - : t("header.signInAria"); - const effectiveExpiry = proAccess.subscriptionActive - ? proAccess.subscriptionTotalExpiresAt || - proAccess.subscriptionExpiresAt - : proAccess.subscriptionExpiresAt; - const expiryInfo = parseExpiryInfo(effectiveExpiry); - const hasQueuedExtension = Boolean( - proAccess.subscriptionActive && - proAccess.subscriptionQueuedDays > 0, - ); - const showRenewReminder = - isAuthenticated && - !proAccess.loading && - !hasQueuedExtension && - ((proAccess.subscriptionActive && - expiryInfo && - expiryInfo.daysLeft <= 3) || - (!proAccess.subscriptionActive && Boolean(expiryInfo))); - const renewReminderLabel = !showRenewReminder - ? "" - : !proAccess.subscriptionActive - ? locale === "en-US" - ? "Pro expired" - : "Pro 已到期" - : locale === "en-US" - ? `Pro ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left` - : `Pro 还剩 ${Math.max(expiryInfo?.daysLeft || 0, 0)} 天`; - - - return ( -
-
- -

PolyWeather

- {t("header.subtitle")} -
- - - -
- - -
- - {t("header.live")} -
- - - - - - - {t("header.docs")} - - - - {isAuthenticated ? : } - - - - - {showRenewReminder ? ( - - {renewReminderLabel} - - ) : null} -
-
- ); -} - diff --git a/frontend/components/dashboard/IntradaySignalScene.tsx b/frontend/components/dashboard/IntradaySignalScene.tsx deleted file mode 100644 index 8cb16a8d..00000000 --- a/frontend/components/dashboard/IntradaySignalScene.tsx +++ /dev/null @@ -1,272 +0,0 @@ -"use client"; - -import { CSSProperties, useEffect, useRef } from "react"; -import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; - -export interface IntradaySignalMetric { - key: string; - label: string; - value: string; - hint: string; - fill: number | null; - tone: string; -} - -function clamp(value: number, min: number, max: number) { - return Math.min(Math.max(value, min), max); -} - -function getToneColor(tone: string) { - if (tone === "cyan") return "#4DA3FF"; - if (tone === "blue") return "#60a5fa"; - if (tone === "amber") return "#f59e0b"; - return "#9FB2C7"; -} - -function hexToRgba(hex: string, alpha: number) { - const sanitized = hex.replace("#", ""); - const numeric = Number.parseInt(sanitized.padEnd(6, "0"), 16); - const r = (numeric >> 16) & 255; - const g = (numeric >> 8) & 255; - const b = numeric & 255; - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} - -/* ── Draw a rounded-rect path (polyfill-safe) ── */ - -function roundRect( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - w: number, - h: number, - r: number | [number, number, number, number], -) { - const corners = typeof r === "number" ? [r, r, r, r] : r; - const [tl, tr, br, bl] = corners; - ctx.beginPath(); - ctx.moveTo(x + tl, y); - ctx.lineTo(x + w - tr, y); - ctx.quadraticCurveTo(x + w, y, x + w, y + tr); - ctx.lineTo(x + w, y + h - br); - ctx.quadraticCurveTo(x + w, y + h, x + w - br, y + h); - ctx.lineTo(x + bl, y + h); - ctx.quadraticCurveTo(x, y + h, x, y + h - bl); - ctx.lineTo(x, y + tl); - ctx.quadraticCurveTo(x, y, x + tl, y); - ctx.closePath(); -} - -/* ── Component ── */ - -export function IntradaySignalScene({ - metrics, - score, -}: { - metrics: IntradaySignalMetric[]; - score: number; -}) { - const containerRef = useRef(null); - const prefersReducedMotion = usePrefersReducedMotion(); - - useEffect(() => { - const host = containerRef.current; - if (!host) return; - - const canvas = document.createElement("canvas"); - canvas.style.cssText = "width:100%;height:100%;display:block;"; - host.appendChild(canvas); - - const ctx = canvas.getContext("2d")!; - let animationId = 0; - - const resize = () => { - const rect = host.getBoundingClientRect(); - const dpr = Math.min(window.devicePixelRatio || 1, 1.5); - canvas.width = rect.width * dpr; - canvas.height = rect.height * dpr; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - }; - - resize(); - - const observer = new ResizeObserver(resize); - observer.observe(host); - - let startTime = performance.now(); - - const render = (now: number) => { - animationId = requestAnimationFrame(render); - - const W = host.clientWidth; - const H = host.clientHeight; - if (W < 1 || H < 1) return; - - ctx.clearRect(0, 0, W, H); - - const elapsed = prefersReducedMotion ? 0 : (now - startTime) / 1000; - - /* ── Layout ── */ - const cx = W / 2; - const floorY = Math.max(H - 28, H * 0.78); - const rx = clamp(Math.min(W / 2 - 24, 160), 60, 160); - const ry = clamp(rx * 0.28, 12, 26); - - const isPositive = score >= 0; - const accentColor = isPositive ? "#4DA3FF" : "#F59E0B"; - const floorGlowColor = isPositive ? "#0e7490" : "#b45309"; - const floorBaseColor = isPositive ? "#10263b" : "#2c1d12"; - - /* ── Floor ── */ - const floorGrad = ctx.createRadialGradient(cx, floorY, 0, cx, floorY, rx); - floorGrad.addColorStop(0, hexToRgba(floorGlowColor, 0.55)); - floorGrad.addColorStop(0.5, floorBaseColor); - floorGrad.addColorStop(1, "#060e1a"); - - ctx.beginPath(); - ctx.ellipse(cx, floorY, rx, ry, 0, 0, Math.PI * 2); - ctx.fillStyle = floorGrad; - ctx.fill(); - - /* Floor glow overlay */ - const floorGlow = ctx.createRadialGradient(cx, floorY, 0, cx, floorY, rx * 0.6); - floorGlow.addColorStop(0, hexToRgba(accentColor, 0.08)); - floorGlow.addColorStop(1, "transparent"); - ctx.beginPath(); - ctx.ellipse(cx, floorY, rx * 0.6, ry * 0.6, 0, 0, Math.PI * 2); - ctx.fillStyle = floorGlow; - ctx.fill(); - - /* ── Ring (oscillating opacity) ── */ - const ringOpacity = 0.38 + Math.sin(elapsed * 0.8) * 0.1; - ctx.beginPath(); - ctx.ellipse(cx, floorY, rx - 10, Math.max(ry - 3, 8), 0, 0, Math.PI * 2); - ctx.strokeStyle = hexToRgba(accentColor, ringOpacity); - ctx.lineWidth = 2.5; - ctx.stroke(); - - /* ── Bar positions ── */ - const barSpacing = rx * 0.48; - const xPositions = [ - cx - barSpacing, - cx - barSpacing / 3, - cx + barSpacing / 3, - cx + barSpacing, - ]; - const barWidth = Math.min(26, rx * 0.16); - const limitedMetrics = metrics.slice(0, 4); - - /* ── Draw bars ── */ - limitedMetrics.forEach((metric, i) => { - const fill = metric.fill ?? 20; - const height = Math.max(12, 14 + (fill / 100) * (floorY - 55)); - const bx = xPositions[i]; - const by = floorY - height; - const color = getToneColor(metric.tone); - const baseGlow = prefersReducedMotion - ? 1 - : 1 + Math.sin(elapsed * 1.2 + i) * 0.06; - - /* Base glow ellipse */ - const glowGrad = ctx.createRadialGradient(bx, floorY, 0, bx, floorY, barWidth * 1.2); - glowGrad.addColorStop(0, hexToRgba(color, 0.35)); - glowGrad.addColorStop(1, "transparent"); - ctx.beginPath(); - ctx.ellipse(bx, floorY, barWidth * baseGlow, 7 * baseGlow, 0, 0, Math.PI * 2); - ctx.fillStyle = glowGrad; - ctx.fill(); - - /* Bar body (rounded top) */ - const barGrad = ctx.createLinearGradient(bx, by, bx, floorY); - barGrad.addColorStop(0, color); - barGrad.addColorStop(0.55, color); - barGrad.addColorStop(1, hexToRgba(color, 0.3)); - roundRect(ctx, bx - barWidth / 2, by, barWidth, height, [4, 4, 0, 0]); - ctx.fillStyle = barGrad; - ctx.fill(); - - /* Bar glow outline */ - ctx.strokeStyle = hexToRgba(color, 0.15); - ctx.lineWidth = 1; - roundRect(ctx, bx - barWidth / 2 - 1, by - 1, barWidth + 2, height + 2, [5, 5, 0, 0]); - ctx.stroke(); - - /* Cap (pulse offset per bar) */ - const pulse = prefersReducedMotion - ? 0 - : Math.sin(elapsed * 1.5 + i * 0.8) * 3; - const capY = by + pulse; - const capR = Math.max(5, barWidth * 0.22); - - /* Cap outer glow */ - const capGlow = ctx.createRadialGradient(bx, capY, 0, bx, capY, capR * 3); - capGlow.addColorStop(0, hexToRgba(color, 0.2)); - capGlow.addColorStop(1, "transparent"); - ctx.beginPath(); - ctx.arc(bx, capY, capR * 3, 0, Math.PI * 2); - ctx.fillStyle = capGlow; - ctx.fill(); - - /* Cap body */ - ctx.beginPath(); - ctx.arc(bx, capY, capR, 0, Math.PI * 2); - ctx.fillStyle = color; - ctx.shadowColor = color; - ctx.shadowBlur = capR * 2; - ctx.fill(); - ctx.shadowBlur = 0; - }); - - /* ── Ambient sparkle dots (only when not reduced motion) ── */ - if (!prefersReducedMotion) { - for (let i = 0; i < 6; i++) { - const angle = elapsed * 0.25 + i * 1.05; - const dist = rx * 0.7 + Math.sin(elapsed * 0.15 + i * 2.3) * 10; - const sx = cx + Math.cos(angle) * dist; - const sy = floorY - ry * 0.4 + Math.sin(angle) * dist * 0.35; - const sparkleSize = 1.5 + Math.sin(elapsed * 2 + i * 1.7) * 0.8; - ctx.beginPath(); - ctx.arc(sx, sy, Math.max(0.5, sparkleSize), 0, Math.PI * 2); - ctx.fillStyle = hexToRgba(accentColor, 0.12 + Math.sin(elapsed * 1.3 + i) * 0.06); - ctx.fill(); - } - } - } - - animationId = requestAnimationFrame(render); - - return () => { - observer.disconnect(); - cancelAnimationFrame(animationId); - if (canvas.parentNode === host) { - host.removeChild(canvas); - } - }; - }, [metrics, prefersReducedMotion, score]); - - return ( -
- - ); -} diff --git a/frontend/components/dashboard/MapCanvas.tsx b/frontend/components/dashboard/MapCanvas.tsx deleted file mode 100644 index cdf84dc2..00000000 --- a/frontend/components/dashboard/MapCanvas.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import "leaflet/dist/leaflet.css"; - -import { useDashboardStore } from "@/hooks/useDashboardStore"; -import { useLeafletMap } from "@/hooks/useLeafletMap"; - -export function MapCanvas({ - onCitySelect, - selectionMode = "focus", -}: { - onCitySelect?: (cityName: string) => void; - selectionMode?: "focus" | "select"; -} = {}) { - const store = useDashboardStore(); - const { containerRef } = useLeafletMap({ - cities: store.cities, - cityDetailsByName: store.cityDetailsByName, - citySummariesByName: store.citySummariesByName, - onClosePanel: store.closePanel, - onEnsureCityDetail: store.ensureCityDetail, - onMapInteractionChange: store.setMapInteractionActive, - onRegisterStopMotion: store.registerMapStopMotion, - onSelectCity: (cityName) => { - onCitySelect?.(cityName); - if (selectionMode === "select") { - void store.selectCity(cityName); - return; - } - void store.focusCity(cityName); - }, - selectedCity: store.selectedCity, - selectedDetail: store.selectedDetail, - suspendMotion: Boolean(store.futureModalDate), - isLoadingDetail: store.loadingState.cityDetail, - }); - - return
; -} diff --git a/frontend/components/dashboard/PolyWeatherDashboard.tsx b/frontend/components/dashboard/PolyWeatherDashboard.tsx deleted file mode 100644 index 54c7e0da..00000000 --- a/frontend/components/dashboard/PolyWeatherDashboard.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client"; - -import { ScanTerminalDashboard } from "@/components/dashboard/ScanTerminalDashboard"; - -export function PolyWeatherDashboard() { - return ; -} diff --git a/frontend/components/dashboard/ScanFilterPanel.tsx b/frontend/components/dashboard/ScanFilterPanel.tsx deleted file mode 100644 index 7fc3a0d1..00000000 --- a/frontend/components/dashboard/ScanFilterPanel.tsx +++ /dev/null @@ -1,116 +0,0 @@ -"use client"; - -import React from "react"; -import { - CircleDot, - Clock3, - Info, - TrendingUp, - Zap, -} from "lucide-react"; -import { useI18n } from "@/hooks/useI18n"; -import type { ScanTerminalFilters } from "@/lib/dashboard-types"; - -export interface FilterState extends ScanTerminalFilters {} - -const SCAN_MODES = [ - { - key: "tradable" as const, - icon: Zap, - labelEn: "Tradable", - labelZh: "可交易机会", - descEn: "Find the best immediate trade", - descZh: "发现当前最值得交易的市场", - }, - { - key: "early" as const, - icon: Clock3, - labelEn: "Early", - labelZh: "早期机会", - descEn: "Long-horizon, lower-priced setups", - descZh: "长时间布局,低价市场", - }, - { - key: "touch" as const, - icon: CircleDot, - labelEn: "Touch Play", - labelZh: "触达博弈", - descEn: "Markets approaching the settle line", - descZh: "接近决策,博弈是否触达", - }, - { - key: "trend" as const, - icon: TrendingUp, - labelEn: "Trend", - labelZh: "趋势确认", - descEn: "Trend-confirmed follow-through", - descZh: "趋势明朗,顺势交易", - }, -] as const; - -export function ScanFilterPanel({ - value, - onChange, -}: { - value: FilterState; - onChange?: (filters: FilterState) => void; -}) { - const { locale } = useI18n(); - const isEn = locale === "en-US"; - - const updateFilter = ( - key: K, - nextValue: FilterState[K], - ) => { - onChange?.({ - ...value, - [key]: nextValue, - }); - }; - - return ( - - ); -} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 75898c90..ca528d38 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -394,15 +394,11 @@ function CityGroupedTable({ {isExpanded && buckets.map((row) => ( onSelect(row)} + className="border-b border-slate-50 bg-slate-50/30 text-slate-500" > - {row.target_label || "--"} + {row.target_label || "--"} {pct(row.edge_percent)} diff --git a/frontend/components/dashboard/WeatherAuraLayer.tsx b/frontend/components/dashboard/WeatherAuraLayer.tsx deleted file mode 100644 index a5c116c9..00000000 --- a/frontend/components/dashboard/WeatherAuraLayer.tsx +++ /dev/null @@ -1,451 +0,0 @@ -"use client"; - -import { CSSProperties, useEffect, useMemo, useState } from "react"; -import { useDashboardSelection } from "@/hooks/useDashboardStore"; -import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; -import { getWeatherAuraProfile } from "@/lib/weather-aura"; - -/* ────────────────────────────────────────────────────────────── - Pure-CSS weather-aura particle layer - Replaces the former Three.js WebGL implementation with - CSS @keyframes animations — no canvas, no WebGL. - ────────────────────────────────────────────────────────────── */ - -function hexToRgba(hex: string, alpha: number) { - const sanitized = hex.replace("#", ""); - const normalized = - sanitized.length === 3 - ? sanitized - .split("") - .map((char) => `${char}${char}`) - .join("") - : sanitized.padEnd(6, "0"); - const numeric = Number.parseInt(normalized, 16); - const r = (numeric >> 16) & 255; - const g = (numeric >> 8) & 255; - const b = numeric & 255; - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} - -/* ── Injection-safe style IDs ── */ - -const AURA_STYLE_ID = "poly-aura-keyframes"; - -/* ── CSS @keyframes (injected once via