Files
hldrCenter/management/src/views/news/QuillEditor.vue

268 lines
8.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.
<template>
<div class="editor-wrapper">
<!-- 富文本编辑器 -->
<div ref="editor" class="quill-container"></div>
<!-- 新增打印内容按钮 -->
<button
class="print-content-btn"
@click="printEditorContent"
>
打印当前编辑器内容到控制台
</button>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
// 编辑器容器引用
const editor = ref<HTMLDivElement | null>(null);
let quillInstance: Quill | null = null;
// 组件挂载后初始化 Quill
onMounted(() => {
if (editor.value) {
// 1. 初始化 Quill 编辑器
quillInstance = new Quill(editor.value, {
theme: 'snow', // 使用带工具栏的主题
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
['link', 'image', 'video'], // 图片、视频按钮
['clean']
],
handlers: {
image: handleClickUpload // 点击工具栏「图片」按钮时触发上传本地图片
}
}
},
placeholder: '请上传或粘贴图片...',
});
// 2. 监听文本变化,用于处理粘贴图片(自动提取 base64 并上传)
quillInstance.on('text-change', (delta, oldDelta, source) => {
if (source === 'user') {
const pastedBase64 = getPastedBase64Image(delta);
if (pastedBase64) {
const selection = quillInstance?.getSelection();
if (!selection) return;
// 🔍 打印关键调试信息
console.log('🔍 text-change 原始 delta.ops:', delta.ops);
console.log('🔍 source:', source);
console.log('🔍 当前 selection:', selection);
console.log('🔍 当前光标位置selection.index:', selection.index);
console.log('🔍 推测的 base64 图片位置imageIndex = selection.index - 1:', selection.index - 1);
const imageIndex = selection.index ;
// 1. 删除 base64 图片
quillInstance?.deleteText(imageIndex, 1);
// 2. 转 base64 为 File 并上传
const blob = base64ToBlob(pastedBase64);
if (blob) {
const file = new File([blob], `pasted-${Date.now()}.png`, { type: blob.type });
uploadImageToServer(file, imageIndex);
}
}
}
});
}
});
/**
* 点击工具栏「图片」按钮时触发:选择本地图片并上传
*/
function handleClickUpload() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = () => {
if (!fileInput.files || !fileInput.files[0]) return;
const selectedFile = fileInput.files[0];
const selection = quillInstance?.getSelection();
const insertIndex = selection ? selection.index : quillInstance?.getLength() || 0;
// 调用统一上传函数
uploadImageToServer(selectedFile, insertIndex);
// 清空 input避免重复选择同一文件不触发 onchange
fileInput.value = '';
};
fileInput.click();
}
/**
* 统一图片上传函数:上传 File 到你的后端接口,插入返回的真实图片 URL
* @param file 用户上传的图片文件(本地选择 or 粘贴转成 File
* @param insertIndex 图片要插入到编辑器的位置
*/
async function uploadImageToServer(file: File, insertIndex: number) {
if (!quillInstance) return;
// 1. 插入「上传中」占位文本
quillInstance.insertText(insertIndex, '[图片上传中...]', { color: '#666', italic: true });
try {
// 2. 构造 FormData用于上传文件
const formData = new FormData();
formData.append('image', file); // ⬅️ 'image' 字段名请根据你的后端接口调整!
// 3. 调用你的真实后端图片上传接口(请替换为你的真实地址!)
const UPLOAD_API_URL = 'http://localhost:8080/api/upload/image'; // ⬅️ 一定要替换成你自己的上传接口!!
const response = await fetch(UPLOAD_API_URL, {
method: 'POST',
body: formData,
// 注意:不要手动设置 Content-Type浏览器会自动生成 multipart/form-data + boundary
});
if (!response.ok) {
throw new Error(`图片上传失败HTTP 状态码:${response.status}`);
}
// 4. 解析返回的 JSON获取图片 URL重要根据你的后端返回字段名调整
const result = await response.json();
// ⬅️ 下面这行是关键!根据你后端返回的数据结构修改,比如:
// - { url: 'https://xxx.com/img.png' } → 取 result.url
// - { success: true, data: { imageUrl: '...' } } → 取 result.data.imageUrl
// - { data: { url: '...' } } → 取 result.data.url
const imageUrl = result.data?.url; // ✅ 请根据实际返回字段修改!!!
if (!imageUrl) {
throw new Error('服务器未返回有效的图片 URL');
}
// 5. 删除「上传中」占位文本
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
// 6. 插入真实的图片 URL不是 base64
quillInstance.insertEmbed(insertIndex, 'image', imageUrl);
// 7. 光标移动到图片后面
quillInstance.setSelection(insertIndex + 1);
} catch (error) {
console.error('图片上传失败:', error);
quillInstance.deleteText(insertIndex, '[图片上传中...]'.length);
quillInstance.insertText(insertIndex, '[图片上传失败,请重试]', { color: '#ff4444', italic: true });
}
}
/**
* 打印当前编辑器内容HTML + 纯文本)到控制台(调试用)
*/
function printEditorContent() {
if (!quillInstance) {
console.warn('编辑器未初始化,无法获取内容');
return;
}
const htmlContent = quillInstance.root.innerHTML;
const plainText = quillInstance.getText();
console.log('=== 编辑器 HTML 内容 ===');
console.log(htmlContent);
console.log('\n=== 编辑器纯文本内容 ===');
console.log(plainText);
}
/**
* 从 Delta 操作中提取粘贴的 base64 图片
*/
function getPastedBase64Image(delta: any): string | null {
for (const op of delta.ops) {
if (op.insert && typeof op.insert === 'object' && op.insert.image && op.insert.image.startsWith('data:image')) {
return op.insert.image;
}
}
return null;
}
/**
* 将 base64 图片字符串转为 Blob 对象
*/
function base64ToBlob(base64: string): Blob | null {
try {
const [prefix, data] = base64.split(',');
if (!prefix || !data) throw new Error('base64 格式错误');
const mimeMatch = prefix.match(/data:(.*?);/);
if (!mimeMatch) throw new Error('无法解析 MIME 类型');
const mime = mimeMatch[1];
const binaryData = atob(data);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
return new Blob([bytes], { type: mime });
} catch (error) {
console.error('base64 转 Blob 失败:', error);
return null;
}
}
</script>
<style scoped>
/* 外层容器:控制编辑器和按钮的布局 */
.editor-wrapper {
display: flex;
flex-direction: column;
gap: 16px; /* 编辑器和按钮之间的间距 */
max-width: 800px; /* 限制最大宽度,避免编辑器过宽 */
margin: 20px auto; /* 水平居中 */
padding: 0 20px;
}
.quill-container {
width: 100%;
border: 1px solid #e5e7eb;
border-radius: 4px;
overflow: hidden; /* 避免工具栏边框与容器重叠 */
}
/* 调整编辑器最小高度和图片显示 */
::v-deep .ql-editor {
min-height: 300px;
}
::v-deep .ql-editor img {
max-width: 100%;
height: auto;
border-radius: 2px;
}
/* 打印按钮样式 */
.print-content-btn {
padding: 8px 16px;
background-color: #4096ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
width: fit-content; /* 按钮宽度适应内容 */
}
.print-content-btn:disabled {
background-color: #c9c9c9;
cursor: not-allowed;
color: #666;
}
.print-content-btn:hover:not(:disabled) {
background-color: #3086e8;
}
</style>