调整扫描面板并新增今日分析入口
This commit is contained in:
@@ -9552,6 +9552,26 @@
|
||||
background: rgba(8, 17, 30, 0.8);
|
||||
}
|
||||
|
||||
.root :global(.scan-map-shell .map) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-map-shell .leaflet-container) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #060d18;
|
||||
}
|
||||
|
||||
.root :global(.scan-map-caption) {
|
||||
color: #8fa4c3;
|
||||
font-size: 13px;
|
||||
@@ -10003,6 +10023,28 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.root :global(.scan-detail-primary-actions) {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.root :global(.scan-detail-analysis-button) {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(23, 217, 139, 0.3);
|
||||
border-radius: 14px;
|
||||
background: rgba(23, 217, 139, 0.12);
|
||||
color: #27ea98;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root :global(.scan-detail-analysis-button:hover) {
|
||||
background: rgba(23, 217, 139, 0.18);
|
||||
border-color: rgba(23, 217, 139, 0.4);
|
||||
}
|
||||
|
||||
.root :global(.scan-detail-section) {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid rgba(90, 123, 166, 0.12);
|
||||
|
||||
@@ -77,12 +77,18 @@ export function getWindowPhaseMeta(
|
||||
tone: "red",
|
||||
};
|
||||
}
|
||||
if (mode === "setup_today" || mode === "early_today") {
|
||||
if (mode === "setup_today") {
|
||||
return {
|
||||
label: locale === "en-US" ? "Touch Play" : "触达博弈",
|
||||
tone: "red",
|
||||
};
|
||||
}
|
||||
if (mode === "early_today") {
|
||||
return {
|
||||
label: locale === "en-US" ? "Early Today" : "日内早段",
|
||||
tone: "blue",
|
||||
};
|
||||
}
|
||||
if (mode === "tomorrow" || mode === "week_ahead") {
|
||||
return {
|
||||
label: locale === "en-US" ? "Early" : "早期机会",
|
||||
@@ -223,7 +229,7 @@ export function OpportunityTable({
|
||||
<span />
|
||||
<span>{isEn ? "City / Market" : "城市 / 市场"}</span>
|
||||
<span>{isEn ? "Local Time / Phase" : "当前时间 / 阶段"}</span>
|
||||
<span>{isEn ? "Model vs Market" : "模型分布 vs 市场分布"}</span>
|
||||
<span>{isEn ? "EMOS vs Market" : "EMOS 分布 vs 市场分布"}</span>
|
||||
<span>{isEn ? "Best Opportunity" : "最佳机会"}</span>
|
||||
<span>{isEn ? "Edge" : "边际优势"}</span>
|
||||
<span>{isEn ? "Score" : "综合得分"}</span>
|
||||
|
||||
@@ -50,9 +50,6 @@ const SCAN_MODES = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const LIQUIDITY_OPTIONS = [500, 1000, 5000, 10000];
|
||||
const EDGE_OPTIONS = [1, 2, 3, 5, 8];
|
||||
|
||||
export function ScanFilterPanel({
|
||||
value,
|
||||
onChange,
|
||||
@@ -127,149 +124,6 @@ export function ScanFilterPanel({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="scan-filter-section">
|
||||
<div className="scan-filter-heading">
|
||||
<span>{isEn ? "Filters" : "筛选条件"}</span>
|
||||
<Info size={14} />
|
||||
</div>
|
||||
|
||||
<div className="scan-range-card">
|
||||
<div className="scan-filter-row-title">
|
||||
{isEn ? "Price Range" : "价格范围"}
|
||||
</div>
|
||||
<div className="scan-range-track-wrap">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={Math.round(value.min_price * 100)}
|
||||
onChange={(e) =>
|
||||
updateFilter(
|
||||
"min_price",
|
||||
Math.min(Number(e.target.value) / 100, value.max_price),
|
||||
)
|
||||
}
|
||||
className="scan-range-slider min"
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={Math.round(value.max_price * 100)}
|
||||
onChange={(e) =>
|
||||
updateFilter(
|
||||
"max_price",
|
||||
Math.max(Number(e.target.value) / 100, value.min_price),
|
||||
)
|
||||
}
|
||||
className="scan-range-slider max"
|
||||
/>
|
||||
</div>
|
||||
<div className="scan-range-labels">
|
||||
<span>{value.min_price.toFixed(2)}</span>
|
||||
<span>{value.max_price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="scan-filter-row">
|
||||
<span className="scan-filter-row-label">
|
||||
{isEn ? "Min Liquidity" : "最小成交量"}
|
||||
</span>
|
||||
<select
|
||||
className="scan-select"
|
||||
value={value.min_liquidity}
|
||||
onChange={(e) => updateFilter("min_liquidity", Number(e.target.value))}
|
||||
>
|
||||
{LIQUIDITY_OPTIONS.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
${option.toLocaleString("en-US")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="scan-filter-row">
|
||||
<span className="scan-filter-row-label">
|
||||
{isEn ? "Min Edge" : "最小边际优势"}
|
||||
</span>
|
||||
<select
|
||||
className="scan-select"
|
||||
value={value.min_edge_pct}
|
||||
onChange={(e) => updateFilter("min_edge_pct", Number(e.target.value))}
|
||||
>
|
||||
{EDGE_OPTIONS.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}%
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="scan-filter-row inline">
|
||||
<span className="scan-filter-row-label">
|
||||
{isEn ? "High Liquidity Only" : "只看高流动性"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`scan-toggle ${value.high_liquidity_only ? "active" : ""}`}
|
||||
onClick={() => {
|
||||
const nextValue = !value.high_liquidity_only;
|
||||
onChange?.({
|
||||
...value,
|
||||
high_liquidity_only: nextValue,
|
||||
min_liquidity: nextValue
|
||||
? Math.max(value.min_liquidity, 5000)
|
||||
: value.min_liquidity,
|
||||
});
|
||||
}}
|
||||
aria-pressed={value.high_liquidity_only}
|
||||
>
|
||||
<span className="scan-toggle-knob" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="scan-filter-row">
|
||||
<span className="scan-filter-row-label">
|
||||
{isEn ? "Market Type" : "市场类型"}
|
||||
</span>
|
||||
<select
|
||||
className="scan-select"
|
||||
value={value.market_type}
|
||||
onChange={(e) =>
|
||||
updateFilter(
|
||||
"market_type",
|
||||
e.target.value as FilterState["market_type"],
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="maxtemp">
|
||||
{isEn ? "Max Temperature" : "最高温度"}
|
||||
</option>
|
||||
<option value="all">{isEn ? "All Markets" : "所有市场"}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="scan-filter-row">
|
||||
<span className="scan-filter-row-label">
|
||||
{isEn ? "Time Range" : "时间范围"}
|
||||
</span>
|
||||
<select
|
||||
className="scan-select"
|
||||
value={value.time_range}
|
||||
onChange={(e) =>
|
||||
updateFilter(
|
||||
"time_range",
|
||||
e.target.value as FilterState["time_range"],
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="today">{isEn ? "Today" : "今天"}</option>
|
||||
<option value="tomorrow">{isEn ? "Tomorrow" : "明天"}</option>
|
||||
<option value="week">{isEn ? "This Week" : "本周"}</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="scan-cta-button"
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Menu,
|
||||
RefreshCw,
|
||||
UserRound,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
startTransition,
|
||||
@@ -27,6 +26,7 @@ import { MapCanvas } from "@/components/dashboard/MapCanvas";
|
||||
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
|
||||
import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar";
|
||||
import { OpportunityTable } from "@/components/dashboard/OpportunityTable";
|
||||
import { FutureForecastModal } from "@/components/dashboard/FutureForecastModal";
|
||||
import {
|
||||
DashboardStoreProvider,
|
||||
useDashboardStore,
|
||||
@@ -247,8 +247,10 @@ function DetailPanel({
|
||||
marketScan?: MarketScan | null;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const store = useDashboardStore();
|
||||
const { locale } = useI18n();
|
||||
const isEn = locale === "en-US";
|
||||
const isPro = store.proAccess.subscriptionActive;
|
||||
|
||||
if (!row) {
|
||||
return (
|
||||
@@ -287,6 +289,12 @@ function DetailPanel({
|
||||
const scoreClass = scoreTone(displayRow.final_score);
|
||||
const phaseMeta = getWindowPhaseMeta(displayRow, locale);
|
||||
|
||||
const openTodayAnalysis = async () => {
|
||||
if (!row.city) return;
|
||||
await store.selectCity(row.city);
|
||||
await store.openTodayModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="scan-detail-panel">
|
||||
<div className="scan-detail-header">
|
||||
@@ -301,9 +309,6 @@ function DetailPanel({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="scan-detail-icon-button" aria-label="close">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="scan-detail-volume-row">
|
||||
@@ -319,6 +324,22 @@ function DetailPanel({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="scan-detail-primary-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="scan-detail-analysis-button"
|
||||
onClick={() => void openTodayAnalysis()}
|
||||
>
|
||||
{isPro
|
||||
? isEn
|
||||
? "Today's Intraday Analysis"
|
||||
: "今日日内分析"
|
||||
: isEn
|
||||
? "Today's Intraday Analysis · Pro"
|
||||
: "今日日内分析 · Pro"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="scan-detail-section">
|
||||
<div className="scan-detail-section-title">
|
||||
{isEn ? "Current Context" : "当前概况"}
|
||||
@@ -804,15 +825,6 @@ function ScanTerminalScreen() {
|
||||
selectedRowId={selectedRowId}
|
||||
onSelectRow={(row) => setSelectedRowId(row.id)}
|
||||
/>
|
||||
{deferredRows.length ? (
|
||||
<div className="scan-view-all-wrap">
|
||||
<button type="button" className="scan-view-all-button">
|
||||
{isEn
|
||||
? `View all ${deferredRows.length} opportunities`
|
||||
: `查看全部 ${deferredRows.length} 个机会`}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -985,6 +997,7 @@ export function ScanTerminalDashboard() {
|
||||
<I18nProvider>
|
||||
<DashboardStoreProvider>
|
||||
<ScanTerminalScreen />
|
||||
<FutureForecastModal />
|
||||
</DashboardStoreProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 451 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 339 KiB |
Reference in New Issue
Block a user