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,
|
ScanOpportunityRow,
|
||||||
} from "@/lib/dashboard-types";
|
} from "@/lib/dashboard-types";
|
||||||
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
|
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
|
||||||
|
import {
|
||||||
|
formatTemperatureValue,
|
||||||
|
normalizeTemperatureLabel,
|
||||||
|
normalizeTemperatureSymbol,
|
||||||
|
} from "@/lib/dashboard-utils";
|
||||||
|
|
||||||
type PhaseMeta = {
|
type PhaseMeta = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -113,6 +118,7 @@ function ProbabilityPreview({
|
|||||||
Boolean(item && (item.label || item.value != null)),
|
Boolean(item && (item.label || item.value != null)),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol);
|
||||||
|
|
||||||
if (!preview.length) {
|
if (!preview.length) {
|
||||||
const targetBase =
|
const targetBase =
|
||||||
@@ -121,11 +127,10 @@ function ProbabilityPreview({
|
|||||||
row.target_lower ??
|
row.target_lower ??
|
||||||
row.target_upper ??
|
row.target_upper ??
|
||||||
null;
|
null;
|
||||||
const unit = row.target_unit || row.temp_symbol || "";
|
|
||||||
const targetLabel =
|
const targetLabel =
|
||||||
targetBase != null
|
targetBase != null
|
||||||
? `${Math.round(Number(targetBase))}${unit}`
|
? formatTemperatureValue(Number(targetBase), tempSymbol)
|
||||||
: row.target_label || "--";
|
: normalizeTemperatureLabel(row.target_label, tempSymbol) || "--";
|
||||||
preview.push({
|
preview.push({
|
||||||
label: targetLabel,
|
label: targetLabel,
|
||||||
model_probability: row.model_event_probability,
|
model_probability: row.model_event_probability,
|
||||||
@@ -141,7 +146,9 @@ function ProbabilityPreview({
|
|||||||
key={`${item.label}-${item.value ?? ""}`}
|
key={`${item.label}-${item.value ?? ""}`}
|
||||||
className={`scan-distribution-card ${item.highlighted ? "featured" : ""}`}
|
className={`scan-distribution-card ${item.highlighted ? "featured" : ""}`}
|
||||||
>
|
>
|
||||||
<strong>{item.label || "--"}</strong>
|
<strong>
|
||||||
|
{normalizeTemperatureLabel(item.label, tempSymbol) || item.label || "--"}
|
||||||
|
</strong>
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "Model" : "模型"}
|
{locale === "en-US" ? "Model" : "模型"}
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ import {
|
|||||||
getLocalizedAirportName,
|
getLocalizedAirportName,
|
||||||
getLocalizedCityName,
|
getLocalizedCityName,
|
||||||
} from "@/lib/dashboard-home-copy";
|
} from "@/lib/dashboard-home-copy";
|
||||||
|
import {
|
||||||
|
formatTemperatureValue,
|
||||||
|
normalizeTemperatureLabel,
|
||||||
|
} from "@/lib/dashboard-utils";
|
||||||
|
|
||||||
const DEFAULT_FILTERS: FilterState = {
|
const DEFAULT_FILTERS: FilterState = {
|
||||||
scan_mode: "tradable",
|
scan_mode: "tradable",
|
||||||
@@ -222,6 +226,7 @@ function DetailPanel({
|
|||||||
detailSignal && detailSignal.market_slug === row.market_slug
|
detailSignal && detailSignal.market_slug === row.market_slug
|
||||||
? detailSignal
|
? detailSignal
|
||||||
: row;
|
: row;
|
||||||
|
const tempSymbol = row.temp_symbol || displayRow.temp_symbol || "°C";
|
||||||
const yesRow = getSideRow(marketScan, row, "yes");
|
const yesRow = getSideRow(marketScan, row, "yes");
|
||||||
const noRow = getSideRow(marketScan, row, "no");
|
const noRow = getSideRow(marketScan, row, "no");
|
||||||
const comparisonBuckets = buildComparisonBuckets(marketScan, row);
|
const comparisonBuckets = buildComparisonBuckets(marketScan, row);
|
||||||
@@ -276,26 +281,33 @@ function DetailPanel({
|
|||||||
<div className="scan-kv">
|
<div className="scan-kv">
|
||||||
<span>{isEn ? "Current Temp" : "当前温度"}</span>
|
<span>{isEn ? "Current Temp" : "当前温度"}</span>
|
||||||
<strong>
|
<strong>
|
||||||
{row.current_temp != null ? `${row.current_temp}${row.temp_symbol || ""}` : "--"}
|
{row.current_temp != null
|
||||||
|
? formatTemperatureValue(row.current_temp, tempSymbol)
|
||||||
|
: "--"}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-kv">
|
<div className="scan-kv">
|
||||||
<span>{isEn ? "Day High (So Far)" : "今日最高(至今)"}</span>
|
<span>{isEn ? "Day High (So Far)" : "今日最高(至今)"}</span>
|
||||||
<strong>
|
<strong>
|
||||||
{row.current_max_so_far != null
|
{row.current_max_so_far != null
|
||||||
? `${row.current_max_so_far}${row.temp_symbol || ""}`
|
? formatTemperatureValue(row.current_max_so_far, tempSymbol)
|
||||||
: "--"}
|
: "--"}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-kv">
|
<div className="scan-kv">
|
||||||
<span>{isEn ? "Target" : "目标温度"}</span>
|
<span>{isEn ? "Target" : "目标温度"}</span>
|
||||||
<strong>{displayRow.target_label || "--"}</strong>
|
<strong>
|
||||||
|
{normalizeTemperatureLabel(displayRow.target_label, tempSymbol) || "--"}
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="scan-kv">
|
<div className="scan-kv">
|
||||||
<span>{isEn ? "Gap To Target" : "距离目标"}</span>
|
<span>{isEn ? "Gap To Target" : "距离目标"}</span>
|
||||||
<strong className={Number(displayRow.gap_to_target || 0) <= 0 ? "warn" : "danger"}>
|
<strong className={Number(displayRow.gap_to_target || 0) <= 0 ? "warn" : "danger"}>
|
||||||
{displayRow.gap_to_target != null
|
{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>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,42 @@ function containsCjk(text: string) {
|
|||||||
return /[\u3400-\u9fff]/.test(text);
|
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(
|
function getLocalizedDynamicCommentary(
|
||||||
detail: CityDetail,
|
detail: CityDetail,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
|
|||||||
Reference in New Issue
Block a user