Files
xinmi/website-docs/.vitepress/theme/MermaidZoom.vue
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

233 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
const MIN = 0.3
const MAX = 8
const open = ref(false)
const scale = ref(1)
const tx = ref(0)
const ty = ref(0)
const stage = ref<HTMLElement>()
let dragging = false
let originX = 0
let originY = 0
function reset() {
scale.value = 1
tx.value = 0
ty.value = 0
}
function close() {
open.value = false
document.documentElement.style.removeProperty('overflow')
}
async function show(source: SVGElement) {
reset()
open.value = true
document.documentElement.style.overflow = 'hidden'
await nextTick()
const host = stage.value
if (!host) return
host.replaceChildren()
// mermaid 的 svg 内联样式以 #id 作用域,且插件会对同 id 元素重新渲染,
// 因此副本必须换一个 id 并同步改写内部样式表
const clone = source.cloneNode(true) as SVGElement
const oldId = source.id
if (oldId) {
const newId = `${oldId}-zoom`
clone.id = newId
clone.querySelectorAll('style').forEach((s) => {
s.textContent = (s.textContent ?? '').split(`#${oldId}`).join(`#${newId}`)
})
}
clone.removeAttribute('width')
clone.removeAttribute('height')
clone.style.maxWidth = 'none'
clone.style.width = '100%'
clone.style.height = '100%'
host.appendChild(clone)
}
function onDocClick(e: MouseEvent) {
const block = (e.target as HTMLElement | null)?.closest?.('.vp-doc .mermaid')
if (!block) return
const svg = block.querySelector('svg')
if (svg) show(svg as SVGElement)
}
function onKey(e: KeyboardEvent) {
if (!open.value) return
if (e.key === 'Escape') close()
if (e.key === '0') reset()
}
function zoomBy(factor: number) {
scale.value = Math.min(MAX, Math.max(MIN, scale.value * factor))
}
function onWheel(e: WheelEvent) {
e.preventDefault()
zoomBy(e.deltaY < 0 ? 1.12 : 1 / 1.12)
}
function onDown(e: MouseEvent) {
dragging = true
originX = e.clientX - tx.value
originY = e.clientY - ty.value
}
function onMove(e: MouseEvent) {
if (!dragging) return
tx.value = e.clientX - originX
ty.value = e.clientY - originY
}
function onUp() {
dragging = false
}
onMounted(() => {
document.addEventListener('click', onDocClick)
document.addEventListener('keydown', onKey)
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
})
onUnmounted(() => {
document.removeEventListener('click', onDocClick)
document.removeEventListener('keydown', onKey)
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
document.documentElement.style.removeProperty('overflow')
})
</script>
<template>
<Teleport to="body">
<Transition name="zoom-fade">
<div v-if="open" class="zoom" @wheel="onWheel" @mousedown.self="close">
<div
ref="stage"
class="zoom-stage"
:style="{ transform: `translate(${tx}px, ${ty}px) scale(${scale})` }"
@mousedown.stop="onDown"
/>
<div class="zoom-bar" @mousedown.stop>
<button title="缩小" @click="zoomBy(1 / 1.35)">−</button>
<span class="zoom-level">{{ Math.round(scale * 100) }}%</span>
<button title="放大" @click="zoomBy(1.35)">+</button>
<span class="zoom-sep" />
<button class="zoom-word" @click="reset">重置</button>
<button class="zoom-word" @click="close">关闭</button>
</div>
<p class="zoom-hint">滚轮缩放 · 拖拽平移 · Esc 关闭</p>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.zoom {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: var(--wk-paper);
cursor: zoom-out;
overscroll-behavior: contain;
}
.zoom-stage {
width: min(1400px, 88vw);
height: 82vh;
cursor: grab;
transform-origin: center center;
}
.zoom-stage:active {
cursor: grabbing;
}
.zoom-bar {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
border: 1px solid var(--wk-rule);
border-radius: 3px;
background: var(--wk-paper);
box-shadow: 0 6px 22px rgba(16, 31, 56, 0.08);
cursor: default;
}
.zoom-bar button {
min-width: 30px;
height: 28px;
padding: 0 8px;
border-radius: 2px;
font-size: 15px;
line-height: 1;
color: var(--wk-ink-soft);
transition: background-color 0.18s ease, color 0.18s ease;
}
.zoom-bar button:hover {
background: var(--wk-paper-2);
color: var(--wk-ink);
}
.zoom-word {
font-size: 12.5px !important;
}
.zoom-level {
min-width: 46px;
text-align: center;
font-family: var(--wk-font-mono);
font-size: 11px;
color: var(--wk-ink-mute);
}
.zoom-sep {
width: 1px;
height: 16px;
margin: 0 4px;
background: var(--wk-rule);
}
.zoom-hint {
position: fixed;
top: 26px;
left: 50%;
transform: translateX(-50%);
margin: 0;
font-family: var(--wk-font-mono);
font-size: 10.5px;
letter-spacing: 0.14em;
color: var(--wk-ink-mute);
}
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: opacity 0.18s ease;
}
.zoom-fade-enter-from,
.zoom-fade-leave-to {
opacity: 0;
}
</style>