mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-06 08:10:48 +08:00
Update MarkmapComponent.tsx
Add an export mind map button to support exporting HTML and PNG.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Markmap } from 'markmap-view'
|
import { Markmap } from 'markmap-view'
|
||||||
import { transformer } from '@/lib/markmap.ts'
|
import { transformer } from '@/lib/markmap.ts'
|
||||||
import { Toolbar, ToolbarButton } from 'markmap-toolbar'
|
import { Toolbar } from 'markmap-toolbar'
|
||||||
import 'markmap-toolbar/dist/style.css'
|
import 'markmap-toolbar/dist/style.css'
|
||||||
|
|
||||||
export interface MarkmapEditorProps {
|
export interface MarkmapEditorProps {
|
||||||
@@ -12,9 +12,11 @@ export interface MarkmapEditorProps {
|
|||||||
/** Toolbar 上要展示的 item id 列表,默认使用 Toolbar.defaultItems */
|
/** Toolbar 上要展示的 item id 列表,默认使用 Toolbar.defaultItems */
|
||||||
toolbarItems?: string[]
|
toolbarItems?: string[]
|
||||||
/** 自定义按钮列表,会依次注册 */
|
/** 自定义按钮列表,会依次注册 */
|
||||||
customButtons?: ToolbarButton[]
|
customButtons?: any[]
|
||||||
/** 容器 SVG 的高度,默认为 600px */
|
/** 容器 SVG 的高度,默认为 600px */
|
||||||
height?: string
|
height?: string
|
||||||
|
/** 文档标题,用于导出HTML时的文件名 */
|
||||||
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MarkmapEditor({
|
export default function MarkmapEditor({
|
||||||
@@ -23,9 +25,10 @@ export default function MarkmapEditor({
|
|||||||
toolbarItems,
|
toolbarItems,
|
||||||
customButtons = [],
|
customButtons = [],
|
||||||
height = '600px',
|
height = '600px',
|
||||||
|
title = 'mindmap',
|
||||||
}: MarkmapEditorProps) {
|
}: MarkmapEditorProps) {
|
||||||
const svgRef = useRef<SVGSVGElement>(null)
|
const svgRef = useRef<SVGSVGElement>(null)
|
||||||
const mmRef = useRef<Markmap>()
|
const mmRef = useRef<Markmap | undefined>()
|
||||||
const toolbarRef = useRef<HTMLDivElement>(null)
|
const toolbarRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// 用于跟踪是否处于全屏状态
|
// 用于跟踪是否处于全屏状态
|
||||||
@@ -56,6 +59,158 @@ export default function MarkmapEditor({
|
|||||||
document.exitFullscreen()
|
document.exitFullscreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出HTML思维导图
|
||||||
|
const exportHtml = () => {
|
||||||
|
try {
|
||||||
|
const { root } = transformer.transform(value)
|
||||||
|
const data = JSON.stringify(root)
|
||||||
|
|
||||||
|
// 创建HTML内容
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${title || 'BiliNote思维导图'}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
#mindmap {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/markmap-view@0.18.10"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<svg id="mindmap"></svg>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
const { markmap } = window;
|
||||||
|
const { Markmap } = markmap;
|
||||||
|
const mm = Markmap.create(document.getElementById('mindmap'));
|
||||||
|
mm.setData(${data});
|
||||||
|
mm.fit();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${title || 'mindmap'}.html`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出HTML失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出PNG思维导图
|
||||||
|
const exportPng = () => {
|
||||||
|
try {
|
||||||
|
if (!svgRef.current) return;
|
||||||
|
|
||||||
|
const svgEl = svgRef.current;
|
||||||
|
|
||||||
|
// 获取SVG实际尺寸
|
||||||
|
const svgWidth = svgEl.width.baseVal.value || svgEl.clientWidth || 800;
|
||||||
|
const svgHeight = svgEl.height.baseVal.value || svgEl.clientHeight || 600;
|
||||||
|
|
||||||
|
// 设置足够大的缩放比例以确保高清输出
|
||||||
|
const scale = 3;
|
||||||
|
|
||||||
|
// 克隆SVG以避免修改原始SVG
|
||||||
|
const clonedSvg = svgEl.cloneNode(true) as SVGSVGElement;
|
||||||
|
|
||||||
|
// 设置SVG的背景为白色
|
||||||
|
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||||
|
style.textContent = 'svg { background-color: white; }';
|
||||||
|
clonedSvg.insertBefore(style, clonedSvg.firstChild);
|
||||||
|
|
||||||
|
// 确保SVG有正确的命名空间
|
||||||
|
clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||||
|
clonedSvg.setAttribute('width', svgWidth.toString());
|
||||||
|
clonedSvg.setAttribute('height', svgHeight.toString());
|
||||||
|
|
||||||
|
// 将SVG转换为Data URI (避免使用Blob URL来解决跨域问题)
|
||||||
|
const svgData = new XMLSerializer().serializeToString(clonedSvg);
|
||||||
|
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
|
||||||
|
const dataUri = `data:image/svg+xml;base64,${svgBase64}`;
|
||||||
|
|
||||||
|
// 创建Canvas
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = svgWidth * scale;
|
||||||
|
canvas.height = svgHeight * scale;
|
||||||
|
|
||||||
|
// 获取上下文并设置白色背景
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('无法获取Canvas上下文');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置白色背景
|
||||||
|
ctx.fillStyle = '#FFFFFF';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// 创建Image对象
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
// 当图片加载完成后,在Canvas上绘制并导出
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
// 应用缩放
|
||||||
|
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
||||||
|
|
||||||
|
// 绘制SVG
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// 重置变换
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
|
||||||
|
// 将Canvas转换为PNG Blob
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
// 创建下载链接
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${title || 'mindmap'}.png`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} else {
|
||||||
|
console.error('无法创建Blob对象');
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Canvas处理失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置图片加载错误处理
|
||||||
|
img.onerror = (error) => {
|
||||||
|
console.error('导出PNG失败(图片加载错误):', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始加载SVG图像 (使用Data URI而不是Blob URL)
|
||||||
|
img.src = dataUri;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出PNG失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化 Markmap 实例 + Toolbar
|
// 初始化 Markmap 实例 + Toolbar
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -82,14 +237,28 @@ export default function MarkmapEditor({
|
|||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
// 文本输入变化回调(如果你自行添加 textarea 编辑区)
|
// 文本输入变化回调(如果你自行添加 textarea 编辑区)
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
// const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onChange(e.target.value)
|
// onChange(e.target.value)
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full flex-col bg-white">
|
<div className="relative flex h-full flex-col bg-white">
|
||||||
{/* 全屏/退出全屏 按钮 */}
|
{/* 全屏/退出全屏 按钮 */}
|
||||||
<div className="absolute top-2 right-2 z-20 flex space-x-2">
|
<div className="absolute top-2 right-2 z-20 flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={exportPng}
|
||||||
|
className="rounded p-1 hover:bg-gray-200"
|
||||||
|
title="导出PNG思维导图"
|
||||||
|
>
|
||||||
|
🖼️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={exportHtml}
|
||||||
|
className="rounded p-1 hover:bg-gray-200"
|
||||||
|
title="导出HTML思维导图"
|
||||||
|
>
|
||||||
|
💾
|
||||||
|
</button>
|
||||||
{isFullscreen ? (
|
{isFullscreen ? (
|
||||||
<button
|
<button
|
||||||
onClick={exitFullscreen}
|
onClick={exitFullscreen}
|
||||||
|
|||||||
Reference in New Issue
Block a user