Skip to content

Commit 3afa43c

Browse files
committed
优化卡片布局和导出功能,添加卡片宽度控制,更新样式和类型定义
1 parent d262f0c commit 3afa43c

7 files changed

Lines changed: 421 additions & 46 deletions

File tree

src/components/AddCardModal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose }) => {
2929

3030
const handleAddCard = (template: CardTemplate) => {
3131
updateProfileData(prev => {
32+
const id = `card_${Date.now()}`;
3233
const newCard = {
3334
...template.data,
34-
id: `card_${Date.now()}`,
35+
id,
36+
layout: template.data.layout ? { ...template.data.layout, i: id } : undefined
3537
};
3638
return { ...prev!, cards: [...prev!.cards, newCard] };
3739
});

src/components/App.tsx

Lines changed: 175 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useState, useMemo, useCallback } from 'react';
22
import { useProfile } from '../context/ProfileContext';
33
import { useTheme } from '../context/ThemeContext';
44
import Toolbar from './Toolbar';
@@ -7,6 +7,12 @@ import Card from './Card';
77
import AddCardModal from './AddCardModal';
88
import { applyThemeColors } from '../utils/colorUtils';
99
import 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

1117
const 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+
118280
export default App;

0 commit comments

Comments
 (0)