Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-09-29 11:47:47 +07:00
co-authored by Claude Opus 5.5
commit 0fbdba5ccf
622 changed files with 72105 additions and 0 deletions
+515
View File
@@ -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);
+92
View File
@@ -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);
+143
View File
@@ -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 });
}
})();
+204
View File
@@ -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);
+22
View File
@@ -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
}
});
})();
+325
View File
@@ -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);
+67
View File
@@ -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);
});
});
})();
+90
View File
@@ -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);