diff --git a/entrypoints/popup/App.css b/entrypoints/popup/App.css index 78f1bea..ba8e741 100644 --- a/entrypoints/popup/App.css +++ b/entrypoints/popup/App.css @@ -5,6 +5,7 @@ body { padding: 0; width: 100%; height: 100%; + scrollbar-gutter: stable; } /* 隐藏body滚动条但允许滚动 */ @@ -254,7 +255,6 @@ body { .nav-list { gap: 12px; flex-wrap: nowrap; - overflow: hidden; } /* 当菜单展开时,允许换行并显示所有可见项 */ diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index d0bacb6..3a6f405 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -1,9 +1,12 @@ -import { HashRouter as Router, Routes, Route, NavLink } from 'react-router-dom'; -import { useState, useEffect } from 'react'; +// App.js +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import TimestampPage from './pages/TimestampPage'; import RecordeReplayPage from './pages/RecordeReplayPage'; +import Navbar from './components/Navbar'; +import RoutePersistence from './components/RoutePersistence'; import './App.css'; +// 路由配置数据 const navItems = [ { path: '/', label: '时间戳', element: }, // {path: '/dzmy', label: '电子木鱼', element: }, @@ -11,86 +14,11 @@ const navItems = [ ]; function App() { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isMobile, setIsMobile] = useState(false); - const [visibleItems, setVisibleItems] = useState(navItems.length); - - // 检测屏幕尺寸变化 - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - setIsMobile(width < 768); - - // 根据屏幕宽度决定显示多少个导航项 - if (width >= 768) { - setVisibleItems(navItems.length); // 大屏幕显示所有 - } else if (width >= 480) { - // 中等屏幕:如果导航项超过3个,显示3个,否则显示全部 - setVisibleItems(Math.min(3, navItems.length)); - } else { - // 小屏幕:如果导航项超过2个,显示2个,否则显示全部 - setVisibleItems(Math.min(2, navItems.length)); - } - }; - - handleResize(); // 初始调用 - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - // 计算哪些导航项应该显示,哪些应该折叠 - const visibleNavItems = navItems.slice(0, visibleItems); - const collapsedNavItems = navItems.slice(visibleItems); - return ( + - - - - {visibleNavItems.map((item) => ( - - (isActive ? 'nav-link active' : 'nav-link')} - onClick={() => isMobile && setIsMenuOpen(false)} - > - {item.label} - - - ))} - - {/* 折叠的导航项 */} - {collapsedNavItems.length > 0 && ( - - - {isMenuOpen && - collapsedNavItems.map((item) => ( - (isActive ? 'nav-link active' : 'nav-link')} - onClick={() => setIsMenuOpen(false)} - > - {item.label} - - ))} - - setIsMenuOpen(!isMenuOpen)} - aria-label={isMenuOpen ? '收起菜单' : '展开菜单'} - > - {isMenuOpen ? '×' : '☰'} - {collapsedNavItems.length > 0 && !isMenuOpen && ( - +{collapsedNavItems.length} - )} - - - )} - - - + {navItems.map((item) => ( diff --git a/entrypoints/popup/components/Navbar.tsx b/entrypoints/popup/components/Navbar.tsx new file mode 100644 index 0000000..8e190eb --- /dev/null +++ b/entrypoints/popup/components/Navbar.tsx @@ -0,0 +1,97 @@ +import { NavLink } from 'react-router-dom'; +import { useState, useEffect, ReactNode } from 'react'; + +interface RouteItem { + path: string; + label: string; + element: ReactNode; +} + +interface NavbarProps { + items?: RouteItem[]; +} + +function Navbar({ items = [] }: NavbarProps) { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [visibleItems, setVisibleItems] = useState(items.length); + + // 检测屏幕尺寸变化 + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + setIsMobile(width < 768); + + // 根据屏幕宽度决定显示多少个导航项 + if (width >= 768) { + setVisibleItems(items.length); // 大屏幕显示所有 + } else if (width >= 480) { + // 中等屏幕:如果导航项超过3个,显示3个,否则显示全部 + setVisibleItems(Math.min(3, items.length)); + } else { + // 小屏幕:如果导航项超过2个,显示2个,否则显示全部 + setVisibleItems(Math.min(2, items.length)); + } + }; + + handleResize(); // 初始调用 + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [items.length]); // 添加依赖,如果 items 长度变化也需要重新计算 + + // 计算哪些导航项应该显示,哪些应该折叠 + const visibleNavItems = items.slice(0, visibleItems); + const collapsedNavItems = items.slice(visibleItems); + + return ( + + + + {/* 显示的导航项 */} + {visibleNavItems.map((item) => ( + + (isActive ? 'nav-link active' : 'nav-link')} + onClick={() => isMobile && setIsMenuOpen(false)} + > + {item?.label} + + + ))} + + {/* 折叠区域逻辑 */} + {collapsedNavItems.length > 0 && ( + + + {isMenuOpen && + collapsedNavItems.map((item) => ( + (isActive ? 'nav-link active' : 'nav-link')} + onClick={() => setIsMenuOpen(false)} + > + {item?.label} + + ))} + + setIsMenuOpen(!isMenuOpen)} + aria-label={isMenuOpen ? '收起菜单' : '展开菜单'} + > + {isMenuOpen ? '×' : '☰'} + {collapsedNavItems.length > 0 && !isMenuOpen && ( + +{collapsedNavItems.length} + )} + + + )} + + + + ); +} + +export default Navbar; diff --git a/entrypoints/popup/components/RoutePersistence.tsx b/entrypoints/popup/components/RoutePersistence.tsx new file mode 100644 index 0000000..c6e2c4a --- /dev/null +++ b/entrypoints/popup/components/RoutePersistence.tsx @@ -0,0 +1,45 @@ +import { useEffect, useRef } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { storage } from '@/utils/storage'; + +const RoutePersistence = () => { + const location = useLocation(); + const navigate = useNavigate(); + + const isRestored = useRef(false); + + useEffect(() => { + const restoreRoute = async () => { + if (isRestored.current) return; + + try { + const lastRoute = await storage.get('app/lastRoute'); + + if (lastRoute && lastRoute !== '/' && location.pathname === '/') { + navigate(lastRoute, { replace: true }); + console.log('跳转路由', lastRoute); + } + } catch (err) { + console.error('恢复路由失败', err); + } finally { + isRestored.current = true; + } + }; + + restoreRoute(); + }, [location, navigate]); + + useEffect(() => { + const saveRoute = async () => { + if (!isRestored.current) return; + await storage.set('app/lastRoute', location.pathname); + console.log('保存路由', location.pathname); + }; + + saveRoute(); + }, [location]); + + return null; +}; + +export default RoutePersistence; diff --git a/types/storage.d.ts b/types/storage.d.ts new file mode 100644 index 0000000..71271cf --- /dev/null +++ b/types/storage.d.ts @@ -0,0 +1,4 @@ +export interface StorageSchema { + 'app/lastRoute': string; + 'app/theme': string; +} diff --git a/utils/storage.ts b/utils/storage.ts new file mode 100644 index 0000000..be0ce2c --- /dev/null +++ b/utils/storage.ts @@ -0,0 +1,43 @@ +import { StorageSchema } from 'types/storage'; + +class StorageUtils { + async get(key: K): Promise; + + async get( + key: K, + defaultValue?: StorageSchema[K], + ): Promise; + + /** + * 获取值 + * @param key + * @param defaultValue + * @returns + */ + async get( + key: K, + defaultValue?: StorageSchema[K], + ): Promise { + const result = await chrome.storage.local.get([key]); + return (result[key] ?? defaultValue) as StorageSchema[K] | undefined; + } + + /** + * 设置值 + * @param key + * @param value + */ + async set(key: K, value: StorageSchema[K]): Promise { + await chrome.storage.local.set({ [key]: value }); + } + + /** + * 删除值 + * @param key + */ + async remove(key: keyof StorageSchema): Promise { + await chrome.storage.local.remove([key]); + } +} + +export const storage = new StorageUtils();