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:
parent
d90dd60e89
commit
a10f79d47e
@ -214,7 +214,13 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
||||
})
|
||||
.then(function(response) {
|
||||
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();
|
||||
})
|
||||
@ -356,6 +362,19 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
||||
|
||||
// 提交表单:校验标题与内容,读取 Tiptap 编辑器 Markdown,调用 create_post 或 update_post。
|
||||
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() {
|
||||
error.set(Some("标题不能为空".to_string()));
|
||||
#[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();
|
||||
|
||||
// 兜底:扫描残留的 blob: 或 data-upload-state(轮询窗口期漏判防护)
|
||||
if md.contains("blob:") || md.contains("data-upload-state") {
|
||||
error.set(Some("检测到未完成上传的图片,请处理后保存".to_string()));
|
||||
return;
|
||||
}
|
||||
|
||||
if md.trim().is_empty() {
|
||||
error.set(Some("内容不能为空".to_string()));
|
||||
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",
|
||||
button {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user