1086 lines
34 KiB
HTML
1086 lines
34 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>
|
||
</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">代理(可选)</label>
|
||
<input
|
||
id="proxy"
|
||
placeholder="socks5://username:password@ipv4:port"
|
||
autocomplete="off"
|
||
/>
|
||
</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> </label>
|
||
<div class="checkbox-row">
|
||
<input type="checkbox" id="check-proxy" />
|
||
<label for="check-proxy">修改代理时自动检测可用性</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<button class="primary" id="btn-qrcode">
|
||
获取登录二维码
|
||
</button>
|
||
<button class="secondary" id="btn-check-scan">
|
||
检测扫码状态
|
||
</button>
|
||
<button class="secondary" id="btn-online">
|
||
获取在线状态
|
||
</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 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 class="slider-card" id="slider-card" style="display: none">
|
||
<div class="small-label">第三方滑块(7765),参数已自动填充,点击「开始验证」提交</div>
|
||
<iframe
|
||
id="slider-frame"
|
||
class="slider-frame"
|
||
src=""
|
||
referrerpolicy="no-referrer"
|
||
></iframe>
|
||
</div>
|
||
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
const API_BASE = 'http://localhost:8000';
|
||
|
||
const state = {
|
||
pollingScan: null,
|
||
pollingOnline: null,
|
||
sliderOpened: false,
|
||
};
|
||
|
||
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 {
|
||
const res = await fetch('/health', { 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;
|
||
$('btn-check-scan').disabled = loading;
|
||
$('btn-online').disabled = loading;
|
||
$('btn-logout').disabled = loading;
|
||
}
|
||
|
||
function getCommonPayload() {
|
||
const key = $('key').value.trim();
|
||
const proxy = $('proxy').value.trim();
|
||
const device = $('device').value;
|
||
const check = $('check-proxy').checked;
|
||
if (!key) {
|
||
alert('请先填写账号唯一标识(key)');
|
||
return null;
|
||
}
|
||
return {
|
||
key,
|
||
proxy: proxy || undefined,
|
||
ipadOrMac: device,
|
||
check,
|
||
};
|
||
}
|
||
|
||
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;
|
||
const res = await fetch(url, {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(options.headers || {}),
|
||
},
|
||
});
|
||
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 onGetQrCode() {
|
||
const payload = getCommonPayload();
|
||
if (!payload) return;
|
||
setLoading(true);
|
||
try {
|
||
log('请求登录二维码...');
|
||
const body = {
|
||
Proxy: payload.proxy || '',
|
||
IpadOrmac: payload.ipadOrMac || '',
|
||
Check: !!payload.check,
|
||
};
|
||
const data = await callApi('/auth/qrcode', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ key: payload.key, ...body }),
|
||
});
|
||
log('获取二维码成功');
|
||
renderQrFromResponse(data);
|
||
updateLoginState('等待扫码 / 确认中', 'pending', '请在 60 秒内使用微信扫码。');
|
||
// 启动自动轮询,每 5 秒检测一次扫码状态
|
||
if (state.pollingScan) {
|
||
clearInterval(state.pollingScan);
|
||
}
|
||
state.pollingScan = setInterval(() => {
|
||
onCheckScanStatus(true);
|
||
}, 5000);
|
||
} catch (e) {
|
||
log('获取二维码失败: ' + e.message, 'error');
|
||
updateLoginState('二维码获取失败', 'offline', e.message || '');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onCheckScanStatus(silent = false) {
|
||
const payload = getCommonPayload();
|
||
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(/auth/slider-form?…),iframe 加载后自动填充 Key/Data62/Original Ticket,提交到第三方 7765
|
||
const sliderUrl = data.slider_url;
|
||
if (sliderUrl && typeof sliderUrl === 'string') {
|
||
state.sliderOpened = true;
|
||
log('使用第三方滑块(7765),参数已自动填充。', 'warn');
|
||
const sliderCard = $('slider-card');
|
||
const sliderFrame = $('slider-frame');
|
||
if (sliderCard && sliderFrame) {
|
||
const iframeSrc = sliderUrl.startsWith('/') ? (API_BASE + sliderUrl) : sliderUrl;
|
||
sliderFrame.src = iframeSrc;
|
||
sliderCard.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
// state == 2 → 登录成功,跳转后端管理页
|
||
if (stateVal === 2 || lower.includes('\"state\":2') || /\"state\"\\s*:\\s*2/.test(lower)) {
|
||
updateLoginState('登录成功', 'ok', text);
|
||
if (state.pollingScan) {
|
||
clearInterval(state.pollingScan);
|
||
state.pollingScan = null;
|
||
}
|
||
showQrExpired('登录成功,正在跳转…');
|
||
window.location.href = 'manage.html';
|
||
// state == 1 → 二维码失效 / 不可用(按你的规则)
|
||
} else if (stateVal === 1 || lower.includes('\"state\":1') || /\"state\"\\s*:\\s*1/.test(lower)) {
|
||
updateLoginState('二维码已过期,请重新获取', 'offline', text);
|
||
if (state.pollingScan) {
|
||
clearInterval(state.pollingScan);
|
||
state.pollingScan = null;
|
||
}
|
||
showQrExpired('二维码已过期,请重新获取');
|
||
} else {
|
||
updateLoginState('扫码状态更新', 'pending', text);
|
||
}
|
||
} 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', text);
|
||
} else {
|
||
updateLoginState('当前账号离线或未登录', 'offline', text);
|
||
}
|
||
} catch (e) {
|
||
log('获取在线状态失败: ' + e.message, 'error');
|
||
updateLoginState('在线状态查询失败', 'unknown', 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();
|
||
onGetQrCode();
|
||
});
|
||
$('btn-check-scan').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onCheckScanStatus(false);
|
||
});
|
||
$('btn-online').addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
onGetOnlineStatus();
|
||
});
|
||
$('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>
|
||
|