From 4950e31c69b753739499dec79dda66a7fcbc8d33 Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Fri, 24 Apr 2026 03:30:55 +0800
Subject: [PATCH] =?UTF-8?q?=E6=89=A9=E5=B1=95=E6=89=AB=E6=8F=8F=E7=BB=88?=
=?UTF-8?q?=E7=AB=AF=E8=A7=86=E5=9B=BE=E5=B9=B6=E4=BC=98=E5=8C=96=E6=B8=A9?=
=?UTF-8?q?=E5=BA=A6=E6=A0=87=E7=AD=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/dashboard/Dashboard.module.css | 163 +++++++++
.../components/dashboard/OpportunityTable.tsx | 30 +-
.../dashboard/ScanTerminalDashboard.tsx | 325 ++++++++++++++++--
3 files changed, 486 insertions(+), 32 deletions(-)
diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css
index 35dcfeb3..9e5459d4 100644
--- a/frontend/components/dashboard/Dashboard.module.css
+++ b/frontend/components/dashboard/Dashboard.module.css
@@ -9238,6 +9238,8 @@
padding: 18px;
gap: 18px;
overflow: hidden;
+ min-height: calc(100vh - 32px);
+ max-height: calc(100vh - 32px);
}
.root :global(.scan-topbar) {
@@ -9445,6 +9447,7 @@
.root :global(.scan-list-section) {
display: flex;
flex-direction: column;
+ flex: 1;
min-height: 0;
}
@@ -9521,9 +9524,169 @@
}
.root :global(.scan-table-body) {
+ flex: 1;
+ min-height: 0;
overflow-y: auto;
}
+.root :global(.scan-map-view),
+.root :global(.scan-calendar-view),
+.root :global(.scan-settings-view) {
+ flex: 1;
+ min-height: 0;
+}
+
+.root :global(.scan-map-view) {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.root :global(.scan-map-shell) {
+ position: relative;
+ flex: 1;
+ min-height: 520px;
+ border: 1px solid rgba(90, 123, 166, 0.12);
+ border-radius: 18px;
+ overflow: hidden;
+ background: rgba(8, 17, 30, 0.8);
+}
+
+.root :global(.scan-map-caption) {
+ color: #8fa4c3;
+ font-size: 13px;
+}
+
+.root :global(.scan-calendar-view) {
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ padding-right: 4px;
+}
+
+.root :global(.scan-calendar-group) {
+ border: 1px solid rgba(90, 123, 166, 0.12);
+ border-radius: 18px;
+ background: rgba(8, 17, 30, 0.8);
+ overflow: hidden;
+}
+
+.root :global(.scan-calendar-group-head) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 14px 18px;
+ border-bottom: 1px solid rgba(90, 123, 166, 0.1);
+}
+
+.root :global(.scan-calendar-date) {
+ font-size: 16px;
+ font-weight: 800;
+ color: #eef7ff;
+}
+
+.root :global(.scan-calendar-count) {
+ font-size: 13px;
+ color: #8fa4c3;
+}
+
+.root :global(.scan-calendar-grid) {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
+ padding: 14px;
+}
+
+.root :global(.scan-calendar-card),
+.root :global(.scan-summary-card),
+.root :global(.scan-settings-card) {
+ border: 1px solid rgba(90, 123, 166, 0.12);
+ border-radius: 16px;
+ background: linear-gradient(180deg, rgba(10, 20, 35, 0.92), rgba(8, 17, 29, 0.92));
+}
+
+.root :global(.scan-calendar-card) {
+ text-align: left;
+ padding: 14px;
+}
+
+.root :global(.scan-calendar-card.selected) {
+ box-shadow: inset 0 0 0 1px rgba(23, 217, 139, 0.42);
+}
+
+.root :global(.scan-calendar-city) {
+ font-size: 16px;
+ font-weight: 800;
+ color: #eef7ff;
+}
+
+.root :global(.scan-calendar-action) {
+ margin-top: 8px;
+ font-size: 15px;
+ font-weight: 800;
+}
+
+.root :global(.scan-calendar-action.buy) {
+ color: #21e391;
+}
+
+.root :global(.scan-calendar-action.sell) {
+ color: #ff6e6e;
+}
+
+.root :global(.scan-calendar-meta) {
+ margin-top: 10px;
+ display: flex;
+ justify-content: space-between;
+ color: #8fa4c3;
+ font-size: 13px;
+}
+
+.root :global(.scan-summary-grid) {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.root :global(.scan-summary-grid.monitor) {
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+}
+
+.root :global(.scan-summary-card),
+.root :global(.scan-settings-card) {
+ padding: 18px;
+}
+
+.root :global(.scan-summary-card.wide) {
+ grid-column: span 1;
+}
+
+.root :global(.scan-summary-label) {
+ font-size: 13px;
+ color: #8fa4c3;
+}
+
+.root :global(.scan-summary-value) {
+ margin-top: 10px;
+ font-size: 28px;
+ font-weight: 800;
+ color: #eef7ff;
+}
+
+.root :global(.scan-settings-view) {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.root :global(.scan-settings-copy) {
+ margin-top: 10px;
+ color: #c8d8ee;
+ font-size: 14px;
+ line-height: 1.6;
+}
+
.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;
diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx
index 79e377e1..c1c4d0e4 100644
--- a/frontend/components/dashboard/OpportunityTable.tsx
+++ b/frontend/components/dashboard/OpportunityTable.tsx
@@ -46,13 +46,22 @@ function formatWindowMinutes(value: number | null | undefined, locale: string) {
return `剩余 ${hours}h ${remains}m`;
}
-function formatAction(row: ScanOpportunityRow, locale: string) {
- if (row.action) return row.action;
+function formatAction(
+ row: ScanOpportunityRow,
+ locale: string,
+ tempSymbol?: string | null,
+) {
+ const formattedTarget = normalizeTemperatureLabel(row.target_label, tempSymbol);
+ if (row.action) {
+ return row.target_label
+ ? row.action.replace(String(row.target_label), formattedTarget || String(row.target_label))
+ : row.action;
+ }
if (row.side === "yes") {
- return `${locale === "en-US" ? "Buy Yes" : "买入 Yes"} ${row.target_label || ""}`.trim();
+ return `${locale === "en-US" ? "Buy Yes" : "买入 Yes"} ${formattedTarget || ""}`.trim();
}
if (row.side === "no") {
- return `${locale === "en-US" ? "Buy No" : "买入 No"} ${row.target_label || ""}`.trim();
+ return `${locale === "en-US" ? "Buy No" : "买入 No"} ${formattedTarget || ""}`.trim();
}
return "--";
}
@@ -223,6 +232,7 @@ export function OpportunityTable({
{rows.map((row, index) => {
const phaseMeta = getWindowPhaseMeta(row, locale);
+ const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol);
const localizedCityName = getLocalizedCityName(
row.city,
row.city_display_name || row.display_name || row.city,
@@ -265,16 +275,18 @@ export function OpportunityTable({
-
-
- {formatAction(row, locale)}
-
+
+
+ {formatAction(row, locale, tempSymbol)}
+
{formatPercent(row.ask != null ? row.ask * 100 : null)} →{" "}
{formatPercent(row.model_probability != null ? row.model_probability * 100 : null)}
- {row.target_label || row.market_direction || "--"}
+ {normalizeTemperatureLabel(row.target_label, tempSymbol) ||
+ row.market_direction ||
+ "--"}
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx
index ff98e640..d982cd73 100644
--- a/frontend/components/dashboard/ScanTerminalDashboard.tsx
+++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx
@@ -23,6 +23,7 @@ import {
FilterState,
ScanFilterPanel,
} from "@/components/dashboard/ScanFilterPanel";
+import { MapCanvas } from "@/components/dashboard/MapCanvas";
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar";
import { OpportunityTable } from "@/components/dashboard/OpportunityTable";
@@ -69,6 +70,17 @@ const NAV_ITEMS = [
{ zh: "设置", en: "Settings" },
];
+type TopSection = "terminal" | "markets" | "analysis" | "portfolio" | "monitor" | "settings";
+type ContentView = "list" | "map" | "calendar";
+const TOP_SECTION_ORDER: TopSection[] = [
+ "terminal",
+ "markets",
+ "analysis",
+ "portfolio",
+ "monitor",
+ "settings",
+];
+
function formatPercent(value?: number | null, signed = false) {
if (value == null || Number.isNaN(Number(value))) return "--";
const numeric = Number(value);
@@ -125,6 +137,16 @@ function confidenceLabel(score?: number | null, locale = "zh-CN") {
return "观察";
}
+function formatShortDate(value?: string | null, locale = "zh-CN") {
+ const text = String(value || "").trim();
+ if (!text) return "--";
+ const date = new Date(`${text}T00:00:00`);
+ if (Number.isNaN(date.getTime())) return text;
+ return locale === "en-US"
+ ? date.toLocaleDateString("en-US", { month: "short", day: "numeric" })
+ : date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" });
+}
+
function getSideRow(
marketScan: MarketScan | null | undefined,
selectedRow: ScanOpportunityRow,
@@ -421,7 +443,8 @@ function DetailPanel({
- {isEn ? "Buy Yes" : "买入 Yes"} {displayRow.target_label || ""}
+ {isEn ? "Buy Yes" : "买入 Yes"}{" "}
+ {normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || ""}
{formatPrice(getDetailSideAsk(yesRow, marketScan, "yes"))} →{" "}
@@ -435,7 +458,8 @@ function DetailPanel({
- {isEn ? "Buy No" : "买入 No"} {displayRow.target_label || ""}
+ {isEn ? "Buy No" : "买入 No"}{" "}
+ {normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || ""}
{formatPrice(getDetailSideAsk(noRow, marketScan, "no"))} →{" "}
@@ -475,6 +499,187 @@ function DetailPanel({
);
}
+function CalendarView({
+ rows,
+ locale,
+ selectedRowId,
+ onSelectRow,
+}: {
+ rows: ScanOpportunityRow[];
+ locale: string;
+ selectedRowId: string | null;
+ onSelectRow: (row: ScanOpportunityRow) => void;
+}) {
+ const groups = useMemo(() => {
+ const byDate = new Map();
+ rows.forEach((row) => {
+ const key = String(row.selected_date || row.local_date || "unknown");
+ const list = byDate.get(key) || [];
+ list.push(row);
+ byDate.set(key, list);
+ });
+ return Array.from(byDate.entries()).sort(([left], [right]) => left.localeCompare(right));
+ }, [rows]);
+
+ if (!groups.length) {
+ return (
+
+
+ {locale === "en-US" ? "No dated opportunities" : "当前没有日期机会"}
+
+
+ );
+ }
+
+ return (
+
+ {groups.map(([date, items]) => (
+
+
+
{formatShortDate(date, locale)}
+
+ {locale === "en-US" ? `${items.length} rows` : `${items.length} 条`}
+
+
+
+ {items.map((row) => (
+
+ ))}
+
+
+ ))}
+
+ );
+}
+
+function OverviewMapView({ locale }: { locale: string }) {
+ const store = useDashboardStore();
+
+ return (
+
+
+
+
+
+ {locale === "en-US"
+ ? `Monitoring ${store.cities.length} cities on the original map canvas.`
+ : `正在用原地图画布监控 ${store.cities.length} 个城市。`}
+
+
+ );
+}
+
+function PortfolioView({
+ rows,
+ locale,
+}: {
+ rows: ScanOpportunityRow[];
+ locale: string;
+}) {
+ const yesCount = rows.filter((row) => row.side === "yes").length;
+ const noCount = rows.filter((row) => row.side === "no").length;
+ const avgScore =
+ rows.length > 0
+ ? rows.reduce((sum, row) => sum + Number(row.final_score || 0), 0) / rows.length
+ : null;
+
+ return (
+
+
+
{locale === "en-US" ? "YES Ideas" : "YES 机会"}
+
{yesCount}
+
+
+
{locale === "en-US" ? "NO Ideas" : "NO 机会"}
+
{noCount}
+
+
+
{locale === "en-US" ? "Avg Score" : "平均评分"}
+
+ {avgScore != null ? `${avgScore.toFixed(0)}/100` : "--"}
+
+
+
+ );
+}
+
+function MonitorView({
+ rows,
+ locale,
+}: {
+ rows: ScanOpportunityRow[];
+ locale: string;
+}) {
+ const groups = useMemo(() => {
+ const result = new Map();
+ rows.forEach((row) => {
+ const label = getWindowPhaseMeta(row, locale).label;
+ result.set(label, (result.get(label) || 0) + 1);
+ });
+ return Array.from(result.entries());
+ }, [rows, locale]);
+
+ return (
+
+ {groups.map(([label, count]) => (
+
+ ))}
+
+ );
+}
+
+function SettingsView({ locale }: { locale: string }) {
+ return (
+
+
+
{locale === "en-US" ? "Market Discovery" : "市场发现"}
+
+ {locale === "en-US" ? "Gamma REST refreshes every 60s." : "Gamma REST 每 60 秒刷新一次。"}
+
+
+
+
{locale === "en-US" ? "Price Layer" : "价格层"}
+
+ {locale === "en-US"
+ ? "CLOB REST prices and books refresh every 30s."
+ : "CLOB REST 价格和盘口每 30 秒刷新一次。"}
+
+
+
+
{locale === "en-US" ? "Server Profile" : "服务器配置"}
+
+ {locale === "en-US"
+ ? "2GB profile: one polling loop, cached REST, no websocket fan-out."
+ : "2G 配置:单轮询、REST 缓存优先、不做 websocket 扇出。"}
+
+
+
+ );
+}
+
function ScanTerminalScreen() {
const store = useDashboardStore();
const { locale, toggleLocale } = useI18n();
@@ -490,6 +695,8 @@ function ScanTerminalScreen() {
const [selectedRowId, setSelectedRowId] = useState(null);
const [detailByRowId, setDetailByRowId] = useState>({});
const [detailLoadingId, setDetailLoadingId] = useState(null);
+ const [activeSection, setActiveSection] = useState("terminal");
+ const [activeView, setActiveView] = useState("list");
const deferredRows = useDeferredValue(terminalData?.rows || []);
const selectedRow = useMemo(() => {
@@ -560,6 +767,55 @@ function ScanTerminalScreen() {
}, [selectedRow, detailByRowId]);
const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null;
+ const resolvedView: ContentView =
+ activeSection === "markets"
+ ? "map"
+ : activeSection === "analysis"
+ ? "calendar"
+ : activeView;
+
+ const renderMainView = () => {
+ if (activeSection === "portfolio") {
+ return ;
+ }
+ if (activeSection === "monitor") {
+ return ;
+ }
+ if (activeSection === "settings") {
+ return ;
+ }
+ if (resolvedView === "map") {
+ return ;
+ }
+ if (resolvedView === "calendar") {
+ return (
+ setSelectedRowId(row.id)}
+ />
+ );
+ }
+ return (
+ <>
+ setSelectedRowId(row.id)}
+ />
+ {deferredRows.length ? (
+
+
+
+ ) : null}
+ >
+ );
+ };
return (
@@ -581,7 +837,10 @@ function ScanTerminalScreen() {
@@ -647,15 +906,50 @@ function ScanTerminalScreen() {
-
- {isEn ? "Sort: Score" : "排序:综合得分"}
+ {isEn
+ ? resolvedView === "calendar"
+ ? "Sort: Date"
+ : resolvedView === "map"
+ ? "Sort: City Map"
+ : "Sort: Score"
+ : resolvedView === "calendar"
+ ? "排序:日期"
+ : resolvedView === "map"
+ ? "排序:地图"
+ : "排序:综合得分"}
@@ -671,22 +965,7 @@ function ScanTerminalScreen() {
{error}
) : (
- <>
-
setSelectedRowId(row.id)}
- />
- {deferredRows.length ? (
-
-
- {isEn
- ? `View all ${deferredRows.length} opportunities`
- : `查看全部 ${deferredRows.length} 个机会`}
-
-
- ) : null}
- >
+ renderMainView()
)}