feat(editor): 添加图片预览功能并支持缩放
- 在编辑器点击图片时显示全屏预览弹窗 - 支持通过鼠标滚轮缩放预览图片(0.1-5倍) - 使用Teleport将弹窗渲染到body避免样式冲突 - 添加淡入动画和关闭按钮提升用户体验
This commit is contained in:
parent
84af4bd798
commit
3373abe563
|
|
@ -1,12 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="live-editor">
|
<div class="live-editor" @click="handleEditorClick">
|
||||||
<EditorToolbar @action="handleToolbarAction" />
|
<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" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { Codemirror } from "vue-codemirror";
|
||||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||||
import { languages } from "@codemirror/language-data";
|
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(() => {
|
onUnmounted(() => {
|
||||||
if (typeof detachPaste === "function") detachPaste();
|
if (typeof detachPaste === "function") detachPaste();
|
||||||
if (removeActionListener) removeActionListener();
|
if (removeActionListener) removeActionListener();
|
||||||
|
|
@ -333,3 +378,70 @@ onUnmounted(() => {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue