242 lines
6.1 KiB
React
242 lines
6.1 KiB
React
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|||
|
|
import { Transformer } from 'markmap-lib';
|
|||
|
|
import { Markmap } from 'markmap-view';
|
|||
|
|
import apiClient from '../utils/apiClient';
|
|||
|
|
import { API_ENDPOINTS } from '../config/api';
|
|||
|
|
import { Brain, Download, Loader } from 'lucide-react';
|
|||
|
|
|
|||
|
|
const MindMap = ({ meetingId, meetingTitle }) => {
|
|||
|
|
const [markdown, setMarkdown] = useState('');
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
const [hasSummary, setHasSummary] = useState(false);
|
|||
|
|
|
|||
|
|
const svgRef = useRef(null);
|
|||
|
|
const markmapRef = useRef(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const fetchSummary = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const endpoint = API_ENDPOINTS.MEETINGS.DETAIL(meetingId);
|
|||
|
|
const response = await apiClient.get(endpoint);
|
|||
|
|
const summary = response.data?.summary;
|
|||
|
|
|
|||
|
|
if (summary) {
|
|||
|
|
setMarkdown(summary);
|
|||
|
|
setHasSummary(true);
|
|||
|
|
} else {
|
|||
|
|
setMarkdown('# 暂无会议总结\n\n请先生成AI总结,才能查看思维导图。');
|
|||
|
|
setHasSummary(false);
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('Failed to fetch summary for mind map:', err);
|
|||
|
|
setError('无法加载会议总结内容。');
|
|||
|
|
setMarkdown('# 加载失败\n\n无法加载会议总结内容,请稍后重试。');
|
|||
|
|
setHasSummary(false);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (meetingId) {
|
|||
|
|
fetchSummary();
|
|||
|
|
}
|
|||
|
|
}, [meetingId]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (loading || !markdown || !svgRef.current) return;
|
|||
|
|
|
|||
|
|
const transformer = new Transformer();
|
|||
|
|
const { root } = transformer.transform(markdown);
|
|||
|
|
|
|||
|
|
if (markmapRef.current) {
|
|||
|
|
markmapRef.current.setData(root);
|
|||
|
|
} else {
|
|||
|
|
markmapRef.current = Markmap.create(svgRef.current, null, root);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
markmapRef.current.fit();
|
|||
|
|
|
|||
|
|
}, [markdown, loading]);
|
|||
|
|
|
|||
|
|
const handleExportPDF = async () => {
|
|||
|
|
if (!svgRef.current) {
|
|||
|
|
alert('思维导图尚未渲染,无法导出。');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 获取SVG元素
|
|||
|
|
const svgElement = svgRef.current;
|
|||
|
|
const svgHTML = svgElement.outerHTML;
|
|||
|
|
|
|||
|
|
if (!svgHTML.trim()) {
|
|||
|
|
alert('思维导图内容为空,无法导出。');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取SVG的尺寸
|
|||
|
|
const svgRect = svgElement.getBoundingClientRect();
|
|||
|
|
const svgWidth = svgRect.width || 800;
|
|||
|
|
const svgHeight = svgRect.height || 600;
|
|||
|
|
|
|||
|
|
// 创建一个隐藏的iframe用于打印
|
|||
|
|
const printFrame = document.createElement('iframe');
|
|||
|
|
printFrame.style.position = 'fixed';
|
|||
|
|
printFrame.style.width = '0';
|
|||
|
|
printFrame.style.height = '0';
|
|||
|
|
printFrame.style.border = 'none';
|
|||
|
|
printFrame.style.left = '-9999px';
|
|||
|
|
document.body.appendChild(printFrame);
|
|||
|
|
|
|||
|
|
// 创建HTML内容
|
|||
|
|
const htmlContent = `<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|||
|
|
<title>${meetingTitle || '会议思维导图'}</title>
|
|||
|
|
<style>
|
|||
|
|
@charset "UTF-8";
|
|||
|
|
@page {
|
|||
|
|
size: A4 landscape;
|
|||
|
|
margin: 15mm;
|
|||
|
|
}
|
|||
|
|
body {
|
|||
|
|
font-family: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 20px;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
min-height: 90vh;
|
|||
|
|
}
|
|||
|
|
h1 {
|
|||
|
|
color: #2563eb;
|
|||
|
|
font-size: 24px;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
.mindmap-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 500px;
|
|||
|
|
border: 1px solid #e5e7eb;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: white;
|
|||
|
|
}
|
|||
|
|
svg {
|
|||
|
|
max-width: 100%;
|
|||
|
|
max-height: 100%;
|
|||
|
|
width: auto;
|
|||
|
|
height: auto;
|
|||
|
|
}
|
|||
|
|
/* 脑图节点样式 */
|
|||
|
|
.markmap-node circle {
|
|||
|
|
fill: #3b82f6;
|
|||
|
|
stroke: #1d4ed8;
|
|||
|
|
stroke-width: 2;
|
|||
|
|
}
|
|||
|
|
.markmap-node text {
|
|||
|
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
|||
|
|
font-size: 14px;
|
|||
|
|
fill: #1e293b;
|
|||
|
|
}
|
|||
|
|
.markmap-link {
|
|||
|
|
stroke: #6b7280;
|
|||
|
|
stroke-width: 2;
|
|||
|
|
fill: none;
|
|||
|
|
}
|
|||
|
|
.footer-section {
|
|||
|
|
margin-top: 30px;
|
|||
|
|
padding-top: 15px;
|
|||
|
|
border-top: 1px solid #e5e7eb;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #6b7280;
|
|||
|
|
text-align: center;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
@media print {
|
|||
|
|
body { padding: 0; }
|
|||
|
|
h1 { page-break-before: avoid; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<h1>${meetingTitle || '会议思维导图'}</h1>
|
|||
|
|
<div class="mindmap-container">
|
|||
|
|
${svgHTML}
|
|||
|
|
</div>
|
|||
|
|
<div class="footer-section">
|
|||
|
|
<p>导出时间:${new Date().toLocaleString('zh-CN')}</p>
|
|||
|
|
</div>
|
|||
|
|
</body>
|
|||
|
|
</html>`;
|
|||
|
|
|
|||
|
|
// 使用Blob创建URL以确保正确的编码
|
|||
|
|
const blob = new Blob([htmlContent], { type: 'text/html; charset=UTF-8' });
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
|
|||
|
|
// 设置iframe的src为blob URL
|
|||
|
|
printFrame.src = url;
|
|||
|
|
|
|||
|
|
// 等待iframe加载完成
|
|||
|
|
printFrame.onload = () => {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
try {
|
|||
|
|
// 执行打印
|
|||
|
|
printFrame.contentWindow.focus();
|
|||
|
|
printFrame.contentWindow.print();
|
|||
|
|
} catch(e) {
|
|||
|
|
console.error("Print failed:", e);
|
|||
|
|
alert("导出PDF失败,您的浏览器可能阻止了打印操作。");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理资源
|
|||
|
|
setTimeout(() => {
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
document.body.removeChild(printFrame);
|
|||
|
|
}, 2000);
|
|||
|
|
}, 500);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('PDF导出失败:', error);
|
|||
|
|
alert('PDF导出失败,请重试。');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className="mindmap-loading">
|
|||
|
|
<Loader className="animate-spin" />
|
|||
|
|
<p>正在加载思维导图...</p>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mindmap-container">
|
|||
|
|
<div className="mindmap-header">
|
|||
|
|
<h3><Brain size={18} /> 思维导图</h3>
|
|||
|
|
{hasSummary && (
|
|||
|
|
<button onClick={handleExportPDF} className="export-pdf-btn-main" disabled={loading || !!error}>
|
|||
|
|
<Download size={16} />
|
|||
|
|
<span>导出PDF</span>
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="markmap-render-area">
|
|||
|
|
<svg ref={svgRef} style={{ width: '100%', height: '100%' }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default MindMap;
|