File size: 6,767 Bytes
4d48c94 e375f30 4d48c94 e375f30 291850b 4d48c94 291850b 4d48c94 e375f30 339fff1 e375f30 4d48c94 291850b 4d48c94 e375f30 291850b e375f30 4d48c94 e375f30 4d48c94 339fff1 4d48c94 339fff1 4d48c94 291850b 4d48c94 e375f30 339fff1 e375f30 4d48c94 e375f30 339fff1 e375f30 291850b e375f30 291850b e375f30 4d48c94 cfa893a 4d48c94 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
import React from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import LeftNav from './LeftNav';
import MobileNav from './MobileNav';
import RightSidebar from './RightSidebar';
import { useSidebar, SIDEBAR_WIDTH_EXPANDED, SIDEBAR_WIDTH_COLLAPSED } from '../../contexts/SidebarContext';
import { Star, Calendar, TrendingUp, User, Sparkles, ChevronLeft, ChevronRight } from 'lucide-react';
import { LifeDestinyResult, UserInput } from '../../types';
interface AppShellProps {
isLoggedIn: boolean;
userInfo: { email: string; points: number } | null;
onLoginClick: () => void;
onLogout: () => void;
onHistorySelect?: (result: LifeDestinyResult, input: UserInput) => void;
onNewCalculation?: () => void;
isAnalysisPanelOpen?: boolean;
}
// Mini sidebar component for collapsed state
const SidebarMini: React.FC<{
onGenerate: () => void;
isLoggedIn: boolean;
}> = ({ onGenerate, isLoggedIn }) => {
const navigate = useNavigate();
const miniItems = [
{ icon: Star, label: '今日', color: 'text-amber-500', onClick: () => navigate('/fortune/daily') },
{ icon: Calendar, label: '本月', color: 'text-blue-500', onClick: () => navigate('/fortune/monthly') },
{ icon: TrendingUp, label: '今年', color: 'text-purple-500', onClick: () => navigate('/fortune/yearly') },
{ icon: Sparkles, label: '测算', color: 'text-primrose', onClick: onGenerate },
];
return (
<div className="flex flex-col items-center py-4 space-y-2">
{miniItems.map((item, index) => (
<button
key={index}
onClick={item.onClick}
className="w-12 h-12 flex flex-col items-center justify-center rounded-xl hover:bg-white/80 transition-colors group"
title={item.label}
>
<item.icon className={`w-5 h-5 ${item.color} group-hover:scale-110 transition-transform`} />
<span className="text-[10px] text-gray-500 mt-0.5">{item.label}</span>
</button>
))}
<div className="h-px w-8 bg-gray-200 my-2" />
<button
onClick={() => navigate(isLoggedIn ? '/dashboard' : '/')}
className="w-12 h-12 flex flex-col items-center justify-center rounded-xl hover:bg-white/80 transition-colors group"
title={isLoggedIn ? '个人中心' : '登录'}
>
<User className="w-5 h-5 text-gray-500 group-hover:scale-110 transition-transform" />
<span className="text-[10px] text-gray-500 mt-0.5">{isLoggedIn ? '我的' : '登录'}</span>
</button>
</div>
);
};
const AppShell: React.FC<AppShellProps> = ({
isLoggedIn,
userInfo,
onLoginClick,
onLogout,
onHistorySelect,
onNewCalculation,
isAnalysisPanelOpen = false,
}) => {
const navigate = useNavigate();
const location = useLocation();
const { isExpanded, isHovered, isCollapsible, setIsHovered, sidebarWidth, toggleExpanded } = useSidebar();
// Check if on homepage
const isHomepage = location.pathname === '/' || location.pathname === '/home';
const handleGenerate = () => {
navigate('/');
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Determine if sidebar should show full content
const showFullSidebar = isExpanded || isHovered || !isCollapsible;
return (
<div className="min-h-screen bg-cream">
{/* Desktop: Three-column layout */}
<div className="hidden md:flex max-w-[1440px] mx-auto">
{/* Left Nav - Fixed/Sticky */}
<aside className="w-[280px] shrink-0">
<div className="fixed top-0 h-screen w-[280px] overflow-y-auto border-r border-light-mint bg-white">
<LeftNav
isLoggedIn={isLoggedIn}
userInfo={userInfo}
onLoginClick={onLoginClick}
onLogout={onLogout}
onHistorySelect={onHistorySelect}
onNewCalculation={onNewCalculation}
/>
</div>
</aside>
{/* Main Column - Scrollable, width adjusts based on sidebar */}
<main
className="flex-1 min-w-0 border-r border-light-mint bg-white min-h-screen transition-all duration-300"
style={{ maxWidth: isCollapsible && !showFullSidebar ? '800px' : '680px' }}
>
<Outlet />
</main>
{/* Right Sidebar - Fixed/Sticky with collapsible support */}
<aside
className="shrink-0 transition-all duration-300 ease-in-out"
style={{ width: sidebarWidth }}
>
<div
className={`fixed top-0 h-screen overflow-y-auto bg-cream transition-all duration-300 ease-in-out ${
isCollapsible && !showFullSidebar ? 'overflow-hidden' : ''
}`}
style={{ width: sidebarWidth }}
onMouseEnter={() => isCollapsible && setIsHovered(true)}
onMouseLeave={() => isCollapsible && setIsHovered(false)}
>
{/* Collapse/Expand Toggle Button */}
{isCollapsible && (
<button
onClick={toggleExpanded}
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-1/2 z-50 w-6 h-12 bg-white border border-gray-200 rounded-full shadow-md hover:shadow-lg hover:bg-gray-50 flex items-center justify-center transition-all duration-200"
title={isExpanded || isHovered ? '收起侧边栏' : '展开侧边栏'}
>
{isExpanded || isHovered ? (
<ChevronRight className="w-4 h-4 text-gray-500" />
) : (
<ChevronLeft className="w-4 h-4 text-gray-500" />
)}
</button>
)}
{/* Full Sidebar Content */}
<div
className={`p-4 transition-opacity duration-200 ${
showFullSidebar ? 'opacity-100' : 'opacity-0 pointer-events-none absolute'
}`}
>
<RightSidebar
isLoggedIn={isLoggedIn}
userInfo={userInfo}
onLogin={onLoginClick}
onLogout={onLogout}
onGenerate={handleGenerate}
isAnalysisPanelOpen={isAnalysisPanelOpen}
/>
</div>
{/* Mini Sidebar (collapsed state) */}
{isCollapsible && !showFullSidebar && (
<div className="p-2 opacity-100">
<SidebarMini onGenerate={handleGenerate} isLoggedIn={isLoggedIn} />
</div>
)}
</div>
</aside>
</div>
{/* Mobile: Single column with bottom nav */}
<div className="md:hidden">
<main className="pb-16">
<Outlet />
</main>
<MobileNav isLoggedIn={isLoggedIn} />
</div>
</div>
);
};
export default AppShell;
|