优化富文本编辑器:1.只读模式隐藏工具栏;2.点击图片可弹窗预览。

This commit is contained in:
LiuDQ 2025-11-05 10:38:57 +08:00
parent 8284a87d36
commit 39c7d5c5f5

View File

@ -24,6 +24,32 @@
@text-change="(e: any) => $emit('update:modelValue', content)"
/>
</div>
<!-- 图片预览对话框 -->
<el-dialog v-model="previewDialogVisible" title="图片预览" width="80%" append-to-body>
<div class="image-preview-container">
<el-image
v-if="currentPreviewImage"
:src="currentPreviewImage"
fit="contain"
style="width: 100%; max-height: 70vh;"
/>
<div v-if="imageUrlList.length > 1" class="image-preview-nav">
<el-button
:disabled="currentImageIndex === 0"
@click="prevImage"
icon="ArrowLeft"
circle
/>
<span class="image-counter">{{ currentImageIndex + 1 }} / {{ imageUrlList.length }}</span>
<el-button
:disabled="currentImageIndex === imageUrlList.length - 1"
@click="nextImage"
icon="ArrowRight"
circle
/>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
@ -32,6 +58,7 @@ import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { QuillEditor, Quill } from '@vueup/vue-quill';
import { propTypes } from '@/utils/propTypes';
import { globalHeaders } from '@/utils/request';
import { nextTick, onMounted, computed } from 'vue';
defineEmits(['update:modelValue']);
@ -59,43 +86,53 @@ const upload = reactive<UploadOption>({
const quillEditorRef = ref();
const uploadRef = ref<HTMLDivElement>();
const options = ref<any>({
theme: 'snow',
bounds: document.body,
debug: 'warn',
modules: {
//
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'], // 线 线
['blockquote', 'code-block'], //
[{ list: 'ordered' }, { list: 'bullet' }], //
[{ indent: '-1' }, { indent: '+1' }], //
[{ size: ['small', false, 'large', 'huge'] }], //
[{ header: [1, 2, 3, 4, 5, 6, false] }], //
[{ color: [] }, { background: [] }], //
[{ align: [] }], //
['clean'], //
['link', 'image', 'video'] //
],
handlers: {
image: (value: boolean) => {
if (value) {
// element
uploadRef.value.click();
} else {
Quill.format('image', true);
//
const previewDialogVisible = ref(false);
const imageUrlList = ref<string[]>([]);
const currentImageIndex = ref(0);
const currentPreviewImage = computed(() => {
return imageUrlList.value[currentImageIndex.value] || '';
});
const options = computed(() => {
return {
theme: 'snow',
bounds: document.body,
debug: 'warn',
modules: {
// -
toolbar: props.readOnly ? false : {
container: [
['bold', 'italic', 'underline', 'strike'], // 线 线
['blockquote', 'code-block'], //
[{ list: 'ordered' }, { list: 'bullet' }], //
[{ indent: '-1' }, { indent: '+1' }], //
[{ size: ['small', false, 'large', 'huge'] }], //
[{ header: [1, 2, 3, 4, 5, 6, false] }], //
[{ color: [] }, { background: [] }], //
[{ align: [] }], //
['clean'], //
['link', 'image', 'video'] //
],
handlers: {
image: (value: boolean) => {
if (value) {
// element
uploadRef.value.click();
} else {
Quill.format('image', true);
}
}
}
}
}
},
placeholder: '请输入内容',
readOnly: props.readOnly
},
placeholder: '请输入内容',
readOnly: props.readOnly
};
});
const styles = computed(() => {
const style: any = {};
let style: any = {};
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`;
}
@ -111,24 +148,139 @@ watch(
(v: string) => {
if (v !== content.value) {
content.value = v || '<p></p>';
//
nextTick(() => {
initImageClickEvent();
});
}
},
{ immediate: true }
);
//
watch(
() => content.value,
() => {
nextTick(() => {
initImageClickEvent();
});
}
);
//
watch(
() => props.readOnly,
(newVal) => {
// Quill
if (quillEditorRef.value) {
const quill = toRaw(quillEditorRef.value).getQuill();
if (quill) {
quill.enable(!newVal);
}
}
//
nextTick(() => {
initImageClickEvent();
});
},
{ immediate: true }
);
//
const initImageClickEvent = () => {
if (!quillEditorRef.value) return;
const quill = toRaw(quillEditorRef.value).getQuill();
if (!quill || !quill.root) return;
const editorContainer = quill.root;
const images = editorContainer.querySelectorAll('img');
//
images.forEach((img: HTMLImageElement) => {
const newImg = img.cloneNode(true) as HTMLImageElement;
img.parentNode?.replaceChild(newImg, img);
});
//
const newImages = editorContainer.querySelectorAll('img');
newImages.forEach((img: HTMLImageElement, index: number) => {
//
img.style.cursor = 'pointer';
img.style.transition = 'opacity 0.3s';
//
img.style.pointerEvents = 'auto';
// - 使capture
const handleImageClick = (e: Event) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// URL
const urlList: string[] = [];
newImages.forEach((item: HTMLImageElement) => {
if (item.src) {
urlList.push(item.src);
}
});
if (urlList.length > 0) {
imageUrlList.value = urlList;
currentImageIndex.value = index;
previewDialogVisible.value = true;
}
};
// 使capture
img.addEventListener('click', handleImageClick, true);
// hover
const handleMouseEnter = () => {
img.style.opacity = '0.8';
};
const handleMouseLeave = () => {
img.style.opacity = '1';
};
img.addEventListener('mouseenter', handleMouseEnter);
img.addEventListener('mouseleave', handleMouseLeave);
});
};
//
const prevImage = () => {
if (currentImageIndex.value > 0) {
currentImageIndex.value--;
}
};
//
const nextImage = () => {
if (currentImageIndex.value < imageUrlList.value.length - 1) {
currentImageIndex.value++;
}
};
//
const handleUploadSuccess = (res: any) => {
//
if (res.code === 200) {
//
const quill = toRaw(quillEditorRef.value).getQuill();
let quill = toRaw(quillEditorRef.value).getQuill();
//
const length = quill.selection.savedRange.index;
let length = quill.selection.savedRange.index;
// res
quill.insertEmbed(length, 'image', res.data.url);
//
quill.setSelection(length + 1);
proxy?.$modal.closeLoading();
//
nextTick(() => {
initImageClickEvent();
});
} else {
proxy?.$modal.msgError('图片插入失败');
proxy?.$modal.closeLoading();
@ -160,6 +312,24 @@ const handleBeforeUpload = (file: any) => {
const handleUploadError = (err: any) => {
proxy?.$modal.msgError('上传文件失败');
};
//
onMounted(() => {
nextTick(() => {
initImageClickEvent();
//
if (props.readOnly && quillEditorRef.value) {
const quill = toRaw(quillEditorRef.value).getQuill();
if (quill) {
//
const editorContainer = quill.root;
if (editorContainer) {
editorContainer.style.pointerEvents = 'auto';
}
}
}
});
});
</script>
<style>
@ -241,4 +411,47 @@ const handleUploadError = (err: any) => {
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
content: '等宽字体';
}
/* 编辑器中的图片样式 */
.editor :deep(.ql-editor img) {
cursor: pointer;
transition: opacity 0.3s;
pointer-events: auto !important; /* 确保图片可以点击 */
}
.editor :deep(.ql-editor img:hover) {
opacity: 0.8;
}
/* 只读模式下的图片样式 - 确保图片仍然可以点击 */
.editor :deep(.ql-editor.ql-disabled) {
pointer-events: none; /* 禁用整个编辑器的指针事件 */
}
.editor :deep(.ql-editor.ql-disabled img) {
pointer-events: auto !important; /* 但图片可以点击 */
cursor: pointer;
}
/* 图片预览容器样式 */
.image-preview-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
}
.image-preview-nav {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.image-counter {
font-size: 16px;
color: #606266;
}
</style>