|
|
|
|
@ -227,6 +227,20 @@ const form = reactive({
|
|
|
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
|
|
|
const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
|
|
|
|
|
|
|
|
// 跟踪尚未完成的粘贴图片 FileReader,弹窗关闭/卸载时 abort,
|
|
|
|
|
// 避免 onload 在 editor 已销毁后调用 dangerouslyInsertHtml 导致 Vue 3.5 patch 异常
|
|
|
|
|
const pendingPasteReaders = new Set();
|
|
|
|
|
const cancelPendingPasteReaders = () => {
|
|
|
|
|
for (const r of pendingPasteReaders) {
|
|
|
|
|
try {
|
|
|
|
|
r.abort();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pendingPasteReaders.clear();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAttachmentChange = (file) => {
|
|
|
|
|
if (file?.size > MAX_FILE_SIZE) {
|
|
|
|
|
ElMessage.warning('文件不能超过 50MB');
|
|
|
|
|
@ -373,7 +387,9 @@ const handleEditorPasteImage = (editor, event) => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
pendingPasteReaders.add(reader);
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
pendingPasteReaders.delete(reader);
|
|
|
|
|
const dataUrl = e.target?.result;
|
|
|
|
|
if (!dataUrl) {
|
|
|
|
|
ElMessage.error('图片读取失败');
|
|
|
|
|
@ -382,17 +398,37 @@ const handleEditorPasteImage = (editor, event) => {
|
|
|
|
|
try {
|
|
|
|
|
if (savedSelection) editor.select(savedSelection);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
try {
|
|
|
|
|
if (typeof editor.focus === 'function') editor.focus();
|
|
|
|
|
} catch (__) {
|
|
|
|
|
/* editor 已销毁 */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// editor 可能已被弹窗关闭流程销毁/卸载,insertHtml 需读套保护
|
|
|
|
|
try {
|
|
|
|
|
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();
|
|
|
|
|
// ⚠️ 千万别同步 form.description!dangerouslyInsertHtml 是只改 editor 内部、直接操作 Slate 节点的底层 API,
|
|
|
|
|
// 同步给 v-model 会触发 wangEditor 的自动 setHtml,叠加后让 Slate operations 历史
|
|
|
|
|
// 里残留旧路径 (如 [0, 2]),下次任何操作就报 "Cannot find a descendant path"。
|
|
|
|
|
// 正确做法:v-model 保持空,提交时从 editor.getHtml() 取最新内容。
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(
|
|
|
|
|
'[OrderFormDialog] 粘贴图片插入失败(editor 可能已销毁):',
|
|
|
|
|
err?.message,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reader.onerror = () => {
|
|
|
|
|
pendingPasteReaders.delete(reader);
|
|
|
|
|
ElMessage.error('图片处理失败');
|
|
|
|
|
};
|
|
|
|
|
reader.onabort = () => {
|
|
|
|
|
pendingPasteReaders.delete(reader);
|
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
@ -414,6 +450,19 @@ const handleEditorCreated = (editor) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
// 显式清空 wangEditor 内部内容和 Slate selection 状态。
|
|
|
|
|
// 原因:paste 图片后 editor 内部有内容,v-show 下弹窗不卸载组件,
|
|
|
|
|
// 二次打开时不调用 setHtml 会导致 Slate 内部残留旧路径的 selection。
|
|
|
|
|
const editor = editorRef.value;
|
|
|
|
|
if (editor) {
|
|
|
|
|
try {
|
|
|
|
|
if (typeof editor.setHtml === 'function') {
|
|
|
|
|
editor.setHtml('');
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
/* editor 已销毁或不可用 */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Object.assign(form, {
|
|
|
|
|
hospitalId: undefined,
|
|
|
|
|
title: '',
|
|
|
|
|
@ -485,6 +534,8 @@ watch(
|
|
|
|
|
|
|
|
|
|
const closeDialog = () => emit('update:modelValue', false);
|
|
|
|
|
const onClosed = () => {
|
|
|
|
|
// 弹窗关闭时 abort 未完成的粘贴 FileReader,防止 onload 跨生命周期访问已销毁的 editor
|
|
|
|
|
cancelPendingPasteReaders();
|
|
|
|
|
resetForm();
|
|
|
|
|
emit('closed');
|
|
|
|
|
};
|
|
|
|
|
@ -493,6 +544,13 @@ const onSubmit = async () => {
|
|
|
|
|
// dept / submitter 去掉两端空格后再校验,避免仅含空格的"伪必填"绕过
|
|
|
|
|
form.department = (form.department || '').trim();
|
|
|
|
|
form.submitter = (form.submitter || '').trim();
|
|
|
|
|
// 提交前从 wangEditor 同步富文本内容到 form.description。
|
|
|
|
|
// dangerouslyInsertHtml 只改 editor 内部不触发 v-model,避免了上一轮踩的坑,
|
|
|
|
|
// 代价是 form.description 不会随粘贴自动更新,所以提交时从 getHtml() 拿最新内容。
|
|
|
|
|
const editor = editorRef.value;
|
|
|
|
|
if (editor && typeof editor.getHtml === 'function') {
|
|
|
|
|
form.description = editor.getHtml() || '';
|
|
|
|
|
}
|
|
|
|
|
await formRef.value.validate(async (valid) => {
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
@ -521,6 +579,7 @@ const removePasteListener = () => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
cancelPendingPasteReaders();
|
|
|
|
|
removePasteListener();
|
|
|
|
|
pasteHandlerRef = null;
|
|
|
|
|
const editor = editorRef.value;
|
|
|
|
|
|