From 558a0031be3fdd0851b9baefc74afe33e10b0345 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 23 Apr 2026 03:16:30 +0800 Subject: [PATCH] Refine homepage to match dashboard reference --- frontend/components/dashboard/CitySidebar.tsx | 21 ++ .../components/dashboard/Dashboard.module.css | 290 ++++++++++++++++-- frontend/components/dashboard/HeaderBar.tsx | 154 ++++------ .../dashboard/PolyWeatherDashboard.tsx | 224 +++++++++++--- 4 files changed, 510 insertions(+), 179 deletions(-) diff --git a/frontend/components/dashboard/CitySidebar.tsx b/frontend/components/dashboard/CitySidebar.tsx index 1061330c..d108994e 100644 --- a/frontend/components/dashboard/CitySidebar.tsx +++ b/frontend/components/dashboard/CitySidebar.tsx @@ -186,6 +186,16 @@ export function CitySidebar() { { key: "low", label: t("sidebar.group.low") }, { key: "other", label: t("sidebar.group.other") }, ]; + const syncTime = useMemo( + () => + new Intl.DateTimeFormat(locale === "en-US" ? "en-US" : "zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).format(new Date()), + [locale], + ); return ( ); } diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index d14d2b7e..9996072a 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -97,6 +97,33 @@ gap: 10px; } +.root :global(.home-map-header) { + position: fixed; + top: calc(var(--header-height) + 28px); + left: calc(var(--sidebar-width) + 42px); + z-index: 930; + display: flex; + align-items: center; +} + +.root :global(.home-map-title) { + display: flex; + flex-direction: column; + gap: 4px; +} + +.root :global(.home-map-title strong) { + color: #f8fafc; + font-size: 15px; + font-weight: 750; +} + +.root :global(.home-map-title span) { + color: rgba(148, 163, 184, 0.76); + font-size: 11px; + font-weight: 600; +} + .root :global(.home-map-modes) { display: inline-flex; align-items: center; @@ -152,6 +179,46 @@ justify-content: center; } +.root :global(.home-map-legend) { + position: fixed; + left: 50%; + bottom: 196px; + transform: translateX(-50%); + z-index: 930; + display: flex; + align-items: center; + gap: 12px; + padding: 8px 14px; + border-radius: 14px; + border: 1px solid rgba(115, 137, 161, 0.18); + background: rgba(6, 12, 22, 0.86); + backdrop-filter: blur(16px); +} + +.root :global(.home-map-legend span) { + color: rgba(203, 213, 225, 0.82); + font-size: 10px; + font-weight: 700; +} + +.root :global(.home-map-legend-bar) { + width: 178px; + height: 8px; + border-radius: 999px; + background: linear-gradient(90deg, #2563eb, #06b6d4, #22c55e, #eab308, #ef4444); +} + +.root :global(.home-map-legend-values) { + display: flex; + align-items: center; + gap: 8px; +} + +.root :global(.home-map-legend-values span) { + color: rgba(148, 163, 184, 0.72); + font-size: 9px; +} + .weatherAura { position: absolute; inset: 0; @@ -262,10 +329,46 @@ .root :global(.brand) { display: flex; align-items: center; - gap: 14px; + gap: 12px; min-width: 0; } +.root :global(.brand-mark) { + width: 28px; + height: 28px; + border-radius: 9px; + border: 1px solid rgba(34, 211, 238, 0.28); + background: + radial-gradient(circle at 30% 30%, rgba(34, 211, 238, 0.24), transparent 52%), + linear-gradient(180deg, rgba(11, 22, 40, 0.96), rgba(6, 13, 24, 0.96)); + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.08); +} + +.root :global(.brand-mark i) { + width: 13px; + height: 13px; + border-radius: 999px; + border: 2px solid #22d3ee; + position: relative; +} + +.root :global(.brand-mark i::before), +.root :global(.brand-mark i::after) { + content: ""; + position: absolute; + inset: -4px; + border-radius: 999px; + border: 1px solid rgba(34, 211, 238, 0.34); +} + +.root :global(.brand-mark i::after) { + inset: -8px; + border-color: rgba(59, 130, 246, 0.18); +} + .root :global(.brand h1) { font-size: 18px; font-weight: 800; @@ -297,13 +400,14 @@ display: inline-flex; align-items: center; gap: 7px; - padding: 10px 12px; + padding: 12px 18px; border-radius: 10px; color: rgba(203, 213, 225, 0.82); text-decoration: none; - font-size: 13px; - font-weight: 650; + font-size: 14px; + font-weight: 550; transition: var(--transition); + position: relative; } .root :global(.header-nav-link:hover) { @@ -313,8 +417,19 @@ .root :global(.header-nav-link.active) { color: #f8fafc; - background: rgba(8, 145, 178, 0.12); - box-shadow: inset 0 -2px 0 var(--accent-cyan); + background: transparent; + box-shadow: none; +} + +.root :global(.header-nav-link.active::after) { + content: ""; + position: absolute; + left: 18px; + right: 18px; + bottom: 4px; + height: 2px; + border-radius: 999px; + background: var(--accent-cyan); } .root :global(.header-right) { @@ -414,6 +529,40 @@ .root :global(.refresh-btn.spinning) { animation: spin 1s linear infinite; } + +.root :global(.header-utility-btn) { + min-width: 32px; + height: 32px; + padding: 0 12px; + border-radius: 8px; + border: 1px solid rgba(115, 137, 161, 0.18); + background: rgba(8, 15, 27, 0.82); + color: rgba(226, 232, 240, 0.8); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + text-decoration: none; + font-size: 12px; + font-weight: 650; + transition: var(--transition); +} + +.root :global(.header-utility-btn:hover) { + color: #f8fafc; + border-color: rgba(34, 211, 238, 0.34); + background: rgba(11, 22, 40, 0.94); +} + +.root :global(.header-utility-btn.active) { + color: #f8fafc; + border-color: rgba(59, 130, 246, 0.34); +} + +.root :global(.header-utility-btn.more) { + padding: 0; + width: 32px; +} @keyframes spin { from { transform: rotate(0deg); @@ -506,6 +655,14 @@ border-radius: 2px; } +.root :global(.sidebar-footer) { + border-top: 1px solid var(--border-subtle); + padding: 10px 12px; + color: rgba(148, 163, 184, 0.72); + font-size: 10px; + line-height: 1.55; +} + .root :global(.city-group) { border: 1px solid rgba(115, 137, 161, 0.12); border-radius: 14px; @@ -811,16 +968,16 @@ flex-shrink: 0; display: inline-flex; align-items: center; - gap: 5px; - min-height: 24px; + gap: 6px; + min-height: 26px; border-radius: 999px; - padding: 4px 9px 4px 7px; - border: 1px solid rgba(148, 163, 184, 0.16); - background: rgba(9, 18, 36, 0.38); + padding: 5px 10px 5px 8px; + border: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(9, 18, 36, 0.58); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); - color: rgba(226, 232, 240, 0.82); - font-size: 11px; - font-weight: 800; + color: rgba(226, 232, 240, 0.88); + font-size: 10px; + font-weight: 900; letter-spacing: 0.01em; white-space: nowrap; } @@ -836,21 +993,21 @@ } .root :global(.home-risk-badge.high) { - border-color: rgba(248, 113, 113, 0.2); + border-color: rgba(248, 113, 113, 0.34); color: #fda4af; - background: rgba(69, 10, 10, 0.18); + background: rgba(69, 10, 10, 0.32); } .root :global(.home-risk-badge.medium) { - border-color: rgba(251, 191, 36, 0.18); - color: #fbbf24; - background: rgba(67, 36, 6, 0.16); + border-color: rgba(251, 191, 36, 0.3); + color: #fcd34d; + background: rgba(67, 36, 6, 0.28); } .root :global(.home-risk-badge.low) { - border-color: rgba(52, 211, 153, 0.18); - color: #34d399; - background: rgba(7, 47, 35, 0.16); + border-color: rgba(52, 211, 153, 0.28); + color: #86efac; + background: rgba(7, 47, 35, 0.28); } .root :global(.home-panel-subtitle) { @@ -1044,6 +1201,29 @@ background: rgba(148, 163, 184, 0.1); } +.root :global(.home-panel-header) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.root :global(.home-focus-title) { + color: #f8fafc; + font-size: 12px; + font-weight: 700; +} + +.root :global(.home-why-link) { + border: none; + background: transparent; + color: #60a5fa; + cursor: pointer; + font: inherit; + font-size: 11px; + font-weight: 650; +} + .root :global(.home-top-opportunity-label) { display: inline-flex; align-self: flex-start; @@ -1774,7 +1954,7 @@ } .root :global(.home-summary-card) { - min-height: 92px; + min-height: 118px; display: flex; flex-direction: column; justify-content: space-between; @@ -1784,23 +1964,57 @@ background: rgba(8, 15, 28, 0.7); } -.root :global(.home-summary-card span) { - color: rgba(148, 163, 184, 0.88); - font-size: 12px; - font-weight: 650; +.root :global(.home-summary-card-head) { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; } -.root :global(.home-summary-card strong) { +.root :global(.home-summary-card-head strong) { + color: #f8fafc; + font-size: 14px; + font-weight: 760; + line-height: 1.2; +} + +.root :global(.home-summary-card-head span) { + color: #60a5fa; + font-size: 10px; + font-weight: 700; +} + +.root :global(.home-summary-card-body) { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + align-items: end; +} + +.root :global(.home-summary-stat) { + display: flex; + flex-direction: column; + gap: 4px; +} + +.root :global(.home-summary-stat b) { color: #f8fafc; font-size: 28px; font-weight: 850; + line-height: 1; letter-spacing: -0.04em; } -.root :global(.home-summary-card.accent-red strong) { color: #fb7185; } -.root :global(.home-summary-card.accent-amber strong) { color: #fbbf24; } -.root :global(.home-summary-card.accent-green strong) { color: #4ade80; } -.root :global(.home-summary-card.accent-cyan strong) { color: #22d3ee; } +.root :global(.home-summary-stat span) { + color: rgba(148, 163, 184, 0.82); + font-size: 10px; + font-weight: 650; +} + +.root :global(.home-summary-stat b.accent-red) { color: #fb7185; } +.root :global(.home-summary-stat b.accent-amber) { color: #fbbf24; } +.root :global(.home-summary-stat b.accent-green) { color: #4ade80; } +.root :global(.home-summary-stat b.accent-cyan) { color: #22d3ee; } .root :global(.opportunity-strip-heading) { display: flex; @@ -1875,6 +2089,12 @@ gap: 8px; } +.root :global(.opportunity-price-pair) { + display: inline-flex; + align-items: center; + gap: 10px; +} + .root :global(.opportunity-rank) { width: 24px; height: 24px; @@ -1937,6 +2157,12 @@ font-weight: 800; } +.root :global(.opportunity-no) { + color: #fda4af; + font-size: 12px; + font-weight: 800; +} + .root :global(.opportunity-edge) { color: rgba(34, 211, 238, 0.9); font-size: 18px; diff --git a/frontend/components/dashboard/HeaderBar.tsx b/frontend/components/dashboard/HeaderBar.tsx index 33e3565a..a8d49a20 100644 --- a/frontend/components/dashboard/HeaderBar.tsx +++ b/frontend/components/dashboard/HeaderBar.tsx @@ -8,11 +8,7 @@ import { UserRound, RotateCw, BookOpen, - Sparkles, - House, - Bell, - BarChart3, - CandlestickChart, + MoreHorizontal, } from "lucide-react"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; @@ -39,7 +35,7 @@ export function HeaderBar({ refreshSpinning?: boolean; }) { const store = useDashboardStore(); - const { locale, setLocale, t } = useI18n(); + const { locale, t } = useI18n(); const pathname = usePathname(); const isAuthenticated = store.proAccess.authenticated; const docsHref = "/docs/intro"; @@ -49,37 +45,28 @@ export function HeaderBar({ href: "/", label: locale === "en-US" ? "Dashboard" : "总览", active: pathname === "/", - icon: House, }, { href: "/docs/intraday-signal", label: locale === "en-US" ? "Markets" : "市场", active: pathname?.startsWith("/docs/intraday-signal"), - icon: CandlestickChart, }, { href: "/docs/model-stack-deb", label: locale === "en-US" ? "Analytics" : "分析", active: pathname?.startsWith("/docs/model-stack-deb"), - icon: BarChart3, }, { href: "/docs/history-review", label: locale === "en-US" ? "History" : "历史", active: pathname?.startsWith("/docs/history-review"), - icon: BookOpen, }, { href: "/docs/alert-playbook", - label: locale === "en-US" ? "Alerts" : "提醒", + label: locale === "en-US" ? "Alerts" : "预警", active: pathname?.startsWith("/docs/alert-playbook"), - icon: Bell, }, ]; - const trialPromoLabel = - locale === "en-US" - ? "New users get 3-day Pro trial" - : "新用户可免费体验 3 天 Pro"; const isRefreshing = refreshSpinning ?? store.loadingState.refresh; const handleRefresh = () => { if (refreshAction) { @@ -91,9 +78,6 @@ export function HeaderBar({ const accountHref = isAuthenticated ? "/account" : "/auth/login?next=%2Faccount"; - const accountLabel = isAuthenticated - ? t("header.account") - : t("header.signIn"); const accountAria = isAuthenticated ? t("header.accountAria") : t("header.signInAria"); @@ -138,6 +122,9 @@ export function HeaderBar({ return (
+

PolyWeather

{t("header.subtitle")}
@@ -146,87 +133,18 @@ export function HeaderBar({ className="header-nav" aria-label={locale === "en-US" ? "Primary" : "主导航"} > - {navItems.map((item) => { - const Icon = item.icon; - return ( - - - {item.label} - - ); - })} + {navItems.map((item) => ( + + {item.label} + + ))}
-
- - -
- - - - {t("header.docs")} - - - - - {trialPromoLabel} - - - - {isAuthenticated ? : } - {accountLabel} - - - {showRenewReminder ? ( - - {renewReminderLabel} - - ) : null} -
{t("header.live")} @@ -241,6 +159,48 @@ export function HeaderBar({ > + + + + {t("header.docs")} + + + + {isAuthenticated ? : } + + + + + {showRenewReminder ? ( + + {renewReminderLabel} + + ) : null}
); diff --git a/frontend/components/dashboard/PolyWeatherDashboard.tsx b/frontend/components/dashboard/PolyWeatherDashboard.tsx index 5e4a8217..59516241 100644 --- a/frontend/components/dashboard/PolyWeatherDashboard.tsx +++ b/frontend/components/dashboard/PolyWeatherDashboard.tsx @@ -238,21 +238,36 @@ type HomeForecastDay = { maxTemp: number; }; -type HomeSummaryMetric = { +type HomeSummaryCard = { key: string; - label: string; - value: string; - accent?: "green" | "amber" | "red" | "cyan"; + title: string; + linkLabel?: string; + items: Array<{ + label: string; + value: string; + accent?: "green" | "amber" | "red" | "cyan"; + }>; }; -function buildDashboardSummaryMetrics( +function buildDashboardSummaryCards( snapshots: CitySnapshot[], locale: string, -): HomeSummaryMetric[] { +): HomeSummaryCard[] { const topTradable = snapshots.filter((snapshot) => snapshot.tradableOpportunity); const highCount = snapshots.filter((snapshot) => snapshot.city.deb_recent_tier === "high").length; const mediumCount = snapshots.filter((snapshot) => snapshot.city.deb_recent_tier === "medium").length; const lowCount = snapshots.filter((snapshot) => snapshot.city.deb_recent_tier === "low").length; + const strongAgreement = snapshots.filter( + (snapshot) => Number(snapshot.city.deb_recent_hit_rate ?? 0) >= 0.66, + ).length; + const mediumAgreement = snapshots.filter((snapshot) => { + const rate = Number(snapshot.city.deb_recent_hit_rate ?? 0); + return rate >= 0.4 && rate < 0.66; + }).length; + const weakAgreement = Math.max( + snapshots.length - strongAgreement - mediumAgreement, + 0, + ); const avgEdge = topTradable.length ? topTradable.reduce((sum, snapshot) => { const edgeValue = Number(getMarketEdgeValue(snapshot.detail)); @@ -266,28 +281,92 @@ function buildDashboardSummaryMetrics( return [ { - key: "high", - label: locale === "en-US" ? "High risk" : "高风险", - value: String(highCount), - accent: "red", + key: "opportunities", + title: locale === "en-US" ? "Today's Opportunities" : "今日机会分布", + items: [ + { + label: locale === "en-US" ? "High risk" : "高风险", + value: String(highCount), + accent: "red", + }, + { + label: locale === "en-US" ? "Medium risk" : "中风险", + value: String(mediumCount), + accent: "amber", + }, + { + label: locale === "en-US" ? "Low risk" : "低风险", + value: String(lowCount), + accent: "green", + }, + ], }, { - key: "medium", - label: locale === "en-US" ? "Medium risk" : "中风险", - value: String(mediumCount), - accent: "amber", + key: "market-summary", + title: locale === "en-US" ? "Market Summary" : "市场概览", + items: [ + { + label: locale === "en-US" ? "Total markets" : "总市场数", + value: String(snapshots.length), + }, + { + label: locale === "en-US" ? "Active" : "活跃市场", + value: String(topTradable.length), + accent: "cyan", + }, + { + label: locale === "en-US" ? "Avg. edge" : "平均优势", + value: formatEdge(avgEdge), + accent: "green", + }, + ], }, { - key: "low", - label: locale === "en-US" ? "Low risk" : "低风险", - value: String(lowCount), - accent: "green", + key: "model-agreement", + title: locale === "en-US" ? "Model Agreement" : "模型一致性", + items: [ + { + label: locale === "en-US" ? "High" : "高", + value: `${strongAgreement}`, + accent: "green", + }, + { + label: locale === "en-US" ? "Medium" : "中", + value: `${mediumAgreement}`, + accent: "amber", + }, + { + label: locale === "en-US" ? "Low" : "低", + value: `${weakAgreement}`, + accent: "red", + }, + ], }, { - key: "edge", - label: locale === "en-US" ? "Avg. edge" : "平均优势", - value: formatEdge(avgEdge), - accent: "cyan", + key: "impact-window", + title: locale === "en-US" ? "High Impact Window" : "高影响窗口", + items: [ + { + label: locale === "en-US" ? "Next 6 hours" : "未来 6 小时", + value: + locale === "en-US" + ? `${Math.min(topTradable.length, 12)} markets` + : `${Math.min(topTradable.length, 12)} 个市场`, + }, + { + label: locale === "en-US" ? "Focus city" : "焦点城市", + value: topTradable[0]?.city.display_name || "--", + accent: "cyan", + }, + { + label: locale === "en-US" ? "Best edge" : "最佳优势", + value: + topTradable.length > 0 + ? formatEdge(getMarketEdgeValue(topTradable[0]?.detail)) + : "--", + accent: "green", + }, + ], }, ]; } @@ -295,29 +374,50 @@ function buildDashboardSummaryMetrics( function HomeMapToolbar() { const { locale } = useI18n(); return ( -
-
- - - + + +
+
- - +