1335 lines
49 KiB
HTML
1335 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>微信登录管理面板</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<style>
|
||
:root {
|
||
color-scheme: light dark;
|
||
--bg: #0f172a;
|
||
--card: #020617;
|
||
--border: #1e293b;
|
||
--accent: #22c55e;
|
||
--accent-soft: rgba(34, 197, 94, 0.12);
|
||
--text: #e5e7eb;
|
||
--muted: #9ca3af;
|
||
--danger: #ef4444;
|
||
--radius: 16px;
|
||
--shadow: 0 18px 45px rgba(15, 23, 42, 0.7);
|
||
}
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: radial-gradient(circle at top, #1e293b 0, #020617 55%, #000 100%);
|
||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'SF Pro Text',
|
||
'Segoe UI', sans-serif;
|
||
color: var(--text);
|
||
}
|
||
.shell {
|
||
width: 100%;
|
||
max-width: 1080px;
|
||
margin: 32px;
|
||
border-radius: 28px;
|
||
background: radial-gradient(circle at top left, #22c55e1f 0, #020617 48%, #000 100%);
|
||
padding: 1px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.shell-inner {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
|
||
gap: 24px;
|
||
padding: 26px 26px 26px 26px;
|
||
border-radius: 27px;
|
||
background:
|
||
radial-gradient(circle at top left, rgba(52, 211, 153, 0.09), transparent 55%),
|
||
radial-gradient(circle at bottom, rgba(59, 130, 246, 0.09), transparent 55%),
|
||
#020617f2;
|
||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||
backdrop-filter: blur(26px);
|
||
}
|
||
@media (max-width: 880px) {
|
||
.shell-inner {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
padding: 20px;
|
||
}
|
||
}
|
||
header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 18px;
|
||
}
|
||
.title {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.title-main {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.pill {
|
||
font-size: 11px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
background: rgba(15, 23, 42, 0.9);
|
||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||
color: var(--muted);
|
||
}
|
||
.title-sub {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.status-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 999px;
|
||
background: #22c55e;
|
||
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.25);
|
||
}
|
||
.status-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.status-row strong {
|
||
color: var(--text);
|
||
font-weight: 500;
|
||
}
|
||
.card {
|
||
border-radius: var(--radius);
|
||
border: 1px solid var(--border);
|
||
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9), #020617);
|
||
padding: 18px 18px 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
.card-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
}
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 12px 14px;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.form-grid {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
}
|
||
}
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.field label {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.field label span {
|
||
color: var(--danger);
|
||
margin-left: 2px;
|
||
}
|
||
input,
|
||
select {
|
||
font: inherit;
|
||
background: rgba(15, 23, 42, 0.95);
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(51, 65, 85, 0.9);
|
||
padding: 8px 11px;
|
||
color: var(--text);
|
||
outline: none;
|
||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease,
|
||
transform 0.08s ease;
|
||
}
|
||
input::placeholder {
|
||
color: rgba(148, 163, 184, 0.8);
|
||
}
|
||
input:focus,
|
||
select:focus {
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.55);
|
||
background: rgba(15, 23, 42, 0.99);
|
||
transform: translateY(-0.5px);
|
||
}
|
||
.checkbox-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.checkbox-row input {
|
||
width: 15px;
|
||
height: 15px;
|
||
border-radius: 4px;
|
||
padding: 0;
|
||
}
|
||
.actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
}
|
||
button {
|
||
border: none;
|
||
border-radius: 999px;
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.03em;
|
||
text-transform: uppercase;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: transform 0.08s ease, box-shadow 0.12s ease, background 0.12s ease,
|
||
opacity 0.12s ease;
|
||
}
|
||
button.primary {
|
||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||
color: #022c22;
|
||
box-shadow: 0 10px 24px rgba(34, 197, 94, 0.35);
|
||
}
|
||
button.secondary {
|
||
background: rgba(15, 23, 42, 0.98);
|
||
color: var(--text);
|
||
border: 1px solid rgba(148, 163, 184, 0.4);
|
||
}
|
||
button.danger {
|
||
background: rgba(127, 29, 29, 0.96);
|
||
color: #fee2e2;
|
||
border: 1px solid rgba(248, 113, 113, 0.7);
|
||
}
|
||
button:disabled {
|
||
opacity: 0.6;
|
||
cursor: wait;
|
||
}
|
||
button:not(:disabled):hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.54);
|
||
}
|
||
button:not(:disabled):active {
|
||
transform: translateY(0px) scale(0.99);
|
||
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.55);
|
||
}
|
||
.qr-card {
|
||
border-radius: var(--radius);
|
||
border: 1px solid rgba(30, 64, 175, 0.8);
|
||
background:
|
||
radial-gradient(circle at top, rgba(59, 130, 246, 0.35), transparent 60%),
|
||
rgba(15, 23, 42, 0.98);
|
||
padding: 18px 16px 15px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.qr-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 13px;
|
||
}
|
||
.badge {
|
||
font-size: 11px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
background: rgba(15, 23, 42, 0.98);
|
||
border: 1px solid rgba(148, 163, 184, 0.6);
|
||
color: var(--muted);
|
||
}
|
||
.qr-box {
|
||
margin-top: 4px;
|
||
border-radius: 18px;
|
||
border: 1px dashed rgba(148, 163, 184, 0.7);
|
||
background:
|
||
radial-gradient(circle at top, rgba(148, 163, 184, 0.23), transparent 55%),
|
||
radial-gradient(circle at bottom, rgba(15, 23, 42, 0.95), #020617);
|
||
padding: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
min-height: 220px;
|
||
}
|
||
.qr-img {
|
||
width: 190px;
|
||
height: 190px;
|
||
border-radius: 18px;
|
||
background: #ffffff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
padding: 16px; /* 给二维码留出白边(quiet zone) */
|
||
box-sizing: border-box;
|
||
box-shadow:
|
||
0 0 0 1px rgba(15, 23, 42, 0.9),
|
||
0 15px 32px rgba(15, 23, 42, 0.9);
|
||
}
|
||
.qr-img img,
|
||
.qr-img canvas {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
display: block;
|
||
}
|
||
.qr-placeholder {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
}
|
||
.qr-expired {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
padding: 12px;
|
||
background: rgba(15, 23, 42, 0.95);
|
||
border-radius: 12px;
|
||
border: 1px dashed rgba(148, 163, 184, 0.5);
|
||
}
|
||
.hint {
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
}
|
||
.hint strong {
|
||
color: var(--text);
|
||
font-weight: 500;
|
||
}
|
||
.log-card {
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||
background:
|
||
radial-gradient(circle at top left, rgba(34, 197, 94, 0.16), transparent 55%),
|
||
rgba(15, 23, 42, 0.98);
|
||
padding: 14px 13px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-height: 120px;
|
||
}
|
||
.log-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 12px;
|
||
}
|
||
.log-body {
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
max-height: 120px;
|
||
overflow: auto;
|
||
padding-right: 4px;
|
||
}
|
||
.log-line {
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
overflow: hidden;
|
||
}
|
||
.log-line.ok {
|
||
color: #4ade80;
|
||
}
|
||
.log-line.warn {
|
||
color: #facc15;
|
||
}
|
||
.log-line.error {
|
||
color: #f97373;
|
||
}
|
||
.tag-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
}
|
||
.tag {
|
||
padding: 1px 7px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||
background: rgba(15, 23, 42, 0.92);
|
||
}
|
||
.small-label {
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
}
|
||
.login-state {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
.login-state.ok {
|
||
color: #4ade80;
|
||
}
|
||
.login-state.pending {
|
||
color: #facc15;
|
||
}
|
||
.login-state.offline {
|
||
color: #f97373;
|
||
}
|
||
.login-state.unknown {
|
||
color: var(--muted);
|
||
}
|
||
.result-card {
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||
background: rgba(15, 23, 42, 0.98);
|
||
padding: 12px 12px 10px;
|
||
margin-top: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.result-body {
|
||
max-height: 120px;
|
||
overflow: auto;
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
background: rgba(15, 23, 42, 0.9);
|
||
border-radius: 10px;
|
||
padding: 6px 8px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
.ticket-card {
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||
background: rgba(15, 23, 42, 0.98);
|
||
padding: 10px 10px 8px;
|
||
margin-top: 12px;
|
||
display: none;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.ticket-body {
|
||
max-height: 80px;
|
||
overflow: auto;
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
background: rgba(15, 23, 42, 0.9);
|
||
border-radius: 10px;
|
||
padding: 6px 8px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
.data62-card {
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(55, 65, 81, 0.9);
|
||
background: rgba(15, 23, 42, 0.98);
|
||
padding: 10px 10px 8px;
|
||
margin-top: 12px;
|
||
display: none;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.data62-body {
|
||
max-height: 120px;
|
||
overflow: auto;
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
background: rgba(15, 23, 42, 0.9);
|
||
border-radius: 10px;
|
||
padding: 6px 8px;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
.result-actions {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.result-actions button {
|
||
padding: 4px 10px;
|
||
font-size: 11px;
|
||
}
|
||
.slider-card {
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(59, 130, 246, 0.8);
|
||
background:
|
||
radial-gradient(circle at top, rgba(59, 130, 246, 0.25), transparent 60%),
|
||
rgba(15, 23, 42, 0.98);
|
||
padding: 10px 10px 8px;
|
||
margin-top: 12px;
|
||
display: none;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.slider-frame {
|
||
width: 100%;
|
||
height: 260px;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(30, 64, 175, 0.8);
|
||
background: #020617;
|
||
}
|
||
.page-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
}
|
||
.home-banner {
|
||
width: 100%;
|
||
text-align: center;
|
||
padding: 20px 24px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: rgba(2, 6, 23, 0.6);
|
||
}
|
||
.home-banner .banner-text {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.06em;
|
||
color: var(--text);
|
||
margin: 0;
|
||
text-shadow: 0 0 20px rgba(34, 197, 94, 0.2);
|
||
}
|
||
.home-banner .banner-text span { color: var(--accent); }
|
||
</style>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="page-wrap">
|
||
<div class="home-banner">
|
||
<h1 class="banner-text">Wechat <span>智能托管服务</span></h1>
|
||
<p style="margin:8px 0 0;font-size:14px"><a href="swagger.html" target="_blank" style="color:var(--accent)">API 文档 (Swagger)</a></p>
|
||
</div>
|
||
<div class="shell">
|
||
<div class="shell-inner">
|
||
<section>
|
||
<header>
|
||
<div class="title">
|
||
<div class="title-main">
|
||
微信登录控制台
|
||
<span class="pill">Admin · iPad / Mac 直登</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="status-row">
|
||
<span class="status-dot" id="health-dot"></span>
|
||
<span>后端状态:<strong id="health-text">检测中...</strong></span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="card">
|
||
<div class="card-title">登录参数</div>
|
||
<div class="form-grid">
|
||
<div class="field">
|
||
<label for="key">
|
||
账号唯一标识 <span>*</span>
|
||
</label>
|
||
<input
|
||
id="key"
|
||
placeholder="例如:your-account-key"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
<div class="field">
|
||
<label for="proxy">代理(可选,仅支持带用户名密码的 socks5h)</label>
|
||
<input
|
||
id="proxy"
|
||
placeholder="socks5h://user:pass@ip:port"
|
||
autocomplete="off"
|
||
/>
|
||
<button type="button" class="secondary" id="btn-check-proxy" style="margin-top: 6px; padding: 4px 10px; font-size: 12px;">检测代理是否正常</button>
|
||
</div>
|
||
<div class="field">
|
||
<label for="device">登录设备类型</label>
|
||
<select id="device">
|
||
<option value="">自动</option>
|
||
<option value="ipad">iPad</option>
|
||
<option value="mac">Mac</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="verify-code">手机验证码(收到后填写)</label>
|
||
<input
|
||
id="verify-code"
|
||
placeholder="例如:123456"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<button class="primary" id="btn-qrcode">
|
||
获取登录二维码
|
||
</button>
|
||
<button class="secondary" id="btn-qrcode-mac" title="使用 Mac 取码(与上方选择 Mac 后点获取二维码等效)">
|
||
重新取码(Mac)
|
||
</button>
|
||
<button class="secondary" id="btn-wake">
|
||
唤醒
|
||
</button>
|
||
<button class="secondary" id="btn-check-scan">
|
||
检测扫码状态
|
||
</button>
|
||
<button class="secondary" id="btn-online">
|
||
获取在线状态
|
||
</button>
|
||
<button class="secondary" id="btn-verify-code">
|
||
验证验证码
|
||
</button>
|
||
<button class="danger" id="btn-logout">
|
||
退出当前账号
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="log-card" style="margin-top: 12px">
|
||
<div class="log-header">
|
||
<span>操作日志</span>
|
||
<button class="secondary" id="btn-clear-log" style="padding: 3px 10px; font-size: 11px">
|
||
清空
|
||
</button>
|
||
</div>
|
||
<div class="log-body" id="log-body"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<aside>
|
||
<div id="qr-area">
|
||
<div class="qr-card">
|
||
<div class="qr-header">
|
||
<span>扫码登录二维码</span>
|
||
<span class="badge" id="login-state-badge">未创建</span>
|
||
</div>
|
||
<div class="qr-box">
|
||
<div class="qr-img" id="qr-img-box">
|
||
<div class="qr-placeholder" id="qr-placeholder">
|
||
点击左侧「获取登录二维码」后,将在这里显示二维码图片或登录链接信息。
|
||
</div>
|
||
</div>
|
||
<div class="hint" id="qr-hint">
|
||
请使用微信客户端扫描二维码完成登录。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="height: 12px"></div>
|
||
<div class="card">
|
||
<div class="small-label">当前账号在线状态</div>
|
||
<div class="login-state unknown" id="login-state-text">未知</div>
|
||
<div class="small-label" id="login-extra">尚未查询</div>
|
||
</div>
|
||
<div id="slider-reopen-wrap" style="display: none; margin-top: 12px;">
|
||
<button type="button" class="secondary" id="btn-show-slider">打开滑块验证</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="slider-area" style="display: none;">
|
||
<div class="card" style="max-width: 480px;">
|
||
<div class="card-title">滑块验证(无数字时:先点「重新取码(Mac)」,手机停确认页再滑)</div>
|
||
<div id="slider-app" data-v-app="">
|
||
<div class="params-section" style="margin-bottom: 12px;">
|
||
<label class="form-label" for="keyInput">Key (7765 服务方):</label>
|
||
<input type="text" class="form-control" id="keyInput" placeholder="408449830" style="width:100%;box-sizing:border-box;padding:8px;margin-bottom:8px;border:1px solid var(--border);border-radius:8px;background:rgba(15,23,42,0.6);color:var(--text);">
|
||
<label class="form-label" for="data62Input">Data62:</label>
|
||
<input type="text" class="form-control" id="data62Input" placeholder="请输入data62" style="width:100%;box-sizing:border-box;padding:8px;margin-bottom:8px;border:1px solid var(--border);border-radius:8px;background:rgba(15,23,42,0.6);color:var(--text);">
|
||
<label class="form-label" for="originalTicketInput">Original Ticket:</label>
|
||
<input type="text" class="form-control" id="originalTicketInput" placeholder="请输入original_ticket" style="width:100%;box-sizing:border-box;padding:8px;margin-bottom:12px;border:1px solid var(--border);border-radius:8px;background:rgba(15,23,42,0.6);color:var(--text);">
|
||
</div>
|
||
<div class="text-center">
|
||
<button type="button" class="btn btn-verify btn-lg" id="slider-btn-verify" disabled style="padding:10px 20px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;">开始验证</button>
|
||
<div class="text-muted mt-2" style="font-size:12px;color:var(--muted);margin-top:8px;">请先填写完整的参数信息</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
// 相对路径由 Node 代理到后端;若从 file:// 打开则请求会失败,改用同源或默认地址
|
||
const API_BASE = (typeof location !== 'undefined' && location.origin && (location.protocol === 'http:' || location.protocol === 'https:'))
|
||
? '' : (typeof location !== 'undefined' ? 'http://127.0.0.1:3000' : '');
|
||
|
||
const state = {
|
||
pollingScan: null,
|
||
pollingOnline: null,
|
||
sliderOpened: false,
|
||
sliderParams: null,
|
||
sliderScriptLoaded: false,
|
||
sliderListenersBound: false,
|
||
qrcodeFetchedAt: 0,
|
||
};
|
||
|
||
function parseSliderUrlParams(sliderUrl) {
|
||
if (!sliderUrl || typeof sliderUrl !== 'string') return null;
|
||
const q = sliderUrl.indexOf('?');
|
||
if (q === -1) return null;
|
||
const params = {};
|
||
sliderUrl.slice(q + 1).split('&').forEach(function(pair) {
|
||
const i = pair.indexOf('=');
|
||
if (i > 0) params[decodeURIComponent(pair.slice(0, i).replace(/\+/g, ' '))] = decodeURIComponent((pair.slice(i + 1) || '').replace(/\+/g, ' '));
|
||
});
|
||
return params;
|
||
}
|
||
|
||
function showSliderAreaAndFill(params) {
|
||
var qrArea = $('qr-area');
|
||
var sliderArea = $('slider-area');
|
||
if (!qrArea || !sliderArea) return;
|
||
qrArea.style.display = 'none';
|
||
sliderArea.style.display = 'block';
|
||
var keyInput = $('keyInput');
|
||
var data62Input = $('data62Input');
|
||
var ticketInput = $('originalTicketInput');
|
||
var btnVerify = $('slider-btn-verify');
|
||
// 仅当有有效参数时填充,避免用 {} 清空已有数据
|
||
var keyVal = params && (params.key || params.Key);
|
||
var data62Val = params && params.data62;
|
||
var ticketVal = params && (params.ticket || params.original_ticket);
|
||
if (keyVal !== undefined && keyVal !== '') if (keyInput) keyInput.value = keyVal;
|
||
if (data62Val !== undefined && data62Val !== '') if (data62Input) data62Input.value = data62Val;
|
||
if (ticketVal !== undefined && ticketVal !== '') if (ticketInput) ticketInput.value = ticketVal;
|
||
function toggleVerifyBtn() {
|
||
if (!btnVerify) return;
|
||
var hasAll = keyInput && data62Input && ticketInput &&
|
||
(keyInput.value || '').trim() && (data62Input.value || '').trim() && (ticketInput.value || '').trim();
|
||
btnVerify.disabled = !hasAll;
|
||
}
|
||
if (!state.sliderListenersBound) {
|
||
state.sliderListenersBound = true;
|
||
if (keyInput) keyInput.addEventListener('input', toggleVerifyBtn);
|
||
if (data62Input) data62Input.addEventListener('input', toggleVerifyBtn);
|
||
if (ticketInput) ticketInput.addEventListener('input', toggleVerifyBtn);
|
||
}
|
||
toggleVerifyBtn();
|
||
if (!state.sliderScriptLoaded) {
|
||
state.sliderScriptLoaded = true;
|
||
var script = document.createElement('script');
|
||
script.type = 'module';
|
||
script.crossOrigin = 'anonymous';
|
||
script.src = (API_BASE || '') + '/auth/slider-assets/N_jYM_2V.js';
|
||
document.body.appendChild(script);
|
||
}
|
||
}
|
||
|
||
function showQrArea() {
|
||
var qrArea = $('qr-area');
|
||
var sliderArea = $('slider-area');
|
||
var reopenWrap = $('slider-reopen-wrap');
|
||
if (qrArea) qrArea.style.display = 'block';
|
||
if (sliderArea) sliderArea.style.display = 'none';
|
||
if (reopenWrap) reopenWrap.style.display = state.sliderParams ? 'block' : 'none';
|
||
}
|
||
|
||
function log(message, level = 'info') {
|
||
const logBody = $('log-body');
|
||
const line = document.createElement('div');
|
||
line.className = 'log-line ' + (level === 'error' ? 'error' : level === 'warn' ? 'warn' : '');
|
||
const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||
line.textContent = `[${ts}] ${message}`;
|
||
logBody.appendChild(line);
|
||
logBody.scrollTop = logBody.scrollHeight;
|
||
}
|
||
|
||
async function checkHealth() {
|
||
try {
|
||
// 通过 /api/ws-status 检测后端是否可用(同时避免某些部署环境未透出 /health)
|
||
const res = await fetch('/api/ws-status', { cache: 'no-store' });
|
||
if (!res.ok) throw new Error('status ' + res.status);
|
||
const data = await res.json().catch(() => ({}));
|
||
$('health-dot').style.background = '#22c55e';
|
||
$('health-text').textContent = '在线';
|
||
log('后端健康检查成功: ' + JSON.stringify(data || {}));
|
||
} catch (e) {
|
||
$('health-dot').style.background = '#ef4444';
|
||
$('health-text').textContent = '不可用';
|
||
log('后端健康检查失败: ' + (e && e.message ? e.message : e), 'error');
|
||
}
|
||
}
|
||
|
||
function setLoading(loading) {
|
||
$('btn-qrcode').disabled = loading;
|
||
var btnQrcodeMac = $('btn-qrcode-mac');
|
||
if (btnQrcodeMac) btnQrcodeMac.disabled = loading;
|
||
var btnWake = $('btn-wake');
|
||
if (btnWake) btnWake.disabled = loading;
|
||
$('btn-check-scan').disabled = loading;
|
||
$('btn-online').disabled = loading;
|
||
var btnVerifyCode = $('btn-verify-code');
|
||
if (btnVerifyCode) btnVerifyCode.disabled = loading;
|
||
$('btn-logout').disabled = loading;
|
||
}
|
||
|
||
function getCommonPayload() {
|
||
const key = $('key').value.trim();
|
||
const proxy = $('proxy').value.trim();
|
||
const device = ($('device') && $('device').value) || '';
|
||
if (!key) {
|
||
alert('请先填写账号唯一标识(key)');
|
||
return null;
|
||
}
|
||
return {
|
||
key,
|
||
proxy: proxy || undefined,
|
||
ipadOrMac: device || 'ipad',
|
||
};
|
||
}
|
||
|
||
function updateLoginState(text, status = 'unknown', extra = '') {
|
||
const el = $('login-state-text');
|
||
el.textContent = text;
|
||
el.className = 'login-state ' + status;
|
||
|
||
$('login-extra').textContent = extra || '';
|
||
|
||
const badge = $('login-state-badge');
|
||
if (status === 'ok') {
|
||
badge.textContent = '已登录 / 在线';
|
||
} else if (status === 'pending') {
|
||
badge.textContent = '等待扫码 / 确认中';
|
||
} else if (status === 'offline') {
|
||
badge.textContent = '已离线 / 未登录';
|
||
} else {
|
||
badge.textContent = '未知';
|
||
}
|
||
}
|
||
|
||
function extractQrTextFromObject(obj) {
|
||
let result = null;
|
||
const base64Like = /^[A-Za-z0-9+/=]+$/;
|
||
|
||
function walk(value, keyHint) {
|
||
if (result || value == null) return;
|
||
|
||
if (typeof value === 'string') {
|
||
// 1) 优先从 URL / 文本中找 data= 后面的内容
|
||
const idx = value.indexOf('data=');
|
||
if (idx !== -1) {
|
||
let s = value.slice(idx + 5);
|
||
const stop = s.search(/[&\s"]/);
|
||
if (stop > 0) s = s.slice(0, stop);
|
||
if (s && s.length >= 16 && base64Like.test(s)) {
|
||
result = s;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 2) 兜底:如果键名看起来与二维码相关,并且值是长 base64,则直接当作二维码数据
|
||
if (
|
||
keyHint &&
|
||
/qr|code|data/i.test(keyHint) &&
|
||
!/62/i.test(keyHint) &&
|
||
value.length >= 32 &&
|
||
base64Like.test(value)
|
||
) {
|
||
result = value;
|
||
return;
|
||
}
|
||
} else if (typeof value === 'object') {
|
||
if (Array.isArray(value)) {
|
||
value.forEach((v) => walk(v, keyHint));
|
||
} else {
|
||
Object.entries(value).forEach(([k, v]) => walk(v, k));
|
||
}
|
||
}
|
||
}
|
||
|
||
walk(obj, '');
|
||
return result;
|
||
}
|
||
|
||
// 从 QrCodeUrl 中解析出微信登录链接(url= 参数),扫码后才能唤起微信登录
|
||
function getWechatLoginUrlFromResponse(data) {
|
||
if (!data || typeof data !== 'object') return null;
|
||
const qrCodeUrl = data.Data?.QrCodeUrl || data.QrCodeUrl || data.qrCodeUrl || data.qrUrl || data.url;
|
||
if (!qrCodeUrl || typeof qrCodeUrl !== 'string') return null;
|
||
const idx = qrCodeUrl.indexOf('url=');
|
||
if (idx === -1) {
|
||
if (/^https?:\/\/weixin\.qq\.com\//i.test(qrCodeUrl)) return qrCodeUrl;
|
||
return qrCodeUrl;
|
||
}
|
||
let url = qrCodeUrl.slice(idx + 4);
|
||
const end = url.search(/[&\s]/);
|
||
if (end > 0) url = url.slice(0, end);
|
||
try {
|
||
url = decodeURIComponent(url);
|
||
} catch (_) {}
|
||
return url || null;
|
||
}
|
||
|
||
function _unused_extractCleanTicketFromResponse(data) {
|
||
if (!data || typeof data !== 'object') return null;
|
||
const obj = data;
|
||
const d = obj.Data && typeof obj.Data === 'object' ? obj.Data : obj;
|
||
let raw =
|
||
d.ticket ||
|
||
obj.ticket ||
|
||
obj.Ticket ||
|
||
(typeof obj.wechat_verify_url === 'string' &&
|
||
obj.wechat_verify_url.includes('ticket=')
|
||
? obj.wechat_verify_url.slice(obj.wechat_verify_url.indexOf('ticket=') + 7)
|
||
: '');
|
||
if (!raw || typeof raw !== 'string') return null;
|
||
|
||
// 如果来自 wechat_verify_url,去掉后续参数
|
||
const amp = raw.indexOf('&');
|
||
if (amp > 0) raw = raw.slice(0, amp);
|
||
|
||
let clean = '';
|
||
for (let i = 0; i < raw.length; i++) {
|
||
const ch = raw[i];
|
||
const code = ch.charCodeAt(0);
|
||
// '<27>' 或非可见 ASCII 视为乱码起点
|
||
if (ch === '<27>' || code < 32 || code > 126) break;
|
||
clean += ch;
|
||
}
|
||
if (!clean) return null;
|
||
return clean;
|
||
}
|
||
|
||
function renderQrFromResponse(data) {
|
||
const box = $('qr-img-box');
|
||
box.innerHTML = '';
|
||
|
||
let qrText = null;
|
||
|
||
if (data && typeof data === 'object') {
|
||
// 1) 优先用「微信登录链接」绘制二维码,扫码后才会唤起登录;Data62 是二进制数据,不能当二维码内容
|
||
qrText = getWechatLoginUrlFromResponse(data);
|
||
if (!qrText && (data.qrUrl || data.qrcodeUrl || data.qr || data.url)) {
|
||
qrText = data.qrUrl || data.qrcodeUrl || data.qr || data.url;
|
||
}
|
||
if (!qrText && (data.qrcode || data.qr_code)) {
|
||
qrText = data.qrcode || data.qr_code;
|
||
}
|
||
if (!qrText) {
|
||
qrText = extractQrTextFromObject(data);
|
||
}
|
||
if (!qrText) {
|
||
const json = JSON.stringify(data, null, 2);
|
||
const pre = document.createElement('pre');
|
||
pre.textContent = json;
|
||
pre.style.fontSize = '10px';
|
||
pre.style.padding = '8px';
|
||
pre.style.whiteSpace = 'pre-wrap';
|
||
pre.style.wordBreak = 'break-all';
|
||
box.appendChild(pre);
|
||
$('qr-hint').textContent =
|
||
'当前接口返回中未包含可识别的二维码字符串,请检查上游服务是否已按文档返回 data= 或专门的二维码字段。';
|
||
log('二维码接口返回(未能自动识别二维码字段): ' + json, 'warn');
|
||
return;
|
||
}
|
||
|
||
} else if (typeof data === 'string') {
|
||
qrText = data;
|
||
}
|
||
|
||
log('用于生成二维码的 data= 内容: ' + qrText, 'ok');
|
||
|
||
const qrContainer = document.createElement('div');
|
||
qrContainer.id = 'qr-code';
|
||
box.appendChild(qrContainer);
|
||
|
||
try {
|
||
// 使用前端 QRCode 库将登录链接渲染为二维码(每次获取二维码后均根据后端返回数据刷新)
|
||
// @ts-ignore
|
||
new QRCode(qrContainer, {
|
||
text: qrText,
|
||
width: 190,
|
||
height: 190,
|
||
});
|
||
$('qr-hint').textContent = '请使用微信客户端扫描上方二维码完成登录。';
|
||
} catch (e) {
|
||
const fallback = document.createElement('pre');
|
||
fallback.textContent = qrText || '';
|
||
fallback.style.fontSize = '10px';
|
||
fallback.style.padding = '8px';
|
||
fallback.style.whiteSpace = 'pre-wrap';
|
||
fallback.style.wordBreak = 'break-all';
|
||
box.appendChild(fallback);
|
||
$('qr-hint').textContent = '二维码生成失败,请复制链接手动生成二维码。';
|
||
log('二维码渲染失败: ' + (e && e.message ? e.message : e), 'error');
|
||
}
|
||
}
|
||
|
||
// 根据扫码状态:扫完或过期时刷新二维码区域为「已过期」提示
|
||
function showQrExpired(message) {
|
||
const box = $('qr-img-box');
|
||
if (!box) return;
|
||
box.innerHTML = '';
|
||
const placeholder = document.createElement('div');
|
||
placeholder.className = 'qr-expired';
|
||
placeholder.textContent = message || '二维码已过期,请重新获取';
|
||
box.appendChild(placeholder);
|
||
const hint = $('qr-hint');
|
||
if (hint) hint.textContent = message || '二维码已过期,请点击「获取登录二维码」重新取码。';
|
||
}
|
||
|
||
async function callApi(path, options = {}) {
|
||
const url = (API_BASE || '') + path;
|
||
let res;
|
||
try {
|
||
res = await fetch(url, {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(options.headers || {}),
|
||
},
|
||
});
|
||
} catch (e) {
|
||
const msg = (e && e.message) ? String(e.message) : String(e);
|
||
const isRefused = /failed to fetch|networkerror|connection refused|load failed/i.test(msg);
|
||
log(isRefused ? '无法连接服务(ERR_CONNECTION_REFUSED):请通过前端地址访问(如 http://localhost:3000)并确保已启动 run-dev.sh 或 Docker。' : '请求失败: ' + msg, 'error');
|
||
throw new Error(isRefused ? '无法连接服务,请确认已通过 http://localhost:3000 打开页面且前后端已启动。' : msg);
|
||
}
|
||
let body = null;
|
||
try {
|
||
body = await res.json();
|
||
} catch (_) {
|
||
// ignore
|
||
}
|
||
if (!res.ok) {
|
||
const msg = (body && (body.error || body.message)) || res.statusText || '请求失败';
|
||
throw new Error(msg);
|
||
}
|
||
return body;
|
||
}
|
||
|
||
async function onWake() {
|
||
const payload = getCommonPayload();
|
||
if (!payload) return;
|
||
setLoading(true);
|
||
try {
|
||
log('唤醒登录(仅扫码登录)...');
|
||
const body = {
|
||
key: payload.key,
|
||
Check: !!payload.check,
|
||
IpadOrmac: payload.ipadOrMac || 'ipad',
|
||
Proxy: payload.proxy || '',
|
||
};
|
||
const data = await callApi('/auth/wake', {
|
||
method: 'POST',
|
||
body: JSON.stringify(body),
|
||
});
|
||
log('唤醒请求已发送');
|
||
if (data && typeof data === 'object' && (data.error || data.detail)) {
|
||
log(JSON.stringify(data.error || data.detail), 'warn');
|
||
}
|
||
updateLoginState('已发送唤醒', 'pending', '如需扫码请点击「获取登录二维码」');
|
||
} catch (e) {
|
||
log('唤醒失败: ' + (e.message || e), 'error');
|
||
updateLoginState('唤醒失败', 'offline', e.message || '');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onCheckProxy() {
|
||
var proxyInput = ($('proxy') && $('proxy').value || '').trim();
|
||
var url = '/api/check-proxy';
|
||
if (proxyInput) url += '?proxy=' + encodeURIComponent(proxyInput);
|
||
log('正在检测代理…' + (proxyInput ? '(使用当前填写的代理)' : '(使用环境变量或 KDL)'));
|
||
var btn = $('btn-check-proxy');
|
||
if (btn) btn.disabled = true;
|
||
try {
|
||
var data = await callApi(url);
|
||
if (data && data.ok) {
|
||
log('代理正常。来源: ' + (data.source || '') + (data.proxy_preview ? ',代理: ' + data.proxy_preview : ''), 'ok');
|
||
} else {
|
||
log('代理不可用: ' + (data && data.error ? data.error : JSON.stringify(data)), 'error');
|
||
}
|
||
} catch (e) {
|
||
log('检测代理失败: ' + (e.message || e), 'error');
|
||
} finally {
|
||
if (btn) btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function onGetQrCode(forceMac) {
|
||
const payload = getCommonPayload();
|
||
if (!payload) return;
|
||
setLoading(true);
|
||
try {
|
||
var deviceLabel = forceMac ? 'Mac' : (payload.ipadOrMac || 'ipad');
|
||
log(forceMac ? '重新取码(Mac)…' : '请求登录二维码(' + deviceLabel + ')...');
|
||
const body = {
|
||
Proxy: payload.proxy || '',
|
||
IpadOrmac: forceMac ? 'mac' : (payload.ipadOrMac || 'ipad'),
|
||
Check: false,
|
||
force_mac: !!forceMac,
|
||
};
|
||
const data = await callApi('/auth/qrcode', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ key: payload.key, ...body }),
|
||
});
|
||
log('获取二维码成功');
|
||
(function() {
|
||
var valid = data._data62_valid;
|
||
var msg = data._data62_check || '未知';
|
||
var rawLen = data._data62_raw_length;
|
||
var cleanLen = data._data62_clean_length;
|
||
var preview = data._data62_preview || '';
|
||
var line1 = 'Data62检查: ' + (valid ? '完整有效' : '不完整/无效') + (msg && msg !== '完整有效' ? '(' + msg + ')' : '');
|
||
var line2 = '原始长度: ' + (rawLen !== undefined ? rawLen : '—') + ', 清理后长度: ' + (cleanLen !== undefined ? cleanLen : '—');
|
||
var line3 = preview ? ('预览: ' + preview) : '';
|
||
log(line1);
|
||
log(line2);
|
||
if (line3) log(line3);
|
||
})();
|
||
state.qrcodeFetchedAt = Date.now();
|
||
renderQrFromResponse(data);
|
||
updateLoginState(forceMac ? '已用 Mac 重新取码,请手机停在确认页并完成下方滑块' : '等待扫码 / 确认中', 'pending', forceMac ? 'Data62 已更新,需滑块时请点「打开滑块验证」' : '请在 60 秒内使用微信扫码。');
|
||
if (forceMac && data) {
|
||
var newData62 = (data.Data && data.Data.Data62) || data.Data62 || data.data62 || '';
|
||
if (newData62) {
|
||
if (!state.sliderParams) state.sliderParams = {};
|
||
state.sliderParams.data62 = newData62;
|
||
state.sliderParams.key = state.sliderParams.key || '408449830';
|
||
state.sliderOpened = true;
|
||
var reopenWrap = $('slider-reopen-wrap');
|
||
if (reopenWrap) reopenWrap.style.display = 'block';
|
||
log('Mac 取码成功,Data62 已更新。上方显示二维码/返回内容;需滑块时请点击「打开滑块验证」。', 'warn');
|
||
}
|
||
}
|
||
if (state.pollingScan) clearInterval(state.pollingScan);
|
||
setTimeout(() => onCheckScanStatus(true), 2000);
|
||
state.pollingScan = setInterval(() => { onCheckScanStatus(true); }, 5000);
|
||
} catch (e) {
|
||
log('获取二维码失败: ' + (e.message || e), 'error');
|
||
updateLoginState('二维码获取失败', 'offline', e.message || '');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onCheckScanStatus(silent = false) {
|
||
const payload = getCommonPayload(false);
|
||
if (!payload) return;
|
||
if (!silent) setLoading(true);
|
||
try {
|
||
if (!silent) log('手动检测扫码状态...');
|
||
const data = await callApi(
|
||
`/auth/scan-status?key=${encodeURIComponent(payload.key)}`,
|
||
);
|
||
const text = JSON.stringify(data);
|
||
log('扫码状态: ' + text);
|
||
|
||
const lower = text.toLowerCase();
|
||
const obj = data && typeof data === 'object' ? data : {};
|
||
const d = obj.Data && typeof obj.Data === 'object' ? obj.Data : obj;
|
||
const stateVal = d.state ?? d.State;
|
||
|
||
// 后端返回滑块 path,在当前页右侧替换为滑块区域并自动填充参数(取码后 15 秒内不自动切,避免未出码就进验证)
|
||
const sliderUrl = data.slider_url;
|
||
var allowSliderSwitch = !state.qrcodeFetchedAt || (Date.now() - state.qrcodeFetchedAt) > 15000;
|
||
if (sliderUrl && typeof sliderUrl === 'string' && allowSliderSwitch) {
|
||
state.sliderOpened = true;
|
||
log('需完成滑块验证,已切换到滑块验证区域。', 'warn');
|
||
const params = parseSliderUrlParams(sliderUrl);
|
||
if (params) state.sliderParams = params;
|
||
showSliderAreaAndFill(params);
|
||
} else if (sliderUrl && !allowSliderSwitch) {
|
||
log('检测到需验证,但刚取码不久,暂不自动切到滑块,请先扫码或稍后再检测。', 'warn');
|
||
}
|
||
|
||
// 检测状态是否成功登录:state==2 或 Success+已登录 等,成功后自动跳转管理页
|
||
var loginSuccess = stateVal === 2 || lower.includes('\"state\":2') || /\"state\"\\s*:\\s*2/.test(lower) ||
|
||
(obj.Success === true && (lower.includes('登录') || lower.includes('online') || d && (d.state === 2 || d.State === 2)));
|
||
if (loginSuccess) {
|
||
updateLoginState('登录成功', 'ok', '');
|
||
if (state.pollingScan) {
|
||
clearInterval(state.pollingScan);
|
||
state.pollingScan = null;
|
||
}
|
||
log('检测到登录成功,正在跳转管理页…', 'ok');
|
||
showQrExpired('登录成功,正在跳转…');
|
||
var keyParam = payload.key ? '?key=' + encodeURIComponent(payload.key) : '';
|
||
window.location.href = 'manage.html' + keyParam;
|
||
// state == 1 → 先判定是否明确「过期」,否则一律视为可能需验证,继续轮询并优先根据 slider_url 切到滑块
|
||
} else if (stateVal === 1 || lower.includes('\"state\":1') || /\"state\"\\s*:\\s*1/.test(lower)) {
|
||
var needVerify = /请提交验证码|提交验证码后登录|验证码后登录|安全验证|完成验证/.test(text) ||
|
||
(d && (d.msg && /请提交验证码|验证码后登录|安全验证|完成验证/.test(d.msg)) || (obj.Text && /请提交验证码|验证码后登录|安全验证|完成验证/.test(obj.Text)));
|
||
var explicitExpired = /二维码已过期|已过期|已失效|失效|请重新获取|重新获取二维码/.test(text) ||
|
||
(d && (d.msg && /已过期|已失效|失效|请重新获取/.test(d.msg)) || (obj.Text && /已过期|已失效|失效|请重新获取/.test(obj.Text)));
|
||
if (explicitExpired && !sliderUrl) {
|
||
updateLoginState('二维码已过期,请重新获取', 'offline', '');
|
||
if (state.pollingScan) {
|
||
clearInterval(state.pollingScan);
|
||
state.pollingScan = null;
|
||
}
|
||
showQrExpired('二维码已过期,请重新获取');
|
||
} else {
|
||
updateLoginState(needVerify || sliderUrl ? '请完成滑块验证' : '正在确认登录状态…', 'pending',
|
||
needVerify || sliderUrl ? '扫码完成,请完成验证后登录' : '');
|
||
// 判定到需验证时切换验证模块;取码后 15 秒内不自动切,避免未出码就进验证
|
||
if (needVerify || sliderUrl) {
|
||
var allowSwitch = !state.qrcodeFetchedAt || (Date.now() - state.qrcodeFetchedAt) > 15000;
|
||
if (allowSwitch) {
|
||
state.sliderOpened = true;
|
||
if (!sliderUrl) log('需完成滑块验证,已切换到滑块验证区域,等待参数…', 'warn');
|
||
showSliderAreaAndFill(state.sliderParams || {});
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
updateLoginState('扫码状态更新', 'pending', '');
|
||
}
|
||
} catch (e) {
|
||
if (!silent) log('检测扫码状态失败: ' + e.message, 'error');
|
||
} finally {
|
||
if (!silent) setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onGetOnlineStatus() {
|
||
const payload = getCommonPayload();
|
||
if (!payload) return;
|
||
setLoading(true);
|
||
try {
|
||
log('查询账号在线状态...');
|
||
const data = await callApi(`/auth/status?key=${encodeURIComponent(payload.key)}`);
|
||
const text = JSON.stringify(data);
|
||
log('在线状态: ' + text);
|
||
const lower = text.toLowerCase();
|
||
if (lower.includes('online') || lower.includes('true') || lower.includes('登录')) {
|
||
updateLoginState('当前账号在线', 'ok', '');
|
||
} else {
|
||
updateLoginState('当前账号离线或未登录', 'offline', '');
|
||
}
|
||
} catch (e) {
|
||
log('获取在线状态失败: ' + e.message, 'error');
|
||
updateLoginState('在线状态查询失败', 'unknown', e.message || '');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onVerifyCode() {
|
||
const payload = getCommonPayload();
|
||
if (!payload) return;
|
||
const code = (($('verify-code') && $('verify-code').value) || '').trim();
|
||
if (!code) {
|
||
alert('请先填写手机上的验证码');
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
log('正在提交验证码验证...');
|
||
const data = await callApi('/auth/verify-code', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
key: payload.key,
|
||
code,
|
||
}),
|
||
});
|
||
log('验证码验证结果: ' + JSON.stringify(data), 'ok');
|
||
updateLoginState('验证码已提交', 'pending', '请稍后点击「获取在线状态」确认是否已登录');
|
||
} catch (e) {
|
||
log('验证码验证失败: ' + (e.message || e), 'error');
|
||
updateLoginState('验证码验证失败', 'offline', e.message || '');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onLogout() {
|
||
const payload = getCommonPayload();
|
||
if (!payload) return;
|
||
if (!confirm('确定要退出该账号的微信登录吗?')) return;
|
||
setLoading(true);
|
||
try {
|
||
log('正在退出登录...');
|
||
const data = await callApi('/auth/logout', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ key: payload.key }),
|
||
});
|
||
log('退出登录完成: ' + JSON.stringify(data));
|
||
updateLoginState('已退出登录', 'offline');
|
||
if (state.pollingScan) {
|
||
clearInterval(state.pollingScan);
|
||
state.pollingScan = null;
|
||
}
|
||
} catch (e) {
|
||
log('退出登录失败: ' + e.message, 'error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
function bindEvents() {
|
||
$('btn-qrcode').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
if ($('slider-area') && $('slider-area').style.display !== 'none') {
|
||
showQrArea();
|
||
}
|
||
onGetQrCode(false);
|
||
});
|
||
$('btn-qrcode-mac') && $('btn-qrcode-mac').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onGetQrCode(true);
|
||
});
|
||
$('btn-wake') && $('btn-wake').addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
onWake();
|
||
});
|
||
$('btn-check-proxy') && $('btn-check-proxy').addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
onCheckProxy();
|
||
});
|
||
$('btn-show-slider') && $('btn-show-slider').addEventListener('click', function() {
|
||
if (state.sliderParams) showSliderAreaAndFill(state.sliderParams);
|
||
});
|
||
$('btn-check-scan').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onCheckScanStatus(false);
|
||
});
|
||
$('btn-online').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onGetOnlineStatus();
|
||
});
|
||
$('btn-verify-code') && $('btn-verify-code').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onVerifyCode();
|
||
});
|
||
$('btn-logout').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onLogout();
|
||
});
|
||
$('btn-clear-log').addEventListener('click', () => {
|
||
$('log-body').innerHTML = '';
|
||
});
|
||
}
|
||
|
||
bindEvents();
|
||
checkHealth();
|
||
(function() {
|
||
try {
|
||
if (typeof localStorage !== 'undefined' && localStorage.getItem('wechat_key')) {
|
||
var keyEl = $('key');
|
||
if (keyEl && !keyEl.value.trim()) keyEl.value = localStorage.getItem('wechat_key');
|
||
}
|
||
$('key').addEventListener('change', function() { try { localStorage.setItem('wechat_key', this.value.trim()); } catch (_) {} });
|
||
$('key').addEventListener('blur', function() { try { localStorage.setItem('wechat_key', this.value.trim()); } catch (_) {} });
|
||
} catch (_) {}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|