@@ -596,36 +596,53 @@
< / section >
< aside >
< div class = "qr-c ard " >
< 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 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 class = "hint" id = "qr-hint" >
请使用微信客户端扫描二维码完成登录。
< / 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 style = "height: 12px" > < / div >
< div class = "card" >
< div class = "small-label" > 当前账号在线状态< / div >
< div class = "login-state unknown" id = "lo gin-state-text" > 未知< / div >
< div class = "small -label" id = "login-extra " > 尚未查询 < / div >
< div id = "slider-area" style = "display: none;" >
< div class = "card" style = "max-width: 480px;" >
< div class = "card-title" > 滑块验证< / div >
< div id = "slider-app" data-v-app = "" >
< div class = "params-section" style = "mar gin-bottom: 12px;" >
< label class = "form -label" for = "keyInput " > Key: < / label >
< input type = "text" class = "form-control" id = "keyInput" placeholder = "请输入key" 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 >
< div class = "slider-card" id = "slider-card" style = "display: none" >
< div class = "small-label" > 需完成滑块验证,已在弹窗打开验证页(与 7765 同结构,本地实现)< / div >
< a id = "slider-open-link" href = "#" target = "_blank" rel = "noopener" class = "btn secondary" > 重新打开滑块验证< / a >
< / div >
< / aside >
< / div >
< / div >
@@ -634,15 +651,78 @@
< script >
const $ = ( id ) => document . getElementById ( id ) ;
// 相对路径, 由 Node 代理到后端,适配 -p/-b 任意端口
const API _BASE = '' ;
// 相对路径由 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 ,
} ;
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' ) ;
if ( params ) {
if ( keyInput ) keyInput . value = params . key || params . Key || '' ;
if ( data62Input ) data62Input . value = params . data62 || '' ;
if ( ticketInput ) ticketInput . value = params . ticket || params . original _ticket || '' ;
}
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' ) ;
@@ -883,14 +963,22 @@
}
async function callApi ( path , options = { } ) {
const url = API _BASE + path ;
cons t res = await fetch ( url , {
... options ,
headers : {
'Content-Type' : 'application/json' ,
... ( options . headers || { } ) ,
} ,
} ) ;
const url = ( API _BASE || '' ) + path ;
le t 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 ( ) ;
@@ -922,10 +1010,11 @@
log ( '获取二维码成功' ) ;
renderQrFromResponse ( data ) ;
updateLoginState ( '等待扫码 / 确认中' , 'pending' , '请在 60 秒内使用微信扫码。' ) ;
// 启动自动轮询,每 5 秒检测一次扫码状态
// 扫完码后自动检测:先 2 秒做一次检测,再每 5 秒轮询,避免误判为「二维码失效」
if ( state . pollingScan ) {
clearInterval ( state . pollingScan ) ;
}
setTimeout ( ( ) => onCheckScanStatus ( true ) , 2000 ) ;
state . pollingScan = setInterval ( ( ) => {
onCheckScanStatus ( true ) ;
} , 5000 ) ;
@@ -954,41 +1043,50 @@
const d = obj . Data && typeof obj . Data === 'object' ? obj . Data : obj ;
const stateVal = d . state ? ? d . State ;
// 后端返回滑块页 path, 在新窗口打开(本地页,与 7765 同 DOM 结构 + 加载 7765 的 module 脚本),不用 iframe
// 后端返回滑块 path, 在当前页右侧替换为滑块区域并自动填充参数
const sliderUrl = data . slider _url ;
if ( sliderUrl && typeof sliderUrl === 'string' ) {
state . sliderOpened = true ;
log ( '需完成滑块验证,已在新窗口打开验证页 。' , 'warn' ) ;
const sliderCard = $ ( ' slider-card' ) ;
const openLink = $ ( 'slider-open-link' ) ;
const fullUrl = sliderUr l. startsWith ( '/' ) ? ( API _BASE + sliderUrl ) : sliderUrl ;
if ( sliderCard ) sliderCard . style . display = 'flex' ;
if ( openLink ) {
openLink . href = fullUrl ;
openLink . target = '_blank' ;
}
try { window . open ( fullUrl , 'slider-verify' , 'width=520,height=520,scrollbars=yes' ) ; } catch ( e ) { log ( '弹窗被拦截,请点击上方链接打开验证页' , 'warn' ) ; }
log ( '需完成滑块验证,已切换到滑块验证区域 。' , 'warn' ) ;
const params = parseSliderUrlParams ( sliderUrl ) ;
if ( params ) state . sliderParams = params ;
showSliderAreaAndFil l( params ) ;
}
// state == 2 → 登录成功,跳转后端管理页
if ( stateVal === 2 || lower . includes ( '\"state\":2' ) || /\"state\"\\s*:\\s*2/ . test ( lower ) ) {
updateLoginState ( '登录成功' , 'ok' , text ) ;
updateLoginState ( '登录成功' , 'ok' , '' ) ;
if ( state . pollingScan ) {
clearInterval ( state . pollingScan ) ;
state . pollingScan = null ;
}
showQrExpired ( '登录成功,正在跳转…' ) ;
window . location . href = 'manage.html' ;
// state == 1 → 二维码失效 / 不可用(按你的规则)
// state == 1 → 先判定是否明确「过期」,否则一律视为可能需验证,继续轮询并优先根据 slider_url 切到滑块
} else if ( stateVal === 1 || lower . includes ( '\"state\":1' ) || /\"state\"\\s*:\\s*1/ . test ( lower ) ) {
updateLoginState ( '二维码已过期,请重新获取' , 'offline' , text ) ;
if ( state . pollingScan ) {
clearInterval ( sta te. pollingScan ) ;
state . pollingScan = null ;
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 ? '扫码完成,请完成验证后登录' : '' ) ;
// 判定到需验证时直接切换验证模块,不等用户点「检测扫码状态」;有 slider_url 已在上方填参,无则先切过去,后续轮询会带参
if ( needVerify || sliderUrl ) {
state . sliderOpened = true ;
if ( ! sliderUrl ) log ( '需完成滑块验证,已切换到滑块验证区域,等待参数…' , 'warn' ) ;
showSliderAreaAndFill ( state . sliderParams || { } ) ;
}
}
showQrExpired ( '二维码已过期,请重新获取' ) ;
} else {
updateLoginState ( '扫码状态更新' , 'pending' , text ) ;
updateLoginState ( '扫码状态更新' , 'pending' , '' ) ;
}
} catch ( e ) {
if ( ! silent ) log ( '检测扫码状态失败: ' + e . message , 'error' ) ;
@@ -1008,9 +1106,9 @@
log ( '在线状态: ' + text ) ;
const lower = text . toLowerCase ( ) ;
if ( lower . includes ( 'online' ) || lower . includes ( 'true' ) || lower . includes ( '登录' ) ) {
updateLoginState ( '当前账号在线' , 'ok' , text ) ;
updateLoginState ( '当前账号在线' , 'ok' , '' ) ;
} else {
updateLoginState ( '当前账号离线或未登录' , 'offline' , text ) ;
updateLoginState ( '当前账号离线或未登录' , 'offline' , '' ) ;
}
} catch ( e ) {
log ( '获取在线状态失败: ' + e . message , 'error' ) ;
@@ -1047,8 +1145,14 @@
function bindEvents ( ) {
$ ( 'btn-qrcode' ) . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
if ( $ ( 'slider-area' ) && $ ( 'slider-area' ) . style . display !== 'none' ) {
showQrArea ( ) ;
}
onGetQrCode ( ) ;
} ) ;
$ ( '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 ) ;