扩展扫描终端视图并优化温度标签
This commit is contained in:
@@ -9238,6 +9238,8 @@
|
|||||||
padding: 18px;
|
padding: 18px;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: calc(100vh - 32px);
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-topbar) {
|
.root :global(.scan-topbar) {
|
||||||
@@ -9445,6 +9447,7 @@
|
|||||||
.root :global(.scan-list-section) {
|
.root :global(.scan-list-section) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9521,9 +9524,169 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.scan-table-body) {
|
.root :global(.scan-table-body) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
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) {
|
.root :global(.scan-table-row) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 54px minmax(220px, 1.15fr) minmax(170px, 0.82fr) minmax(250px, 1.15fr) minmax(180px, 0.92fr) 120px 112px;
|
grid-template-columns: 54px minmax(220px, 1.15fr) minmax(170px, 0.82fr) minmax(250px, 1.15fr) minmax(180px, 0.92fr) 120px 112px;
|
||||||
|
|||||||
@@ -46,13 +46,22 @@ function formatWindowMinutes(value: number | null | undefined, locale: string) {
|
|||||||
return `剩余 ${hours}h ${remains}m`;
|
return `剩余 ${hours}h ${remains}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAction(row: ScanOpportunityRow, locale: string) {
|
function formatAction(
|
||||||
if (row.action) return row.action;
|
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") {
|
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") {
|
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 "--";
|
return "--";
|
||||||
}
|
}
|
||||||
@@ -223,6 +232,7 @@ export function OpportunityTable({
|
|||||||
<div className="scan-table-body">
|
<div className="scan-table-body">
|
||||||
{rows.map((row, index) => {
|
{rows.map((row, index) => {
|
||||||
const phaseMeta = getWindowPhaseMeta(row, locale);
|
const phaseMeta = getWindowPhaseMeta(row, locale);
|
||||||
|
const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol);
|
||||||
const localizedCityName = getLocalizedCityName(
|
const localizedCityName = getLocalizedCityName(
|
||||||
row.city,
|
row.city,
|
||||||
row.city_display_name || row.display_name || row.city,
|
row.city_display_name || row.display_name || row.city,
|
||||||
@@ -265,16 +275,18 @@ export function OpportunityTable({
|
|||||||
|
|
||||||
<ProbabilityPreview row={row} locale={locale} />
|
<ProbabilityPreview row={row} locale={locale} />
|
||||||
|
|
||||||
<div className="scan-trade-cell">
|
<div className="scan-trade-cell">
|
||||||
<div className={`scan-trade-main ${row.side === "no" ? "sell" : "buy"}`}>
|
<div className={`scan-trade-main ${row.side === "no" ? "sell" : "buy"}`}>
|
||||||
{formatAction(row, locale)}
|
{formatAction(row, locale, tempSymbol)}
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-trade-sub">
|
<div className="scan-trade-sub">
|
||||||
{formatPercent(row.ask != null ? row.ask * 100 : null)} →{" "}
|
{formatPercent(row.ask != null ? row.ask * 100 : null)} →{" "}
|
||||||
{formatPercent(row.model_probability != null ? row.model_probability * 100 : null)}
|
{formatPercent(row.model_probability != null ? row.model_probability * 100 : null)}
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-trade-note">
|
<div className="scan-trade-note">
|
||||||
{row.target_label || row.market_direction || "--"}
|
{normalizeTemperatureLabel(row.target_label, tempSymbol) ||
|
||||||
|
row.market_direction ||
|
||||||
|
"--"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
FilterState,
|
FilterState,
|
||||||
ScanFilterPanel,
|
ScanFilterPanel,
|
||||||
} from "@/components/dashboard/ScanFilterPanel";
|
} from "@/components/dashboard/ScanFilterPanel";
|
||||||
|
import { MapCanvas } from "@/components/dashboard/MapCanvas";
|
||||||
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
|
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
|
||||||
import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar";
|
import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar";
|
||||||
import { OpportunityTable } from "@/components/dashboard/OpportunityTable";
|
import { OpportunityTable } from "@/components/dashboard/OpportunityTable";
|
||||||
@@ -69,6 +70,17 @@ const NAV_ITEMS = [
|
|||||||
{ zh: "设置", en: "Settings" },
|
{ 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) {
|
function formatPercent(value?: number | null, signed = false) {
|
||||||
if (value == null || Number.isNaN(Number(value))) return "--";
|
if (value == null || Number.isNaN(Number(value))) return "--";
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
@@ -125,6 +137,16 @@ function confidenceLabel(score?: number | null, locale = "zh-CN") {
|
|||||||
return "观察";
|
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(
|
function getSideRow(
|
||||||
marketScan: MarketScan | null | undefined,
|
marketScan: MarketScan | null | undefined,
|
||||||
selectedRow: ScanOpportunityRow,
|
selectedRow: ScanOpportunityRow,
|
||||||
@@ -421,7 +443,8 @@ function DetailPanel({
|
|||||||
<div className="scan-trade-cards">
|
<div className="scan-trade-cards">
|
||||||
<div className="scan-trade-card buy">
|
<div className="scan-trade-card buy">
|
||||||
<div className="scan-trade-card-title">
|
<div className="scan-trade-card-title">
|
||||||
{isEn ? "Buy Yes" : "买入 Yes"} {displayRow.target_label || ""}
|
{isEn ? "Buy Yes" : "买入 Yes"}{" "}
|
||||||
|
{normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || ""}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{formatPrice(getDetailSideAsk(yesRow, marketScan, "yes"))} →{" "}
|
{formatPrice(getDetailSideAsk(yesRow, marketScan, "yes"))} →{" "}
|
||||||
@@ -435,7 +458,8 @@ function DetailPanel({
|
|||||||
</div>
|
</div>
|
||||||
<div className="scan-trade-card sell">
|
<div className="scan-trade-card sell">
|
||||||
<div className="scan-trade-card-title">
|
<div className="scan-trade-card-title">
|
||||||
{isEn ? "Buy No" : "买入 No"} {displayRow.target_label || ""}
|
{isEn ? "Buy No" : "买入 No"}{" "}
|
||||||
|
{normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || ""}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{formatPrice(getDetailSideAsk(noRow, marketScan, "no"))} →{" "}
|
{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<string, ScanOpportunityRow[]>();
|
||||||
|
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 (
|
||||||
|
<div className="scan-empty-state compact">
|
||||||
|
<div className="scan-empty-title">
|
||||||
|
{locale === "en-US" ? "No dated opportunities" : "当前没有日期机会"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scan-calendar-view">
|
||||||
|
{groups.map(([date, items]) => (
|
||||||
|
<section key={date} className="scan-calendar-group">
|
||||||
|
<div className="scan-calendar-group-head">
|
||||||
|
<div className="scan-calendar-date">{formatShortDate(date, locale)}</div>
|
||||||
|
<div className="scan-calendar-count">
|
||||||
|
{locale === "en-US" ? `${items.length} rows` : `${items.length} 条`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="scan-calendar-grid">
|
||||||
|
{items.map((row) => (
|
||||||
|
<button
|
||||||
|
key={row.id}
|
||||||
|
type="button"
|
||||||
|
className={`scan-calendar-card ${selectedRowId === row.id ? "selected" : ""}`}
|
||||||
|
onClick={() => onSelectRow(row)}
|
||||||
|
>
|
||||||
|
<div className="scan-calendar-city">
|
||||||
|
{getLocalizedCityName(
|
||||||
|
row.city,
|
||||||
|
row.city_display_name || row.display_name || row.city,
|
||||||
|
locale,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`scan-calendar-action ${row.side === "no" ? "sell" : "buy"}`}>
|
||||||
|
{row.action || row.target_label || "--"}
|
||||||
|
</div>
|
||||||
|
<div className="scan-calendar-meta">
|
||||||
|
<span>{row.local_time || "--"}</span>
|
||||||
|
<span>{formatPercent(row.edge_percent, true)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverviewMapView({ locale }: { locale: string }) {
|
||||||
|
const store = useDashboardStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scan-map-view">
|
||||||
|
<div className="scan-map-shell">
|
||||||
|
<MapCanvas />
|
||||||
|
</div>
|
||||||
|
<div className="scan-map-caption">
|
||||||
|
{locale === "en-US"
|
||||||
|
? `Monitoring ${store.cities.length} cities on the original map canvas.`
|
||||||
|
: `正在用原地图画布监控 ${store.cities.length} 个城市。`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="scan-summary-grid">
|
||||||
|
<div className="scan-summary-card">
|
||||||
|
<div className="scan-summary-label">{locale === "en-US" ? "YES Ideas" : "YES 机会"}</div>
|
||||||
|
<div className="scan-summary-value">{yesCount}</div>
|
||||||
|
</div>
|
||||||
|
<div className="scan-summary-card">
|
||||||
|
<div className="scan-summary-label">{locale === "en-US" ? "NO Ideas" : "NO 机会"}</div>
|
||||||
|
<div className="scan-summary-value">{noCount}</div>
|
||||||
|
</div>
|
||||||
|
<div className="scan-summary-card wide">
|
||||||
|
<div className="scan-summary-label">{locale === "en-US" ? "Avg Score" : "平均评分"}</div>
|
||||||
|
<div className="scan-summary-value">
|
||||||
|
{avgScore != null ? `${avgScore.toFixed(0)}/100` : "--"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MonitorView({
|
||||||
|
rows,
|
||||||
|
locale,
|
||||||
|
}: {
|
||||||
|
rows: ScanOpportunityRow[];
|
||||||
|
locale: string;
|
||||||
|
}) {
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
const result = new Map<string, number>();
|
||||||
|
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 (
|
||||||
|
<div className="scan-summary-grid monitor">
|
||||||
|
{groups.map(([label, count]) => (
|
||||||
|
<div key={label} className="scan-summary-card">
|
||||||
|
<div className="scan-summary-label">{label}</div>
|
||||||
|
<div className="scan-summary-value">{count}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsView({ locale }: { locale: string }) {
|
||||||
|
return (
|
||||||
|
<div className="scan-settings-view">
|
||||||
|
<div className="scan-settings-card">
|
||||||
|
<div className="scan-summary-label">{locale === "en-US" ? "Market Discovery" : "市场发现"}</div>
|
||||||
|
<div className="scan-settings-copy">
|
||||||
|
{locale === "en-US" ? "Gamma REST refreshes every 60s." : "Gamma REST 每 60 秒刷新一次。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="scan-settings-card">
|
||||||
|
<div className="scan-summary-label">{locale === "en-US" ? "Price Layer" : "价格层"}</div>
|
||||||
|
<div className="scan-settings-copy">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "CLOB REST prices and books refresh every 30s."
|
||||||
|
: "CLOB REST 价格和盘口每 30 秒刷新一次。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="scan-settings-card">
|
||||||
|
<div className="scan-summary-label">{locale === "en-US" ? "Server Profile" : "服务器配置"}</div>
|
||||||
|
<div className="scan-settings-copy">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "2GB profile: one polling loop, cached REST, no websocket fan-out."
|
||||||
|
: "2G 配置:单轮询、REST 缓存优先、不做 websocket 扇出。"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ScanTerminalScreen() {
|
function ScanTerminalScreen() {
|
||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { locale, toggleLocale } = useI18n();
|
const { locale, toggleLocale } = useI18n();
|
||||||
@@ -490,6 +695,8 @@ function ScanTerminalScreen() {
|
|||||||
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
|
||||||
const [detailByRowId, setDetailByRowId] = useState<Record<string, MarketScan | null>>({});
|
const [detailByRowId, setDetailByRowId] = useState<Record<string, MarketScan | null>>({});
|
||||||
const [detailLoadingId, setDetailLoadingId] = useState<string | null>(null);
|
const [detailLoadingId, setDetailLoadingId] = useState<string | null>(null);
|
||||||
|
const [activeSection, setActiveSection] = useState<TopSection>("terminal");
|
||||||
|
const [activeView, setActiveView] = useState<ContentView>("list");
|
||||||
const deferredRows = useDeferredValue(terminalData?.rows || []);
|
const deferredRows = useDeferredValue(terminalData?.rows || []);
|
||||||
|
|
||||||
const selectedRow = useMemo(() => {
|
const selectedRow = useMemo(() => {
|
||||||
@@ -560,6 +767,55 @@ function ScanTerminalScreen() {
|
|||||||
}, [selectedRow, detailByRowId]);
|
}, [selectedRow, detailByRowId]);
|
||||||
|
|
||||||
const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null;
|
const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null;
|
||||||
|
const resolvedView: ContentView =
|
||||||
|
activeSection === "markets"
|
||||||
|
? "map"
|
||||||
|
: activeSection === "analysis"
|
||||||
|
? "calendar"
|
||||||
|
: activeView;
|
||||||
|
|
||||||
|
const renderMainView = () => {
|
||||||
|
if (activeSection === "portfolio") {
|
||||||
|
return <PortfolioView rows={deferredRows} locale={locale} />;
|
||||||
|
}
|
||||||
|
if (activeSection === "monitor") {
|
||||||
|
return <MonitorView rows={deferredRows} locale={locale} />;
|
||||||
|
}
|
||||||
|
if (activeSection === "settings") {
|
||||||
|
return <SettingsView locale={locale} />;
|
||||||
|
}
|
||||||
|
if (resolvedView === "map") {
|
||||||
|
return <OverviewMapView locale={locale} />;
|
||||||
|
}
|
||||||
|
if (resolvedView === "calendar") {
|
||||||
|
return (
|
||||||
|
<CalendarView
|
||||||
|
rows={deferredRows}
|
||||||
|
locale={locale}
|
||||||
|
selectedRowId={selectedRowId}
|
||||||
|
onSelectRow={(row) => setSelectedRowId(row.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OpportunityTable
|
||||||
|
rows={deferredRows}
|
||||||
|
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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.root, detailChromeStyles.root, modalChromeStyles.root)}>
|
<div className={clsx(styles.root, detailChromeStyles.root, modalChromeStyles.root)}>
|
||||||
@@ -581,7 +837,10 @@ function ScanTerminalScreen() {
|
|||||||
<button
|
<button
|
||||||
key={item.zh}
|
key={item.zh}
|
||||||
type="button"
|
type="button"
|
||||||
className={`scan-topbar-tab ${index === 0 ? "active" : ""}`}
|
className={`scan-topbar-tab ${
|
||||||
|
TOP_SECTION_ORDER[index] === activeSection ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveSection(TOP_SECTION_ORDER[index])}
|
||||||
>
|
>
|
||||||
{isEn ? item.en : item.zh}
|
{isEn ? item.en : item.zh}
|
||||||
</button>
|
</button>
|
||||||
@@ -647,15 +906,50 @@ function ScanTerminalScreen() {
|
|||||||
<section className="scan-list-section">
|
<section className="scan-list-section">
|
||||||
<div className="scan-list-header">
|
<div className="scan-list-header">
|
||||||
<div className="scan-list-tabs">
|
<div className="scan-list-tabs">
|
||||||
<button type="button" className="active">
|
<button
|
||||||
|
type="button"
|
||||||
|
className={activeSection === "terminal" && resolvedView === "list" ? "active" : ""}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveSection("terminal");
|
||||||
|
setActiveView("list");
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isEn ? "Opportunity List" : "机会列表"}
|
{isEn ? "Opportunity List" : "机会列表"}
|
||||||
</button>
|
</button>
|
||||||
<button type="button">{isEn ? "Distribution View" : "分布视图"}</button>
|
<button
|
||||||
<button type="button">{isEn ? "Calendar View" : "日历视图"}</button>
|
type="button"
|
||||||
|
className={resolvedView === "map" ? "active" : ""}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveSection("terminal");
|
||||||
|
setActiveView("map");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isEn ? "Distribution View" : "分布视图"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={resolvedView === "calendar" ? "active" : ""}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveSection("terminal");
|
||||||
|
setActiveView("calendar");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isEn ? "Calendar View" : "日历视图"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-list-controls">
|
<div className="scan-list-controls">
|
||||||
<div className="scan-sort-pill">
|
<div className="scan-sort-pill">
|
||||||
{isEn ? "Sort: Score" : "排序:综合得分"}
|
{isEn
|
||||||
|
? resolvedView === "calendar"
|
||||||
|
? "Sort: Date"
|
||||||
|
: resolvedView === "map"
|
||||||
|
? "Sort: City Map"
|
||||||
|
: "Sort: Score"
|
||||||
|
: resolvedView === "calendar"
|
||||||
|
? "排序:日期"
|
||||||
|
: resolvedView === "map"
|
||||||
|
? "排序:地图"
|
||||||
|
: "排序:综合得分"}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="scan-icon-pill" aria-label="menu">
|
<button type="button" className="scan-icon-pill" aria-label="menu">
|
||||||
<Menu size={16} />
|
<Menu size={16} />
|
||||||
@@ -671,22 +965,7 @@ function ScanTerminalScreen() {
|
|||||||
<div className="scan-empty-copy">{error}</div>
|
<div className="scan-empty-copy">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
renderMainView()
|
||||||
<OpportunityTable
|
|
||||||
rows={deferredRows}
|
|
||||||
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}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user