Files
wechatAiclaw/public/index.html
2026-03-11 09:44:17 +08:00

1087 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">代理(可选)</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>&nbsp;</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>