222 lines
5.1 KiB
Vue
222 lines
5.1 KiB
Vue
<template>
|
|
<div class="mermaid-wrapper" ref="wrapperRef">
|
|
<div class="mermaid svg-mermaid" ref="mermaidRef" :data-graph="graph" @click="openPreview"></div>
|
|
<button class="mermaid-expand" @click.stop="openPreview" title="全屏预览">
|
|
<Maximize2 :size="8" />
|
|
</button>
|
|
<ImagePreview :visible="showPreview" :html="renderedHtml" @close="showPreview = false" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { Maximize2 } from '@lucide/vue'
|
|
import ImagePreview from './ImagePreview.vue'
|
|
|
|
const props = defineProps({
|
|
graph: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
id: {
|
|
type: String,
|
|
default: () => `mermaid-${Math.random().toString(36).slice(2, 9)}`,
|
|
},
|
|
})
|
|
|
|
const wrapperRef = ref(null)
|
|
const mermaidRef = ref(null)
|
|
const showPreview = ref(false)
|
|
const renderedHtml = ref('')
|
|
let mut = null
|
|
|
|
// Zoom/pan state
|
|
let scale = 1
|
|
let translateX = 0
|
|
let translateY = 0
|
|
let isDragging = false
|
|
let lastX = 0
|
|
let lastY = 0
|
|
|
|
const updateTransform = () => {
|
|
if (!mermaidRef.value) return
|
|
mermaidRef.value.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`
|
|
mermaidRef.value.style.transformOrigin = '0 0'
|
|
}
|
|
|
|
const onWheel = (e) => {
|
|
e.preventDefault()
|
|
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
|
const newScale = Math.min(Math.max(scale * delta, 0.2), 5)
|
|
|
|
const rect = wrapperRef.value.getBoundingClientRect()
|
|
const cx = e.clientX - rect.left
|
|
const cy = e.clientY - rect.top
|
|
|
|
translateX = cx - (cx - translateX) * (newScale / scale)
|
|
translateY = cy - (cy - translateY) * (newScale / scale)
|
|
scale = newScale
|
|
|
|
updateTransform()
|
|
}
|
|
|
|
const onMouseDown = (e) => {
|
|
if (e.button !== 0) return
|
|
isDragging = true
|
|
lastX = e.clientX
|
|
lastY = e.clientY
|
|
wrapperRef.value.style.cursor = 'grabbing'
|
|
}
|
|
|
|
const onMouseMove = (e) => {
|
|
if (!isDragging) return
|
|
translateX += e.clientX - lastX
|
|
translateY += e.clientY - lastY
|
|
lastX = e.clientX
|
|
lastY = e.clientY
|
|
updateTransform()
|
|
}
|
|
|
|
const onMouseUp = () => {
|
|
isDragging = false
|
|
if (wrapperRef.value) wrapperRef.value.style.cursor = 'grab'
|
|
}
|
|
|
|
const onDblClick = () => {
|
|
scale = 1
|
|
translateX = 0
|
|
translateY = 0
|
|
updateTransform()
|
|
}
|
|
|
|
function openPreview() {
|
|
if (mermaidRef.value && mermaidRef.value.innerHTML) {
|
|
renderedHtml.value = mermaidRef.value.innerHTML
|
|
showPreview.value = true
|
|
}
|
|
}
|
|
|
|
const renderMermaid = async () => {
|
|
if (!mermaidRef.value) return
|
|
|
|
try {
|
|
const mermaid = (await import('mermaid')).default
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default',
|
|
securityLevel: 'loose',
|
|
})
|
|
|
|
const graphData = props.graph || mermaidRef.value.getAttribute('data-graph') || ''
|
|
if (!graphData) return
|
|
|
|
const decoded = decodeURIComponent(graphData)
|
|
const { svg } = await mermaid.render(props.id, decoded)
|
|
mermaidRef.value.innerHTML = svg
|
|
|
|
// Reset zoom/pan on re-render
|
|
scale = 1
|
|
translateX = 0
|
|
translateY = 0
|
|
updateTransform()
|
|
} catch (error) {
|
|
console.error('Mermaid render error:', error)
|
|
mermaidRef.value.innerHTML = `<pre style="color: red;">Mermaid Error: ${error.message}</pre>`
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
renderMermaid()
|
|
|
|
mut = new MutationObserver(() => renderMermaid())
|
|
mut.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
|
|
const el = wrapperRef.value
|
|
if (el) {
|
|
el.addEventListener('wheel', onWheel, { passive: false })
|
|
el.addEventListener('mousedown', onMouseDown)
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
window.addEventListener('mouseup', onMouseUp)
|
|
el.addEventListener('dblclick', onDblClick)
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (mut) mut.disconnect()
|
|
|
|
const el = wrapperRef.value
|
|
if (el) {
|
|
el.removeEventListener('wheel', onWheel)
|
|
el.removeEventListener('mousedown', onMouseDown)
|
|
el.removeEventListener('dblclick', onDblClick)
|
|
}
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
window.removeEventListener('mouseup', onMouseUp)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mermaid-wrapper {
|
|
overflow: hidden;
|
|
cursor: grab;
|
|
border: 1px solid var(--vp-c-divider);
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
margin: 24px 0;
|
|
position: relative;
|
|
width: 100%;
|
|
min-height: 200px;
|
|
}
|
|
|
|
.mermaid-wrapper:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.mermaid {
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.mermaid :deep(.svg-mermaid) {
|
|
width: 100%;
|
|
height: auto;
|
|
min-width: 600px;
|
|
}
|
|
|
|
.mermaid-expand {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
width: 28px !important;
|
|
height: 28px !important;
|
|
min-width: 28px !important;
|
|
min-height: 28px !important;
|
|
max-width: 28px !important;
|
|
max-height: 28px !important;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid var(--vp-c-divider);
|
|
border-radius: 6px;
|
|
background: var(--vp-c-bg);
|
|
color: var(--vp-c-text-2);
|
|
cursor: pointer;
|
|
/* opacity: 0;
|
|
transition: opacity 0.2s;*/
|
|
z-index: 2;
|
|
flex-shrink: 0;
|
|
padding: 0;
|
|
margin: 0;
|
|
line-height: 1;
|
|
}
|
|
/*
|
|
.mermaid-wrapper:hover .mermaid-expand {
|
|
opacity: 1;
|
|
}*/
|
|
|
|
.mermaid-expand:hover {
|
|
border-color: var(--vp-c-brand-1);
|
|
color: var(--vp-c-brand-1);
|
|
}
|
|
</style>
|