Polish homepage trend interactions

This commit is contained in:
2569718930@qq.com
2026-04-23 01:34:39 +08:00
parent 731d2e5a18
commit 1ed45ded9f
2 changed files with 169 additions and 6 deletions
@@ -1314,6 +1314,49 @@
stroke-width: 1.6;
}
.root :global(.home-intraday-hotspot) {
position: absolute;
width: 18px;
height: 18px;
transform: translate(-50%, -50%);
border: 0;
border-radius: 999px;
background: transparent;
cursor: pointer;
}
.root :global(.home-intraday-hotspot:focus-visible) {
outline: 1px solid rgba(34, 211, 238, 0.6);
outline-offset: 1px;
}
.root :global(.home-intraday-tooltip) {
position: absolute;
transform: translate(-50%, -100%);
pointer-events: none;
display: inline-flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
border: 1px solid rgba(34, 211, 238, 0.22);
border-radius: 10px;
background: rgba(7, 15, 28, 0.96);
box-shadow: 0 10px 26px rgba(2, 6, 23, 0.34);
white-space: nowrap;
}
.root :global(.home-intraday-tooltip strong) {
color: #f8fafc;
font-size: 12px;
font-weight: 900;
}
.root :global(.home-intraday-tooltip span) {
color: rgba(148, 163, 184, 0.88);
font-size: 10px;
font-weight: 700;
}
.root :global(.home-intraday-meta) {
color: rgba(148, 163, 184, 0.9);
font-size: 11px;
@@ -2,7 +2,7 @@
import clsx from "clsx";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useEffect, useMemo, useRef } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import styles from "./Dashboard.module.css";
import detailChromeStyles from "./DetailPanelChrome.module.css";
import modalChromeStyles from "./ModalChrome.module.css";
@@ -222,6 +222,13 @@ type HomeTrendChart = {
forecastPath: string;
legendText: string;
observationDots: Array<{ cx: number; cy: number; key: string }>;
hoverPoints: Array<{
cx: number;
cy: number;
key: string;
label: string;
temperatureText: string;
}>;
};
type HomeForecastDay = {
@@ -230,6 +237,8 @@ type HomeForecastDay = {
maxTemp: number;
};
const RECENT_OPENED_CITIES_STORAGE_KEY = "polyWeather_recent_opened_cities_v1";
function projectHomeTrendPoint(
x: number,
y: number,
@@ -319,11 +328,30 @@ function buildHomeTrendChart(
key: `${point.labelTime}-${index}`,
};
});
const hoverSource = observationSeries.length > 0 ? observationSeries : forecastSeries;
const hoverPoints = hoverSource.map((point, index) => {
const projected = projectHomeTrendPoint(
point.x,
point.y,
chartData.xMin,
chartData.xMax,
chartData.min,
chartData.max,
);
return {
cx: projected.cx,
cy: projected.cy,
key: `hover-${point.labelTime}-${index}`,
label: point.labelTime,
temperatureText: formatTemperature(point.y, detail.temp_symbol || "°C"),
};
});
return {
forecastPath,
legendText: chartData.legendText,
observationDots,
hoverPoints,
};
}
@@ -441,6 +469,13 @@ function isTradableMarketOpportunity(detail?: CityDetail | null) {
function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
const store = useDashboardStore();
const { locale } = useI18n();
const [hoveredTrendPoint, setHoveredTrendPoint] = useState<{
cx: number;
cy: number;
key: string;
label: string;
temperatureText: string;
} | null>(null);
const selectedSnapshot = store.selectedCity
? snapshots.find((snapshot) => snapshot.city.name === store.selectedCity)
: null;
@@ -672,6 +707,39 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
<circle key={point.key} cx={point.cx} cy={point.cy} r="3.2" />
))}
</svg>
{trendChart.hoverPoints.map((point) => (
<button
key={point.key}
type="button"
className="home-intraday-hotspot"
style={{ left: `${point.cx}px`, top: `${point.cy}px` }}
aria-label={`${point.label} ${point.temperatureText}`}
onMouseEnter={() => setHoveredTrendPoint(point)}
onMouseLeave={() =>
setHoveredTrendPoint((current) =>
current?.key === point.key ? null : current,
)
}
onFocus={() => setHoveredTrendPoint(point)}
onBlur={() =>
setHoveredTrendPoint((current) =>
current?.key === point.key ? null : current,
)
}
/>
))}
{hoveredTrendPoint ? (
<div
className="home-intraday-tooltip"
style={{
left: `${Math.min(248, Math.max(48, hoveredTrendPoint.cx))}px`,
top: `${Math.max(8, hoveredTrendPoint.cy - 34)}px`,
}}
>
<strong>{hoveredTrendPoint.temperatureText}</strong>
<span>{hoveredTrendPoint.label}</span>
</div>
) : null}
</div>
<div className="home-intraday-meta">
{trendChart.legendText ||
@@ -825,10 +893,21 @@ function HomeIntelligencePanel({ snapshots }: { snapshots: CitySnapshot[] }) {
);
}
function OpportunityStrip({ snapshots }: { snapshots: CitySnapshot[] }) {
function OpportunityStrip({
snapshots,
recentCityNames,
}: {
snapshots: CitySnapshot[];
recentCityNames: string[];
}) {
const store = useDashboardStore();
const { locale } = useI18n();
const items = snapshots.slice(0, 4);
const items = recentCityNames
.map((cityName) =>
snapshots.find((snapshot) => snapshot.city.name === cityName) || null,
)
.filter((snapshot): snapshot is CitySnapshot => snapshot != null)
.slice(0, 4);
if (!items.length) return null;
@@ -838,9 +917,9 @@ function OpportunityStrip({ snapshots }: { snapshots: CitySnapshot[] }) {
aria-label={locale === "en-US" ? "Opportunity strip" : "机会条"}
>
<div className="opportunity-strip-heading">
<span>{locale === "en-US" ? "Today focus" : "今日焦点"}</span>
<span>{locale === "en-US" ? "Recently opened" : "最近打开"}</span>
<strong>
{locale === "en-US" ? "Cities worth opening first" : "优先打开的城市"}
{locale === "en-US" ? "Recently opened cities" : "最近打开的城市"}
</strong>
</div>
<div className="opportunity-card-grid">
@@ -884,6 +963,7 @@ function DashboardScreen() {
const { t } = useI18n();
const didAutoFocusRef = useRef(false);
const preloadedOpportunityRef = useRef<Set<string>>(new Set());
const [recentCityNames, setRecentCityNames] = useState<string[]>([]);
const activeSummary = store.selectedCity
? store.citySummariesByName[store.selectedCity] || null
: null;
@@ -945,6 +1025,19 @@ function DashboardScreen() {
const showHomepageChrome =
!store.historyState.isOpen && !store.futureModalDate;
useEffect(() => {
if (typeof window === "undefined") return;
try {
const raw = window.localStorage.getItem(RECENT_OPENED_CITIES_STORAGE_KEY);
if (!raw) return;
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return;
setRecentCityNames(
parsed.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8),
);
} catch {}
}, []);
useEffect(() => {
if (didAutoFocusRef.current) return;
if (!showHomepageChrome) return;
@@ -968,6 +1061,26 @@ function DashboardScreen() {
});
}, [homepageSnapshots, showHomepageChrome, store]);
useEffect(() => {
const selectedCity = store.selectedCity;
if (!selectedCity) return;
setRecentCityNames((current) => {
const next = [
selectedCity,
...current.filter((name) => name !== selectedCity),
].slice(0, 8);
if (typeof window !== "undefined") {
try {
window.localStorage.setItem(
RECENT_OPENED_CITIES_STORAGE_KEY,
JSON.stringify(next),
);
} catch {}
}
return next;
});
}, [store.selectedCity]);
return (
<div
className={clsx(
@@ -982,7 +1095,14 @@ function DashboardScreen() {
{showHomepageChrome ? (
<>
<HomeIntelligencePanel snapshots={homepageSnapshots} />
<OpportunityStrip snapshots={homepageSnapshots} />
<OpportunityStrip
snapshots={homepageSnapshots}
recentCityNames={
recentCityNames.length
? recentCityNames
: homepageSnapshots.slice(0, 4).map((snapshot) => snapshot.city.name)
}
/>
</>
) : null}
{showCitySyncToast ? (