fix(mobile): replace top tab strip with hamburger drawer
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

The previous top tab strip ate vertical space and didn't match the
desktop sidebar UX. Add a Menu button next to the title (mobile only)
that slides the existing Sidebar in from the left as a drawer, with a
backdrop, scroll lock, and auto-close on selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-29 18:28:39 +08:00
parent d76a64a0d7
commit 4df06babd0
3 changed files with 117 additions and 68 deletions

View File

@@ -1,11 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Dashboard } from './components/Dashboard'; import { Dashboard } from './components/Dashboard';
import { Sidebar, sidebarMenuItems } from './components/Sidebar'; import { Sidebar } from './components/Sidebar';
import { Header } from './components/Header'; import { Header } from './components/Header';
import { cn } from './lib/utils';
export default function App() { export default function App() {
const [currentView, setCurrentView] = useState('overall'); const [currentView, setCurrentView] = useState('overall');
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const viewTitles: Record<string, string> = { const viewTitles: Record<string, string> = {
'overall': '全网精细化运营大盘', 'overall': '全网精细化运营大盘',
@@ -15,31 +15,17 @@ export default function App() {
return ( return (
<div className="flex h-screen bg-slate-50 overflow-hidden font-sans"> <div className="flex h-screen bg-slate-50 overflow-hidden font-sans">
<Sidebar currentView={currentView} setCurrentView={setCurrentView} /> <Sidebar
currentView={currentView}
setCurrentView={setCurrentView}
mobileOpen={mobileNavOpen}
onMobileClose={() => setMobileNavOpen(false)}
/>
<div className="flex flex-col flex-1 overflow-hidden"> <div className="flex flex-col flex-1 overflow-hidden">
<Header title={viewTitles[currentView] || '数据分析'} /> <Header
<nav className="md:hidden bg-white border-b border-slate-200 px-2 shrink-0 overflow-x-auto"> title={viewTitles[currentView] || '数据分析'}
<div className="flex gap-1 min-w-max"> onMenuClick={() => setMobileNavOpen(true)}
{sidebarMenuItems.map((item) => { />
const active = currentView === item.id;
return (
<button
key={item.id}
onClick={() => setCurrentView(item.id)}
className={cn(
"flex items-center gap-1.5 px-3 py-2.5 text-xs font-semibold whitespace-nowrap border-b-2 transition-colors",
active
? "text-indigo-600 border-indigo-500"
: "text-slate-500 border-transparent active:text-slate-700"
)}
>
<item.icon className={cn("w-4 h-4", active ? "text-indigo-500" : "text-slate-400")} />
{item.name}
</button>
);
})}
</div>
</nav>
<main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8"> <main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
<Dashboard currentView={currentView} /> <Dashboard currentView={currentView} />
</main> </main>

View File

@@ -1,11 +1,20 @@
import React from 'react'; import React from 'react';
import { Bell, Search, UserCircle, ChevronDown } from 'lucide-react'; import { Bell, Search, UserCircle, ChevronDown, Menu } from 'lucide-react';
export function Header({ title }: { title: string }) { export function Header({ title, onMenuClick }: { title: string; onMenuClick?: () => void }) {
return ( return (
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-4 md:px-6 shrink-0 z-10 w-full transition-all"> <header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-4 md:px-6 shrink-0 z-10 w-full transition-all">
<div className="flex items-center"> <div className="flex items-center min-w-0">
<h1 className="text-lg font-bold text-slate-800 mr-8 tracking-tight">{title}</h1> {onMenuClick && (
<button
onClick={onMenuClick}
aria-label="打开导航菜单"
className="md:hidden mr-2 -ml-1 p-2 text-slate-600 hover:bg-slate-100 active:bg-slate-200 rounded-lg transition-colors shrink-0"
>
<Menu className="w-5 h-5" />
</button>
)}
<h1 className="text-lg font-bold text-slate-800 mr-8 tracking-tight truncate">{title}</h1>
<div className="hidden md:flex items-center bg-slate-100 px-3 py-2 rounded-lg w-64 focus-within:ring-2 focus-within:ring-indigo-500/20 border border-transparent focus-within:border-indigo-500/50 transition-all"> <div className="hidden md:flex items-center bg-slate-100 px-3 py-2 rounded-lg w-64 focus-within:ring-2 focus-within:ring-indigo-500/20 border border-transparent focus-within:border-indigo-500/50 transition-all">
<Search className="w-4 h-4 text-slate-400 mr-2 shrink-0" /> <Search className="w-4 h-4 text-slate-400 mr-2 shrink-0" />
<input <input

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React, { useEffect } from 'react';
import { BarChart3, Calendar, Fuel, Users } from 'lucide-react'; import { BarChart3, Calendar, Fuel, Users, X } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
export const sidebarMenuItems = [ export const sidebarMenuItems = [
@@ -8,20 +8,73 @@ export const sidebarMenuItems = [
{ id: 'efficiency', name: '站点效能监控', icon: BarChart3 }, { id: 'efficiency', name: '站点效能监控', icon: BarChart3 },
]; ];
export function Sidebar({ currentView, setCurrentView }: { currentView: string; setCurrentView: (v: string) => void }) { type SidebarProps = {
currentView: string;
setCurrentView: (v: string) => void;
mobileOpen?: boolean;
onMobileClose?: () => void;
};
export function Sidebar({ currentView, setCurrentView, mobileOpen = false, onMobileClose }: SidebarProps) {
useEffect(() => {
if (!mobileOpen) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = prev;
};
}, [mobileOpen]);
const handleSelect = (id: string) => {
setCurrentView(id);
onMobileClose?.();
};
return ( return (
<aside className="w-64 bg-slate-900 border-r border-slate-800 hidden md:flex flex-col text-slate-300 shadow-xl overflow-hidden relative"> <>
{/* Mobile backdrop */}
<div
className={cn(
"md:hidden fixed inset-0 z-30 bg-slate-900/60 backdrop-blur-sm transition-opacity",
mobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"
)}
onClick={onMobileClose}
aria-hidden="true"
/>
<aside
className={cn(
"bg-slate-900 border-r border-slate-800 flex flex-col text-slate-300 shadow-xl overflow-hidden relative",
// Desktop: always shown, in-flow
"md:flex md:static md:w-64 md:translate-x-0",
// Mobile: drawer
"fixed inset-y-0 left-0 z-40 w-72 max-w-[80vw] transform transition-transform duration-300 ease-out",
mobileOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
)}
aria-hidden={!mobileOpen ? undefined : false}
>
<div className="absolute top-0 left-0 w-full h-[600px] bg-gradient-to-b from-indigo-500/10 to-transparent pointer-events-none" /> <div className="absolute top-0 left-0 w-full h-[600px] bg-gradient-to-b from-indigo-500/10 to-transparent pointer-events-none" />
<div className="h-16 flex items-center px-6 border-b border-white/5 shrink-0 z-10 bg-slate-900/50 backdrop-blur-md"> <div className="h-16 flex items-center justify-between px-6 border-b border-white/5 shrink-0 z-10 bg-slate-900/50 backdrop-blur-md">
<Fuel className="w-6 h-6 text-indigo-400 mr-2 drop-shadow-md" /> <div className="flex items-center min-w-0">
<span className="text-lg font-bold text-white tracking-tight"> BI</span> <Fuel className="w-6 h-6 text-indigo-400 mr-2 drop-shadow-md shrink-0" />
<span className="text-lg font-bold text-white tracking-tight truncate"> BI</span>
</div>
{onMobileClose && (
<button
onClick={onMobileClose}
aria-label="关闭导航菜单"
className="md:hidden -mr-2 p-2 text-slate-400 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
)}
</div> </div>
<nav className="flex-1 py-6 px-4 space-y-2 z-10 overflow-y-auto"> <nav className="flex-1 py-6 px-4 space-y-2 z-10 overflow-y-auto">
<div className="text-xs font-bold text-slate-500/80 uppercase tracking-wider mb-4 px-3"></div> <div className="text-xs font-bold text-slate-500/80 uppercase tracking-wider mb-4 px-3"></div>
{sidebarMenuItems.map((item) => ( {sidebarMenuItems.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => setCurrentView(item.id)} onClick={() => handleSelect(item.id)}
className={cn( className={cn(
"flex items-center w-full px-3 py-3 rounded-xl text-sm font-semibold transition-all duration-200 text-left group", "flex items-center w-full px-3 py-3 rounded-xl text-sm font-semibold transition-all duration-200 text-left group",
currentView === item.id currentView === item.id
@@ -47,5 +100,6 @@ export function Sidebar({ currentView, setCurrentView }: { currentView: string;
</div> </div>
</div> </div>
</aside> </aside>
</>
); );
} }