jingrow/components/presentation/Presentation.jsx

278 lines
7.5 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 {
// 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>
</>
);
}