feat: implement scan terminal dashboard with real-time opportunity tracking and visualization components
This commit is contained in:
@@ -42,6 +42,8 @@ frontend/.vercel/
|
|||||||
frontend/*.tsbuildinfo
|
frontend/*.tsbuildinfo
|
||||||
frontend/.codex-next-dev*.log
|
frontend/.codex-next-dev*.log
|
||||||
frontend/.codex-next-start*.log
|
frontend/.codex-next-start*.log
|
||||||
|
frontend/.next-dev.log
|
||||||
|
frontend/.next-start.log
|
||||||
.codex-backend-*.log
|
.codex-backend-*.log
|
||||||
|
|
||||||
.npm-cache/
|
.npm-cache/
|
||||||
|
|||||||
+24
-24
@@ -9,28 +9,28 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
/* ── Background Scale ── */
|
/* ── Background Scale ── */
|
||||||
--color-bg-base: #080c14;
|
--color-bg-base: #0B1220;
|
||||||
--color-bg-raised: #0d1321;
|
--color-bg-raised: #111A2E;
|
||||||
--color-bg-overlay: #111827;
|
--color-bg-overlay: #16213A;
|
||||||
--color-bg-card: rgba(15, 23, 42, 0.72);
|
--color-bg-card: rgba(17, 26, 46, 0.88);
|
||||||
--color-bg-input: rgba(255, 255, 255, 0.04);
|
--color-bg-input: rgba(22, 33, 58, 0.72);
|
||||||
|
|
||||||
/* ── Text Scale ── */
|
/* ── Text Scale ── */
|
||||||
--color-text-primary: #f1f5f9;
|
--color-text-primary: #E6EDF3;
|
||||||
--color-text-secondary: #94a3b8;
|
--color-text-secondary: #9FB2C7;
|
||||||
--color-text-muted: #64748b;
|
--color-text-muted: #6B7A90;
|
||||||
--color-text-disabled: #475569;
|
--color-text-disabled: #6B7A90;
|
||||||
|
|
||||||
/* ── Accent Colors (Fintech 3-Color) ── */
|
/* ── Accent Colors (Fintech 3-Color) ── */
|
||||||
--color-accent-primary: #00e0a4;
|
--color-accent-primary: #4DA3FF;
|
||||||
--color-accent-secondary: #7b61ff;
|
--color-accent-secondary: #4DA3FF;
|
||||||
--color-accent-tertiary: #a78bfa;
|
--color-accent-tertiary: #93C5FD;
|
||||||
|
|
||||||
/* ── Signal / Semantic Colors ── */
|
/* ── Signal / Semantic Colors ── */
|
||||||
--color-signal-success: #00e0a4;
|
--color-signal-success: #22C55E;
|
||||||
--color-signal-warning: #ffb020;
|
--color-signal-warning: #F59E0B;
|
||||||
--color-signal-danger: #ff4d6a;
|
--color-signal-danger: #EF4444;
|
||||||
--color-signal-info: #7b61ff;
|
--color-signal-info: #4DA3FF;
|
||||||
|
|
||||||
/* ── Risk Colors (aliased from signal) ── */
|
/* ── Risk Colors (aliased from signal) ── */
|
||||||
--color-risk-high: var(--color-signal-danger);
|
--color-risk-high: var(--color-signal-danger);
|
||||||
@@ -38,16 +38,16 @@
|
|||||||
--color-risk-low: var(--color-signal-success);
|
--color-risk-low: var(--color-signal-success);
|
||||||
|
|
||||||
/* ── Border ── */
|
/* ── Border ── */
|
||||||
--color-border-default: rgba(123, 97, 255, 0.12);
|
--color-border-default: rgba(159, 178, 199, 0.16);
|
||||||
--color-border-hover: rgba(123, 97, 255, 0.28);
|
--color-border-hover: rgba(77, 163, 255, 0.38);
|
||||||
--color-border-subtle: rgba(255, 255, 255, 0.06);
|
--color-border-subtle: rgba(159, 178, 199, 0.08);
|
||||||
|
|
||||||
/* ── Shadow / Elevation ── */
|
/* ── Shadow / Elevation ── */
|
||||||
--shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.3);
|
--shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
--shadow-elevation-2: 0 8px 24px rgba(0, 0, 0, 0.45);
|
--shadow-elevation-2: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
--shadow-elevation-3: 0 20px 60px rgba(0, 0, 0, 0.6);
|
--shadow-elevation-3: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
--shadow-glow-accent: 0 0 20px rgba(0, 224, 164, 0.25);
|
--shadow-glow-accent: 0 0 20px rgba(77, 163, 255, 0.24);
|
||||||
--shadow-glow-secondary: 0 0 20px rgba(123, 97, 255, 0.25);
|
--shadow-glow-secondary: 0 0 20px rgba(111, 183, 255, 0.22);
|
||||||
|
|
||||||
/* ── Typography ── */
|
/* ── Typography ── */
|
||||||
--font-data:
|
--font-data:
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-pill.low {
|
.map-pill.low {
|
||||||
background: linear-gradient(135deg, #10b981, #047857);
|
background: linear-gradient(135deg, #22C55E, #047857);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-pill.active {
|
.map-pill.active {
|
||||||
@@ -329,12 +329,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.marker-bubble.risk-low {
|
.marker-bubble.risk-low {
|
||||||
background: linear-gradient(135deg, #059669, #10b981);
|
background: linear-gradient(135deg, #059669, #22C55E);
|
||||||
border-color: rgba(16, 185, 129, 0.5);
|
border-color: rgba(16, 185, 129, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-bubble.risk-low::after {
|
.marker-bubble.risk-low::after {
|
||||||
border-top-color: #10b981;
|
border-top-color: #22C55E;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-name {
|
.marker-name {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,16 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
|||||||
() => getTemperatureChartData(detail, locale),
|
() => getTemperatureChartData(detail, locale),
|
||||||
[detail, locale],
|
[detail, locale],
|
||||||
);
|
);
|
||||||
|
const forecastLabel = chartData?.datasets.hasMgmHourly
|
||||||
|
? locale === "en-US"
|
||||||
|
? "MGM Forecast"
|
||||||
|
: "MGM 预测"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "DEB Forecast"
|
||||||
|
: "DEB 预测";
|
||||||
|
const observationLabel =
|
||||||
|
chartData?.observationLabel ||
|
||||||
|
(locale === "en-US" ? "METAR Observation" : "METAR 实况");
|
||||||
|
|
||||||
const canvasRef = useChart(() => {
|
const canvasRef = useChart(() => {
|
||||||
if (!chartData) {
|
if (!chartData) {
|
||||||
@@ -50,28 +60,20 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
|||||||
borderWidth: 1.8,
|
borderWidth: 1.8,
|
||||||
data: forecastPoints,
|
data: forecastPoints,
|
||||||
fill: false,
|
fill: false,
|
||||||
label: chartData.datasets.hasMgmHourly
|
label: forecastLabel,
|
||||||
? locale === "en-US"
|
|
||||||
? "MGM Forecast"
|
|
||||||
: "MGM 预测"
|
|
||||||
: locale === "en-US"
|
|
||||||
? "DEB Forecast"
|
|
||||||
: "DEB 预测",
|
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
tension: 0.28,
|
tension: 0.28,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
backgroundColor: "#00E0A4",
|
backgroundColor: "#4DA3FF",
|
||||||
borderColor: "#00E0A4",
|
borderColor: "#4DA3FF",
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
data: chartData.datasets.metarPoints,
|
data: chartData.datasets.metarPoints,
|
||||||
fill: false,
|
fill: false,
|
||||||
label:
|
label: observationLabel,
|
||||||
chartData.observationLabel ||
|
pointHoverRadius: 5,
|
||||||
(locale === "en-US" ? "METAR Observation" : "METAR 实况"),
|
pointRadius: 3.2,
|
||||||
pointHoverRadius: 6,
|
|
||||||
pointRadius: 3.8,
|
|
||||||
showLine: false,
|
showLine: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -79,12 +81,13 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
|||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
interaction: { intersect: false, mode: "index" },
|
interaction: { intersect: false, mode: "index" },
|
||||||
|
layout: { padding: { bottom: 0, left: 0, right: 6, top: 4 } },
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: "rgba(15, 23, 42, 0.95)",
|
backgroundColor: "rgba(15, 23, 42, 0.95)",
|
||||||
borderColor: "rgba(0, 224, 164, 0.25)",
|
borderColor: "rgba(77, 163, 255, 0.28)",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -97,8 +100,9 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
|||||||
typeof index === "number" && index % 4 === 0
|
typeof index === "number" && index % 4 === 0
|
||||||
? chartData.times[index]
|
? chartData.times[index]
|
||||||
: "",
|
: "",
|
||||||
color: "#64748b",
|
color: "#6B7A90",
|
||||||
font: { size: 10 },
|
font: { size: 10 },
|
||||||
|
maxTicksLimit: 6,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -107,22 +111,37 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
|||||||
max: chartData.max,
|
max: chartData.max,
|
||||||
min: chartData.min,
|
min: chartData.min,
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
|
callback: (value) =>
|
||||||
color: "#64748b",
|
`${Number(value).toFixed(chartData.yTickStep < 1 ? 1 : 0)}${detail.temp_symbol || "°C"}`,
|
||||||
|
color: "#6B7A90",
|
||||||
font: { size: 10 },
|
font: { size: 10 },
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
stepSize: chartData.yTickStep,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: "line",
|
type: "line",
|
||||||
} satisfies ChartConfiguration<"line">;
|
} satisfies ChartConfiguration<"line">;
|
||||||
}, [chartData, detail.temp_symbol, locale]);
|
}, [chartData, detail.temp_symbol, forecastLabel, observationLabel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="detail-mini-chart-wrap">
|
<div className="detail-mini-chart-wrap">
|
||||||
<div className="detail-mini-chart">
|
<div className="detail-mini-chart">
|
||||||
<canvas ref={canvasRef} />
|
<canvas ref={canvasRef} />
|
||||||
</div>
|
</div>
|
||||||
|
{chartData ? (
|
||||||
|
<div className="detail-mini-chart-legend">
|
||||||
|
<span>
|
||||||
|
<i className="forecast" />
|
||||||
|
{forecastLabel}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<i className="observation" />
|
||||||
|
{observationLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.panel-action-button-primary) {
|
.root :global(.panel-action-button-primary) {
|
||||||
color: #05111f;
|
color: #FFFFFF;
|
||||||
background: linear-gradient(135deg, #67e8f9, #38bdf8);
|
background: linear-gradient(135deg, #4DA3FF, #3B82F6);
|
||||||
border-color: rgba(103, 232, 249, 0.3);
|
border-color: rgba(77, 163, 255, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root :global(.panel-action-button-secondary) {
|
.root :global(.panel-action-button-secondary) {
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ function formatMinuteAxisLabel(value: number) {
|
|||||||
function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) {
|
function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) {
|
||||||
if (emoji === "☀️") return <Sun size={size} color="#facc15" />;
|
if (emoji === "☀️") return <Sun size={size} color="#facc15" />;
|
||||||
if (emoji === "⛅" || emoji === "🌤️")
|
if (emoji === "⛅" || emoji === "🌤️")
|
||||||
return <CloudSun size={size} color="#38bdf8" />;
|
return <CloudSun size={size} color="#4DA3FF" />;
|
||||||
if (emoji === "☁️") return <Cloud size={size} color="#94a3b8" />;
|
if (emoji === "☁️") return <Cloud size={size} color="#9FB2C7" />;
|
||||||
if (emoji === "🌧️" || emoji === "🌦️")
|
if (emoji === "🌧️" || emoji === "🌦️")
|
||||||
return <CloudRain size={size} color="#60a5fa" />;
|
return <CloudRain size={size} color="#60a5fa" />;
|
||||||
if (emoji === "⛈️") return <CloudLightning size={size} color="#c084fc" />;
|
if (emoji === "⛈️") return <CloudLightning size={size} color="#c084fc" />;
|
||||||
@@ -71,7 +71,7 @@ function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) {
|
|||||||
return <CloudSnow size={size} color="#7dd3fc" />;
|
return <CloudSnow size={size} color="#7dd3fc" />;
|
||||||
if (emoji === "🌫️") return <CloudFog size={size} color="#a1a1aa" />;
|
if (emoji === "🌫️") return <CloudFog size={size} color="#a1a1aa" />;
|
||||||
if (emoji === "💨") return <Wind size={size} color="#cbd5e1" />;
|
if (emoji === "💨") return <Wind size={size} color="#cbd5e1" />;
|
||||||
return <Search size={size} color="#64748b" />;
|
return <Search size={size} color="#6B7A90" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMarketPercent(value?: number | null) {
|
function formatMarketPercent(value?: number | null) {
|
||||||
@@ -354,8 +354,8 @@ function DailyTemperatureChart({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
backgroundColor: "rgba(52, 211, 153, 0.05)",
|
backgroundColor: "rgba(77, 163, 255, 0.06)",
|
||||||
borderColor: "rgba(52, 211, 153, 0.6)",
|
borderColor: "rgba(77, 163, 255, 0.66)",
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
data: todayChartData.datasets.debPastSeries,
|
data: todayChartData.datasets.debPastSeries,
|
||||||
fill: true,
|
fill: true,
|
||||||
@@ -366,7 +366,7 @@ function DailyTemperatureChart({
|
|||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
});
|
});
|
||||||
datasets.push({
|
datasets.push({
|
||||||
borderColor: "rgba(52, 211, 153, 0.35)",
|
borderColor: "rgba(77, 163, 255, 0.36)",
|
||||||
borderDash: [5, 3],
|
borderDash: [5, 3],
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
data: todayChartData.datasets.debFutureSeries,
|
data: todayChartData.datasets.debFutureSeries,
|
||||||
@@ -379,8 +379,8 @@ function DailyTemperatureChart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
datasets.push({
|
datasets.push({
|
||||||
backgroundColor: "#00E0A4",
|
backgroundColor: "#4DA3FF",
|
||||||
borderColor: "#00E0A4",
|
borderColor: "#4DA3FF",
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
data: todayChartData.datasets.metarSeries,
|
data: todayChartData.datasets.metarSeries,
|
||||||
fill: false,
|
fill: false,
|
||||||
@@ -431,7 +431,7 @@ function DailyTemperatureChart({
|
|||||||
Math.abs(todayChartData.datasets.offset) > 0.3
|
Math.abs(todayChartData.datasets.offset) > 0.3
|
||||||
) {
|
) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
borderColor: "rgba(123, 97, 255, 0.2)",
|
borderColor: "rgba(77, 163, 255, 0.22)",
|
||||||
borderDash: [2, 4],
|
borderDash: [2, 4],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
data: todayChartData.datasets.tempsSeries,
|
data: todayChartData.datasets.tempsSeries,
|
||||||
@@ -495,7 +495,7 @@ function DailyTemperatureChart({
|
|||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
labels: {
|
labels: {
|
||||||
color: "#94a3b8",
|
color: "#9FB2C7",
|
||||||
filter: (legendItem, chartData) => {
|
filter: (legendItem, chartData) => {
|
||||||
const text = String(legendItem.text || "");
|
const text = String(legendItem.text || "");
|
||||||
if (!text) return false;
|
if (!text) return false;
|
||||||
@@ -513,7 +513,7 @@ function DailyTemperatureChart({
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
||||||
borderColor: "rgba(0, 224, 164, 0.2)",
|
borderColor: "rgba(77, 163, 255, 0.24)",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: (items) => {
|
title: (items) => {
|
||||||
@@ -598,7 +598,7 @@ function DailyTemperatureChart({
|
|||||||
}
|
}
|
||||||
return formatMinuteAxisLabel(minutes);
|
return formatMinuteAxisLabel(minutes);
|
||||||
},
|
},
|
||||||
color: "#64748b",
|
color: "#6B7A90",
|
||||||
font: { family: "Inter", size: 10 },
|
font: { family: "Inter", size: 10 },
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
@@ -608,9 +608,11 @@ function DailyTemperatureChart({
|
|||||||
max: todayChartData.max,
|
max: todayChartData.max,
|
||||||
min: todayChartData.min,
|
min: todayChartData.min,
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
|
callback: (value) =>
|
||||||
color: "#64748b",
|
`${Number(value).toFixed(todayChartData.yTickStep < 1 ? 1 : 0)}${detail.temp_symbol || "°C"}`,
|
||||||
|
color: "#6B7A90",
|
||||||
font: { family: "Inter", size: 10 },
|
font: { family: "Inter", size: 10 },
|
||||||
|
stepSize: todayChartData.yTickStep,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -626,8 +628,8 @@ function DailyTemperatureChart({
|
|||||||
data: {
|
data: {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: "rgba(0, 224, 164, 0.08)",
|
backgroundColor: "rgba(77, 163, 255, 0.08)",
|
||||||
borderColor: "#00E0A4",
|
borderColor: "#4DA3FF",
|
||||||
data: view.slice.map((point) => point.temp),
|
data: view.slice.map((point) => point.temp),
|
||||||
fill: false,
|
fill: false,
|
||||||
label:
|
label:
|
||||||
@@ -637,7 +639,7 @@ function DailyTemperatureChart({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
borderColor: "#a78bfa",
|
borderColor: "#93C5FD",
|
||||||
borderDash: [5, 4],
|
borderDash: [5, 4],
|
||||||
data: view.slice.map((point) => point.dewPoint),
|
data: view.slice.map((point) => point.dewPoint),
|
||||||
fill: false,
|
fill: false,
|
||||||
@@ -654,13 +656,13 @@ function DailyTemperatureChart({
|
|||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
labels: {
|
labels: {
|
||||||
color: "#94a3b8",
|
color: "#9FB2C7",
|
||||||
font: { family: "Inter", size: 11 },
|
font: { family: "Inter", size: 11 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
||||||
borderColor: "rgba(0, 224, 164, 0.2)",
|
borderColor: "rgba(77, 163, 255, 0.24)",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (ctx) =>
|
label: (ctx) =>
|
||||||
@@ -673,7 +675,7 @@ function DailyTemperatureChart({
|
|||||||
x: {
|
x: {
|
||||||
grid: { color: "rgba(255,255,255,0.04)" },
|
grid: { color: "rgba(255,255,255,0.04)" },
|
||||||
ticks: {
|
ticks: {
|
||||||
color: "#64748b",
|
color: "#6B7A90",
|
||||||
font: { family: "Inter", size: 10 },
|
font: { family: "Inter", size: 10 },
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
@@ -682,7 +684,7 @@ function DailyTemperatureChart({
|
|||||||
grid: { color: "rgba(255,255,255,0.04)" },
|
grid: { color: "rgba(255,255,255,0.04)" },
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => `${value}${unit}`,
|
callback: (value) => `${value}${unit}`,
|
||||||
color: "#64748b",
|
color: "#6B7A90",
|
||||||
font: { family: "Inter", size: 10 },
|
font: { family: "Inter", size: 10 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ function clamp(value: number, min: number, max: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getToneColor(tone: string) {
|
function getToneColor(tone: string) {
|
||||||
if (tone === "cyan") return "#00E0A4";
|
if (tone === "cyan") return "#4DA3FF";
|
||||||
if (tone === "blue") return "#60a5fa";
|
if (tone === "blue") return "#60a5fa";
|
||||||
if (tone === "amber") return "#f59e0b";
|
if (tone === "amber") return "#f59e0b";
|
||||||
return "#94a3b8";
|
return "#9FB2C7";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IntradaySignalScene({
|
export function IntradaySignalScene({
|
||||||
@@ -77,7 +77,7 @@ export function IntradaySignalScene({
|
|||||||
const ring = new THREE.Mesh(
|
const ring = new THREE.Mesh(
|
||||||
new THREE.TorusGeometry(2.8, 0.03, 18, 100),
|
new THREE.TorusGeometry(2.8, 0.03, 18, 100),
|
||||||
new THREE.MeshBasicMaterial({
|
new THREE.MeshBasicMaterial({
|
||||||
color: new THREE.Color(score >= 0 ? "#00E0A4" : "#FFB020"),
|
color: new THREE.Color(score >= 0 ? "#4DA3FF" : "#F59E0B"),
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
}),
|
}),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -694,8 +694,8 @@ export function TemperatureChart() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
backgroundColor: "rgba(52, 211, 153, 0.05)",
|
backgroundColor: "rgba(77, 163, 255, 0.06)",
|
||||||
borderColor: "rgba(52, 211, 153, 0.6)",
|
borderColor: "rgba(77, 163, 255, 0.66)",
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
data: chartData.datasets.debPast,
|
data: chartData.datasets.debPast,
|
||||||
fill: true,
|
fill: true,
|
||||||
@@ -705,7 +705,7 @@ export function TemperatureChart() {
|
|||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
});
|
});
|
||||||
datasets.push({
|
datasets.push({
|
||||||
borderColor: "rgba(52, 211, 153, 0.35)",
|
borderColor: "rgba(77, 163, 255, 0.36)",
|
||||||
borderDash: [5, 3],
|
borderDash: [5, 3],
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
data: chartData.datasets.debFuture,
|
data: chartData.datasets.debFuture,
|
||||||
@@ -717,8 +717,8 @@ export function TemperatureChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
datasets.push({
|
datasets.push({
|
||||||
backgroundColor: "#00E0A4",
|
backgroundColor: "#4DA3FF",
|
||||||
borderColor: "#00E0A4",
|
borderColor: "#4DA3FF",
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
data: chartData.datasets.metarPoints,
|
data: chartData.datasets.metarPoints,
|
||||||
fill: false,
|
fill: false,
|
||||||
@@ -750,7 +750,7 @@ export function TemperatureChart() {
|
|||||||
Math.abs(chartData.datasets.offset) > 0.3
|
Math.abs(chartData.datasets.offset) > 0.3
|
||||||
) {
|
) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
borderColor: "rgba(123, 97, 255, 0.2)",
|
borderColor: "rgba(77, 163, 255, 0.22)",
|
||||||
borderDash: [2, 4],
|
borderDash: [2, 4],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
data: chartData.datasets.temps,
|
data: chartData.datasets.temps,
|
||||||
@@ -773,7 +773,7 @@ export function TemperatureChart() {
|
|||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: "rgba(15, 23, 42, 0.9)",
|
backgroundColor: "rgba(15, 23, 42, 0.9)",
|
||||||
borderColor: "rgba(52, 211, 153, 0.3)",
|
borderColor: "rgba(77, 163, 255, 0.28)",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -786,7 +786,7 @@ export function TemperatureChart() {
|
|||||||
typeof index === "number" && index % 3 === 0
|
typeof index === "number" && index % 3 === 0
|
||||||
? chartData.times[index]
|
? chartData.times[index]
|
||||||
: "",
|
: "",
|
||||||
color: "#64748b",
|
color: "#6B7A90",
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -795,8 +795,10 @@ export function TemperatureChart() {
|
|||||||
max: chartData.max,
|
max: chartData.max,
|
||||||
min: chartData.min,
|
min: chartData.min,
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => `${value}${data.temp_symbol || "°C"}`,
|
callback: (value) =>
|
||||||
color: "#64748b",
|
`${Number(value).toFixed(chartData.yTickStep < 1 ? 1 : 0)}${data.temp_symbol || "°C"}`,
|
||||||
|
color: "#6B7A90",
|
||||||
|
stepSize: chartData.yTickStep,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function ScanKPIBar({
|
|||||||
tone: "blue",
|
tone: "blue",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isEn ? "Win-rate Basis" : "胜率依据",
|
label: isEn ? "Forecast Center" : "预测中枢",
|
||||||
value: bestRow ? formatTemperature(bestRow.deb_prediction, bestRow.temp_symbol || bestRow.target_unit) : "--",
|
value: bestRow ? formatTemperature(bestRow.deb_prediction, bestRow.temp_symbol || bestRow.target_unit) : "--",
|
||||||
note: bestRow
|
note: bestRow
|
||||||
? `${isEn ? "Focus" : "焦点"} ${bestRow.city_display_name || bestRow.display_name || bestRow.city} · DEB / ${isEn ? "models" : "模型"} / METAR`
|
? `${isEn ? "Focus" : "焦点"} ${bestRow.city_display_name || bestRow.display_name || bestRow.city} · DEB / ${isEn ? "models" : "模型"} / METAR`
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -475,7 +475,7 @@ export function WeatherAuraLayer() {
|
|||||||
`radial-gradient(circle at 78% 20%, ${hexToRgba(aura.secondary, 0.14 * aura.intensity)}, transparent 34%)`,
|
`radial-gradient(circle at 78% 20%, ${hexToRgba(aura.secondary, 0.14 * aura.intensity)}, transparent 34%)`,
|
||||||
`radial-gradient(circle at 52% 78%, ${hexToRgba(aura.tertiary, 0.12 * aura.intensity)}, transparent 38%)`,
|
`radial-gradient(circle at 52% 78%, ${hexToRgba(aura.tertiary, 0.12 * aura.intensity)}, transparent 38%)`,
|
||||||
aura.effect === "rain" || aura.effect === "storm"
|
aura.effect === "rain" || aura.effect === "storm"
|
||||||
? `linear-gradient(180deg, ${hexToRgba("#67e8f9", 0.06 * aura.effectIntensity)}, transparent 45%)`
|
? `linear-gradient(180deg, ${hexToRgba("#6FB7FF", 0.06 * aura.effectIntensity)}, transparent 45%)`
|
||||||
: aura.effect === "snow"
|
: aura.effect === "snow"
|
||||||
? `linear-gradient(180deg, ${hexToRgba("#e2e8f0", 0.06 * aura.effectIntensity)}, transparent 45%)`
|
? `linear-gradient(180deg, ${hexToRgba("#e2e8f0", 0.06 * aura.effectIntensity)}, transparent 45%)`
|
||||||
: aura.effect === "fog"
|
: aura.effect === "fog"
|
||||||
|
|||||||
@@ -939,24 +939,15 @@ export function DashboardStoreProvider({
|
|||||||
setSelectedForecastDate(null);
|
setSelectedForecastDate(null);
|
||||||
setFutureModalDate(null);
|
setFutureModalDate(null);
|
||||||
setForecastModalMode(null);
|
setForecastModalMode(null);
|
||||||
const depth: CityDetailDepth = proAccessRef.current.subscriptionActive
|
|
||||||
? "market"
|
|
||||||
: "panel";
|
|
||||||
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
||||||
void Promise.allSettled([
|
void Promise.allSettled([
|
||||||
ensureCitySummary(cityName),
|
ensureCitySummary(cityName),
|
||||||
ensureCityDetail(cityName, false, depth),
|
ensureCityDetail(cityName, false, "panel"),
|
||||||
])
|
])
|
||||||
.then(([, detail]) => {
|
.then(([, detail]) => {
|
||||||
if (selectedCityRef.current !== cityName) return;
|
if (selectedCityRef.current !== cityName) return;
|
||||||
if (detail.status === "fulfilled") {
|
if (detail.status === "fulfilled") {
|
||||||
setSelectedForecastDate(detail.value.local_date);
|
setSelectedForecastDate(detail.value.local_date);
|
||||||
if (
|
|
||||||
proAccessRef.current.subscriptionActive &&
|
|
||||||
!detail.value.market_scan
|
|
||||||
) {
|
|
||||||
void ensureCityMarketScan(cityName, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -965,15 +956,6 @@ export function DashboardStoreProvider({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedCity) return;
|
|
||||||
if (!proAccess.subscriptionActive) return;
|
|
||||||
const detail = cityDetailsByName[selectedCity];
|
|
||||||
if (!detail) return;
|
|
||||||
if (detail.market_scan) return;
|
|
||||||
void ensureCityMarketScan(selectedCity, false);
|
|
||||||
}, [cityDetailsByName, proAccess.subscriptionActive, selectedCity]);
|
|
||||||
|
|
||||||
const clearCityFocus = () => {
|
const clearCityFocus = () => {
|
||||||
selectedCityRef.current = null;
|
selectedCityRef.current = null;
|
||||||
setSelectedCity(null);
|
setSelectedCity(null);
|
||||||
@@ -1220,11 +1202,6 @@ export function DashboardStoreProvider({
|
|||||||
const hasFullCachedDetail =
|
const hasFullCachedDetail =
|
||||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||||
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
!hasSparseDetailCoverage(cachedDetail, dateStr);
|
||||||
const hasMarketCachedDetail = detailSatisfiesDepth(
|
|
||||||
cachedDetail,
|
|
||||||
"market",
|
|
||||||
dateStr,
|
|
||||||
);
|
|
||||||
const todayDate =
|
const todayDate =
|
||||||
cachedDetail?.local_date ||
|
cachedDetail?.local_date ||
|
||||||
cachedDetail?.forecast?.daily?.[0]?.date ||
|
cachedDetail?.forecast?.daily?.[0]?.date ||
|
||||||
@@ -1235,9 +1212,6 @@ export function DashboardStoreProvider({
|
|||||||
setSelectedForecastDate(dateStr);
|
setSelectedForecastDate(dateStr);
|
||||||
setFutureModalDate(dateStr);
|
setFutureModalDate(dateStr);
|
||||||
setForecastModalMode(modalMode);
|
setForecastModalMode(modalMode);
|
||||||
if (!hasMarketCachedDetail || forceRefresh) {
|
|
||||||
void ensureCityDetail(cityName, forceRefresh, "market").catch(() => {});
|
|
||||||
}
|
|
||||||
if (!hasFullCachedDetail || forceRefresh) {
|
if (!hasFullCachedDetail || forceRefresh) {
|
||||||
setLoadingState((current) => ({
|
setLoadingState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -1282,11 +1256,6 @@ export function DashboardStoreProvider({
|
|||||||
const hasFullCachedDetail =
|
const hasFullCachedDetail =
|
||||||
detailSatisfiesDepth(cachedDetail, "full") &&
|
detailSatisfiesDepth(cachedDetail, "full") &&
|
||||||
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
!hasSparseDetailCoverage(cachedDetail, cachedDetail?.local_date);
|
||||||
const hasMarketCachedDetail = detailSatisfiesDepth(
|
|
||||||
cachedDetail,
|
|
||||||
"market",
|
|
||||||
cachedDetail?.local_date,
|
|
||||||
);
|
|
||||||
const targetDate =
|
const targetDate =
|
||||||
cachedDetail?.local_date ||
|
cachedDetail?.local_date ||
|
||||||
cachedDetail?.forecast?.daily?.[0]?.date ||
|
cachedDetail?.forecast?.daily?.[0]?.date ||
|
||||||
@@ -1306,13 +1275,6 @@ export function DashboardStoreProvider({
|
|||||||
...current,
|
...current,
|
||||||
futureDeep: needsDetailRefresh,
|
futureDeep: needsDetailRefresh,
|
||||||
}));
|
}));
|
||||||
if (!hasMarketCachedDetail || forceRefresh) {
|
|
||||||
void ensureCityDetail(
|
|
||||||
cityName,
|
|
||||||
Boolean(forceRefresh),
|
|
||||||
"market",
|
|
||||||
).catch(() => {});
|
|
||||||
}
|
|
||||||
void ensureCityDetail(
|
void ensureCityDetail(
|
||||||
cityName,
|
cityName,
|
||||||
needsDetailRefresh,
|
needsDetailRefresh,
|
||||||
|
|||||||
@@ -326,6 +326,42 @@ function buildTemperatureTickLabels(times: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNiceTemperatureScale(values: number[], tempSymbol?: string | null) {
|
||||||
|
const numericValues = values.filter((value) => Number.isFinite(Number(value)));
|
||||||
|
if (!numericValues.length) {
|
||||||
|
return { max: 1, min: 0, step: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawMin = Math.min(...numericValues);
|
||||||
|
const rawMax = Math.max(...numericValues);
|
||||||
|
const spread = Math.max(0.1, rawMax - rawMin);
|
||||||
|
const isFahrenheit = normalizeTemperatureSymbol(tempSymbol) === "°F";
|
||||||
|
const padding = Math.max(isFahrenheit ? 1.5 : 0.8, spread * 0.12);
|
||||||
|
const paddedMin = rawMin - padding;
|
||||||
|
const paddedMax = rawMax + padding;
|
||||||
|
const paddedSpread = Math.max(0.1, paddedMax - paddedMin);
|
||||||
|
const candidates = isFahrenheit ? [1, 2, 5, 10, 20] : [0.5, 1, 2, 5, 10];
|
||||||
|
let step =
|
||||||
|
candidates.find((candidate) => candidate >= paddedSpread / 4) ||
|
||||||
|
candidates[candidates.length - 1];
|
||||||
|
let min = Math.floor(paddedMin / step) * step;
|
||||||
|
let max = Math.ceil(paddedMax / step) * step;
|
||||||
|
|
||||||
|
if (min < 0 && rawMin >= 0 && rawMin <= step * 1.25) min = 0;
|
||||||
|
if (max <= min) max = min + step * 4;
|
||||||
|
|
||||||
|
while ((max - min) / step + 1 > 6) {
|
||||||
|
const nextStep = candidates.find((candidate) => candidate > step);
|
||||||
|
if (!nextStep) break;
|
||||||
|
step = nextStep;
|
||||||
|
min = Math.floor(paddedMin / step) * step;
|
||||||
|
max = Math.ceil(paddedMax / step) * step;
|
||||||
|
if (min < 0 && rawMin >= 0 && rawMin <= step * 1.25) min = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { max, min, step };
|
||||||
|
}
|
||||||
|
|
||||||
function buildSeriesPoints(
|
function buildSeriesPoints(
|
||||||
times: string[],
|
times: string[],
|
||||||
values: Array<number | null | undefined>,
|
values: Array<number | null | undefined>,
|
||||||
@@ -905,8 +941,10 @@ export function getTemperatureChartData(
|
|||||||
|
|
||||||
if (!allValues.length) return null;
|
if (!allValues.length) return null;
|
||||||
|
|
||||||
const min = Math.floor(Math.min(...allValues)) - 1;
|
const yScale = getNiceTemperatureScale(allValues, detail.temp_symbol);
|
||||||
const max = Math.ceil(Math.max(...allValues)) + 1;
|
const min = yScale.min;
|
||||||
|
const max = yScale.max;
|
||||||
|
const yTickStep = yScale.step;
|
||||||
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
||||||
? detail.taf?.signal?.markers || []
|
? detail.taf?.signal?.markers || []
|
||||||
: [];
|
: [];
|
||||||
@@ -1240,6 +1278,7 @@ export function getTemperatureChartData(
|
|||||||
times,
|
times,
|
||||||
xMax,
|
xMax,
|
||||||
xMin,
|
xMin,
|
||||||
|
yTickStep,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "node scripts/sync-next-server-chunks.mjs --clean-root && next dev",
|
||||||
"build": "next build",
|
"build": "next build && node scripts/sync-next-server-chunks.mjs",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { copyFile, mkdir, readdir, unlink } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const serverDir = path.join(process.cwd(), ".next", "server");
|
||||||
|
const chunksDir = path.join(serverDir, "chunks");
|
||||||
|
const cleanRoot = process.argv.includes("--clean-root");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await readdir(chunksDir, { withFileTypes: true });
|
||||||
|
await mkdir(serverDir, { recursive: true });
|
||||||
|
await Promise.all(
|
||||||
|
entries
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".js"))
|
||||||
|
.map(async (entry) => {
|
||||||
|
const target = path.join(serverDir, entry.name);
|
||||||
|
if (cleanRoot) {
|
||||||
|
await unlink(target).catch((error) => {
|
||||||
|
if (error?.code !== "ENOENT") throw error;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await copyFile(path.join(chunksDir, entry.name), target);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== "ENOENT") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user