feat: v1.5.0 — GitHub 账号添加/编辑改为弹窗式(带✏️编辑按钮)

- 「添加 GitHub 账号」改为卡片里的按钮,点击弹出填写弹窗
  (模态:备注/用户名/Token/私有,含取消/验证并添加/ESC/点背景关闭)
- 表格每行新增 ✏️ 编辑按钮:弹出编辑弹窗,预填当前值
  (Token 字段空着+占位符显示当前掩码,留空则保留原 Token,
  只改备注/私有时无需重新输入 Token)
- 后端新增 /api/accounts/edit 端点(按 id 改备注/私有/Token),
  add 改为严格新增(gh_user 重复则拒绝并引导用编辑功能)
- 顶部横幅「去添加 Token」直接打开当前账号的编辑弹窗并聚焦 Token
- 移除了原来表内的「添加 Token」按钮(编辑弹窗已覆盖该流程)
- manifest 1.4.1 → 1.5.0
This commit is contained in:
cbb
2026-10-15 01:03:47 +08:00
parent 43b4d757bb
commit ca2fb7e735
2 changed files with 187 additions and 50 deletions
+186 -49
View File
@@ -956,6 +956,29 @@ h2 .h2sub{font-size:12px;font-weight:400;color:#8a919e;margin-left:10px}
.saved-tip{font-size:12px;font-weight:600;white-space:nowrap}
.saved-tip.ok{color:#0a7d34}
.saved-tip.err{color:#b91c1c}
input[readonly]{background:#f6f8fa;color:#646a73;cursor:not-allowed}
.b-icon{padding:5px 9px;min-width:auto;font-size:13px}
/* ---------- 模态弹窗 ---------- */
.modal{display:none;position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000;align-items:center;justify-content:center;padding:20px}
.modal.show{display:flex}
.modal-box{background:#fff;border-radius:14px;width:100%;max-width:480px;box-shadow:0 20px 60px rgba(0,0,0,.25);max-height:90vh;overflow:auto;animation:modalIn .15s ease-out}
@keyframes modalIn{from{transform:translateY(-8px);opacity:.6}to{transform:translateY(0);opacity:1}}
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:18px 22px;border-bottom:1px solid #eef0f3}
.modal-head h3{font-size:17px;margin:0}
.modal-close{background:none;border:none;font-size:24px;color:#646a73;cursor:pointer;padding:2px 10px;border-radius:6px;line-height:1}
.modal-close:hover{background:#f0f2f5;color:#1f2329}
.modal-body{padding:22px;display:flex;flex-direction:column;gap:8px}
.modal-body input[type=text],.modal-body input[type=password]{
border:1px solid #d0d5dd;border-radius:10px;padding:11px 14px;font-size:14px;outline:none;width:100%;box-sizing:border-box;font-family:inherit}
.modal-body input:focus{border-color:#7c3aed}
.modal-foot{padding:14px 22px;border-top:1px solid #eef0f3;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.modal-msg{font-size:13px;flex:1;min-width:0;word-break:break-word}
.modal-msg.ok{color:#0a7d34}
.modal-msg.err{color:#b91c1c}
.modal-msg:empty{display:none}
@media (max-width:760px){
.modal-box{max-width:none}
}
table.hover tbody tr:hover{background:#f8fafc}
td.ops{white-space:nowrap}
td.ops button,td.ops .pill{margin:2px 4px 2px 0}
@@ -1219,21 +1242,9 @@ PAGE_SETTINGS = """
</div>
<div class="card">
<h2>添加 / 更新 GitHub 账号<span class="h2sub">Token 验证通过后自动识别用户名</span></h2>
<label class="flabel">备注名<span class="sub">方便自己识别,可留空</span></label>
<div class="addrow"><input id="accLabel" placeholder="如:我的主账号"></div>
<label class="flabel">GitHub 用户名<span class="sub">留空则按 Token 自动识别</span></label>
<div class="addrow"><input id="accUser" placeholder="如 cbb2625274797"></div>
<label class="flabel">Personal Access Token</label>
<div class="addrow"><input id="accToken" type="password" placeholder="ghp_ 开头"></div>
<label class="flabel">自动创建的仓库设为</label>
<div class="addrow" style="align-items:center">
<label class="chk"><input type="checkbox" id="accPrivate" checked> 私有(推荐)</label>
</div>
<div class="btns" style="margin-top:14px">
<button class="b-add" id="btnAccAdd" onclick="addAccount()">✓ 验证并添加</button>
</div>
<div class="msg" id="msgAcc"></div>
<div class="tip">
<p style="color:#646a73;margin-bottom:14px">添加多个账号后可随时切换推送目标,每个账号独立记录同步进度。已存在的账号可点表格行右边的 ✏️ 编辑。</p>
<button class="b-add" onclick="openAddModal()" style="font-size:15px;padding:12px 24px">➕ 添加 GitHub 账号</button>
<div class="tip" style="margin-top:16px">
Token 获取:GitHub → Settings → Developer settings → Personal access tokens →
<b>Tokens (classic)</b> → Generate new token,勾选 <b>repo</b> 权限(即可自动创建仓库并推送)。<br>
链接:<a href="https://github.com/settings/tokens" target="_blank">github.com/settings/tokens</a> ·
@@ -1352,15 +1363,16 @@ async function refresh(){
?`<div class="banner warn"><span class="ic">⚠️</span><div><b>当前账号「${esc(cur.label)}」通过 SSH 私钥推送,暂时无法自动创建仓库。</b>
<br>GitHub 的创建仓库接口只认 Token 认证,SSH 私钥只能推送<b>已存在</b>的仓库——这是 GitHub 的限制,不是软件没做。
<br>给这个账号配上 Token 即可解锁(推送也会改走 HTTPS,更稳更快):
<button class="b-mini" style="margin-left:6px" onclick="upgradeAcc('${esc(cur.id)}','${esc(cur.gh_user)}')">去添加 Token</button></div></div>`
<button class="b-mini" style="margin-left:6px" onclick="upgradeAcc('${esc(cur.id)}')">去添加 Token</button></div></div>`
:'';
allAccounts=s.accounts||[];
document.getElementById('tbAcc').innerHTML=(s.accounts||[]).map(a=>{
const isCur=a.id===s.active;
const ops=isCur
? '<span class="pill on">当前使用</span>'
: `<button class="b-mini" onclick="switchAcc('${esc(a.id)}')">切换到此账号</button>`;
const verify=a.has_token?`<button class="b-mini" onclick="verifyAcc('${esc(a.id)}')">验证</button>`:'';
const upgrade=a.has_token?'':`<button class="b-mini" onclick="upgradeAcc('${esc(a.id)}','${esc(a.gh_user)}')">添加 Token</button>`;
const edit=`<button class="b-mini b-icon" title="编辑" onclick="openEditModal('${esc(a.id)}')">✏️</button>`;
const del=(a.has_token||(s.accounts||[]).length>1)
? `<button class="b-del" onclick="delAcc('${esc(a.id)}')">删除</button>`:'';
const method=a.has_token
@@ -1373,33 +1385,78 @@ async function refresh(){
+`<td data-label="Token" class="mono">${esc(a.token||'(未配置)')}</td>`
+`<td data-label="推送方式">${method}</td>`
+`<td data-label="自动建库">${create}</td>`
+`<td data-label="操作" class="ops">${ops} ${verify} ${upgrade} ${del}</td></tr>`;
+`<td data-label="操作" class="ops">${ops} ${edit} ${verify} ${del}</td></tr>`;
}).join('')||'<tr><td colspan="6" style="color:#646a73">暂无账号</td></tr>';
}catch(e){showMsg('msgAcc','刷新失败: '+e,false)}
}
function upgradeAcc(id,ghUser){
// 预填「添加/更新账号」表单并滚动过去,引导给旧账号补 Token
document.getElementById('accLabel').value='';
document.getElementById('accUser').value=ghUser||'';
document.getElementById('accToken').value='';
document.getElementById('accToken').focus();
document.getElementById('accToken').scrollIntoView({behavior:'smooth',block:'center'});
showMsg('msgAcc','已为你填好用户名 '+ghUser+',把 Token 粘贴进去点「验证并添加」即可(会更新原账号)',true);
// ---- GitHub 账号:弹窗式添加/编辑 ----
let allAccounts=[];
function showModalMsg(id,t,ok){
const m=document.getElementById(id);
m.textContent=t||''; m.className='modal-msg'+(ok===true?' ok':(ok===false?' err':''));
}
async function addAccount(){
const label=document.getElementById('accLabel').value.trim();
const gh_user=document.getElementById('accUser').value.trim();
const token=document.getElementById('accToken').value.trim();
const priv=document.getElementById('accPrivate').checked;
if(!token){showMsg('msgAcc','请填写 Personal Access Token',false);return;}
const b=document.getElementById('btnAccAdd');b.disabled=true;b.textContent='⏳ 验证中…';
function closeModal(id){ document.getElementById(id).classList.remove('show'); }
function openAddModal(){
document.getElementById('addLabel').value='';
document.getElementById('addUser').value='';
document.getElementById('addToken').value='';
document.getElementById('addPrivate').checked=true;
showModalMsg('addMsg','',null);
document.getElementById('modalAdd').classList.add('show');
setTimeout(()=>document.getElementById('addLabel').focus(),50);
}
function openEditModal(id, focusToken){
const acc=allAccounts.find(a=>a.id===id);
if(!acc)return;
document.getElementById('modalEdit').dataset.id=id;
document.getElementById('editLabel').value=acc.label||'';
document.getElementById('editUser').value=acc.gh_user||'';
document.getElementById('editToken').value='';
document.getElementById('editToken').placeholder=acc.token?'当前:'+acc.token:'(未配置)';
document.getElementById('editPrivate').checked=!!acc.private;
showModalMsg('editMsg','',null);
document.getElementById('modalEdit').classList.add('show');
setTimeout(()=>{
const el=focusToken?document.getElementById('editToken'):document.getElementById('editLabel');
el.focus();
},50);
}
function upgradeAcc(id){
// 顶部横幅「去添加 Token」:直接打开当前账号的编辑弹窗,聚焦 Token
openEditModal(id, true);
}
async function saveAdd(){
const body={
label: document.getElementById('addLabel').value.trim(),
gh_user: document.getElementById('addUser').value.trim(),
token: document.getElementById('addToken').value.trim(),
private: document.getElementById('addPrivate').checked,
};
if(!body.token){ showModalMsg('addMsg','请填写 Personal Access Token',false); return; }
const b=document.getElementById('btnAddSave'); b.disabled=true; b.textContent='⏳ 验证中…';
try{
const r=await (await fetch('api/accounts/add',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({label,gh_user,token,private:priv})})).json();
showMsg('msgAcc',r.msg||JSON.stringify(r),!!r.ok);
if(r.ok){document.getElementById('accLabel').value='';document.getElementById('accUser').value='';document.getElementById('accToken').value='';}
}catch(e){showMsg('msgAcc','请求失败: '+e,false)}
finally{b.disabled=false;b.textContent='✓ 验证并添加';refresh();}
const r=await (await fetch('api/accounts/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
showModalMsg('addMsg', r.msg||'', !!r.ok);
if(r.ok){ setTimeout(()=>{ closeModal('modalAdd'); refresh(); }, 700); }
}catch(e){ showModalMsg('addMsg','请求失败: '+e, false); }
finally{ b.disabled=false; b.textContent='✓ 验证并添加'; }
}
async function saveEdit(){
const id=document.getElementById('modalEdit').dataset.id;
if(!id){ showModalMsg('editMsg','内部错误:未选中账号',false); return; }
const body={
id,
label: document.getElementById('editLabel').value.trim(),
token: document.getElementById('editToken').value.trim(), // 空 = 不改
private: document.getElementById('editPrivate').checked,
};
const b=document.getElementById('btnEditSave'); b.disabled=true; b.textContent='⏳ 保存中…';
try{
const r=await (await fetch('api/accounts/edit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
showModalMsg('editMsg', r.msg||'', !!r.ok);
if(r.ok){ setTimeout(()=>{ closeModal('modalEdit'); refresh(); }, 700); }
}catch(e){ showModalMsg('editMsg','请求失败: '+e, false); }
finally{ b.disabled=false; b.textContent='保存'; }
}
async function switchAcc(id){
const r=await (await fetch('api/accounts/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})).json();
@@ -1437,6 +1494,64 @@ function fmtInterval(s){
}
refresh();
</script>
// ---- 模态弹窗:添加 ----
<div class="modal" id="modalAdd">
<div class="modal-box">
<div class="modal-head">
<h3>添加 GitHub 账号</h3>
<button class="modal-close" onclick="closeModal('modalAdd')" aria-label="关闭">×</button>
</div>
<div class="modal-body">
<label class="flabel" style="margin-top:0">备注名<span class="sub">方便自己识别,可留空</span></label>
<input id="addLabel" placeholder="如:我的主账号" autocomplete="off">
<label class="flabel">GitHub 用户名<span class="sub">留空则按 Token 自动识别</span></label>
<input id="addUser" placeholder="如 cbb2625274797" autocomplete="off">
<label class="flabel">Personal Access Token</label>
<input id="addToken" type="password" placeholder="ghp_ 开头" autocomplete="off">
<label class="chk" style="margin-top:8px"><input type="checkbox" id="addPrivate" checked> 自动创建的仓库设为私有(推荐)</label>
</div>
<div class="modal-foot">
<div class="modal-msg" id="addMsg"></div>
<button class="b-mini" onclick="closeModal('modalAdd')">取消</button>
<button class="b-add" id="btnAddSave" onclick="saveAdd()">✓ 验证并添加</button>
</div>
</div>
</div>
// ---- 模态弹窗:编辑 ----
<div class="modal" id="modalEdit">
<div class="modal-box">
<div class="modal-head">
<h3>编辑 GitHub 账号</h3>
<button class="modal-close" onclick="closeModal('modalEdit')" aria-label="关闭">×</button>
</div>
<div class="modal-body">
<label class="flabel" style="margin-top:0">备注名</label>
<input id="editLabel" autocomplete="off">
<label class="flabel">GitHub 用户名<span class="sub">账号标识,不可修改</span></label>
<input id="editUser" readonly tabindex="-1">
<label class="flabel">Personal Access Token<span class="sub">留空则保留当前 Token</span></label>
<input id="editToken" type="password" placeholder="ghp_ 开头(留空不修改)" autocomplete="off">
<label class="chk" style="margin-top:8px"><input type="checkbox" id="editPrivate"> 自动创建的仓库设为私有(推荐)</label>
</div>
<div class="modal-foot">
<div class="modal-msg" id="editMsg"></div>
<button class="b-mini" onclick="closeModal('modalEdit')">取消</button>
<button class="b-add" id="btnEditSave" onclick="saveEdit()">保存</button>
</div>
</div>
</div>
<script>
// 弹窗:点遮罩关闭 + ESC 关闭
document.querySelectorAll('.modal').forEach(m=>{
m.addEventListener('click',e=>{ if(e.target===m) m.classList.remove('show'); });
});
document.addEventListener('keydown',e=>{
if(e.key==='Escape') document.querySelectorAll('.modal.show').forEach(m=>m.classList.remove('show'));
});
</script>
"""
@@ -1633,6 +1748,7 @@ class Handler(BaseHTTPRequestHandler):
notes.append(f"注意:在新地址上找不到用户 {cfg['gitea_user']},请检查")
self._json({"ok": True, "msg": "已保存(" + ";".join(notes) + ")"})
elif path == "/api/accounts/add":
# 严格新增:Token 对应的 gh_user 若已存在则拒绝,引导用户用编辑功能
token = (data.get("token") or "").strip()
label = (data.get("label") or "").strip()
gh_user = (data.get("gh_user") or "").strip()
@@ -1647,16 +1763,9 @@ class Handler(BaseHTTPRequestHandler):
if gh_user and gh_user != login:
self._json({"ok": False, "msg": f"Token 对应的账号是 {login},与填写的 {gh_user} 不一致"})
return
existing = next((a for a in accounts if a["gh_user"] == login), None)
if existing:
existing["token"] = token
existing["private"] = private
if label:
existing["label"] = label
active_id = existing["id"]
save_accounts()
log(f"GitHub 账号 {login} 的 Token 已更新")
self._json({"ok": True, "msg": f"账号 {login} 已存在,Token 已更新并切换为当前账号"})
if next((a for a in accounts if a["gh_user"] == login), None):
self._json({"ok": False,
"msg": f"GitHub 账号 {login} 已存在,请用编辑功能(每行右边的✏️)更新 Token/备注"})
return
acc = {"id": uuid.uuid4().hex[:8], "label": label or login,
"gh_user": login, "token": token, "private": private,
@@ -1670,6 +1779,34 @@ class Handler(BaseHTTPRequestHandler):
log(f"新增 GitHub 账号 {login}({'私有' if private else '公开'}仓库)")
self._json({"ok": True, "msg": f"账号 {login} 验证通过,已添加"
+ ("并切换为当前账号" if active_id == acc["id"] else "")})
elif path == "/api/accounts/edit":
# 按 id 编辑已有账号:token 留空则保留,可改 label / private
aid = (data.get("id") or "").strip()
acc = find_account(aid)
if not acc:
self._json({"ok": False, "msg": "账号不存在"})
return
new_token = (data.get("token") or "").strip()
if new_token:
ok, login, err = gh_verify_token(new_token)
if not ok:
self._json({"ok": False, "msg": f"Token 验证失败: {err}"})
return
if login != acc["gh_user"]:
self._json({"ok": False,
"msg": f"Token 对应的账号是 {login},与当前账号 {acc['gh_user']} 不一致"})
return
acc["token"] = new_token
log(f"GitHub 账号 {acc['gh_user']} 的 Token 已通过编辑更新")
if "private" in data:
acc["private"] = bool(data.get("private"))
if "label" in data:
label = (data.get("label") or "").strip()
if label:
acc["label"] = label
save_accounts()
self._json({"ok": True,
"msg": f"账号 {acc['gh_user']} 已保存" + ("(Token 已更新)" if new_token else "")})
elif path == "/api/accounts/switch":
aid = (data.get("id") or "").strip()
if not find_account(aid):
+1 -1
View File
@@ -1,5 +1,5 @@
appname = GiteaHubSync
version = 1.4.1
version = 1.5.0
display_name = Gitea→GitHub 同步助手
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
platform = all