// 「系统」Tab:数据备份导出/导入(依赖 base.js:esc/_csrfHeaders/apiPost/showToast/handleResult) let _restoreToken = ''; // 预览通过后的暂存标识(用于 apply) // ================== 导出 ================== async function doExportBackup(){ const st=document.getElementById('backup-status'); const btn=event&&event.target||null; if(btn)btn.disabled=true; const includeApk=document.getElementById('backup-include-apk').checked; if(st)st.textContent='正在生成备份并打包…'; try{ const r=await fetch('/api/system/backup/export',{ method:'POST', headers:{'Content-Type':'application/json',..._csrfHeaders()}, body:JSON.stringify({include_apk:includeApk}) }); if(r.status===401){window.location='/login';return;} if(!r.ok){ let msg='导出失败'; try{const d=await r.json();if(d&&d.error)msg=d.error;}catch(e){} showToast(msg,'error'); if(st)st.textContent=''; return; } const blob=await r.blob(); const cd=r.headers.get('Content-Disposition')||''; const m=/filename="?([^";]+)"?/i.exec(cd); const fname=m?m[1]:('auto_control_backup_'+Date.now()+'.zip'); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url;a.download=fname;document.body.appendChild(a); a.click();a.remove(); setTimeout(()=>URL.revokeObjectURL(url),3000); if(st)st.textContent='已生成 '+fname; showToast('导出成功','success'); }catch(e){ showToast('导出失败: '+e,'error'); if(st)st.textContent=''; }finally{ if(btn)btn.disabled=false; } } // ================== 导入:上传预览 ================== async function previewRestore(){ const fileEl=document.getElementById('restore-file'); const f=fileEl&&fileEl.files&&fileEl.files[0]; const box=document.getElementById('restore-preview'); const st=document.getElementById('restore-status'); const applyBtn=document.getElementById('btn-apply-restore'); if(!f){showToast('请先选择备份文件(.zip 或 .db)','error');return;} _restoreToken=''; if(applyBtn)applyBtn.style.display='none'; if(st)st.textContent=''; box.innerHTML='解析校验中…'; const fd=new FormData(); fd.append('file',f); try{ const r=await fetch('/api/system/backup/preview',{ method:'POST', headers:_csrfHeaders(), // FormData 不能手动设 Content-Type body:fd }); if(r.status===401){window.location='/login';return;} const d=await r.json(); box.innerHTML=''; if(!d.ok){showToast(d.error||'解析失败','error');return;} _restoreToken=d.token; renderRestorePreview(d.preview); if(applyBtn)applyBtn.style.display=''; }catch(e){ box.innerHTML=''; showToast('上传失败: '+e,'error'); } } function renderRestorePreview(p){ const box=document.getElementById('restore-preview'); const rows=(p.tables||[]).map(t=> ''+esc(t.label||t.table)+''+esc(t.table)+''+esc(t.rows)+'').join(''); const warns=(p.warnings||[]).map(w=>'
  • '+esc(w)+'
  • ').join(''); box.innerHTML= ''+ ''+ ''+ ''+ ''+ ''+ '
    文件'+esc(p.file_name||'')+'('+(p.file_size!=null?fmtSize(p.file_size):'')+')
    库结构版本备份 v'+esc(p.schema_version)+' / 当前 v'+esc(p.current_schema_version)+'
    完整性'+esc(p.integrity||'')+'
    '+ '
    表数据行数
    '+ ''+(rows||'')+'
    业务表行数
    —
    '+ (warns?'
    ':''); } function fmtSize(n){ if(n==null)return ''; if(n<1024)return n+' B'; if(n<1024*1024)return (n/1024).toFixed(1)+' KB'; return (n/1024/1024).toFixed(1)+' MB'; } // ================== 导入:确认应用 ================== async function applyRestore(){ const applyBtn=document.getElementById('btn-apply-restore'); const st=document.getElementById('restore-status'); if(!_restoreToken)return; if(!confirm('确认应用导入?\n\n系统会先自动备份当前库(data/backups/pre_restore_*.db),再把待导入数据覆盖现有全部数据。\n导入将在重启 web_server 后生效。\n\n确定继续?'))return; if(applyBtn)applyBtn.disabled=true; if(st)st.textContent='正在生成恢复任务…'; const r=await apiPost('/api/system/backup/apply',{token:_restoreToken}); if(!r)return; if(r.ok){ const box=document.getElementById('restore-preview'); box.innerHTML='
    '+ '✅ 恢复任务已生成。
    当前库已自动备份到 '+esc(r.backup_name||'')+'。
    '+ '请重启 web_server,启动时将自动应用导入数据(恢复完成前不要再次导入)。
    '; if(st)st.textContent=''; if(applyBtn)applyBtn.style.display='none'; document.getElementById('restore-file').value=''; _restoreToken=''; }else{ showToast(r.error||'应用导入失败','error'); if(applyBtn)applyBtn.disabled=false; } }