feat: implement scan terminal dashboard with real-time opportunity tracking and visualization components
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,16 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
() => getTemperatureChartData(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(() => {
|
||||
if (!chartData) {
|
||||
@@ -50,28 +60,20 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
borderWidth: 1.8,
|
||||
data: forecastPoints,
|
||||
fill: false,
|
||||
label: chartData.datasets.hasMgmHourly
|
||||
? locale === "en-US"
|
||||
? "MGM Forecast"
|
||||
: "MGM 预测"
|
||||
: locale === "en-US"
|
||||
? "DEB Forecast"
|
||||
: "DEB 预测",
|
||||
label: forecastLabel,
|
||||
pointRadius: 0,
|
||||
spanGaps: true,
|
||||
tension: 0.28,
|
||||
},
|
||||
{
|
||||
backgroundColor: "#00E0A4",
|
||||
borderColor: "#00E0A4",
|
||||
backgroundColor: "#4DA3FF",
|
||||
borderColor: "#4DA3FF",
|
||||
borderWidth: 0,
|
||||
data: chartData.datasets.metarPoints,
|
||||
fill: false,
|
||||
label:
|
||||
chartData.observationLabel ||
|
||||
(locale === "en-US" ? "METAR Observation" : "METAR 实况"),
|
||||
pointHoverRadius: 6,
|
||||
pointRadius: 3.8,
|
||||
label: observationLabel,
|
||||
pointHoverRadius: 5,
|
||||
pointRadius: 3.2,
|
||||
showLine: false,
|
||||
},
|
||||
],
|
||||
@@ -79,12 +81,13 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
},
|
||||
options: {
|
||||
interaction: { intersect: false, mode: "index" },
|
||||
layout: { padding: { bottom: 0, left: 0, right: 6, top: 4 } },
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(15, 23, 42, 0.95)",
|
||||
borderColor: "rgba(0, 224, 164, 0.25)",
|
||||
borderColor: "rgba(77, 163, 255, 0.28)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
},
|
||||
@@ -97,8 +100,9 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
typeof index === "number" && index % 4 === 0
|
||||
? chartData.times[index]
|
||||
: "",
|
||||
color: "#64748b",
|
||||
color: "#6B7A90",
|
||||
font: { size: 10 },
|
||||
maxTicksLimit: 6,
|
||||
maxRotation: 0,
|
||||
},
|
||||
},
|
||||
@@ -107,22 +111,37 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
||||
max: chartData.max,
|
||||
min: chartData.min,
|
||||
ticks: {
|
||||
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
|
||||
color: "#64748b",
|
||||
callback: (value) =>
|
||||
`${Number(value).toFixed(chartData.yTickStep < 1 ? 1 : 0)}${detail.temp_symbol || "°C"}`,
|
||||
color: "#6B7A90",
|
||||
font: { size: 10 },
|
||||
maxTicksLimit: 5,
|
||||
stepSize: chartData.yTickStep,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "line",
|
||||
} satisfies ChartConfiguration<"line">;
|
||||
}, [chartData, detail.temp_symbol, locale]);
|
||||
}, [chartData, detail.temp_symbol, forecastLabel, observationLabel]);
|
||||
|
||||
return (
|
||||
<div className="detail-mini-chart-wrap">
|
||||
<div className="detail-mini-chart">
|
||||
<canvas ref={canvasRef} />
|
||||
</div>
|
||||
{chartData ? (
|
||||
<div className="detail-mini-chart-legend">
|
||||
<span>
|
||||
<i className="forecast" />
|
||||
{forecastLabel}
|
||||
</span>
|
||||
<span>
|
||||
<i className="observation" />
|
||||
{observationLabel}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,9 +90,9 @@
|
||||
}
|
||||
|
||||
.root :global(.panel-action-button-primary) {
|
||||
color: #05111f;
|
||||
background: linear-gradient(135deg, #67e8f9, #38bdf8);
|
||||
border-color: rgba(103, 232, 249, 0.3);
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(135deg, #4DA3FF, #3B82F6);
|
||||
border-color: rgba(77, 163, 255, 0.34);
|
||||
}
|
||||
|
||||
.root :global(.panel-action-button-secondary) {
|
||||
|
||||
@@ -62,8 +62,8 @@ function formatMinuteAxisLabel(value: number) {
|
||||
function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) {
|
||||
if (emoji === "☀️") return <Sun size={size} color="#facc15" />;
|
||||
if (emoji === "⛅" || emoji === "🌤️")
|
||||
return <CloudSun size={size} color="#38bdf8" />;
|
||||
if (emoji === "☁️") return <Cloud size={size} color="#94a3b8" />;
|
||||
return <CloudSun size={size} color="#4DA3FF" />;
|
||||
if (emoji === "☁️") return <Cloud size={size} color="#9FB2C7" />;
|
||||
if (emoji === "🌧️" || emoji === "🌦️")
|
||||
return <CloudRain size={size} color="#60a5fa" />;
|
||||
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" />;
|
||||
if (emoji === "🌫️") return <CloudFog size={size} color="#a1a1aa" />;
|
||||
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) {
|
||||
@@ -354,8 +354,8 @@ function DailyTemperatureChart({
|
||||
});
|
||||
} else {
|
||||
datasets.push({
|
||||
backgroundColor: "rgba(52, 211, 153, 0.05)",
|
||||
borderColor: "rgba(52, 211, 153, 0.6)",
|
||||
backgroundColor: "rgba(77, 163, 255, 0.06)",
|
||||
borderColor: "rgba(77, 163, 255, 0.66)",
|
||||
borderWidth: 1.5,
|
||||
data: todayChartData.datasets.debPastSeries,
|
||||
fill: true,
|
||||
@@ -366,7 +366,7 @@ function DailyTemperatureChart({
|
||||
tension: 0.3,
|
||||
});
|
||||
datasets.push({
|
||||
borderColor: "rgba(52, 211, 153, 0.35)",
|
||||
borderColor: "rgba(77, 163, 255, 0.36)",
|
||||
borderDash: [5, 3],
|
||||
borderWidth: 1.5,
|
||||
data: todayChartData.datasets.debFutureSeries,
|
||||
@@ -379,8 +379,8 @@ function DailyTemperatureChart({
|
||||
}
|
||||
|
||||
datasets.push({
|
||||
backgroundColor: "#00E0A4",
|
||||
borderColor: "#00E0A4",
|
||||
backgroundColor: "#4DA3FF",
|
||||
borderColor: "#4DA3FF",
|
||||
borderWidth: 0,
|
||||
data: todayChartData.datasets.metarSeries,
|
||||
fill: false,
|
||||
@@ -431,7 +431,7 @@ function DailyTemperatureChart({
|
||||
Math.abs(todayChartData.datasets.offset) > 0.3
|
||||
) {
|
||||
datasets.push({
|
||||
borderColor: "rgba(123, 97, 255, 0.2)",
|
||||
borderColor: "rgba(77, 163, 255, 0.22)",
|
||||
borderDash: [2, 4],
|
||||
borderWidth: 1,
|
||||
data: todayChartData.datasets.tempsSeries,
|
||||
@@ -495,7 +495,7 @@ function DailyTemperatureChart({
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: "#94a3b8",
|
||||
color: "#9FB2C7",
|
||||
filter: (legendItem, chartData) => {
|
||||
const text = String(legendItem.text || "");
|
||||
if (!text) return false;
|
||||
@@ -513,7 +513,7 @@ function DailyTemperatureChart({
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
||||
borderColor: "rgba(0, 224, 164, 0.2)",
|
||||
borderColor: "rgba(77, 163, 255, 0.24)",
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
title: (items) => {
|
||||
@@ -598,7 +598,7 @@ function DailyTemperatureChart({
|
||||
}
|
||||
return formatMinuteAxisLabel(minutes);
|
||||
},
|
||||
color: "#64748b",
|
||||
color: "#6B7A90",
|
||||
font: { family: "Inter", size: 10 },
|
||||
maxRotation: 0,
|
||||
},
|
||||
@@ -608,9 +608,11 @@ function DailyTemperatureChart({
|
||||
max: todayChartData.max,
|
||||
min: todayChartData.min,
|
||||
ticks: {
|
||||
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
|
||||
color: "#64748b",
|
||||
callback: (value) =>
|
||||
`${Number(value).toFixed(todayChartData.yTickStep < 1 ? 1 : 0)}${detail.temp_symbol || "°C"}`,
|
||||
color: "#6B7A90",
|
||||
font: { family: "Inter", size: 10 },
|
||||
stepSize: todayChartData.yTickStep,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -626,8 +628,8 @@ function DailyTemperatureChart({
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: "rgba(0, 224, 164, 0.08)",
|
||||
borderColor: "#00E0A4",
|
||||
backgroundColor: "rgba(77, 163, 255, 0.08)",
|
||||
borderColor: "#4DA3FF",
|
||||
data: view.slice.map((point) => point.temp),
|
||||
fill: false,
|
||||
label:
|
||||
@@ -637,7 +639,7 @@ function DailyTemperatureChart({
|
||||
},
|
||||
{
|
||||
backgroundColor: "transparent",
|
||||
borderColor: "#a78bfa",
|
||||
borderColor: "#93C5FD",
|
||||
borderDash: [5, 4],
|
||||
data: view.slice.map((point) => point.dewPoint),
|
||||
fill: false,
|
||||
@@ -654,13 +656,13 @@ function DailyTemperatureChart({
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: "#94a3b8",
|
||||
color: "#9FB2C7",
|
||||
font: { family: "Inter", size: 11 },
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(15, 23, 42, 0.96)",
|
||||
borderColor: "rgba(0, 224, 164, 0.2)",
|
||||
borderColor: "rgba(77, 163, 255, 0.24)",
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: (ctx) =>
|
||||
@@ -673,7 +675,7 @@ function DailyTemperatureChart({
|
||||
x: {
|
||||
grid: { color: "rgba(255,255,255,0.04)" },
|
||||
ticks: {
|
||||
color: "#64748b",
|
||||
color: "#6B7A90",
|
||||
font: { family: "Inter", size: 10 },
|
||||
maxRotation: 0,
|
||||
},
|
||||
@@ -682,7 +684,7 @@ function DailyTemperatureChart({
|
||||
grid: { color: "rgba(255,255,255,0.04)" },
|
||||
ticks: {
|
||||
callback: (value) => `${value}${unit}`,
|
||||
color: "#64748b",
|
||||
color: "#6B7A90",
|
||||
font: { family: "Inter", size: 10 },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,10 +18,10 @@ function clamp(value: number, min: number, max: number) {
|
||||
}
|
||||
|
||||
function getToneColor(tone: string) {
|
||||
if (tone === "cyan") return "#00E0A4";
|
||||
if (tone === "cyan") return "#4DA3FF";
|
||||
if (tone === "blue") return "#60a5fa";
|
||||
if (tone === "amber") return "#f59e0b";
|
||||
return "#94a3b8";
|
||||
return "#9FB2C7";
|
||||
}
|
||||
|
||||
export function IntradaySignalScene({
|
||||
@@ -77,7 +77,7 @@ export function IntradaySignalScene({
|
||||
const ring = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(2.8, 0.03, 18, 100),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: new THREE.Color(score >= 0 ? "#00E0A4" : "#FFB020"),
|
||||
color: new THREE.Color(score >= 0 ? "#4DA3FF" : "#F59E0B"),
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
}),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -694,8 +694,8 @@ export function TemperatureChart() {
|
||||
});
|
||||
} else {
|
||||
datasets.push({
|
||||
backgroundColor: "rgba(52, 211, 153, 0.05)",
|
||||
borderColor: "rgba(52, 211, 153, 0.6)",
|
||||
backgroundColor: "rgba(77, 163, 255, 0.06)",
|
||||
borderColor: "rgba(77, 163, 255, 0.66)",
|
||||
borderWidth: 1.5,
|
||||
data: chartData.datasets.debPast,
|
||||
fill: true,
|
||||
@@ -705,7 +705,7 @@ export function TemperatureChart() {
|
||||
tension: 0.3,
|
||||
});
|
||||
datasets.push({
|
||||
borderColor: "rgba(52, 211, 153, 0.35)",
|
||||
borderColor: "rgba(77, 163, 255, 0.36)",
|
||||
borderDash: [5, 3],
|
||||
borderWidth: 1.5,
|
||||
data: chartData.datasets.debFuture,
|
||||
@@ -717,8 +717,8 @@ export function TemperatureChart() {
|
||||
}
|
||||
|
||||
datasets.push({
|
||||
backgroundColor: "#00E0A4",
|
||||
borderColor: "#00E0A4",
|
||||
backgroundColor: "#4DA3FF",
|
||||
borderColor: "#4DA3FF",
|
||||
borderWidth: 0,
|
||||
data: chartData.datasets.metarPoints,
|
||||
fill: false,
|
||||
@@ -750,7 +750,7 @@ export function TemperatureChart() {
|
||||
Math.abs(chartData.datasets.offset) > 0.3
|
||||
) {
|
||||
datasets.push({
|
||||
borderColor: "rgba(123, 97, 255, 0.2)",
|
||||
borderColor: "rgba(77, 163, 255, 0.22)",
|
||||
borderDash: [2, 4],
|
||||
borderWidth: 1,
|
||||
data: chartData.datasets.temps,
|
||||
@@ -773,7 +773,7 @@ export function TemperatureChart() {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: "rgba(15, 23, 42, 0.9)",
|
||||
borderColor: "rgba(52, 211, 153, 0.3)",
|
||||
borderColor: "rgba(77, 163, 255, 0.28)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
},
|
||||
@@ -786,7 +786,7 @@ export function TemperatureChart() {
|
||||
typeof index === "number" && index % 3 === 0
|
||||
? chartData.times[index]
|
||||
: "",
|
||||
color: "#64748b",
|
||||
color: "#6B7A90",
|
||||
maxRotation: 0,
|
||||
},
|
||||
},
|
||||
@@ -795,8 +795,10 @@ export function TemperatureChart() {
|
||||
max: chartData.max,
|
||||
min: chartData.min,
|
||||
ticks: {
|
||||
callback: (value) => `${value}${data.temp_symbol || "°C"}`,
|
||||
color: "#64748b",
|
||||
callback: (value) =>
|
||||
`${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",
|
||||
},
|
||||
{
|
||||
label: isEn ? "Win-rate Basis" : "胜率依据",
|
||||
label: isEn ? "Forecast Center" : "预测中枢",
|
||||
value: bestRow ? formatTemperature(bestRow.deb_prediction, bestRow.temp_symbol || bestRow.target_unit) : "--",
|
||||
note: bestRow
|
||||
? `${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 52% 78%, ${hexToRgba(aura.tertiary, 0.12 * aura.intensity)}, transparent 38%)`,
|
||||
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"
|
||||
? `linear-gradient(180deg, ${hexToRgba("#e2e8f0", 0.06 * aura.effectIntensity)}, transparent 45%)`
|
||||
: aura.effect === "fog"
|
||||
|
||||
Reference in New Issue
Block a user