生产管理系统前端 菜单箭头显示fix
This commit is contained in:
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { LeftSidebar } from './components/LeftSidebar';
|
import { LeftSidebar } from './components/LeftSidebar';
|
||||||
import { MainContent } from './components/MainContent';
|
import { MainContent } from './components/MainContent';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// 菜单项数据结构定义
|
// 菜单项数据结构定义
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
@@ -230,18 +231,23 @@ export function SideBarOld({
|
|||||||
router.push(path);
|
router.push(path);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full bg-gray-100">
|
<div className={cn(
|
||||||
|
"flex h-full bg-background",
|
||||||
|
"min-h-screen"
|
||||||
|
)}>
|
||||||
{/* 左侧导航栏 - 独立滚动 */}
|
{/* 左侧导航栏 - 独立滚动 */}
|
||||||
<div className="sidebarScroll">
|
{!isMobile && (
|
||||||
<LeftSidebar
|
<div className="sidebarScroll">
|
||||||
menus={menus}
|
<LeftSidebar
|
||||||
activePath={currentPath}
|
menus={menus}
|
||||||
onNavigate={handleNavigate}
|
activePath={currentPath}
|
||||||
isMobile={isMobile}
|
onNavigate={handleNavigate}
|
||||||
isCollapsed={!isMobile && isCollapsed}
|
isMobile={isMobile}
|
||||||
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
isCollapsed={isCollapsed}
|
||||||
/>
|
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 右侧主内容 - 独立滚动 */}
|
{/* 右侧主内容 - 独立滚动 */}
|
||||||
<div className="flex-1 contentScroll">
|
<div className="flex-1 contentScroll">
|
||||||
@@ -253,6 +259,20 @@ export function SideBarOld({
|
|||||||
{children}
|
{children}
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 移动端侧边栏 */}
|
||||||
|
{isMobile && (
|
||||||
|
<div className="fixed inset-y-0 left-0 z-50 w-64 sidebarScroll">
|
||||||
|
<LeftSidebar
|
||||||
|
menus={menus}
|
||||||
|
activePath={currentPath}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isCollapsed={false}
|
||||||
|
onToggleCollapse={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ChevronDown, ChevronLeft, ChevronRight, Menu, X } from 'lucide-react';
|
import { ChevronDown, ChevronLeft, ChevronRight, Menu, X } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible';
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -32,35 +39,19 @@ export function LeftSidebar({
|
|||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
onToggleCollapse
|
onToggleCollapse
|
||||||
}: LeftSidebarProps) {
|
}: LeftSidebarProps) {
|
||||||
// 根据activePath自动展开包含该路径的菜单
|
// 初始状态下所有菜单都折叠
|
||||||
const getInitialExpandedMenus = () => {
|
const getInitialExpandedMenus = () => {
|
||||||
const expanded = new Set<string>();
|
return new Set<string>();
|
||||||
menus.forEach(menu => {
|
|
||||||
if (menu.children?.some(child => child.path === activePath)) {
|
|
||||||
expanded.add(menu.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 如果没有匹配的,默认展开第一个
|
|
||||||
if (expanded.size === 0 && menus.length > 0) {
|
|
||||||
expanded.add(menus[0].id);
|
|
||||||
}
|
|
||||||
return expanded;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(getInitialExpandedMenus());
|
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(getInitialExpandedMenus());
|
||||||
|
|
||||||
// 当activePath或menus变化时,自动展开对应的菜单
|
// 不自动展开菜单,由用户手动控制
|
||||||
|
|
||||||
|
// 当侧边栏状态改变时,折叠所有菜单
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
menus.forEach(menu => {
|
setExpandedMenus(new Set());
|
||||||
if (menu.children?.some(child => child.path === activePath)) {
|
}, [isCollapsed]);
|
||||||
setExpandedMenus(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.add(menu.id);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [activePath, menus]);
|
|
||||||
|
|
||||||
const toggleMenu = (menuId: string) => {
|
const toggleMenu = (menuId: string) => {
|
||||||
setExpandedMenus(prev => {
|
setExpandedMenus(prev => {
|
||||||
@@ -77,35 +68,39 @@ export function LeftSidebar({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-white border-r border-gray-200 transition-all duration-300 flex flex-col h-full",
|
"bg-background border-r transition-all duration-300 flex flex-col h-full",
|
||||||
isMobile ? "fixed inset-y-0 left-0 z-50" : "relative",
|
isMobile ? "fixed inset-y-0 left-0 z-50" : "relative",
|
||||||
isCollapsed ? "w-16" : "w-64"
|
isCollapsed ? "w-16" : "w-64"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 头部 - 缩小高度 */}
|
{/* 头部 - 缩小高度 */}
|
||||||
<div className="px-2 py-1 border-b border-gray-200">
|
<div className="px-2 py-1 border-b">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className={cn(
|
<h2 className={cn(
|
||||||
"font-medium text-gray-900 text-sm transition-all duration-300",
|
"font-medium text-sm transition-all duration-300",
|
||||||
isCollapsed ? "hidden" : "block"
|
isCollapsed ? "hidden" : "block"
|
||||||
)}>
|
)}>
|
||||||
导航菜单
|
导航菜单
|
||||||
</h2>
|
</h2>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<X className="w-4 h-4 text-gray-600" />
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
/* 根据侧边栏状态显示不同按钮 */
|
/* 根据侧边栏状态显示不同按钮 */
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
onClick={onToggleCollapse}
|
onClick={onToggleCollapse}
|
||||||
className="p-1 rounded hover:bg-gray-100 transition-colors"
|
className="h-8 w-8"
|
||||||
title={isCollapsed ? "展开菜单" : "收起菜单"}
|
title={isCollapsed ? "展开菜单" : "收起菜单"}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<ChevronRight className="w-4 h-4 text-gray-600" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronLeft className="w-4 h-4 text-gray-600" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,52 +114,93 @@ export function LeftSidebar({
|
|||||||
{menus.map((menu) => (
|
{menus.map((menu) => (
|
||||||
<div key={menu.id}>
|
<div key={menu.id}>
|
||||||
{/* 一级菜单 */}
|
{/* 一级菜单 */}
|
||||||
<button
|
{menu.children ? (
|
||||||
onClick={() => toggleMenu(menu.id)}
|
<Collapsible
|
||||||
className={cn(
|
open={expandedMenus.has(menu.id)}
|
||||||
"w-full flex items-center justify-between px-3 py-2 rounded-md transition-colors text-sm",
|
onOpenChange={(open) => {
|
||||||
"hover:bg-gray-100 hover:text-gray-900",
|
if (open) {
|
||||||
isCollapsed ? "justify-center px-2 py-3" : "px-3 py-2"
|
setExpandedMenus(prev => new Set(prev).add(menu.id));
|
||||||
)}
|
} else {
|
||||||
title={isCollapsed ? menu.label : undefined}
|
setExpandedMenus(prev => {
|
||||||
>
|
const newSet = new Set(prev);
|
||||||
<div className="flex items-center gap-2">
|
newSet.delete(menu.id);
|
||||||
{menu.icon && (
|
return newSet;
|
||||||
<span className="flex-shrink-0">
|
});
|
||||||
{menu.icon}
|
}
|
||||||
</span>
|
}}
|
||||||
)}
|
>
|
||||||
{!isCollapsed && (
|
<CollapsibleTrigger asChild>
|
||||||
<span className="text-gray-700">{menu.label}</span>
|
<Button
|
||||||
)}
|
variant="ghost"
|
||||||
</div>
|
|
||||||
{!isCollapsed && menu.children && (
|
|
||||||
expandedMenus.has(menu.id) ? (
|
|
||||||
<ChevronDown className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 二级菜单 */}
|
|
||||||
{!isCollapsed && menu.children && expandedMenus.has(menu.id) && (
|
|
||||||
<div className="ml-4 mt-1 space-y-1">
|
|
||||||
{menu.children.map((child) => (
|
|
||||||
<button
|
|
||||||
key={child.id}
|
|
||||||
onClick={() => child.path && onNavigate(child.path)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full text-left px-3 py-2 rounded-md transition-colors text-xs",
|
"w-full justify-between text-sm font-normal",
|
||||||
activePath === child.path
|
isCollapsed ? "justify-center px-2 py-3" : "px-3 py-2"
|
||||||
? "bg-green-50 text-green-700 font-medium border-l-2 border-green-600"
|
|
||||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
|
||||||
)}
|
)}
|
||||||
|
title={isCollapsed ? menu.label : undefined}
|
||||||
>
|
>
|
||||||
{child.label}
|
<div className="flex items-center gap-2">
|
||||||
</button>
|
{menu.icon && (
|
||||||
))}
|
<span className="flex-shrink-0">
|
||||||
</div>
|
{menu.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span>{menu.label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{menu.children && (
|
||||||
|
isCollapsed ? null : (
|
||||||
|
<ChevronRight
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 shrink-0 transition-transform duration-200",
|
||||||
|
expandedMenus.has(menu.id) && "rotate-90"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
{/* 二级菜单 */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="ml-4 mt-1 space-y-1">
|
||||||
|
{menu.children.map((child) => (
|
||||||
|
<Button
|
||||||
|
key={child.id}
|
||||||
|
variant={activePath === child.path ? "secondary" : "ghost"}
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-xs font-normal h-8",
|
||||||
|
activePath === child.path && "bg-muted text-muted-foreground font-medium border-l-2 border-primary"
|
||||||
|
)}
|
||||||
|
onClick={() => child.path && onNavigate(child.path)}
|
||||||
|
>
|
||||||
|
{child.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-sm font-normal",
|
||||||
|
isCollapsed ? "justify-center px-2 py-3" : "px-3 py-2"
|
||||||
|
)}
|
||||||
|
title={isCollapsed ? menu.label : undefined}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{menu.icon && (
|
||||||
|
<span className="flex-shrink-0">
|
||||||
|
{menu.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span>{menu.label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -172,9 +208,9 @@ export function LeftSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部 */}
|
{/* 底部 */}
|
||||||
<div className="p-4 border-t border-gray-200">
|
<div className="p-4 border-t">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"text-xs text-gray-500",
|
"text-xs text-muted-foreground",
|
||||||
isCollapsed ? "text-center" : "text-left"
|
isCollapsed ? "text-center" : "text-left"
|
||||||
)}>
|
)}>
|
||||||
{isCollapsed ? "管理" : "管理系统"}
|
{isCollapsed ? "管理" : "管理系统"}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
|
|
||||||
interface MainContentProps {
|
interface MainContentProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -27,22 +29,43 @@ export function MainContent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (isMobile) {
|
||||||
<>
|
return (
|
||||||
{/* 移动端侧边栏遮罩 */}
|
<>
|
||||||
{isMobile && showMobileSidebar && (
|
{/* 移动端菜单按钮 */}
|
||||||
<div
|
<div className="flex items-center justify-between p-4 border-b bg-background">
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
<Button
|
||||||
onClick={() => setShowMobileSidebar(false)}
|
variant="ghost"
|
||||||
/>
|
size="icon"
|
||||||
)}
|
onClick={() => setShowMobileSidebar(true)}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 主内容区域 - 去掉顶部白色横条 */}
|
{/* 移动端侧边栏 */}
|
||||||
<div className="flex-1 flex flex-col">
|
<Sheet open={showMobileSidebar} onOpenChange={setShowMobileSidebar}>
|
||||||
<div className="p-6">
|
<SheetContent side="left" className="p-0 w-64">
|
||||||
|
{/* 这里应该渲染LeftSidebar内容,但需要通过props传递 */}
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-muted-foreground">侧边栏内容</p>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
{/* 主内容区域 */}
|
||||||
|
<div className="flex-1 p-6 bg-background">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col bg-background">
|
||||||
|
<div className="p-6">
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs))
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user