feat(editor): 添加图片预览功能并支持缩放

- 在编辑器点击图片时显示全屏预览弹窗
- 支持通过鼠标滚轮缩放预览图片(0.1-5倍)
- 使用Teleport将弹窗渲染到body避免样式冲突
- 添加淡入动画和关闭按钮提升用户体验
This commit is contained in:
cfq 2026-01-28 18:48:36 +08:00
parent 84af4bd798
commit 3373abe563
1 changed files with 114 additions and 2 deletions

View File

@ -1,12 +1,29 @@
<template>
<div class="live-editor">
<div class="live-editor" @click="handleEditorClick">
<EditorToolbar @action="handleToolbarAction" />
<codemirror v-model="code" placeholder="请输入 Markdown 内容..." :style="{ height: '0', flex: 1, fontSize: 'var(--editor-font-size)', color: 'var(--primary-color)' }" :autofocus="true" :indent-with-tab="true" :tab-size="2" :extensions="extensions" @ready="handleReady" @change="handleChange" />
<!-- 图片预览弹窗 -->
<Teleport to="body">
<div v-if="previewImage.visible" class="image-preview-modal" @click="closePreview" @wheel.prevent="handlePreviewWheel">
<div class="preview-controls">
<span class="close-btn" @click.stop="closePreview" title="关闭">×</span>
</div>
<div class="preview-content" @click.stop>
<img
:src="previewImage.url"
:style="{ transform: `scale(${previewImage.scale})` }"
class="preview-image"
draggable="false"
/>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { computed, onUnmounted, ref, watch, shallowRef } from "vue";
import { computed, onUnmounted, ref, watch, shallowRef, reactive } from "vue";
import { Codemirror } from "vue-codemirror";
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { languages } from "@codemirror/language-data";
@ -306,6 +323,34 @@ const executeAction = (view, action, payload) => {
}
};
//
const previewImage = reactive({
visible: false,
url: "",
scale: 1,
rotate: 0,
});
const handleEditorClick = (e) => {
if (e.target.classList.contains("cm-md-image-preview")) {
previewImage.url = e.target.src;
previewImage.scale = 1;
previewImage.rotate = 0;
previewImage.visible = true;
}
};
const closePreview = () => {
previewImage.visible = false;
};
const handlePreviewWheel = (e) => {
const delta = e.deltaY > 0 ? -0.1 : 0.1;
const newScale = previewImage.scale + delta;
// 0.1 - 5
previewImage.scale = Math.min(Math.max(0.1, newScale), 5);
};
onUnmounted(() => {
if (typeof detachPaste === "function") detachPaste();
if (removeActionListener) removeActionListener();
@ -333,3 +378,70 @@ onUnmounted(() => {
color: var(--primary-color);
}
</style>
<style>
/* 图片预览弹窗样式 - 全局生效以支持 Teleport */
.image-preview-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.9);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
animation: fadeIn 0.2s ease-out;
}
.preview-controls {
position: absolute;
top: 30px;
right: 40px;
z-index: 10000;
}
.close-btn {
color: rgba(255, 255, 255, 0.8);
font-size: 48px;
cursor: pointer;
line-height: 1;
transition: all 0.2s;
display: block;
font-family: Arial, sans-serif;
}
.close-btn:hover {
color: #fff;
transform: scale(1.1);
}
.preview-content {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.preview-image {
max-width: 90%;
max-height: 90%;
object-fit: contain;
transition: transform 0.1s ease-out;
cursor: grab;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
}
.preview-image:active {
cursor: grabbing;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>