支持 app.ini 配置与系统服务安装,并优化前端布局与无障碍体验。
引入类 Gitea 的 app.ini、Windows Service/systemd 控制;前端增加侧栏抽屉、回到顶部、标签输入与浮层 a11y。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard, FolderKanban, FileText, MessageSquare, Users, Settings, ArrowLeft,
|
||||
LayoutDashboard, FolderKanban, FileText, MessageSquare, Users, Settings, ArrowLeft, Moon, Sun, Menu, X,
|
||||
} from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { useTheme, useMediaQuery } from '../hooks/useTheme';
|
||||
import { useOverlayA11y } from '../hooks/useOverlayA11y';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { cn } from '@/lib/utils';
|
||||
import BackToTop from '../components/BackToTop';
|
||||
|
||||
const NAV = [
|
||||
{ to: '/admin/dashboard', label: '仪表盘', icon: LayoutDashboard },
|
||||
@@ -20,7 +23,17 @@ const NAV = [
|
||||
/** React 管理后台布局,与前台 SPA 风格统一 */
|
||||
export default function AdminLayout() {
|
||||
const { user, loading } = useAuth();
|
||||
const { theme, toggle } = useTheme();
|
||||
const isNarrow = useMediaQuery('(max-width: 768px)');
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
const nav = useNavigate();
|
||||
const drawerRef = useRef<HTMLElement>(null);
|
||||
const closeRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const closeNav = useCallback(() => setNavOpen(false), []);
|
||||
useOverlayA11y(isNarrow && navOpen, closeNav, drawerRef, {
|
||||
initialFocusRef: closeRef,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
@@ -34,15 +47,50 @@ export default function AdminLayout() {
|
||||
}
|
||||
}, [user, loading, nav]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNarrow) setNavOpen(false);
|
||||
}, [isNarrow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(isNarrow && navOpen)) return;
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = prev; };
|
||||
}, [isNarrow, navOpen]);
|
||||
|
||||
if (loading) {
|
||||
return <div className="flex justify-center py-24"><Spinner size="lg" /></div>;
|
||||
}
|
||||
if (!user || user.role !== 'admin') return null;
|
||||
|
||||
const navLinks = NAV.map(({ to, label, icon: Icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) => cn('admin-nav-item', isActive && 'active')}
|
||||
onClick={closeNav}
|
||||
>
|
||||
<Icon size={16} aria-hidden />
|
||||
{label}
|
||||
</NavLink>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="admin-shell">
|
||||
<header className="admin-topbar">
|
||||
<div className="admin-topbar-brand">
|
||||
{isNarrow && (
|
||||
<button
|
||||
type="button"
|
||||
className="header-icon-btn"
|
||||
aria-label={navOpen ? '关闭导航' : '打开导航'}
|
||||
aria-expanded={navOpen}
|
||||
aria-controls="admin-nav-drawer"
|
||||
onClick={() => setNavOpen(v => !v)}
|
||||
>
|
||||
{navOpen ? <X size={18} aria-hidden /> : <Menu size={18} aria-hidden />}
|
||||
</button>
|
||||
)}
|
||||
<div className="admin-topbar-mark">姜</div>
|
||||
<div>
|
||||
<div className="admin-topbar-title">姜十三论坛</div>
|
||||
@@ -50,48 +98,77 @@ export default function AdminLayout() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-topbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="header-icon-btn"
|
||||
onClick={toggle}
|
||||
aria-label={theme === 'light' ? '切换暗色模式' : '切换亮色模式'}
|
||||
title={theme === 'light' ? '切换暗色模式' : '切换亮色模式'}
|
||||
>
|
||||
{theme === 'light' ? <Moon size={18} aria-hidden /> : <Sun size={18} aria-hidden />}
|
||||
</button>
|
||||
<button type="button" className="admin-link-btn" onClick={() => nav('/')}>
|
||||
<ArrowLeft size={16} />
|
||||
返回论坛
|
||||
<ArrowLeft size={16} aria-hidden />
|
||||
{!isNarrow && '返回论坛'}
|
||||
</button>
|
||||
<span className="admin-topbar-user">{user.nickname}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="admin-body">
|
||||
<aside className="admin-sidebar">
|
||||
{NAV.map(({ to, label, icon: Icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) => cn('admin-nav-item', isActive && 'active')}
|
||||
>
|
||||
<Icon size={16} />
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</aside>
|
||||
{!isNarrow && (
|
||||
<aside className="admin-sidebar">
|
||||
{navLinks}
|
||||
</aside>
|
||||
)}
|
||||
<main className="admin-main">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{isNarrow && navOpen && (
|
||||
<div className="admin-nav-drawer-root">
|
||||
<button
|
||||
type="button"
|
||||
className="aside-drawer-backdrop"
|
||||
aria-label="关闭导航"
|
||||
tabIndex={-1}
|
||||
onClick={closeNav}
|
||||
/>
|
||||
<aside
|
||||
id="admin-nav-drawer"
|
||||
ref={drawerRef}
|
||||
className="admin-nav-drawer"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="管理导航"
|
||||
>
|
||||
<div className="admin-nav-drawer-head">
|
||||
<span>管理导航</span>
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
className="header-icon-btn"
|
||||
aria-label="关闭"
|
||||
onClick={closeNav}
|
||||
>
|
||||
<X size={18} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
<nav className="admin-nav-drawer-body">
|
||||
{navLinks}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BackToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 管理页通用权限守卫 */
|
||||
/** 管理页就绪状态(鉴权由 AdminLayout 负责,此处不再重复跳转) */
|
||||
export function useAdminGuard() {
|
||||
const { user, loading } = useAuth();
|
||||
const nav = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (!user) nav('/login');
|
||||
else if (user.role !== 'admin') {
|
||||
notify.warning('需要管理员权限');
|
||||
nav('/');
|
||||
}
|
||||
}, [user, loading, nav]);
|
||||
|
||||
return { user, loading, ready: !loading && !!user && user.role === 'admin' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user