178 lines
6.4 KiB
TypeScript
178 lines
6.4 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||
|
|
import { motion } from 'framer-motion';
|
||
|
|
import {
|
||
|
|
FileText,
|
||
|
|
Warehouse,
|
||
|
|
CreditCard,
|
||
|
|
LogOut,
|
||
|
|
Home,
|
||
|
|
ChevronRight,
|
||
|
|
Factory,
|
||
|
|
Menu,
|
||
|
|
X
|
||
|
|
} from 'lucide-react';
|
||
|
|
import { useAuth } from '../../contexts/AuthContext';
|
||
|
|
import { useResponsive } from '../../hooks/useResponsive';
|
||
|
|
|
||
|
|
interface NavItem {
|
||
|
|
id: string;
|
||
|
|
icon: React.ElementType;
|
||
|
|
label: string;
|
||
|
|
path: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const navItems: NavItem[] = [
|
||
|
|
{ id: 'home', icon: Home, label: '首页', path: '/textile' },
|
||
|
|
{ id: 'plans', icon: FileText, label: '计划总览', path: '/textile/plans' },
|
||
|
|
{ id: 'fabric', icon: Warehouse, label: '已生产坯布', path: '/textile/fabric-warehouse' },
|
||
|
|
{ id: 'yarn', icon: Warehouse, label: '原料纱仓库', path: '/textile/yarn-warehouse' },
|
||
|
|
{ id: 'payment', icon: CreditCard, label: '待结款', path: '/textile/payments' }
|
||
|
|
];
|
||
|
|
|
||
|
|
export function TextileLayout() {
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const location = useLocation();
|
||
|
|
const { auth, logout } = useAuth();
|
||
|
|
const { isMobile } = useResponsive();
|
||
|
|
|
||
|
|
// 从 localStorage 读取侧边栏状态,默认展开
|
||
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||
|
|
const saved = localStorage.getItem('textile_sidebar_collapsed');
|
||
|
|
return saved ? JSON.parse(saved) : false;
|
||
|
|
});
|
||
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||
|
|
|
||
|
|
// 保存侧边栏状态到 localStorage
|
||
|
|
const toggleSidebar = () => {
|
||
|
|
const newState = !sidebarCollapsed;
|
||
|
|
setSidebarCollapsed(newState);
|
||
|
|
localStorage.setItem('textile_sidebar_collapsed', JSON.stringify(newState));
|
||
|
|
};
|
||
|
|
|
||
|
|
const isActive = (path: string) => {
|
||
|
|
const currentPath = location.pathname;
|
||
|
|
|
||
|
|
// 首页精确匹配
|
||
|
|
if (path === '/textile') {
|
||
|
|
return currentPath === '/textile';
|
||
|
|
}
|
||
|
|
|
||
|
|
// 精确匹配当前路径
|
||
|
|
if (currentPath === path) return true;
|
||
|
|
|
||
|
|
// 对于 /textile/plans 路径,不应该匹配子路径
|
||
|
|
if (path === '/textile/plans') {
|
||
|
|
return currentPath === '/textile/plans';
|
||
|
|
}
|
||
|
|
|
||
|
|
// 对于其他路径,匹配子路径
|
||
|
|
if (currentPath.startsWith(path + '/')) return true;
|
||
|
|
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleLogout = async () => {
|
||
|
|
await logout();
|
||
|
|
navigate('/login');
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen bg-slate-50">
|
||
|
|
{/* 桌面端侧边栏 - 固定不动,无入场动画 */}
|
||
|
|
{!isMobile && (
|
||
|
|
<aside
|
||
|
|
className={`fixed left-0 top-0 h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white z-50 flex flex-col ${
|
||
|
|
sidebarCollapsed ? 'w-20' : 'w-64'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{/* Logo区域 */}
|
||
|
|
<div className="h-16 flex items-center px-4 border-b border-slate-700/50">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500 to-green-600 flex items-center justify-center shadow-lg">
|
||
|
|
<Factory className="w-6 h-6 text-white" />
|
||
|
|
</div>
|
||
|
|
{!sidebarCollapsed && (
|
||
|
|
<div>
|
||
|
|
<h1 className="font-bold text-lg leading-tight">纺织厂工作台</h1>
|
||
|
|
<p className="text-xs text-slate-400">{auth.company?.name || '未登录'}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 导航菜单 */}
|
||
|
|
<nav className="flex-1 py-4 px-3 overflow-y-auto">
|
||
|
|
<div className="space-y-1">
|
||
|
|
{navItems.map((item) => {
|
||
|
|
const active = isActive(item.path);
|
||
|
|
const Icon = item.icon;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={item.id}
|
||
|
|
onClick={() => navigate(item.path)}
|
||
|
|
className={`w-full flex items-center gap-3 px-3 py-3 rounded-xl transition-colors duration-200 group ${
|
||
|
|
active
|
||
|
|
? 'bg-gradient-to-r from-emerald-500/20 to-green-500/20 text-emerald-400 border border-emerald-500/30'
|
||
|
|
: 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||
|
|
active ? 'bg-emerald-500/20' : 'bg-slate-700/50 group-hover:bg-slate-600'
|
||
|
|
}`}>
|
||
|
|
<Icon className="w-5 h-5" />
|
||
|
|
</div>
|
||
|
|
{!sidebarCollapsed && (
|
||
|
|
<div className="flex-1 text-left">
|
||
|
|
<div className="font-medium text-sm">{item.label}</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{!sidebarCollapsed && active && (
|
||
|
|
<ChevronRight className="w-4 h-4 text-emerald-400" />
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
{/* 底部操作区 */}
|
||
|
|
<div className="p-3 border-t border-slate-700/50 space-y-2">
|
||
|
|
<button
|
||
|
|
onClick={toggleSidebar}
|
||
|
|
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-700/50 hover:text-white transition-colors"
|
||
|
|
>
|
||
|
|
<div className={`w-8 h-8 rounded-lg bg-slate-700/50 flex items-center justify-center ${
|
||
|
|
sidebarCollapsed ? 'rotate-180' : ''
|
||
|
|
}`}>
|
||
|
|
<ChevronRight className="w-4 h-4" />
|
||
|
|
</div>
|
||
|
|
{!sidebarCollapsed && <span className="text-sm">收起菜单</span>}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<motion.button
|
||
|
|
onClick={handleLogout}
|
||
|
|
whileHover={{ scale: 1.02 }}
|
||
|
|
whileTap={{ scale: 0.98 }}
|
||
|
|
className="w-full flex items-center gap-3 px-3 py-3 rounded-xl text-red-400 hover:bg-red-500/10 transition-colors"
|
||
|
|
>
|
||
|
|
<div className="w-10 h-10 rounded-lg bg-red-500/10 flex items-center justify-center">
|
||
|
|
<LogOut className="w-5 h-5" />
|
||
|
|
</div>
|
||
|
|
{!sidebarCollapsed && <span className="font-medium text-sm">退出登录</span>}
|
||
|
|
</motion.button>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 主内容区 - 移动端无侧边栏,桌面端有侧边栏 */}
|
||
|
|
<main className={`${
|
||
|
|
!isMobile && (sidebarCollapsed ? 'ml-20' : 'ml-64')
|
||
|
|
}`}>
|
||
|
|
<Outlet />
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|