feat: 添加导航栏组件,优化路由持久化逻辑,更新存储工具类
This commit is contained in:
@@ -5,6 +5,7 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 隐藏body滚动条但允许滚动 */
|
/* 隐藏body滚动条但允许滚动 */
|
||||||
@@ -254,7 +255,6 @@ body {
|
|||||||
.nav-list {
|
.nav-list {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当菜单展开时,允许换行并显示所有可见项 */
|
/* 当菜单展开时,允许换行并显示所有可见项 */
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { HashRouter as Router, Routes, Route, NavLink } from 'react-router-dom';
|
// App.js
|
||||||
import { useState, useEffect } from 'react';
|
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import TimestampPage from './pages/TimestampPage';
|
import TimestampPage from './pages/TimestampPage';
|
||||||
import RecordeReplayPage from './pages/RecordeReplayPage';
|
import RecordeReplayPage from './pages/RecordeReplayPage';
|
||||||
|
import Navbar from './components/Navbar';
|
||||||
|
import RoutePersistence from './components/RoutePersistence';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
// 路由配置数据
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/', label: '时间戳', element: <TimestampPage /> },
|
{ path: '/', label: '时间戳', element: <TimestampPage /> },
|
||||||
// {path: '/dzmy', label: '电子木鱼', element: <ElectronicWoodenFishPage/>},
|
// {path: '/dzmy', label: '电子木鱼', element: <ElectronicWoodenFishPage/>},
|
||||||
@@ -11,86 +14,11 @@ const navItems = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
|
<RoutePersistence />
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<nav className="nav">
|
<Navbar items={navItems} />
|
||||||
<div className="nav-content">
|
|
||||||
<ul className={`nav-list ${isMenuOpen ? 'open' : ''}`}>
|
|
||||||
{visibleNavItems.map((item) => (
|
|
||||||
<li key={item.path}>
|
|
||||||
<NavLink
|
|
||||||
to={item.path}
|
|
||||||
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
|
|
||||||
onClick={() => isMobile && setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 折叠的导航项 */}
|
|
||||||
{collapsedNavItems.length > 0 && (
|
|
||||||
<li className="nav-collapse-item">
|
|
||||||
<div className={`nav-collapse-content ${isMenuOpen ? 'show' : ''}`}>
|
|
||||||
{isMenuOpen &&
|
|
||||||
collapsedNavItems.map((item) => (
|
|
||||||
<NavLink
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="nav-toggle"
|
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
||||||
aria-label={isMenuOpen ? '收起菜单' : '展开菜单'}
|
|
||||||
>
|
|
||||||
<span className="nav-toggle-icon">{isMenuOpen ? '×' : '☰'}</span>
|
|
||||||
{collapsedNavItems.length > 0 && !isMenuOpen && (
|
|
||||||
<span className="nav-collapse-count">+{collapsedNavItems.length}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<nav className="nav">
|
||||||
|
<div className="nav-content">
|
||||||
|
<ul className={`nav-list ${isMenuOpen ? 'open' : ''}`}>
|
||||||
|
{/* 显示的导航项 */}
|
||||||
|
{visibleNavItems.map((item) => (
|
||||||
|
<li key={item?.path}>
|
||||||
|
<NavLink
|
||||||
|
to={item?.path}
|
||||||
|
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
|
||||||
|
onClick={() => isMobile && setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{item?.label}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 折叠区域逻辑 */}
|
||||||
|
{collapsedNavItems.length > 0 && (
|
||||||
|
<li className="nav-collapse-item">
|
||||||
|
<div className={`nav-collapse-content ${isMenuOpen ? 'show' : ''}`}>
|
||||||
|
{isMenuOpen &&
|
||||||
|
collapsedNavItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item?.path}
|
||||||
|
to={item?.path}
|
||||||
|
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{item?.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="nav-toggle"
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
aria-label={isMenuOpen ? '收起菜单' : '展开菜单'}
|
||||||
|
>
|
||||||
|
<span className="nav-toggle-icon">{isMenuOpen ? '×' : '☰'}</span>
|
||||||
|
{collapsedNavItems.length > 0 && !isMenuOpen && (
|
||||||
|
<span className="nav-collapse-count">+{collapsedNavItems.length}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
@@ -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;
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export interface StorageSchema {
|
||||||
|
'app/lastRoute': string;
|
||||||
|
'app/theme': string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { StorageSchema } from 'types/storage';
|
||||||
|
|
||||||
|
class StorageUtils {
|
||||||
|
async get<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]>;
|
||||||
|
|
||||||
|
async get<K extends keyof StorageSchema>(
|
||||||
|
key: K,
|
||||||
|
defaultValue?: StorageSchema[K],
|
||||||
|
): Promise<StorageSchema[K] | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取值
|
||||||
|
* @param key
|
||||||
|
* @param defaultValue
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async get<K extends keyof StorageSchema>(
|
||||||
|
key: K,
|
||||||
|
defaultValue?: StorageSchema[K],
|
||||||
|
): Promise<StorageSchema[K] | undefined> {
|
||||||
|
const result = await chrome.storage.local.get([key]);
|
||||||
|
return (result[key] ?? defaultValue) as StorageSchema[K] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置值
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
async set<K extends keyof StorageSchema>(key: K, value: StorageSchema[K]): Promise<void> {
|
||||||
|
await chrome.storage.local.set({ [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除值
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
async remove(key: keyof StorageSchema): Promise<void> {
|
||||||
|
await chrome.storage.local.remove([key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const storage = new StorageUtils();
|
||||||
Reference in New Issue
Block a user