@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* 认证相关前端封装:注册 / 登录 / 游客登录走本站 BFF,token 等写入 cookie。
|
||||
* 未注册用户自动游客登录(POST /api/auth/guest → 上游 /api/app/auth/guest)。
|
||||
*/
|
||||
(function (global) {
|
||||
var UUID_KEY = "uuid";
|
||||
var DEV_ID_KEY = "guest_dev_id";
|
||||
var CHANNEL_KEY = "channel";
|
||||
var GUEST_LOGOUT_FLAG_KEY = "guest_logout_manual";
|
||||
var guestLoginPromise = null;
|
||||
var volatileDevID = "";
|
||||
/** 上游约定:token 失效需重新登录 */
|
||||
var TOKEN_EXPIRED_CODE = 1000;
|
||||
var tokenRecoveryPromise = null;
|
||||
|
||||
function toast(message, type) {
|
||||
if (typeof global.showToast === "function") {
|
||||
global.showToast(message, type || "info");
|
||||
return;
|
||||
}
|
||||
// 调试兜底:无 toast 组件时用简易提示
|
||||
try {
|
||||
var tip = String(message || "").trim();
|
||||
if (!tip) return;
|
||||
var el = document.getElementById("auth-debug-toast");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "auth-debug-toast";
|
||||
el.setAttribute("role", "status");
|
||||
el.style.cssText =
|
||||
"position:fixed;left:50%;bottom:48px;transform:translateX(-50%);z-index:99999;" +
|
||||
"max-width:86vw;padding:10px 16px;border-radius:8px;font-size:14px;line-height:1.4;" +
|
||||
"color:#fff;background:rgba(20,20,20,.88);box-shadow:0 8px 24px rgba(0,0,0,.25);";
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
el.textContent = tip;
|
||||
el.style.background =
|
||||
type === "error"
|
||||
? "rgba(180,40,40,.92)"
|
||||
: type === "success"
|
||||
? "rgba(30,120,70,.92)"
|
||||
: "rgba(20,20,20,.88)";
|
||||
el.style.display = "block";
|
||||
clearTimeout(el._hideTimer);
|
||||
el._hideTimer = setTimeout(function () {
|
||||
el.style.display = "none";
|
||||
}, 2600);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function getInviteCode() {
|
||||
try {
|
||||
var params = new URLSearchParams(global.location.search || "");
|
||||
return (
|
||||
params.get("inviteCode") ||
|
||||
params.get("invite") ||
|
||||
params.get("code") ||
|
||||
""
|
||||
).trim();
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function hasSessionToken() {
|
||||
return !!(global.CookieUtil && global.CookieUtil.getToken && global.CookieUtil.getToken());
|
||||
}
|
||||
|
||||
// 与 91porn-h5-client generateUid 保持一致:16 位十六进制随机串 + 毫秒时间戳。
|
||||
function generateUUID() {
|
||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
var uuid = [];
|
||||
for (var i = 0; i < 16; i++) {
|
||||
uuid[i] = chars.charAt(Math.floor(Math.random() * 16));
|
||||
}
|
||||
return uuid.join("") + Date.now();
|
||||
}
|
||||
|
||||
function syncDevID(devID) {
|
||||
if (!devID) return;
|
||||
try {
|
||||
localStorage.setItem(UUID_KEY, devID);
|
||||
// 保留旧 key,兼容已上线页面和服务端游客 cookie。
|
||||
localStorage.setItem(DEV_ID_KEY, devID);
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||||
global.CookieUtil.set(DEV_ID_KEY, devID, { days: 365 });
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
function getOrCreateDevID() {
|
||||
var devID = "";
|
||||
try {
|
||||
devID = (localStorage.getItem(UUID_KEY) || "").trim();
|
||||
} catch (e) {}
|
||||
if (devID) {
|
||||
syncDevID(devID);
|
||||
return devID;
|
||||
}
|
||||
|
||||
// 首屏 SSR 可能已生成同规则 UUID 并写入 cookie,优先复用以保持游客身份一致。
|
||||
var fromCookie =
|
||||
global.CookieUtil && typeof global.CookieUtil.get === "function"
|
||||
? global.CookieUtil.get(DEV_ID_KEY)
|
||||
: "";
|
||||
if (fromCookie) {
|
||||
syncDevID(fromCookie);
|
||||
return fromCookie;
|
||||
}
|
||||
|
||||
if (!volatileDevID) {
|
||||
volatileDevID = generateUUID();
|
||||
}
|
||||
syncDevID(volatileDevID);
|
||||
return volatileDevID;
|
||||
}
|
||||
|
||||
function getChannel() {
|
||||
if (
|
||||
global.ChannelAttribution &&
|
||||
typeof global.ChannelAttribution.get === "function"
|
||||
) {
|
||||
return global.ChannelAttribution.get();
|
||||
}
|
||||
|
||||
var channel = "";
|
||||
try {
|
||||
channel = (localStorage.getItem(CHANNEL_KEY) || "").trim();
|
||||
if (channel) {
|
||||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||||
global.CookieUtil.set(CHANNEL_KEY, channel, { days: 365 });
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
channel =
|
||||
global.CookieUtil && typeof global.CookieUtil.get === "function"
|
||||
? String(global.CookieUtil.get(CHANNEL_KEY) || "").trim()
|
||||
: "";
|
||||
if (channel) {
|
||||
try {
|
||||
localStorage.setItem(CHANNEL_KEY, channel);
|
||||
} catch (e2) {}
|
||||
return channel;
|
||||
}
|
||||
} catch (e3) {}
|
||||
try {
|
||||
var params = new URLSearchParams(global.location.search || "");
|
||||
channel = (params.get("dc") || params.get("ch") || params.get("pc") || "").trim();
|
||||
if (channel) {
|
||||
localStorage.setItem(CHANNEL_KEY, channel);
|
||||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||||
global.CookieUtil.set(CHANNEL_KEY, channel, { days: 365 });
|
||||
}
|
||||
}
|
||||
} catch (e4) {}
|
||||
return channel;
|
||||
}
|
||||
|
||||
function clearGuestLogoutFlag() {
|
||||
try {
|
||||
sessionStorage.removeItem(GUEST_LOGOUT_FLAG_KEY);
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (global.CookieUtil && typeof global.CookieUtil.remove === "function") {
|
||||
global.CookieUtil.remove("guest_skip_auto");
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
function markGuestLogout() {
|
||||
try {
|
||||
sessionStorage.setItem(GUEST_LOGOUT_FLAG_KEY, "1");
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||||
// 不设 days → session cookie,配合 SSR 跳过本次自动游客
|
||||
global.CookieUtil.set("guest_skip_auto", "1");
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
function isGuestLogoutMarked() {
|
||||
try {
|
||||
if (sessionStorage.getItem(GUEST_LOGOUT_FLAG_KEY) === "1") return true;
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (
|
||||
global.CookieUtil &&
|
||||
typeof global.CookieUtil.get === "function" &&
|
||||
global.CookieUtil.get("guest_skip_auto") === "1"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch (e2) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function syncTrackerUid(user) {
|
||||
if (!user || !global.tracker || typeof global.tracker.setUid !== "function") return;
|
||||
var uid = user.userId != null ? user.userId : user.uid != null ? user.uid : user.id;
|
||||
if (uid == null || String(uid) === "") return;
|
||||
global.tracker.setUid(String(uid));
|
||||
}
|
||||
|
||||
function applyAuthSession(res, failTip) {
|
||||
if (!res || res.code !== 200 || !res.data) {
|
||||
var err = new Error((res && (res.tip || res.msg)) || failTip || "请求失败");
|
||||
err.code = res && res.code;
|
||||
err.data = res;
|
||||
throw err;
|
||||
}
|
||||
if (global.CookieUtil && typeof global.CookieUtil.setAuthSession === "function") {
|
||||
global.CookieUtil.setAuthSession(res.data);
|
||||
}
|
||||
clearGuestLogoutFlag();
|
||||
syncTrackerUid(res.data);
|
||||
try {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("auth:login", { detail: { user: res.data } })
|
||||
);
|
||||
} catch (e) {}
|
||||
return res;
|
||||
}
|
||||
|
||||
function register(payload) {
|
||||
var channel = getChannel();
|
||||
var body = {
|
||||
accountNo: (payload.accountNo || "").trim(),
|
||||
password: payload.password || "",
|
||||
devID: getOrCreateDevID(),
|
||||
channel: channel,
|
||||
};
|
||||
var invite = (payload.agentInviteCode || getInviteCode() || "").trim();
|
||||
if (invite) body.agentInviteCode = invite;
|
||||
|
||||
// 游客升级注册:携带当前游客 token,上游按 Authorization 绑定原游客账号。
|
||||
var options = {};
|
||||
var token =
|
||||
global.CookieUtil && typeof global.CookieUtil.getToken === "function"
|
||||
? global.CookieUtil.getToken()
|
||||
: "";
|
||||
var registered =
|
||||
global.CookieUtil && typeof global.CookieUtil.isRegistered === "function"
|
||||
? global.CookieUtil.isRegistered()
|
||||
: false;
|
||||
if (token && !registered) {
|
||||
options.headers = { Authorization: token };
|
||||
}
|
||||
|
||||
return global.HttpRequest.post("/api/auth/register", body, options).then(function (res) {
|
||||
return applyAuthSession(res, "注册失败");
|
||||
});
|
||||
}
|
||||
|
||||
function login(payload) {
|
||||
var body = {
|
||||
account: (payload.account || "").trim(),
|
||||
password: payload.password || "",
|
||||
devID: getOrCreateDevID(),
|
||||
channel: getChannel(),
|
||||
};
|
||||
return global.HttpRequest.post("/api/auth/login", body).then(function (res) {
|
||||
return applyAuthSession(res, "登录失败");
|
||||
});
|
||||
}
|
||||
|
||||
function resetPassword(payload) {
|
||||
return global.HttpRequest.post("/api/auth/resetPassword", {
|
||||
accountNo: (payload.accountNo || "").trim(),
|
||||
email: (payload.email || "").trim(),
|
||||
newPassword: payload.newPassword || "",
|
||||
});
|
||||
}
|
||||
|
||||
function guestLogin() {
|
||||
var devId = getOrCreateDevID();
|
||||
return global.HttpRequest.post("/api/auth/guest", {
|
||||
devID: devId,
|
||||
deviceId: devId,
|
||||
channel: getChannel(),
|
||||
mac: "",
|
||||
}).then(function (res) {
|
||||
return applyAuthSession(res, "游客登录失败");
|
||||
});
|
||||
}
|
||||
|
||||
function isAuthEndpoint(url) {
|
||||
return /\/api\/auth\/(guest|login|register|resetPassword)(\?|$)/.test(String(url || ""));
|
||||
}
|
||||
|
||||
function isSafeRedirect(path) {
|
||||
return typeof path === "string" && path.charAt(0) === "/" && path.charAt(1) !== "/";
|
||||
}
|
||||
|
||||
/** PC 打开登录弹窗;H5 跳转登录页 */
|
||||
function promptRegisteredRelogin() {
|
||||
toast("登录已过期,请重新登录", "warning");
|
||||
var isMobile =
|
||||
global.matchMedia && global.matchMedia("(max-width: 768px)").matches;
|
||||
if (!isMobile && typeof global.openLoginPop === "function") {
|
||||
try {
|
||||
global.openLoginPop();
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (typeof global.openLoginDialog === "function") {
|
||||
try {
|
||||
var opened = global.openLoginDialog();
|
||||
if (opened && typeof opened.then === "function") {
|
||||
opened.catch(function () {
|
||||
global.location.href = "/login";
|
||||
});
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} catch (e2) {}
|
||||
}
|
||||
global.location.href = "/login";
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口返回 code=1000(token 失效)时的统一处理。
|
||||
* - 游客:清会话后走游客登录,成功后可重试原请求
|
||||
* - 注册账号:清会话并打开登录弹窗 / 跳转登录页
|
||||
* @returns {Promise<{ok:boolean, guest?:boolean, needLogin?:boolean, res?:any}>}
|
||||
*/
|
||||
function handleTokenExpired() {
|
||||
if (tokenRecoveryPromise) {
|
||||
return tokenRecoveryPromise;
|
||||
}
|
||||
|
||||
var wasRegistered =
|
||||
global.CookieUtil && typeof global.CookieUtil.isRegistered === "function"
|
||||
? global.CookieUtil.isRegistered()
|
||||
: false;
|
||||
|
||||
if (global.CookieUtil && typeof global.CookieUtil.clearAuth === "function") {
|
||||
global.CookieUtil.clearAuth();
|
||||
}
|
||||
try {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("auth:logout", { detail: { reason: "token-expired" } })
|
||||
);
|
||||
} catch (e) {}
|
||||
|
||||
if (wasRegistered) {
|
||||
var registeredJob = Promise.resolve().then(function () {
|
||||
promptRegisteredRelogin();
|
||||
return { ok: false, needLogin: true };
|
||||
});
|
||||
tokenRecoveryPromise = registeredJob;
|
||||
registeredJob.finally(function () {
|
||||
setTimeout(function () {
|
||||
if (tokenRecoveryPromise === registeredJob) {
|
||||
tokenRecoveryPromise = null;
|
||||
}
|
||||
}, 1200);
|
||||
});
|
||||
return registeredJob;
|
||||
}
|
||||
|
||||
clearGuestLogoutFlag();
|
||||
var guestJob = guestLogin()
|
||||
.then(function (res) {
|
||||
return { ok: true, guest: true, res: res };
|
||||
})
|
||||
.catch(function (err) {
|
||||
toast("会话已失效,请刷新页面重试", "error");
|
||||
throw err;
|
||||
})
|
||||
.finally(function () {
|
||||
if (tokenRecoveryPromise === guestJob) {
|
||||
tokenRecoveryPromise = null;
|
||||
}
|
||||
});
|
||||
tokenRecoveryPromise = guestJob;
|
||||
return guestJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* 无 token(未自己登录)时自动游客登录。
|
||||
* SSR 也会在首屏补游客;此处作客户端兜底。
|
||||
* 手动退出后跳过本次;清掉标记后,下次进页再自动游客。
|
||||
* BFF: POST /api/auth/guest → 上游 POST /api/app/auth/guest
|
||||
*/
|
||||
function ensureGuestSession() {
|
||||
if (hasSessionToken()) {
|
||||
return Promise.resolve({ ok: true, skipped: true });
|
||||
}
|
||||
if (isGuestLogoutMarked()) {
|
||||
// 本次因手动退出跳过;清标记,下次进页恢复自动游客
|
||||
clearGuestLogoutFlag();
|
||||
return Promise.resolve({ ok: true, skipped: true, reason: "manual-logout" });
|
||||
}
|
||||
if (!global.HttpRequest) {
|
||||
return Promise.resolve({ ok: false, skipped: true, reason: "no-http" });
|
||||
}
|
||||
if (!guestLoginPromise) {
|
||||
guestLoginPromise = guestLogin()
|
||||
.then(function (res) {
|
||||
return { ok: true, res: res };
|
||||
})
|
||||
.catch(function (err) {
|
||||
return { ok: false, error: err };
|
||||
})
|
||||
.finally(function () {
|
||||
guestLoginPromise = null;
|
||||
});
|
||||
}
|
||||
return guestLoginPromise;
|
||||
}
|
||||
|
||||
function uploadImage(file) {
|
||||
var fd = new FormData();
|
||||
fd.append("file", file);
|
||||
return global.HttpRequest.post("/api/upload/image", fd).then(function (res) {
|
||||
if (!res || res.code !== 200 || !res.data || !res.data.url) {
|
||||
var err = new Error((res && (res.tip || res.msg)) || "上传失败");
|
||||
err.code = res && res.code;
|
||||
err.data = res;
|
||||
throw err;
|
||||
}
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
|
||||
function updateProfile(payload) {
|
||||
var body = {};
|
||||
if (payload.nickName != null && String(payload.nickName).trim() !== "") {
|
||||
body.nickName = String(payload.nickName).trim();
|
||||
}
|
||||
if (payload.introduction != null) {
|
||||
body.introduction = String(payload.introduction).trim();
|
||||
}
|
||||
if (payload.avatar) body.avatar = payload.avatar;
|
||||
if (payload.oldPassword) body.oldPassword = payload.oldPassword;
|
||||
if (payload.newPassword) body.newPassword = payload.newPassword;
|
||||
if (typeof payload.privacyMode === "boolean") body.privacyMode = payload.privacyMode;
|
||||
|
||||
return global.HttpRequest.post("/api/auth/profile", body).then(function (res) {
|
||||
if (!res || res.code !== 200) {
|
||||
var err = new Error((res && (res.tip || res.msg)) || "保存失败");
|
||||
err.code = res && res.code;
|
||||
err.data = res;
|
||||
throw err;
|
||||
}
|
||||
if (res.data && global.CookieUtil && typeof global.CookieUtil.setAuthSession === "function") {
|
||||
global.CookieUtil.setAuthSession(res.data);
|
||||
}
|
||||
if (res.data) syncTrackerUid(res.data);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || "").trim());
|
||||
}
|
||||
|
||||
/** 登录凭证:账号 4–16 位,或合法邮箱 */
|
||||
function isValidLoginAccount(account) {
|
||||
var v = String(account || "").trim();
|
||||
if (!v) return false;
|
||||
if (v.indexOf("@") >= 0) return isValidEmail(v);
|
||||
return v.length >= 4 && v.length <= 16;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
markGuestLogout();
|
||||
if (global.CookieUtil && typeof global.CookieUtil.clearAuth === "function") {
|
||||
global.CookieUtil.clearAuth();
|
||||
}
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent("auth:logout", { detail: { reason: "manual" } }));
|
||||
} catch (e) {}
|
||||
if (!global.HttpRequest) {
|
||||
return Promise.resolve({ code: 200 });
|
||||
}
|
||||
return global.HttpRequest.post("/api/auth/logout", {}).catch(function () {
|
||||
return { code: 200 };
|
||||
});
|
||||
}
|
||||
|
||||
global.AuthAPI = {
|
||||
TOKEN_EXPIRED_CODE: TOKEN_EXPIRED_CODE,
|
||||
register: register,
|
||||
login: login,
|
||||
logout: logout,
|
||||
resetPassword: resetPassword,
|
||||
guestLogin: guestLogin,
|
||||
ensureGuestSession: ensureGuestSession,
|
||||
handleTokenExpired: handleTokenExpired,
|
||||
isAuthEndpoint: isAuthEndpoint,
|
||||
markGuestLogout: markGuestLogout,
|
||||
hasSessionToken: hasSessionToken,
|
||||
uploadImage: uploadImage,
|
||||
updateProfile: updateProfile,
|
||||
getInviteCode: getInviteCode,
|
||||
getOrCreateDevID: getOrCreateDevID,
|
||||
getChannel: getChannel,
|
||||
isValidEmail: isValidEmail,
|
||||
isValidLoginAccount: isValidLoginAccount,
|
||||
toast: toast,
|
||||
};
|
||||
|
||||
// 每次页面初始化都准备好持久设备 UUID,并捕获首次渠道。
|
||||
getOrCreateDevID();
|
||||
getChannel();
|
||||
|
||||
// 首屏无 token → 自动游客登录
|
||||
ensureGuestSession();
|
||||
})(typeof window !== "undefined" ? window : this);
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 全站渠道首次归因。
|
||||
* 与落地页约定一致,按 dc、ch、pc 顺序识别,并同步到 localStorage 与 cookie。
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var STORAGE_KEY = "channel";
|
||||
var QUERY_KEYS = ["dc", "ch", "pc"];
|
||||
|
||||
function clean(value) {
|
||||
return String(value == null ? "" : value).trim();
|
||||
}
|
||||
|
||||
function readStorage() {
|
||||
try {
|
||||
return clean(global.localStorage.getItem(STORAGE_KEY));
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function writeStorage(channel) {
|
||||
try {
|
||||
global.localStorage.setItem(STORAGE_KEY, channel);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function readCookie() {
|
||||
try {
|
||||
var match = document.cookie.match(/(?:^|;\s*)channel=([^;]*)/);
|
||||
return match ? clean(decodeURIComponent(match[1])) : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function writeCookie(channel) {
|
||||
try {
|
||||
var secure = global.location && global.location.protocol === "https:" ? "; Secure" : "";
|
||||
document.cookie =
|
||||
STORAGE_KEY +
|
||||
"=" +
|
||||
encodeURIComponent(channel) +
|
||||
"; Path=/; Max-Age=31536000; SameSite=Lax" +
|
||||
secure;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function readQuery() {
|
||||
try {
|
||||
var params = new URLSearchParams(global.location.search || "");
|
||||
for (var i = 0; i < QUERY_KEYS.length; i += 1) {
|
||||
var channel = clean(params.get(QUERY_KEYS[i]));
|
||||
if (channel) return channel;
|
||||
}
|
||||
} catch (e) {}
|
||||
return "";
|
||||
}
|
||||
|
||||
function syncTracker(channel) {
|
||||
try {
|
||||
if (global.tracker && typeof global.tracker.setChannel === "function") {
|
||||
global.tracker.setChannel(channel);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function capture() {
|
||||
// localStorage 与服务端 cookie 都属于已归因结果,必须先于当前 URL。
|
||||
var channel = readStorage() || readCookie() || readQuery();
|
||||
if (!channel) return "";
|
||||
|
||||
writeStorage(channel);
|
||||
writeCookie(channel);
|
||||
global.__CHANNEL_CODE__ = channel;
|
||||
syncTracker(channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
global.ChannelAttribution = {
|
||||
get: capture,
|
||||
capture: capture,
|
||||
};
|
||||
|
||||
var channel = capture();
|
||||
try {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("channel:ready", { detail: { channel: channel } })
|
||||
);
|
||||
} catch (e) {}
|
||||
})(window);
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 播放页 / 全站轻量兼容:Chrome / Safari / 夸克 / QQ(X5) / UC / 百度等
|
||||
* - 标记 UA,便于定向样式
|
||||
* - 动态视口高度 --app-vh
|
||||
* - Element.matches / closest / Promise.finally 兜底(旧版 QQ / 夸克)
|
||||
*/
|
||||
(function () {
|
||||
var root = document.documentElement;
|
||||
var ua = (navigator && navigator.userAgent) || "";
|
||||
|
||||
var isIOS = /iP(hone|od|ad)/.test(ua);
|
||||
var isAndroid = /Android/i.test(ua);
|
||||
var isQuark = /Quark/i.test(ua);
|
||||
var isQQ = /QQ\//i.test(ua) || /MQQBrowser/i.test(ua);
|
||||
var isUC = /UCBrowser|UCWEB/i.test(ua);
|
||||
var isWeChat = /MicroMessenger/i.test(ua);
|
||||
var isBaidu = /baidubrowser|bdbrowser|BaiduHD/i.test(ua);
|
||||
var isHuawei = /HuaweiBrowser|HONORBrowser|HBPC/i.test(ua);
|
||||
var isMiui = /MiuiBrowser/i.test(ua);
|
||||
var isSogou = /SogouMobileBrowser|SogouMSE/i.test(ua);
|
||||
var isSafari =
|
||||
/Safari/i.test(ua) &&
|
||||
!/Chrome|CriOS|Chromium|Edg|Quark|MQQBrowser|UCBrowser|MicroMessenger|baidubrowser|MiuiBrowser|HuaweiBrowser|Sogou/i.test(
|
||||
ua
|
||||
);
|
||||
|
||||
// 易劫持 video 的国产浏览器:强制走页面内 DPlayer + hls.js
|
||||
var forceCustomPlayer =
|
||||
isQuark ||
|
||||
isQQ ||
|
||||
isUC ||
|
||||
isWeChat ||
|
||||
isBaidu ||
|
||||
isHuawei ||
|
||||
isMiui ||
|
||||
isSogou ||
|
||||
/VivoBrowser|HeyTapBrowser|OPPOBrowser|HeyTap|QihooBrowser|360Browser|360SE|360EE/i.test(
|
||||
ua
|
||||
);
|
||||
|
||||
root.classList.add("compat-ready");
|
||||
if (isIOS) root.classList.add("is-ios");
|
||||
if (isAndroid) root.classList.add("is-android");
|
||||
if (isQuark) root.classList.add("is-quark");
|
||||
if (isQQ) root.classList.add("is-qq");
|
||||
if (isUC) root.classList.add("is-uc");
|
||||
if (isWeChat) root.classList.add("is-wechat");
|
||||
if (isBaidu) root.classList.add("is-baidu");
|
||||
if (isHuawei) root.classList.add("is-huawei");
|
||||
if (isMiui) root.classList.add("is-miui");
|
||||
if (isSogou) root.classList.add("is-sogou");
|
||||
if (isSafari) root.classList.add("is-safari");
|
||||
if (forceCustomPlayer) root.classList.add("is-cn-browser");
|
||||
|
||||
window.BrowserCompat = {
|
||||
isIOS: isIOS,
|
||||
isAndroid: isAndroid,
|
||||
isQuark: isQuark,
|
||||
isQQ: isQQ,
|
||||
isUC: isUC,
|
||||
isWeChat: isWeChat,
|
||||
isBaidu: isBaidu,
|
||||
isHuawei: isHuawei,
|
||||
isMiui: isMiui,
|
||||
isSogou: isSogou,
|
||||
isSafari: isSafari,
|
||||
forceCustomPlayer: forceCustomPlayer,
|
||||
needsX5Inline: isQQ || isWeChat || isUC || isQuark,
|
||||
};
|
||||
|
||||
/** 点击目标可能是 Text 节点(夸克/QQ 常见),统一取 Element 再 closest */
|
||||
window.domClosest = function (from, selector) {
|
||||
var el = from;
|
||||
if (!el) return null;
|
||||
if (el.nodeType !== 1) el = el.parentElement || el.parentNode;
|
||||
if (!el || el.nodeType !== 1 || typeof el.closest !== "function") return null;
|
||||
return el.closest(selector);
|
||||
};
|
||||
|
||||
if (!Element.prototype.matches) {
|
||||
Element.prototype.matches =
|
||||
Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector ||
|
||||
function (selector) {
|
||||
var list = (this.document || this.ownerDocument).querySelectorAll(selector);
|
||||
var i = list.length;
|
||||
while (--i >= 0 && list.item(i) !== this) {}
|
||||
return i > -1;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Element.prototype.closest) {
|
||||
Element.prototype.closest = function (selector) {
|
||||
var el = this;
|
||||
while (el && el.nodeType === 1) {
|
||||
if (el.matches(selector)) return el;
|
||||
el = el.parentElement || el.parentNode;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof Promise !== "undefined" && !Promise.prototype.finally) {
|
||||
Promise.prototype.finally = function (onFinally) {
|
||||
var P = this.constructor;
|
||||
return this.then(
|
||||
function (value) {
|
||||
return P.resolve(typeof onFinally === "function" ? onFinally() : onFinally).then(
|
||||
function () {
|
||||
return value;
|
||||
}
|
||||
);
|
||||
},
|
||||
function (reason) {
|
||||
return P.resolve(typeof onFinally === "function" ? onFinally() : onFinally).then(
|
||||
function () {
|
||||
throw reason;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function setAppVh() {
|
||||
var h = window.innerHeight || 0;
|
||||
if (window.visualViewport && window.visualViewport.height) {
|
||||
h = window.visualViewport.height;
|
||||
}
|
||||
if (h > 0) {
|
||||
root.style.setProperty("--app-vh", h + "px");
|
||||
}
|
||||
}
|
||||
|
||||
setAppVh();
|
||||
window.addEventListener("resize", setAppVh, { passive: true });
|
||||
window.addEventListener("orientationchange", function () {
|
||||
setTimeout(setAppVh, 120);
|
||||
});
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener("resize", setAppVh, { passive: true });
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Cookie 读写(登录 token / sid 等统一落 cookie)
|
||||
*/
|
||||
(function (global) {
|
||||
function encode(value) {
|
||||
return encodeURIComponent(String(value == null ? "" : value));
|
||||
}
|
||||
|
||||
function decode(value) {
|
||||
try {
|
||||
// 兼容服务端历史 QueryEscape:空格被编成 "+",decodeURIComponent 不会还原
|
||||
return decodeURIComponent(String(value || "").replace(/\+/g, "%20"));
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
if (!name) return "";
|
||||
var key = encodeURIComponent(name) + "=";
|
||||
var parts = String(document.cookie || "").split(";");
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var part = parts[i].replace(/^\s+/, "");
|
||||
if (part.indexOf(key) === 0) {
|
||||
return decode(part.slice(key.length));
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {{ days?: number, path?: string, sameSite?: string, secure?: boolean }} [options]
|
||||
*/
|
||||
function setCookie(name, value, options) {
|
||||
options = options || {};
|
||||
var days = options.days;
|
||||
var path = options.path || "/";
|
||||
var sameSite = options.sameSite || "Lax";
|
||||
var parts = [
|
||||
encodeURIComponent(name) + "=" + encode(value),
|
||||
"path=" + path,
|
||||
"SameSite=" + sameSite,
|
||||
];
|
||||
if (typeof days === "number") {
|
||||
var expires = new Date();
|
||||
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
parts.push("expires=" + expires.toUTCString());
|
||||
}
|
||||
if (options.secure || (global.location && global.location.protocol === "https:")) {
|
||||
parts.push("Secure");
|
||||
}
|
||||
document.cookie = parts.join("; ");
|
||||
}
|
||||
|
||||
function removeCookie(name, path) {
|
||||
setCookie(name, "", { days: -1, path: path || "/" });
|
||||
}
|
||||
|
||||
var AUTH = {
|
||||
TOKEN: "token",
|
||||
SID: "sid",
|
||||
S_TOKEN: "_s_token_",
|
||||
USER_ID: "userId",
|
||||
ACCOUNT_NO: "accountNo",
|
||||
NICK_NAME: "nickName",
|
||||
INTRODUCTION: "introduction",
|
||||
AVATAR: "avatar",
|
||||
EMAIL: "email",
|
||||
INVITE_CODE: "inviteCode",
|
||||
RECHARGE: "recharge",
|
||||
VIP_TYPE: "vipType",
|
||||
VIP_EXPIRE: "currentVipExpire",
|
||||
SHOW_CARD_ID: "showCardId",
|
||||
SHOW_CARD_NAME: "showCardName",
|
||||
USER_TYPE: "userType",
|
||||
IS_REGISTER: "isRegister",
|
||||
};
|
||||
|
||||
var AUTH_KEYS = [
|
||||
AUTH.TOKEN,
|
||||
AUTH.USER_ID,
|
||||
AUTH.ACCOUNT_NO,
|
||||
AUTH.NICK_NAME,
|
||||
AUTH.INTRODUCTION,
|
||||
AUTH.AVATAR,
|
||||
AUTH.EMAIL,
|
||||
AUTH.INVITE_CODE,
|
||||
AUTH.RECHARGE,
|
||||
AUTH.VIP_TYPE,
|
||||
AUTH.VIP_EXPIRE,
|
||||
AUTH.SHOW_CARD_ID,
|
||||
AUTH.SHOW_CARD_NAME,
|
||||
AUTH.USER_TYPE,
|
||||
AUTH.IS_REGISTER,
|
||||
"_token_",
|
||||
AUTH.SID,
|
||||
AUTH.S_TOKEN,
|
||||
];
|
||||
|
||||
function getToken() {
|
||||
return getCookie(AUTH.TOKEN) || getCookie("_token_") || "";
|
||||
}
|
||||
|
||||
function setToken(token, days) {
|
||||
setCookie(AUTH.TOKEN, token || "", { days: typeof days === "number" ? days : 7 });
|
||||
}
|
||||
|
||||
function getSid() {
|
||||
return getCookie(AUTH.SID) || "";
|
||||
}
|
||||
|
||||
function setSid(sid, days) {
|
||||
setCookie(AUTH.SID, sid || "", { days: typeof days === "number" ? days : 7 });
|
||||
}
|
||||
|
||||
function setAuthSession(user, days) {
|
||||
if (!user) return;
|
||||
var d = typeof days === "number" ? days : 7;
|
||||
if (user.token) setToken(user.token, d);
|
||||
if (user.userId != null) setCookie(AUTH.USER_ID, String(user.userId), { days: d });
|
||||
if (user.accountNo) setCookie(AUTH.ACCOUNT_NO, user.accountNo, { days: d });
|
||||
if (user.nickName) setCookie(AUTH.NICK_NAME, user.nickName, { days: d });
|
||||
if (user.introduction != null) setCookie(AUTH.INTRODUCTION, user.introduction || "", { days: d });
|
||||
if (user.avatar != null) setCookie(AUTH.AVATAR, user.avatar || "", { days: d });
|
||||
if (user.email != null) setCookie(AUTH.EMAIL, user.email || "", { days: d });
|
||||
if (user.inviteCode != null) setCookie(AUTH.INVITE_CODE, user.inviteCode || "", { days: d });
|
||||
// 仅在响应显式带 recharge 时写入(含 0),避免局部更新把余额抹成 0
|
||||
if (Object.prototype.hasOwnProperty.call(user, "recharge")) {
|
||||
setCookie(AUTH.RECHARGE, String(Number(user.recharge) || 0), { days: d });
|
||||
}
|
||||
if (user.vipType != null) setCookie(AUTH.VIP_TYPE, String(user.vipType), { days: d });
|
||||
if (user.currentVipExpire != null) {
|
||||
setCookie(AUTH.VIP_EXPIRE, user.currentVipExpire || "", { days: d });
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(user, "showCardId")) {
|
||||
setCookie(AUTH.SHOW_CARD_ID, String(user.showCardId != null ? user.showCardId : ""), { days: d });
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(user, "showCardName")) {
|
||||
setCookie(AUTH.SHOW_CARD_NAME, user.showCardName || "", { days: d });
|
||||
}
|
||||
if (user.type != null) setCookie(AUTH.USER_TYPE, String(user.type), { days: d });
|
||||
if (Object.prototype.hasOwnProperty.call(user, "isRegister") || user.type != null) {
|
||||
var registered = !!(user.isRegister || Number(user.type) === 1);
|
||||
setCookie(AUTH.IS_REGISTER, registered ? "1" : "0", { days: d });
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthSession() {
|
||||
var token = getToken();
|
||||
if (!token) return null;
|
||||
return {
|
||||
token: token,
|
||||
userId: getCookie(AUTH.USER_ID),
|
||||
accountNo: getCookie(AUTH.ACCOUNT_NO),
|
||||
nickName: getCookie(AUTH.NICK_NAME),
|
||||
introduction: getCookie(AUTH.INTRODUCTION),
|
||||
avatar: getCookie(AUTH.AVATAR),
|
||||
email: getCookie(AUTH.EMAIL),
|
||||
inviteCode: getCookie(AUTH.INVITE_CODE),
|
||||
recharge: getCookie(AUTH.RECHARGE),
|
||||
vipType: getCookie(AUTH.VIP_TYPE),
|
||||
currentVipExpire: getCookie(AUTH.VIP_EXPIRE),
|
||||
showCardId: getCookie(AUTH.SHOW_CARD_ID),
|
||||
showCardName: getCookie(AUTH.SHOW_CARD_NAME),
|
||||
type: getCookie(AUTH.USER_TYPE),
|
||||
isRegister: getCookie(AUTH.IS_REGISTER) === "1",
|
||||
};
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
for (var i = 0; i < AUTH_KEYS.length; i++) {
|
||||
removeCookie(AUTH_KEYS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return !!getToken();
|
||||
}
|
||||
|
||||
/** 是否正式注册账号(非游客) */
|
||||
function isRegistered() {
|
||||
if (!isLoggedIn()) return false;
|
||||
if (getCookie(AUTH.IS_REGISTER) === "1") return true;
|
||||
return Number(getCookie(AUTH.USER_TYPE)) === 1;
|
||||
}
|
||||
|
||||
global.CookieUtil = {
|
||||
get: getCookie,
|
||||
set: setCookie,
|
||||
remove: removeCookie,
|
||||
AUTH: AUTH,
|
||||
getToken: getToken,
|
||||
setToken: setToken,
|
||||
getSid: getSid,
|
||||
setSid: setSid,
|
||||
setAuthSession: setAuthSession,
|
||||
getAuthSession: getAuthSession,
|
||||
clearAuth: clearAuth,
|
||||
isLoggedIn: isLoggedIn,
|
||||
isRegistered: isRegistered,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : this);
|
||||
@@ -0,0 +1,22 @@
|
||||
(function () {
|
||||
if (typeof QRCode === "undefined") return;
|
||||
|
||||
var nodes = document.querySelectorAll("[data-qr]");
|
||||
Array.prototype.forEach.call(nodes, function (el) {
|
||||
var text = (el.getAttribute("data-qr") || "").trim();
|
||||
if (!text) return;
|
||||
el.innerHTML = "";
|
||||
try {
|
||||
new QRCode(el, {
|
||||
text: text,
|
||||
width: 96,
|
||||
height: 96,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: QRCode.CorrectLevel.M,
|
||||
});
|
||||
} catch (err) {
|
||||
// ignore render errors; keep empty box
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 前端请求封装。
|
||||
* - 登录态走 cookie(credentials: include)
|
||||
* - 浏览器无法改写 User-Agent;X-Real-IP 由服务端反代上游时注入
|
||||
* - 同源业务请求携带 SDK 设备字段;短剧服务端事件额外携带完整公共字段快照
|
||||
* - token 重试复用同一份 SDK Header、埋点上下文和 request_id
|
||||
* - request_id 只用于请求关联;最终 event_id 由服务端按每条事件的最终字段生成
|
||||
* - 直连上游时若需自定义头,请走本站 Go 代理接口
|
||||
*/
|
||||
(function (global) {
|
||||
var DEFAULT_TIMEOUT_MS = 30000;
|
||||
var TRACKER_CONTEXT_TIMEOUT_MS = 1500;
|
||||
var DRAMA_SERVER_EVENT_ROUTES = {
|
||||
"/api/playlet/chapter/like/set": "drama_like",
|
||||
"/api/playlet/like/set": "drama_like",
|
||||
"/api/playlet/chapter/collect/set": "drama_collect",
|
||||
"/api/playlet/collect/set": "drama_collect",
|
||||
"/api/comment/send": "drama_comment",
|
||||
"/api/playlet/chapter/unlock": "drama_purchase",
|
||||
};
|
||||
|
||||
function randomRequestId() {
|
||||
var bytes = new Uint8Array(16);
|
||||
if (global.crypto && typeof global.crypto.getRandomValues === "function") {
|
||||
global.crypto.getRandomValues(bytes);
|
||||
return Array.prototype.map.call(bytes, function (value) {
|
||||
return (value + 256).toString(16).slice(-2);
|
||||
}).join("");
|
||||
}
|
||||
var fallback = (Date.now().toString(16) + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)).slice(0, 32);
|
||||
while (fallback.length < 32) fallback += "0";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function cleanHeaderPart(value) {
|
||||
return String(value == null ? "" : value).replace(/[^\x20-\x7E]|[;=]/g, " ").trim();
|
||||
}
|
||||
|
||||
function sameOrigin(url) {
|
||||
try {
|
||||
return new URL(url, global.location.href).origin === global.location.origin;
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function dramaServerEvent(url, method, options) {
|
||||
if (method === "GET" || method === "HEAD" || !sameOrigin(url)) return "";
|
||||
if (options.trackerEvent === false) return "";
|
||||
if (typeof options.trackerEvent === "string") {
|
||||
var explicit = options.trackerEvent.trim();
|
||||
return /^(drama_like|drama_comment|drama_collect|drama_purchase)$/.test(explicit)
|
||||
? explicit
|
||||
: "";
|
||||
}
|
||||
try {
|
||||
return DRAMA_SERVER_EVENT_ROUTES[new URL(url, global.location.href).pathname] || "";
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function commonFields(options) {
|
||||
if (options._trackerCommonFieldsPromise) return options._trackerCommonFieldsPromise;
|
||||
var getter = global.tracker && typeof global.tracker.getCommonFields === "function"
|
||||
? global.tracker.getCommonFields
|
||||
: global.WebSDK && typeof global.WebSDK.getCommonFields === "function"
|
||||
? global.WebSDK.getCommonFields.bind(global.WebSDK)
|
||||
: null;
|
||||
options._trackerCommonFieldsPromise = getter
|
||||
? Promise.resolve().then(function () { return getter(); }).catch(function () { return {}; })
|
||||
: Promise.resolve({});
|
||||
return options._trackerCommonFieldsPromise;
|
||||
}
|
||||
|
||||
function currentDramaContext(options) {
|
||||
var context;
|
||||
if (options && options.trackerContext && typeof options.trackerContext === "object") {
|
||||
context = options.trackerContext;
|
||||
} else if (global.tracker && typeof global.tracker.getDramaContext === "function") {
|
||||
context = global.tracker.getDramaContext() || {};
|
||||
} else {
|
||||
context = {};
|
||||
}
|
||||
return {
|
||||
media_id: String(context.media_id == null ? "" : context.media_id).trim(),
|
||||
drama_id: String(context.drama_id == null ? "" : context.drama_id).trim(),
|
||||
episode: Math.max(1, parseInt(context.episode, 10) || 1),
|
||||
video_id: String(context.video_id == null ? "" : context.video_id).trim(),
|
||||
recommend_trace_id: String(context.recommend_trace_id == null ? "" : context.recommend_trace_id).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function base64URLJSON(value) {
|
||||
try {
|
||||
var json = JSON.stringify(value);
|
||||
var bytes = encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, function (_, hex) {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
});
|
||||
return global.btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function trackerHeaders(url, method, options) {
|
||||
var eventName = dramaServerEvent(url, method, options);
|
||||
if (!eventName) return Promise.resolve({});
|
||||
if (!options._trackerContextPromise) {
|
||||
if (!options._trackerRequestId) options._trackerRequestId = randomRequestId();
|
||||
var drama = currentDramaContext(options);
|
||||
if (!drama || !String(drama.drama_id || "").trim()) {
|
||||
options._trackerContextPromise = Promise.resolve({});
|
||||
} else {
|
||||
options._trackerContextPromise = commonFields(options).then(function (fields) {
|
||||
var packedContext = base64URLJSON({
|
||||
v: 1,
|
||||
request_id: options._trackerRequestId,
|
||||
event_name: eventName,
|
||||
common: fields,
|
||||
drama: drama,
|
||||
});
|
||||
if (!packedContext) return {};
|
||||
var result = {
|
||||
"X-Tracker-Request-Id": options._trackerRequestId,
|
||||
"X-Tracker-Event-Name": eventName,
|
||||
"X-DC-Request-Context": packedContext,
|
||||
};
|
||||
result["X-User-Agent"] = [
|
||||
"DevID=" + cleanHeaderPart(fields.device_id),
|
||||
"DevType=" + cleanHeaderPart(fields.device || "OTHER"),
|
||||
"SysType=" + (fields.device === "Android" ? "h5_android" : fields.device === "iOS" ? "h5_ios" : "h5_pc"),
|
||||
"Ver=" + cleanHeaderPart(fields.app_version || "1.0.0"),
|
||||
"BuildID=web",
|
||||
"device_brand=" + cleanHeaderPart(fields.device_brand),
|
||||
"device_model=" + cleanHeaderPart(fields.device_model),
|
||||
"sid=" + cleanHeaderPart(fields.sid),
|
||||
].join(";");
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
var settled = false;
|
||||
var timer = global.setTimeout(function () {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve({});
|
||||
}, TRACKER_CONTEXT_TIMEOUT_MS);
|
||||
options._trackerContextPromise.then(function (headers) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
global.clearTimeout(timer);
|
||||
resolve(headers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildQuery(params) {
|
||||
if (!params) return "";
|
||||
var pairs = [];
|
||||
Object.keys(params).forEach(function (key) {
|
||||
var value = params[key];
|
||||
if (value == null) return;
|
||||
pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value)));
|
||||
});
|
||||
return pairs.length ? "?" + pairs.join("&") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {{
|
||||
* method?: string,
|
||||
* data?: any,
|
||||
* params?: Record<string, any>,
|
||||
* headers?: Record<string, string>,
|
||||
* timeout?: number,
|
||||
* trackerEvent?: false|string,
|
||||
* trackerContext?: Record<string, any>,
|
||||
* }} [options]
|
||||
*/
|
||||
function request(url, options) {
|
||||
options = options || {};
|
||||
var method = String(options.method || "GET").toUpperCase();
|
||||
var headers = Object.assign(
|
||||
{ Accept: "application/json" },
|
||||
options.headers || {}
|
||||
);
|
||||
var finalUrl = url + buildQuery(options.params);
|
||||
var body;
|
||||
var controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
||||
var timer;
|
||||
|
||||
if (options.data != null && method !== "GET" && method !== "HEAD") {
|
||||
if (typeof FormData !== "undefined" && options.data instanceof FormData) {
|
||||
body = options.data;
|
||||
} else {
|
||||
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
||||
body =
|
||||
headers["Content-Type"].indexOf("application/json") >= 0
|
||||
? JSON.stringify(options.data)
|
||||
: options.data;
|
||||
}
|
||||
}
|
||||
|
||||
var timeoutMs =
|
||||
typeof options.timeout === "number" ? options.timeout : DEFAULT_TIMEOUT_MS;
|
||||
|
||||
function sdkCommonHeader() {
|
||||
if (headers["X-SDK-Common"] || options._skipSDKCommon || !sameOrigin(finalUrl)) {
|
||||
return Promise.resolve("");
|
||||
}
|
||||
if (!options._sdkCommonHeaderPromise) {
|
||||
options._sdkCommonHeaderPromise = Promise.resolve()
|
||||
.then(function () {
|
||||
if (
|
||||
global.tracker &&
|
||||
typeof global.tracker.getSDKCommonHeader === "function"
|
||||
) {
|
||||
return global.tracker.getSDKCommonHeader();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.catch(function () { return ""; });
|
||||
}
|
||||
return options._sdkCommonHeaderPromise;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
trackerHeaders(finalUrl, method, options).catch(function () { return {}; }),
|
||||
sdkCommonHeader(),
|
||||
])
|
||||
.then(function (prepared) {
|
||||
var tracking = prepared[0] || {};
|
||||
Object.keys(tracking).forEach(function (key) {
|
||||
if (headers[key] == null) headers[key] = tracking[key];
|
||||
});
|
||||
if (!headers["X-SDK-Common"] && prepared[1]) {
|
||||
headers["X-SDK-Common"] = prepared[1];
|
||||
}
|
||||
var fetchOpts = {
|
||||
method: method,
|
||||
headers: headers,
|
||||
credentials: "include",
|
||||
signal: controller ? controller.signal : undefined,
|
||||
};
|
||||
if (body != null) fetchOpts.body = body;
|
||||
if (controller && timeoutMs > 0) {
|
||||
timer = setTimeout(function () {
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
}
|
||||
return fetch(finalUrl, fetchOpts);
|
||||
})
|
||||
.then(function (res) {
|
||||
var contentType = res.headers.get("content-type") || "";
|
||||
var parse = contentType.indexOf("application/json") >= 0
|
||||
? res.json()
|
||||
: res.text();
|
||||
return parse.then(function (data) {
|
||||
if (!res.ok) {
|
||||
var err = new Error(
|
||||
(data && data.msg) || (data && data.message) || res.statusText || "request failed"
|
||||
);
|
||||
err.status = res.status;
|
||||
err.code = data && data.code;
|
||||
err.data = data;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
})
|
||||
.then(function (data) {
|
||||
return handleTokenExpiredResponse(url, options, data);
|
||||
})
|
||||
.finally(function () {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务码 1000:token 失效。
|
||||
* 游客 → 重新游客登录并重试一次;注册账号 → 打开登录弹窗/跳转登录页。
|
||||
*/
|
||||
function handleTokenExpiredResponse(url, options, data) {
|
||||
options = options || {};
|
||||
var expiredCode =
|
||||
(global.AuthAPI && global.AuthAPI.TOKEN_EXPIRED_CODE) || 1000;
|
||||
if (!data || Number(data.code) !== Number(expiredCode)) {
|
||||
return data;
|
||||
}
|
||||
if (options._skipTokenRecovery || options._tokenRetried) {
|
||||
return data;
|
||||
}
|
||||
if (
|
||||
global.AuthAPI &&
|
||||
typeof global.AuthAPI.isAuthEndpoint === "function" &&
|
||||
global.AuthAPI.isAuthEndpoint(url)
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
if (!global.AuthAPI || typeof global.AuthAPI.handleTokenExpired !== "function") {
|
||||
return data;
|
||||
}
|
||||
return global.AuthAPI.handleTokenExpired().then(function (result) {
|
||||
if (result && result.ok && result.guest) {
|
||||
return request(
|
||||
url,
|
||||
Object.assign({}, options, { _tokenRetried: true })
|
||||
);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
global.HttpRequest = {
|
||||
request: request,
|
||||
get: function (url, options) {
|
||||
return request(url, Object.assign({}, options, { method: "GET" }));
|
||||
},
|
||||
post: function (url, data, options) {
|
||||
return request(url, Object.assign({}, options, { method: "POST", data: data }));
|
||||
},
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : this);
|
||||
@@ -0,0 +1,67 @@
|
||||
(function () {
|
||||
var buttons = document.querySelectorAll("[data-share-page]");
|
||||
if (!buttons.length) return;
|
||||
|
||||
var RESET_MS = 2000;
|
||||
|
||||
function fallbackCopy(text) {
|
||||
var ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.setAttribute("readonly", "");
|
||||
ta.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:0;opacity:0;";
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
ta.setSelectionRange(0, ta.value.length);
|
||||
var ok = false;
|
||||
try {
|
||||
ok = document.execCommand("copy");
|
||||
} catch (_) {
|
||||
ok = false;
|
||||
}
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
return navigator.clipboard.writeText(text).catch(function () {
|
||||
fallbackCopy(text);
|
||||
});
|
||||
}
|
||||
fallbackCopy(text);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function showCopied(btn, label, original) {
|
||||
if (label) {
|
||||
label.textContent = "链接已复制";
|
||||
label.classList.add("is-copied");
|
||||
}
|
||||
btn.classList.add("is-share-copied");
|
||||
btn.setAttribute("aria-label", "链接已复制");
|
||||
if (typeof window.showToast === "function") {
|
||||
window.showToast("链接已复制", "success");
|
||||
}
|
||||
if (btn._shareResetTimer) window.clearTimeout(btn._shareResetTimer);
|
||||
btn._shareResetTimer = window.setTimeout(function () {
|
||||
if (label) {
|
||||
label.textContent = original;
|
||||
label.classList.remove("is-copied");
|
||||
}
|
||||
btn.classList.remove("is-share-copied");
|
||||
btn.setAttribute("aria-label", original || "分享");
|
||||
btn._shareResetTimer = 0;
|
||||
}, RESET_MS);
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(buttons, function (btn) {
|
||||
var label = btn.querySelector("[data-share-page-label]");
|
||||
var original = (label && label.textContent) || btn.getAttribute("aria-label") || "分享";
|
||||
btn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
showCopied(btn, label, original);
|
||||
copyText(window.location.href);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,90 @@
|
||||
(function (global) {
|
||||
var root = document.querySelector("[data-toast]");
|
||||
if (!root) return;
|
||||
|
||||
var panel = root.querySelector("[data-toast-panel]");
|
||||
var textEl = root.querySelector("[data-toast-text]");
|
||||
var iconSuccess = root.querySelector("[data-toast-icon-success]");
|
||||
var iconError = root.querySelector("[data-toast-icon-error]");
|
||||
var hideTimer = 0;
|
||||
var removeTimer = 0;
|
||||
var DEFAULT_MS = 2600;
|
||||
|
||||
function normalizeType(type) {
|
||||
var t = String(type || "").toLowerCase();
|
||||
if (t === "success" || t === "ok") return "success";
|
||||
// 失败态:error / fail / warning 等非成功统一走失败样式
|
||||
return "error";
|
||||
}
|
||||
|
||||
function clearTimers() {
|
||||
if (hideTimer) {
|
||||
window.clearTimeout(hideTimer);
|
||||
hideTimer = 0;
|
||||
}
|
||||
if (removeTimer) {
|
||||
window.clearTimeout(removeTimer);
|
||||
removeTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
root.classList.remove("is-visible");
|
||||
removeTimer = window.setTimeout(function () {
|
||||
root.setAttribute("hidden", "");
|
||||
root.classList.remove("is-success", "is-error");
|
||||
if (textEl) textEl.textContent = "";
|
||||
if (iconSuccess) {
|
||||
iconSuccess.setAttribute("hidden", "");
|
||||
iconSuccess.style.display = "none";
|
||||
}
|
||||
if (iconError) {
|
||||
iconError.setAttribute("hidden", "");
|
||||
iconError.style.display = "none";
|
||||
}
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function showToast(message, type, duration) {
|
||||
var tip = String(message == null ? "" : message).trim();
|
||||
if (!tip || !panel || !textEl) return;
|
||||
|
||||
var kind = normalizeType(type);
|
||||
clearTimers();
|
||||
|
||||
root.classList.remove("is-success", "is-error", "is-visible");
|
||||
root.classList.add(kind === "success" ? "is-success" : "is-error");
|
||||
textEl.textContent = tip;
|
||||
|
||||
if (iconSuccess) {
|
||||
if (kind === "success") {
|
||||
iconSuccess.removeAttribute("hidden");
|
||||
iconSuccess.style.display = "";
|
||||
} else {
|
||||
iconSuccess.setAttribute("hidden", "");
|
||||
iconSuccess.style.display = "none";
|
||||
}
|
||||
}
|
||||
if (iconError) {
|
||||
if (kind === "error") {
|
||||
iconError.removeAttribute("hidden");
|
||||
iconError.style.display = "";
|
||||
} else {
|
||||
iconError.setAttribute("hidden", "");
|
||||
iconError.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
root.removeAttribute("hidden");
|
||||
// 下一帧再加 visible,保证过渡动画生效
|
||||
window.requestAnimationFrame(function () {
|
||||
root.classList.add("is-visible");
|
||||
});
|
||||
|
||||
var ms = Number(duration);
|
||||
if (!ms || ms < 800) ms = DEFAULT_MS;
|
||||
hideTimer = window.setTimeout(hide, ms);
|
||||
}
|
||||
|
||||
global.showToast = showToast;
|
||||
})(window);
|
||||
Reference in New Issue
Block a user