feat: implement dashboard store and scan terminal components for city market analysis

This commit is contained in:
2569718930@qq.com
2026-04-24 06:31:29 +08:00
parent 92537b4637
commit c05cdd17c6
4 changed files with 113 additions and 118 deletions
@@ -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);
}
@@ -153,33 +153,40 @@ function ProbabilityPreview({
});
}
const modelLabel = locale === "en-US" ? "M" : "模";
const marketLabel = locale === "en-US" ? "P" : "市";
return (
<div className="scan-distribution-preview">
{preview.slice(0, 6).map((item) => (
<div
key={`${item.label}-${item.value ?? ""}`}
className={`scan-distribution-card ${item.highlighted ? "featured" : ""}`}
>
<strong>
{normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"}
</strong>
<span>
{locale === "en-US" ? "Model" : "模型"}
<br />
{formatPercent(
item.model_probability != null ? item.model_probability * 100 : null,
)}
</span>
<br />
<span>
{locale === "en-US" ? "Market" : "市场"}
<br />
{formatPercent(
item.market_probability != null ? item.market_probability * 100 : null,
)}
</span>
</div>
))}
{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 (
<div
key={`${item.label}-${item.value ?? ""}`}
className={`scan-distribution-card ${item.highlighted ? "featured" : ""}`}
>
<strong>
{normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"}
</strong>
<span className="scan-distribution-line model">
<b>{modelLabel}</b>
<i style={{ width: `${modelWidth}%` }} />
<em>{formatPercent(modelPercent)}</em>
</span>
<span className="scan-distribution-line market">
<b>{marketLabel}</b>
<i style={{ width: `${marketWidth}%` }} />
<em>{formatPercent(marketPercent)}</em>
</span>
</div>
);
})}
</div>
);
}
@@ -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({
</button>
</div>
{showInlineAnalysis && (
<section className="scan-detail-section scan-detail-inline-analysis">
<div className="scan-detail-section-title">
{isEn ? "Today's Intraday Analysis" : "今日日内分析"}
</div>
{analysisLoading ? (
<div className="scan-inline-analysis-state">
{isEn ? "Loading model spread card..." : "正在加载模型分歧卡片..."}
</div>
) : analysisDetail ? (
<ModelForecast
detail={analysisDetail}
hideTitle
targetDate={row.selected_date || null}
/>
) : (
<div className="scan-inline-analysis-state">
{isEn ? "No intraday model card available yet." : "当前还没有可展示的日内模型卡片。"}
</div>
)}
</section>
)}
<section className="scan-detail-section">
<div className="scan-detail-section-title">
{isEn ? "Current Context" : "当前概况"}
@@ -503,8 +464,8 @@ function DetailPanel({
<section className="scan-detail-section">
<div className="scan-timeline-head">
<span>00:00</span>
<span>23:59</span>
<span>{cityDetail?.forecast?.sunrise || "--"}</span>
<span>{cityDetail?.forecast?.sunset || "--"}</span>
</div>
<div className="scan-timeline-bar">
<span
@@ -521,7 +482,7 @@ function DetailPanel({
/>
</div>
<div className="scan-timeline-caption">
{phaseMeta.label}
{isEn ? "Sunrise / Sunset" : "日出 / 日落"} · {phaseMeta.label}
</div>
</section>
@@ -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() {
<I18nProvider>
<DashboardStoreProvider>
<ScanTerminalScreen />
<FutureForecastModal />
</DashboardStoreProvider>
</I18nProvider>
);
+3 -2
View File
@@ -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 &&