feat: add scan dashboard components and local development configuration

This commit is contained in:
2569718930@qq.com
2026-04-24 02:14:20 +08:00
parent 79b58a8b98
commit a1faacb302
5 changed files with 82 additions and 8 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "frontend-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--", "--hostname", "127.0.0.1", "--port", "3001"],
"port": 3001
}
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(npm --prefix \"/e/web/PolyWeather/frontend\" run build)",
"mcp__Claude_Preview__preview_start"
]
}
}
@@ -8,6 +8,11 @@ import type {
ScanOpportunityRow,
} from "@/lib/dashboard-types";
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
import {
formatTemperatureValue,
normalizeTemperatureLabel,
normalizeTemperatureSymbol,
} from "@/lib/dashboard-utils";
type PhaseMeta = {
label: string;
@@ -113,6 +118,7 @@ function ProbabilityPreview({
Boolean(item && (item.label || item.value != null)),
)
: [];
const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol);
if (!preview.length) {
const targetBase =
@@ -121,11 +127,10 @@ function ProbabilityPreview({
row.target_lower ??
row.target_upper ??
null;
const unit = row.target_unit || row.temp_symbol || "";
const targetLabel =
targetBase != null
? `${Math.round(Number(targetBase))}${unit}`
: row.target_label || "--";
? formatTemperatureValue(Number(targetBase), tempSymbol)
: normalizeTemperatureLabel(row.target_label, tempSymbol) || "--";
preview.push({
label: targetLabel,
model_probability: row.model_event_probability,
@@ -141,7 +146,9 @@ function ProbabilityPreview({
key={`${item.label}-${item.value ?? ""}`}
className={`scan-distribution-card ${item.highlighted ? "featured" : ""}`}
>
<strong>{item.label || "--"}</strong>
<strong>
{normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"}
</strong>
<span>
{locale === "en-US" ? "Model" : "模型"}
<br />
@@ -43,6 +43,10 @@ import {
getLocalizedAirportName,
getLocalizedCityName,
} from "@/lib/dashboard-home-copy";
import {
formatTemperatureValue,
normalizeTemperatureLabel,
} from "@/lib/dashboard-utils";
const DEFAULT_FILTERS: FilterState = {
scan_mode: "tradable",
@@ -222,6 +226,7 @@ function DetailPanel({
detailSignal && detailSignal.market_slug === row.market_slug
? detailSignal
: row;
const tempSymbol = row.temp_symbol || displayRow.temp_symbol || "°C";
const yesRow = getSideRow(marketScan, row, "yes");
const noRow = getSideRow(marketScan, row, "no");
const comparisonBuckets = buildComparisonBuckets(marketScan, row);
@@ -276,26 +281,33 @@ function DetailPanel({
<div className="scan-kv">
<span>{isEn ? "Current Temp" : "当前温度"}</span>
<strong>
{row.current_temp != null ? `${row.current_temp}${row.temp_symbol || ""}` : "--"}
{row.current_temp != null
? formatTemperatureValue(row.current_temp, tempSymbol)
: "--"}
</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Day High (So Far)" : "今日最高(至今)"}</span>
<strong>
{row.current_max_so_far != null
? `${row.current_max_so_far}${row.temp_symbol || ""}`
? formatTemperatureValue(row.current_max_so_far, tempSymbol)
: "--"}
</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Target" : "目标温度"}</span>
<strong>{displayRow.target_label || "--"}</strong>
<strong>
{normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || "--"}
</strong>
</div>
<div className="scan-kv">
<span>{isEn ? "Gap To Target" : "距离目标"}</span>
<strong className={Number(displayRow.gap_to_target || 0) <= 0 ? "warn" : "danger"}>
{displayRow.gap_to_target != null
? `${displayRow.gap_to_target >= 0 ? "+" : ""}${displayRow.gap_to_target.toFixed(1)}${row.temp_symbol || ""}`
? formatTemperatureValue(displayRow.gap_to_target, tempSymbol, {
signed: true,
digits: 1,
})
: "--"}
</strong>
</div>
+36
View File
@@ -45,6 +45,42 @@ function containsCjk(text: string) {
return /[\u3400-\u9fff]/.test(text);
}
export function normalizeTemperatureSymbol(value?: string | null) {
return String(value || "").toUpperCase().includes("F") ? "°F" : "°C";
}
export function formatTemperatureValue(
value: number | null | undefined,
symbol?: string | null,
options?: { signed?: boolean; digits?: number },
) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return "--";
const digits = options?.digits ?? 0;
const sign = options?.signed && numeric >= 0 ? "+" : "";
return `${sign}${numeric.toFixed(digits)}${normalizeTemperatureSymbol(symbol)}`;
}
export function normalizeTemperatureLabel(
label?: string | null,
fallbackSymbol?: string | null,
) {
const raw = String(label || "").trim();
if (!raw) return "";
const normalizedSymbol = normalizeTemperatureSymbol(fallbackSymbol);
let next = raw.replace(/℃/gi, "°C");
next = next.replace(/°?([CF])\b/gi, (_, unit: string) => `°${unit.toUpperCase()}`);
if (!/[°][CF]/.test(next) && /\d/.test(next)) {
next = `${next}${normalizedSymbol}`;
}
if (normalizedSymbol === "°F") {
next = next.replace(/°C/g, "°F");
} else {
next = next.replace(/°F/g, "°C");
}
return next;
}
function getLocalizedDynamicCommentary(
detail: CityDetail,
locale: Locale,