当前位置: 首页 > news >正文

vue3使用mermaid生成图表,并可编辑

效果图

实际代码


<template><div class="mermaid-container" style="z-index: 99999" ref="wrapperRef"><!-- 控制栏 --><div class="control-bar"><div class="control-bar-flex control-bar-tab-wrap"><div :class="['control-bar-tab-item', showCode ? '' : 'control-bar-tab-item-active']" @click="toggleCode">图表</div><div :class="['control-bar-tab-item', showCode ? 'control-bar-tab-item-active' : '']" @click="toggleCode">代码</div></div><div class="control-bar-flex"><div v-if="showCode"><div class="control-bar-dropdown" style="margin-right: 8px" @click="onCopy"><icon-park type="copy" size="16" theme="outline" fill="rgba(82,82,82)" style="margin-right: 4px"></icon-park><span>复制</span></div></div><template v-else><el-tooltip class="box-item" effect="dark" content="缩小" placement="top"><icon-park type="zoom-out" size="16" theme="outline" fill="rgba(82,82,82)" @click="zoomOut" style="margin-right: 16px; cursor: pointer"></icon-park></el-tooltip><el-tooltip class="box-item" effect="dark" content="放大" placement="top"><icon-park type="zoom-in" size="16" theme="outline" fill="rgba(82,82,82)" @click="zoomIn" style="margin-right: 16px; cursor: pointer"></icon-park></el-tooltip><div class="control-bar-line"></div><div class="control-bar-dropdown" @click="editHandle" style="margin-right: 4px"><icon-park type="edit" size="16" theme="outline" fill="rgba(82,82,82)" style="margin-right: 4px"></icon-park><span>编辑</span></div></template><el-dropdown trigger="click"><div class="control-bar-dropdown"><icon-park type="download" size="16" theme="outline" fill="rgba(82,82,82)" style="margin-right: 4px"></icon-park><span>下载</span></div><template #dropdown><el-dropdown-menu><el-dropdown-item @click="downloadSVG">下载 SVG</el-dropdown-item><el-dropdown-item @click="downloadPNG">下载 PNG</el-dropdown-item><el-dropdown-item @click="downloadCode">下载代码</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></div><!-- 代码/图表切换 --><pre class="mermaid-code" v-if="showCode"><code class="hljs code-block-body">{{ code }}</code></pre><div v-else id="graphContainer" class="mermaid-chart" @mousedown="startDrag" @mouseup="stopDrag" @mouseleave="stopDrag" @mousemove="dragFlowchart"><div id="custom-output" class="mermaid-chart-container" ref="mermaidContainer" :style="{ transform: `translate3d(0, ${offsetY}px, 0) scale(${scale})`, maxWidth: `${maxWidth}px` }"><!-- {{ code }} --></div></div><div id="diagram" class="mermaid-container" style="float: left; width: 1px; height: 1px; overflow: hidden"><svg xmlns="http://www.w3.org/2000/svg" id="abc" width="200" height="200"><g transform="translate(50,50)"></g></svg></div><el-dialog v-model="showEditDialog" title="编辑" width="98%" @close="closeEditDialog" modal-class="drawioFrame-dialog" :close-on-click-modal="false"><iframe ref="drawioFrame" :src="iframeSrc" style="width: calc(100% - 4px); height: calc(100% - 4px); border: 0"></iframe><template #footer><!-- <el-button @click="showEditDialog = false">关闭编辑器</el-button> --></template></el-dialog></div>
</template><script setup lang="ts" name="MermaidDiagram">
import { ref, onMounted, nextTick, watch, inject } from 'vue'
import hljs from 'highlight.js'const props = defineProps({code: {type: String,required: true},dialogVisible: {type: Boolean,default: false}
})const onceMessage = inject('$onceMessage')
const showCode = ref(false)
const scale = ref(1)
const mermaidContainer = ref(null)
const maxWidth = ref(0)
const wrapperRef = ref(null)
const svgHeight = ref(0)
const svgWidth = ref(0)
const isDragging = ref(false)
const offsetY = ref(0) // 纵向偏移量
const startY = ref(0) // 鼠标按下时的初始Y坐标
const showEditDialog = ref(false)
const drawioFrame = ref(null)
const iframeSrc = ref('https://embed.diagrams.net/?embed=1&ui=atlas&spin=1&proto=json')
const baseXml = ref(``)
const isDrawioReady = ref(false)// 合并初始化监听(优化逻辑)
const initMessageHandler = () => {// 确保每次对话框打开时重新监听window.addEventListener('message', handleDrawioMessage)
}// 初始化Mermaid
onMounted(() => {refreshMermaid()initMermaid()calculateMaxWidth()window.addEventListener('resize', calculateMaxWidth)initMessageHandler()
})const calculateMaxWidth = () => {if (wrapperRef.value) {maxWidth.value = wrapperRef.value.clientWidth}
}// 监听显示状态变化
watch(() => showCode.value,newVal => {if (newVal) {nextTick(() => {// 具体高亮方式选其一// 方式1:全局重新高亮hljs.highlightAll()// 方式2:定向高亮(推荐)const codeBlocks = document.querySelectorAll('.mermaid-code code')codeBlocks.forEach(block => {hljs.highlightElement(block)})})}}
)// 监听显示状态变化
// watch(
//     () => [props.code,props.dialogVisible],
//     newVal => {
//         if (newVal[0] && newVal[1]) {
//             refreshMermaid()
//             initMermaid()
//         }
//     }
// )const initMermaid = () => {mermaid.initialize({startOnLoad: false,theme: 'default',flowchart: {useMaxWidth: false,htmlLabels: true,curve: 'basis'},securityLevel: 'loose' // 允许SVG操作})renderDiagram()
}const renderDiagram = async () => {await nextTick()if (mermaidContainer.value) {try {// 使用 mermaid.init 生成流程图,并传入回调函数// mermaid.init(undefined, mermaidContainer.value, () => {//     // 在回调函数中获取高度//     const svgElement = mermaidContainer.value.querySelector('svg')//     if (svgElement) {//         // 获取 viewBox 高度,这是 Mermaid 渲染后的实际高度//         const viewBox = svgElement.viewBox?.baseVal//         const height = viewBox?.height || svgElement.getBoundingClientRect().height//         const width = viewBox?.width || svgElement.getBoundingClientRect().width//         console.log('Mermaid渲染后高度:', height)//         console.log('Mermaid渲染后width:', width)//         svgHeight.value = height//         svgWidth.value = width//         // 计算缩放比例//         if (height > 546) {//             const targetScale = 546 / height//             scale.value = Math.min(targetScale, 1)//             console.log(`自动缩放: 原始高度${height}px, 缩放比例${targetScale.toFixed(2)}`)//         } else {//             scale.value = 1//         }//     }// })const input = props.codeconst outputId = 'mermaid-' + Math.random().toString(36).substr(2, 9)// 使用render方法渲染图表mermaid.render(outputId, input).then(result => {mermaidContainer.value.innerHTML = result.svgsetTimeout(() => {// 在回调函数中获取高度const svgElement = mermaidContainer.value.querySelector('svg')if (svgElement) {// 获取 viewBox 高度,这是 Mermaid 渲染后的实际高度const viewBox = svgElement.viewBox?.baseValconst height = viewBox?.height || svgElement.getBoundingClientRect().heightconst width = viewBox?.width || svgElement.getBoundingClientRect().widthconsole.log('Mermaid渲染后高度:', height)console.log('Mermaid渲染后width:', width)svgHeight.value = heightsvgWidth.value = width//     // 计算缩放比例if (height > 546) {const targetScale = 546 / heightscale.value = Math.min(targetScale, 1)console.log(`自动缩放: 原始高度${height}px, 缩放比例${targetScale.toFixed(2)}`)} else {scale.value = 1}}}, 0)}).catch(error => {console.error('渲染错误:', error)mermaidContainer.value.innerHTML = '<div style="color: red; padding: 10px; background-color: #ffe6e6;">渲染错误: ' + error.message + '</div>'})} catch (err) {console.error('Mermaid渲染错误:', err)}}
}// 控制方法
const toggleCode = () => {showCode.value = !showCode.valuesetTimeout(() => {if (!showCode.value) {refreshMermaid()initMermaid()}}, 0)
}const refreshMermaid = () => {scale.value = 1isDragging.value = falseoffsetY.value = 0startY.value = 0showCode.value = false
}const draw = async (diag: any) => {let conf5 = mermaid.getConfig2()if (diag.db.getData) {const data4Layout = diag.db.getData()const direction = diag.db.getDirection()data4Layout.type = diag.typedata4Layout.layoutAlgorithm = 'dagre'data4Layout.direction = directiondata4Layout.nodeSpacing = conf5?.nodeSpacing || 50data4Layout.rankSpacing = conf5?.rankSpacing || 50data4Layout.markers = ['point', 'circle', 'cross']data4Layout.diagramId = 'id-111'return data4Layout}return null
}const svgToImage = (svgString: any, format = 'png') => {return new Promise((resolve, reject) => {try {// 创建SVG图像const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' })const svgUrl = URL.createObjectURL(svgBlob)// 加载图像const img = new Image()img.onload = function () {try {// 创建canvasconst canvas = document.createElement('canvas')const ctx = canvas.getContext('2d')canvas.width = img.widthcanvas.height = img.heightctx.drawImage(img, 0, 0)const imageData = canvas.toDataURL(`image/${format}`, 0.9)URL.revokeObjectURL(svgUrl)resolve({data: imageData,width: img.width,height: img.height})} catch (err) {reject(`转换canvas时出错: ${err.message}`)}}img.onerror = function () {URL.revokeObjectURL(svgUrl)reject('SVG加载失败')}img.src = svgUrl} catch (err) {reject(`SVG解析错误: ${err.message}`)}})
}const svgToBase64 = (svgContent: any) => {// 处理Unicode字符const bytes = new TextEncoder().encode(svgContent)const binString = Array.from(bytes, byte => String.fromCharCode(byte)).join('')const base64 = btoa(binString)return `shape=image;noLabel=1;verticalAlign=top;imageAspect=1;image=data:image/svg+xml,${base64}`
}const createDrawIoXml = (imageData: any, width: any, height: any, text: any) => {const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {const r = (Math.random() * 16) | 0const v = c === 'x' ? r : (r & 0x3) | 0x8return v.toString(16)})// 创建draw.io XML文件内容let xml = ''xml += '<mxGraphModel dx="0" dy="0" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + width + '" pageHeight="' + height + '">\n'xml += '  <root>\n'xml += '    <mxCell id="0" />\n'xml += '    <mxCell id="1" parent="0" />\n'xml += '    <UserObject label="" mermaidData="' + text + '" id="' + uuid + '">\n'xml += '      <mxCell style="' + imageData + ';" vertex="1" parent="1">\n'xml += '         <mxGeometry x="0" y="0" width="' + width + '" height="' + height + '" as="geometry" />\n'xml += '      </mxCell>\n'xml += '    </UserObject>\n'xml += '  </root>\n'xml += '</mxGraphModel>\n'return xml
}const start = async () => {let mermaidText = props.codeconst diagram = await mermaid.mermaidAPI.getDiagramFromText(mermaidText)// console.log('完整的解析结果:', diagram)let data4Layout = await draw(diagram)if (data4Layout) {console.log('完整的解析结果:', data4Layout)console.log(mermaid.select_default2('#diagram'))const ss = mermaid.select_default2('#abc')let ddd = await mermaid.render3(data4Layout, ss)const drawio = mxMermaidToDrawio(ddd, data4Layout.type, {})// console.log('===========drawio', drawio)return drawio} else {console.log('没有解析到数据')const result = await mermaid.render('diagram2', mermaidText)const imageResult = await svgToImage(result.svg)const data = svgToBase64(result.svg)console.log(imageResult)let d = { data: mermaidText }const jsonString = JSON.stringify(d)const escapedJson = jsonString.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')console.log(escapedJson)const drawioXml = createDrawIoXml(data, svgWidth.value, svgHeight.value, escapedJson)console.log('=========drawioXml', drawioXml)return drawioXml}
}const editHandle = async () => {baseXml.value = await start()showEditDialog.value = trueawait nextTick()
}// 2. 监听 draw.io 的初始化完成事件
const handleDrawioMessage = event => {if (event.data === '{"event":"init"}') {isDrawioReady.value = truetryLoadDiagram()}
}// 3. 只有 iframe 和 draw.io 都就绪时,才发送 XML
const tryLoadDiagram = () => {if (isDrawioReady.value) {loadDiagram()}
}// 调整对话框关闭事件处理
const closeEditDialog = () => {showEditDialog.value = falseisDrawioReady.value = false // 重置状态initMessageHandler() // 重新绑定监听以便下次使用
}const loadDiagram = () => {const frame = drawioFrame.valueif (frame) {frame.contentWindow.postMessage(JSON.stringify({action: 'load',xml: baseXml.value}),'*')}
}const zoomIn = () => {// scale.value = Math.min(scale.value + 0.1, 2)const newScale = scale.value + 0.1const scaledWidth = (mermaidContainer.value?.scrollWidth || 0) * newScale// 只有当缩放后宽度小于容器宽度时才允许放大if (scaledWidth <= maxWidth.value) {scale.value = newScale} else {// 自动调整到最大允许比例scale.value = maxWidth.value / (mermaidContainer.value?.scrollWidth || maxWidth.value)}
}const zoomOut = () => {scale.value = Math.max(scale.value - 0.1, 0.5)
}const downloadSVG = () => {const svg = mermaidContainer.value?.querySelector('svg')if (!svg) returnconst serializer = new XMLSerializer()const source = serializer.serializeToString(svg)const svgBlob = new Blob([source], { type: 'image/svg+xml' })downloadFile(svgBlob, 'diagram.svg')
}const downloadPNG = async () => {const svg = mermaidContainer.value?.querySelector('svg')if (!svg) returnconst canvas = document.createElement('canvas')const ctx = canvas.getContext('2d')const data = new XMLSerializer().serializeToString(svg)const img = new Image()img.onload = () => {canvas.width = svgWidth.valuecanvas.height = svgHeight.valuectx.drawImage(img, 0, 0)canvas.toBlob(blob => {downloadFile(blob, 'diagram.png')}, 'image/png')}img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(data)))
}const downloadCode = async () => {// 创建 Blob 对象const blob = new Blob([props.code], { type: 'text/plain' })downloadFile(blob, 'diagram.mermaid')
}const downloadFile = (blob, filename) => {const link = document.createElement('a')link.href = URL.createObjectURL(blob)link.download = filenamedocument.body.appendChild(link)link.click()document.body.removeChild(link)
}const onCopy = () => {const textarea = document.createElement('textarea') // 直接构建textareatextarea.value = `${props.code}` // 设置内容document.body.appendChild(textarea) // 添加临时实例textarea.select() // 选择实例内容document.execCommand('Copy') // 执行复制document.body.removeChild(textarea) // 删除临时实例onceMessage.success('复制成功')
}// 开始拖动
const startDrag = e => {isDragging.value = truestartY.value = e.clientYdocument.body.style.cursor = 'grab' // 五指抓取手势document.body.style.userSelect = 'none' // 禁用文本选择
}// 停止拖动
const stopDrag = () => {if (!isDragging.value) returnisDragging.value = falsedocument.body.style.cursor = '' // 恢复默认光标document.body.style.userSelect = '' // 恢复文本选择
}// 拖动中计算位置
const dragFlowchart = e => {if (!isDragging.value) returnconst deltaY = e.clientY - startY.valueoffsetY.value += deltaYstartY.value = e.clientY // 更新当前鼠标位置// 根据缩放比例动态计算最大偏移量const containerHeight = 546 // 容器固定高度const scaledHeight = svgHeight.value * scale.value // 缩放后的实际高度const maxOffset = Math.max(0, scaledHeight - containerHeight) // 计算最大可偏移量// 限制拖动边界offsetY.value = Math.max(-maxOffset, Math.min(maxOffset, offsetY.value))
}
</script><style lang="scss">
.drawioFrame-dialog {max-height: calc(100vh) !important;.ep-dialog {padding: 4px;height: calc(96vh) !important;}.ep-dialog__header {margin-bottom: 2px;}.ep-dialog__headerbtn {top: -4px;}.ep-dialog__body {padding: 0 !important;margin: 0 !important;max-height: 100% !important;height: calc(96vh - 24px - 8px) !important;}.ep-dialog__footer {display: none;}
}
</style>
<style lang="scss" scoped>
.mermaid-container {// border: 1px solid #eee;border-radius: 12px;// padding: 10px;// margin: 20px 0;background-color: #fafafa;color: #494949;.control-bar {display: flex;border-radius: 12px 12px 0 0;justify-content: space-between;display: flex;padding: 6px 14px 6px 6px;background-color: #f5f5f5;.control-bar-flex {display: flex;justify-content: center;align-items: center;color: rgba(82, 82, 82);}.control-bar-tab-wrap {background-color: rgba(0, 0, 0, 0.03);border-radius: 8px;}.control-bar-tab-item {height: 26px;font-size: 12px;white-space: nowrap;cursor: pointer;border-radius: 8px;flex: 1;justify-content: center;align-items: center;padding: 0 14px;font-weight: 400;display: flex;position: relative;}.control-bar-tab-item-active {display: flex;background-color: #fff;font-weight: 600;box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.02), 0 6px 10px 0 rgba(0, 0, 0, 0.04);}.control-bar-line {width: 1px;height: 14px;margin-right: 12px;background-color: rgba(187, 187, 187, 1);}.control-bar-dropdown {cursor: pointer;display: flex;align-items: center;cursor: pointer;padding: 0px 4px;height: 28px;font-size: 12px;color: rgba(82, 82, 82);border-radius: 12px;&:hover {background-color: rgba(0, 0, 0, 0.04);}}}
}.mermaid-chart {overflow: auto;background-color: #fafafa;border-radius: 0 0 12px 12px;overflow: hidden;max-height: 546px;display: flex;justify-content: center;align-items: center;user-select: none; // 禁用文本选择will-change: transform; // 提示浏览器优化渲染.mermaid-chart-container {will-change: transform; // 提示浏览器优化渲染user-select: none; // 禁用文本选择}/* 鼠标按下时切换为激活手势 */.mermaid-flowchart:active {cursor: grabbing;}
}.mermaid-chart > div {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;
}.mermaid-code {background-color: #fafafa;padding: calc(9.144px) calc(13.716px) calc(9.144px) calc(13.716px);white-space: pre-wrap;overflow: auto;text-align: left;border-radius: 0 0 12px 12px;font-size: 12px;
}.icon {font-style: normal;
}
</style>

http://www.dtcms.com/a/273112.html

相关文章:

  • 数学建模:多目标规划:ε约束法、 理想点法
  • 【大模型推理论文阅读】Enhancing Latent Computation in Transformerswith Latent Tokens
  • pharokka phold--快速噬菌体注释工具
  • 深入了解 Vim 编辑器:从入门到精通
  • MySQL高级特性全面解析:约束、表关系、多表查询与事务
  • 深入剖析C++ RPC框架原理:有栈协程与分布式系统设计
  • 技术学习_检索增强生成(RAG)
  • QT数据交互全解析:JSON处理与HTTP通信
  • 云原生技术与应用-Docker高级管理--Dockerfile镜像制作
  • 西部数据WD授权代理商-深圳同袍存储科技有限公司
  • 医学+AI!湖北中医药大学信息工程学院与和鲸科技签约101数智领航计划
  • Web后端开发工程师AI协作指南
  • 龙迅#LT7911E适用于TPYE-C/DP/EDP转MIPIDSI/LVDS应用功能,支持DSC 分辨率缩放,分辨率高达4K60HZ!
  • 寒武纪MLU370编程陷阱:float32精度丢失的硬件级解决方案——混合精度训练中的定点数补偿算法设计
  • Linux指令与权限
  • uniapp滚动组件, HuimayunScroll:高性能移动端滚动组件的设计与实现
  • window显示驱动开发—XR_BIAS 和 PresentDXGI
  • Spring原理揭秘--ApplicationContext(二)
  • bRPC源码解析:深入理解bthread协程机制与上下文切换的底层实现
  • 单相/三相可选:光伏并网双向计量电表技术白皮书
  • 【研报复现】方正金工:(1)适度冒险 因子
  • 【网络】Linux 内核优化实战 - net.ipv4.tcp_keepalive_intv
  • Linux 命令行与 shell 脚本编程大全4版学习-1了解Linux
  • tk.mybatis多层括号嵌套SQL查询
  • 本地部署文档管理系统 Paperless-ngx 并实现外部访问
  • 腾讯云分为几个区域
  • K线连续涨跌统计与分析工具
  • C++的类中的虚拟继承【底层剖析(配图解)】
  • Java多线程:核心技术与实战指南
  • 鸿蒙智行6月交付新车52747辆 单日交付量3651辆