mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 02:40:50 +08:00
feat(ProviderNav): update mobile layout to use bottom floating navigation and improve scroll handling
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user