feat(mobile): 小屏收起侧栏,页头菜单按钮打开分组导航抽屉

- ≤820 隐藏整个侧栏(资料卡 / 本地服务 / 导航都收起),主内容直接顶上来
- 页头新增只在手机显示的菜单按钮,打开左侧抽屉:分组导航 Menu,选中后自动关闭并跳转
- 抽屉带遮罩、Esc 关闭、打开时锁住背后滚动;桌面不显示按钮
This commit is contained in:
2026-11-05 20:05:38 +08:00
parent 6e17169ac4
commit 9c8e05c0ee
3 changed files with 134 additions and 7 deletions
+69 -3
View File
@@ -860,9 +860,8 @@
} }
@media (max-width: 820px) { @media (max-width: 820px) {
/* 手机上只留分组导航:资料卡 / 本地服务卡太占地,导航还留着方便进分组 */ /* 手机上侧栏整体收起,分组导航改由页头的菜单按钮打开抽屉 */
#page-scroll > aside .profile-card, #page-scroll > aside {
#page-scroll > aside .service-panel {
display: none; display: none;
} }
@@ -1657,3 +1656,70 @@ html.theme-transition .site-background {
display: block; display: block;
} }
} }
/* ---------- 小屏分组导航抽屉 ---------- */
/* 桌面不显示菜单按钮(抽屉只能从小屏页头打开) */
.header-nav-toggle {
display: none;
}
.nav-drawer {
position: fixed;
inset: 0;
z-index: calc(var(--su-z-header) + 10);
display: flex;
}
.nav-drawer-backdrop {
position: absolute;
inset: 0;
border: none;
background: rgba(0, 0, 0, 0.45);
cursor: pointer;
}
.nav-drawer-panel {
position: relative;
display: flex;
flex-direction: column;
gap: var(--su-space-3);
width: min(86vw, 340px);
height: 100%;
margin-right: auto;
padding: var(--su-space-4);
background-color: var(--su-bg-card);
box-shadow: var(--su-shadow-pop);
overflow-y: auto;
animation: nav-drawer-in 180ms var(--su-ease-out);
}
.nav-drawer-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--su-space-2);
}
@keyframes nav-drawer-in {
from {
transform: translateX(-100%);
}
to {
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.nav-drawer-panel {
animation: none;
}
}
@media (max-width: 820px) {
/* 手机页头才显示菜单按钮(必须写在上面的 display: none 之后) */
.header-nav-toggle {
display: inline-flex;
}
}
+54 -4
View File
@@ -3,11 +3,14 @@ import {
Card, Card,
Empty, Empty,
EngineIcon, EngineIcon,
Icon,
IconButton,
Layout, Layout,
Menu, Menu,
Notice, Notice,
SearchField, SearchField,
Space, Space,
Title,
Toaster, Toaster,
type EngineIconName, type EngineIconName,
type MenuItem, type MenuItem,
@@ -240,6 +243,8 @@ export default function AppNext() {
const [loginOpen, setLoginOpen] = useState(false) const [loginOpen, setLoginOpen] = useState(false)
const [editingKey, setEditingKey] = useState<string | null>(null) const [editingKey, setEditingKey] = useState<string | null>(null)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
/** 小屏的分组导航抽屉 */
const [navOpen, setNavOpen] = useState(false)
const searchRef = useRef<HTMLInputElement>(null) const searchRef = useRef<HTMLInputElement>(null)
const [searchEngineId, setSearchEngineId] = useState<string | null>(readSearchEngine) const [searchEngineId, setSearchEngineId] = useState<string | null>(readSearchEngine)
const [engineOpen, setEngineOpen] = useState(false) const [engineOpen, setEngineOpen] = useState(false)
@@ -552,6 +557,29 @@ export default function AppNext() {
? `g:${route.key}` ? `g:${route.key}`
: `g:${route.key}` : `g:${route.key}`
/** 侧栏与手机抽屉共用:选完分组就关掉抽屉 */
const selectNav = (key: string) => {
setNavOpen(false)
if (key === 'home') navigate('/')
else navigate(`/g/${encodeURIComponent(key.slice(2))}`)
}
useEffect(() => {
if (!navOpen) return
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') setNavOpen(false)
}
// 抽屉打开时锁住背后的滚动
const scroll = document.getElementById('page-scroll')
const previous = scroll?.style.overflowY ?? ''
if (scroll !== null) scroll.style.overflowY = 'hidden'
window.addEventListener('keydown', onKey)
return () => {
window.removeEventListener('keydown', onKey)
if (scroll !== null) scroll.style.overflowY = previous
}
}, [navOpen])
function renderEntries( function renderEntries(
group: GroupNode, group: GroupNode,
entries: Record<string, unknown>[], entries: Record<string, unknown>[],
@@ -886,6 +914,7 @@ export default function AppNext() {
void logout().then(() => setSession({ authenticated: false, has_admin: true })) void logout().then(() => setSession({ authenticated: false, has_admin: true }))
}} }}
onSettings={() => window.location.assign('/admin')} onSettings={() => window.location.assign('/admin')}
onOpenNav={() => setNavOpen(true)}
/> />
} }
toolbar={ toolbar={
@@ -983,10 +1012,7 @@ export default function AppNext() {
items={sidebarItems} items={sidebarItems}
activeKey={activeKey} activeKey={activeKey}
itemHeight={39} itemHeight={39}
onSelect={(key) => { onSelect={selectNav}
if (key === 'home') navigate('/')
else navigate(`/g/${encodeURIComponent(key.slice(2))}`)
}}
/> />
</div> </div>
)} )}
@@ -1015,6 +1041,30 @@ export default function AppNext() {
</Layout> </Layout>
<BackToTop visible={scrolled} targetId="page-scroll" /> <BackToTop visible={scrolled} targetId="page-scroll" />
{navOpen && (
<div className="nav-drawer">
<button
type="button"
className="nav-drawer-backdrop"
aria-label="关闭分组导航"
onClick={() => setNavOpen(false)}
/>
<div className="nav-drawer-panel" role="dialog" aria-modal="true" aria-label="分组导航">
<div className="nav-drawer-head">
<Title level={4} as="h2" weight="semibold">
分组导航
</Title>
<IconButton
label="关闭分组导航"
icon={<Icon name="close_line" />}
onClick={() => setNavOpen(false)}
/>
</div>
<Menu items={sidebarItems} activeKey={activeKey} itemHeight={44} onSelect={selectNav} />
</div>
</div>
)}
</> </>
) )
} }
@@ -39,6 +39,8 @@ type SiteHeaderProps = {
onLogin: () => void onLogin: () => void
onLogout: () => void onLogout: () => void
onSettings: () => void onSettings: () => void
/** 小屏打开分组导航抽屉;不传则不显示菜单按钮 */
onOpenNav?: () => void
} }
const tagTone: Record<SettingsTone, 'neutral' | 'primary' | 'success'> = { const tagTone: Record<SettingsTone, 'neutral' | 'primary' | 'success'> = {
@@ -67,6 +69,7 @@ export function SiteHeader({
onLogin, onLogin,
onLogout, onLogout,
onSettings, onSettings,
onOpenNav,
}: SiteHeaderProps) { }: SiteHeaderProps) {
const context = { count: projectCount, project: sampleProject, live } const context = { count: projectCount, project: sampleProject, live }
@@ -97,6 +100,14 @@ export function SiteHeader({
</div> </div>
<div className="header-actions"> <div className="header-actions">
{onOpenNav !== undefined && (
<IconButton
className="header-nav-toggle"
label="分组导航"
icon={<Icon name="menu_line" />}
onClick={onOpenNav}
/>
)}
{ready && ( {ready && (
<> <>
{site.actions.theme && ( {site.actions.theme && (