diff --git a/src/views/admin/hospitals/components/ServiceRecordDialog.vue b/src/views/admin/hospitals/components/ServiceRecordDialog.vue index 58daf50..e297ff0 100644 --- a/src/views/admin/hospitals/components/ServiceRecordDialog.vue +++ b/src/views/admin/hospitals/components/ServiceRecordDialog.vue @@ -601,8 +601,8 @@ const viewReport = async (filePath) => { } .history-list { - min-height: 80px; - max-height: 200px; + min-height: 80px; + max-height: 240px; overflow-y: auto; .history-items { diff --git a/src/views/admin/orders/components/OrderFormDialog.vue b/src/views/admin/orders/components/OrderFormDialog.vue index c95457e..8016a41 100644 --- a/src/views/admin/orders/components/OrderFormDialog.vue +++ b/src/views/admin/orders/components/OrderFormDialog.vue @@ -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( + ``, + ); + 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, + ); } - if (typeof editor.focus === 'function') editor.focus(); - editor.dangerouslyInsertHtml( - ``, - ); - if (typeof editor.updateView === 'function') editor.updateView(); }; 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; diff --git a/src/views/admin/orders/list.vue b/src/views/admin/orders/list.vue index 5f85389..22f7260 100644 --- a/src/views/admin/orders/list.vue +++ b/src/views/admin/orders/list.vue @@ -91,7 +91,7 @@ - +