feat(mobile): 小屏收起侧栏,页头菜单按钮打开分组导航抽屉
- ≤820 隐藏整个侧栏(资料卡 / 本地服务 / 导航都收起),主内容直接顶上来 - 页头新增只在手机显示的菜单按钮,打开左侧抽屉:分组导航 Menu,选中后自动关闭并跳转 - 抽屉带遮罩、Esc 关闭、打开时锁住背后滚动;桌面不显示按钮
This commit is contained in:
+69
-3
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user