feat(write): upload failure notices, save blocking, and server error passthrough

- 顶部上传失败提示:多条堆叠,×关闭同时删除编辑器内失败占位符(避免孤儿)
  for err in upload_errors().clone() 取 owned 值,onclick 捕获 owned id
- 保存拦截双重防护:
  · counts 检查(uploading/error > 0 阻止保存并提示张数)
  · markdown 兜底扫描 blob:/data-upload-state(轮询窗口期漏判防护)
- onImageUpload fetch 改造:非 2xx 时读取服务端 error 字段,
  透传中文错误(如"文件超过大小限制")而非 'Upload failed: 413'
This commit is contained in:
xfy 2026-06-22 15:38:53 +08:00
parent d90dd60e89
commit a10f79d47e

View File

@ -214,7 +214,13 @@ fn write_editor(post_id: Option<i32>) -> Element {
}) })
.then(function(response) { .then(function(response) {
if (!response.ok) { if (!response.ok) {
throw new Error('Upload failed: ' + response.status); // 读取服务端返回的中文错误({"success":false,"error":"文件超过大小限制"}
return response.json().catch(function() { return null; }).then(function(data) {
if (data && data.error) {
throw new Error(data.error);
}
throw new Error(': ' + response.status);
});
} }
return response.json(); return response.json();
}) })
@ -356,6 +362,19 @@ fn write_editor(post_id: Option<i32>) -> Element {
// 提交表单:校验标题与内容,读取 Tiptap 编辑器 Markdown调用 create_post 或 update_post。 // 提交表单:校验标题与内容,读取 Tiptap 编辑器 Markdown调用 create_post 或 update_post。
let on_submit = move |_| { let on_submit = move |_| {
// 上传未完成/失败拦截:有占位符时阻止保存
let in_flight = uploads_in_flight.read();
if in_flight.uploading > 0 || in_flight.error > 0 {
let msg = if in_flight.uploading > 0 {
format!("{} 张图片正在上传,请等待完成后再保存", in_flight.uploading)
} else {
format!("{} 张图片上传失败,请移除或重试后再保存", in_flight.error)
};
error.set(Some(msg));
return;
}
drop(in_flight);
if title().trim().is_empty() { if title().trim().is_empty() {
error.set(Some("标题不能为空".to_string())); error.set(Some("标题不能为空".to_string()));
#[allow(clippy::needless_return)] #[allow(clippy::needless_return)]
@ -373,6 +392,12 @@ 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") {
error.set(Some("检测到未完成上传的图片,请处理后保存".to_string()));
return;
}
if md.trim().is_empty() { if md.trim().is_empty() {
error.set(Some("内容不能为空".to_string())); error.set(Some("内容不能为空".to_string()));
return; return;
@ -579,6 +604,34 @@ fn write_editor(post_id: Option<i32>) -> Element {
} }
} }
// 上传失败提示:多条堆叠,×关闭同时删除编辑器内失败占位符(避免孤儿)
for err in upload_errors().clone() {
div { class: "flex-shrink-0 flex items-center justify-between gap-3 px-4 py-2 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl text-sm border border-red-100 dark:border-red-900/30 mb-2",
span { "图片上传失败: {err.file_name} — {err.message}" }
button {
class: "shrink-0 text-red-400 hover:text-red-600 cursor-pointer text-lg leading-none",
aria_label: "关闭提示",
onclick: {
// 捕获 owned id避免借用临时值
let id = err.id.clone();
let mut upload_errors = upload_errors;
move |_| {
// 关闭提示同时删除编辑器内失败占位符(避免孤儿)
#[cfg(target_arch = "wasm32")]
{
let _ = js_sys::eval(&format!(
"(function(){{var e=window.TiptapEditor&&window.TiptapEditor._instances&&window.TiptapEditor._instances.get('tiptap-editor');if(e&&e.removeUploadByUploadId){{e.removeUploadByUploadId({:?});}}}})()",
id
));
}
upload_errors.write().retain(|e| e.id != id);
}
},
"×"
}
}
}
// 底部操作栏 - 在编辑器下方,左对齐 // 底部操作栏 - 在编辑器下方,左对齐
div { class: "flex-shrink-0 flex items-center gap-2 pt-2 pb-4", div { class: "flex-shrink-0 flex items-center gap-2 pt-2 pb-4",
button { button {