|
|
|
@ -316,6 +316,7 @@
|
|
|
|
mode="default"
|
|
|
|
mode="default"
|
|
|
|
style="height: 200px; overflow-y: auto"
|
|
|
|
style="height: 200px; overflow-y: auto"
|
|
|
|
@on-created="handleEditorCreated"
|
|
|
|
@on-created="handleEditorCreated"
|
|
|
|
|
|
|
|
@custom-paste="handleEditorPasteImage"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</el-form-item>
|
|
|
|
</el-form-item>
|
|
|
|
@ -360,7 +361,14 @@
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
<script setup>
|
|
|
|
import { onBeforeUnmount, onMounted, reactive, ref, shallowRef } from "vue";
|
|
|
|
import {
|
|
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
|
|
onBeforeUnmount,
|
|
|
|
|
|
|
|
onMounted,
|
|
|
|
|
|
|
|
reactive,
|
|
|
|
|
|
|
|
ref,
|
|
|
|
|
|
|
|
shallowRef,
|
|
|
|
|
|
|
|
} from "vue";
|
|
|
|
import { useRouter } from "vue-router";
|
|
|
|
import { useRouter } from "vue-router";
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
|
|
|
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
|
|
|
@ -516,6 +524,8 @@ const quickForm = reactive({
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const fileInputRef = ref();
|
|
|
|
const fileInputRef = ref();
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
|
|
|
|
// 粘贴到富文本的图片大小上限(base64 内联存储,避免 HTML 过大)
|
|
|
|
|
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
const triggerFilePicker = () => {
|
|
|
|
const triggerFilePicker = () => {
|
|
|
|
fileInputRef.value?.click();
|
|
|
|
fileInputRef.value?.click();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -562,11 +572,13 @@ const quickRules = {
|
|
|
|
{
|
|
|
|
{
|
|
|
|
required: true,
|
|
|
|
required: true,
|
|
|
|
validator: (_, value, callback) => {
|
|
|
|
validator: (_, value, callback) => {
|
|
|
|
|
|
|
|
const hasImage = /<img\s/i.test(value || "");
|
|
|
|
const text = (value || "")
|
|
|
|
const text = (value || "")
|
|
|
|
.replace(/<[^>]*>/g, "")
|
|
|
|
.replace(/<[^>]*>/g, "")
|
|
|
|
.replace(/ /gi, "")
|
|
|
|
.replace(/ /gi, "")
|
|
|
|
.trim();
|
|
|
|
.trim();
|
|
|
|
if (!text) return callback(new Error($t("msg.pleaseInputDescription")));
|
|
|
|
if (!text && !hasImage)
|
|
|
|
|
|
|
|
return callback(new Error($t("msg.pleaseInputDescription")));
|
|
|
|
callback();
|
|
|
|
callback();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
trigger: "change",
|
|
|
|
trigger: "change",
|
|
|
|
@ -586,11 +598,98 @@ const toolbarConfig = {
|
|
|
|
"group-emoji",
|
|
|
|
"group-emoji",
|
|
|
|
],
|
|
|
|
],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
// 从剪贴板中捞出图片文件(同时兼容 items 和 files 两种来源)
|
|
|
|
|
|
|
|
const collectPastedImages = (clipboardData) => {
|
|
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
const items = clipboardData?.items;
|
|
|
|
|
|
|
|
if (items) {
|
|
|
|
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
|
|
|
|
const item = items[i];
|
|
|
|
|
|
|
|
if (item.kind === "file" && item.type?.startsWith("image/")) {
|
|
|
|
|
|
|
|
const file = item.getAsFile();
|
|
|
|
|
|
|
|
if (file) files.push(file);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (files.length === 0 && clipboardData?.files) {
|
|
|
|
|
|
|
|
for (let i = 0; i < clipboardData.files.length; i++) {
|
|
|
|
|
|
|
|
const file = clipboardData.files[i];
|
|
|
|
|
|
|
|
if (file.type?.startsWith("image/")) files.push(file);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return files;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 共用的图片粘贴处理逻辑(customPaste 和原生监听都走这里,用事件标记防重复)
|
|
|
|
|
|
|
|
const handleEditorPasteImage = (editor, event) => {
|
|
|
|
|
|
|
|
if (event.__wpsImageHandled) return;
|
|
|
|
|
|
|
|
const imageFiles = collectPastedImages(event.clipboardData);
|
|
|
|
|
|
|
|
if (imageFiles.length === 0) return;
|
|
|
|
|
|
|
|
event.__wpsImageHandled = true;
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
if (typeof event.stopPropagation === "function") event.stopPropagation();
|
|
|
|
|
|
|
|
if (typeof event.stopImmediatePropagation === "function")
|
|
|
|
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
|
|
|
|
// ElMessage.info(`检测到 ${imageFiles.length} 张粘贴图片,正在处理...`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 同步先拿到当前 selection,等 FileReader 异步回调里恢复
|
|
|
|
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
|
|
|
|
const savedSelection = editor.selection;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
imageFiles.forEach((file) => {
|
|
|
|
|
|
|
|
if (file.size > MAX_PASTE_IMAGE_SIZE) {
|
|
|
|
|
|
|
|
ElMessage.warning("粘贴的图片不能超过 5MB,请使用下方附件上传");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
|
|
|
const dataUrl = e.target?.result;
|
|
|
|
|
|
|
|
if (!dataUrl) {
|
|
|
|
|
|
|
|
ElMessage.error("图片读取失败");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 恢复保存的选区
|
|
|
|
|
|
|
|
if (savedSelection) editor.select(savedSelection);
|
|
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof editor.focus === "function") editor.focus();
|
|
|
|
|
|
|
|
editor.dangerouslyInsertHtml(
|
|
|
|
|
|
|
|
`<img src="${dataUrl}" style="max-width:100%;height:auto;" />`,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
// 手动触发一次更新保证 v-model 同步
|
|
|
|
|
|
|
|
if (typeof editor.updateView === "function") editor.updateView();
|
|
|
|
|
|
|
|
// ElMessage.success("图片插入成功");
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.onerror = () => {
|
|
|
|
|
|
|
|
ElMessage.error("图片处理失败");
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
return false; // 告诉 wangEditor 跳过默认处理(customPaste 调用时生效)
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:customPaste 不能放在 default-config 里(会被 wangEditor for Vue 检测到抛错)
|
|
|
|
|
|
|
|
// 已改用 <Editor> 上的 @custom-paste 事件 + contentEditable 原生监听双重保障
|
|
|
|
const editorConfig = {
|
|
|
|
const editorConfig = {
|
|
|
|
placeholder: $t("placeholder.description"),
|
|
|
|
placeholder: $t("placeholder.description"),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
let pasteHandlerRef = null;
|
|
|
|
const handleEditorCreated = (editor) => {
|
|
|
|
const handleEditorCreated = (editor) => {
|
|
|
|
editorRef.value = editor;
|
|
|
|
editorRef.value = editor;
|
|
|
|
|
|
|
|
// 额外在 contentEditable 上绑一份 capture 监听(防 customPaste 未生效的兜底)
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
|
|
|
|
|
|
|
|
if (!root) return;
|
|
|
|
|
|
|
|
const editable =
|
|
|
|
|
|
|
|
root.querySelector?.('[contenteditable="true"]') || root;
|
|
|
|
|
|
|
|
if (pasteHandlerRef) {
|
|
|
|
|
|
|
|
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
pasteHandlerRef = (event) => handleEditorPasteImage(editor, event);
|
|
|
|
|
|
|
|
editable.addEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetQuickForm = () => {
|
|
|
|
const resetQuickForm = () => {
|
|
|
|
@ -648,8 +747,19 @@ onMounted(() => {
|
|
|
|
document.addEventListener("click", onDocClick);
|
|
|
|
document.addEventListener("click", onDocClick);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const removePasteListener = () => {
|
|
|
|
|
|
|
|
const editor = editorRef.value;
|
|
|
|
|
|
|
|
if (!editor || !pasteHandlerRef) return;
|
|
|
|
|
|
|
|
const root = typeof editor.$el === "function" ? editor.$el() : editor.$el;
|
|
|
|
|
|
|
|
if (!root) return;
|
|
|
|
|
|
|
|
const editable = root.querySelector?.('[contenteditable="true"]') || root;
|
|
|
|
|
|
|
|
editable.removeEventListener("paste", pasteHandlerRef, { capture: true });
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
document.removeEventListener("click", onDocClick);
|
|
|
|
document.removeEventListener("click", onDocClick);
|
|
|
|
|
|
|
|
removePasteListener();
|
|
|
|
|
|
|
|
pasteHandlerRef = null;
|
|
|
|
const editor = editorRef.value;
|
|
|
|
const editor = editorRef.value;
|
|
|
|
if (editor == null) return;
|
|
|
|
if (editor == null) return;
|
|
|
|
editor.destroy();
|
|
|
|
editor.destroy();
|
|
|
|
|