686 lines
20 KiB
HTML
686 lines
20 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>神彩算 - 微信登录</title>
|
|
<script src="../config.js"></script>
|
|
<script src="../utils/ApiClient.js"></script>
|
|
<script src="../utils/AuthPageUtil.js"></script>
|
|
<link rel="stylesheet" href="../public/css/social-callback.css" />
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
<div class="card">
|
|
<div class="brand">神彩算</div>
|
|
<div class="title" id="title">微信登录</div>
|
|
<div class="desc" id="desc">正在处理微信授权,请稍候...</div>
|
|
<div class="status" id="status">
|
|
<span class="spinner"></span>
|
|
<span id="statusText">正在登录</span>
|
|
</div>
|
|
<div class="error" id="errorBox"></div>
|
|
|
|
<form class="bind-form" id="bindForm" autocomplete="off">
|
|
<div class="form-row">
|
|
<label for="phoneInput">绑定手机号</label>
|
|
<input
|
|
class="input"
|
|
id="phoneInput"
|
|
maxlength="11"
|
|
placeholder="请输入手机号"
|
|
type="text"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="smsInput">短信验证码</label>
|
|
<div class="sms-row">
|
|
<input
|
|
class="sms-input"
|
|
id="smsInput"
|
|
maxlength="6"
|
|
placeholder="请输入验证码"
|
|
type="text"
|
|
/>
|
|
<button class="sms-button" id="sendSmsButton" type="button">
|
|
获取验证码
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="submit" id="bindButton" type="submit">
|
|
绑定并登录
|
|
</button>
|
|
|
|
<div class="tips">
|
|
该微信还没有绑定手机号,请先完成绑定。手机号不存在时会创建账号,已存在时会绑定到该账号。
|
|
</div>
|
|
</form>
|
|
|
|
<div class="actions">
|
|
<a href="login.html">返回登录</a>
|
|
<button id="retryButton" type="button">重试</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const params = new URLSearchParams(window.location.search);
|
|
const socialContext = {
|
|
source: params.get("source") || "wechat_open",
|
|
code: params.get("code") || params.get("socialCode") || "",
|
|
state: params.get("state") || params.get("socialState") || "",
|
|
bindToken: params.get("bindToken") || "",
|
|
bindVerifyType: "slide",
|
|
captchaUuid: "",
|
|
};
|
|
const ACCOUNT_BIND_MODE = "account_bind";
|
|
const ACCOUNT_BIND_MAX_AGE = 10 * 60 * 1000;
|
|
|
|
const $ = (selector) => document.querySelector(selector);
|
|
const status = $("#status");
|
|
const statusText = $("#statusText");
|
|
const errorBox = $("#errorBox");
|
|
const bindForm = $("#bindForm");
|
|
const sendSmsButton = $("#sendSmsButton");
|
|
const bindButton = $("#bindButton");
|
|
let bindVerifyLoaded = false;
|
|
|
|
function socialHeaders() {
|
|
return { clientid: ApiClient.CLIENT_ID };
|
|
}
|
|
|
|
function setStatus(text, loading = true) {
|
|
status.style.display = "flex";
|
|
status.querySelector(".spinner").style.display = loading ? "" : "none";
|
|
statusText.textContent = text;
|
|
}
|
|
|
|
function normalizeMessage(message, fallback) {
|
|
const text = String(message || "").trim();
|
|
const map = {
|
|
"rate.limiter.message": "操作太频繁,请稍后再试",
|
|
};
|
|
|
|
return map[text] || text || fallback;
|
|
}
|
|
|
|
function showError(message) {
|
|
errorBox.textContent = normalizeMessage(message, "登录失败,请稍后重试");
|
|
errorBox.style.display = "block";
|
|
}
|
|
|
|
function hideError() {
|
|
errorBox.textContent = "";
|
|
errorBox.style.display = "none";
|
|
}
|
|
|
|
function getToken(data) {
|
|
return (
|
|
data.access_token ||
|
|
data.accessToken ||
|
|
data.token ||
|
|
data.access_token_value ||
|
|
""
|
|
);
|
|
}
|
|
|
|
function getBindToken(rawData) {
|
|
const data = rawData || {};
|
|
const nested = data.data || {};
|
|
|
|
return (
|
|
data.bindToken ||
|
|
data.bind_token ||
|
|
data.phoneBindToken ||
|
|
data.socialBindToken ||
|
|
nested.bindToken ||
|
|
nested.bind_token ||
|
|
nested.phoneBindToken ||
|
|
nested.socialBindToken ||
|
|
""
|
|
);
|
|
}
|
|
|
|
function saveLoginData(rawData) {
|
|
const data = rawData || {};
|
|
const token = getToken(data);
|
|
|
|
if (!token) {
|
|
return false;
|
|
}
|
|
|
|
localStorage.setItem("token", token);
|
|
localStorage.setItem("access_token", data.access_token || token);
|
|
localStorage.setItem("userInfo", JSON.stringify(data));
|
|
localStorage.setItem("accountInfo", JSON.stringify(data));
|
|
sessionStorage.removeItem("authExpiredRedirecting");
|
|
sessionStorage.removeItem("authExpiredMessage");
|
|
|
|
return true;
|
|
}
|
|
|
|
function homeUrl() {
|
|
if (
|
|
location.hostname === "localhost" ||
|
|
location.hostname === "127.0.0.1" ||
|
|
location.port
|
|
) {
|
|
return "index.html";
|
|
}
|
|
|
|
return "/";
|
|
}
|
|
|
|
function redirectHome() {
|
|
setStatus("登录成功,正在跳转...", false);
|
|
window.setTimeout(() => {
|
|
window.location.replace(homeUrl());
|
|
}, 500);
|
|
}
|
|
|
|
function readStoredValue(key) {
|
|
try {
|
|
return sessionStorage.getItem(key) || localStorage.getItem(key) || "";
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function removeStoredValue(key) {
|
|
try {
|
|
sessionStorage.removeItem(key);
|
|
} catch (e) {}
|
|
|
|
try {
|
|
localStorage.removeItem(key);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function clearAccountBindMode() {
|
|
removeStoredValue("wechatSocialMode");
|
|
removeStoredValue("wechatSocialReturnUrl");
|
|
removeStoredValue("wechatSocialStartedAt");
|
|
}
|
|
|
|
function markAccountBindSuccess() {
|
|
try {
|
|
sessionStorage.setItem("wechatAccountBindSuccess", "1");
|
|
sessionStorage.setItem("wechatAccountBindSuccessAt", String(Date.now()));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function getAccountBindReturnUrl() {
|
|
const stored = readStoredValue("wechatSocialReturnUrl");
|
|
|
|
if (stored && stored.charAt(0) === "/" && !/^\/\//.test(stored)) {
|
|
return stored;
|
|
}
|
|
|
|
return location.hostname === "localhost" || location.hostname === "127.0.0.1" || location.port
|
|
? "usercenter.html"
|
|
: "/html/usercenter.html";
|
|
}
|
|
|
|
function isAccountBindCallback() {
|
|
const mode = readStoredValue("wechatSocialMode");
|
|
const startedAt = Number(readStoredValue("wechatSocialStartedAt") || 0);
|
|
const isFresh = !startedAt || Date.now() - startedAt <= ACCOUNT_BIND_MAX_AGE;
|
|
const token = localStorage.getItem("token") || localStorage.getItem("access_token") || "";
|
|
|
|
return mode === ACCOUNT_BIND_MODE && isFresh && Boolean(token);
|
|
}
|
|
|
|
function redirectAccountBindDone() {
|
|
const returnUrl = getAccountBindReturnUrl();
|
|
markAccountBindSuccess();
|
|
clearAccountBindMode();
|
|
setStatus("微信绑定成功,正在返回个人中心...", false);
|
|
window.setTimeout(() => {
|
|
window.location.replace(returnUrl);
|
|
}, 500);
|
|
}
|
|
|
|
async function socialBindAccount() {
|
|
hideError();
|
|
bindForm.style.display = "none";
|
|
setStatus("正在绑定微信");
|
|
|
|
if (!socialContext.code || !socialContext.state) {
|
|
status.style.display = "none";
|
|
showError("绑定参数缺失,请回到个人中心重新发起微信绑定");
|
|
return;
|
|
}
|
|
|
|
const res = await ApiClient.post(
|
|
ApiClient.API.authSocialBind,
|
|
{
|
|
source: socialContext.source,
|
|
socialCode: socialContext.code,
|
|
socialState: socialContext.state,
|
|
},
|
|
{
|
|
headers: socialHeaders(),
|
|
},
|
|
);
|
|
|
|
if (ApiClient.isSuccess(res)) {
|
|
redirectAccountBindDone();
|
|
return;
|
|
}
|
|
|
|
status.style.display = "none";
|
|
showError((res && res.msg) || "微信绑定失败,请回到个人中心重新发起绑定");
|
|
}
|
|
|
|
function isBindRequired(res) {
|
|
const data = ApiClient.pickData(res) || {};
|
|
const bindToken = getBindToken({ ...data, data: res && res.data });
|
|
const status = String(
|
|
data.loginStatus ||
|
|
data.login_status ||
|
|
data.status ||
|
|
data.state ||
|
|
data.result ||
|
|
data.code ||
|
|
"",
|
|
).toUpperCase();
|
|
const message = String(
|
|
(res && (res.msg || res.message)) ||
|
|
data.msg ||
|
|
data.message ||
|
|
"",
|
|
);
|
|
|
|
return Boolean(
|
|
data.bindRequired ||
|
|
data.needBind ||
|
|
data.needBindPhone ||
|
|
data.needPhoneBind ||
|
|
data.phoneBindRequired ||
|
|
status === "NEED_BIND_PHONE" ||
|
|
status === "NEED_BIND_MOBILE" ||
|
|
status === "NEED_PHONE_BIND" ||
|
|
Boolean(bindToken) ||
|
|
message.includes("\u7ed1\u5b9a") ||
|
|
message.includes("\u624b\u673a\u53f7") ||
|
|
message.includes("\u624b\u673a") ||
|
|
/bind|phone/i.test(message),
|
|
);
|
|
}
|
|
|
|
function ensureCaptchaRow() {
|
|
let row = $("#captchaRow");
|
|
|
|
if (row) {
|
|
return row;
|
|
}
|
|
|
|
const phoneRow = $("#phoneInput").closest(".form-row");
|
|
phoneRow.insertAdjacentHTML(
|
|
"afterend",
|
|
`
|
|
<div class="form-row captcha-row" id="captchaRow">
|
|
<label for="captchaInput">图形验证码</label>
|
|
<div class="captcha-box">
|
|
<input
|
|
class="input"
|
|
id="captchaInput"
|
|
maxlength="6"
|
|
placeholder="请输入图形验证码"
|
|
type="text"
|
|
/>
|
|
<img class="captcha-image" id="captchaImage" alt="点击刷新验证码" />
|
|
</div>
|
|
</div>
|
|
`,
|
|
);
|
|
row = $("#captchaRow");
|
|
$("#captchaImage").addEventListener("click", refreshBindCaptcha);
|
|
return row;
|
|
}
|
|
|
|
function setBindVerifyType(type) {
|
|
socialContext.bindVerifyType = type === "slide" ? "slide" : "captcha";
|
|
const row = ensureCaptchaRow();
|
|
|
|
if (socialContext.bindVerifyType === "captcha") {
|
|
row.style.display = "block";
|
|
refreshBindCaptcha();
|
|
return;
|
|
}
|
|
|
|
row.style.display = "none";
|
|
}
|
|
|
|
function refreshBindCaptcha() {
|
|
if (socialContext.bindVerifyType !== "captcha") {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
socialContext.captchaUuid = "";
|
|
const input = $("#captchaInput");
|
|
const image = $("#captchaImage");
|
|
if (input) input.value = "";
|
|
if (image) image.removeAttribute("src");
|
|
|
|
return ApiClient.get(
|
|
ApiClient.API.authCode,
|
|
{},
|
|
{
|
|
publicRequest: true,
|
|
headers: socialHeaders(),
|
|
},
|
|
)
|
|
.then((res) => {
|
|
const data = ApiClient.pickData(res) || {};
|
|
|
|
if (!ApiClient.isSuccess(res) || !data) {
|
|
throw new Error((res && res.msg) || "图形验证码获取失败");
|
|
}
|
|
|
|
socialContext.captchaUuid = data.uuid || data.captchaId || "";
|
|
if (image) {
|
|
image.src = data.img || data.image || data.base64 || "";
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
showError(error.message || "图形验证码获取失败,请刷新重试");
|
|
});
|
|
}
|
|
|
|
function initBindVerify() {
|
|
if (bindVerifyLoaded) {
|
|
return;
|
|
}
|
|
|
|
bindVerifyLoaded = true;
|
|
setBindVerifyType("slide");
|
|
}
|
|
|
|
function showBindForm(message, data = {}) {
|
|
socialContext.bindToken = getBindToken(data) || socialContext.bindToken;
|
|
const desc = normalizeMessage(message, "");
|
|
$("#title").textContent = "绑定手机号";
|
|
$("#desc").textContent =
|
|
!desc || desc === "操作成功"
|
|
? "微信还没有绑定手机号,请先完成手机验证"
|
|
: desc;
|
|
status.style.display = "none";
|
|
bindForm.style.display = "block";
|
|
initBindVerify();
|
|
}
|
|
|
|
async function socialLogin() {
|
|
hideError();
|
|
bindForm.style.display = "none";
|
|
setStatus("正在登录");
|
|
|
|
if (!socialContext.code || !socialContext.state) {
|
|
status.style.display = "none";
|
|
showError("登录参数缺失,请返回登录页重新发起微信登录");
|
|
return;
|
|
}
|
|
|
|
const res = await ApiClient.post(
|
|
ApiClient.API.authSocialLogin,
|
|
{
|
|
source: socialContext.source,
|
|
socialCode: socialContext.code,
|
|
socialState: socialContext.state,
|
|
},
|
|
{
|
|
publicRequest: true,
|
|
headers: socialHeaders(),
|
|
},
|
|
);
|
|
|
|
const data = ApiClient.pickData(res) || {};
|
|
|
|
if (ApiClient.isSuccess(res) && saveLoginData(data)) {
|
|
redirectHome();
|
|
return;
|
|
}
|
|
|
|
if (ApiClient.isSuccess(res) || isBindRequired(res)) {
|
|
showBindForm((res && res.msg) || "", {
|
|
...data,
|
|
data: res && res.data,
|
|
});
|
|
return;
|
|
}
|
|
|
|
status.style.display = "none";
|
|
showError((res && res.msg) || "微信登录失败,请返回登录页重试");
|
|
}
|
|
|
|
function validateBindForm() {
|
|
const phone = $("#phoneInput").value.trim();
|
|
const smsCode = $("#smsInput").value.trim();
|
|
|
|
if (!AuthPageUtil.isPhone(phone)) {
|
|
showError("请输入正确的手机号");
|
|
return null;
|
|
}
|
|
|
|
if (!/^\d{4,6}$/.test(smsCode)) {
|
|
showError("请输入4-6位短信验证码");
|
|
return null;
|
|
}
|
|
|
|
if (!socialContext.bindToken) {
|
|
showError("缺少绑定凭证,请返回登录页重新扫码");
|
|
return null;
|
|
}
|
|
|
|
hideError();
|
|
|
|
return { phone, smsCode };
|
|
}
|
|
|
|
function startSmsCountdown(seconds = 60) {
|
|
let left = seconds;
|
|
sendSmsButton.disabled = true;
|
|
sendSmsButton.textContent = `${left}s后重发`;
|
|
|
|
const timer = window.setInterval(() => {
|
|
left -= 1;
|
|
if (left <= 0) {
|
|
window.clearInterval(timer);
|
|
sendSmsButton.disabled = false;
|
|
sendSmsButton.textContent = "获取验证码";
|
|
return;
|
|
}
|
|
sendSmsButton.textContent = `${left}s后重发`;
|
|
}, 1000);
|
|
}
|
|
|
|
function getBindCaptchaParams() {
|
|
const code = ($("#captchaInput") && $("#captchaInput").value.trim()) || "";
|
|
|
|
if (!socialContext.captchaUuid || !code) {
|
|
showError("请先输入图形验证码");
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
verifyType: "captcha",
|
|
uuid: socialContext.captchaUuid,
|
|
code,
|
|
};
|
|
}
|
|
|
|
function openBindSlideCaptcha() {
|
|
return new Promise((resolve, reject) => {
|
|
AuthPageUtil.openSlideCaptcha({
|
|
title: "绑定手机号安全验证",
|
|
subtitle: "拖动滑块完成验证后发送短信",
|
|
onSuccess: resolve,
|
|
onCancel: () => reject(new Error("已取消安全验证")),
|
|
onError: (error) => reject(error || new Error("滑动验证失败")),
|
|
});
|
|
});
|
|
}
|
|
|
|
async function getBindSmsVerifyParams() {
|
|
if (socialContext.bindVerifyType === "slide") {
|
|
sendSmsButton.textContent = "验证中...";
|
|
const result = await openBindSlideCaptcha();
|
|
const slideToken =
|
|
result.slideToken ||
|
|
result.token ||
|
|
result.verifyToken ||
|
|
"";
|
|
const slideUuid = result.slideUuid || result.uuid || "";
|
|
const slideXValue = result.slideX !== undefined ? result.slideX : result.x;
|
|
|
|
if (!slideToken && !slideUuid) {
|
|
throw new Error("滑动验证未返回凭证,请重试");
|
|
}
|
|
|
|
const params = {
|
|
verifyType: "slide",
|
|
};
|
|
|
|
if (slideToken) {
|
|
params.slideToken = slideToken;
|
|
}
|
|
|
|
if (slideUuid) {
|
|
params.slideUuid = slideUuid;
|
|
}
|
|
|
|
if (slideXValue !== undefined && slideXValue !== null && slideXValue !== "") {
|
|
params.slideX = Number(slideXValue) || 0;
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
return getBindCaptchaParams();
|
|
}
|
|
|
|
async function requestBindPhoneSmsCode(phone, verifyParams) {
|
|
return ApiClient.get(
|
|
ApiClient.API.authSocialBindPhoneSmsCode,
|
|
{
|
|
phoneNumber: phone,
|
|
...verifyParams,
|
|
},
|
|
{
|
|
publicRequest: true,
|
|
headers: socialHeaders(),
|
|
},
|
|
);
|
|
}
|
|
|
|
async function sendSmsCode() {
|
|
const phone = $("#phoneInput").value.trim();
|
|
|
|
if (!AuthPageUtil.isPhone(phone)) {
|
|
showError("请输入正确的手机号");
|
|
return;
|
|
}
|
|
|
|
if (!socialContext.bindToken) {
|
|
showError("缺少绑定凭证,请返回登录页重新扫码");
|
|
return;
|
|
}
|
|
|
|
hideError();
|
|
sendSmsButton.disabled = true;
|
|
sendSmsButton.textContent = "发送中...";
|
|
|
|
try {
|
|
const verifyParams = await getBindSmsVerifyParams();
|
|
|
|
if (!verifyParams) {
|
|
sendSmsButton.disabled = false;
|
|
sendSmsButton.textContent = "获取验证码";
|
|
return;
|
|
}
|
|
|
|
sendSmsButton.textContent = "发送中...";
|
|
const res = await requestBindPhoneSmsCode(phone, verifyParams);
|
|
|
|
if (!ApiClient.isSuccess(res)) {
|
|
throw new Error((res && res.msg) || "短信验证码发送失败");
|
|
}
|
|
|
|
startSmsCountdown();
|
|
} catch (error) {
|
|
sendSmsButton.disabled = false;
|
|
sendSmsButton.textContent = "获取验证码";
|
|
showError(error.message || "短信验证码发送失败");
|
|
if (socialContext.bindVerifyType === "captcha") {
|
|
refreshBindCaptcha();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function submitBind(event) {
|
|
event.preventDefault();
|
|
const formData = validateBindForm();
|
|
|
|
if (!formData) {
|
|
return;
|
|
}
|
|
|
|
bindButton.disabled = true;
|
|
bindButton.textContent = "绑定中...";
|
|
|
|
try {
|
|
const res = await ApiClient.post(
|
|
ApiClient.API.authSocialBindPhone,
|
|
{
|
|
bindToken: socialContext.bindToken,
|
|
phoneNumber: formData.phone,
|
|
smsCode: formData.smsCode,
|
|
},
|
|
{
|
|
publicRequest: true,
|
|
headers: socialHeaders(),
|
|
},
|
|
);
|
|
|
|
if (!ApiClient.isSuccess(res)) {
|
|
throw new Error((res && res.msg) || "绑定失败,请稍后重试");
|
|
}
|
|
|
|
const data = ApiClient.pickData(res) || {};
|
|
|
|
if (saveLoginData(data)) {
|
|
redirectHome();
|
|
return;
|
|
}
|
|
|
|
setStatus("绑定成功,正在登录...");
|
|
await socialLogin();
|
|
} catch (error) {
|
|
showError(error.message || "绑定失败,请稍后重试");
|
|
} finally {
|
|
bindButton.disabled = false;
|
|
bindButton.textContent = "绑定并登录";
|
|
}
|
|
}
|
|
|
|
sendSmsButton.addEventListener("click", sendSmsCode);
|
|
bindForm.addEventListener("submit", submitBind);
|
|
$("#retryButton").addEventListener("click", () => {
|
|
const action = isAccountBindCallback() ? socialBindAccount : socialLogin;
|
|
action().catch((error) => {
|
|
status.style.display = "none";
|
|
showError(error.message || "微信登录失败,请返回登录页重试");
|
|
});
|
|
});
|
|
|
|
const initialAction = isAccountBindCallback() ? socialBindAccount : socialLogin;
|
|
initialAction().catch((error) => {
|
|
status.style.display = "none";
|
|
showError(error.message || "微信登录失败,请返回登录页重试");
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|