Skip to content

Commit 3612cdc

Browse files
committed
fix 显示效果
1 parent 6da846e commit 3612cdc

1 file changed

Lines changed: 127 additions & 50 deletions

File tree

src/components/mdx/ExcalidrawSlides.tsx

Lines changed: 127 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import React, { useState, useEffect, useRef, useCallback } from 'react';
2+
import LZString from 'lz-string';
3+
4+
const ResetIcon = () => (
5+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
6+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
7+
<path d="M3 3v5h5" />
8+
</svg>
9+
);
10+
11+
const OverviewIcon = () => (
12+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
13+
<rect width="7" height="7" x="3" y="3" rx="1" />
14+
<rect width="7" height="7" x="14" y="3" rx="1" />
15+
<rect width="7" height="7" x="14" y="14" rx="1" />
16+
<rect width="7" height="7" x="3" y="14" rx="1" />
17+
</svg>
18+
);
219

320
const ExcalidrawIcon = (props: React.SVGProps<SVGSVGElement>) => (
4-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
5-
<path d="M12 19l7-7 3 3-7 7-3-3z"></path>
6-
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path>
7-
<path d="M2 2l7.586 7.586"></path>
8-
<circle cx="11" cy="11" r="2"></circle>
21+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" {...props}>
22+
<path fill="#6965db" d="M29.937 25.078a.19.19 0 0 0-.185-.042c-1.464-2.162-3.325-4.213-5.128-6.193l-.297-.325a.17.17 0 0 0-.042-.105a.2.2 0 0 0-.118-.07l-.06-.063l-.042-.031c-.052-.112-.185-.196-.332-.122c-.551.283-1.047.688-1.536 1.062c-.654.5-1.293 1.02-1.894 1.579a6 6 0 0 0-.688.73c-.098.129-.024.251.095.303q-.64.63-1.286 1.307a.2.2 0 0 0-.056.154a.2.2 0 0 0 .077.143l.755.576s.003.01.01.014c1.08 1.065 2.973 2.543 4.978 4.108q.446.351.897.702q.204.247.392.49a.2.2 0 0 0 .279.038c.045.034.09.073.136.108a.2.2 0 0 0 .28-.035a.2.2 0 0 0 .038-.108c.014 0 .025.01.035.01a.2.2 0 0 0 .147-.063l3.556-3.884a.196.196 0 0 0-.014-.28zm-10.21-1.345q.037.047.073.088c.406.342.839.712 1.279 1.09l-1.789-1.366l-.181-.126a2 2 0 0 1-.108-.084l-.133-.112s.024-.024.035-.038l.122-.123c.6-.607 1.631-1.62 2.162-2.116c-.562.566-1.7 2.225-1.456 2.787zm6.123 4.824l-1.474-1.125a37 37 0 0 0-1.83-1.757c.796.615 1.477 1.135 1.579 1.226c.772.689.737.563 1.268 1.017l.639.464c-.063.056-.126.116-.185.172zm.37.283l-.027-.02l.17-.134l-.139.154zM2.843 6.031l.14.737c.24 1.292.464 2.456.89 3.34l.168.67c.066.255.16.573.248.64c.995.88 2.522 2.193 4.153 3.43a.2.2 0 0 0 .245-.004q.006.01.014.014a.2.2 0 0 0 .132.052a.2.2 0 0 0 .147-.066c2.089-2.323 3.643-4.234 4.75-5.834a.44.44 0 0 0 .102-.293c.07-.084.143-.168.21-.237a.195.195 0 0 0-.035-.3a.2.2 0 0 0-.06-.127a95 95 0 0 0-1.208-1.145a104 104 0 0 1-2.715-2.624L10 4.264a.2.2 0 0 0-.077-.05c-.388-.136-1.184-.272-2.186-.447c-1.475-.251-3.493-.6-5.31-1.142h-.014v-.003s-.007 0-.01.007h-.004l.014-.007s-.108.003-.13.014a.2.2 0 0 0-.065.052c-.018.021-.032.042-.165.07c-.132.028.028 0 .039 0h-.039v.01c.025.12.018.203.056.34c-.007.034.074.356.084.387l.64 2.536zm10.81 2.284l-.013.018l-.224-.248q.114.107.238.23zm-2.476 3.28l-.035.042l-.007-.007q.02-.017.045-.034zm-1.415-7.02c.123.122.608.576.72.688c-.507-.231-1.768-.818-2.354-1.006c.576.1 1.372.23 1.634.317zm-6.7-.968c.294.503.525 2.267.755 3.982c-.13-.552-.24-1.09-.346-1.607c-.181-.894-.349-1.694-.583-2.403q.075.006.171.017q-.002.006.007.01zm-.1-.423q-.122-.012-.217-.017q-.01-.021-.014-.042l.23.063zm-.776.157v-.007zm27.434-.412c.014-.08-.384-.433-.259-.44c.297-.014.3-.471 0-.458c-.394.021-.793.112-1.177.186q-1.036.195-2.068.422a85 85 0 0 0-4.576 1.087c-.475.129-.999.244-1.435.475c-.147.076-.14.234-.06.331a.3.3 0 0 1-.097.032q-.195.035-.388.066a.198.198 0 0 0-.136.3c-.81 1.084-1.733 2.25-2.732 3.476a351 351 0 0 0-3.046 3.543c-3.287 3.863-7.014 8.243-11.143 12.1a.2.2 0 0 0-.01.279a.2.2 0 0 0 .066.045l-.168.154a.18.18 0 0 0-.056.112l-.08.087a.2.2 0 0 0 .01.28a.2.2 0 0 0 .28-.01l.042-.046a.293.293 0 0 1 .426 0l.681.73l-.482-.402a.2.2 0 0 0-.28.024a.2.2 0 0 0 .025.28l5.177 4.342a.2.2 0 0 0 .269-.014l.126-.126a.2.2 0 0 0 .22-.042c7.017-7.049 12.669-12.376 19.142-17.137a.2.2 0 0 0 .08-.178a.2.2 0 0 0 .168-.136c1.194-3.654 1.425-6.889 1.495-8.478l.007-.024q.01-.027.014-.05l.017-.065a.95.95 0 0 0-.052-.751zM17.072 8.647q.471-.54.933-1.055C15.993 10.24 12.66 14.32 7.942 19.168c3.213-3.555 6.451-7.24 9.13-10.52zM5.702 27.094l-.01-.01l.07.014a.2.2 0 0 0-.06 0zm2.41 2.243l-.017-.014l.01-.01c.007 0 .01.006.014.006c0 .007-.007.01-.01.018zm2.92-2.519l.482-.503l.01.018c-.163.16-.328.325-.495.485zm.783-.772l.304-.356q.004-.006.014-.014a201 201 0 0 1 3.555-3.58l.025-.021l.95-.727a520 520 0 0 0-4.848 4.702zm7.562-19.519c-.65.846-1.362 1.942-1.966 2.82c-1.908 2.762-8.048 9.528-8.185 9.657c-.946.916-3.8 3.654-5.62 5.37a1 1 0 0 0-.119.122a.277.277 0 0 1 .01-.395c8.67-8.174 13.93-14.982 16.069-17.947c-.046.115-.084.24-.192.38zm5.767 2.48l-.003.007c0-.007-.007-.024.003-.007m-2.375 1.51c-.79-.458-1.16-1.143-.947-1.834l.067-.23q.041-.1.098-.2c.206-.342.52-.646.88-.824q.026-.01.052-.014a.3.3 0 0 1-.014-.15c.018-.109.088-.203.23-.203c.235 0 .961.216 1.237.454q.127.1.238.22c.105.122.258.321.335.465c.046.02.08.216.133.317q.073.242.063.49c0 .006 0 .006.003.01c-.01.024 0 .13-.014.14c-.035.251-.126.5-.262.716l-.038.056c0 .003-.007.007-.01.014a1.6 1.6 0 0 1-.378.394c-.44.311-.953.406-1.467.276a2 2 0 0 1-.2-.087zm5.683-.57c-.171.716-.38 1.464-.629 2.229c-.01.028-.01.055-.01.08a.2.2 0 0 0-.094.038a106 106 0 0 0-4.535 3.532a250 250 0 0 1 3.923-3.476a2.24 2.24 0 0 0 .737-1.306l.196-1.178l.01-.035c.088-.248.468-.14.409.116z" />
923
</svg>
1024
);
1125

@@ -15,9 +29,16 @@ interface ExcalidrawSlidesProps {
1529
width?: string | number;
1630
title?: string;
1731
subtitle?: string;
32+
fontFamily?: string;
1833
}
1934

20-
export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, width = "100%" }: ExcalidrawSlidesProps) {
35+
export function ExcalidrawSlides({
36+
snapshotUrl,
37+
title,
38+
height = 500,
39+
width = "100%",
40+
fontFamily = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif'
41+
}: ExcalidrawSlidesProps) {
2142
const containerRef = useRef<HTMLDivElement>(null);
2243
const svgContainerRef = useRef<HTMLDivElement>(null);
2344
const svgRef = useRef<SVGSVGElement | null>(null);
@@ -51,7 +72,31 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
5172
try {
5273
const res = await fetch(`${snapshotUrl}?t=${Date.now()}`);
5374
if (!res.ok) throw new Error(`Failed to load: ${res.status}`);
54-
const json = await res.json();
75+
76+
const textContent = await res.text();
77+
let json;
78+
79+
// Try parsing as standard JSON first
80+
try {
81+
json = JSON.parse(textContent);
82+
} catch (e) {
83+
// If not JSON, try parsing as Obsidian-Excalidraw Markdown
84+
// Look for ```compressed-json ... ``` block
85+
const match = textContent.match(/```compressed-json\s*([\s\S]*?)```/);
86+
if (match) {
87+
// Remove all whitespace (newlines, spaces) as LZString expects a continuous string
88+
const compressed = match[1].replace(/\s/g, '');
89+
const decompressed = LZString.decompressFromBase64(compressed);
90+
if (decompressed) {
91+
json = JSON.parse(decompressed);
92+
} else {
93+
throw new Error("Failed to decompress Excalidraw data");
94+
}
95+
} else {
96+
throw new Error("Invalid Excalidraw file format");
97+
}
98+
}
99+
55100
const elements = json.elements || [];
56101

57102
const frameElements = elements.filter((el: any) => el.type === "frame")
@@ -88,28 +133,43 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
88133
try {
89134
const { exportToSvg } = await import("@excalidraw/excalidraw");
90135

91-
const renderElements = data.elements.map((el: any) => {
92-
if (el.type === 'frame') {
93-
return { ...el, strokeColor: '#00000000', backgroundColor: 'transparent', name: '' };
94-
}
95-
return el;
96-
});
136+
// Filter out deleted elements to ensure accurate bounds and rendering
137+
const activeElements = data.elements.filter((el: any) => !el.isDeleted);
97138

98-
// Calculate JSON Min Bounds
139+
// Calculate raw bounding box of active elements (including frames)
99140
let minX = Infinity, minY = Infinity;
100-
renderElements.forEach((el: any) => {
141+
activeElements.forEach((el: any) => {
101142
if (el.x < minX) minX = el.x;
102143
if (el.y < minY) minY = el.y;
103144
});
104145
if (minX === Infinity) { minX = 0; minY = 0; }
105146

147+
// Dynamic Frame Rendering Options
148+
const frameRendering = viewMode === 'overview'
149+
? { enabled: true, name: true, outline: true, clip: true }
150+
: { enabled: false, name: false, outline: false, clip: true };
151+
106152
const svg = await exportToSvg({
107-
elements: renderElements,
108-
appState: { ...data.appState, exportBackground: true, viewBackgroundColor: "#ffffff" },
153+
elements: activeElements,
154+
appState: {
155+
...data.appState,
156+
exportBackground: true,
157+
viewBackgroundColor: "#ffffff",
158+
frameRendering
159+
},
109160
files: data.files || {},
110-
exportPadding: 10, // Restored padding to look nice, we will account for offset
161+
exportPadding: 10,
111162
});
112163

164+
// Inject Custom Font
165+
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
166+
style.textContent = `
167+
text {
168+
font-family: ${fontFamily}, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif !important;
169+
}
170+
`;
171+
svg.prepend(style);
172+
113173
svg.removeAttribute('width');
114174
svg.removeAttribute('height');
115175
svg.style.width = "100%";
@@ -122,8 +182,6 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
122182
setRawViewBox(vb);
123183
currentViewBoxRef.current = vb;
124184

125-
// Calculate Offset: SVG ViewBox Origin - JSON Element Origin
126-
// This accounts for padding or any normalization Excalidraw did.
127185
setCoordinateOffset({
128186
x: vb[0] - minX,
129187
y: vb[1] - minY
@@ -137,7 +195,7 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
137195
} catch (e) { console.error(e); }
138196
}
139197
renderSvg();
140-
}, [data]);
198+
}, [data, viewMode]);
141199

142200
const animate = useCallback((time: number) => {
143201
if (!transitionRef.current) return;
@@ -267,12 +325,13 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
267325

268326
if (!isClient) return <div style={containerStyle} className="bg-gray-50 flex items-center justify-center border-2 border-gray-200 rounded-xl">Initializing...</div>;
269327

328+
const hasFrames = frames.length > 0;
329+
270330
return (
271331
<div className="flex flex-col w-full my-6 bg-white border border-gray-200 rounded-2xl overflow-hidden shadow-sm">
272332
<div className="bg-gray-50 border-b border-gray-100 px-6 py-2.5 flex items-center justify-between">
273333
<div className="flex flex-col">
274334
<h3 className="text-sm font-bold text-gray-800 m-0 leading-tight">{title || "Excalidraw"}</h3>
275-
{subtitle && <span className="text-[10px] text-gray-400 mt-0.5 font-medium">{subtitle}</span>}
276335
</div>
277336
<div className="opacity-60 hover:opacity-100 transition-opacity">
278337
<ExcalidrawIcon className="w-5 h-5 text-purple-600" />
@@ -294,7 +353,7 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
294353
{loading && <div className="absolute inset-0 z-20 flex items-center justify-center bg-white/80 backdrop-blur-sm"><div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div></div>}
295354

296355
{/* Frame Label (Slide Mode) */}
297-
{viewMode === 'slide' && (
356+
{hasFrames && viewMode === 'slide' && (
298357
<div className="absolute bottom-4 left-4 z-50 pointer-events-none">
299358
<span className="px-2 py-1 bg-white/90 backdrop-blur text-gray-600 text-[10px] font-bold uppercase tracking-wider rounded border border-gray-200 shadow-sm">
300359
{frames[currentSlide]?.name || `Slide ${currentSlide + 1}`}
@@ -305,45 +364,63 @@ export function ExcalidrawSlides({ snapshotUrl, title, subtitle, height = 500, w
305364

306365
<div className="bg-gray-50 border-t border-gray-100 px-4 py-2 flex items-center justify-between h-10">
307366
<div className="w-24">
308-
<button
309-
onClick={() => setViewMode(prev => prev === 'overview' ? 'slide' : 'overview')}
310-
className={`text-[10px] font-bold uppercase tracking-widest transition-colors px-2 py-1 rounded ${viewMode === 'overview' ? 'text-blue-600 bg-blue-50' : 'text-gray-400 hover:text-gray-600'}`}
311-
>
312-
{viewMode === 'overview' ? 'Slides' : 'Overview'}
313-
</button>
367+
{hasFrames ? (
368+
<button
369+
onClick={() => setViewMode(prev => prev === 'overview' ? 'slide' : 'overview')}
370+
className={`text-[10px] font-bold uppercase tracking-widest transition-colors px-2 py-1 rounded ${
371+
viewMode === 'overview'
372+
? 'text-red-600 bg-red-50'
373+
: 'text-blue-600 bg-blue-50'
374+
}`}
375+
>
376+
{viewMode === 'overview' ? 'Overview' : 'Slides'}
377+
</button>
378+
) : (
379+
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400 bg-gray-50 px-2 py-1 rounded cursor-not-allowed">
380+
Overview
381+
</span>
382+
)}
314383
</div>
315384

316-
<div className="flex items-center bg-white border border-gray-200 rounded-lg px-1.5 py-0.5 shadow-sm gap-1">
317-
<button onClick={() => goToSlide((currentSlide - 1 + frames.length) % frames.length)} className="p-1 hover:bg-gray-50 rounded text-gray-500 transition-colors"><svg width="14" height="14" viewBox="0 0 15 15" fill="none"><path d="M8.84182 3.13514C9.04327 3.32401 9.05348 3.64042 8.86462 3.84188L5.43521 7.49991L8.86462 11.1579C9.05348 11.3594 9.04327 11.6758 8.84182 11.8647C8.64036 12.0535 8.32394 12.0433 8.13508 11.8419L4.38508 7.84188C4.20477 7.64955 4.20477 7.35027 4.38508 7.15794L8.13508 3.15794C8.32394 2.95648 8.64036 2.94628 8.84182 3.13514Z" fill="currentColor" fillRule="evenodd"></path></svg></button>
318-
319-
<div className="flex items-center gap-1 px-1">
320-
<input
321-
type="text"
322-
value={currentSlide + 1}
323-
onChange={(e) => {
324-
const val = parseInt(e.target.value);
325-
if (!isNaN(val)) goToSlide(val - 1);
326-
}}
327-
className="w-6 bg-transparent text-[10px] font-black text-center text-gray-800 border-none outline-none focus:ring-0 p-0"
328-
/>
329-
<span className="text-[10px] font-black text-gray-300">/</span>
330-
<span className="text-[10px] font-black text-gray-400 w-6 text-center">{frames.length}</span>
331-
</div>
332-
333-
<button onClick={() => goToSlide((currentSlide + 1) % frames.length)} className="p-1 hover:bg-gray-50 rounded text-gray-500 transition-colors"><svg width="14" height="14" viewBox="0 0 15 15" fill="none"><path d="M6.1584 3.13523C5.95694 3.32411 5.94673 3.64053 6.1356 3.84199L9.565 7.50003L6.1356 11.1581C5.94673 11.3596 5.95694 11.676 6.1584 11.8648C6.35986 12.0537 6.67628 12.0435 6.86514 11.842L10.6151 7.84201C10.7954 7.64968 10.7954 7.35038 10.6151 7.15805L6.86514 3.15805C6.67628 2.95659 6.35986 2.94638 6.1584 3.13523Z" fill="currentColor" fillRule="evenodd"></path></svg></button>
385+
<div className="flex-1 flex justify-center">
386+
{hasFrames && (
387+
<div className="flex items-center bg-white border border-gray-200 rounded-lg px-1.5 py-0.5 shadow-sm gap-1">
388+
<button onClick={() => goToSlide((currentSlide - 1 + frames.length) % frames.length)} className="p-1 hover:bg-gray-50 rounded text-gray-500 transition-colors"><svg width="14" height="14" viewBox="0 0 15 15" fill="none"><path d="M8.84182 3.13514C9.04327 3.32401 9.05348 3.64042 8.86462 3.84188L5.43521 7.49991L8.86462 11.1579C9.05348 11.3594 9.04327 11.6758 8.84182 11.8647C8.64036 12.0535 8.32394 12.0433 8.13508 11.8419L4.38508 7.84188C4.20477 7.64955 4.20477 7.35027 4.38508 7.15794L8.13508 3.15794C8.32394 2.95648 8.64036 2.94628 8.84182 3.13514Z" fill="currentColor" fillRule="evenodd"></path></svg></button>
389+
390+
<div className="flex items-center gap-1 px-1">
391+
<input
392+
type="text"
393+
value={currentSlide + 1}
394+
onChange={(e) => {
395+
const val = parseInt(e.target.value);
396+
if (!isNaN(val)) goToSlide(val - 1);
397+
}}
398+
className="w-6 bg-transparent text-[10px] font-black text-center text-gray-800 border-none outline-none focus:ring-0 p-0"
399+
/>
400+
<span className="text-[10px] font-black text-gray-300">/</span>
401+
<span className="text-[10px] font-black text-gray-400 w-6 text-center">{frames.length}</span>
402+
</div>
403+
404+
<button onClick={() => goToSlide((currentSlide + 1) % frames.length)} className="p-1 hover:bg-gray-50 rounded text-gray-500 transition-colors"><svg width="14" height="14" viewBox="0 0 15 15" fill="none"><path d="M6.1584 3.13523C5.95694 3.32411 5.94673 3.64053 6.1356 3.84199L9.565 7.50003L6.1356 11.1581C5.94673 11.3596 5.95694 11.676 6.1584 11.8648C6.35986 12.0537 6.67628 12.0435 6.86514 11.842L10.6151 7.84201C10.7954 7.64968 10.7954 7.35038 10.6151 7.15805L6.86514 3.15805C6.67628 2.95659 6.35986 2.94638 6.1584 3.13523Z" fill="currentColor" fillRule="evenodd"></path></svg></button>
405+
</div>
406+
)}
334407
</div>
335408

336409
<div className="flex items-center gap-1 w-24 justify-end">
337410
<button
338411
onClick={() => {
339412
syncView();
340413
}}
341-
className="w-7 h-7 flex items-center justify-center hover:bg-gray-200 rounded transition-colors text-xs"
414+
className="w-7 h-7 flex items-center justify-center hover:bg-gray-200 rounded transition-colors text-gray-500"
342415
title="Reset View"
343416
>
344-
🔄
417+
<ResetIcon />
345418
</button>
346-
<button onClick={() => setViewMode('overview')} className="w-7 h-7 flex items-center justify-center hover:bg-red-100 rounded transition-colors text-[10px]" title="Overview">⏹️</button>
419+
{hasFrames && (
420+
<button onClick={() => setViewMode('overview')} className="w-7 h-7 flex items-center justify-center hover:bg-red-100 rounded transition-colors text-gray-500 hover:text-red-500" title="Overview">
421+
<OverviewIcon />
422+
</button>
423+
)}
347424
</div>
348425
</div>
349426
</div>

0 commit comments

Comments
 (0)