fix(upload): address code review findings
final code review 后的修复:
1. blob 检查收紧(Important): md.contains("blob:") 误伤合法讨论 blob URL
的代码块/正文,改为检测 markdown 图片语法 ](blob: 才算占位符泄漏
2. 重试错误原地更新(Minor): seen_error_ids 去重逻辑导致重试后再失败时
顶部提示停留在旧错误文案;改为已存在 id 时原地更新 message
3. destroy 清理 coordinatorRef(Minor): destroy() 调 setUploadCoordinator(null),
避免 NodeView 指向已销毁的 coordinator(防御性)
4. 删除 NodeView 未用的 editor 字段(Nit): 死代码清理
验证: cargo test (384 passed) / clippy (本次零警告) / dx check / tiptap build 全过
This commit is contained in:
parent
a10f79d47e
commit
cee4f3f3ef
@ -235,6 +235,8 @@ class TiptapEditorInstance {
|
|||||||
this.editor?.destroy()
|
this.editor?.destroy()
|
||||||
this.editor = null
|
this.editor = null
|
||||||
this.coordinator = null
|
this.coordinator = null
|
||||||
|
// 清除 NodeView 的 coordinator 引用,避免指向已销毁的实例
|
||||||
|
setUploadCoordinator(null)
|
||||||
// 清理源码模式相关引用(容器 innerHTML 已清空,DOM 会随之移除)
|
// 清理源码模式相关引用(容器 innerHTML 已清空,DOM 会随之移除)
|
||||||
this.sourceTextarea = null
|
this.sourceTextarea = null
|
||||||
this.toggleButton = null
|
this.toggleButton = null
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Image } from '@tiptap/extension-image'
|
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'
|
import type { Node as PMNode } from '@tiptap/pm/model'
|
||||||
|
|
||||||
/** NodeView 按钮点击回调注入接口。 */
|
/** NodeView 按钮点击回调注入接口。 */
|
||||||
@ -20,7 +20,6 @@ export interface UploadNodeViewCallbacks {
|
|||||||
* 属性变化时 ProseMirror 调 update(node),NodeView 比较新旧 data-upload-state 重渲染遮罩。
|
* 属性变化时 ProseMirror 调 update(node),NodeView 比较新旧 data-upload-state 重渲染遮罩。
|
||||||
*/
|
*/
|
||||||
class UploadImageNodeView {
|
class UploadImageNodeView {
|
||||||
private editor: Editor
|
|
||||||
private node: PMNode
|
private node: PMNode
|
||||||
private callbacks: UploadNodeViewCallbacks
|
private callbacks: UploadNodeViewCallbacks
|
||||||
|
|
||||||
@ -30,12 +29,10 @@ class UploadImageNodeView {
|
|||||||
|
|
||||||
constructor(opts: {
|
constructor(opts: {
|
||||||
node: PMNode
|
node: PMNode
|
||||||
editor: Editor
|
|
||||||
HTMLAttributes: Record<string, unknown>
|
HTMLAttributes: Record<string, unknown>
|
||||||
callbacks: UploadNodeViewCallbacks
|
callbacks: UploadNodeViewCallbacks
|
||||||
}) {
|
}) {
|
||||||
this.node = opts.node
|
this.node = opts.node
|
||||||
this.editor = opts.editor
|
|
||||||
this.callbacks = opts.callbacks
|
this.callbacks = opts.callbacks
|
||||||
|
|
||||||
this.container = document.createElement('div')
|
this.container = document.createElement('div')
|
||||||
@ -183,10 +180,9 @@ export const UploadImage = Image.configure({ allowBase64: true }).extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ({ node, HTMLAttributes, editor }) => {
|
return ({ node, HTMLAttributes }) => {
|
||||||
return new UploadImageNodeView({
|
return new UploadImageNodeView({
|
||||||
node,
|
node,
|
||||||
editor,
|
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
onRetry: (id) => coordinatorRef?.retryUpload(id),
|
onRetry: (id) => coordinatorRef?.retryUpload(id),
|
||||||
|
|||||||
@ -332,14 +332,21 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
for ev in parsed.events {
|
for ev in parsed.events {
|
||||||
match ev.kind.as_str() {
|
match ev.kind.as_str() {
|
||||||
"error" => {
|
"error" => {
|
||||||
if !seen_error_ids.contains(&ev.upload_id) {
|
let msg = ev.error_msg.unwrap_or_else(|| "上传失败".to_string());
|
||||||
seen_error_ids.insert(ev.upload_id.clone());
|
if seen_error_ids.insert(ev.upload_id.clone()) {
|
||||||
|
// 新失败:追加提示
|
||||||
upload_errors.write().push(UploadErrorEntry {
|
upload_errors.write().push(UploadErrorEntry {
|
||||||
id: ev.upload_id,
|
id: ev.upload_id,
|
||||||
file_name: ev.file_name,
|
file_name: ev.file_name,
|
||||||
message: ev.error_msg
|
message: msg,
|
||||||
.unwrap_or_else(|| "上传失败".to_string()),
|
|
||||||
});
|
});
|
||||||
|
} 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" => {
|
"success" | "removed" => {
|
||||||
@ -392,8 +399,10 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
})()
|
})()
|
||||||
"#).ok().and_then(|v| v.as_string()).unwrap_or_default();
|
"#).ok().and_then(|v| v.as_string()).unwrap_or_default();
|
||||||
|
|
||||||
// 兜底:扫描残留的 blob: 或 data-upload-state(轮询窗口期漏判防护)
|
// 兜底:扫描残留的上传占位符标记(轮询窗口期漏判防护)
|
||||||
if md.contains("blob:") || md.contains("data-upload-state") {
|
// 检测  形式的泄漏图片 src,而非裸 "blob:" 字符串,
|
||||||
|
// 避免误伤合法讨论 blob URL 的代码块/正文。
|
||||||
|
if md.contains("](blob:") || md.contains("data-upload-state") {
|
||||||
error.set(Some("检测到未完成上传的图片,请处理后保存".to_string()));
|
error.set(Some("检测到未完成上传的图片,请处理后保存".to_string()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user