nacho commited on
Commit
3cb96d8
·
1 Parent(s): f249601

feat: account detail modal — click eye icon to see full info + screenshots per account

Browse files
.deepseek/pastes/paste-2026-05-18-124817-6d59a6f3.md ADDED
The diff for this file is too large to render. See raw diff
 
main.py CHANGED
@@ -443,6 +443,45 @@ async def login_account(request: Request, admin_key: str = Header(...)):
443
  return {"ok": True, "message": "Login task started"}
444
 
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  @app.post("/admin/verify")
447
  async def admin_verify(request: Request):
448
  """Verify admin key for panel login."""
 
443
  return {"ok": True, "message": "Login task started"}
444
 
445
 
446
+ @app.get("/admin/accounts/{email:path}")
447
+ async def get_account_detail(email: str, admin_key: str = Header(...)):
448
+ """Get detailed info for a single account, including related screenshots."""
449
+ if admin_key != config.server.admin_key:
450
+ raise HTTPException(status_code=401, detail="Invalid admin key")
451
+
452
+ if email not in manager.accounts:
453
+ raise HTTPException(status_code=404, detail="Account not found")
454
+
455
+ a = manager.accounts[email]
456
+ detail = {
457
+ "email": a.email, "name": a.name, "proxy": a.proxy,
458
+ "in_use": a.in_use, "logged_in": a.logged_in,
459
+ "is_muted": a.is_muted, "muted_until": a.muted_until,
460
+ "error_count": a.error_count, "last_error": a.last_error,
461
+ "last_used": a.last_used,
462
+ }
463
+
464
+ safe = email.replace("@", "_at_").replace("+", "_plus_")
465
+ screenshots = []
466
+ if SCREENSHOT_DIR.exists():
467
+ for f in sorted(SCREENSHOT_DIR.glob(f"*{safe}*.png"), key=lambda p: p.stat().st_mtime, reverse=True):
468
+ txt = f.with_suffix(".txt")
469
+ err = ""
470
+ if txt.exists():
471
+ try:
472
+ err = txt.read_text(encoding="utf-8").strip()
473
+ except Exception:
474
+ pass
475
+ screenshots.append({
476
+ "name": f.name, "url": f"/static/screenshots/{f.name}",
477
+ "size_kb": round(f.stat().st_size / 1024, 1),
478
+ "time": time.strftime("%m-%d %H:%M", time.localtime(f.stat().st_mtime)),
479
+ "error": err,
480
+ })
481
+ detail["screenshots"] = screenshots
482
+ return detail
483
+
484
+
485
  @app.post("/admin/verify")
486
  async def admin_verify(request: Request):
487
  """Verify admin key for panel login."""
static/index.html CHANGED
@@ -238,6 +238,22 @@ html.light-mode .log-viewer{background:rgba(0,0,0,.04);color:#475569}
238
 
239
  @media(max-width:639px){.hide-xs{display:none!important}}
240
  @media(max-width:1023px){.hide-md{display:none!important}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  </style>
242
  </head>
243
  <body>
@@ -397,6 +413,17 @@ html.light-mode .log-viewer{background:rgba(0,0,0,.04);color:#475569}
397
  </div>
398
  </div>
399
 
 
 
 
 
 
 
 
 
 
 
 
400
  </main>
401
  </div>
402
 
@@ -592,7 +619,7 @@ async function loadAccounts(){
592
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
593
  <td class="hide-xs">${a.is_muted?`<span class="badge badge-warn" title="${a.muted_until||''}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
594
  <td class="hide-xs">${a.error_count>0?`<span class="badge badge-off" title="${(a.last_error||'').replace(/"/g,'&quot;').replace(/'/g,"&#39;")}">${a.error_count}</span>`:'—'}</td>
595
- <td><button class="btn btn-sm" onclick="doLoginAccount('${a.email}')">${a.logged_in?'重连':'登录'}</button></td>
596
  </tr>`;
597
  }
598
  document.getElementById('tbl').innerHTML=r||'<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--text-muted)">暂无账号</td></tr>';
@@ -712,6 +739,43 @@ async function setLevel(lvl){
712
  }catch(e){toast(e.message,0)}
713
  }
714
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
715
  async function loadScreenshots(){
716
  try{
717
  const d=await api('/admin/screenshots',{headers:{'admin-key':getAdminKey()}});
 
238
 
239
  @media(max-width:639px){.hide-xs{display:none!important}}
240
  @media(max-width:1023px){.hide-md{display:none!important}}
241
+
242
+ /* detail modal */
243
+ .detail-overlay{position:fixed;inset:0;z-index:250;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .25s;padding:20px}
244
+ .detail-overlay.open{opacity:1;pointer-events:auto}
245
+ .detail-box{background:var(--surface-solid);border:1px solid var(--border);border-radius:var(--radius);max-width:640px;width:100%;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.5)}
246
+ .detail-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface-solid);z-index:1}
247
+ .detail-header h2{font-size:15px;font-weight:600}
248
+ .detail-body{padding:16px 18px}
249
+ .detail-body .kv{display:grid;grid-template-columns:120px 1fr;gap:6px 12px;font-size:12px;margin-bottom:10px}
250
+ .detail-body .kv .k{color:var(--text-dim);font-weight:500}
251
+ .detail-body .kv .v{color:var(--text);word-break:break-all}
252
+ .detail-body .kv .v.err{color:var(--red)}
253
+ .detail-body h3{font-size:13px;font-weight:600;margin:16px 0 8px;color:var(--text)}
254
+ .detail-body .ss-thumbs{display:flex;flex-wrap:wrap;gap:6px}
255
+ .detail-body .ss-thumbs a{display:inline-block;padding:4px 8px;background:rgba(0,0,0,.2);border:1px solid var(--border);border-radius:6px;color:var(--text);text-decoration:none;font-size:10px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
256
+ .detail-body .ss-thumbs a:hover{border-color:var(--accent)}
257
  </style>
258
  </head>
259
  <body>
 
413
  </div>
414
  </div>
415
 
416
+ <!-- Account Detail Modal -->
417
+ <div class="detail-overlay" id="detailOverlay" onclick="closeDetail(event)">
418
+ <div class="detail-box" onclick="event.stopPropagation()">
419
+ <div class="detail-header">
420
+ <h2 id="detailTitle">账号详情</h2>
421
+ <button class="btn btn-sm" onclick="closeDetail()">✕</button>
422
+ </div>
423
+ <div class="detail-body" id="detailBody">加载中…</div>
424
+ </div>
425
+ </div>
426
+
427
  </main>
428
  </div>
429
 
 
619
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
620
  <td class="hide-xs">${a.is_muted?`<span class="badge badge-warn" title="${a.muted_until||''}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
621
  <td class="hide-xs">${a.error_count>0?`<span class="badge badge-off" title="${(a.last_error||'').replace(/"/g,'&quot;').replace(/'/g,"&#39;")}">${a.error_count}</span>`:'—'}</td>
622
+ <td><button class="btn btn-sm" onclick="doLoginAccount('${a.email}')">${a.logged_in?'重连':'登录'}</button> <button class="btn btn-sm" onclick="showDetail('${a.email}')" title="详情">👁️</button></td>
623
  </tr>`;
624
  }
625
  document.getElementById('tbl').innerHTML=r||'<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--text-muted)">暂无账号</td></tr>';
 
739
  }catch(e){toast(e.message,0)}
740
  }
741
 
742
+ async function showDetail(email){
743
+ const overlay=document.getElementById('detailOverlay');
744
+ const body=document.getElementById('detailBody');
745
+ const title=document.getElementById('detailTitle');
746
+ overlay.classList.add('open');
747
+ title.textContent='加载中…';
748
+ body.innerHTML='<span style="color:var(--text-dim)">加载中…</span>';
749
+ try{
750
+ const d=await api('/admin/accounts/'+encodeURIComponent(email),{headers:{'admin-key':getAdminKey()}});
751
+ title.textContent=d.email;
752
+ const mutedTag=d.is_muted?`<span class="badge badge-warn">禁言至 ${d.muted_until||'?'}</span>`:'<span class="badge badge-idle">正常</span>';
753
+ body.innerHTML=`
754
+ <div class="kv">
755
+ <span class="k">邮箱</span><span class="v">${d.email}</span>
756
+ <span class="k">备注</span><span class="v">${d.name||'—'}</span>
757
+ <span class="k">代理</span><span class="v">${d.proxy||'—'}</span>
758
+ <span class="k">状态</span><span class="v"><span class="badge ${d.logged_in?'badge-on':'badge-off'}">${d.logged_in?'在线':'离线'}</span> <span class="badge ${d.in_use?'badge-on':'badge-idle'}">${d.in_use?'使用中':'空闲'}</span></span>
759
+ <span class="k">禁言</span><span class="v">${mutedTag}</span>
760
+ <span class="k">错误次数</span><span class="v">${d.error_count}</span>
761
+ <span class="k">最后错误</span><span class="v err">${(d.last_error||'—').replace(/</g,'&lt;')}</span>
762
+ <span class="k">最后使用</span><span class="v">${d.last_used?new Date(d.last_used*1000).toLocaleString():'—'}</span>
763
+ </div>
764
+ <h3>📸 相关截图 (${d.screenshots.length})</h3>
765
+ <div class="ss-thumbs">${d.screenshots.length?d.screenshots.map(s=>`<a href="${s.url}" target="_blank" title="${s.error||s.name}">🖼️ ${s.name.substring(0,30)} · ${s.time}</a>`).join(''):'<span style="color:var(--text-muted);font-size:11px">暂无</span>'}</div>`;
766
+ }catch(e){
767
+ body.innerHTML=`<span style="color:var(--red)">加载失败: ${e.message}</span>`;
768
+ }
769
+ }
770
+
771
+ function closeDetail(e){
772
+ if(e&&e.target!==document.getElementById('detailOverlay'))return;
773
+ document.getElementById('detailOverlay').classList.remove('open');
774
+ }
775
+
776
+ // ESC to close
777
+ document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDetail()});
778
+
779
  async function loadScreenshots(){
780
  try{
781
  const d=await api('/admin/screenshots',{headers:{'admin-key':getAdminKey()}});