jingrow/components/presentation/Presentation.jsx

280 lines
8.0 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';
import './Presentation.css';
// 简单的动态主题加载器
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 字段,使用 items 数据构建幻灯片
const items = data.items || [];
if (items.length === 0) {
return parseSingleSlide(data);
}
return parseItemsSlides(items);
} 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('');
};
// 解析单个幻灯片
const parseSingleSlide = (data) => {
return `<section>
<h1>${data.title || '演示文稿'}</h1>
${data.subtitle ? `<p class="lead">${data.subtitle}</p>` : ''}
${data.description ? `<div class="mt-4">${marked(data.description)}</div>` : ''}
${data.image ? `<img src="${data.image}" alt="${data.title}" style="max-width: 100%; height: auto; margin: 1rem 0;" />` : ''}
</section>`;
};
// 解析items数组的幻灯片
const parseItemsSlides = (items) => {
return items.map((item, index) => {
const slideContent = [];
if (item.item_title) {
slideContent.push(`<h1>${item.item_title}</h1>`);
}
if (item.item_subtitle) {
slideContent.push(`<p class="lead">${item.item_subtitle}</p>`);
}
if (item.item_description) {
slideContent.push(`<div class="mt-4">${marked(item.item_description)}</div>`);
}
if (item.item_image) {
slideContent.push(`<img src="${item.item_image}" alt="${item.item_title || 'Slide'}" style="max-width: 100%; height: auto; margin: 1rem 0;" />`);
}
if (item.item_video_src) {
slideContent.push(`<video controls style="max-width: 100%; height: auto; margin: 1rem 0;">
<source src="${item.item_video_src}" type="video/mp4">
您的浏览器不支持视频播放。
</video>`);
}
if (item.item_button_text && item.item_button_link) {
slideContent.push(`<div class="mt-6">
<a href="${item.item_button_link}" class="btn btn-primary" target="_blank" rel="noopener noreferrer">
${item.item_button_text}
</a>
</div>`);
}
return `<section>${slideContent.join('')}</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) {
console.error('Reveal.js初始化失败:', 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>
);
}