mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-15 16:28:33 +08:00
优化富文本编辑器:1.只读模式隐藏工具栏;2.点击图片可弹窗预览。
This commit is contained in:
parent
8284a87d36
commit
39c7d5c5f5
@ -24,6 +24,32 @@
|
|||||||
@text-change="(e: any) => $emit('update:modelValue', content)"
|
@text-change="(e: any) => $emit('update:modelValue', content)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { QuillEditor, Quill } from '@vueup/vue-quill';
|
||||||
import { propTypes } from '@/utils/propTypes';
|
import { propTypes } from '@/utils/propTypes';
|
||||||
import { globalHeaders } from '@/utils/request';
|
import { globalHeaders } from '@/utils/request';
|
||||||
|
import { nextTick, onMounted, computed } from 'vue';
|
||||||
|
|
||||||
defineEmits(['update:modelValue']);
|
defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
@ -59,43 +86,53 @@ const upload = reactive<UploadOption>({
|
|||||||
const quillEditorRef = ref();
|
const quillEditorRef = ref();
|
||||||
const uploadRef = ref<HTMLDivElement>();
|
const uploadRef = ref<HTMLDivElement>();
|
||||||
|
|
||||||
const options = ref<any>({
|
// 图片预览相关
|
||||||
theme: 'snow',
|
const previewDialogVisible = ref(false);
|
||||||
bounds: document.body,
|
const imageUrlList = ref<string[]>([]);
|
||||||
debug: 'warn',
|
const currentImageIndex = ref(0);
|
||||||
modules: {
|
const currentPreviewImage = computed(() => {
|
||||||
// 工具栏配置
|
return imageUrlList.value[currentImageIndex.value] || '';
|
||||||
toolbar: {
|
});
|
||||||
container: [
|
|
||||||
['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
|
const options = computed(() => {
|
||||||
['blockquote', 'code-block'], // 引用 代码块
|
return {
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
|
theme: 'snow',
|
||||||
[{ indent: '-1' }, { indent: '+1' }], // 缩进
|
bounds: document.body,
|
||||||
[{ size: ['small', false, 'large', 'huge'] }], // 字体大小
|
debug: 'warn',
|
||||||
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
modules: {
|
||||||
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
// 工具栏配置 - 只读模式下隐藏工具栏
|
||||||
[{ align: [] }], // 对齐方式
|
toolbar: props.readOnly ? false : {
|
||||||
['clean'], // 清除文本格式
|
container: [
|
||||||
['link', 'image', 'video'] // 链接、图片、视频
|
['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
|
||||||
],
|
['blockquote', 'code-block'], // 引用 代码块
|
||||||
handlers: {
|
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
|
||||||
image: (value: boolean) => {
|
[{ indent: '-1' }, { indent: '+1' }], // 缩进
|
||||||
if (value) {
|
[{ size: ['small', false, 'large', 'huge'] }], // 字体大小
|
||||||
// 调用element图片上传
|
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
||||||
uploadRef.value.click();
|
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
||||||
} else {
|
[{ align: [] }], // 对齐方式
|
||||||
Quill.format('image', true);
|
['clean'], // 清除文本格式
|
||||||
|
['link', 'image', 'video'] // 链接、图片、视频
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
image: (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
// 调用element图片上传
|
||||||
|
uploadRef.value.click();
|
||||||
|
} else {
|
||||||
|
Quill.format('image', true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
placeholder: '请输入内容',
|
||||||
placeholder: '请输入内容',
|
readOnly: props.readOnly
|
||||||
readOnly: props.readOnly
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const styles = computed(() => {
|
const styles = computed(() => {
|
||||||
const style: any = {};
|
let style: any = {};
|
||||||
if (props.minHeight) {
|
if (props.minHeight) {
|
||||||
style.minHeight = `${props.minHeight}px`;
|
style.minHeight = `${props.minHeight}px`;
|
||||||
}
|
}
|
||||||
@ -111,24 +148,139 @@ watch(
|
|||||||
(v: string) => {
|
(v: string) => {
|
||||||
if (v !== content.value) {
|
if (v !== content.value) {
|
||||||
content.value = v || '<p></p>';
|
content.value = v || '<p></p>';
|
||||||
|
// 内容更新后,添加图片点击事件
|
||||||
|
nextTick(() => {
|
||||||
|
initImageClickEvent();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ 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) => {
|
const handleUploadSuccess = (res: any) => {
|
||||||
// 如果上传成功
|
// 如果上传成功
|
||||||
if (res.code === 200) {
|
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为服务器返回的图片链接地址
|
// 插入图片,res为服务器返回的图片链接地址
|
||||||
quill.insertEmbed(length, 'image', res.data.url);
|
quill.insertEmbed(length, 'image', res.data.url);
|
||||||
// 调整光标到最后
|
// 调整光标到最后
|
||||||
quill.setSelection(length + 1);
|
quill.setSelection(length + 1);
|
||||||
proxy?.$modal.closeLoading();
|
proxy?.$modal.closeLoading();
|
||||||
|
|
||||||
|
// 图片插入后,重新初始化图片点击事件
|
||||||
|
nextTick(() => {
|
||||||
|
initImageClickEvent();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
proxy?.$modal.msgError('图片插入失败');
|
proxy?.$modal.msgError('图片插入失败');
|
||||||
proxy?.$modal.closeLoading();
|
proxy?.$modal.closeLoading();
|
||||||
@ -160,6 +312,24 @@ const handleBeforeUpload = (file: any) => {
|
|||||||
const handleUploadError = (err: any) => {
|
const handleUploadError = (err: any) => {
|
||||||
proxy?.$modal.msgError('上传文件失败');
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -241,4 +411,47 @@ const handleUploadError = (err: any) => {
|
|||||||
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
|
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
|
||||||
content: '等宽字体';
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user