fix(mobile): replace top tab strip with hamburger drawer
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
38
src/App.tsx
38
src/App.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user