1- import React , { useEffect , useState } from 'react' ;
1+ import React , { useEffect , useState , useMemo , useCallback } from 'react' ;
22import { useProfile } from '../context/ProfileContext' ;
33import { useTheme } from '../context/ThemeContext' ;
44import Toolbar from './Toolbar' ;
@@ -7,6 +7,12 @@ import Card from './Card';
77import AddCardModal from './AddCardModal' ;
88import { applyThemeColors } from '../utils/colorUtils' ;
99import EditableText from './ui/EditableText' ;
10+ import { Responsive , WidthProvider , Layout } from 'react-grid-layout' ;
11+ import 'react-grid-layout/css/styles.css' ;
12+ import 'react-resizable/css/styles.css' ;
13+ import { CardData , ProfileData } from '../types/data' ;
14+
15+ const ResponsiveGridLayout = WidthProvider ( Responsive ) ;
1016
1117const SeoContent : React . FC = ( ) => (
1218 < div className = "intro-section" style = { { position : 'absolute' , width : '1px' , height : '1px' , overflow : 'hidden' , left : '-9999px' , top : '-9999px' , opacity : 0 } } >
@@ -22,12 +28,12 @@ function App() {
2228 const { profileData, isLoaded, updateProfileData } = useProfile ( ) ;
2329 const { theme, setTheme } = useTheme ( ) ;
2430 const [ isAddCardModalOpen , setAddCardModalOpen ] = useState ( false ) ;
31+ const [ mounted , setMounted ] = useState ( false ) ;
32+ const [ contentHeights , setContentHeights ] = useState < Record < string , number > > ( { } ) ;
2533
26- // The update logic is now throttled at the source (Toolbar),
27- // so we can apply the theme directly whenever the global state changes.
28- // The debounce logic has been removed.
2934 useEffect ( ( ) => {
30- const art = `
35+ setMounted ( true ) ;
36+ const art = `
3137
3238 ▄████▄ ██░ ██ ██▓▒███████▒ █ ██ ██ ▄█▀ █ ██ ▒█████
3339▒██▀ ▀█ ▓██░ ██▒▓██▒▒ ▒ ▒ ▄▀░ ██ ▓██▒ ██▄█▒ ██ ▓██▒▒██▒ ██▒
@@ -44,7 +50,7 @@ function App() {
4450 ` ;
4551
4652 const versionInfo = `
47- 芝士扩列条编辑器 V2.3.3
53+ 芝士扩列条编辑器 V2.4.0
4854 构建时间: ${ process . env . REACT_APP_BUILD_TIME ?? import . meta?. env ?. VITE_BUILD_TIME ?? new Date ( ) . toLocaleString ( ) }
4955 chizukuo@icloud.com
5056 ` ;
@@ -75,29 +81,184 @@ function App() {
7581 }
7682 } , [ profileData ?. userSettings . accentColor ] ) ;
7783
78- const handleFooterUpdate = ( html : string ) => {
84+ // Migration effect: Ensure all cards have a layout property
85+ useEffect ( ( ) => {
86+ if ( ! profileData ) return ;
87+ let updatesNeeded = false ;
88+ const newCards = profileData . cards . map ( ( card , index ) => {
89+ if ( ! card . layout ) {
90+ updatesNeeded = true ;
91+ let w = 1 ;
92+ if ( card . layoutSpan ?. includes ( 'span 2' ) ) w = 2 ;
93+ if ( card . layoutSpan ?. includes ( 'span 3' ) ) w = 3 ;
94+ // Simple default layout logic
95+ return {
96+ ...card ,
97+ layout : { i : card . id , x : ( index * w ) % 3 , y : Infinity , w, h : 10 }
98+ } ;
99+ }
100+ return card ;
101+ } ) ;
102+
103+ if ( updatesNeeded ) {
104+ updateProfileData ( ( prev : ProfileData | null ) => {
105+ if ( ! prev ) return null ;
106+ return { ...prev , cards : newCards } ;
107+ } ) ;
108+ }
109+ } , [ profileData , updateProfileData ] ) ;
110+
111+ const handleFooterUpdate = useCallback ( ( html : string ) => {
79112 if ( profileData ) {
80- updateProfileData ( prev => ( {
113+ updateProfileData ( ( prev : ProfileData | null ) => ( {
81114 ...prev ! ,
82115 userSettings : { ...prev ! . userSettings , footerText : html }
83116 } ) ) ;
84117 }
85- } ;
118+ } , [ profileData , updateProfileData ] ) ;
119+
120+ const handleLayoutChange = useCallback ( ( layout : Layout [ ] ) => {
121+ if ( ! profileData ) return ;
122+
123+ // Check if layout actually changed to avoid infinite loops
124+ const hasChanged = layout . some ( l => {
125+ const card = profileData . cards . find ( c => c . id === l . i ) ;
126+ if ( ! card ) return false ;
127+ const currentLayout = card . layout ;
128+ if ( ! currentLayout ) return true ;
129+ return currentLayout . x !== l . x || currentLayout . y !== l . y || currentLayout . w !== l . w || currentLayout . h !== l . h ;
130+ } ) ;
131+
132+ if ( hasChanged ) {
133+ updateProfileData ( ( prev : ProfileData | null ) => {
134+ if ( ! prev ) return null ;
135+ const newCards = prev . cards . map ( card => {
136+ const layoutItem = layout . find ( ( l : any ) => l . i === card . id ) ;
137+ if ( layoutItem ) {
138+ return {
139+ ...card ,
140+ layout : {
141+ i : layoutItem . i ,
142+ x : layoutItem . x ,
143+ y : layoutItem . y ,
144+ w : layoutItem . w ,
145+ h : layoutItem . h
146+ }
147+ } ;
148+ }
149+ return card ;
150+ } ) ;
151+ // Sort cards based on layout (y then x) to keep DOM order somewhat consistent with visual order
152+ // This is optional but good for accessibility and tab order
153+ newCards . sort ( ( a , b ) => {
154+ const la = a . layout || { y : 0 , x : 0 } ;
155+ const lb = b . layout || { y : 0 , x : 0 } ;
156+ if ( la . y === lb . y ) return la . x - lb . x ;
157+ return la . y - lb . y ;
158+ } ) ;
159+
160+ return { ...prev , cards : newCards } ;
161+ } ) ;
162+ }
163+ } , [ profileData , updateProfileData ] ) ;
164+
165+ const handleHeightChange = useCallback ( ( id : string , height : number ) => {
166+ setContentHeights ( prev => ( { ...prev , [ id ] : height } ) ) ;
167+ } , [ ] ) ;
168+
169+ useEffect ( ( ) => {
170+ if ( ! profileData ) return ;
171+
172+ const rowGroups : Record < number , string [ ] > = { } ;
173+ // Group by Y
174+ profileData . cards . forEach ( card => {
175+ const y = card . layout ?. y || 0 ;
176+ if ( ! rowGroups [ y ] ) rowGroups [ y ] = [ ] ;
177+ rowGroups [ y ] . push ( card . id ) ;
178+ } ) ;
179+
180+ let updatesNeeded = false ;
181+ const newCards = profileData . cards . map ( card => {
182+ const y = card . layout ?. y || 0 ;
183+ const rowIds = rowGroups [ y ] ;
184+
185+ // Find max pixel height in this row
186+ let maxPixelHeight = 0 ;
187+ rowIds . forEach ( id => {
188+ const h = contentHeights [ id ] || 0 ;
189+ if ( h > maxPixelHeight ) maxPixelHeight = h ;
190+ } ) ;
191+
192+ // Convert to grid units
193+ const rowHeight = 10 ;
194+ const marginY = 24 ;
195+ const requiredH = Math . ceil ( ( maxPixelHeight + marginY ) / ( rowHeight + marginY ) ) ;
196+
197+ if ( card . layout ?. h !== requiredH ) {
198+ updatesNeeded = true ;
199+ return {
200+ ...card ,
201+ layout : { ...( card . layout || { i : card . id , x : 0 , y : 0 , w : 1 } ) , h : requiredH }
202+ } ;
203+ }
204+ return card ;
205+ } ) ;
206+
207+ if ( updatesNeeded ) {
208+ updateProfileData ( ( prev : ProfileData | null ) => {
209+ if ( ! prev ) return null ;
210+ return { ...prev , cards : newCards } ;
211+ } ) ;
212+ }
213+ } , [ contentHeights , profileData , updateProfileData ] ) ;
86214
87215 if ( ! isLoaded || ! profileData ) {
88216 return < div > Loading...</ div > ; // Or a loading spinner
89217 }
90218
219+ // Generate initial layout if missing
220+ const layouts = {
221+ lg : profileData . cards . map ( ( card , index ) => {
222+ if ( card . layout ) return { ...card . layout , i : card . id } ;
223+ let w = 1 ;
224+ if ( card . layoutSpan ?. includes ( 'span 2' ) ) w = 2 ;
225+ if ( card . layoutSpan ?. includes ( 'span 3' ) ) w = 3 ;
226+ return { i : card . id , x : ( index * w ) % 3 , y : Math . floor ( index / 3 ) * 10 , w, h : 10 } ;
227+ } )
228+ } ;
229+
91230 return (
92231 < >
93232 < SeoContent />
94233 < Toolbar onAddCardClick = { ( ) => setAddCardModalOpen ( true ) } />
95234 < main id = "profileCardContainer" className = "py-10 px-4 md:px-6 lg:px-8 min-h-screen flex flex-col items-center" >
96235 < ProfileHeader />
97- < div className = "grid-container" >
98- { profileData . cards . map ( ( card , index ) => (
99- < Card key = { card . id } cardData = { card } cardIndex = { index } />
100- ) ) }
236+ < div style = { { width : '100%' } } >
237+ { mounted && (
238+ < ResponsiveGridLayout
239+ className = "layout"
240+ layouts = { layouts }
241+ breakpoints = { { lg : 960 , md : 600 , sm : 0 } }
242+ cols = { { lg : 3 , md : 2 , sm : 1 } }
243+ rowHeight = { 10 }
244+ margin = { [ 24 , 24 ] }
245+ onLayoutChange = { ( layout ) => handleLayoutChange ( layout ) }
246+ draggableHandle = ".drag-handle"
247+ isDraggable = { true }
248+ isResizable = { false }
249+ >
250+ { profileData . cards . map ( ( card , index ) => (
251+ < div key = { card . id } data-grid = { card . layout || { x : ( index ) % 3 , y : Math . floor ( index / 3 ) * 10 , w : 1 , h : 10 , i : card . id } } >
252+ < Card
253+ key = { card . id }
254+ cardData = { card }
255+ cardIndex = { index }
256+ onHeightChange = { ( h ) => handleHeightChange ( card . id , h ) }
257+ />
258+ </ div >
259+ ) ) }
260+ </ ResponsiveGridLayout >
261+ ) }
101262 </ div >
102263 < footer className = "page-footer" >
103264 < EditableText
@@ -115,4 +276,5 @@ function App() {
115276 ) ;
116277}
117278
279+
118280export default App ;
0 commit comments