feat: 站长保护(逃生通道写死 admin + 站长帖仅本人可删改)+ 移动端汉堡菜单 + 站长帖操作显隐

This commit is contained in:
2026-08-12 20:52:16 +08:00
parent e097fa260b
commit 5c220f7ecf
6 changed files with 287 additions and 59 deletions
+92 -1
View File
@@ -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>