feat: implement ScanTerminalDashboard with KPI bar and opportunity table components

This commit is contained in:
2569718930@qq.com
2026-04-25 06:52:17 +08:00
parent d5fb40a544
commit 4410cebce7
3 changed files with 18 additions and 16 deletions
@@ -319,13 +319,13 @@ function getOpportunityStrength(edgePercent?: number | null, locale = "zh-CN") {
const normalized = Number.isFinite(edge) ? edge : 0; const normalized = Number.isFinite(edge) ? edge : 0;
if (normalized >= 20) { if (normalized >= 20) {
return { return {
label: locale === "en-US" ? "Strong" : "强机会", label: locale === "en-US" ? "High confidence" : "高胜率",
tone: "strong", tone: "strong",
}; };
} }
if (normalized >= 10) { if (normalized >= 10) {
return { return {
label: locale === "en-US" ? "Medium" : "中机会", label: locale === "en-US" ? "Medium confidence" : "中等胜率",
tone: "medium", tone: "medium",
}; };
} }
+6 -8
View File
@@ -5,10 +5,9 @@ import { useI18n } from "@/hooks/useI18n";
import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types"; import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types";
import { getMarketFocus } from "@/lib/scan-market-focus"; import { getMarketFocus } from "@/lib/scan-market-focus";
function formatPercent(value?: number | null, signed = false): string { function formatTemperature(value?: number | null, unit?: string | null): string {
if (value == null || Number.isNaN(Number(value))) return "--"; if (value == null || Number.isNaN(Number(value))) return "--";
const numeric = Number(value); return `${Number(value).toFixed(1)}${unit || "°C"}`;
return `${signed && numeric >= 0 ? "+" : ""}${numeric.toFixed(1)}%`;
} }
function countByRisk(rows: ScanOpportunityRow[]) { function countByRisk(rows: ScanOpportunityRow[]) {
@@ -37,7 +36,6 @@ export function ScanKPIBar({
}) { }) {
const { locale } = useI18n(); const { locale } = useI18n();
const isEn = locale === "en-US"; const isEn = locale === "en-US";
const summary = response?.summary;
const riskCounts = countByRisk(rows); const riskCounts = countByRisk(rows);
const tradableRows = rows.filter((row) => row.tradable && !row.closed); const tradableRows = rows.filter((row) => row.tradable && !row.closed);
const liveRows = rows.filter((row) => row.active || row.accepting_orders); const liveRows = rows.filter((row) => row.active || row.accepting_orders);
@@ -79,14 +77,14 @@ export function ScanKPIBar({
{ {
label: isEn ? "Book Status" : "盘口状态", label: isEn ? "Book Status" : "盘口状态",
value: `${liveRows.length}`, value: `${liveRows.length}`,
note: `${isEn ? "Tradable" : "可交易"} ${tradableRows.length} · ${isEn ? "No edge" : "无机会"} ${Math.max(0, rows.length - tradableRows.length)}`, note: `${isEn ? "Evaluable" : "可评估"} ${tradableRows.length} · ${isEn ? "Pending" : "待评估"} ${Math.max(0, rows.length - tradableRows.length)}`,
tone: "blue", tone: "blue",
}, },
{ {
label: isEn ? "Opportunity Quality" : "机会质量", label: isEn ? "Win-rate Basis" : "胜率依据",
value: formatPercent(summary?.avg_edge_percent, true), value: bestRow ? formatTemperature(bestRow.deb_prediction, bestRow.temp_symbol || bestRow.target_unit) : "--",
note: bestRow note: bestRow
? `${isEn ? "Best" : "最佳"} ${bestRow.city_display_name || bestRow.display_name || bestRow.city} · ${formatPercent(bestRow.edge_percent, true)}` ? `${isEn ? "Focus" : "焦点"} ${bestRow.city_display_name || bestRow.display_name || bestRow.city} · DEB / ${isEn ? "models" : "模型"} / METAR`
: isEn : isEn
? "No active market now" ? "No active market now"
: "当前没有活跃市场", : "当前没有活跃市场",
@@ -405,7 +405,7 @@ function ScanAiAnalysisView({
<div className="scan-ai-analysis-view"> <div className="scan-ai-analysis-view">
<section className="scan-ai-summary-card"> <section className="scan-ai-summary-card">
<div> <div>
<strong>{isEn ? "V4 city analysis" : "V4 城市级分析"}</strong> <strong>{isEn ? "AI city forecast" : "AI 城市级预测"}</strong>
<p> <p>
{aiScan?.status === "ready" {aiScan?.status === "ready"
? (isEn ? aiScan.summary_en || aiScan.summary_zh : aiScan.summary_zh || aiScan.summary_en) || ? (isEn ? aiScan.summary_en || aiScan.summary_zh : aiScan.summary_zh || aiScan.summary_en) ||
@@ -457,7 +457,7 @@ function ScanAiAnalysisView({
(isEn (isEn
? group.thesis?.thesis_en || group.thesis?.summary_en || group.rows[0]?.ai_city_thesis_en ? group.thesis?.thesis_en || group.thesis?.summary_en || group.rows[0]?.ai_city_thesis_en
: group.thesis?.thesis_zh || group.thesis?.summary_zh || group.rows[0]?.ai_city_thesis_zh) || : group.thesis?.thesis_zh || group.thesis?.summary_zh || group.rows[0]?.ai_city_thesis_zh) ||
(isEn ? "Waiting for V4 city thesis." : "等待 V4 城市研判。"); (isEn ? "Waiting for AI city forecast." : "等待 AI 城市预测。");
const clusterNote = const clusterNote =
group.thesis?.model_cluster_note || group.rows[0]?.ai_city_model_cluster_note || null; group.thesis?.model_cluster_note || group.rows[0]?.ai_city_model_cluster_note || null;
return ( return (
@@ -483,7 +483,7 @@ function ScanAiAnalysisView({
<div key={row.id} className={`scan-ai-contract ${decision}`}> <div key={row.id} className={`scan-ai-contract ${decision}`}>
<div> <div>
<b> <b>
{normalizeTemperatureLabel(row.action || row.target_label, tempSymbol) || {normalizeTemperatureLabel(row.target_label, tempSymbol) ||
row.market_question || row.market_question ||
row.id} row.id}
</b> </b>
@@ -491,7 +491,11 @@ function ScanAiAnalysisView({
{row.deb_prediction != null {row.deb_prediction != null
? `DEB ${formatTemperatureValue(row.deb_prediction, tempSymbol)} · ` ? `DEB ${formatTemperatureValue(row.deb_prediction, tempSymbol)} · `
: ""} : ""}
{row.edge_percent != null ? `edge ${Number(row.edge_percent).toFixed(1)}%` : "--"} {row.ai_forecast_confidence
? `${isEn ? "confidence" : "信心"} ${row.ai_forecast_confidence}`
: isEn
? "forecast mapping"
: "预测映射"}
</small> </small>
</div> </div>
<span>{getAiDecisionLabel(row, locale)}</span> <span>{getAiDecisionLabel(row, locale)}</span>
@@ -1202,10 +1206,10 @@ function ScanTerminalScreen() {
: "当前显示上次成功快照" : "当前显示上次成功快照"
: isEn : isEn
? isPro ? isPro
? "Read-only market scan with peak-first main signal" ? "Read-only market scan with DEB-first win-rate signal"
: "Free preview: distribution view and city briefing" : "Free preview: distribution view and city briefing"
: isPro : isPro
? "只读市场扫描,主信号按 EMOS 主峰优先" ? "只读市场扫描,主信号按 DEB 最终融合预测优先"
: "免费预览:分布视图和城市简报可查看"} : "免费预览:分布视图和城市简报可查看"}
</span> </span>
</div> </div>