|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { Layout } from "antd"; |
| 3 | +import { Layout, Button } from "antd"; |
4 | 4 | import { TopNavbar } from "./TopNavbar"; |
5 | 5 | import { SideNavigation } from "./SideNavigation"; |
6 | | -import { Footer } from "./Footer"; |
7 | | -import { HEADER_CONFIG, FOOTER_CONFIG } from "@/const/layoutConstants"; |
| 6 | +import { FooterLayout } from "./FooterLayout"; |
| 7 | +import { HEADER_CONFIG, FOOTER_CONFIG, SIDER_CONFIG } from "@/const/layoutConstants"; |
8 | 8 | import React from "react"; |
| 9 | +import { useState, useEffect } from "react"; |
9 | 10 |
|
10 | | -const { Content } = Layout; |
| 11 | +import { |
| 12 | + ChevronLeft, |
| 13 | + ChevronRight, |
| 14 | +} from "lucide-react"; |
11 | 15 |
|
| 16 | +const { Header, Sider, Content, Footer } = Layout; |
12 | 17 | interface NavigationLayoutProps { |
13 | 18 | children: React.ReactNode; |
14 | 19 | onAuthRequired?: () => void; |
@@ -49,78 +54,141 @@ export function NavigationLayout({ |
49 | 54 | // Use RESERVED_HEIGHT for layout calculations (actual space occupied) |
50 | 55 | const headerReservedHeight = parseInt(HEADER_CONFIG.RESERVED_HEIGHT); |
51 | 56 | const footerReservedHeight = parseInt(FOOTER_CONFIG.RESERVED_HEIGHT); |
52 | | - |
53 | 57 | const contentMinHeight = showFooter |
54 | 58 | ? `calc(100vh - ${headerReservedHeight}px - ${footerReservedHeight}px)` |
55 | 59 | : `calc(100vh - ${headerReservedHeight}px)`; |
| 60 | + |
| 61 | + const [collapsed, setCollapsed] = useState(false); |
56 | 62 |
|
57 | | - return ( |
58 | | - <div className={`${contentMode === "fullscreen" ? "h-screen" : "min-h-screen"} flex flex-col ${contentMode === "fullscreen" ? "bg-white dark:bg-slate-900" : "bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"} overflow-hidden`}> |
59 | | - {/* Top navigation bar */} |
60 | | - <TopNavbar |
61 | | - additionalTitle={topNavbarAdditionalTitle} |
62 | | - additionalRightContent={topNavbarAdditionalRightContent} |
63 | | - /> |
| 63 | + const layoutStyle: React.CSSProperties = { |
| 64 | + height: "100vh", // Key: fill the viewport height |
| 65 | + width: "100vw", // Key: fill the viewport width |
| 66 | + overflow: "hidden", // Key: prevent the outermost scrollbar |
| 67 | + backgroundColor: "#fff" |
| 68 | + }; |
| 69 | + |
| 70 | + const siderStyle: React.CSSProperties = { |
| 71 | + textAlign: "start", |
| 72 | + display: "flex", // use column layout to allow inner scrollable area |
| 73 | + flexDirection: "column", |
| 74 | + alignItems: "stretch", |
| 75 | + justifyContent: "flex-start", |
| 76 | + position: "fixed", |
| 77 | + top: headerReservedHeight, |
| 78 | + bottom: showFooter? footerReservedHeight: 0, |
| 79 | + left: 0, |
| 80 | + backgroundColor: "#fff", |
| 81 | + overflow: "visible", |
| 82 | + // Ensure the sider (and its toggle) sits above main content |
| 83 | + zIndex: 998, |
| 84 | + }; |
| 85 | + |
| 86 | + const siderInnerStyle: React.CSSProperties = { |
| 87 | + height: "100%", |
| 88 | + overflowY: "auto", |
| 89 | + overflowX: "hidden", |
| 90 | + WebkitOverflowScrolling: "touch", |
| 91 | + display: "flex", |
| 92 | + flexDirection: "column", |
| 93 | + }; |
| 94 | + |
| 95 | + const headerStyle: React.CSSProperties = { |
| 96 | + textAlign: "center", |
| 97 | + height: headerReservedHeight, // Fixed height 64px |
| 98 | + backgroundColor: "#fff", |
| 99 | + lineHeight: "64px", |
| 100 | + paddingInline: 0, |
| 101 | + flexShrink: 0, // Prevent shrinking |
| 102 | + }; |
64 | 103 |
|
65 | | - {/* Layout with sidebar and content */} |
66 | | - <Layout |
67 | | - className="flex-1 bg-transparent" |
68 | | - style={{ |
69 | | - marginTop: 0, |
70 | | - marginBottom: 0, |
71 | | - ...(contentMode === "fullscreen" |
72 | | - ? { height: contentMinHeight } |
73 | | - : { minHeight: contentMinHeight } |
74 | | - ), |
75 | | - }} |
76 | | - > |
77 | | - {/* Side navigation */} |
78 | | - <SideNavigation |
79 | | - onAuthRequired={onAuthRequired} |
80 | | - onAdminRequired={onAdminRequired} |
81 | | - onViewChange={onViewChange} |
82 | | - currentView={currentView} |
83 | | - /> |
| 104 | + const footerStyle: React.CSSProperties = { |
| 105 | + textAlign: "center", |
| 106 | + height: showFooter? footerReservedHeight: 0, // Fixed height 64px |
| 107 | + lineHeight: showFooter? footerReservedHeight: 0, |
| 108 | + padding: 0, |
| 109 | + flexShrink: 0, // 防止被挤压 |
| 110 | + backgroundColor: "#fff", |
| 111 | + }; |
84 | 112 |
|
85 | | - {/* Main content area */} |
86 | | - <Content |
87 | | - className={ |
88 | | - contentMode === "centered" |
89 | | - ? "flex-1 flex items-center justify-center overflow-hidden" |
90 | | - : contentMode === "fullscreen" |
91 | | - ? "flex-1 overflow-hidden" |
92 | | - : "flex-1 overflow-auto" |
93 | | - } |
94 | | - style={{ |
95 | | - paddingTop: contentMode === "fullscreen" ? `${headerReservedHeight}px` : `${headerReservedHeight}px`, |
96 | | - paddingBottom: contentMode === "fullscreen" ? (showFooter ? `${footerReservedHeight}px` : 0) : (showFooter ? `${footerReservedHeight}px` : 0) |
97 | | - }} |
98 | | - > |
99 | | - {contentMode === "centered" ? ( |
100 | | - <div className="w-full h-full flex items-center justify-center p-4"> |
101 | | - {children} |
| 113 | + const contentStyle: React.CSSProperties = { |
| 114 | + // Key settings: |
| 115 | + overflowY: "auto", // If content overflows vertically, scroll inside Content only |
| 116 | + overflowX: "hidden", // Hide horizontal scrollbar (optional) |
| 117 | + display: "flex", // (optional) used to center content for demo |
| 118 | + alignItems: "center", // (optional) |
| 119 | + justifyContent: "center", // (optional) |
| 120 | + position: "relative", // Allow absolute-positioned children |
| 121 | + marginLeft: collapsed |
| 122 | + ? `${SIDER_CONFIG.COLLAPSED_WIDTH}px` |
| 123 | + : `${SIDER_CONFIG.EXPANDED_WIDTH}px`, |
| 124 | + backgroundColor: "#fff" |
| 125 | + }; |
| 126 | + |
| 127 | + return ( |
| 128 | + <Layout style={layoutStyle}> |
| 129 | + <Header style={headerStyle}> |
| 130 | + <TopNavbar |
| 131 | + additionalTitle={topNavbarAdditionalTitle} |
| 132 | + additionalRightContent={topNavbarAdditionalRightContent} |
| 133 | + /> |
| 134 | + </Header> |
| 135 | + |
| 136 | + <Layout > |
| 137 | + <Sider |
| 138 | + style={siderStyle} |
| 139 | + width={SIDER_CONFIG.EXPANDED_WIDTH} |
| 140 | + collapsed={collapsed} |
| 141 | + trigger={null} |
| 142 | + breakpoint="lg" |
| 143 | + collapsedWidth={SIDER_CONFIG.COLLAPSED_WIDTH} |
| 144 | + className="dark:bg-slate-900/95 border-r border-slate-200 dark:border-slate-700 backdrop-blur-sm shadow-sm" |
| 145 | + > |
| 146 | + {/* Side navigation - wrapped to allow internal vertical scrolling when needed */} |
| 147 | + <div style={siderInnerStyle}> |
| 148 | + <SideNavigation |
| 149 | + onAuthRequired={onAuthRequired} |
| 150 | + onAdminRequired={onAdminRequired} |
| 151 | + onViewChange={onViewChange} |
| 152 | + currentView={currentView} |
| 153 | + collapsed={collapsed} |
| 154 | + /> |
102 | 155 | </div> |
103 | | - ) : ( |
104 | | - children |
105 | | - )} |
106 | | - </Content> |
107 | | - </Layout> |
| 156 | + <Button |
| 157 | + type="primary" |
| 158 | + shape="circle" |
| 159 | + size="small" |
| 160 | + onClick={() => setCollapsed(!collapsed)} |
| 161 | + style={{ |
| 162 | + position: "absolute", |
| 163 | + top: "50%", |
| 164 | + transform: "translateY(-50%)", |
| 165 | + right: "-12px", |
| 166 | + transition: "right 0.2s ease, left 0.2s ease", |
| 167 | + // Place toggle above most content; Sider already has high z-index |
| 168 | + zIndex: 999, |
| 169 | + }} |
| 170 | + icon={collapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronLeft className="w-3 h-3" />} |
| 171 | + /> |
| 172 | + </Sider> |
| 173 | + {/* Main content area */} |
| 174 | + <Content style={contentStyle} |
| 175 | + > |
| 176 | + {contentMode === "centered" ? ( |
| 177 | + <div className="w-full h-full flex items-center justify-center"> |
| 178 | + {children} |
| 179 | + </div> |
| 180 | + ) : ( |
| 181 | + children |
| 182 | + )} |
| 183 | + </Content> |
| 184 | + </Layout> |
108 | 185 |
|
109 | | - {/* Fixed footer at bottom */} |
110 | | - {showFooter && ( |
111 | | - <div |
112 | | - style={{ |
113 | | - position: "fixed", |
114 | | - bottom: 0, |
115 | | - left: 0, |
116 | | - right: 0, |
117 | | - zIndex: 10, |
118 | | - }} |
119 | | - > |
120 | | - <Footer /> |
121 | | - </div> |
122 | | - )} |
123 | | - </div> |
| 186 | + { showFooter && |
| 187 | + <Footer style={footerStyle}> |
| 188 | + <FooterLayout /> |
| 189 | + </Footer> |
| 190 | + } |
| 191 | + </Layout> |
124 | 192 | ); |
125 | 193 | } |
126 | 194 |
|
0 commit comments