100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
"use client";
|
||
import { EditorContent, useEditor } from '@tiptap/react';
|
||
import { StarterKit } from '@tiptap/starter-kit';
|
||
import { Image } from '@tiptap/extension-image';
|
||
import { TaskItem, TaskList } from '@tiptap/extension-list';
|
||
import { TextAlign } from '@tiptap/extension-text-align';
|
||
import { Typography } from '@tiptap/extension-typography';
|
||
import { Highlight } from '@tiptap/extension-highlight';
|
||
import { Subscript } from '@tiptap/extension-subscript';
|
||
import { Superscript } from '@tiptap/extension-superscript';
|
||
import { HorizontalRule } from '@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension';
|
||
import { useEffect } from 'react';
|
||
|
||
// 导入必要的样式
|
||
import '@/components/tiptap-node/blockquote-node/blockquote-node.scss';
|
||
import '@/components/tiptap-node/code-block-node/code-block-node.scss';
|
||
import '@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss';
|
||
import '@/components/tiptap-node/list-node/list-node.scss';
|
||
import '@/components/tiptap-node/image-node/image-node.scss';
|
||
import '@/components/tiptap-node/heading-node/heading-node.scss';
|
||
import '@/components/tiptap-node/paragraph-node/paragraph-node.scss';
|
||
|
||
interface BlogViewerProps {
|
||
content: any;
|
||
onContentReady?: () => void;
|
||
}
|
||
|
||
export default function BlogViewer({ content, onContentReady }: BlogViewerProps) {
|
||
const editor = useEditor({
|
||
immediatelyRender: false,
|
||
editable: false,
|
||
editorProps: {
|
||
attributes: {
|
||
class: 'simple-editor prose prose-gray max-w-none dark:prose-invert',
|
||
},
|
||
},
|
||
extensions: [
|
||
StarterKit.configure({
|
||
horizontalRule: false,
|
||
link: {
|
||
openOnClick: true,
|
||
enableClickSelection: false,
|
||
},
|
||
}),
|
||
HorizontalRule,
|
||
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||
TaskList,
|
||
TaskItem.configure({ nested: true }),
|
||
Highlight.configure({ multicolor: true }),
|
||
Image,
|
||
Typography,
|
||
Superscript,
|
||
Subscript,
|
||
],
|
||
content: content || '',
|
||
});
|
||
|
||
// 为标题添加 ID,并在内容加载完成后通知父组件
|
||
useEffect(() => {
|
||
if (editor && editor.view.dom) {
|
||
// 等待 DOM 更新
|
||
setTimeout(() => {
|
||
const headings = editor.view.dom.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||
headings.forEach((heading, index) => {
|
||
const element = heading as HTMLElement;
|
||
if (!element.id) {
|
||
const text = element.textContent || '';
|
||
let id = text
|
||
.toLowerCase()
|
||
.replace(/[^\w\s-]/g, '')
|
||
.replace(/[\s_-]+/g, '-')
|
||
.replace(/^-+|-+$/g, '');
|
||
|
||
// 确保 ID 唯一
|
||
let uniqueId = id;
|
||
let counter = 1;
|
||
while (document.getElementById(uniqueId)) {
|
||
uniqueId = `${id}-${counter}`;
|
||
counter++;
|
||
}
|
||
|
||
element.id = uniqueId;
|
||
}
|
||
});
|
||
|
||
// 通知父组件内容已准备好
|
||
onContentReady?.();
|
||
}, 100);
|
||
}
|
||
}, [editor, content, onContentReady]);
|
||
|
||
return (
|
||
<div className="blog-viewer-wrapper min-h-[400px] p-6">
|
||
<EditorContent
|
||
editor={editor}
|
||
className="blog-viewer-content"
|
||
/>
|
||
</div>
|
||
);
|
||
} |