diff --git a/libs/tiptap-editor/src/index.ts b/libs/tiptap-editor/src/index.ts index e0218e1..5d67c19 100644 --- a/libs/tiptap-editor/src/index.ts +++ b/libs/tiptap-editor/src/index.ts @@ -235,6 +235,8 @@ class TiptapEditorInstance { this.editor?.destroy() this.editor = null this.coordinator = null + // 清除 NodeView 的 coordinator 引用,避免指向已销毁的实例 + setUploadCoordinator(null) // 清理源码模式相关引用(容器 innerHTML 已清空,DOM 会随之移除) this.sourceTextarea = null this.toggleButton = null diff --git a/libs/tiptap-editor/src/upload-image.ts b/libs/tiptap-editor/src/upload-image.ts index f45f6d3..7744dea 100644 --- a/libs/tiptap-editor/src/upload-image.ts +++ b/libs/tiptap-editor/src/upload-image.ts @@ -1,5 +1,5 @@ import { Image } from '@tiptap/extension-image' -import { mergeAttributes, type Editor } from '@tiptap/core' +import { mergeAttributes } from '@tiptap/core' import type { Node as PMNode } from '@tiptap/pm/model' /** NodeView 按钮点击回调注入接口。 */ @@ -20,7 +20,6 @@ export interface UploadNodeViewCallbacks { * 属性变化时 ProseMirror 调 update(node),NodeView 比较新旧 data-upload-state 重渲染遮罩。 */ class UploadImageNodeView { - private editor: Editor private node: PMNode private callbacks: UploadNodeViewCallbacks @@ -30,12 +29,10 @@ class UploadImageNodeView { constructor(opts: { node: PMNode - editor: Editor HTMLAttributes: Record callbacks: UploadNodeViewCallbacks }) { this.node = opts.node - this.editor = opts.editor this.callbacks = opts.callbacks this.container = document.createElement('div') @@ -183,10 +180,9 @@ export const UploadImage = Image.configure({ allowBase64: true }).extend({ }, addNodeView() { - return ({ node, HTMLAttributes, editor }) => { + return ({ node, HTMLAttributes }) => { return new UploadImageNodeView({ node, - editor, HTMLAttributes, callbacks: { onRetry: (id) => coordinatorRef?.retryUpload(id), diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index df3a067..9bbeccb 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -332,14 +332,21 @@ fn write_editor(post_id: Option) -> Element { for ev in parsed.events { match ev.kind.as_str() { "error" => { - if !seen_error_ids.contains(&ev.upload_id) { - seen_error_ids.insert(ev.upload_id.clone()); + let msg = ev.error_msg.unwrap_or_else(|| "上传失败".to_string()); + if seen_error_ids.insert(ev.upload_id.clone()) { + // 新失败:追加提示 upload_errors.write().push(UploadErrorEntry { id: ev.upload_id, file_name: ev.file_name, - message: ev.error_msg - .unwrap_or_else(|| "上传失败".to_string()), + message: msg, }); + } else { + // 已存在的 id(重试后再失败):原地更新消息, + // 避免顶部提示停留在旧错误文案。 + let mut errors = upload_errors.write(); + if let Some(entry) = errors.iter_mut().find(|e| e.id == ev.upload_id) { + entry.message = msg; + } } } "success" | "removed" => { @@ -392,8 +399,10 @@ fn write_editor(post_id: Option) -> Element { })() "#).ok().and_then(|v| v.as_string()).unwrap_or_default(); - // 兜底:扫描残留的 blob: 或 data-upload-state(轮询窗口期漏判防护) - if md.contains("blob:") || md.contains("data-upload-state") { + // 兜底:扫描残留的上传占位符标记(轮询窗口期漏判防护) + // 检测 ![](blob:...) 形式的泄漏图片 src,而非裸 "blob:" 字符串, + // 避免误伤合法讨论 blob URL 的代码块/正文。 + if md.contains("](blob:") || md.contains("data-upload-state") { error.set(Some("检测到未完成上传的图片,请处理后保存".to_string())); return; }