feat: Implement new dashboard panel sections, utilities, and a future forecast modal.
This commit is contained in:
@@ -715,69 +715,6 @@ export function FutureForecastModal() {
|
|||||||
{settlementBucketLabel}
|
{settlementBucketLabel}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="market-edge-box">
|
|
||||||
<div className="market-edge-header">
|
|
||||||
<span>
|
|
||||||
📊 {locale === "en-US" ? "Core Edge" : "核心测算"}
|
|
||||||
</span>
|
|
||||||
<strong
|
|
||||||
className={clsx(
|
|
||||||
"market-edge-val",
|
|
||||||
Number(marketScan?.edge_percent) > 0
|
|
||||||
? "positive"
|
|
||||||
: "negative",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
({marketEdge})
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<div className="market-edge-compare">
|
|
||||||
<div className="edge-stat">
|
|
||||||
<span className="edge-label">
|
|
||||||
{locale === "en-US" ? "Model Prob:" : "模型概率:"}
|
|
||||||
</span>
|
|
||||||
<strong className="edge-value">
|
|
||||||
{modelProbability}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<div className="edge-stat">
|
|
||||||
<span className="edge-label">
|
|
||||||
{locale === "en-US"
|
|
||||||
? "Market Prob:"
|
|
||||||
: "市场概率:"}
|
|
||||||
</span>
|
|
||||||
<strong className="edge-value">
|
|
||||||
{marketMidpoint}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Layer 2: Order Book */}
|
|
||||||
<div className="market-layer-book">
|
|
||||||
<div className="market-sub-title">
|
|
||||||
📈 {locale === "en-US" ? "Order Book" : "订单簿报价"}
|
|
||||||
</div>
|
|
||||||
<div className="market-book-row">
|
|
||||||
<span className="book-label">YES:</span>
|
|
||||||
<span className="book-quote">
|
|
||||||
<strong>{marketYesBuy}</strong> /{" "}
|
|
||||||
<strong>{marketYesSell}</strong>
|
|
||||||
</span>
|
|
||||||
<span className="book-spread">
|
|
||||||
({locale === "en-US" ? "Spread" : "价差"}{" "}
|
|
||||||
{marketSpread})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="market-book-row">
|
|
||||||
<span className="book-label">NO:</span>
|
|
||||||
<span className="book-quote">
|
|
||||||
<strong>{marketNoBuy}</strong> /{" "}
|
|
||||||
<strong>{marketNoSell}</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Layer 3: Context */}
|
{/* Layer 3: Context */}
|
||||||
|
|||||||
@@ -552,8 +552,8 @@ export function ModelForecast({
|
|||||||
}) {
|
}) {
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const view = getModelView(detail, targetDate);
|
const view = getModelView(detail, targetDate);
|
||||||
const modelEntries = Object.entries(view.models).filter(([, value]) =>
|
const modelEntries = Object.entries(view.models).filter(
|
||||||
Number.isFinite(Number(value)),
|
([, value]) => value !== null && value !== undefined && Number.isFinite(Number(value)),
|
||||||
);
|
);
|
||||||
const numericValues = modelEntries.map(([, value]) => Number(value));
|
const numericValues = modelEntries.map(([, value]) => Number(value));
|
||||||
const comparisonValues =
|
const comparisonValues =
|
||||||
@@ -567,9 +567,7 @@ export function ModelForecast({
|
|||||||
const range = Math.max(maxValue - minValue, 1);
|
const range = Math.max(maxValue - minValue, 1);
|
||||||
const getModelDisplayName = (name: string) => {
|
const getModelDisplayName = (name: string) => {
|
||||||
if (String(name).trim().toLowerCase() === "meteoblue") {
|
if (String(name).trim().toLowerCase() === "meteoblue") {
|
||||||
return locale === "en-US"
|
return "Meteoblue";
|
||||||
? "Meteoblue (Daily Max)"
|
|
||||||
: "Meteoblue (日最高温)";
|
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -330,28 +330,34 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getModelView(detail: CityDetail, targetDate?: string | null) {
|
export function getModelView(detail: CityDetail, targetDate?: string | null) {
|
||||||
|
const toFiniteNumber = (value: unknown): number | null => {
|
||||||
|
if (value === null || value === undefined || value === "") return null;
|
||||||
|
const numeric = Number(value);
|
||||||
|
return Number.isFinite(numeric) ? numeric : null;
|
||||||
|
};
|
||||||
|
|
||||||
const pickMeteoblueForDate = (dateStr: string) => {
|
const pickMeteoblueForDate = (dateStr: string) => {
|
||||||
const meteoblue = detail.source_forecasts?.meteoblue;
|
const meteoblue = detail.source_forecasts?.meteoblue;
|
||||||
if (!meteoblue) return null;
|
if (!meteoblue) return null;
|
||||||
|
|
||||||
const dates = detail.forecast?.daily?.map((item) => item.date) || [];
|
const dates = detail.forecast?.daily?.map((item) => item.date) || [];
|
||||||
const index = dates.findIndex((date) => date === dateStr);
|
const index = dates.findIndex((date) => date === dateStr);
|
||||||
|
const todayHigh = toFiniteNumber(meteoblue.today_high);
|
||||||
|
|
||||||
|
// Today must always use Meteoblue daily max (today_high) first.
|
||||||
|
if (dateStr === detail.local_date && todayHigh != null) {
|
||||||
|
return todayHigh;
|
||||||
|
}
|
||||||
|
|
||||||
const dailyHighs = Array.isArray(meteoblue.daily_highs)
|
const dailyHighs = Array.isArray(meteoblue.daily_highs)
|
||||||
? meteoblue.daily_highs
|
? meteoblue.daily_highs
|
||||||
: [];
|
: [];
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const byDailyHigh = Number(dailyHighs[index]);
|
const byDailyHigh = toFiniteNumber(dailyHighs[index]);
|
||||||
if (Number.isFinite(byDailyHigh)) return byDailyHigh;
|
if (byDailyHigh != null) return byDailyHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayHigh = Number(meteoblue.today_high);
|
if (index === 0 && todayHigh != null) return todayHigh;
|
||||||
if (
|
|
||||||
Number.isFinite(todayHigh) &&
|
|
||||||
(dateStr === detail.local_date || index === 0)
|
|
||||||
) {
|
|
||||||
return todayHigh;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -361,11 +367,14 @@ export function getModelView(detail: CityDetail, targetDate?: string | null) {
|
|||||||
dateStr: string,
|
dateStr: string,
|
||||||
) => {
|
) => {
|
||||||
const nextModels = { ...models };
|
const nextModels = { ...models };
|
||||||
if (!Number.isFinite(Number(nextModels.Meteoblue))) {
|
const existing = toFiniteNumber(nextModels.Meteoblue);
|
||||||
|
if (existing == null) {
|
||||||
const meteoblueVal = pickMeteoblueForDate(dateStr);
|
const meteoblueVal = pickMeteoblueForDate(dateStr);
|
||||||
if (Number.isFinite(Number(meteoblueVal))) {
|
if (meteoblueVal != null) {
|
||||||
nextModels.Meteoblue = Number(meteoblueVal);
|
nextModels.Meteoblue = meteoblueVal;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
nextModels.Meteoblue = existing;
|
||||||
}
|
}
|
||||||
return nextModels;
|
return nextModels;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user