生产管理系统前端 1.修复系统导航过长的问题 2.利用旧菜单交互 开发菜单与导航
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { AppSidebar, AppSidebarProps, SidebarData } from "@/components/app-sidebar"
|
||||
import { AppSidebar, AppSidebarProps } from "@/components/app-sidebar"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
|
||||
276
crop-x/src/components/layouts/SideBar/SideBarOld.tsx
Normal file
276
crop-x/src/components/layouts/SideBar/SideBarOld.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { LeftSidebar } from './components/LeftSidebar';
|
||||
import { MainContent } from './components/MainContent';
|
||||
|
||||
// 菜单项数据结构定义
|
||||
interface NavItem {
|
||||
title: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
isActive?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface SideBarData {
|
||||
navMain: NavItem[];
|
||||
}
|
||||
|
||||
// 内部菜单项结构(用于LeftSidebar组件)
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
children?: {
|
||||
id: string;
|
||||
label: string;
|
||||
path?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface SideBarOldProps {
|
||||
children: React.ReactNode;
|
||||
activePath?: string;
|
||||
onNavigate?: (path: string) => void;
|
||||
data?: SideBarData;
|
||||
}
|
||||
|
||||
const defaultSideBarData: SideBarData = {
|
||||
navMain: [
|
||||
{
|
||||
title: "租户管理",
|
||||
url: "/central-config/tenant",
|
||||
icon: "🏢",
|
||||
items: [
|
||||
{
|
||||
title: "企业审核",
|
||||
url: "/central-config/tenant/enterprise-audit",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "审核历史",
|
||||
url: "/central-config/tenant/audit-history",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "企业信息",
|
||||
url: "/central-config/tenant/enterprise-info",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "用户管理",
|
||||
url: "/central-config/tenant/user-management",
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "用户管理",
|
||||
url: "/central-config/user",
|
||||
icon: "👥",
|
||||
items: [
|
||||
{
|
||||
title: "员工管理",
|
||||
url: "/central-config/user/employee",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "角色管理",
|
||||
url: "/central-config/user/role",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "菜单管理",
|
||||
url: "/central-config/user/menu",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "权限配置管理",
|
||||
url: "/central-config/user/permission",
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "系统参数",
|
||||
url: "/central-config/system",
|
||||
icon: "🔧",
|
||||
items: [
|
||||
{
|
||||
title: "系统设置",
|
||||
url: "/central-config/system/settings",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "分类字典",
|
||||
url: "/central-config/system/category",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "数据字典",
|
||||
url: "/central-config/system/dictionary",
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "系统监控",
|
||||
url: "/central-config/monitor",
|
||||
icon: "📈",
|
||||
items: [
|
||||
{
|
||||
title: "登录日志",
|
||||
url: "/central-config/monitor/login-log",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "操作日志",
|
||||
url: "/central-config/monitor/operation-log",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "性能监控",
|
||||
url: "/central-config/monitor/performance",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "网络日志",
|
||||
url: "/central-config/monitor/network-log",
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "消息中心",
|
||||
url: "/central-config/message",
|
||||
icon: "📨",
|
||||
items: [
|
||||
{
|
||||
title: "消息发送",
|
||||
url: "/central-config/message/send",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "消息模版",
|
||||
url: "/central-config/message/template",
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
title: "消息日志",
|
||||
url: "/central-config/message/log",
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 转换 SideBarData 为 MenuItem 格式的工具函数
|
||||
const convertSideBarDataToMenus = (sideBarData?: SideBarData): MenuItem[] => {
|
||||
if (!sideBarData?.navMain) return [];
|
||||
|
||||
return sideBarData.navMain.map(item => ({
|
||||
id: item.url.replace(/\/[^\/]+/g, '').replace('/', '') || item.title.replace(/\s+/g, '-').toLowerCase(),
|
||||
label: item.title,
|
||||
icon: <span className="text-lg">{item.icon}</span>,
|
||||
children: item.items?.map(child => ({
|
||||
id: child.url.split('/').pop() || child.title.replace(/\s+/g, '-').toLowerCase(),
|
||||
label: child.title,
|
||||
path: child.url,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
export function SideBarOld({
|
||||
children,
|
||||
activePath,
|
||||
onNavigate,
|
||||
data
|
||||
}: SideBarOldProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [currentPath, setCurrentPath] = useState(pathname || activePath || '/machinery/list');
|
||||
|
||||
// 使用传入的数据或默认数据
|
||||
const menus = convertSideBarDataToMenus(data || defaultSideBarData);
|
||||
|
||||
// 检测是否为移动设备
|
||||
useEffect(() => {
|
||||
const checkIsMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
};
|
||||
|
||||
checkIsMobile();
|
||||
window.addEventListener('resize', checkIsMobile);
|
||||
return () => window.removeEventListener('resize', checkIsMobile);
|
||||
}, []);
|
||||
|
||||
// 监听路由变化,同步当前路径
|
||||
useEffect(() => {
|
||||
if (pathname) {
|
||||
setCurrentPath(pathname);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
// 移动端时自动展开侧边栏
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setIsCollapsed(false);
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
setCurrentPath(path);
|
||||
onNavigate?.(path);
|
||||
// 使用 Next.js 标准路由跳转
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
// 获取当前页面的面包屑
|
||||
const getCurrentBreadcrumb = () => {
|
||||
const allItems: { label: string; path?: string }[] = [];
|
||||
|
||||
menus.forEach(menu => {
|
||||
if (menu.children?.some(child => child.path === currentPath)) {
|
||||
allItems.push({ label: menu.label });
|
||||
const activeChild = menu.children.find(child => child.path === currentPath);
|
||||
if (activeChild) {
|
||||
allItems.push({ label: activeChild.label });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return allItems;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100" style={{ height: '100vh' }}>
|
||||
{/* 左侧导航栏 */}
|
||||
<LeftSidebar
|
||||
menus={menus}
|
||||
activePath={currentPath}
|
||||
onNavigate={handleNavigate}
|
||||
isMobile={isMobile}
|
||||
isCollapsed={!isMobile && isCollapsed}
|
||||
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
||||
/>
|
||||
|
||||
{/* 右侧主内容 */}
|
||||
<MainContent
|
||||
breadcrumb={getCurrentBreadcrumb()}
|
||||
isMobile={isMobile}
|
||||
sidebarOpen={!isCollapsed}
|
||||
onToggleSidebar={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
{children}
|
||||
</MainContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
179
crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx
Normal file
179
crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ChevronDown, ChevronRight, Menu, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
children?: {
|
||||
id: string;
|
||||
label: string;
|
||||
path?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface LeftSidebarProps {
|
||||
menus: MenuItem[];
|
||||
activePath: string;
|
||||
onNavigate: (path: string) => void;
|
||||
isMobile?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
onToggleCollapse?: () => void;
|
||||
}
|
||||
|
||||
export function LeftSidebar({
|
||||
menus,
|
||||
activePath,
|
||||
onNavigate,
|
||||
isMobile = false,
|
||||
isCollapsed = false,
|
||||
onToggleCollapse
|
||||
}: LeftSidebarProps) {
|
||||
// 根据activePath自动展开包含该路径的菜单
|
||||
const getInitialExpandedMenus = () => {
|
||||
const expanded = 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());
|
||||
|
||||
// 当activePath或menus变化时,自动展开对应的菜单
|
||||
useEffect(() => {
|
||||
menus.forEach(menu => {
|
||||
if (menu.children?.some(child => child.path === activePath)) {
|
||||
setExpandedMenus(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(menu.id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [activePath, menus]);
|
||||
|
||||
const toggleMenu = (menuId: string) => {
|
||||
setExpandedMenus(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(menuId)) {
|
||||
newSet.delete(menuId);
|
||||
} else {
|
||||
newSet.add(menuId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white border-r border-gray-200 transition-all duration-300 flex flex-col",
|
||||
isMobile ? "fixed inset-y-0 left-0 z-50" : "relative",
|
||||
isCollapsed ? "w-16" : "w-64"
|
||||
)}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className={cn(
|
||||
"font-semibold text-gray-900 transition-all duration-300",
|
||||
isCollapsed ? "hidden" : "block"
|
||||
)}>
|
||||
导航菜单
|
||||
</h2>
|
||||
{isMobile ? (
|
||||
<X className="w-5 h-5 text-gray-600" />
|
||||
) : (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="p-1 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<Menu className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
<div className={cn(
|
||||
"flex-1 overflow-y-auto p-4",
|
||||
isCollapsed ? "p-2" : "p-4"
|
||||
)}>
|
||||
<nav className="space-y-2">
|
||||
{menus.map((menu) => (
|
||||
<div key={menu.id}>
|
||||
{/* 一级菜单 */}
|
||||
<button
|
||||
onClick={() => toggleMenu(menu.id)}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between px-3 py-2 rounded-md transition-colors text-sm",
|
||||
"hover:bg-gray-100 hover:text-gray-900",
|
||||
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 className="text-gray-700">{menu.label}</span>
|
||||
)}
|
||||
</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(
|
||||
"w-full text-left px-3 py-2 rounded-md transition-colors text-xs",
|
||||
activePath === child.path
|
||||
? "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"
|
||||
)}
|
||||
>
|
||||
{child.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 底部 */}
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
<div className={cn(
|
||||
"text-xs text-gray-500",
|
||||
isCollapsed ? "text-center" : "text-left"
|
||||
)}>
|
||||
{isCollapsed ? "管理" : "管理系统"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
crop-x/src/components/layouts/SideBar/components/MainContent.tsx
Normal file
112
crop-x/src/components/layouts/SideBar/components/MainContent.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Menu, X, ChevronRight, Home, FileText, Settings } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MainContentProps {
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
isMobile?: boolean;
|
||||
sidebarOpen?: boolean;
|
||||
onToggleSidebar?: () => void;
|
||||
breadcrumb?: {
|
||||
label: string;
|
||||
path?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function MainContent({
|
||||
title = "当前页面",
|
||||
children,
|
||||
isMobile = false,
|
||||
sidebarOpen = false,
|
||||
onToggleSidebar,
|
||||
breadcrumb = []
|
||||
}: MainContentProps) {
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
if (isMobile) {
|
||||
setShowMobileSidebar(!showMobileSidebar);
|
||||
} else {
|
||||
onToggleSidebar?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 移动端侧边栏遮罩 */}
|
||||
{isMobile && showMobileSidebar && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||
onClick={() => setShowMobileSidebar(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex-1 flex flex-col min-h-screen bg-gray-50">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="bg-white border-b border-gray-200 px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 菜单按钮 */}
|
||||
<button
|
||||
onClick={handleToggleSidebar}
|
||||
className="p-2 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
{isMobile ? (
|
||||
showMobileSidebar ? (
|
||||
<X className="w-5 h-5 text-gray-600" />
|
||||
) : (
|
||||
<Menu className="w-5 h-5 text-gray-600" />
|
||||
)
|
||||
) : (
|
||||
<Menu className="w-5 h-5 text-gray-600" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 面包屑导航 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Home className="w-4 h-4 text-gray-500" />
|
||||
{breadcrumb.length > 0 ? (
|
||||
breadcrumb.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
{item.path ? (
|
||||
<a
|
||||
href={item.path}
|
||||
className="text-sm text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-gray-900 font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-gray-900 font-medium">{title}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
|
||||
{/* 页面内容 */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
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