feat(ProviderNav): update mobile layout to use bottom floating navigation and improve scroll handling

This commit is contained in:
LTbinglingfeng
2026-02-01 02:24:05 +08:00
parent 237cca5680
commit f8c4a434ed
3 changed files with 88 additions and 19 deletions

View File

@@ -75,9 +75,39 @@
} }
} }
// 小屏幕隐藏悬浮导航 // 小屏幕改为底部横向浮层
@media (max-width: 1200px) { @media (max-width: 1200px) {
.navContainer { .navContainer {
display: none; top: auto;
right: auto;
left: 50%;
bottom: calc(12px + env(safe-area-inset-bottom));
transform: translateX(-50%);
width: min(520px, calc(100vw - 24px));
}
.navList {
flex-direction: row;
gap: 6px;
padding: 8px 10px;
border-radius: 999px;
overflow-x: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.navItem {
width: 36px;
height: 36px;
border-radius: 999px;
flex: 0 0 auto;
}
.icon {
width: 22px;
height: 22px;
} }
} }

View File

@@ -29,25 +29,47 @@ const PROVIDERS: ProviderNavItem[] = [
]; ];
const HEADER_OFFSET = 24; const HEADER_OFFSET = 24;
type ScrollContainer = HTMLElement | (Window & typeof globalThis);
export function ProviderNav() { export function ProviderNav() {
const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const [activeProvider, setActiveProvider] = useState<ProviderId | null>(null); const [activeProvider, setActiveProvider] = useState<ProviderId | null>(null);
const scrollContainerRef = useRef<HTMLElement | null>(null); const contentScrollerRef = useRef<HTMLElement | null>(null);
const getHeaderHeight = useCallback(() => {
const header = document.querySelector('.main-header') as HTMLElement | null;
if (header) return header.getBoundingClientRect().height;
const raw = getComputedStyle(document.documentElement).getPropertyValue('--header-height');
const value = Number.parseFloat(raw);
return Number.isFinite(value) ? value : 0;
}, []);
const getContentScroller = useCallback(() => {
if (contentScrollerRef.current && document.contains(contentScrollerRef.current)) {
return contentScrollerRef.current;
}
const getScrollContainer = useCallback(() => {
if (scrollContainerRef.current) return scrollContainerRef.current;
const container = document.querySelector('.content') as HTMLElement | null; const container = document.querySelector('.content') as HTMLElement | null;
scrollContainerRef.current = container; contentScrollerRef.current = container;
return container; return container;
}, []); }, []);
const getScrollContainer = useCallback((): ScrollContainer => {
// Mobile layout uses document scroll (layout switches at 768px); desktop uses the `.content` scroller.
const isMobile = window.matchMedia('(max-width: 768px)').matches;
if (isMobile) return window;
return getContentScroller() ?? window;
}, [getContentScroller]);
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
const container = getScrollContainer(); const container = getScrollContainer();
if (!container) return; if (!container) return;
const containerRect = container.getBoundingClientRect(); const isElementScroller = container instanceof HTMLElement;
const activationLine = containerRect.top + HEADER_OFFSET + 1; const headerHeight = isElementScroller ? 0 : getHeaderHeight();
const containerTop = isElementScroller ? container.getBoundingClientRect().top : 0;
const activationLine = containerTop + headerHeight + HEADER_OFFSET + 1;
let currentActive: ProviderId | null = null; let currentActive: ProviderId | null = null;
for (const provider of PROVIDERS) { for (const provider of PROVIDERS) {
@@ -71,31 +93,44 @@ export function ProviderNav() {
} }
setActiveProvider(currentActive); setActiveProvider(currentActive);
}, [getScrollContainer]); }, [getHeaderHeight, getScrollContainer]);
useEffect(() => { useEffect(() => {
const container = getScrollContainer(); const contentScroller = getContentScroller();
if (!container) return;
container.addEventListener('scroll', handleScroll, { passive: true }); // Listen to both: desktop scroll happens on `.content`; mobile uses `window`.
window.addEventListener('scroll', handleScroll, { passive: true });
contentScroller?.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('resize', handleScroll);
handleScroll(); handleScroll();
return () => container.removeEventListener('scroll', handleScroll); return () => {
}, [handleScroll, getScrollContainer]); window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleScroll);
contentScroller?.removeEventListener('scroll', handleScroll);
};
}, [getContentScroller, handleScroll]);
const scrollToProvider = (providerId: ProviderId) => { const scrollToProvider = (providerId: ProviderId) => {
const container = getScrollContainer(); const container = getScrollContainer();
const element = document.getElementById(`provider-${providerId}`); const element = document.getElementById(`provider-${providerId}`);
if (!element || !container) return; if (!element || !container) return;
setActiveProvider(providerId);
// Mobile: scroll the document (header is fixed, so offset by header height).
if (!(container instanceof HTMLElement)) {
const headerHeight = getHeaderHeight();
const elementTop = element.getBoundingClientRect().top + window.scrollY;
const target = Math.max(0, elementTop - headerHeight - HEADER_OFFSET);
window.scrollTo({ top: target, behavior: 'smooth' });
return;
}
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
const elementRect = element.getBoundingClientRect(); const elementRect = element.getBoundingClientRect();
const scrollTop = container.scrollTop + (elementRect.top - containerRect.top) - HEADER_OFFSET; const scrollTop = container.scrollTop + (elementRect.top - containerRect.top) - HEADER_OFFSET;
setActiveProvider(providerId); container.scrollTo({ top: scrollTop, behavior: 'smooth' });
container.scrollTo({
top: scrollTop,
behavior: 'smooth',
});
}; };
const navContent = ( const navContent = (

View File

@@ -27,6 +27,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-xl; gap: $spacing-xl;
@include mobile {
padding-bottom: calc(72px + env(safe-area-inset-bottom));
}
} }
.section { .section {