From 43b4d757bb5309d2ad21b37ec8bfa9e47da6517a Mon Sep 17 00:00:00 2001 From: cbb Date: Tue, 13 Oct 2026 21:58:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v1.4.1=20=E2=80=94=20=E6=95=B0=E5=80=BC?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=94=B9=E5=8A=A8=E5=8D=B3=E7=94=9F=E6=95=88?= =?UTF-8?q?=EF=BC=8C=E5=8E=BB=E6=8E=89=E4=BF=9D=E5=AD=98=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同步间隔/并发数/重试次数三个数值输入改 onchange 自动保存: 改完失焦或按回车即生效并显示「✓ 已生效」(2 秒后淡出), 不再需要点保存按钮 - 非法值(超范围/非数字)自动回滚成服务器值并红字提示原因 - 防重复保存:值没变不触发请求;空值前端拦截不发请求 - 修复 saveInterval 等包装函数未 return 内部 Promise 导致调用方 无法等待保存完成的问题 - manifest 1.4.0 → 1.4.1 --- GiteaHubSync/app/backend/panel.py | 76 +++++++++++++++++-------------- GiteaHubSync/manifest | 2 +- README.md | 2 + 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/GiteaHubSync/app/backend/panel.py b/GiteaHubSync/app/backend/panel.py index c9ef80c..64527fc 100644 --- a/GiteaHubSync/app/backend/panel.py +++ b/GiteaHubSync/app/backend/panel.py @@ -952,6 +952,10 @@ h2 .h2sub{font-size:12px;font-weight:400;color:#8a919e;margin-left:10px} .opttext .swsub{font-size:12px;color:#646a73;margin-top:2px;line-height:1.6} .numin{flex:none;width:110px;border:1px solid #d0d5dd;border-radius:10px;padding:10px 12px;font-size:14px;outline:none;text-align:center} .numin:focus{border-color:#7c3aed} +.numwrap{display:flex;align-items:center;gap:8px;flex:none} +.saved-tip{font-size:12px;font-weight:600;white-space:nowrap} +.saved-tip.ok{color:#0a7d34} +.saved-tip.err{color:#b91c1c} table.hover tbody tr:hover{background:#f8fafc} td.ops{white-space:nowrap} td.ops button,td.ops .pill{margin:2px 4px 2px 0} @@ -1263,24 +1267,30 @@ PAGE_SETTINGS = """
- - +
+ + +
同步间隔(秒)
- - +
+ + +
同时同步项目数
- - +
+ + +
失败重试次数
@@ -1293,6 +1303,30 @@ PAGE_SETTINGS = """ async function jget(u){const r=await fetch(u);return r.json()} function esc(s){const d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML} function showMsg(id,t,ok){const m=document.getElementById(id);m.textContent=t;m.className='msg '+(ok?'ok':'err');} +// ---- 数值设置:改动即生效(失焦/回车自动保存,无需点按钮) ---- +let lastVals={}; +function valChanged(inputId,key){const v=document.getElementById(inputId).value;if(lastVals[key]===v)return false;lastVals[key]=v;return true;} +function flashSaved(id,ok,msg){ + const el=document.getElementById(id); + el.textContent=ok?'✓ 已生效':('✗ '+(msg||'保存失败')); + el.className='saved-tip '+(ok?'ok':'err'); + if(ok)setTimeout(()=>{if(el.textContent==='✓ 已生效')el.textContent='';},2000); +} +async function saveSetting(key,inputId,tipId,field){ + if(!valChanged(inputId,key))return; // 没改动不重复保存 + const v=parseInt(document.getElementById(inputId).value,10); + if(isNaN(v)){flashSaved(tipId,false,'请输入有效数字');return;} + let r; + try{ + r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({[field]:v})})).json(); + }catch(e){flashSaved(tipId,false,'网络错误');return;} + await refresh(); // 刷新后非法值会自动回滚成服务器值 + flashSaved(tipId,!!r.ok,r.msg); +} +async function saveInterval(){return saveSetting('interval','syncInterval','intervalSaved','interval');} +async function saveWorkers(){return saveSetting('workers','syncWorkers','workersSaved','max_workers');} +async function saveRetries(){return saveSetting('retries','syncRetries','retriesSaved','max_retries');} async function refresh(){ try{ const s=await jget('api/settings'); @@ -1304,13 +1338,14 @@ async function refresh(){ document.getElementById('swAuto').checked=!!s.auto_create; const iv=document.getElementById('syncInterval'); iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400; - document.getElementById('intervalTip').textContent='每隔 '+fmtInterval(s.interval||10)+' 检查一次 Gitea 是否有新提交('+(s.min_interval||5)+'~'+(s.max_interval||86400)+' 秒),改完立即生效'; + document.getElementById('intervalTip').textContent='每隔 '+fmtInterval(s.interval||10)+' 检查一次 Gitea 是否有新提交('+(s.min_interval||5)+'~'+(s.max_interval||86400)+' 秒),改动即生效'; const wk=document.getElementById('syncWorkers'); wk.value=s.max_workers||3;wk.min=s.min_workers||1;wk.max=s.max_workers_limit||8; document.getElementById('workersTip').textContent='每轮最多 '+(s.max_workers||3)+' 个项目同时克隆/推送(1~'+(s.max_workers_limit||8)+'),项目多时能显著加快;网络或 NAS 负载高就调小'; const rt=document.getElementById('syncRetries'); rt.value=(s.max_retries==null?5:s.max_retries);rt.min=0;rt.max=20; document.getElementById('retriesTip').textContent='同步失败后每隔 '+(s.retry_interval||60)+' 秒自动重试失败的仓库,最多 '+(s.max_retries==null?5:s.max_retries)+' 次(0 = 不重试);与同步间隔无关,专治网络抖动'; + lastVals={interval:iv.value,workers:wk.value,retries:rt.value}; const cur=(s.accounts||[]).find(a=>a.id===s.active); // 当前账号无 Token 时,顶部提示为什么不能自动建库、怎么解锁 document.getElementById('accBanner').innerHTML=(cur&&!cur.has_token) @@ -1400,33 +1435,6 @@ function fmtInterval(s){ if(s>=60)return Math.round(s/6)/10+' 分钟'; return s+' 秒'; } -async function saveInterval(){ - const v=parseInt(document.getElementById('syncInterval').value,10); - const b=document.getElementById('btnInterval');b.disabled=true;b.textContent='⏳ 保存中…'; - try{ - const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({interval:v})})).json(); - showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok); - }catch(e){showMsg('msgCfg','请求失败: '+e,false)} - finally{b.disabled=false;b.textContent='保存间隔';refresh();} -} -async function saveWorkers(){ - const v=parseInt(document.getElementById('syncWorkers').value,10); - const b=document.getElementById('btnWorkers');b.disabled=true;b.textContent='⏳ 保存中…'; - try{ - const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({max_workers:v})})).json(); - showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok); - }catch(e){showMsg('msgCfg','请求失败: '+e,false)} - finally{b.disabled=false;b.textContent='保存并发';refresh();} -} -async function saveRetries(){ - const v=parseInt(document.getElementById('syncRetries').value,10); - const b=document.getElementById('btnRetries');b.disabled=true;b.textContent='⏳ 保存中…'; - try{ - const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({max_retries:v})})).json(); - showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok); - }catch(e){showMsg('msgCfg','请求失败: '+e,false)} - finally{b.disabled=false;b.textContent='保存次数';refresh();} -} refresh(); """ diff --git a/GiteaHubSync/manifest b/GiteaHubSync/manifest index 1c31bde..b16e4e7 100644 --- a/GiteaHubSync/manifest +++ b/GiteaHubSync/manifest @@ -1,5 +1,5 @@ appname = GiteaHubSync -version = 1.4.0 +version = 1.4.1 display_name = Gitea→GitHub 同步助手 desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。 platform = all diff --git a/README.md b/README.md index fc49ba2..0ac47a0 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ NAS 重启自动恢复。控制面板带自动同步开关、立即同步、仓 - **同步间隔可调**:设置页可改检查间隔(5 秒 ~ 1 天,默认 10 秒),改完立即生效 - **多项目并行同步**:每轮最多 N 个项目同时克隆/推送(默认 3,可调 1~8); 在状态页点单个仓库的「同步」也不再被整轮同步阻塞(同一仓库有防重入) +- **设置改动即生效**:同步间隔 / 并发数 / 重试次数等数值设置改完自动保存 + (失焦或回车即生效,无需点按钮),非法值自动回滚并提示 - **失败自动重试**:同步失败后每隔 1 分钟自动重试失败的仓库,最多 N 次 (默认 5,可调 0~20,0 = 不重试);重试与同步间隔无关,专治网络抖动, 重试中的仓库在状态页显示「重试中 x/N」