fix(system): 备份/恢复任务轮询永不启动导致按钮卡死在 loading

点击「创建备份」后接口返回 task id,但按钮一直转圈、列表不刷新。
根因是 AGENTS.md 踩坑记录里的「Reactive hooks 不追踪普通 props/snapshot」:

进度轮询 use_future 在挂载时把 active_task_id 快照进 _task_id_for_poll
(彼时为 None),use_future 只运行一次即 return;用户点击后 create_backup
返回 task id 并 active_task_id.set(Some(id)),但 future 已结束、永不重跑
→ 轮询不启动 → busy 永远 true。恢复确认 use_future 有同样的快照陷阱
(_pending_for_confirm),导致点「恢复」连确认框都不弹。

改用与 DbStatusTab 自动刷新一致的长生命周期 loop 模式:use_future 只跑
一次,内部 loop 每轮迭代读 active_task_id() / pending_restore() 信号,
空闲时 200ms yield 呼吸。这样信号更新在下一轮迭代即被感知,无需依赖
ReactiveContext 对挂载期快照的订阅。
This commit is contained in:
xfy 2026-07-13 13:56:22 +08:00
parent cf14aae17f
commit 2e40836480

View File

@ -1254,96 +1254,118 @@ fn BackupTab() -> Element {
}); });
// 恢复确认pending_restore 被设置时弹出原生 confirm确认后发起 restore_backup // 恢复确认pending_restore 被设置时弹出原生 confirm确认后发起 restore_backup
// 并开始进度轮询。用 use_future 响应 pending_restore 变化。 // 并开始进度轮询。
// _pending_for_confirm 被 clone 进 async moveFnMut 可重入),用 Option::clone 兜底。 //
let _pending_for_confirm = pending_restore.read().clone(); // 采用与 DbStatusTab 自动刷新相同的长生命周期 loop 模式:在每次循环内部读
// pending_restore() 信号。原先用 use_future 在挂载时快照一次 pending_restore
//(彼时为 Noneuse_future 仅运行一次即 return用户后续点击"恢复"设置
// pending_restore 后 future 不会重跑Dioxus 的 ReactiveContext 只订阅闭包
// *执行期间* 读到的 signal同步体的 .read().clone() 不建立订阅)→ 确认框
// 永不弹出。改成 loop 内读信号后,下一轮迭代即拿到最新值。
use_future(move || { use_future(move || {
let mut pending_restore = pending_restore; let mut pending_restore = pending_restore;
let mut busy = busy; let mut busy = busy;
let mut active_progress = active_progress; let mut active_progress = active_progress;
let mut active_task_id = active_task_id; let mut active_task_id = active_task_id;
let mut error = error; let mut error = error;
let pending_for_confirm = _pending_for_confirm.clone();
async move { async move {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
let fname = match pending_for_confirm { loop {
Some(f) => f, let fname = match pending_restore() {
None => return, Some(f) => f,
}; None => {
let confirmed = web_sys::window() // 空闲:短 yield 让事件循环呼吸,避免忙等。
.and_then(|w| { wasm_sleep(200).await;
w.confirm_with_message(&format!( continue;
"恢复将覆盖现有数据,确认恢复 {}\n\n仅本系统生成的备份可恢复。", }
fname };
)) // 取到了:先清空 pending防止 loop 重入时再次确认。
.ok() pending_restore.set(None);
}) let confirmed = web_sys::window()
== Some(true); .and_then(|w| {
if confirmed { w.confirm_with_message(&format!(
busy.set(true); "恢复将覆盖现有数据,确认恢复 {}\n\n仅本系统生成的备份可恢复。",
active_progress.set(None); fname
match restore_backup(fname, true).await { ))
Ok(id) => active_task_id.set(Some(id)), .ok()
Err(e) => { })
error.set(Some(e.to_string())); == Some(true);
busy.set(false); if confirmed {
busy.set(true);
active_progress.set(None);
match restore_backup(fname, true).await {
Ok(id) => active_task_id.set(Some(id)),
Err(e) => {
error.set(Some(e.to_string()));
busy.set(false);
}
} }
} }
// 处理完一轮后回到 loop 顶部继续等待下一个 pending。
} }
// 无论确认与否都清空 pending
pending_restore.set(None);
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let _ = (pending_restore, busy, active_progress, active_task_id, error, pending_for_confirm); let _ = (pending_restore, busy, active_progress, active_task_id, error);
} }
} }
}); });
// 任务进度轮询active_task_id 存在时每 1.5s 拉取进度Done/Failed 后停止 + 刷新列表 // 任务进度轮询active_task_id 存在时每 1.5s 拉取进度Done/Failed 后停止 + 刷新列表。
// _task_id_for_poll 被 clone 进 async moveFnMut 可重入)。 //
let _task_id_for_poll = active_task_id.read().clone(); // 同样用长生命周期 loop + 循环内读 active_task_id() 的模式。原先在挂载时把
// active_task_id 快照进 _task_id_for_poll彼时为 Noneuse_future 只跑一次
// 即 return用户点"创建备份"后 create_backup 返回 task id 并设置信号,但
// future 已结束 → 轮询永不启动busy 永远为 true用户报告的 bug
use_future(move || { use_future(move || {
let mut active_task_id = active_task_id; let mut active_task_id = active_task_id;
let mut active_progress = active_progress; let mut active_progress = active_progress;
let mut backups_f = backups; let mut backups_f = backups;
let mut busy_f = busy; let mut busy_f = busy;
let task_id_for_poll = _task_id_for_poll.clone();
async move { async move {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
let tid = match task_id_for_poll {
Some(t) => t,
None => return,
};
loop { loop {
wasm_sleep(1500).await; let tid = match active_task_id() {
match get_task_progress(tid.clone()).await { Some(t) => t,
Ok(p) => { None => {
let done = p.status == TaskStatus::Done || p.status == TaskStatus::Failed; // 空闲:短 yield最多 200ms 后响应新任务。
active_progress.set(Some(p)); wasm_sleep(200).await;
if done { continue;
// 刷新列表(备份完成后新文件出现)并清理任务态 }
if let Ok(list) = list_backups().await { };
backups_f.set(list); // 有任务在途:进入 1.5s 轮询,直到 Done/Failed/出错。
loop {
wasm_sleep(1500).await;
match get_task_progress(tid.clone()).await {
Ok(p) => {
let done = p.status == TaskStatus::Done
|| p.status == TaskStatus::Failed;
active_progress.set(Some(p));
if done {
// 刷新列表(备份完成后新文件出现)并清理任务态
if let Ok(list) = list_backups().await {
backups_f.set(list);
}
active_task_id.set(None);
busy_f.set(false);
break;
} }
}
Err(_) => {
active_task_id.set(None); active_task_id.set(None);
busy_f.set(false); busy_f.set(false);
break; break;
} }
} }
Err(_) => {
active_task_id.set(None);
busy_f.set(false);
break;
}
} }
// 内层 loop 退出后回到外层,继续等待下一个任务或空闲。
} }
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let _ = (active_task_id, active_progress, backups_f, busy_f, task_id_for_poll); let _ = (active_task_id, active_progress, backups_f, busy_f);
} }
} }
}); });