fix: improve mobile terminal region tabs
This commit is contained in:
@@ -861,24 +861,46 @@ function PolyWeatherTerminal({
|
|||||||
() => buildContinentGroups(filteredRegionRows, isEn),
|
() => buildContinentGroups(filteredRegionRows, isEn),
|
||||||
[filteredRegionRows, isEn]
|
[filteredRegionRows, isEn]
|
||||||
);
|
);
|
||||||
|
const mobileGroups = useMemo(
|
||||||
|
() => buildContinentGroups(rows, isEn),
|
||||||
|
[rows, isEn],
|
||||||
|
);
|
||||||
const [mobileTab, setMobileTab] = useState<string>("active_signals");
|
const [mobileTab, setMobileTab] = useState<string>("active_signals");
|
||||||
const mobileActiveGroup = useMemo(
|
const mobileActiveGroup = useMemo(
|
||||||
() => continentGroups.find((g) => g.key === mobileTab) || continentGroups[0],
|
() => mobileGroups.find((g) => g.key === mobileTab) || mobileGroups[0],
|
||||||
[continentGroups, mobileTab]
|
[mobileGroups, mobileTab],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (continentGroups.length > 0 && !continentGroups.find((g) => g.key === mobileTab)) {
|
if (mobileGroups.length > 0 && !mobileGroups.find((g) => g.key === mobileTab)) {
|
||||||
setMobileTab(continentGroups[0].key);
|
setMobileTab(mobileGroups[0].key);
|
||||||
}
|
}
|
||||||
}, [continentGroups, mobileTab]);
|
}, [mobileGroups, mobileTab]);
|
||||||
|
const mobileSelectedRowInActiveGroup = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedRow && mobileActiveGroup?.rows.some((row) => row.id === selectedRow.id)
|
||||||
|
? selectedRow
|
||||||
|
: null,
|
||||||
|
[mobileActiveGroup?.rows, selectedRow],
|
||||||
|
);
|
||||||
const mobileChartRow = useMemo(
|
const mobileChartRow = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectedRow ||
|
mobileSelectedRowInActiveGroup ||
|
||||||
mobileActiveGroup?.rows.slice(0, MOBILE_TERMINAL_CHARTS)[0] ||
|
mobileActiveGroup?.rows[0] ||
|
||||||
filteredRegionRows[0] ||
|
filteredRegionRows[0] ||
|
||||||
null,
|
null,
|
||||||
[filteredRegionRows, mobileActiveGroup?.rows, selectedRow],
|
[filteredRegionRows, mobileActiveGroup?.rows, mobileSelectedRowInActiveGroup],
|
||||||
);
|
);
|
||||||
|
const handleMobileSelectTab = useCallback((key: string) => {
|
||||||
|
setMobileTab(key);
|
||||||
|
if (selectedRegionKey !== "all") {
|
||||||
|
setSelectedRegionKey("all");
|
||||||
|
}
|
||||||
|
const nextGroup = mobileGroups.find((group) => group.key === key);
|
||||||
|
const nextRow = nextGroup?.rows[0];
|
||||||
|
if (nextRow) {
|
||||||
|
setSelectedRow(nextRow);
|
||||||
|
}
|
||||||
|
}, [mobileGroups, selectedRegionKey, setSelectedRegionKey, setSelectedRow]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filteredRegionRows.length) return;
|
if (!filteredRegionRows.length) return;
|
||||||
if (!selectedRow || !filteredRegionRows.some((row) => row.id === selectedRow.id)) {
|
if (!selectedRow || !filteredRegionRows.some((row) => row.id === selectedRow.id)) {
|
||||||
@@ -967,16 +989,18 @@ function PolyWeatherTerminal({
|
|||||||
<>
|
<>
|
||||||
{/* Mobile layout */}
|
{/* Mobile layout */}
|
||||||
<div className="flex flex-col gap-2 lg:hidden overflow-auto flex-1 pb-6">
|
<div className="flex flex-col gap-2 lg:hidden overflow-auto flex-1 pb-6">
|
||||||
<MobileRegionTabs
|
<div className="mobile-region-tabs-shell sticky top-0 z-30 -mx-2 bg-[#eef2f6] pb-2 shadow-sm">
|
||||||
activeTab={mobileTab}
|
<MobileRegionTabs
|
||||||
groups={continentGroups}
|
activeTab={mobileTab}
|
||||||
isEn={isEn}
|
groups={mobileGroups}
|
||||||
onSelectTab={setMobileTab}
|
isEn={isEn}
|
||||||
/>
|
onSelectTab={handleMobileSelectTab}
|
||||||
<div className="rounded border border-blue-200 bg-blue-50 px-3 py-2 text-[11px] font-semibold leading-4 text-blue-700">
|
/>
|
||||||
{isEn
|
<div className="mx-2 mt-1 rounded border border-blue-200 bg-blue-50 px-2 py-1 text-[10px] font-semibold leading-3 text-blue-700">
|
||||||
? "Mobile renders one chart. Rotate to landscape to inspect the full terminal grid."
|
{isEn
|
||||||
: "手机端仅渲染 1 个图表。建议横屏查看完整终端网格。"}
|
? "Mobile renders one chart. Rotate to landscape for the full grid."
|
||||||
|
: "手机端仅渲染 1 个图表。建议横屏查看完整终端网格。"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{mobileChartRow && (
|
{mobileChartRow && (
|
||||||
<div className="h-[420px] min-h-[420px] overflow-hidden rounded border border-[#d2d9e2] bg-white">
|
<div className="h-[420px] min-h-[420px] overflow-hidden rounded border border-[#d2d9e2] bg-white">
|
||||||
@@ -1001,16 +1025,16 @@ function PolyWeatherTerminal({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Mobile Selected Row Detail */}
|
{/* Mobile Selected Row Detail */}
|
||||||
{selectedRow && (
|
{mobileChartRow && (
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-black text-slate-900 mb-2">{rowName(selectedRow)}</h3>
|
<h3 className="text-sm font-black text-slate-900 mb-2">{rowName(mobileChartRow)}</h3>
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
{[
|
{[
|
||||||
["Obs", temp(selectedRow.current_temp, selectedRow.temp_symbol)],
|
["Obs", temp(mobileChartRow.current_temp, mobileChartRow.temp_symbol)],
|
||||||
["High", temp(selectedRow.current_max_so_far, selectedRow.temp_symbol)],
|
["High", temp(mobileChartRow.current_max_so_far, mobileChartRow.temp_symbol)],
|
||||||
["DEB", temp(selectedRow.deb_prediction, selectedRow.temp_symbol)],
|
["DEB", temp(mobileChartRow.deb_prediction, mobileChartRow.temp_symbol)],
|
||||||
["Gap", temp(selectedRow.signed_gap ?? selectedRow.gap_to_target, selectedRow.temp_symbol)],
|
["Gap", temp(mobileChartRow.signed_gap ?? mobileChartRow.gap_to_target, mobileChartRow.temp_symbol)],
|
||||||
["Edge", pct(selectedRow.edge_percent)],
|
["Edge", pct(mobileChartRow.edge_percent)],
|
||||||
["Market", "--"],
|
["Market", "--"],
|
||||||
].map(([label, value]) => (
|
].map(([label, value]) => (
|
||||||
<div key={label} className="rounded border border-slate-200 bg-slate-50 p-2">
|
<div key={label} className="rounded border border-slate-200 bg-slate-50 p-2">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function MobileRegionTabs({
|
|||||||
onSelectTab: (key: string) => void;
|
onSelectTab: (key: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="mobile-region-tabs flex overflow-x-auto border-b border-slate-200 bg-white px-2">
|
<div className="mobile-region-tabs flex min-h-11 snap-x gap-1 overflow-x-auto border-b border-slate-200 bg-white px-2 [-webkit-overflow-scrolling:touch] [scrollbar-width:none]">
|
||||||
{groups.map((g) => {
|
{groups.map((g) => {
|
||||||
const label = isEn ? g.labelEn : g.labelZh;
|
const label = isEn ? g.labelEn : g.labelZh;
|
||||||
const isActive = activeTab === g.key;
|
const isActive = activeTab === g.key;
|
||||||
@@ -23,9 +23,10 @@ export function MobileRegionTabs({
|
|||||||
<button
|
<button
|
||||||
key={g.key}
|
key={g.key}
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-pressed={isActive}
|
||||||
onClick={() => onSelectTab(g.key)}
|
onClick={() => onSelectTab(g.key)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"shrink-0 px-3 py-2.5 text-xs font-bold whitespace-nowrap border-b-2 transition-colors",
|
"flex min-h-11 shrink-0 snap-start items-center border-b-2 px-3 text-xs font-bold whitespace-nowrap transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "border-blue-600 text-blue-700"
|
? "border-blue-600 text-blue-700"
|
||||||
: "border-transparent text-slate-500 hover:text-slate-700"
|
: "border-transparent text-slate-500 hover:text-slate-700"
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export function runTests() {
|
|||||||
path.join(projectRoot, "components", "dashboard", "scan-terminal", "use-scan-terminal-query.ts"),
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "use-scan-terminal-query.ts"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
const mobileRegionTabsSource = fs.readFileSync(
|
||||||
|
path.join(projectRoot, "components", "dashboard", "scan-terminal", "MobileRegionTabs.tsx"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
dashboardSource.includes("MAX_TERMINAL_CHARTS = 9"),
|
dashboardSource.includes("MAX_TERMINAL_CHARTS = 9"),
|
||||||
@@ -57,6 +61,16 @@ export function runTests() {
|
|||||||
dashboardSource.includes("disableClose={true}"),
|
dashboardSource.includes("disableClose={true}"),
|
||||||
"mobile terminal should render one selected chart and suggest landscape for the full grid",
|
"mobile terminal should render one selected chart and suggest landscape for the full grid",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
dashboardSource.includes("mobile-region-tabs-shell") &&
|
||||||
|
dashboardSource.includes("handleMobileSelectTab") &&
|
||||||
|
dashboardSource.includes("mobileSelectedRowInActiveGroup") &&
|
||||||
|
dashboardSource.includes("mobileActiveGroup?.rows[0]") &&
|
||||||
|
mobileRegionTabsSource.includes("min-h-11") &&
|
||||||
|
mobileRegionTabsSource.includes("snap-x") &&
|
||||||
|
mobileRegionTabsSource.includes("aria-pressed={isActive}"),
|
||||||
|
"mobile region navigation must stay visible above the landscape hint, remain touch-friendly, and switch the visible chart with the active region",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
!dashboardSource.includes("disableClose={visibleSlots.filter(Boolean).length <= 1}"),
|
!dashboardSource.includes("disableClose={visibleSlots.filter(Boolean).length <= 1}"),
|
||||||
"desktop chart slots must allow clearing the last visible city so users can close a 1x1 chart",
|
"desktop chart slots must allow clearing the last visible city so users can close a 1x1 chart",
|
||||||
|
|||||||
Reference in New Issue
Block a user