feat: add scan dashboard components and local development configuration
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user