jingrow/components/presentation/Presentation.jsx

280 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import Reveal from 'reveal.js';
import 'reveal.js/dist/reveal.css';
import { marked } from 'marked';
const loadTheme = async (themeName) => {
if (!themeName) return;
try {
await import(`./themes/${themeName}.css`);
} catch (error) {
try {
await import(`reveal.js/dist/theme/${themeName}.css`);
} catch (error2) {
await import('reveal.js/dist/theme/black.css');
}
}
};
const REVEAL_CONFIG = {
hash: true,
transition: 'slide',
transitionSpeed: 'default',
backgroundTransition: 'fade',
controls: true,
progress: true,
center: true,
touch: true,
loop: false,
rtl: false,
navigationMode: 'default',
shuffle: false,
fragments: true,
fragmentInURL: false,
embedded: false,
help: true,
showNotes: true,
autoPlayMedia: null,
preloadIframes: null,
autoSlide: 0,
autoSlideStoppable: true,
autoSlideMethod: Reveal.navigateNext,
defaultTiming: null,
mouseWheel: false,
hideInactiveCursor: true,
hideCursorTime: 5000,
previewLinks: false,
postMessage: true,
postMessageEvents: false,
focusBodyOnPageVisibilityChange: true,
viewDistance: 3,
mobileViewDistance: 2,
display: 'block',
hideAnchorsOnUrl: false,
plugins: []
};
marked.setOptions({
breaks: true,
gfm: true,
headerIds: false,
mangle: false,
});
export default function Presentation({ data }) {
const deckRef = useRef(null);
const revealRef = useRef(null);
const [mounted, setMounted] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setMounted(true);
}, []);
const parseMarkdownToSlides = useCallback((data) => {
if (!data) {
return `<section><h1>演示文稿</h1><p>暂无内容</p></section>`;
}
try {
if (data.content) {
return parseContentSlides(data);
}
return `<section><h1>${data.title || '演示文稿'}</h1><p>暂无内容</p></section>`;
} catch (err) {
return `<section><h1>解析错误</h1><p>内容解析失败,请检查数据格式</p></section>`;
}
}, []);
const parseContentSlides = (data) => {
// 移除frontmatter部分只处理内容部分
let content = data.content;
// 如果内容以---开头说明有frontmatter需要移除
if (content.startsWith('---')) {
const frontmatterEnd = content.indexOf('---', 3);
if (frontmatterEnd !== -1) {
content = content.substring(frontmatterEnd + 3).trim();
}
}
const slideSeparators = /^---$/gm;
const sections = content.split(slideSeparators);
return sections.map((section, index) => {
const trimmedSection = section.trim();
if (!trimmedSection) return '';
// 匹配背景和类属性
const slideCommentMatch = trimmedSection.match(/<!-- \.slide: ([^>]+) -->/);
let backgroundAttr = '';
let classAttr = '';
let contentWithoutComment = trimmedSection;
if (slideCommentMatch) {
const slideAttrs = slideCommentMatch[1];
// 提取data-background属性
const backgroundMatch = slideAttrs.match(/data-background="([^"]+)"/);
if (backgroundMatch) {
backgroundAttr = ` data-background="${backgroundMatch[1]}"`;
}
// 提取class属性
const classMatch = slideAttrs.match(/class="([^"]+)"/);
if (classMatch) {
classAttr = ` class="${classMatch[1]}"`;
}
// 移除整个slide注释
contentWithoutComment = trimmedSection.replace(/<!-- \.slide: [^>]+ -->/g, '');
}
const htmlContent = marked(contentWithoutComment);
if (index === 0 && !htmlContent.includes('<h1>') && data.title) {
return `<section${backgroundAttr}${classAttr}><h1>${data.title}</h1>${htmlContent}</section>`;
}
return `<section${backgroundAttr}${classAttr}>${htmlContent}</section>`;
}).join('');
};
const initializeReveal = useCallback(async () => {
if (!deckRef.current || !data) return;
try {
if (revealRef.current) {
revealRef.current.destroy();
}
const themeName = data.theme || 'default';
await loadTheme(themeName);
revealRef.current = new Reveal(deckRef.current, REVEAL_CONFIG);
revealRef.current.initialize();
const slides = parseMarkdownToSlides(data);
const slidesContainer = deckRef.current.querySelector('.slides');
if (slidesContainer) {
slidesContainer.innerHTML = slides;
}
setError(null);
} catch (err) {
setError('PPT初始化失败请刷新页面重试');
}
}, [data, parseMarkdownToSlides]);
useEffect(() => {
if (mounted && data) {
initializeReveal();
}
return () => {
if (revealRef.current) {
revealRef.current.destroy();
}
};
}, [mounted, data, initializeReveal]);
if (error) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-white text-xl text-center">
<p className="mb-4">PPT加载失败</p>
<p className="text-sm text-gray-400">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
刷新页面
</button>
</div>
</div>
);
}
if (!mounted) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-white text-xl">正在加载内容...</div>
</div>
);
}
return (
<>
<div className="reveal-container">
<div
ref={deckRef}
className="reveal"
>
<div className="slides">
<section>
<p>正在加载内容...</p>
</section>
</div>
</div>
</div>
<style jsx global>{`
.reveal-container {
height: 100vh;
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
.reveal-container .reveal {
height: 100% !important;
width: 100% !important;
}
.reveal-container .reveal .slides {
height: 100% !important;
}
.reveal-container .reveal .slides section {
height: 100% !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
text-align: center !important;
padding: 2rem !important;
box-sizing: border-box !important;
}
.reveal-container .reveal .slides section > * {
margin: 0.5rem 0 !important;
}
.reveal-container .reveal .slides section h1,
.reveal-container .reveal .slides section h2,
.reveal-container .reveal .slides section h3 {
margin-bottom: 1rem !important;
}
.reveal-container .reveal .slides section p {
margin: 0.5rem 0 !important;
line-height: 1.6 !important;
}
.reveal-container .reveal .slides section img,
.reveal-container .reveal .slides section video {
max-width: 100% !important;
height: auto !important;
margin: 1rem 0 !important;
}
`}</style>
</>
);
}