feat: 站长保护(逃生通道写死 admin + 站长帖仅本人可删改)+ 移动端汉堡菜单 + 站长帖操作显隐
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
import { useTheme } from '../theme.jsx';
|
||||
import * as authApi from '../api/auth.js';
|
||||
@@ -23,6 +23,27 @@ export default function Layout() {
|
||||
const [version, setVersion] = useState('');
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
// ── 移动端汉堡抽屉 ──
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menuBtnRef = useRef(null);
|
||||
const drawerRef = useRef(null);
|
||||
|
||||
// 抽屉焦点管理:打开锁定背景滚动并聚焦首个可聚焦项,关闭还原滚动并聚焦汉堡按钮
|
||||
useEffect(() => {
|
||||
if (menuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
const first = drawerRef.current && drawerRef.current.querySelector('a, button, [tabindex]');
|
||||
if (first && typeof first.focus === 'function') first.focus();
|
||||
else if (drawerRef.current) drawerRef.current.focus();
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
if (menuBtnRef.current && typeof menuBtnRef.current.focus === 'function') menuBtnRef.current.focus();
|
||||
}
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}, [menuOpen]);
|
||||
|
||||
const closeMenu = () => setMenuOpen(false);
|
||||
|
||||
// 初始化向导:未完成安装时跳转 /setup.html(迁移自 nav.js)
|
||||
useEffect(() => {
|
||||
fetch('/api/setup/status')
|
||||
@@ -149,6 +170,17 @@ export default function Layout() {
|
||||
<>
|
||||
<nav className={navClass} id="mainNav">
|
||||
<div className="nav-left">
|
||||
<button
|
||||
ref={menuBtnRef}
|
||||
type="button"
|
||||
className="btn-icon nav-hamburger"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
aria-label="打开导航菜单"
|
||||
aria-expanded={menuOpen}
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<span className="material-icons">menu</span>
|
||||
</button>
|
||||
<Link to="/" className="nav-brand">{siteName}</Link>
|
||||
<span className="nav-version">v{version}</span>
|
||||
<div className="nav-tabs">
|
||||
@@ -196,6 +228,65 @@ export default function Layout() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* 移动端导航抽屉(汉堡菜单) */}
|
||||
<div className={'drawer-overlay' + (menuOpen ? ' open' : '')} onClick={closeMenu} aria-hidden="true" />
|
||||
<aside
|
||||
ref={drawerRef}
|
||||
className={'drawer' + (menuOpen ? ' open' : '')}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="导航菜单"
|
||||
aria-hidden={!menuOpen}
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => { if (e.key === 'Escape') closeMenu(); }}
|
||||
>
|
||||
<div className="drawer-header">
|
||||
<span className="drawer-brand">{siteName}</span>
|
||||
<button type="button" className="btn-icon" onClick={closeMenu} aria-label="关闭菜单">
|
||||
<span className="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav className="drawer-menu" aria-label="页面导航">
|
||||
{tabs.filter((t) => t.show).map((t) =>
|
||||
t.fullPage ? (
|
||||
<a key={t.to} href={t.to} className="drawer-item" onClick={closeMenu}>{t.label}</a>
|
||||
) : (
|
||||
<NavLink
|
||||
key={t.to}
|
||||
to={t.to}
|
||||
end={t.end}
|
||||
className={({ isActive }) => 'drawer-item' + (isActive ? ' active' : '')}
|
||||
onClick={closeMenu}
|
||||
>
|
||||
{t.label}
|
||||
</NavLink>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
<div className="drawer-section">
|
||||
<div className="drawer-section-title">账号</div>
|
||||
{user ? (
|
||||
<>
|
||||
<Link to="/profile.html" className="drawer-item" onClick={closeMenu}>个人中心</Link>
|
||||
<button type="button" className="drawer-item drawer-item-btn" onClick={() => { closeMenu(); handleLogout(); }}>
|
||||
退出登录
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/login.html" className="drawer-item" onClick={closeMenu}>登录</Link>
|
||||
<Link to="/register.html" className="drawer-item" onClick={closeMenu}>注册</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="drawer-footer">
|
||||
<button type="button" className="btn btn-tonal btn-sm" onClick={() => { closeMenu(); handleToggleTheme(); }}>
|
||||
<span className="material-icons" style={{ fontSize: 16 }}>{theme === 'dark' ? 'light_mode' : 'dark_mode'}</span>
|
||||
{theme === 'dark' ? '切换浅色模式' : '切换深色模式'}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="page">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user