feat: add dashboard panel components for hero summary and temperature chart
This commit is contained in:
@@ -552,9 +552,35 @@ 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(
|
const fallbackMeteoblue = detail.source_forecasts?.meteoblue?.today_high;
|
||||||
([, value]) => value !== null && value !== undefined && Number.isFinite(Number(value)),
|
const modelsMap = { ...view.models };
|
||||||
|
|
||||||
|
// 强行插入 Meteoblue,防止 helper 函数或日期对齐导致其被剔除
|
||||||
|
if (
|
||||||
|
modelsMap["Meteoblue"] == null &&
|
||||||
|
fallbackMeteoblue != null &&
|
||||||
|
(!targetDate || targetDate === detail.local_date)
|
||||||
|
) {
|
||||||
|
modelsMap["Meteoblue"] = fallbackMeteoblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelEntries = Object.entries(modelsMap).filter(
|
||||||
|
([, value]) =>
|
||||||
|
value !== null && value !== undefined && Number.isFinite(Number(value)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 如果没有任何数值,给出提示
|
||||||
|
if (modelEntries.length === 0) {
|
||||||
|
return (
|
||||||
|
<section className="models-section">
|
||||||
|
{!hideTitle && <h3>{t("section.models")}</h3>}
|
||||||
|
<div className="model-bars">
|
||||||
|
<EmptyState text={t("section.noModels")} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const numericValues = modelEntries.map(([, value]) => Number(value));
|
const numericValues = modelEntries.map(([, value]) => Number(value));
|
||||||
const comparisonValues =
|
const comparisonValues =
|
||||||
view.deb != null ? [...numericValues, Number(view.deb)] : numericValues;
|
view.deb != null ? [...numericValues, Number(view.deb)] : numericValues;
|
||||||
@@ -576,72 +602,66 @@ export function ModelForecast({
|
|||||||
<section className="models-section">
|
<section className="models-section">
|
||||||
{!hideTitle && <h3>{t("section.models")}</h3>}
|
{!hideTitle && <h3>{t("section.models")}</h3>}
|
||||||
<div className="model-bars">
|
<div className="model-bars">
|
||||||
{!modelEntries.length ? (
|
{modelEntries
|
||||||
<EmptyState text={t("section.noModels")} />
|
.sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0))
|
||||||
) : (
|
.map(([name, value]) => {
|
||||||
<>
|
const numeric = Number(value);
|
||||||
{modelEntries
|
const width = ((numeric - minValue) / range) * 100;
|
||||||
.sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0))
|
const debLine =
|
||||||
.map(([name, value]) => {
|
view.deb != null
|
||||||
const numeric = Number(value);
|
? ((Number(view.deb) - minValue) / range) * 100
|
||||||
const width = ((numeric - minValue) / range) * 100;
|
: null;
|
||||||
const debLine =
|
|
||||||
view.deb != null
|
|
||||||
? ((Number(view.deb) - minValue) / range) * 100
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={name} className="model-row">
|
<div key={name} className="model-row">
|
||||||
<div className="model-name" title={name}>
|
<div className="model-name" title={name}>
|
||||||
{getModelDisplayName(name)}
|
{getModelDisplayName(name)}
|
||||||
</div>
|
|
||||||
<div className="model-bar-track">
|
|
||||||
<div
|
|
||||||
className="model-bar-fill"
|
|
||||||
style={{ width: `${width}%` }}
|
|
||||||
>
|
|
||||||
{numeric}
|
|
||||||
{detail.temp_symbol}
|
|
||||||
</div>
|
|
||||||
{debLine != null && (
|
|
||||||
<div
|
|
||||||
className="model-deb-line"
|
|
||||||
style={{ left: `${debLine}%` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{view.deb != null && (
|
|
||||||
<div
|
|
||||||
className="model-row"
|
|
||||||
style={{
|
|
||||||
borderTop: "1px solid rgba(255,255,255,0.06)",
|
|
||||||
marginTop: "6px",
|
|
||||||
paddingTop: "6px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="model-name"
|
|
||||||
style={{ color: "var(--accent-cyan)", fontWeight: 700 }}
|
|
||||||
>
|
|
||||||
DEB
|
|
||||||
</div>
|
</div>
|
||||||
<div className="model-bar-track">
|
<div className="model-bar-track">
|
||||||
<div
|
<div
|
||||||
className="model-bar-fill deb"
|
className="model-bar-fill"
|
||||||
style={{
|
style={{ width: `${width}%` }}
|
||||||
width: `${((Number(view.deb) - minValue) / range) * 100}%`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{Number(view.deb)}
|
{numeric}
|
||||||
{detail.temp_symbol}
|
{detail.temp_symbol}
|
||||||
</div>
|
</div>
|
||||||
|
{debLine != null && (
|
||||||
|
<div
|
||||||
|
className="model-deb-line"
|
||||||
|
style={{ left: `${debLine}%` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</>
|
})}
|
||||||
|
{view.deb != null && (
|
||||||
|
<div
|
||||||
|
className="model-row"
|
||||||
|
style={{
|
||||||
|
borderTop: "1px solid rgba(255,255,255,0.06)",
|
||||||
|
marginTop: "6px",
|
||||||
|
paddingTop: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="model-name"
|
||||||
|
style={{ color: "var(--accent-cyan)", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
DEB
|
||||||
|
</div>
|
||||||
|
<div className="model-bar-track">
|
||||||
|
<div
|
||||||
|
className="model-bar-fill deb"
|
||||||
|
style={{
|
||||||
|
width: `${((Number(view.deb) - minValue) / range) * 100}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Number(view.deb)}
|
||||||
|
{detail.temp_symbol}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user