增加贴图功能

master
2358328281@qq.com 5 days ago
parent f29c3dc589
commit 5d9626f55b

@ -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(/&nbsp;/gi, "") .replace(/&nbsp;/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();

@ -43,6 +43,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>
@ -69,7 +70,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from "vue"; import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { ArrowLeft } from "@element-plus/icons-vue"; import { ArrowLeft } from "@element-plus/icons-vue";
@ -106,11 +107,12 @@ const rules = {
{ {
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(/&nbsp;/gi, "") .replace(/&nbsp;/gi, "")
.trim(); .trim();
if (!text) return callback(new Error($t('placeholder.remark'))); if (!text && !hasImage) return callback(new Error($t('placeholder.remark')));
callback(); callback();
}, },
trigger: "change", trigger: "change",
@ -133,8 +135,94 @@ const toolbarConfig = {
const editorConfig = { const editorConfig = {
placeholder: $t('placeholder.remark'), placeholder: $t('placeholder.remark'),
}; };
// base64 HTML
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
// 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();
// 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;" />`,
);
if (typeof editor.updateView === "function") editor.updateView();
// ElMessage.success("");
};
reader.onerror = () => {
ElMessage.error("图片处理失败");
};
reader.readAsDataURL(file);
});
return false; // wangEditor
};
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 loadDetail = async () => { const loadDetail = async () => {
@ -166,9 +254,20 @@ const submit = async () => {
const goBack = () => router.back(); const goBack = () => router.back();
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 });
};
onMounted(loadDetail); onMounted(loadDetail);
onBeforeUnmount(() => { onBeforeUnmount(() => {
removePasteListener();
pasteHandlerRef = null;
const editor = editorRef.value; const editor = editorRef.value;
if (editor == null) return; if (editor == null) return;
editor.destroy(); editor.destroy();

Loading…
Cancel
Save