diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css
index d736bef6..a52d9001 100644
--- a/frontend/components/dashboard/Dashboard.module.css
+++ b/frontend/components/dashboard/Dashboard.module.css
@@ -9879,20 +9879,24 @@
.root :global(.scan-distribution-card) {
min-width: 0;
- text-align: center;
- padding: 9px 5px;
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 8px 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;
+ font-size: 10px;
}
.root :global(.scan-distribution-card strong) {
display: block;
- margin-bottom: 8px;
+ margin-bottom: 1px;
color: #fff;
- font-size: 14px;
+ font-size: 13px;
+ line-height: 1.1;
+ text-align: center;
overflow-wrap: anywhere;
}
@@ -9900,6 +9904,47 @@
color: #22eb98;
}
+.root :global(.scan-distribution-line) {
+ display: grid;
+ grid-template-columns: 12px minmax(22px, 1fr) auto;
+ gap: 4px;
+ align-items: center;
+ min-width: 0;
+ color: rgba(180, 199, 223, 0.9);
+}
+
+.root :global(.scan-distribution-line b) {
+ color: rgba(148, 163, 184, 0.78);
+ font-size: 9px;
+ font-weight: 800;
+}
+
+.root :global(.scan-distribution-line i) {
+ display: block;
+ max-width: 100%;
+ height: 4px;
+ border-radius: 999px;
+}
+
+.root :global(.scan-distribution-line.model i) {
+ background: linear-gradient(90deg, #17d98b, #20e3d2);
+}
+
+.root :global(.scan-distribution-line.market i) {
+ background: linear-gradient(90deg, #3f8cff, #57a8ff);
+}
+
+.root :global(.scan-distribution-line em) {
+ overflow: hidden;
+ color: rgba(226, 237, 252, 0.92);
+ font-size: 10px;
+ font-style: normal;
+ font-weight: 760;
+ line-height: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.root :global(.scan-trade-cell) {
display: flex;
flex-direction: column;
@@ -10083,35 +10128,6 @@
border-color: rgba(23, 217, 139, 0.4);
}
-.root :global(.scan-detail-inline-analysis) {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.root :global(.scan-detail-inline-analysis .models-section) {
- margin: 0;
- padding: 0;
- border: 0;
- background: transparent;
-}
-
-.root :global(.scan-detail-inline-analysis .model-bars) {
- padding: 14px;
- border-radius: 16px;
- border: 1px solid rgba(90, 123, 166, 0.14);
- background: rgba(10, 20, 35, 0.72);
-}
-
-.root :global(.scan-inline-analysis-state) {
- padding: 14px;
- border-radius: 14px;
- border: 1px solid rgba(90, 123, 166, 0.12);
- color: #9bb0cb;
- font-size: 13px;
- background: rgba(10, 20, 35, 0.52);
-}
-
.root :global(.scan-detail-section) {
padding: 16px 0;
border-bottom: 1px solid rgba(90, 123, 166, 0.12);
@@ -10447,6 +10463,14 @@
color: #647a98;
}
+.root :global(.scan-terminal.light .scan-distribution-line em) {
+ color: #243b5a;
+}
+
+.root :global(.scan-terminal.light .scan-distribution-line b) {
+ color: #6f86a4;
+}
+
.root :global(.scan-terminal.light .scan-mode-tab),
.root :global(.scan-terminal.light .scan-table-shell),
.root :global(.scan-terminal.light .scan-table-header),
@@ -10455,9 +10479,7 @@
.root :global(.scan-terminal.light .scan-summary-card),
.root :global(.scan-terminal.light .scan-settings-card),
.root :global(.scan-terminal.light .scan-distribution-card),
-.root :global(.scan-terminal.light .scan-trade-card),
-.root :global(.scan-terminal.light .scan-detail-inline-analysis .model-bars),
-.root :global(.scan-terminal.light .scan-inline-analysis-state) {
+.root :global(.scan-terminal.light .scan-trade-card) {
border-color: rgba(35, 72, 118, 0.12);
background: rgba(255, 255, 255, 0.72);
}
diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx
index 39255cc1..b3220684 100644
--- a/frontend/components/dashboard/OpportunityTable.tsx
+++ b/frontend/components/dashboard/OpportunityTable.tsx
@@ -153,33 +153,40 @@ function ProbabilityPreview({
});
}
+ const modelLabel = locale === "en-US" ? "M" : "模";
+ const marketLabel = locale === "en-US" ? "P" : "市";
+
return (
- {preview.slice(0, 6).map((item) => (
-
-
- {normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"}
-
-
- {locale === "en-US" ? "Model" : "模型"}
-
- {formatPercent(
- item.model_probability != null ? item.model_probability * 100 : null,
- )}
-
-
-
- {locale === "en-US" ? "Market" : "市场"}
-
- {formatPercent(
- item.market_probability != null ? item.market_probability * 100 : null,
- )}
-
-
- ))}
+ {preview.slice(0, 6).map((item) => {
+ const modelPercent =
+ item.model_probability != null ? item.model_probability * 100 : null;
+ const marketPercent =
+ item.market_probability != null ? item.market_probability * 100 : null;
+ const modelWidth = Math.max(3, Math.min(100, Number(modelPercent || 0)));
+ const marketWidth = Math.max(3, Math.min(100, Number(marketPercent || 0)));
+
+ return (
+
+
+ {normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"}
+
+
+ {modelLabel}
+
+ {formatPercent(modelPercent)}
+
+
+ {marketLabel}
+
+ {formatPercent(marketPercent)}
+
+
+ );
+ })}
);
}
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx
index 9172d84b..3b9d3b15 100644
--- a/frontend/components/dashboard/ScanTerminalDashboard.tsx
+++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx
@@ -25,9 +25,9 @@ import {
FilterState,
ScanFilterPanel,
} from "@/components/dashboard/ScanFilterPanel";
+import { FutureForecastModal } from "@/components/dashboard/FutureForecastModal";
import { MapCanvas } from "@/components/dashboard/MapCanvas";
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
-import { ModelForecast } from "@/components/dashboard/PanelSections";
import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar";
import { OpportunityTable } from "@/components/dashboard/OpportunityTable";
import {
@@ -151,12 +151,11 @@ function formatShortDate(value?: string | null, locale = "zh-CN") {
: date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" });
}
-function formatUserLocalTime(locale = "zh-CN") {
- return new Intl.DateTimeFormat(locale === "en-US" ? "en-US" : "zh-CN", {
- hour: "2-digit",
- minute: "2-digit",
- hour12: false,
- }).format(new Date());
+function formatUserLocalTime() {
+ const now = new Date();
+ return `${String(now.getHours()).padStart(2, "0")}:${String(
+ now.getMinutes(),
+ ).padStart(2, "0")}`;
}
function getLocalDateIndex(value?: string | null) {
@@ -345,30 +344,15 @@ function DetailPanel({
);
const scoreClass = scoreTone(displayRow.final_score);
const phaseMeta = getWindowPhaseMeta(displayRow, locale);
- const analysisDetail =
+ const cityDetail =
store.selectedDetail?.name?.toLowerCase() === row.city.toLowerCase()
? store.selectedDetail
: store.cityDetailsByName[row.city] || null;
- const [showInlineAnalysis, setShowInlineAnalysis] = useState(false);
- const analysisLoading = Boolean(
- showInlineAnalysis &&
- row.city &&
- !analysisDetail &&
- store.loadingState.cityDetail,
- );
-
- useEffect(() => {
- setShowInlineAnalysis(false);
- }, [row.id]);
const openTodayAnalysis = async () => {
if (!row.city) return;
- if (showInlineAnalysis && analysisDetail) {
- setShowInlineAnalysis(false);
- return;
- }
- setShowInlineAnalysis(true);
await store.selectCity(row.city);
+ await store.openTodayModal();
};
return (
@@ -416,29 +400,6 @@ function DetailPanel({
- {showInlineAnalysis && (
-
-
- {isEn ? "Today's Intraday Analysis" : "今日日内分析"}
-
- {analysisLoading ? (
-
- {isEn ? "Loading model spread card..." : "正在加载模型分歧卡片..."}
-
- ) : analysisDetail ? (
-
- ) : (
-
- {isEn ? "No intraday model card available yet." : "当前还没有可展示的日内模型卡片。"}
-
- )}
-
- )}
-
{isEn ? "Current Context" : "当前概况"}
@@ -503,8 +464,8 @@ function DetailPanel({
- 00:00
- 23:59
+ {cityDetail?.forecast?.sunrise || "--"}
+ {cityDetail?.forecast?.sunset || "--"}
- {phaseMeta.label}
+ {isEn ? "Sunrise / Sunset" : "日出 / 日落"} · {phaseMeta.label}
@@ -901,12 +862,12 @@ function ScanTerminalScreen() {
}, [activeFilters]);
useEffect(() => {
- setUserLocalTime(formatUserLocalTime(locale));
+ setUserLocalTime(formatUserLocalTime());
const intervalId = window.setInterval(() => {
- setUserLocalTime(formatUserLocalTime(locale));
- }, 30_000);
+ setUserLocalTime(formatUserLocalTime());
+ }, 10_000);
return () => window.clearInterval(intervalId);
- }, [locale]);
+ }, []);
useEffect(() => {
const stored = window.localStorage.getItem("polyweather_scan_theme");
@@ -932,6 +893,9 @@ function ScanTerminalScreen() {
useEffect(() => {
if (!activeDetailRow) return;
void fetchDetail(activeDetailRow);
+ if (!store.cityDetailsByName[activeDetailRow.city]) {
+ void store.ensureCityDetail(activeDetailRow.city, false, "panel").catch(() => {});
+ }
}, [activeDetailRow, detailByRowId]);
const handleMapCitySelect = useCallback((cityName: string) => {
@@ -1171,6 +1135,7 @@ export function ScanTerminalDashboard() {
+
);
diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx
index 85374d58..d8670482 100644
--- a/frontend/hooks/useDashboardStore.tsx
+++ b/frontend/hooks/useDashboardStore.tsx
@@ -1237,12 +1237,13 @@ export function DashboardStoreProvider({
},
openHistory,
openTodayModal: async (forceRefresh?: boolean) => {
- if (!selectedCity) {
+ const activeCity = selectedCityRef.current || selectedCity;
+ if (!activeCity) {
return;
}
mapStopMotionRef.current();
- const cityName = selectedCity;
+ const cityName = activeCity;
const modalSeq = (modalOpenSeqRef.current += 1);
const isLatestModalRequest = () =>
modalOpenSeqRef.current === modalSeq &&