Files
shencaisuan/html/social-callback.html
T
2026-06-12 21:36:58 +08:00

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>