278 lines
7.5 KiB
JavaScript
278 lines
7.5 KiB
JavaScript
'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 {
|
||
// 1. 优先尝试加载自定义主题
|
||
await import(`./themes/${themeName}.css`);
|
||
console.log(`加载自定义主题: ${themeName}`);
|
||
} catch (error) {
|
||
try {
|
||
// 2. 尝试加载 Reveal.js 内置主题
|
||
await import(`reveal.js/dist/theme/${themeName}.css`);
|
||
console.log(`加载内置主题: ${themeName}`);
|
||
} catch (error2) {
|
||
// 3. 如果都失败,使用默认主题
|
||
console.warn(`主题 ${themeName} 未找到,使用默认主题`);
|
||
await import('reveal.js/dist/theme/black.css');
|
||
}
|
||
}
|
||
};
|
||
|
||
// Reveal.js配置
|
||
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解析器(只配置一次)
|
||
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);
|
||
}, []);
|
||
|
||
// 解析Markdown内容为幻灯片
|
||
const parseMarkdownToSlides = useCallback((data) => {
|
||
if (!data) {
|
||
return `<section><h1>演示文稿</h1><p>暂无内容</p></section>`;
|
||
}
|
||
|
||
try {
|
||
// 如果有 content 字段,直接使用 markdown 内容
|
||
if (data.content) {
|
||
return parseContentSlides(data);
|
||
}
|
||
|
||
// 如果没有 content 字段,返回默认内容
|
||
return `<section><h1>${data.title || '演示文稿'}</h1><p>暂无内容</p></section>`;
|
||
} catch (err) {
|
||
console.error('解析幻灯片内容失败:', err);
|
||
return `<section><h1>解析错误</h1><p>内容解析失败,请检查数据格式</p></section>`;
|
||
}
|
||
}, []);
|
||
|
||
// 解析content字段的幻灯片
|
||
const parseContentSlides = (data) => {
|
||
const slideSeparators = /^---$/gm;
|
||
const sections = data.content.split(slideSeparators);
|
||
|
||
return sections.map((section, index) => {
|
||
const trimmedSection = section.trim();
|
||
if (!trimmedSection) return '';
|
||
|
||
// 检查是否有背景设置
|
||
const backgroundMatch = trimmedSection.match(/<!-- \.slide: data-background="([^"]+)"[^>]* -->/);
|
||
let backgroundAttr = '';
|
||
let contentWithoutBackground = trimmedSection;
|
||
|
||
if (backgroundMatch) {
|
||
backgroundAttr = ` data-background="${backgroundMatch[1]}"`;
|
||
contentWithoutBackground = trimmedSection.replace(/<!-- \.slide: data-background="[^"]+"[^>]* -->/g, '');
|
||
}
|
||
|
||
const htmlContent = marked(contentWithoutBackground);
|
||
|
||
// 如果是第一个幻灯片且没有标题,添加标题
|
||
if (index === 0 && !htmlContent.includes('<h1>') && data.title) {
|
||
return `<section${backgroundAttr}><h1>${data.title}</h1>${htmlContent}</section>`;
|
||
}
|
||
|
||
return `<section${backgroundAttr}>${htmlContent}</section>`;
|
||
}).join('');
|
||
};
|
||
|
||
// 初始化Reveal.js
|
||
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();
|
||
|
||
// 更新slides内容
|
||
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">正在加载PPT...</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>
|
||
</>
|
||
);
|
||
}
|