fix:修复页面bug

This commit is contained in:
丹尼尔
2026-03-11 12:21:25 +08:00
parent dae013dbeb
commit 6da73da8d7
6 changed files with 177 additions and 60 deletions

View File

@@ -1,4 +1,7 @@
PORT=3000
# Node 代理 /api、/auth 时转发的后端地址(单容器部署可不设,默认 127.0.0.1:8000
# BACKEND_PORT=8000
# BACKEND_HOST=127.0.0.1
WECHAT_UPSTREAM_BASE_URL=http://113.44.162.180:7006
CHECK_STATUS_BASE_URL=http://113.44.162.180:7006
# 第三方滑块(7765)iframe 加载自带预填表单页,提交到下方地址

View File

@@ -488,7 +488,11 @@ async def slider_asset_proxy(path: str):
if resp.status_code >= 400:
raise HTTPException(status_code=resp.status_code, detail=resp.text[:200])
media_type = "application/javascript" if path.endswith(".js") else "application/octet-stream"
return Response(content=resp.content, media_type=media_type)
return Response(
content=resp.content,
media_type=media_type,
headers={"Cache-Control": "no-store, no-cache, must-revalidate", "Pragma": "no-cache"},
)
except HTTPException:
raise
except Exception as e:

View File

@@ -596,6 +596,7 @@
</section>
<aside>
<div id="qr-area">
<div class="qr-card">
<div class="qr-header">
<span>扫码登录二维码</span>
@@ -612,20 +613,36 @@
</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>
<a id="slider-open-link" href="#" target="_blank" rel="noopener" class="btn secondary">重新打开滑块验证</a>
<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">滑块验证</div>
<div id="slider-app" data-v-app="">
<div class="params-section" style="margin-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>
</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;
const res = await fetch(url, {
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();
@@ -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 = sliderUrl.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;
showSliderAreaAndFill(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);
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('扫码状态更新', 'pending', text);
updateLoginState(needVerify || sliderUrl ? '请完成滑块验证' : '正在确认登录状态…', 'pending',
needVerify || sliderUrl ? '扫码完成,请完成验证后登录' : '');
// 判定到需验证时直接切换验证模块,不等用户点「检测扫码状态」;有 slider_url 已在上方填参,无则先切过去,后续轮询会带参
if (needVerify || sliderUrl) {
state.sliderOpened = true;
if (!sliderUrl) log('需完成滑块验证,已切换到滑块验证区域,等待参数…', 'warn');
showSliderAreaAndFill(state.sliderParams || {});
}
}
} else {
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);

View File

@@ -52,7 +52,7 @@ WECHAT_UPSTREAM_BASE_URL="${UPSTREAM_URL}" uvicorn backend.main:app --host 0.0.0
BACKEND_PID=$!
echo "Starting Node frontend dev server on :${FRONTEND_PORT}..."
PORT="${FRONTEND_PORT}" npm run dev &
PORT="${FRONTEND_PORT}" BACKEND_PORT="${BACKEND_PORT}" npm run dev &
FRONTEND_PID=$!
trap 'echo "Stopping dev servers..."; kill ${BACKEND_PID} ${FRONTEND_PID} 2>/dev/null || true' INT TERM

View File

@@ -7,8 +7,9 @@ import path from 'path';
dotenv.config();
const app = express();
const backendHost = process.env.BACKEND_HOST || '127.0.0.1';
const backendPort = Number(process.env.BACKEND_PORT) || 8000;
const backendBase = `http://127.0.0.1:${backendPort}`;
const backendBase = `http://${backendHost}:${backendPort}`;
app.use(cors());
app.use(express.json());
@@ -41,6 +42,11 @@ app.get('/openapi.json', (req, res) => proxyToBackend(req, res));
const publicDir = path.join(__dirname, '../public');
app.use(express.static(publicDir));
// 避免 Chrome DevTools 请求该路径时产生 404
app.get('/.well-known/appspecific/com.chrome.devtools.json', (_req, res) => {
res.status(204).end();
});
app.get('/health', (_req, res) => {
res.json({ status: 'ok', server: 'node-static' });
});

View File

@@ -10,7 +10,7 @@ uvicorn backend.main:app --host 0.0.0.0 --port "${BACKEND_PORT}" &
BACKEND_PID=$!
echo "Starting Node static frontend on :${PORT}..."
node dist/server.js &
PORT="${PORT}" BACKEND_PORT="${BACKEND_PORT}" node dist/server.js &
FRONTEND_PID=$!
trap 'echo "Stopping services..."; kill ${BACKEND_PID} ${FRONTEND_PID} 2>/dev/null || true' INT TERM