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);
+55
View File
@@ -0,0 +1,55 @@
(function () {
var root = document.querySelector("[data-eighteen]");
if (!root) return;
var key = "hg_age_ok";
var enterBtn = root.querySelector("[data-eighteen-enter]");
var exitBtn = root.querySelector("[data-eighteen-exit]");
function alreadyOk() {
try {
return window.localStorage.getItem(key) === "1";
} catch (e) {
return false;
}
}
function close() {
var wasOpen = root.classList.contains("is-open");
root.classList.remove("is-open");
document.body.classList.remove("eighteen-locked");
document.documentElement.classList.add("eighteen-ok");
if (wasOpen) {
document.dispatchEvent(new CustomEvent("eighteen:closed"));
}
}
function open() {
root.classList.add("is-open");
document.body.classList.add("eighteen-locked");
}
// 已确认过:不展示
if (alreadyOk() || document.documentElement.classList.contains("eighteen-ok")) {
close();
return;
}
// 首次进入:展示
open();
if (enterBtn) {
enterBtn.addEventListener("click", function () {
try {
window.localStorage.setItem(key, "1");
} catch (e) {}
close();
});
}
if (exitBtn) {
exitBtn.addEventListener("click", function () {
window.location.href = "https://www.google.com";
});
}
})();
+56
View File
@@ -0,0 +1,56 @@
(() => {
const root = document.querySelector('[data-favorites]');
const manage = root?.querySelector('[data-favorites-manage]');
if (!manage) return;
manage.addEventListener('click', () => {
const active = document.body.classList.toggle('is-managing');
manage.setAttribute('aria-pressed', String(active));
manage.querySelector('span').textContent = active ? '完成' : '管理';
});
root.querySelectorAll('[data-favorites-remove]').forEach(button => {
button.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
if (button.disabled) return;
button.disabled = true;
const kind = button.dataset.kind || 'short';
const id = Number(button.dataset.id);
let url = '/api/playlet/collect/set';
let body = {id, isCollect: false};
if (kind === 'chapter') {
url = '/api/playlet/chapter/collect/set';
body = {chapterId: id, isCollect: false};
} else if (kind === 'series') {
url = '/api/video/collect/set';
body = {id, isCollect: false};
}
const trackerContext = kind === 'series' ? null : {
media_id: '',
drama_id: String(button.dataset.dramaId || ''),
drama_title: String(button.dataset.dramaTitle || ''),
episode: Math.max(1, Number(button.dataset.episode) || 1),
total_episode: Math.max(1, Number(button.dataset.totalEpisode) || 1),
video_id: String(button.dataset.videoId || ''),
drama_content_type: 'short_drama',
recommend_trace_id: '',
};
try {
if (!window.HttpRequest || typeof window.HttpRequest.post !== 'function') {
throw new Error('请求服务未就绪');
}
const result = await window.HttpRequest.post(
url,
body,
trackerContext ? {trackerContext} : undefined,
);
if (!result || Number(result.code) !== 200) throw new Error((result && (result.tip || result.msg)) || '取消收藏失败');
window.location.reload();
} catch (_) {
button.disabled = false;
window.alert('操作失败,请稍后重试');
}
});
});
})();
+111
View File
@@ -0,0 +1,111 @@
(() => {
const root = document.querySelector('[data-favorites]');
if (!root) return;
const cards = Array.from(root.querySelectorAll('[data-favorites-card]'));
const filters = Array.from(root.querySelectorAll('[data-favorites-filter]'));
const manage = root.querySelector('[data-favorites-manage]');
const manageLabel = manage ? manage.querySelector('span') : null;
const toolbar = root.querySelector('[data-favorites-toolbar]');
const grid = root.querySelector('[data-favorites-grid]');
const empty = root.querySelector('[data-favorites-empty]');
const filterEmpty = root.querySelector('[data-favorites-filter-empty]');
const recommendations = root.querySelector('[data-favorites-recommendations]');
const loadMore = root.querySelector('[data-favorites-more]');
const pagination = root.querySelector('[data-favorites-pagination]');
const pages = root.querySelector('[data-favorites-pages]');
const previous = root.querySelector('[data-favorites-prev]');
const next = root.querySelector('[data-favorites-next]');
const count = root.querySelector('[data-favorites-count]');
const mobile = window.matchMedia('(max-width: 768px)');
let activeFilter = 'all';
let currentPage = 1;
let mobileVisible = 9;
function matches(card) {
if (card.dataset.removed === 'true') return false;
if (activeFilter === 'following') return card.dataset.following === 'true';
if (activeFilter === 'complete') return card.dataset.complete === 'true';
return activeFilter === 'all' || card.dataset.kind === activeFilter;
}
function render() {
const hasSavedWorks = cards.some(card => card.dataset.removed !== 'true');
const collectionEmpty = !hasSavedWorks;
const filtered = cards.filter(matches);
const pageCount = Math.ceil(filtered.length / 12);
currentPage = Math.min(currentPage, Math.max(1, pageCount));
const start = mobile.matches ? 0 : (currentPage - 1) * 12;
const end = mobile.matches ? mobileVisible : start + 12;
cards.forEach(card => { card.hidden = true; });
filtered.slice(start, end).forEach(card => { card.hidden = false; });
root.classList.toggle('is-empty', collectionEmpty);
toolbar.hidden = collectionEmpty;
grid.hidden = collectionEmpty;
empty.hidden = !collectionEmpty;
if (recommendations) recommendations.hidden = !collectionEmpty;
filterEmpty.hidden = collectionEmpty || filtered.length !== 0;
if (collectionEmpty) {
document.body.classList.remove('is-managing');
if (manage) manage.setAttribute('aria-pressed', 'false');
if (manageLabel) manageLabel.textContent = '管理';
}
loadMore.hidden = filtered.length === 0 || mobileVisible >= filtered.length;
pagination.hidden = filtered.length === 0;
count.textContent = `共 ${filtered.length} 部`;
previous.disabled = currentPage <= 1;
next.disabled = currentPage >= pageCount;
pages.replaceChildren();
for (let page = 1; page <= pageCount; page++) {
const button = document.createElement('button');
button.className = `favorites-pagination__button${page === currentPage ? ' is-active' : ''}`;
button.type = 'button';
button.textContent = String(page);
button.dataset.favoritesPage = String(page);
if (page === currentPage) button.setAttribute('aria-current', 'page');
button.addEventListener('click', () => {
currentPage = page;
render();
});
pages.append(button);
}
}
filters.forEach(filter => filter.addEventListener('click', () => {
activeFilter = filter.dataset.favoritesFilter;
currentPage = 1;
mobileVisible = 9;
filters.forEach(option => {
const selected = option === filter;
option.classList.toggle('is-active', selected);
option.setAttribute('aria-pressed', String(selected));
});
render();
}));
if (manage) manage.addEventListener('click', () => {
const managing = document.body.classList.toggle('is-managing');
manage.setAttribute('aria-pressed', String(managing));
manageLabel.textContent = managing ? '完成' : '管理';
});
root.querySelectorAll('[data-favorites-remove]').forEach(button => button.addEventListener('click', () => {
button.closest('[data-favorites-card]').dataset.removed = 'true';
render();
}));
loadMore.addEventListener('click', () => {
mobileVisible += 9;
render();
});
previous.addEventListener('click', () => {
if (currentPage > 1) { currentPage--; render(); }
});
next.addEventListener('click', () => {
if (currentPage < Math.ceil(cards.filter(matches).length / 12)) { currentPage++; render(); }
});
mobile.addEventListener('change', render);
render();
})();
+633
View File
@@ -0,0 +1,633 @@
(function () {
var header = document.querySelector(".site-header");
var drawer = document.querySelector("[data-drawer]");
var openBtn = document.querySelector("[data-drawer-open]");
var userMenu = document.querySelector("[data-user-menu]");
if (header) {
var threshold = 48;
var ticking = false;
function measure() {
document.documentElement.style.setProperty("--site-header-height", header.getBoundingClientRect().height + "px");
}
function update() {
ticking = false;
header.classList.toggle("is-scrolled", window.scrollY > threshold);
}
function onScroll() {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(update);
}
measure();
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", measure);
if ("ResizeObserver" in window) new ResizeObserver(measure).observe(header);
}
// 用户下拉菜单
if (userMenu) {
var toggle = userMenu.querySelector("[data-user-menu-toggle]");
function openUserMenu() {
userMenu.classList.add("is-open");
if (toggle) toggle.setAttribute("aria-expanded", "true");
}
function closeUserMenu() {
userMenu.classList.remove("is-open");
if (toggle) toggle.setAttribute("aria-expanded", "false");
}
function toggleUserMenu(e) {
e.stopPropagation();
if (userMenu.classList.contains("is-open")) {
closeUserMenu();
} else {
openUserMenu();
}
}
if (toggle) {
toggle.addEventListener("click", toggleUserMenu);
}
document.addEventListener("click", function (e) {
if (!userMenu.contains(e.target)) {
closeUserMenu();
}
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeUserMenu();
});
}
// 左侧抽屉
if (drawer && openBtn) {
var closeEls = drawer.querySelectorAll("[data-drawer-close]");
function openDrawer() {
drawer.classList.add("is-open");
document.body.classList.add("drawer-open");
openBtn.setAttribute("aria-expanded", "true");
}
function closeDrawer() {
drawer.classList.remove("is-open");
document.body.classList.remove("drawer-open");
openBtn.setAttribute("aria-expanded", "false");
}
openBtn.setAttribute("aria-expanded", "false");
openBtn.setAttribute("aria-controls", "site-drawer");
openBtn.addEventListener("click", openDrawer);
closeEls.forEach(function (el) {
el.addEventListener("click", closeDrawer);
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && drawer.classList.contains("is-open")) {
closeDrawer();
}
});
}
function openAccountSwitch(event) {
if (event) event.preventDefault();
if (userMenu) {
userMenu.classList.remove("is-open");
var userToggle = userMenu.querySelector("[data-user-menu-toggle]");
if (userToggle) userToggle.setAttribute("aria-expanded", "false");
}
if (drawer) {
drawer.classList.remove("is-open");
document.body.classList.remove("drawer-open");
if (openBtn) openBtn.setAttribute("aria-expanded", "false");
}
if (typeof window.openAccountSwitchLogin === "function") {
window.openAccountSwitchLogin();
return;
}
if (typeof window.openLoginPop === "function") {
window.openLoginPop();
return;
}
window.location.href = "/login";
}
document.querySelectorAll("[data-account-switch]").forEach(function (btn) {
btn.addEventListener("click", openAccountSwitch);
});
// 搜索表单:走路径 /search/{q},不用 query;同时写入本地搜索历史
var SEARCH_HISTORY_KEY = "hgdj_search_history";
var SEARCH_HISTORY_MAX = 10;
function searchResultsPath(q) {
q = String(q || "").trim();
if (!q) return "/search";
return "/search/" + encodeURIComponent(q).replace(/%20/g, "%20");
}
function readSearchHistory() {
try {
var raw = window.localStorage.getItem(SEARCH_HISTORY_KEY);
var list = raw ? JSON.parse(raw) : [];
if (!Array.isArray(list)) return [];
return list
.map(function (item) {
return String(item || "").trim();
})
.filter(Boolean)
.slice(0, SEARCH_HISTORY_MAX);
} catch (_) {
return [];
}
}
function writeSearchHistory(list) {
try {
window.localStorage.setItem(
SEARCH_HISTORY_KEY,
JSON.stringify(list.slice(0, SEARCH_HISTORY_MAX))
);
} catch (_) {
/* ignore quota / private mode */
}
}
function pushSearchHistory(q) {
q = String(q || "").trim();
if (!q) return;
var list = readSearchHistory().filter(function (item) {
return item !== q;
});
list.unshift(q);
writeSearchHistory(list);
}
function clearSearchHistory() {
try {
window.localStorage.removeItem(SEARCH_HISTORY_KEY);
} catch (_) {
/* ignore */
}
}
document.querySelectorAll("form[data-path-search]").forEach(function (form) {
form.addEventListener("submit", function (event) {
event.preventDefault();
var input = form.querySelector('input[type="search"], input[name="q"]');
var q = input ? String(input.value || "").trim() : "";
if (!q) {
if (input) input.focus();
return;
}
pushSearchHistory(q);
window.location.href = searchResultsPath(q);
});
var input = form.querySelector("[data-header-search-input], input[type='search'], input[name='q']");
var clear = form.querySelector("[data-header-search-clear]");
if (input && clear) {
function syncClear() {
clear.hidden = !String(input.value || "").trim();
}
input.addEventListener("input", syncClear);
clear.addEventListener("click", function () {
input.value = "";
syncClear();
input.dispatchEvent(new Event("input", { bubbles: true }));
input.focus();
});
syncClear();
}
// PC header 搜索下拉:聚焦展开历史 + 热搜;输入时切换联想
var dropdown = form.querySelector("[data-header-search-dropdown]");
if (!dropdown || !input) return;
var historyBox = form.querySelector("[data-header-search-history]");
var historyChips = form.querySelector("[data-header-history-chips]");
var historyClear = form.querySelector("[data-header-history-clear]");
var idleBox = form.querySelector("[data-header-search-idle]");
var suggestBox = form.querySelector("[data-header-search-suggest]");
var closeTimer = null;
var suggestTimer = null;
var suggestSeq = 0;
var PLACEHOLDER_IMG = "/static/images/placeholder.png?v=2";
function escapeHtml(text) {
return String(text || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function highlightHTML(text, keyword) {
var raw = String(text || "");
var key = String(keyword || "").trim();
if (!raw) return "";
if (!key) return escapeHtml(raw);
var lowerRaw = raw.toLowerCase();
var lowerKey = key.toLowerCase();
var idx = lowerRaw.indexOf(lowerKey);
if (idx < 0) return escapeHtml(raw);
var before = raw.slice(0, idx);
var hit = raw.slice(idx, idx + key.length);
var after = raw.slice(idx + key.length);
return (
escapeHtml(before) +
'<span class="is-hit">' +
escapeHtml(hit) +
"</span>" +
escapeHtml(after)
);
}
function renderHistory() {
if (!historyBox || !historyChips) return;
var list = readSearchHistory();
historyChips.innerHTML = "";
if (!list.length) {
historyBox.hidden = true;
return;
}
historyBox.hidden = false;
list.forEach(function (term) {
var link = document.createElement("a");
link.className = "search-dropdown__chip";
link.href = searchResultsPath(term);
link.textContent = term;
historyChips.appendChild(link);
});
}
function showIdlePanel() {
if (idleBox) idleBox.hidden = false;
if (suggestBox) {
suggestBox.hidden = true;
suggestBox.innerHTML = "";
}
}
function showSuggestPanel() {
if (idleBox) idleBox.hidden = true;
if (suggestBox) suggestBox.hidden = false;
}
function renderSuggest(data, keyword) {
if (!suggestBox) return;
var html = "";
var feature = data && data.feature;
var items = (data && data.items) || [];
if (feature && feature.url) {
html +=
'<article class="search-dropdown__feature">' +
'<a class="search-dropdown__feature-cover" href="' +
escapeHtml(feature.url) +
'">' +
'<img decoding="async" loading="lazy" src="' +
PLACEHOLDER_IMG +
'" data-src="' +
escapeHtml(feature.cover || "") +
'" alt="' +
escapeHtml(feature.title || "") +
'">' +
"</a>" +
'<div class="search-dropdown__feature-copy">' +
'<h2 class="search-dropdown__feature-title">' +
highlightHTML(feature.title, keyword) +
"</h2>" +
(feature.meta
? '<p class="search-dropdown__feature-meta">' + escapeHtml(feature.meta) + "</p>"
: "") +
"</div>" +
'<a class="search-dropdown__feature-play" href="' +
escapeHtml(feature.url) +
'">' +
'<img decoding="async" loading="lazy" src="' +
PLACEHOLDER_IMG +
'" data-src="/static/images/play_hollow.svg" alt="播放">播放</a>' +
"</article>";
}
if (items.length) {
html += '<div class="search-dropdown__suggest-list">';
items.forEach(function (item) {
if (!item || !item.url) return;
html +=
'<a class="search-dropdown__suggest-item" href="' +
escapeHtml(item.url) +
'" data-suggest-query="' +
escapeHtml(item.query || item.title || keyword) +
'">' +
'<img decoding="async" loading="lazy" class="search-dropdown__suggest-icon" src="' +
PLACEHOLDER_IMG +
'" data-src="/static/images/search.svg" alt="">' +
'<span class="search-dropdown__suggest-text">' +
highlightHTML(item.title || item.query || "", keyword) +
"</span>" +
'<span class="search-dropdown__suggest-type">' +
escapeHtml(item.kind || "") +
"</span>" +
"</a>";
});
html += "</div>";
} else if (!feature) {
html +=
'<p class="search-dropdown__suggest-empty">暂无相关结果,回车搜索“' +
escapeHtml(keyword) +
'”</p>';
}
suggestBox.innerHTML = html;
showSuggestPanel();
if (typeof window.initLazyImages === "function") {
window.initLazyImages(suggestBox, { eager: true });
}
}
function fetchSuggest(keyword) {
var q = String(keyword || "").trim();
if (!q || !suggestBox) {
showIdlePanel();
openDropdown();
return;
}
var seq = ++suggestSeq;
showSuggestPanel();
suggestBox.innerHTML =
'<p class="search-dropdown__suggest-empty">搜索中…</p>';
openDropdown();
var req;
if (window.HttpRequest && typeof window.HttpRequest.get === "function") {
req = window.HttpRequest.get("/api/web/search/suggest", {
params: { keyword: q },
});
} else {
req = fetch(
"/api/web/search/suggest?keyword=" + encodeURIComponent(q),
{ credentials: "include", headers: { Accept: "application/json" } }
).then(function (res) {
return res.json();
});
}
Promise.resolve(req)
.then(function (data) {
if (seq !== suggestSeq) return;
renderSuggest(data || {}, q);
})
.catch(function () {
if (seq !== suggestSeq) return;
renderSuggest(
{
feature: null,
items: [
{
title: q,
kind: "搜索",
query: q,
url: searchResultsPath(q),
},
],
},
q
);
});
}
function scheduleSuggest() {
var q = String(input.value || "").trim();
if (suggestTimer) clearTimeout(suggestTimer);
if (!q) {
suggestSeq += 1;
showIdlePanel();
openDropdown();
return;
}
suggestTimer = setTimeout(function () {
suggestTimer = null;
fetchSuggest(q);
}, 220);
}
function openDropdown() {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
var q = String(input.value || "").trim();
if (q) {
if (suggestBox && suggestBox.hidden) {
scheduleSuggest();
}
dropdown.hidden = false;
form.classList.add("is-open");
input.setAttribute("aria-expanded", "true");
return;
}
showIdlePanel();
renderHistory();
var hasHistory = historyBox && !historyBox.hidden;
var hasHot = !!(idleBox && idleBox.querySelector(".search-dropdown__hot"));
if (!hasHistory && !hasHot) {
dropdown.hidden = true;
form.classList.remove("is-open");
input.setAttribute("aria-expanded", "false");
return;
}
dropdown.hidden = false;
form.classList.add("is-open");
input.setAttribute("aria-expanded", "true");
}
function closeDropdown() {
dropdown.hidden = true;
form.classList.remove("is-open");
input.setAttribute("aria-expanded", "false");
}
function scheduleClose() {
if (closeTimer) clearTimeout(closeTimer);
closeTimer = setTimeout(function () {
closeTimer = null;
if (!form.contains(document.activeElement)) {
closeDropdown();
}
}, 120);
}
input.addEventListener("focus", openDropdown);
input.addEventListener("click", openDropdown);
input.addEventListener("input", function () {
if (clear) clear.hidden = !String(input.value || "").trim();
scheduleSuggest();
});
input.addEventListener("blur", scheduleClose);
dropdown.addEventListener("mousedown", function (event) {
// 点击下拉内链接/按钮时先阻止 input blur 抢先关闭
event.preventDefault();
});
dropdown.addEventListener("click", function (event) {
var chip = event.target.closest
? event.target.closest(
".search-dropdown__chip, .search-dropdown__link, .search-dropdown__feature-play, .search-dropdown__feature-cover, .search-dropdown__suggest-item"
)
: null;
if (chip && chip.href) {
var q = "";
if (chip.classList.contains("search-dropdown__chip")) {
q = String(chip.textContent || "").trim();
} else if (chip.classList.contains("search-dropdown__link")) {
var name = chip.querySelector(".search-dropdown__name");
if (name) q = String(name.textContent || "").trim();
} else if (chip.getAttribute("data-suggest-query")) {
q = String(chip.getAttribute("data-suggest-query") || "").trim();
} else {
q = String(input.value || "").trim();
}
if (q) pushSearchHistory(q);
window.location.href = chip.href;
}
});
if (historyClear) {
historyClear.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
clearSearchHistory();
renderHistory();
openDropdown();
input.focus();
});
}
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && !dropdown.hidden) {
closeDropdown();
input.blur();
}
});
document.addEventListener("mousedown", function (event) {
if (dropdown.hidden) return;
if (form.contains(event.target)) return;
closeDropdown();
});
});
var PLACEHOLDER = "/static/images/placeholder.png?v=2";
var DEFAULT_AVATAR = "/static/images/profile/avatar.png";
function usableAvatar(url) {
var u = String(url || "").trim();
if (!u) return "";
if (/^https?:\/\//i.test(u) || u.indexOf("/static/") === 0) return u;
return "";
}
function setAvatarImg(img, url) {
if (!img) return;
var src = usableAvatar(url);
img.removeAttribute("data-decrypted");
img.removeAttribute("data-decrypt-failed");
if (src && typeof window.shouldDecryptImage === "function" && window.shouldDecryptImage(src)) {
img.classList.add("lazy");
img.src = PLACEHOLDER;
img.setAttribute("data-src", src);
img.setAttribute("data-lazy-eager", "1");
} else {
img.classList.remove("lazy");
img.removeAttribute("data-src");
img.removeAttribute("data-lazy-eager");
img.src = src || DEFAULT_AVATAR;
}
}
function refreshAvatarDecrypt(root) {
var scope = root || document;
if (typeof window.initLazyImages === "function") {
window.initLazyImages(scope, { eager: true });
return;
}
if (typeof window.loadLazyNode !== "function") return;
scope
.querySelectorAll(
"img.user-chip__avatar.lazy[data-src], img.user-menu__avatar.lazy[data-src], img.drawer__avatar.lazy[data-src], img.play-composer__avatar-img.lazy[data-src]"
)
.forEach(function (img) {
window.loadLazyNode(img);
});
}
function formatRecharge(raw) {
var n = Math.floor(Number(raw));
if (!isFinite(n) || n < 0) n = 0;
var s = String(n);
if (s.length <= 3) return s;
var out = [];
var rem = s.length % 3;
if (rem) out.push(s.slice(0, rem));
for (var i = rem; i < s.length; i += 3) out.push(s.slice(i, i + 3));
return out.join(",");
}
function syncHeaderUser() {
var session =
window.CookieUtil && typeof CookieUtil.getAuthSession === "function"
? CookieUtil.getAuthSession()
: null;
if (!session) return;
var avatar = usableAvatar(session.avatar);
document.querySelectorAll("[data-header-avatar]").forEach(function (img) {
if (avatar) setAvatarImg(img, avatar);
});
var name = String(session.nickName || session.accountNo || "").trim();
if (name) {
document.querySelectorAll(".user-chip__name, .user-menu__meta strong, .drawer__meta strong").forEach(function (el) {
el.textContent = name;
});
}
if (session.userId) {
document.querySelectorAll(".user-menu__meta span, .drawer__meta span").forEach(function (el) {
if (/^ID\b/.test(String(el.textContent || "").trim())) {
el.textContent = "ID " + session.userId;
}
});
}
if (Object.prototype.hasOwnProperty.call(session, "recharge") && session.recharge !== "") {
var text = formatRecharge(session.recharge);
document.querySelectorAll("[data-header-recharge]").forEach(function (el) {
el.textContent = text;
});
}
refreshAvatarDecrypt(document);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", syncHeaderUser);
} else {
syncHeaderUser();
}
document.addEventListener("auth:login", syncHeaderUser);
document.addEventListener("auth:logout", syncHeaderUser);
window.refreshAvatarDecrypt = refreshAvatarDecrypt;
window.setAvatarImg = setAvatarImg;
window.syncHeaderUser = syncHeaderUser;
})();
+337
View File
@@ -0,0 +1,337 @@
(function () {
var root = document.querySelector("[data-hero]");
if (!root) return;
var slides = Array.prototype.slice.call(root.querySelectorAll("[data-hero-slide]"));
var thumbs = Array.prototype.slice.call(root.querySelectorAll("[data-hero-index]"));
var currentEl = root.querySelector("[data-hero-current]");
var fillEl = root.querySelector(".hero__pager-fill");
var bgEl = root.querySelector("[data-hero-bg]");
var total = slides.length;
if (!total) return;
var index = 0;
var timer = null;
var interval = 3000;
var bgToken = 0;
var touchStartX = null;
var touchStartY = null;
var mouseStartX = null;
var mouseStartY = null;
var mouseDown = false;
var suppressClickUntil = 0;
var posterPreloadTimer = null;
var bgLayers = [];
var activeBgIndex = 0;
if (bgEl) {
var alternateBg = bgEl.cloneNode(false);
alternateBg.classList.remove("is-active", "lazy");
alternateBg.removeAttribute("data-hero-bg");
alternateBg.removeAttribute("data-src");
alternateBg.removeAttribute("data-lazy-eager");
alternateBg.removeAttribute("data-decrypted");
alternateBg.removeAttribute("data-decrypt-failed");
alternateBg.removeAttribute("fetchpriority");
alternateBg.src = "/static/images/placeholder.png?v=2";
bgEl.parentNode.insertBefore(alternateBg, bgEl.nextSibling);
bgLayers = [bgEl, alternateBg];
}
// 初始 HTML 只展示首张;脚本就绪后改为叠层,由 CSS 完成平滑交叉过渡。
slides.forEach(function (slide, i) {
slide.removeAttribute("hidden");
slide.setAttribute("aria-hidden", i === index ? "false" : "true");
});
function pad(n) {
return n < 10 ? "0" + n : String(n);
}
function slideBgSrc(slide) {
var media = slide.querySelector("[data-hero-media]");
if (!media) return "";
return media.getAttribute("data-hero-media") || media.getAttribute("data-src") || "";
}
function loadSlidePoster(slideIndex) {
var slide = slides[slideIndex];
if (!slide) return;
var poster = slide.querySelector("[data-hero-poster]");
if (!poster || poster.getAttribute("data-hero-loaded") === "1" || poster.getAttribute("data-hero-loading") === "1") {
return;
}
var src = poster.getAttribute("data-src") || "";
if (!src) return;
poster.setAttribute("data-hero-loading", "1");
function done() {
poster.removeAttribute("data-hero-loading");
poster.setAttribute("data-hero-loaded", "1");
}
if (typeof window.processImage === "function") {
window.processImage(src, poster).then(done);
return;
}
poster.addEventListener("load", done, { once: true });
poster.addEventListener("error", done, { once: true });
poster.src = src;
}
function queueNextPoster() {
if (posterPreloadTimer) {
window.clearTimeout(posterPreloadTimer);
}
if (total < 2) return;
posterPreloadTimer = window.setTimeout(function () {
loadSlidePoster((index + 1) % total);
}, 600);
}
function syncBg(slide) {
if (!bgLayers.length) return;
var src = slideBgSrc(slide);
if (!src) return;
var currentBg = bgLayers[activeBgIndex];
var nextBgIndex = activeBgIndex === 0 ? 1 : 0;
var targetBg = bgLayers[nextBgIndex];
if (currentBg.getAttribute("data-src") === src) {
targetBg = currentBg;
nextBgIndex = activeBgIndex;
}
targetBg.setAttribute("data-src", src);
targetBg.removeAttribute("data-decrypted");
targetBg.removeAttribute("data-decrypt-failed");
var token = ++bgToken;
function reveal(result) {
if (token !== bgToken) return;
if (result) {
targetBg.setAttribute("data-decrypted", "1");
}
if (nextBgIndex === activeBgIndex) return;
window.requestAnimationFrame(function () {
if (token !== bgToken) return;
targetBg.classList.add("is-active");
currentBg.classList.remove("is-active");
activeBgIndex = nextBgIndex;
});
}
if (typeof window.processImage === "function") {
// 必须走解密;禁止把加密原址直接赋给 src
window.processImage(src, targetBg).then(reveal);
return;
}
targetBg.src = src;
reveal(src);
}
function goTo(next) {
var nextIndex = ((next % total) + total) % total;
if (nextIndex === index) return;
index = nextIndex;
loadSlidePoster(index);
queueNextPoster();
slides.forEach(function (slide, i) {
var active = i === index;
slide.classList.toggle("is-active", active);
slide.setAttribute("aria-hidden", active ? "false" : "true");
if (active) {
syncBg(slide);
}
});
thumbs.forEach(function (thumb, i) {
var active = i === index;
thumb.classList.toggle("is-active", active);
thumb.setAttribute("aria-selected", active ? "true" : "false");
});
if (currentEl) currentEl.textContent = pad(index + 1);
if (fillEl) fillEl.style.width = ((index + 1) / total) * 100 + "%";
}
function next() {
goTo(index + 1);
}
function previous() {
goTo(index - 1);
}
function start() {
stop();
if (total < 2 || mouseDown || touchStartX !== null) return;
timer = window.setInterval(next, interval);
}
function stop() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
thumbs.forEach(function (thumb) {
var thumbIndex = Number(thumb.getAttribute("data-hero-index"));
function preloadThumbSlide() {
if (!Number.isNaN(thumbIndex)) {
loadSlidePoster(thumbIndex);
}
}
thumb.addEventListener("mouseenter", preloadThumbSlide);
thumb.addEventListener("focus", preloadThumbSlide);
thumb.addEventListener("touchstart", preloadThumbSlide, { passive: true });
thumb.addEventListener("click", function () {
if (Number.isNaN(thumbIndex)) return;
goTo(thumbIndex);
start();
});
});
var canHover = !window.matchMedia || window.matchMedia("(hover: hover)").matches;
if (canHover) {
root.addEventListener("mouseenter", stop);
root.addEventListener("mouseleave", start);
}
root.addEventListener("dragstart", function (event) {
event.preventDefault();
});
root.addEventListener("mousedown", function (event) {
if (event.button !== 0) return;
mouseStartX = event.clientX;
mouseStartY = event.clientY;
mouseDown = true;
root.classList.add("is-dragging");
stop();
});
window.addEventListener("mousemove", function (event) {
if (!mouseDown || mouseStartX === null || mouseStartY === null) return;
var deltaX = event.clientX - mouseStartX;
var deltaY = event.clientY - mouseStartY;
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) >= 6) {
event.preventDefault();
}
});
window.addEventListener("mouseup", function (event) {
if (!mouseDown || mouseStartX === null || mouseStartY === null) return;
var deltaX = event.clientX - mouseStartX;
var deltaY = event.clientY - mouseStartY;
var isHorizontalSwipe = Math.abs(deltaX) >= 50 && Math.abs(deltaX) > Math.abs(deltaY) * 1.2;
mouseStartX = null;
mouseStartY = null;
mouseDown = false;
root.classList.remove("is-dragging");
if (isHorizontalSwipe) {
suppressClickUntil = Date.now() + 400;
if (deltaX < 0) {
next();
} else {
previous();
}
}
start();
});
window.addEventListener("blur", function () {
if (!mouseDown) return;
mouseStartX = null;
mouseStartY = null;
mouseDown = false;
root.classList.remove("is-dragging");
start();
});
root.addEventListener(
"touchstart",
function (event) {
if (event.touches.length !== 1) return;
touchStartX = event.touches[0].clientX;
touchStartY = event.touches[0].clientY;
stop();
},
{ passive: true }
);
root.addEventListener(
"touchend",
function (event) {
if (touchStartX === null || touchStartY === null || !event.changedTouches.length) {
start();
return;
}
var touch = event.changedTouches[0];
var deltaX = touch.clientX - touchStartX;
var deltaY = touch.clientY - touchStartY;
var isHorizontalSwipe = Math.abs(deltaX) >= 50 && Math.abs(deltaX) > Math.abs(deltaY) * 1.2;
touchStartX = null;
touchStartY = null;
if (isHorizontalSwipe) {
event.preventDefault();
suppressClickUntil = Date.now() + 400;
if (deltaX < 0) {
next();
} else {
previous();
}
}
start();
},
{ passive: false }
);
root.addEventListener(
"touchcancel",
function () {
touchStartX = null;
touchStartY = null;
start();
},
{ passive: true }
);
root.addEventListener(
"click",
function (event) {
if (Date.now() >= suppressClickUntil) return;
event.preventDefault();
event.stopPropagation();
},
true
);
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
stop();
} else {
start();
}
});
if (fillEl) fillEl.style.width = (1 / total) * 100 + "%";
loadSlidePoster(index);
queueNextPoster();
// 首帧也走解密;与 imgDecypt 的 eager 加载可共享 decryptedURLCache
syncBg(slides[0]);
start();
})();
+115
View File
@@ -0,0 +1,115 @@
(() => {
const root = document.querySelector('[data-history]');
if (!root) return;
const filters = Array.from(root.querySelectorAll('[data-history-filter]'));
const groups = Array.from(root.querySelectorAll('[data-history-group]'));
const items = Array.from(root.querySelectorAll('[data-history-item]'));
const empty = root.querySelector('[data-history-empty]');
const clear = root.querySelector('[data-history-clear]');
let activeKind = 'all';
async function postJSON(url, body) {
if (window.HttpRequest && typeof window.HttpRequest.post === 'function') {
return window.HttpRequest.post(url, body || {});
}
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body || {}),
});
return response.json();
}
function update() {
root.dataset.activeKind = activeKind;
items.forEach(item => {
item.hidden = item.dataset.removed === 'true' || (activeKind !== 'all' && item.dataset.kind !== activeKind);
});
groups.forEach(group => {
group.hidden = !Array.from(group.querySelectorAll('[data-history-item]')).some(item =>
!item.hidden && getComputedStyle(item).display !== 'none'
);
});
const hasVisible = groups.some(group => !group.hidden);
if (empty) empty.hidden = hasVisible;
if (clear) {
clear.hidden = items.length === 0 || items.every(item => item.dataset.removed === 'true');
clear.disabled = clear.hidden;
}
filters.forEach(filter => {
const selected = filter.dataset.historyFilter === activeKind;
filter.classList.toggle('is-active', selected);
filter.setAttribute('aria-pressed', String(selected));
});
}
filters.forEach(filter => filter.addEventListener('click', () => {
activeKind = filter.dataset.historyFilter;
update();
}));
root.querySelectorAll('[data-history-remove]').forEach(button => button.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
if (button.disabled) return;
const item = button.closest('[data-history-item]');
if (!item) return;
const id = Number(item.dataset.id);
const kind = item.dataset.kind || 'short';
if (!id) {
item.dataset.removed = 'true';
update();
return;
}
button.disabled = true;
try {
let result;
if (kind === 'series') {
result = await postJSON('/api/video/history/delete', {videoId: id});
} else {
result = await postJSON('/api/playlet/history/delete', {playletId: id});
}
if (!result || Number(result.code) !== 200) {
throw new Error((result && (result.tip || result.msg)) || '删除失败');
}
item.dataset.removed = 'true';
update();
} catch (error) {
window.alert(error.message || '删除失败,请稍后重试');
button.disabled = false;
}
}));
if (clear) {
clear.addEventListener('click', async () => {
if (clear.disabled) return;
if (!window.confirm('确定清空全部观看历史?')) return;
clear.disabled = true;
try {
const [playletResult, videoResult] = await Promise.all([
postJSON('/api/playlet/history/clear', {}),
postJSON('/api/video/history/clear', {}),
]);
const playletOk = playletResult && Number(playletResult.code) === 200;
const videoOk = videoResult && Number(videoResult.code) === 200;
if (!playletOk && !videoOk) {
throw new Error(
(videoResult && (videoResult.tip || videoResult.msg)) ||
(playletResult && (playletResult.tip || playletResult.msg)) ||
'清空失败'
);
}
items.forEach(item => { item.dataset.removed = 'true'; });
update();
window.location.reload();
} catch (error) {
window.alert(error.message || '清空失败,请稍后重试');
clear.disabled = false;
}
});
}
window.addEventListener('resize', update);
update();
})();
+320
View File
@@ -0,0 +1,320 @@
(function () {
var HISTORY_KEY = "hgdj_search_history";
var HISTORY_MAX = 10;
var PLACEHOLDER_IMG = "/static/images/placeholder.png?v=2";
var input = document.querySelector("[data-mobile-search-input]");
var form = input && input.closest("form");
var clear = document.querySelector("[data-mobile-search-clear]");
var defaultView = document.querySelector("[data-search-default]");
var suggestions = document.querySelector("[data-search-suggestions]");
var suggestList = document.querySelector("[data-search-suggest-list]");
var history = document.querySelector("[data-search-history]");
var historyChips = document.querySelector("[data-history-chips]");
var historyClear = document.querySelector("[data-history-clear]");
if (!input || !form || !clear || !defaultView || !suggestions || !suggestList) return;
var suggestTimer = null;
var suggestSeq = 0;
function searchResultsPath(q) {
q = String(q || "").trim();
if (!q) return "/search";
return "/search/" + encodeURIComponent(q);
}
function escapeHtml(text) {
return String(text || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function highlightHTML(text, keyword) {
var raw = String(text || "");
var key = String(keyword || "").trim();
if (!raw) return "";
if (!key) return escapeHtml(raw);
var lowerRaw = raw.toLowerCase();
var lowerKey = key.toLowerCase();
var idx = lowerRaw.indexOf(lowerKey);
if (idx < 0) return escapeHtml(raw);
var before = raw.slice(0, idx);
var hit = raw.slice(idx, idx + key.length);
var after = raw.slice(idx + key.length);
return (
escapeHtml(before) +
'<span class="mobile-search__highlight">' +
escapeHtml(hit) +
"</span>" +
escapeHtml(after)
);
}
function readHistory() {
try {
var raw = window.localStorage.getItem(HISTORY_KEY);
var list = raw ? JSON.parse(raw) : [];
if (!Array.isArray(list)) return [];
return list
.map(function (item) {
return String(item || "").trim();
})
.filter(Boolean)
.slice(0, HISTORY_MAX);
} catch (_) {
return [];
}
}
function writeHistory(list) {
try {
window.localStorage.setItem(HISTORY_KEY, JSON.stringify(list.slice(0, HISTORY_MAX)));
} catch (_) {
/* ignore quota / private mode */
}
}
function pushHistory(q) {
q = String(q || "").trim();
if (!q) return;
var list = readHistory().filter(function (item) {
return item !== q;
});
list.unshift(q);
writeHistory(list);
}
function clearHistory() {
try {
window.localStorage.removeItem(HISTORY_KEY);
} catch (_) {
/* ignore */
}
}
function renderHistory() {
if (!history || !historyChips) return;
var list = readHistory();
historyChips.innerHTML = "";
if (!list.length) {
history.hidden = true;
return;
}
history.hidden = false;
list.forEach(function (term) {
var link = document.createElement("a");
link.className = "mobile-search__chip";
link.href = searchResultsPath(term);
link.textContent = term;
historyChips.appendChild(link);
});
}
function renderSuggest(data, keyword) {
var html = "";
var feature = data && data.feature;
var items = (data && data.items) || [];
if (feature && feature.url) {
html +=
'<article class="mobile-search__suggestion-feature">' +
'<a class="mobile-search__feature-cover" href="' +
escapeHtml(feature.url) +
'" aria-label="' +
escapeHtml(feature.title || "") +
'">' +
'<img decoding="async" loading="lazy" src="' +
PLACEHOLDER_IMG +
'" data-src="' +
escapeHtml(feature.cover || "") +
'" alt="' +
escapeHtml(feature.title || "") +
'">' +
"</a>" +
'<div class="mobile-search__feature-copy">' +
'<h2 class="mobile-search__feature-title">' +
highlightHTML(feature.title, keyword) +
"</h2>" +
(feature.meta
? '<p class="mobile-search__feature-meta">' + escapeHtml(feature.meta) + "</p>"
: "") +
"</div>" +
'<a class="mobile-search__feature-play" href="' +
escapeHtml(feature.url) +
'">' +
'<img decoding="async" loading="lazy" src="' +
PLACEHOLDER_IMG +
'" data-src="/static/images/play_hollow.svg" alt="播放">播放</a>' +
"</article>";
}
items.forEach(function (item) {
if (!item || !item.url) return;
html +=
'<a class="mobile-search__suggestion" href="' +
escapeHtml(item.url) +
'" data-suggest-query="' +
escapeHtml(item.query || item.title || keyword) +
'">' +
'<img decoding="async" loading="lazy" class="mobile-search__suggestion-icon" src="' +
PLACEHOLDER_IMG +
'" data-src="/static/images/search.svg" alt="搜索">' +
'<span class="mobile-search__suggestion-text">' +
highlightHTML(item.title || item.query || "", keyword) +
"</span>" +
'<span class="mobile-search__suggestion-type">' +
escapeHtml(item.kind || "") +
"</span>" +
"</a>";
});
if (!html) {
html =
'<p class="mobile-search__suggest-empty">暂无相关结果,可直接搜索“' +
escapeHtml(keyword) +
'”</p>';
}
suggestList.innerHTML = html;
if (typeof window.initLazyImages === "function") {
window.initLazyImages(suggestList, { eager: true });
}
}
function fetchSuggest(keyword) {
var q = String(keyword || "").trim();
if (!q) {
suggestList.innerHTML = "";
return;
}
var seq = ++suggestSeq;
suggestList.innerHTML =
'<p class="mobile-search__suggest-empty">搜索中…</p>';
var req;
if (window.HttpRequest && typeof window.HttpRequest.get === "function") {
req = window.HttpRequest.get("/api/web/search/suggest", {
params: { keyword: q },
});
} else {
req = fetch(
"/api/web/search/suggest?keyword=" + encodeURIComponent(q),
{ credentials: "include", headers: { Accept: "application/json" } }
).then(function (res) {
return res.json();
});
}
Promise.resolve(req)
.then(function (data) {
if (seq !== suggestSeq) return;
renderSuggest(data || {}, q);
})
.catch(function () {
if (seq !== suggestSeq) return;
renderSuggest(
{
feature: null,
items: [
{
title: q,
kind: "搜索",
query: q,
url: searchResultsPath(q),
},
],
},
q
);
});
}
function syncPanels() {
var query = input.value.trim();
var hasQuery = query.length > 0;
defaultView.hidden = hasQuery;
suggestions.hidden = !hasQuery;
clear.hidden = !hasQuery;
if (!hasQuery) {
suggestSeq += 1;
suggestList.innerHTML = "";
}
}
function scheduleSuggest() {
syncPanels();
var q = input.value.trim();
if (suggestTimer) clearTimeout(suggestTimer);
if (!q) return;
suggestTimer = setTimeout(function () {
suggestTimer = null;
fetchSuggest(q);
}, 220);
}
input.addEventListener("input", scheduleSuggest);
clear.addEventListener("click", function () {
input.value = "";
input.dispatchEvent(new Event("input"));
input.focus();
});
form.addEventListener("submit", function (event) {
event.preventDefault();
var q = input.value.trim();
if (!q) {
input.focus();
return;
}
pushHistory(q);
window.location.href = searchResultsPath(q);
});
if (historyClear && history) {
historyClear.addEventListener("click", function () {
clearHistory();
renderHistory();
});
}
// 从联想 / 热搜跳转搜索结果时也写入本地历史
document.addEventListener("click", function (event) {
var link = event.target.closest("a[data-suggest-query], a[href^='/search/']");
if (!link || link.hasAttribute("data-search-close")) return;
var q = String(link.getAttribute("data-suggest-query") || "").trim();
if (!q) {
var href = link.getAttribute("href") || "";
var match = href.match(/^\/search\/([^/?#]+)/);
if (!match) return;
try {
q = decodeURIComponent(match[1]);
} catch (_) {
q = match[1];
}
}
if (q) pushHistory(q);
});
document.querySelectorAll("[data-search-close]").forEach(function (link) {
link.addEventListener("click", function (event) {
if (!document.referrer) return;
try {
if (new URL(document.referrer).origin === window.location.origin) {
event.preventDefault();
window.history.back();
}
} catch (_) {
/* Use the link destination when the referrer cannot be read. */
}
});
});
renderHistory();
syncPanels();
if (input.value.trim()) {
fetchSuggest(input.value.trim());
}
})();
+264
View File
@@ -0,0 +1,264 @@
(function () {
var QUEUE_VERSION = "2";
var DISMISSED_KEY = "dialogAdDismissed";
var PROGRESS_KEY = "dialogAdProgress";
var mask = document.querySelector("[data-dialog-ad]");
var previousBodyOverflow = "";
var hasDisplayedItem = false;
if (!mask) return;
var items = Array.prototype.slice.call(mask.querySelectorAll("[data-dialog-ad-item]"));
if (!items.length) return;
var queueSignature =
QUEUE_VERSION +
"|" +
items
.map(function (item, index) {
var image = item.querySelector(".dialog-ad-image");
return (
item.getAttribute("data-dialog-ad-id") ||
(image && image.getAttribute("data-dialog-ad-src")) ||
String(index)
);
})
.join("|");
function readSession(key) {
try {
return window.sessionStorage.getItem(key) || "";
} catch (error) {
return "";
}
}
function writeSession(key, value) {
try {
window.sessionStorage.setItem(key, value);
} catch (error) {
// sessionStorage 被禁用时仍允许本页依次展示和关闭广告。
}
}
function removeSession(key) {
try {
window.sessionStorage.removeItem(key);
} catch (error) {
// sessionStorage 被禁用时无需清理。
}
}
function isDismissed() {
return readSession(DISMISSED_KEY) === queueSignature;
}
function readProgress() {
try {
var progress = JSON.parse(readSession(PROGRESS_KEY));
if (!progress || progress.signature !== queueSignature) return 0;
var value = Number(progress.index);
if (!Number.isFinite(value) || value < 0) return 0;
return Math.floor(value);
} catch (error) {
return 0;
}
}
function rememberProgress(index) {
writeSession(
PROGRESS_KEY,
JSON.stringify({ signature: queueSignature, index: index }),
);
}
var currentIndex = readProgress();
var displayedIndex = -1;
var loadGeneration = 0;
function emitTrack(item, eventType) {
if (!item || eventType === "close") return;
if (window.tracker && typeof window.tracker.externalAd === "function") {
window.tracker.externalAd(item, eventType);
return;
}
try {
document.dispatchEvent(new CustomEvent("external-ad:track", {
detail: { element: item, eventType: eventType },
}));
} catch (error) {
// 埋点不可用时不影响广告展示与关闭。
}
}
function hideAllItems() {
items.forEach(function (item) {
item.hidden = true;
item.setAttribute("hidden", "");
});
}
function setActiveItem(index) {
hideAllItems();
var item = items[index];
if (!item) return false;
item.hidden = false;
item.removeAttribute("hidden");
return true;
}
function openMask() {
if (isDismissed()) return;
if (!mask.classList.contains("is-open")) {
previousBodyOverflow = document.body.style.overflow;
mask.hidden = false;
mask.removeAttribute("hidden");
mask.classList.add("is-open");
document.body.style.overflow = "hidden";
}
var item = displayedIndex >= 0 ? items[displayedIndex] : null;
if (item && !item.__dialogAdShowTracked) {
item.__dialogAdShowTracked = true;
emitTrack(item, "show");
}
}
function finishQueue() {
loadGeneration += 1;
displayedIndex = -1;
if (hasDisplayedItem) {
writeSession(DISMISSED_KEY, queueSignature);
} else {
removeSession(DISMISSED_KEY);
}
removeSession(PROGRESS_KEY);
hideAllItems();
mask.classList.remove("is-open");
mask.hidden = true;
mask.setAttribute("hidden", "");
document.body.style.overflow = previousBodyOverflow;
}
function showWhenAgeGateAllows() {
if (isDismissed()) return;
var ageGate = document.querySelector("[data-eighteen]");
if (ageGate && ageGate.classList.contains("is-open")) {
document.addEventListener("eighteen:closed", openMask, { once: true });
return;
}
openMask();
}
function waitForImage(image) {
return new Promise(function (resolve, reject) {
if (image.complete) {
if (image.naturalWidth > 0) resolve();
else reject(new Error("image decode failed"));
return;
}
var timeout = window.setTimeout(function () {
cleanup();
reject(new Error("image load timeout"));
}, 12000);
function cleanup() {
window.clearTimeout(timeout);
image.removeEventListener("load", handleLoad);
image.removeEventListener("error", handleError);
}
function handleLoad() {
cleanup();
if (image.naturalWidth > 0) resolve();
else reject(new Error("image decode failed"));
}
function handleError() {
cleanup();
reject(new Error("image load failed"));
}
image.addEventListener("load", handleLoad, { once: true });
image.addEventListener("error", handleError, { once: true });
});
}
function loadDialogImage(image) {
var source = image.getAttribute("data-dialog-ad-src") || "";
if (!source) return Promise.reject(new Error("image source is empty"));
if (typeof window.processImage === "function") {
return window.processImage(source, image).then(function (result) {
if (!result) throw new Error("image decrypt failed");
return waitForImage(image);
});
}
image.src = source;
return waitForImage(image);
}
function showItem(index) {
if (index >= items.length) {
finishQueue();
return;
}
currentIndex = index;
displayedIndex = -1;
var generation = ++loadGeneration;
rememberProgress(currentIndex);
hideAllItems();
var item = items[index];
var image = item.querySelector(".dialog-ad-image");
function displayItem() {
if (loadGeneration !== generation || currentIndex !== index) return;
if (!setActiveItem(index)) return;
displayedIndex = index;
hasDisplayedItem = true;
showWhenAgeGateAllows();
}
function skipBrokenItem() {
if (loadGeneration !== generation || currentIndex !== index) return;
showItem(index + 1);
}
if (!image) return skipBrokenItem();
loadDialogImage(image).then(displayItem).catch(skipBrokenItem);
}
function showNextItem() {
var index = displayedIndex >= 0 ? displayedIndex : currentIndex;
showItem(index + 1);
}
mask.addEventListener("click", function (event) {
var item = event.target.closest("[data-dialog-ad-item]");
if (!item || item.hidden || !mask.contains(item)) return;
var index = items.indexOf(item);
if (index < 0 || index !== displayedIndex) return;
if (event.target.closest("[data-dialog-ad-link]")) {
emitTrack(item, "click");
}
if (event.target.closest("[data-dialog-ad-close]")) {
emitTrack(item, "close");
showItem(index + 1);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && mask.classList.contains("is-open") && displayedIndex >= 0) {
emitTrack(items[displayedIndex], "close");
showNextItem();
}
});
if (isDismissed()) return;
if (currentIndex >= items.length) {
finishQueue();
return;
}
showItem(currentIndex);
})();
+36
View File
@@ -0,0 +1,36 @@
(function () {
var root = document.querySelector("[data-download-pop]");
if (!root) return;
var openers = document.querySelectorAll("[data-download-open]");
var closers = root.querySelectorAll("[data-download-close]");
var previousOverflow = "";
function open(event) {
if (event) {
event.preventDefault();
}
root.classList.add("is-open");
previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
}
function close() {
root.classList.remove("is-open");
document.body.style.overflow = previousOverflow;
}
Array.prototype.forEach.call(openers, function (el) {
el.addEventListener("click", open);
});
Array.prototype.forEach.call(closers, function (el) {
el.addEventListener("click", close);
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && root.classList.contains("is-open")) {
close();
}
});
})();
+313
View File
@@ -0,0 +1,313 @@
(function () {
var form = document.querySelector("[data-feedback-form]");
if (!form) return;
var types = form.querySelectorAll(".feedback-type");
var content = form.querySelector("[data-feedback-content]");
var count = form.querySelector("[data-feedback-count]");
var uploads = form.querySelector("[data-feedback-uploads]");
var fileInput = form.querySelector("[data-feedback-file]");
var submitBtn = form.querySelector(".feedback-form__submit");
var files = [];
var maxFiles = 3;
var submitting = false;
var ALLOWED_MIME = {
"image/jpeg": true,
"image/jpg": true,
"image/pjpeg": true,
"image/png": true,
"image/gif": true,
};
var ALLOWED_EXT = {
jpg: true,
jpeg: true,
png: true,
gif: true,
};
var TYPE_MAP = {
play: 2, // 播放问题
account: 3, // 账号问题
report: 4, // 内容举报 → 其他
suggest: 1, // 功能建议
other: 4, // 其他
};
function toast(message, type) {
if (typeof window.showToast === "function") {
window.showToast(message, type);
return;
}
if (window.AuthAPI && typeof window.AuthAPI.toast === "function") {
window.AuthAPI.toast(message, type || "info");
return;
}
window.alert(message);
}
Array.prototype.forEach.call(types, function (type) {
var input = type.querySelector('input[type="radio"]');
if (!input) return;
input.addEventListener("change", function () {
Array.prototype.forEach.call(types, function (item) {
item.classList.toggle("is-active", item.querySelector("input").checked);
});
});
});
if (content && count) {
content.addEventListener("input", function () {
count.textContent = String(Array.from(content.value || "").length);
});
}
function isAllowedImage(file) {
if (!file) return false;
var mime = String(file.type || "").toLowerCase();
if (mime && ALLOWED_MIME[mime]) return true;
var name = String(file.name || "");
var dot = name.lastIndexOf(".");
if (dot < 0) return false;
var ext = name.slice(dot + 1).toLowerCase();
return !!ALLOWED_EXT[ext];
}
function renderUploads() {
if (!uploads || !fileInput) return;
uploads.innerHTML = "";
files.forEach(function (file, index) {
var item = document.createElement("div");
item.className = "feedback-upload feedback-upload--preview";
if (file.uploading) item.classList.add("is-uploading");
if (file.error) item.classList.add("is-error");
var img = document.createElement("img");
img.src = "/static/images/placeholder.png?v=2";
img.setAttribute("data-src", file.previewUrl);
img.alt = file.name || "截图";
img.loading = "lazy";
img.decoding = "async";
var remove = document.createElement("button");
remove.type = "button";
remove.className = "feedback-upload__remove";
remove.setAttribute("aria-label", "删除截图");
remove.innerHTML = '<img class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/close.svg" alt="" loading="lazy" decoding="async">';
remove.addEventListener("click", function () {
if (file.previewUrl) URL.revokeObjectURL(file.previewUrl);
files.splice(index, 1);
renderUploads();
});
item.appendChild(img);
item.appendChild(remove);
uploads.appendChild(item);
});
if (files.length < maxFiles) {
var label = document.createElement("label");
label.className = "feedback-upload feedback-upload--add";
label.appendChild(fileInput);
fileInput.hidden = true;
var icon = document.createElement("img");
icon.className = "icon";
icon.src = "/static/images/placeholder.png?v=2";
icon.setAttribute("data-src", "/static/images/plus-dim.svg");
icon.alt = "";
icon.loading = "lazy";
icon.decoding = "async";
label.appendChild(icon);
var text = document.createElement("span");
text.textContent = "上传截图";
label.appendChild(text);
uploads.appendChild(label);
} else {
uploads.appendChild(fileInput);
fileInput.hidden = true;
}
if (typeof window.initLazyImages === "function") {
window.initLazyImages(uploads);
}
}
function extractUploadURL(data) {
if (!data) return "";
if (typeof data === "string") return data;
return String(data.sourceNameFs || data.url || data.path || data.id || "").trim();
}
function uploadOne(fileItem) {
if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") {
return Promise.reject(new Error("上传服务未就绪"));
}
if (fileItem.remoteURL) return Promise.resolve(fileItem.remoteURL);
fileItem.uploading = true;
renderUploads();
var fd = new FormData();
fd.append("file", fileItem.file, fileItem.name || "image.jpg");
return window.HttpRequest.post("/api/upload/image", fd)
.then(function (res) {
if (!res || Number(res.code) !== 200 || !res.data) {
throw new Error((res && (res.tip || res.msg)) || "上传失败");
}
var url = extractUploadURL(res.data);
if (!url) throw new Error("上传成功但未返回图片地址");
fileItem.remoteURL = url;
fileItem.uploading = false;
fileItem.error = false;
renderUploads();
return url;
})
.catch(function (err) {
fileItem.uploading = false;
fileItem.error = true;
renderUploads();
throw err;
});
}
function uploadAll() {
var chain = Promise.resolve([]);
files.forEach(function (item) {
chain = chain.then(function (urls) {
return uploadOne(item).then(function (url) {
urls.push(url);
return urls;
});
});
});
return chain;
}
function selectedType() {
var checked = form.querySelector('input[name="type"]:checked');
var key = checked ? String(checked.value || "") : "play";
return TYPE_MAP[key] || 2;
}
function apiTip(payload, fallback) {
if (!payload) return fallback;
if (payload.tip || payload.msg) return String(payload.tip || payload.msg);
if (payload.data && (payload.data.tip || payload.data.msg)) {
return String(payload.data.tip || payload.data.msg);
}
if (payload.message) return String(payload.message);
return fallback;
}
function setBusy(on) {
submitting = !!on;
if (submitBtn) {
submitBtn.disabled = !!on;
submitBtn.classList.toggle("is-loading", !!on);
if (on) {
if (!submitBtn.dataset.labelHtml) {
submitBtn.dataset.labelHtml = submitBtn.innerHTML;
}
submitBtn.textContent = "提交中…";
} else if (submitBtn.dataset.labelHtml) {
submitBtn.innerHTML = submitBtn.dataset.labelHtml;
}
}
if (content) content.disabled = !!on;
if (fileInput) fileInput.disabled = !!on;
Array.prototype.forEach.call(form.querySelectorAll("input, textarea, button"), function (el) {
if (el === submitBtn) return;
if (el === fileInput) return;
if (el === content) return;
if (el.type === "file") return;
el.disabled = !!on;
});
}
if (fileInput) {
fileInput.addEventListener("change", function () {
var selected = Array.prototype.slice.call(fileInput.files || []);
var rejectedFormat = false;
var rejectedFull = false;
selected.forEach(function (file) {
if (files.length >= maxFiles) {
rejectedFull = true;
return;
}
if (!isAllowedImage(file)) {
rejectedFormat = true;
return;
}
files.push({
name: file.name,
file: file,
previewUrl: URL.createObjectURL(file),
remoteURL: "",
uploading: false,
error: false,
});
});
fileInput.value = "";
renderUploads();
if (rejectedFormat) toast("仅支持 jpg / png / gif 格式", "error");
else if (rejectedFull) toast("最多上传 3 张图片", "error");
});
}
renderUploads();
form.addEventListener("submit", function (event) {
event.preventDefault();
if (submitting) return;
if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") {
toast("反馈服务未就绪", "error");
return;
}
var text = String((content && content.value) || "").trim();
if (!text) {
toast("请填写问题描述", "error");
return;
}
if (Array.from(text).length > 500) {
toast("问题描述最多 500 字", "error");
return;
}
var contactInput = form.querySelector('input[name="contact"]');
var contact = contactInput ? String(contactInput.value || "").trim() : "";
var type = selectedType();
setBusy(true);
uploadAll()
.then(function (screenshots) {
var payload = {
type: type,
content: text,
contact: contact,
screenshots: screenshots || [],
};
return window.HttpRequest.post("/api/feedback/add", payload);
})
.then(function (res) {
if (!res || Number(res.code) !== 200) {
var failTip = apiTip(res, "提交失败");
var failErr = new Error(failTip);
failErr.data = res;
throw failErr;
}
toast("反馈提交成功", "success");
form.reset();
files.forEach(function (f) {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl);
});
files = [];
Array.prototype.forEach.call(types, function (item) {
var input = item.querySelector("input");
item.classList.toggle("is-active", !!(input && input.checked));
});
if (count) count.textContent = "0";
renderUploads();
})
.catch(function (err) {
toast(apiTip(err, "提交失败,请稍后重试"), "error");
})
.finally(function () {
setBusy(false);
});
});
})();
+15
View File
@@ -0,0 +1,15 @@
(function () {
var root = document.querySelector("[data-help-faq]");
if (!root) return;
var items = root.querySelectorAll(".help-faq__item");
Array.prototype.forEach.call(items, function (item) {
item.addEventListener("toggle", function () {
if (!item.open) return;
Array.prototype.forEach.call(items, function (other) {
if (other !== item) other.open = false;
});
});
});
})();
+106
View File
@@ -0,0 +1,106 @@
(function () {
var toc = document.querySelector(".legal-toc");
if (!toc) return;
var layout = document.querySelector(".legal-layout");
var list = toc.querySelector(".legal-toc__list");
var items = Array.prototype.slice.call(toc.querySelectorAll(".legal-toc__item"));
var activeId = "";
var TOP = 88;
var desktopMq = window.matchMedia("(min-width: 769px)");
var slot = null;
var pinnedHeight = 0;
function ensureSlot() {
if (slot || !layout || !toc.parentNode) return;
if (toc.parentNode.classList && toc.parentNode.classList.contains("legal-toc-slot")) {
slot = toc.parentNode;
return;
}
slot = document.createElement("div");
slot.className = "legal-toc-slot";
toc.parentNode.insertBefore(slot, toc);
slot.appendChild(toc);
}
function clearPin() {
toc.classList.remove("is-fixed");
toc.style.top = "";
toc.style.left = "";
toc.style.width = "";
if (slot) slot.style.height = "";
}
function updatePin() {
if (!layout || !desktopMq.matches) {
clearPin();
return;
}
ensureSlot();
if (!slot) return;
if (!toc.classList.contains("is-fixed")) {
pinnedHeight = toc.offsetHeight;
}
var layoutRect = layout.getBoundingClientRect();
var slotRect = slot.getBoundingClientRect();
var width = slot.offsetWidth;
if (slotRect.top >= TOP) {
clearPin();
return;
}
if (!pinnedHeight) pinnedHeight = toc.offsetHeight;
slot.style.height = pinnedHeight + "px";
var top = Math.min(TOP, layoutRect.bottom - pinnedHeight);
toc.classList.add("is-fixed");
toc.style.top = top + "px";
toc.style.left = slotRect.left + "px";
toc.style.width = width + "px";
}
ensureSlot();
updatePin();
window.addEventListener("scroll", updatePin, { passive: true });
window.addEventListener("resize", function () {
clearPin();
updatePin();
});
if (typeof desktopMq.addEventListener === "function") {
desktopMq.addEventListener("change", function () {
clearPin();
updatePin();
});
} else if (typeof desktopMq.addListener === "function") {
desktopMq.addListener(function () {
clearPin();
updatePin();
});
}
function centerChip(item) {
if (!list || list.scrollWidth <= list.clientWidth) return;
var listRect = list.getBoundingClientRect();
var itemRect = item.getBoundingClientRect();
var left = list.scrollLeft + itemRect.left - listRect.left - (list.clientWidth - item.clientWidth) / 2;
list.scrollTo({ left: Math.max(0, left), behavior: "smooth" });
}
function setActive(item) {
if (!item) return;
var id = (item.getAttribute("href") || "").slice(1);
if (id === activeId) return;
activeId = id;
items.forEach(function (link) {
link.classList.toggle("is-active", link === item);
});
centerChip(item);
}
items.forEach(function (item) {
item.addEventListener("click", function () { setActive(item); });
});
})();
+137
View File
@@ -0,0 +1,137 @@
(function () {
var form = document.querySelector("[data-login-page-form]");
var password = document.querySelector("[data-login-page-password]");
var toggle = document.querySelector("[data-login-page-toggle]");
var toggleIcon = document.querySelector("[data-login-page-toggle-icon]");
var submitBtn = document.querySelector("[data-login-page-submit]") || (form && form.querySelector('button[type="submit"]'));
var root = form || document;
if (toggle && password && toggleIcon) {
toggle.addEventListener("click", function () {
var show = password.getAttribute("type") === "password";
password.setAttribute("type", show ? "text" : "password");
toggleIcon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg");
toggle.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
});
}
function clearFieldError(name) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.remove("is-error");
if (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
}
}
function clearAllErrors() {
root.querySelectorAll("[data-field]").forEach(function (el) {
el.classList.remove("is-error");
});
root.querySelectorAll("[data-field-error]").forEach(function (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
});
}
function setFieldError(name, message) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.add("is-error");
if (err) {
err.hidden = false;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = message || "";
}
}
function isLen4to16(value) {
var len = String(value || "").length;
return len >= 4 && len <= 16;
}
function validateForm() {
clearAllErrors();
var fd = new FormData(form);
var account = String(fd.get("account") || "").trim();
var pwd = String(fd.get("password") || "");
var valid = true;
if (!account) {
setFieldError("account", "请输入账号");
valid = false;
} else if (
!(window.AuthAPI && AuthAPI.isValidLoginAccount
? AuthAPI.isValidLoginAccount(account)
: isLen4to16(account) || account.indexOf("@") >= 0)
) {
setFieldError("account", "账号需为 4–16 位,或合法邮箱");
valid = false;
}
if (!pwd) {
setFieldError("password", "请输入密码");
valid = false;
} else if (!isLen4to16(pwd)) {
setFieldError("password", "密码需为 4–16 位");
valid = false;
}
if (!valid) return null;
return { account: account, password: pwd };
}
function mapServerError(err) {
var tip = (err && err.message) || "登录失败,请稍后重试";
if (/账号|账户|用户|不存在|邮箱/.test(tip) && !/密码/.test(tip)) {
setFieldError("account", tip);
return;
}
setFieldError("password", tip);
}
function setSubmitting(loading) {
if (!submitBtn) return;
submitBtn.disabled = !!loading;
submitBtn.classList.toggle("is-loading", !!loading);
submitBtn.textContent = loading ? "登录中…" : "登录";
}
if (form) {
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("input", function () {
var name = input.getAttribute("name");
if (name) clearFieldError(name);
});
});
form.addEventListener("submit", function (event) {
event.preventDefault();
if (!window.AuthAPI || !window.HttpRequest) {
setFieldError("password", "认证脚本未加载,请刷新页面");
return;
}
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
var payload = validateForm();
if (!payload) return;
setSubmitting(true);
AuthAPI.login(payload)
.then(function (res) {
AuthAPI.toast((res && res.tip) || "登录成功", "success");
window.setTimeout(function () {
window.location.href = "/";
}, 400);
})
.catch(function (err) {
setSubmitting(false);
mapServerError(err);
AuthAPI.toast((err && err.message) || "登录失败", "error");
});
});
}
})();
+252
View File
@@ -0,0 +1,252 @@
(function () {
var root = document.querySelector("[data-login-pop]");
if (!root) return;
var form = root.querySelector("[data-login-form]");
var password = root.querySelector("[data-login-password]");
var toggle = root.querySelector("[data-login-toggle]");
var toggleIcon = root.querySelector("[data-login-toggle-icon]");
var title = root.querySelector("[data-login-title]");
var description = root.querySelector("[data-login-desc]");
var submitBtn = root.querySelector("[data-login-submit]") || (form && form.querySelector('button[type="submit"]'));
var previousOverflow = "";
var switchAccountMode = false;
function closeRegister() {
if (typeof window.closeRegisterPop === "function") {
window.closeRegisterPop();
return;
}
var register = document.querySelector("[data-register-pop]");
if (register) register.classList.remove("is-open");
}
function closeUserMenu() {
var menu = document.querySelector("[data-user-menu].is-open");
if (menu) {
menu.classList.remove("is-open");
var t = menu.querySelector("[data-user-menu-toggle]");
if (t) t.setAttribute("aria-expanded", "false");
}
}
function clearFieldError(name) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.remove("is-error");
if (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
}
}
function clearAllErrors() {
root.querySelectorAll("[data-field]").forEach(function (el) {
el.classList.remove("is-error");
});
root.querySelectorAll("[data-field-error]").forEach(function (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
});
}
function setFieldError(name, message) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.add("is-error");
if (err) {
err.hidden = false;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = message || "";
}
}
function isLen4to16(value) {
var len = String(value || "").length;
return len >= 4 && len <= 16;
}
function validateForm() {
clearAllErrors();
var fd = new FormData(form);
var account = String(fd.get("account") || "").trim();
var pwd = String(fd.get("password") || "");
var valid = true;
if (!account) {
setFieldError("account", "请输入账号");
valid = false;
} else if (
!(window.AuthAPI && AuthAPI.isValidLoginAccount
? AuthAPI.isValidLoginAccount(account)
: isLen4to16(account) || account.indexOf("@") >= 0)
) {
setFieldError("account", "账号需为 4–16 位,或合法邮箱");
valid = false;
}
if (!pwd) {
setFieldError("password", "请输入密码");
valid = false;
} else if (!isLen4to16(pwd)) {
setFieldError("password", "密码需为 4–16 位");
valid = false;
}
if (!valid) return null;
return { account: account, password: pwd };
}
function mapServerError(err) {
var tip = (err && err.message) || "登录失败,请稍后重试";
if (/账号|账户|用户|不存在|邮箱/.test(tip) && !/密码/.test(tip)) {
setFieldError("account", tip);
return;
}
setFieldError("password", tip);
}
function isMobile() {
return window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
}
function open(input) {
var event = input && typeof input.preventDefault === "function" ? input : null;
var options = event ? {} : input || {};
if (isMobile() && !options.forcePopup) {
if (event) event.preventDefault();
window.location.href = "/login";
return;
}
if (event) event.preventDefault();
switchAccountMode = !!options.switchAccount;
closeRegister();
closeUserMenu();
clearAllErrors();
if (options.resetForm && form) form.reset();
if (password) password.setAttribute("type", "password");
if (toggle) toggle.setAttribute("aria-label", "显示密码");
if (toggleIcon) {
toggleIcon.setAttribute("src", "/static/images/eye.svg");
toggleIcon.setAttribute("data-src", "/static/images/eye.svg");
}
if (title) title.textContent = switchAccountMode ? "切换账号" : "登录黄果短剧";
if (description) {
description.textContent = switchAccountMode
? "请输入其他账号和密码完成切换"
: "登录后可收藏作品、解锁付费剧集并查看观看历史";
}
root.classList.add("is-open");
previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
var first = root.querySelector(".login-pop__input");
if (first) {
window.setTimeout(function () {
first.focus();
}, 0);
}
}
function close() {
root.classList.remove("is-open");
document.body.style.overflow = previousOverflow;
clearAllErrors();
switchAccountMode = false;
}
window.openLoginPop = open;
window.openAccountSwitchLogin = function () {
open({ forcePopup: true, switchAccount: true, resetForm: true });
};
window.closeLoginPop = close;
document.addEventListener("click", function (event) {
var opener = event.target.closest("[data-login-open]");
if (opener) {
open(event);
return;
}
// PC:拦截站内 /login 链接,改为打开弹窗;H5 放行跳转登录页
var loginLink = event.target.closest('a[href="/login"], a[href="/login/"], a[href="#login"]');
if (loginLink && !isMobile()) {
open(event);
return;
}
if (!root.classList.contains("is-open")) return;
if (event.target.closest("[data-login-close]")) {
close();
}
});
if (toggle && password && toggleIcon) {
toggle.addEventListener("click", function () {
var show = password.getAttribute("type") === "password";
password.setAttribute("type", show ? "text" : "password");
toggleIcon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg");
toggle.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
});
}
function setSubmitting(loading) {
if (!submitBtn) return;
submitBtn.disabled = !!loading;
submitBtn.classList.toggle("is-loading", !!loading);
submitBtn.textContent = loading ? "登录中…" : "登录";
}
if (form) {
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("input", function () {
var name = input.getAttribute("name");
if (name) clearFieldError(name);
});
});
form.addEventListener("submit", function (event) {
event.preventDefault();
if (!window.AuthAPI || !window.HttpRequest) {
setFieldError("password", "认证脚本未加载,请刷新页面");
return;
}
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
var payload = validateForm();
if (!payload) return;
setSubmitting(true);
AuthAPI.login(payload)
.then(function (res) {
AuthAPI.toast((res && res.tip) || "登录成功", "success");
window.setTimeout(function () {
close();
window.location.reload();
}, 400);
})
.catch(function (err) {
setSubmitting(false);
mapServerError(err);
AuthAPI.toast((err && err.message) || "登录失败", "error");
});
});
}
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && root.classList.contains("is-open")) {
close();
}
});
try {
var params = new URLSearchParams(window.location.search || "");
if (params.get("auth") === "login") {
params.delete("auth");
var qs = params.toString();
window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : "") + window.location.hash);
open();
}
} catch (e) {}
})();
File diff suppressed because it is too large Load Diff
+599
View File
@@ -0,0 +1,599 @@
(function () {
var root = document.querySelector("[data-play-composer]");
if (!root) return;
var commentsRoot = document.querySelector("[data-play-comments]") || root.closest(".play-comments");
var input = root.querySelector("[data-comment-input]");
var countEl = root.querySelector("[data-comment-count]");
var sendBtn = root.querySelector("[data-comment-send]");
var listEl = document.querySelector("[data-comment-list]");
var totalEl = document.querySelector("[data-comment-total-label]");
var moreBtn = document.querySelector("[data-comment-more]");
var sending = false;
var inlineSending = false;
var loadingMore = false;
var MAX_LEN = 200;
var PAGE_SIZE = 4;
var replyTarget = null;
function toast(message, type) {
if (typeof window.showToast === "function") {
window.showToast(message, type);
return;
}
if (window.AuthAPI && typeof window.AuthAPI.toast === "function") {
window.AuthAPI.toast(message, type || "info");
return;
}
}
function toInt(v, fallback) {
var n = parseInt(v, 10);
return isNaN(n) ? fallback || 0 : n;
}
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function getPageSize() {
return Math.max(
1,
toInt(commentsRoot && commentsRoot.getAttribute("data-comment-size"), PAGE_SIZE)
);
}
function getPage() {
return Math.max(
1,
toInt(commentsRoot && commentsRoot.getAttribute("data-comment-page"), 1)
);
}
function getLoaded() {
if (!listEl) return 0;
return listEl.querySelectorAll(".play-comment:not(.play-comment--empty)").length;
}
function setPagerState(page, loaded, total) {
if (!commentsRoot) return;
if (page != null) commentsRoot.setAttribute("data-comment-page", String(page));
if (loaded != null) commentsRoot.setAttribute("data-comment-loaded", String(loaded));
if (total != null) commentsRoot.setAttribute("data-comment-total", String(total));
syncMoreButton(total != null ? total : toInt(commentsRoot.getAttribute("data-comment-total"), 0));
}
function syncMoreButton(total) {
if (!moreBtn) return;
var loaded = getLoaded();
var hasMore = total > loaded;
if (hasMore) {
moreBtn.hidden = false;
moreBtn.removeAttribute("hidden");
} else {
moreBtn.hidden = true;
moreBtn.setAttribute("hidden", "");
}
}
function syncCount() {
if (!input || !countEl) return;
var len = Array.from(input.value || "").length;
countEl.textContent = len + " / " + MAX_LEN;
}
function formatTime(raw) {
var s = String(raw || "").trim();
if (!s) return "刚刚";
var d = new Date(s);
if (isNaN(d.getTime())) return s;
return (
d.getFullYear() +
"/" +
(d.getMonth() + 1) +
"/" +
d.getDate() +
" " +
String(d.getHours()).padStart(2, "0") +
":" +
String(d.getMinutes()).padStart(2, "0")
);
}
function renderCommentItem(item) {
item = item || {};
var avatar = String(item.userAvatar || "").trim();
var name = escapeHtml(item.userName || "匿名用户");
var text = escapeHtml(item.text || "");
var time = escapeHtml(formatTime(item.createdAt));
var like = toInt(item.countLike, 0);
var liked = Number(item.isLike) === 1;
var avatarHtml = avatar
? '<img class="play-comment__avatar lazy" src="/static/images/placeholder.png?v=2" data-src="' +
escapeHtml(avatar) +
'" alt="" loading="lazy" decoding="async">'
: '<img class="play-comment__avatar" src="/static/images/placeholder.png?v=2" data-src="/static/images/profile/avatar.png" alt="" loading="lazy" decoding="async">';
var children = Array.isArray(item.children) ? item.children : [];
var replies = children
.map(function (child) {
var cName = escapeHtml(child.userName || "用户");
var replyTo = child.replyUserName
? " 回复 " + escapeHtml(child.replyUserName)
: "";
return (
'<div class="play-comment__reply">' +
cName +
replyTo +
":" +
escapeHtml(child.text || "") +
"</div>"
);
})
.join("");
var commentId = escapeHtml(item.id || "");
var rawName = String(item.userName || "匿名用户");
return (
'<li class="play-comment" data-comment-id="' +
commentId +
'">' +
avatarHtml +
'<div class="play-comment__body">' +
'<div class="play-comment__meta">' +
'<span class="play-comment__name">' +
name +
"</span>" +
'<span class="play-comment__time">' +
time +
"</span>" +
"</div>" +
'<p class="play-comment__text">' +
text +
"</p>" +
replies +
'<div class="play-comment__actions">' +
'<button class="play-comment__action' +
(liked ? " is-active" : "") +
'" type="button" aria-label="点赞" data-comment-like="' +
commentId +
'" data-liked="' +
(liked ? "1" : "2") +
'" data-like-raw="' +
escapeHtml(like) +
'">' +
'<img class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/heart.svg" alt="" loading="lazy" decoding="async">' +
'<span data-like-count>' +
escapeHtml(like) +
"</span>" +
"</button>" +
'<button class="play-comment__action" type="button" data-comment-reply data-comment-id="' +
commentId +
'" data-parents-id="' +
commentId +
'" data-reply-name="' +
escapeHtml(rawName) +
'">回复</button>' +
"</div></div></li>"
);
}
function getLoadedIds() {
var ids = Object.create(null);
if (!listEl) return ids;
listEl.querySelectorAll(".play-comment[data-comment-id]").forEach(function (el) {
var id = String(el.getAttribute("data-comment-id") || "").trim();
if (id) ids[id] = true;
});
return ids;
}
function updateTotal(n) {
if (!totalEl) return;
totalEl.textContent = n > 0 ? n + " 条" : "暂无评论";
}
function renderList(list, totalCount) {
if (!listEl) return;
list = Array.isArray(list) ? list : [];
clearReplyTarget();
if (!list.length) {
listEl.innerHTML =
'<li class="play-comment play-comment--empty"><p class="play-comment__text">暂无评论</p></li>';
} else {
listEl.innerHTML = list.map(renderCommentItem).join("");
if (typeof window.initLazyImages === "function") {
window.initLazyImages(listEl);
}
}
var total = typeof totalCount === "number" ? totalCount : toInt(commentsRoot && commentsRoot.getAttribute("data-comment-total"), 0);
if (typeof totalCount === "number") {
updateTotal(totalCount);
}
setPagerState(1, list.length, total);
}
// 下一页追加到现有列表底部,不替换已展示评论。
function appendList(list, totalCount) {
if (!listEl) return;
list = Array.isArray(list) ? list : [];
var total =
typeof totalCount === "number"
? totalCount
: toInt(commentsRoot && commentsRoot.getAttribute("data-comment-total"), 0);
if (typeof totalCount === "number") {
updateTotal(totalCount);
}
if (!list.length) {
setPagerState(getPage(), getLoaded(), total);
return;
}
var empty = listEl.querySelector(".play-comment--empty");
if (empty) empty.remove();
var existing = getLoadedIds();
var fresh = list.filter(function (item) {
var id = String((item && item.id) || "").trim();
return id && !existing[id];
});
if (!fresh.length) {
setPagerState(getPage(), getLoaded(), total);
return;
}
var wrap = document.createElement("ul");
wrap.innerHTML = fresh.map(renderCommentItem).join("");
var nodes = [];
while (wrap.firstChild) {
nodes.push(wrap.firstChild);
listEl.appendChild(wrap.firstChild);
}
if (typeof window.initLazyImages === "function") {
nodes.forEach(function (node) {
if (node && node.nodeType === 1) window.initLazyImages(node);
});
window.initLazyImages(listEl);
}
setPagerState(getPage(), getLoaded(), total);
}
function fetchCommentPage(pageNum) {
if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") {
return Promise.reject(new Error("评论服务未就绪"));
}
var objectId = toInt(root.getAttribute("data-object-id"), 0);
var objectType = toInt(root.getAttribute("data-object-type"), 10);
if (objectId <= 0) return Promise.reject(new Error("缺少评论对象"));
var pageSize = getPageSize();
return window.HttpRequest.get("/api/comment/list", {
params: {
objectId: objectId,
objectType: objectType,
pageNum: pageNum,
pageSize: pageSize,
sortOrder: 1,
},
}).then(function (res) {
if (!res || Number(res.code) !== 200 || !res.data) {
throw new Error((res && (res.tip || res.msg)) || "加载失败");
}
return res.data;
});
}
function refreshList() {
return fetchCommentPage(1)
.then(function (data) {
renderList(data.list || [], toInt(data.totalCount, 0));
})
.catch(function () {});
}
function loadMoreComments() {
if (loadingMore || !moreBtn) return;
var nextPage = getPage() + 1;
loadingMore = true;
moreBtn.disabled = true;
fetchCommentPage(nextPage)
.then(function (data) {
var list = data.list || [];
var total = toInt(data.totalCount, 0);
if (commentsRoot) {
commentsRoot.setAttribute("data-comment-page", String(nextPage));
}
// 只追加,绝不 renderList 整表替换
appendList(list, total);
})
.catch(function (err) {
toast((err && err.message) || "加载失败,请稍后重试", "error");
})
.finally(function () {
loadingMore = false;
if (moreBtn) moreBtn.disabled = false;
});
}
function removeInlineComposer() {
if (!listEl) return;
listEl.querySelectorAll("[data-comment-inline-composer]").forEach(function (el) {
el.remove();
});
}
function clearReplyTarget() {
replyTarget = null;
removeInlineComposer();
}
function applyLikeUI(btn, isLike, countLike) {
if (!btn) return;
var liked = Number(isLike) === 1;
btn.classList.toggle("is-active", liked);
btn.setAttribute("data-liked", liked ? "1" : "2");
if (countLike != null) {
btn.setAttribute("data-like-raw", String(countLike));
var countEl = btn.querySelector("[data-like-count]");
if (countEl) countEl.textContent = String(countLike);
}
}
function toggleCommentLike(btn) {
if (!btn || btn.getAttribute("data-liking") === "1") return;
if (root.getAttribute("data-logged-in") !== "1") {
var loginBtn = document.querySelector("[data-login-open]");
if (loginBtn) loginBtn.click();
return;
}
if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") {
toast("点赞服务未就绪", "error");
return;
}
var commentId = toInt(btn.getAttribute("data-comment-like"), 0);
if (commentId <= 0) return;
var likedNow = Number(btn.getAttribute("data-liked") || 2) === 1;
var nextLike = !likedNow;
var prevCount = toInt(btn.getAttribute("data-like-raw"), 0);
var nextCount = Math.max(0, prevCount + (nextLike ? 1 : -1));
btn.setAttribute("data-liking", "1");
btn.disabled = true;
applyLikeUI(btn, nextLike ? 1 : 2, nextCount);
var payload = { commentId: commentId, isLike: nextLike };
window.HttpRequest.post("/api/comment/like/set", payload)
.then(function (res) {
if (!res || Number(res.code) !== 200) {
throw new Error((res && (res.tip || res.msg)) || "操作失败");
}
var data = res.data || {};
applyLikeUI(
btn,
data.isLike != null ? data.isLike : nextLike ? 1 : 2,
data.countLike != null ? data.countLike : nextCount
);
})
.catch(function (err) {
applyLikeUI(btn, likedNow ? 1 : 2, prevCount);
toast((err && err.message) || "操作失败,请稍后重试", "error");
})
.finally(function () {
btn.removeAttribute("data-liking");
btn.disabled = false;
});
}
function openInlineComposer(commentEl, target) {
if (!commentEl || !target) return;
removeInlineComposer();
replyTarget = target;
var body = commentEl.querySelector(".play-comment__body") || commentEl;
var actions = body.querySelector(".play-comment__actions");
var box = document.createElement("div");
box.className = "play-comment__inline";
box.setAttribute("data-comment-inline-composer", "");
var name = target.replyName || "用户";
box.innerHTML =
'<div class="play-comment__inline-hint">' +
"<span>回复 @" +
escapeHtml(name) +
"</span>" +
'<button type="button" data-comment-reply-cancel>取消</button>' +
"</div>" +
'<div class="play-comment__inline-row">' +
'<input class="play-comment__inline-input" type="text" maxlength="200" placeholder="回复 @' +
escapeHtml(name) +
'…" data-comment-inline-input autocomplete="off" />' +
'<button class="play-comment__inline-send" type="button" data-comment-inline-send>发送</button>' +
"</div>";
if (actions && actions.parentNode === body) {
if (actions.nextSibling) body.insertBefore(box, actions.nextSibling);
else body.appendChild(box);
} else {
body.appendChild(box);
}
var inlineInput = box.querySelector("[data-comment-inline-input]");
if (inlineInput) inlineInput.focus();
}
function setSending(on) {
sending = !!on;
if (sendBtn) sendBtn.disabled = !!on;
if (input) input.disabled = !!on;
}
function submitTopComment() {
if (sending) return;
if (root.getAttribute("data-logged-in") !== "1") {
var loginBtn = document.querySelector("[data-login-open]");
if (loginBtn) loginBtn.click();
return;
}
if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") {
toast("评论服务未就绪", "error");
return;
}
var text = String((input && input.value) || "").trim();
if (!text) {
toast("请输入评论内容", "error");
return;
}
if (Array.from(text).length > MAX_LEN) {
toast("评论最多 200 字", "error");
return;
}
var objectId = toInt(root.getAttribute("data-object-id"), 0);
var objectType = toInt(root.getAttribute("data-object-type"), 10);
if (objectId <= 0) {
toast("缺少评论对象", "error");
return;
}
var payload = {
objectId: objectId,
objectType: objectType,
text: text,
parentsId: 0,
replyCommentId: 0,
};
setSending(true);
window.HttpRequest.post("/api/comment/send", payload)
.then(function (res) {
if (!res || Number(res.code) !== 200) {
throw new Error((res && (res.tip || res.msg)) || "发表失败");
}
if (input) input.value = "";
syncCount();
clearReplyTarget();
toast("发布成功,评论审核后才会展示", "success");
return refreshList();
})
.catch(function (err) {
toast((err && err.message) || "发表失败,请稍后重试", "error");
})
.finally(function () {
setSending(false);
});
}
function sendInlineReply(box) {
if (!box || inlineSending || !replyTarget) return;
if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") {
toast("评论服务未就绪", "error");
return;
}
var inlineInput = box.querySelector("[data-comment-inline-input]");
var btn = box.querySelector("[data-comment-inline-send]");
if (!inlineInput) return;
var text = String(inlineInput.value || "").trim();
if (!text) {
toast("请输入评论内容", "error");
return;
}
if (Array.from(text).length > MAX_LEN) {
toast("评论最多 200 字", "error");
return;
}
var objectId = toInt(root.getAttribute("data-object-id"), 0);
var objectType = toInt(root.getAttribute("data-object-type"), 10);
if (objectId <= 0) {
toast("缺少评论对象", "error");
return;
}
inlineSending = true;
if (btn) btn.disabled = true;
inlineInput.disabled = true;
var payload = {
objectId: objectId,
objectType: objectType,
text: text,
parentsId: toInt(replyTarget.parentsId, 0),
replyCommentId: toInt(
replyTarget.replyCommentId || replyTarget.parentsId,
0
),
};
window.HttpRequest.post("/api/comment/send", payload)
.then(function (res) {
if (!res || Number(res.code) !== 200) {
throw new Error((res && (res.tip || res.msg)) || "发表失败");
}
clearReplyTarget();
toast("发布成功,评论审核后才会展示", "success");
return refreshList();
})
.catch(function (err) {
toast((err && err.message) || "发表失败,请稍后重试", "error");
})
.finally(function () {
inlineSending = false;
if (btn) btn.disabled = false;
if (inlineInput) inlineInput.disabled = false;
});
}
if (input) {
input.addEventListener("input", syncCount);
input.addEventListener("keydown", function (event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submitTopComment();
}
});
syncCount();
}
if (sendBtn) {
sendBtn.addEventListener("click", submitTopComment);
}
if (moreBtn) {
moreBtn.addEventListener("click", function (event) {
event.preventDefault();
loadMoreComments();
});
}
if (listEl) {
listEl.addEventListener("click", function (event) {
var likeBtn = window.domClosest(event.target, "[data-comment-like]");
if (likeBtn && listEl.contains(likeBtn)) {
event.preventDefault();
toggleCommentLike(likeBtn);
return;
}
var cancelBtn = window.domClosest(event.target, "[data-comment-reply-cancel]");
if (cancelBtn && listEl.contains(cancelBtn)) {
clearReplyTarget();
return;
}
var sendInlineBtn = window.domClosest(event.target, "[data-comment-inline-send]");
if (sendInlineBtn && listEl.contains(sendInlineBtn)) {
var box = sendInlineBtn.closest("[data-comment-inline-composer]");
sendInlineReply(box);
return;
}
var replyBtn = window.domClosest(event.target, "[data-comment-reply]");
if (!replyBtn || !listEl.contains(replyBtn)) return;
if (root.getAttribute("data-logged-in") !== "1") {
var loginBtn = document.querySelector("[data-login-open]");
if (loginBtn) loginBtn.click();
return;
}
var parentsId = toInt(replyBtn.getAttribute("data-parents-id"), 0);
var commentId = toInt(replyBtn.getAttribute("data-comment-id"), parentsId);
if (parentsId <= 0) return;
var commentEl = replyBtn.closest(".play-comment");
openInlineComposer(commentEl, {
parentsId: parentsId,
replyCommentId: commentId,
replyName: replyBtn.getAttribute("data-reply-name") || "用户",
});
});
listEl.addEventListener("keydown", function (event) {
if (event.key !== "Enter" || event.shiftKey) return;
var inlineInput = window.domClosest(event.target, "[data-comment-inline-input]");
if (!inlineInput || !listEl.contains(inlineInput)) return;
event.preventDefault();
var box = inlineInput.closest("[data-comment-inline-composer]");
sendInlineReply(box);
});
}
})();
+169
View File
@@ -0,0 +1,169 @@
(function () {
var form = document.querySelector("[data-register-page-form]");
var password = document.querySelector("[data-register-page-password]");
var confirm = document.querySelector("[data-register-page-password-confirm]");
var submitBtn = document.querySelector("[data-register-page-submit]") || (form && form.querySelector('button[type="submit"]'));
var root = form || document;
function bindToggle(key, input) {
var btn = document.querySelector('[data-register-page-toggle="' + key + '"]');
var icon = document.querySelector('[data-register-page-toggle-icon="' + key + '"]');
if (!btn || !input || !icon) return;
btn.addEventListener("click", function () {
var show = input.getAttribute("type") === "password";
input.setAttribute("type", show ? "text" : "password");
icon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg");
btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
});
}
bindToggle("password", password);
bindToggle("confirm", confirm);
function clearFieldError(name) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.remove("is-error");
if (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
}
}
function clearAllErrors() {
root.querySelectorAll("[data-field]").forEach(function (el) {
el.classList.remove("is-error");
});
root.querySelectorAll("[data-field-error]").forEach(function (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
});
}
function setFieldError(name, message) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.add("is-error");
if (err) {
err.hidden = false;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = message || "";
}
}
function isAccountNo(value) {
return /^[A-Za-z0-9]{6,20}$/.test(String(value || ""));
}
function isPassword(value) {
var pwd = String(value || "");
if (pwd.length < 8) return false;
return /[A-Za-z]/.test(pwd) && /[0-9]/.test(pwd);
}
function validateForm() {
clearAllErrors();
var fd = new FormData(form);
var account = String(fd.get("account") || "").trim();
var pwd = String(fd.get("password") || "");
var pwd2 = String(fd.get("passwordConfirm") || "");
var agree = form.querySelector('input[name="agree"]');
var valid = true;
if (!account) {
setFieldError("account", "请输入账号");
valid = false;
} else if (!isAccountNo(account)) {
setFieldError("account", "账号需为 6 到 20 位字母或数字");
valid = false;
}
if (!pwd) {
setFieldError("password", "请输入密码");
valid = false;
} else if (!isPassword(pwd)) {
setFieldError("password", "密码需 8 位以上,且同时包含字母与数字");
valid = false;
}
if (!pwd2) {
setFieldError("passwordConfirm", "请再次输入密码");
valid = false;
} else if (pwd2 !== pwd) {
setFieldError("passwordConfirm", "两次输入的密码不一致");
valid = false;
}
if (agree && !agree.checked) {
setFieldError("agree", "请先同意用户条款与隐私政策");
valid = false;
}
if (!valid) return null;
return { accountNo: account, password: pwd };
}
function mapServerError(err) {
var tip = (err && err.message) || "注册失败,请稍后重试";
var code = err && err.code;
if (code === 5005 || /已存在|重复|占用/.test(tip)) {
setFieldError("account", tip);
return;
}
if (/密码|password/i.test(tip)) {
setFieldError("password", tip);
return;
}
setFieldError("account", tip);
}
function setSubmitting(loading) {
if (!submitBtn) return;
submitBtn.disabled = !!loading;
submitBtn.classList.toggle("is-loading", !!loading);
submitBtn.textContent = loading ? "注册中…" : "注册";
}
if (form) {
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("input", function () {
var name = input.getAttribute("name");
if (name === "agree") clearFieldError("agree");
else if (name) clearFieldError(name);
});
if (input.type === "checkbox") {
input.addEventListener("change", function () {
clearFieldError("agree");
});
}
});
form.addEventListener("submit", function (event) {
event.preventDefault();
if (!window.AuthAPI || !window.HttpRequest) {
setFieldError("account", "认证脚本未加载,请刷新页面");
return;
}
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
var payload = validateForm();
if (!payload) return;
setSubmitting(true);
AuthAPI.register(payload)
.then(function (res) {
AuthAPI.toast((res && res.tip) || "注册成功", "success");
window.setTimeout(function () {
window.location.href = "/";
}, 400);
})
.catch(function (err) {
setSubmitting(false);
mapServerError(err);
AuthAPI.toast((err && err.message) || "注册失败", "error");
});
});
}
})();
+256
View File
@@ -0,0 +1,256 @@
(function () {
var root = document.querySelector("[data-register-pop]");
if (!root) return;
var form = root.querySelector("[data-register-form]");
var password = root.querySelector("[data-register-password]");
var confirm = root.querySelector("[data-register-password-confirm]");
var submitBtn = root.querySelector("[data-register-submit]") || (form && form.querySelector('button[type="submit"]'));
var previousOverflow = "";
function closeLogin() {
if (typeof window.closeLoginPop === "function") {
window.closeLoginPop();
return;
}
var login = document.querySelector("[data-login-pop]");
if (login) login.classList.remove("is-open");
}
function closeUserMenu() {
var menu = document.querySelector("[data-user-menu].is-open");
if (menu) {
menu.classList.remove("is-open");
var t = menu.querySelector("[data-user-menu-toggle]");
if (t) t.setAttribute("aria-expanded", "false");
}
}
function clearFieldError(name) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.remove("is-error");
if (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
}
}
function clearAllErrors() {
root.querySelectorAll("[data-field]").forEach(function (el) {
el.classList.remove("is-error");
});
root.querySelectorAll("[data-field-error]").forEach(function (err) {
err.hidden = true;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = "";
});
}
function setFieldError(name, message) {
var field = root.querySelector('[data-field="' + name + '"]');
var err = root.querySelector('[data-field-error="' + name + '"]');
if (field) field.classList.add("is-error");
if (err) {
err.hidden = false;
var text = err.querySelector("[data-field-error-text]");
if (text) text.textContent = message || "";
}
}
function isAccountNo(value) {
return /^[A-Za-z0-9]{6,20}$/.test(String(value || ""));
}
function isPassword(value) {
var pwd = String(value || "");
if (pwd.length < 8) return false;
return /[A-Za-z]/.test(pwd) && /[0-9]/.test(pwd);
}
function bindingMessage(message, fallback) {
return String(message || fallback || "").replace(/注册/g, "绑定");
}
function validateForm() {
clearAllErrors();
var fd = new FormData(form);
var account = String(fd.get("account") || "").trim();
var pwd = String(fd.get("password") || "");
var pwd2 = String(fd.get("passwordConfirm") || "");
var agree = form.querySelector('input[name="agree"]');
var valid = true;
if (!account) {
setFieldError("account", "请输入账号");
valid = false;
} else if (!isAccountNo(account)) {
setFieldError("account", "账号需为 6 到 20 位字母或数字");
valid = false;
}
if (!pwd) {
setFieldError("password", "请输入密码");
valid = false;
} else if (!isPassword(pwd)) {
setFieldError("password", "密码需 8 位以上,且同时包含字母与数字");
valid = false;
}
if (!pwd2) {
setFieldError("passwordConfirm", "请再次输入密码");
valid = false;
} else if (pwd2 !== pwd) {
setFieldError("passwordConfirm", "两次输入的密码不一致");
valid = false;
}
if (agree && !agree.checked) {
setFieldError("agree", "请先同意用户条款与隐私政策");
valid = false;
}
if (!valid) return null;
return { accountNo: account, password: pwd };
}
function mapServerError(err) {
var tip = bindingMessage(err && err.message, "绑定失败,请稍后重试");
var code = err && err.code;
if (code === 5005 || /已存在|重复|占用/.test(tip)) {
setFieldError("account", tip);
return;
}
if (/密码|password/i.test(tip)) {
setFieldError("password", tip);
return;
}
setFieldError("account", tip);
}
function open(event) {
if (event) event.preventDefault();
closeLogin();
closeUserMenu();
clearAllErrors();
root.classList.add("is-open");
previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
var first = root.querySelector(".register-pop__input");
if (first) {
window.setTimeout(function () {
first.focus();
}, 0);
}
}
function close() {
root.classList.remove("is-open");
document.body.style.overflow = previousOverflow;
clearAllErrors();
}
window.openRegisterPop = open;
window.closeRegisterPop = close;
function bindToggle(key, input) {
var btn = root.querySelector('[data-register-toggle="' + key + '"]');
var icon = root.querySelector('[data-register-toggle-icon="' + key + '"]');
if (!btn || !input || !icon) return;
btn.addEventListener("click", function () {
var show = input.getAttribute("type") === "password";
input.setAttribute("type", show ? "text" : "password");
icon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg");
btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
});
}
document.addEventListener("click", function (event) {
var opener = event.target.closest("[data-register-open]");
if (opener) {
open(event);
return;
}
// 站内注册入口统一使用绑定账号弹窗
var registerLink = event.target.closest('a[href="/register"], a[href="/register/"], a[href="#register"]');
if (registerLink) {
open(event);
return;
}
if (!root.classList.contains("is-open")) return;
if (event.target.closest("[data-register-close]")) {
close();
}
});
bindToggle("password", password);
bindToggle("confirm", confirm);
function setSubmitting(loading) {
if (!submitBtn) return;
submitBtn.disabled = !!loading;
submitBtn.classList.toggle("is-loading", !!loading);
submitBtn.textContent = loading ? "绑定中…" : "绑定";
}
if (form) {
form.querySelectorAll("input").forEach(function (input) {
input.addEventListener("input", function () {
var name = input.getAttribute("name");
if (name === "agree") clearFieldError("agree");
else if (name) clearFieldError(name);
});
if (input.type === "checkbox") {
input.addEventListener("change", function () {
clearFieldError("agree");
});
}
});
form.addEventListener("submit", function (event) {
event.preventDefault();
if (!window.AuthAPI || !window.HttpRequest) {
setFieldError("account", "认证脚本未加载,请刷新页面");
return;
}
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
var payload = validateForm();
if (!payload) return;
setSubmitting(true);
AuthAPI.register(payload)
.then(function (res) {
AuthAPI.toast(bindingMessage(res && res.tip, "绑定成功"), "success");
window.setTimeout(function () {
close();
window.location.reload();
}, 400);
})
.catch(function (err) {
setSubmitting(false);
mapServerError(err);
AuthAPI.toast(bindingMessage(err && err.message, "绑定失败"), "error");
});
});
}
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && root.classList.contains("is-open")) {
close();
}
});
try {
var params = new URLSearchParams(window.location.search || "");
if (params.get("auth") === "register") {
params.delete("auth");
var qs = params.toString();
window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : "") + window.location.hash);
open();
}
} catch (e) {}
})();
+246
View File
@@ -0,0 +1,246 @@
(function () {
var root = document.querySelector("[data-recharge-modal]");
if (!root) return;
var panel = root.querySelector(".recharge-modal__panel");
var openers = document.querySelectorAll("[data-recharge-open]");
var closers = root.querySelectorAll("[data-recharge-close]");
var packagesRoot = root.querySelector("[data-recharge-packages]");
var methodsRoot = root.querySelector("[data-recharge-methods]");
var amount = root.querySelector("[data-recharge-amount]");
var pay = root.querySelector("[data-recharge-pay]");
var status = root.querySelector("[data-recharge-status]");
var previousOverflow = "";
var returnFocus = null;
var paying = false;
function packages() {
return root.querySelectorAll("[data-recharge-package]");
}
function methods() {
return root.querySelectorAll("[data-recharge-method]");
}
function showStatus(message, ok) {
if (!status) return;
status.textContent = message || "";
status.hidden = !message;
status.classList.toggle("is-ok", !!ok);
}
function open(trigger) {
if (!root.hidden) return;
var oldDialog = document.querySelector("[data-play-coin].is-open");
if (oldDialog) {
var oldClose = oldDialog.querySelector("button[data-play-coin-close]");
if (oldClose) oldClose.click();
}
returnFocus = trigger && oldDialog ? document.querySelector("[data-play-coin-open]") : trigger;
previousOverflow = document.body.style.overflow;
showStatus("");
root.hidden = false;
document.body.style.overflow = "hidden";
if (panel) panel.focus();
}
function close() {
if (root.hidden) return;
root.hidden = true;
document.body.style.overflow = previousOverflow;
if (returnFocus && typeof returnFocus.focus === "function") returnFocus.focus();
}
function parsePayMethods(raw) {
try {
var list = JSON.parse(raw || "[]");
return Array.isArray(list) ? list : [];
} catch (e) {
return [];
}
}
function renderMethods(list) {
if (!methodsRoot) return;
methodsRoot.replaceChildren();
if (!list.length) {
var empty = document.createElement("p");
empty.className = "recharge-modal__empty";
empty.textContent = "该套餐暂无可用支付方式";
methodsRoot.appendChild(empty);
return;
}
list.forEach(function (item, index) {
var type = String((item && item.type) || "").trim();
var name = String((item && item.typeName) || type || "支付").trim();
if (!type) return;
var btn = document.createElement("button");
btn.className = "recharge-modal__method" + (index === 0 ? " is-selected" : "");
btn.type = "button";
btn.setAttribute("data-recharge-method", type);
btn.setAttribute("aria-pressed", index === 0 ? "true" : "false");
btn.innerHTML =
'<span class="recharge-modal__method-check" aria-hidden="true"><img src="/static/images/placeholder.png?v=2" data-src="/static/images/recharge/check.svg" alt="" loading="lazy" decoding="async"></span>' +
'<span class="recharge-modal__method-copy"><span></span><small>即时到账</small></span>';
btn.querySelector(".recharge-modal__method-copy span").textContent = name;
methodsRoot.appendChild(btn);
});
if (typeof window.initLazyImages === "function") {
window.initLazyImages(methodsRoot);
}
}
function selectPackage(button) {
Array.prototype.forEach.call(packages(), function (item) {
var selected = item === button;
item.classList.toggle("is-selected", selected);
item.setAttribute("aria-pressed", String(selected));
});
if (amount) amount.textContent = "¥" + (button.getAttribute("data-price") || "0");
renderMethods(parsePayMethods(button.getAttribute("data-pay-methods")));
showStatus("");
}
function bindMethodClicks() {
if (!methodsRoot) return;
methodsRoot.addEventListener("click", function (event) {
var button = event.target.closest("[data-recharge-method]");
if (!button || !methodsRoot.contains(button)) return;
Array.prototype.forEach.call(methods(), function (item) {
var selected = item === button;
item.classList.toggle("is-selected", selected);
item.setAttribute("aria-pressed", String(selected));
});
showStatus("");
});
}
Array.prototype.forEach.call(openers, function (button) {
button.addEventListener("click", function (event) {
event.preventDefault();
open(button);
});
});
Array.prototype.forEach.call(closers, function (element) {
element.addEventListener("click", close);
});
if (packagesRoot) {
packagesRoot.addEventListener("click", function (event) {
var button = event.target.closest("[data-recharge-package]");
if (!button || !packagesRoot.contains(button)) return;
selectPackage(button);
});
}
bindMethodClicks();
if (pay) {
pay.addEventListener("click", function () {
if (paying) return;
var selectedPackage = root.querySelector("[data-recharge-package].is-selected");
var selectedMethod = root.querySelector("[data-recharge-method].is-selected");
var packageId = selectedPackage ? Number(selectedPackage.getAttribute("data-package-id") || 0) : 0;
var payMode = selectedMethod ? selectedMethod.getAttribute("data-recharge-method") : "";
if (!packageId) {
showStatus("请选择充值套餐");
return;
}
if (!payMode) {
showStatus("请选择支付方式");
return;
}
paying = true;
pay.disabled = true;
showStatus("正在创建支付订单…");
var payload = { packageId: packageId, payMode: payMode };
var req =
window.HttpRequest && typeof window.HttpRequest.post === "function"
? window.HttpRequest.post("/api/coin/buy", payload)
: fetch("/api/coin/buy", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then(function (r) {
return r.json().then(function (body) {
return body;
});
});
req
.then(function (result) {
if (!result || Number(result.code) !== 200 || !result.data || !result.data.payUrl) {
throw new Error((result && (result.tip || result.msg)) || "创建支付订单失败");
}
showStatus("订单创建成功,正在前往支付…", true);
openPayInNewWindow(result.data.payUrl);
paying = false;
pay.disabled = false;
})
.catch(function (error) {
showStatus((error && error.message) || "创建支付订单失败,请稍后重试");
paying = false;
pay.disabled = false;
});
});
}
function openPayInNewWindow(url) {
url = String(url || "").trim();
if (!url) return;
var opened = null;
try {
opened = window.open(url, "_blank", "noopener,noreferrer");
} catch (e) {
opened = null;
}
if (opened) {
try {
opened.opener = null;
} catch (e2) {}
showStatus("已在新窗口打开支付页面,请完成支付后返回本页", true);
return;
}
// 弹窗被拦截时用 <a target=_blank> 再试,仍不跳转当前页
var link = document.createElement("a");
link.href = url;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatus("若未打开支付页,请允许本站弹窗后重试", true);
}
document.addEventListener("keydown", function (event) {
if (root.hidden) return;
if (event.key === "Escape") {
event.preventDefault();
close();
return;
}
if (event.key !== "Tab") return;
var focusable = root.querySelectorAll("button:not([disabled]), a[href]");
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (event.shiftKey && (document.activeElement === first || document.activeElement === panel)) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
window.openRechargeModal = function (trigger) {
open(trigger || null);
};
if (new URLSearchParams(window.location.search).get("recharge") === "1") open(null);
})();
+322
View File
@@ -0,0 +1,322 @@
(() => {
const root = document.querySelector('[data-recharge]');
if (!root) return;
const packages = Array.from(root.querySelectorAll('[data-recharge-package]'));
const methodsRoot = root.querySelector('[data-recharge-methods]');
const methodEmpty = root.querySelector('[data-recharge-method-empty]');
const payButton = root.querySelector('[data-recharge-pay]');
const price = root.querySelector('[data-recharge-price]');
const coins = root.querySelector('[data-recharge-coins]');
const status = root.querySelector('[data-recharge-status]');
const rowsRoot = root.querySelector('[data-recharge-rows]');
const filters = Array.from(root.querySelectorAll('[data-recharge-filter]'));
const pagination = root.querySelector('[data-recharge-pagination]');
const pageButtons = root.querySelector('[data-recharge-pages]');
const previous = root.querySelector('[data-recharge-prev]');
const next = root.querySelector('[data-recharge-next]');
const total = root.querySelector('[data-recharge-total]');
const empty = root.querySelector('[data-recharge-history-empty]');
const more = root.querySelector('[data-recharge-more]');
const exportBtn = root.querySelector('[data-recharge-export]');
const mobile = window.matchMedia('(max-width: 768px)');
let selectedPackage = null;
let selectedMethod = '';
let activeType = 0;
let currentPage = Number(pagination.dataset.page) || 1;
let pageSize = Number(pagination.dataset.pageSize) || 5;
let totalCount = Number(pagination.dataset.total) || 0;
let expanded = false;
function showStatus(message, success) {
status.textContent = message;
status.hidden = !message;
status.classList.toggle('is-success', Boolean(success));
}
function choose(button, choices) {
choices.forEach(choice => {
const isSelected = choice === button;
choice.classList.toggle('is-selected', isSelected);
choice.setAttribute('aria-checked', String(isSelected));
});
}
function parseMethods(button) {
try {
const parsed = JSON.parse(button.dataset.payMethods || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch (_) {
return [];
}
}
function renderMethods(items) {
methodsRoot.replaceChildren();
selectedMethod = '';
items.forEach((item, index) => {
const type = String(item.type || '').trim();
if (!type) return;
const button = document.createElement('button');
button.className = 'recharge-method';
button.type = 'button';
button.setAttribute('role', 'radio');
button.dataset.rechargeMethod = type;
button.innerHTML = '<span class="recharge-method__radio"><img src="/static/images/placeholder.png?v=2" data-src="/static/images/recharge/check.svg" alt="" loading="lazy" decoding="async"></span><span><strong></strong><small>支付成功后到账</small></span>';
button.querySelector('strong').textContent = item.typeName || type;
button.addEventListener('click', () => {
const choices = Array.from(methodsRoot.querySelectorAll('[data-recharge-method]'));
choose(button, choices);
selectedMethod = type;
showStatus('');
});
methodsRoot.appendChild(button);
if (index === 0) {
button.classList.add('is-selected');
button.setAttribute('aria-checked', 'true');
selectedMethod = type;
} else {
button.setAttribute('aria-checked', 'false');
}
});
const hasMethod = Boolean(selectedMethod);
methodEmpty.hidden = hasMethod;
payButton.disabled = !selectedPackage || !hasMethod;
if (typeof window.initLazyImages === 'function') {
window.initLazyImages(methodsRoot);
}
}
function selectPackage(button) {
selectedPackage = button;
choose(button, packages);
price.textContent = button.dataset.price || '—';
coins.textContent = button.dataset.coins || '—';
renderMethods(parseMethods(button));
showStatus('');
}
packages.forEach(button => button.addEventListener('click', () => selectPackage(button)));
const initialPackage = packages.find(button => button.classList.contains('is-selected')) || packages[0];
if (initialPackage) selectPackage(initialPackage);
else payButton.disabled = true;
payButton.addEventListener('click', async () => {
if (!selectedPackage || !selectedMethod) {
showStatus('请选择套餐和支付方式');
return;
}
payButton.disabled = true;
showStatus('正在创建支付订单…');
const payload = {
packageId: Number(selectedPackage.dataset.packageId),
payMode: selectedMethod,
};
try {
let result;
if (window.HttpRequest && typeof window.HttpRequest.post === 'function') {
result = await window.HttpRequest.post('/api/coin/buy', payload);
} else {
const response = await fetch('/api/coin/buy', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
result = await response.json();
}
if (!result || Number(result.code) !== 200 || !result.data || !result.data.payUrl) {
if (result && (Number(result.code) === 401 || Number(result.code) === 1000)) {
const login = document.querySelector('[data-login-open]');
if (login) login.click();
else window.location.href = '/login';
return;
}
throw new Error((result && (result.tip || result.msg)) || '创建支付订单失败');
}
showStatus('订单创建成功,正在前往支付…', true);
openPayInNewWindow(result.data.payUrl);
payButton.disabled = false;
} catch (error) {
showStatus(error.message || '创建支付订单失败,请稍后重试');
payButton.disabled = false;
}
});
function openPayInNewWindow(url) {
url = String(url || '').trim();
if (!url) return;
let opened = null;
try {
opened = window.open(url, '_blank', 'noopener,noreferrer');
} catch (e) {
opened = null;
}
if (opened) {
try {
opened.opener = null;
} catch (e2) {}
showStatus('已在新窗口打开支付页面,请完成支付后返回本页', true);
return;
}
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatus('若未打开支付页,请允许本站弹窗后重试', true);
}
function appendText(parent, tag, className, value) {
const node = document.createElement(tag);
node.className = className;
node.textContent = value || '';
parent.appendChild(node);
return node;
}
function renderRows(items) {
rowsRoot.replaceChildren();
items.forEach(item => {
const row = document.createElement('div');
row.className = 'recharge-history__row';
row.dataset.rechargeRow = '';
row.dataset.category = item.category || '';
appendText(row, 'span', 'recharge-history__kind', item.kind);
const detail = document.createElement('span');
detail.className = 'recharge-history__detail-line';
appendText(detail, 'span', 'recharge-history__description', item.description);
const time = document.createElement('time');
time.className = 'recharge-history__time';
appendText(time, 'span', 'only-desktop', item.time);
appendText(time, 'span', 'only-mobile', item.mobileTime);
detail.appendChild(time);
row.appendChild(detail);
const amount = appendText(row, 'strong', 'recharge-history__amount', item.amount);
amount.classList.toggle('is-credit', Boolean(item.credit));
rowsRoot.appendChild(row);
});
empty.hidden = items.length > 0;
empty.textContent = '暂无记录';
}
function renderPagination() {
const pageCount = Math.ceil(totalCount / pageSize);
currentPage = Math.min(currentPage, Math.max(1, pageCount));
pagination.hidden = totalCount === 0;
more.hidden = totalCount <= 5 && !expanded;
previous.disabled = currentPage <= 1;
next.disabled = currentPage >= pageCount;
total.textContent = '共 ' + totalCount + ' 条';
pageButtons.replaceChildren();
const firstPage = Math.max(1, Math.min(currentPage - 1, Math.max(1, pageCount - 3)));
for (let page = firstPage; page <= Math.min(pageCount, firstPage + 3); page += 1) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = String(page);
button.classList.toggle('is-active', page === currentPage);
if (page === currentPage) button.setAttribute('aria-current', 'page');
button.addEventListener('click', () => loadHistory(page));
pageButtons.appendChild(button);
}
}
async function loadHistory(page) {
empty.hidden = false;
empty.textContent = '加载中…';
try {
const query = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
type: String(activeType),
});
const url = '/api/coin/trans/list?' + query.toString();
let result;
if (window.HttpRequest && typeof window.HttpRequest.get === 'function') {
result = await window.HttpRequest.get(url);
} else {
const response = await fetch(url, {credentials: 'same-origin'});
result = await response.json();
}
if (!result || Number(result.code) !== 200 || !result.data) {
throw new Error((result && (result.tip || result.msg)) || '交易明细加载失败');
}
currentPage = Number(result.data.pageNum) || page;
totalCount = Number(result.data.totalCount) || 0;
renderRows(Array.isArray(result.data.list) ? result.data.list : []);
renderPagination();
} catch (error) {
rowsRoot.replaceChildren();
empty.hidden = false;
empty.textContent = error.message || '交易明细加载失败';
pagination.hidden = true;
}
}
async function exportHistory() {
if (!exportBtn) return;
exportBtn.disabled = true;
exportBtn.classList.add('is-loading');
try {
const query = new URLSearchParams({type: String(activeType)});
const url = '/api/coin/trans/export?' + query.toString();
const response = await fetch(url, {credentials: 'same-origin'});
const contentType = (response.headers.get('Content-Type') || '').toLowerCase();
if (contentType.includes('application/json')) {
const result = await response.json();
throw new Error((result && (result.tip || result.msg)) || '导出失败');
}
if (!response.ok) {
throw new Error('导出失败');
}
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
let filename = 'coin-trans.csv';
const match = /filename\*?=(?:UTF-8''|")?([^\";]+)/i.exec(disposition);
if (match && match[1]) {
filename = decodeURIComponent(match[1].replace(/"/g, '').trim()) || filename;
}
const objectURL = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectURL;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectURL);
showStatus('导出成功', true);
} catch (error) {
showStatus(error.message || '导出失败', false);
} finally {
exportBtn.disabled = false;
exportBtn.classList.remove('is-loading');
}
}
filters.forEach(button => button.addEventListener('click', () => {
activeType = Number(button.dataset.rechargeFilter) || 0;
filters.forEach(option => {
const selected = option === button;
option.classList.toggle('is-active', selected);
option.setAttribute('aria-pressed', String(selected));
});
loadHistory(1);
}));
previous.addEventListener('click', () => { if (currentPage > 1) loadHistory(currentPage - 1); });
next.addEventListener('click', () => loadHistory(currentPage + 1));
if (exportBtn) {
exportBtn.addEventListener('click', () => exportHistory());
}
more.addEventListener('click', () => {
expanded = !expanded;
pageSize = expanded ? 100 : 5;
more.firstChild.textContent = expanded ? '收起 ' : '全部 ';
more.setAttribute('aria-expanded', String(expanded));
loadHistory(1);
});
renderPagination();
})();
+106
View File
@@ -0,0 +1,106 @@
(function () {
var grid = document.querySelector('[data-search-grid]');
if (!grid) return;
var cards = Array.from(grid.querySelectorAll('.search-card'));
var pagination = document.querySelector('[data-pagination]');
var pageNumbers = pagination.querySelector('[data-page-numbers]');
var previous = pagination.querySelector('[data-page-prev]');
var next = pagination.querySelector('[data-page-next]');
var results = document.querySelector('.search-results');
var page = pageFromURL();
function pageFromURL() {
var match = window.location.pathname.match(/\/page\/(\d+)\/?$/);
var value = match ? Number(match[1]) : 1;
return Number.isInteger(value) && value > 0 ? value : 1;
}
function updateURL(replace) {
var basePath = window.location.pathname.replace(/\/page\/\d+\/?$/, '').replace(/\/$/, '');
var path = basePath || '/search';
if (page > 1) path += '/page/' + String(page);
if (path !== window.location.pathname) {
window.history[replace ? 'replaceState' : 'pushState']({}, '', path);
}
}
function pageTokens(total, mobile) {
var tokens = [];
var visibleEdge = mobile ? 3 : 5;
var i;
if (total <= visibleEdge + 2) {
for (i = 1; i <= total; i++) tokens.push(i);
} else if (page <= (mobile ? 2 : 3)) {
for (i = 1; i <= visibleEdge; i++) tokens.push(i);
tokens.push('ellipsis', total);
} else if (page >= total - (mobile ? 1 : 2)) {
tokens.push(1, 'ellipsis');
for (i = total - visibleEdge + 1; i <= total; i++) tokens.push(i);
} else if (mobile) {
tokens.push(1, 'ellipsis', page, 'ellipsis', total);
} else {
tokens.push(1, 'ellipsis', page - 1, page, page + 1, 'ellipsis', total);
}
return tokens;
}
function render() {
var mobile = window.matchMedia('(max-width: 900px)').matches;
var pageSize = mobile ? 9 : 12;
var totalPages = Math.max(1, Math.ceil(cards.length / pageSize));
var start;
if (page > totalPages) {
page = totalPages;
updateURL(true);
}
start = (page - 1) * pageSize;
cards.forEach(function (card, index) {
card.hidden = index < start || index >= start + pageSize;
});
previous.disabled = page === 1;
next.disabled = page === totalPages;
pageNumbers.replaceChildren();
pageTokens(totalPages, mobile).forEach(function (token) {
if (token === 'ellipsis') {
var dots = document.createElement('span');
dots.className = 'search-pagination__ellipsis';
dots.textContent = '…';
dots.setAttribute('aria-hidden', 'true');
pageNumbers.appendChild(dots);
return;
}
var button = document.createElement('button');
button.type = 'button';
button.textContent = String(token);
button.setAttribute('aria-label', '第 ' + token + ' 页');
if (token === page) {
button.className = 'is-current';
button.setAttribute('aria-current', 'page');
}
button.addEventListener('click', function () { goToPage(token); });
pageNumbers.appendChild(button);
});
}
function goToPage(number) {
var totalPages = Math.max(1, Math.ceil(cards.length / (window.matchMedia('(max-width: 900px)').matches ? 9 : 12)));
if (number < 1 || number > totalPages || number === page) return;
page = number;
updateURL(false);
render();
results.scrollIntoView({ block: 'start' });
}
previous.addEventListener('click', function () { goToPage(page - 1); });
next.addEventListener('click', function () { goToPage(page + 1); });
window.addEventListener('popstate', function () {
page = pageFromURL();
render();
});
window.addEventListener('resize', render);
render();
})();
+143
View File
@@ -0,0 +1,143 @@
(() => {
const form = document.querySelector('[data-setting-form]');
if (!form) return;
const status = form.querySelector('[data-setting-status]');
const save = form.querySelector('[data-setting-save]');
const passwordPop = document.querySelector('[data-setting-password-pop]');
const passwordForm = document.querySelector('[data-setting-password-form]');
const passwordOpen = document.querySelector('[data-setting-password-open]');
const passwordStatus = document.querySelector('[data-setting-password-status]');
const passwordSubmit = document.querySelector('[data-setting-password-submit]');
let previousOverflow = '';
function showStatus(message, success = false) {
status.textContent = message;
status.classList.toggle('is-success', success);
}
form.addEventListener('submit', async event => {
event.preventDefault();
const data = {};
const nickname = form.elements.nickName.value.trim();
const introduction = form.elements.introduction.value.trim();
if (!nickname || [...nickname].length > 40) {
showStatus('昵称须为 1 至 40 个字符');
form.elements.nickName.focus();
return;
}
if ([...introduction].length > 300) {
showStatus('简介不能超过 300 个字符');
return;
}
if (nickname !== form.elements.nickName.defaultValue) data.nickName = nickname;
if (introduction !== form.elements.introduction.defaultValue.trim()) data.introduction = introduction;
if (!Object.keys(data).length) {
showStatus('没有需要保存的修改');
return;
}
save.disabled = true;
showStatus('保存中…');
try {
const response = await fetch('/api/auth/profile', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok || result.code !== 200) throw new Error(result.tip || '保存失败,请稍后重试');
showStatus('保存成功', true);
window.setTimeout(() => window.location.reload(), 800);
} catch (error) {
showStatus(error.message || '保存失败,请稍后重试');
save.disabled = false;
}
});
function setPasswordStatus(message, success = false) {
if (!passwordStatus) return;
passwordStatus.textContent = message || '';
passwordStatus.classList.toggle('is-success', success);
}
function openPasswordPop() {
if (!passwordPop || !passwordForm) return;
passwordForm.reset();
setPasswordStatus('');
previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
passwordPop.hidden = false;
const first = passwordForm.elements.oldPassword;
if (first) window.setTimeout(() => first.focus(), 0);
}
function closePasswordPop() {
if (!passwordPop || passwordPop.hidden) return;
passwordPop.hidden = true;
document.body.style.overflow = previousOverflow;
if (passwordForm) passwordForm.reset();
setPasswordStatus('');
if (passwordOpen) passwordOpen.focus();
}
if (passwordOpen) passwordOpen.addEventListener('click', openPasswordPop);
if (passwordPop) {
passwordPop.querySelectorAll('[data-setting-password-close]').forEach(button => {
button.addEventListener('click', closePasswordPop);
});
}
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && passwordPop && !passwordPop.hidden) closePasswordPop();
});
if (passwordForm) {
passwordForm.addEventListener('submit', async event => {
event.preventDefault();
const oldPassword = passwordForm.elements.oldPassword.value;
const newPassword = passwordForm.elements.newPassword.value;
const confirmPassword = passwordForm.elements.confirmPassword.value;
if (oldPassword.length < 4 || oldPassword.length > 16) {
setPasswordStatus('请输入 4–16 位当前密码');
passwordForm.elements.oldPassword.focus();
return;
}
if (newPassword.length < 4 || newPassword.length > 16) {
setPasswordStatus('请输入 4–16 位新密码');
passwordForm.elements.newPassword.focus();
return;
}
if (newPassword !== confirmPassword) {
setPasswordStatus('两次输入的新密码不一致');
passwordForm.elements.confirmPassword.focus();
return;
}
passwordSubmit.disabled = true;
passwordSubmit.textContent = '修改中…';
setPasswordStatus('');
try {
const response = await fetch('/api/auth/profile', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldPassword, newPassword }),
});
const result = await response.json();
if (!response.ok || result.code !== 200) throw new Error(result.tip || '密码修改失败,请稍后重试');
setPasswordStatus('密码修改成功', true);
window.setTimeout(() => {
closePasswordPop();
window.location.reload();
}, 600);
} catch (error) {
setPasswordStatus(error.message || '密码修改失败,请稍后重试');
passwordSubmit.disabled = false;
passwordSubmit.textContent = '确认修改';
}
});
}
})();
+921
View File
@@ -0,0 +1,921 @@
/**
* Web SDK v1.2.0 接入层。
* 页面/点击/导航沿用 jha-1043 的口径,短剧使用最新版 dramaEvent/dramaStatusChange。
*/
(function (global) {
"use strict";
var state = {
initialized: false,
initializing: false,
queue: [],
page: { key: "internal_other", name: "站内其他页面" },
referrer: { key: "direct", name: "直接访问" },
userType: "normal",
pageStartedAt: Date.now(),
dramaViewSent: false,
adShown: {},
};
function stringValue(value, fallback) {
var result = value == null ? "" : String(value).trim();
return result || (fallback == null ? "" : String(fallback));
}
function integer(value, fallback) {
var result = parseInt(value, 10);
return isFinite(result) ? result : (fallback || 0);
}
function clamp(value, min, max) {
var result = Number(value);
if (!isFinite(result)) result = min;
return Math.max(min, Math.min(max, result));
}
function sdkAvailable() {
return !!(global.WebSDK && global.WebSDK.track);
}
function logError() {}
function invoke(method, payload) {
if (!state.initialized || !sdkAvailable()) {
return new Promise(function (resolve) {
state.queue.push([method, payload, resolve]);
});
}
try {
var fn = global.WebSDK.track[method];
if (typeof fn !== "function") {
var missing = new Error("SDK method does not exist: " + method);
logError(missing);
return Promise.resolve({ sent: false, ok: false, reason: "unknown_method" });
}
var result = fn(payload);
return Promise.resolve(result).then(function (response) {
if (response && response.ok === false) {
logError(method + " rejected: " + JSON.stringify(response));
}
return response;
}).catch(function (error) {
logError(error);
return { sent: false, ok: false, reason: "exception" };
});
} catch (error) {
logError(error);
return Promise.resolve({ sent: false, ok: false, reason: "exception" });
}
}
function flushQueue() {
var pending = state.queue.splice(0, state.queue.length);
pending.forEach(function (entry) {
Promise.resolve(invoke(entry[0], entry[1])).then(entry[2]);
});
}
function currentChannel(value) {
var channel = stringValue(value);
if (!channel && global.ChannelAttribution && typeof global.ChannelAttribution.get === "function") {
channel = stringValue(global.ChannelAttribution.get());
}
if (!channel) channel = stringValue(global.__CHANNEL_CODE__, "organic");
var normalized = channel.toLowerCase();
if (["self", "natural", "official", "channel", "channel://natural"].indexOf(normalized) >= 0) {
return "organic";
}
if (!channel || channel.length > 50 || !/^[A-Za-z0-9_.-]+$/.test(channel) ||
["{}", "0", "-", "unknown"].indexOf(normalized) >= 0) {
return "organic";
}
return channel;
}
function normalizeUid(value) {
var uid = stringValue(value);
if (uid === "0" || uid.length < 4 || uid.length > 32 || !/^[A-Za-z0-9_]+$/.test(uid) ||
["u_1001", "stress_user", "u_lab"].indexOf(uid.toLowerCase()) >= 0) return "";
return uid;
}
function inferPage(path) {
path = stringValue(path, "/").replace(/\/$/, "") || "/";
if (path === "/") return { key: "home", name: "首页" };
var rules = [
[/^\/series-play(?:\/|$)/, "series_play", "AI成人剧集播放页"],
[/^\/series(?:\/|$)/, "series", "AI成人剧集"],
[/^\/play(?:\/|$)/, "play", "短剧播放页"],
[/^\/detail(?:\/|$)/, "detail", "短剧详情"],
[/^\/list(?:\/|$)/, "list", "短剧列表"],
[/^\/rank(?:\/|$)/, "rank", "排行榜"],
[/^\/topic-detail(?:\/|$)/, "topic_detail", "专题详情"],
[/^\/topic(?:\/|$)/, "topic", "专题列表"],
[/^\/tags(?:\/|$)/, "tags", "标签详情"],
[/^\/search(?:-mobile)?(?:\/|$)/, "search", "搜索结果页"],
[/^\/history(?:\/|$)/, "history", "观看历史"],
[/^\/favorites(?:\/|$)/, "favorites", "我的收藏"],
[/^\/profile(?:\/|$)/, "profile", "个人中心"],
];
for (var i = 0; i < rules.length; i += 1) {
if (rules[i][0].test(path)) return { key: rules[i][1], name: rules[i][2] };
}
return { key: "internal_other", name: "站内其他页面" };
}
function readReferrer() {
try {
var saved = global.sessionStorage.getItem("__tracker_last_page");
if (saved) {
var parsed = JSON.parse(saved);
if (parsed && parsed.key) return parsed;
}
} catch (error) {}
try {
if (document.referrer) {
var url = new URL(document.referrer, global.location.href);
if (url.host === global.location.host) return inferPage(url.pathname);
return { key: "external", name: url.host || "外部站点" };
}
} catch (error) {}
return { key: "direct", name: "直接访问" };
}
function rememberPage() {
try { global.sessionStorage.setItem("__tracker_last_page", JSON.stringify(state.page)); } catch (error) {}
}
function init(options) {
if (state.initialized || state.initializing) return;
options = options || {};
var inferred = inferPage(global.location && global.location.pathname);
state.page = {
key: stringValue(options.pageKey, inferred.key),
name: stringValue(options.pageName, inferred.name),
};
state.userType = options.userType === "vip" ? "vip" : "normal";
state.referrer = readReferrer();
state.initializing = true;
var attempts = 0;
var timer = global.setInterval(function () {
attempts += 1;
if (!sdkAvailable() && attempts < 100) return;
global.clearInterval(timer);
if (!sdkAvailable()) {
state.initializing = false;
return;
}
try {
var result = global.WebSDK.init({
appId: stringValue(options.appId),
uid: normalizeUid(options.uid),
channel: currentChannel(options.channel),
encryptedConfig: stringValue(options.encryptedConfig),
appVersion: stringValue(options.appVersion, "1.0.0"),
disableAutoEvents: ["app_page_view", "page_click"],
});
Promise.resolve(result).then(function () {
state.initialized = true;
state.initializing = false;
__commonFieldsReady = null;
loadCommonFields();
flushQueue();
onReady();
}).catch(function (error) {
state.initializing = false;
logError(error);
});
} catch (error) {
state.initializing = false;
logError(error);
}
}, 50);
}
function whenDOMReady(callback) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", callback, { once: true });
} else {
callback();
}
}
function trackPageView(extra) {
extra = extra || {};
invoke("appPageView", {
user_type: stringValue(extra.user_type, state.userType),
page_key: state.page.key,
page_name: state.page.name,
referrer_page_key: state.referrer.key,
referrer_page_name: state.referrer.name,
current_page_key: state.page.key,
current_page_name: state.page.name,
page_load_time: pageLoadDuration(),
recommend_trace_id: stringValue(extra.recommend_trace_id),
});
rememberPage();
}
function pageLoadDuration() {
try {
if (global.performance && typeof global.performance.getEntriesByType === "function") {
var entries = global.performance.getEntriesByType("navigation");
if (entries && entries[0]) {
var navigation = entries[0];
var end = navigation.loadEventEnd || navigation.domContentLoadedEventEnd || global.performance.now();
if (isFinite(end)) return Math.max(0, Math.round(end));
}
}
if (global.performance && typeof global.performance.now === "function") {
return Math.max(0, Math.round(global.performance.now()));
}
} catch (error) {}
return Math.max(0, Date.now() - state.pageStartedAt);
}
function trackPageClick(event) {
var doc = document.documentElement || {};
var viewportWidth = global.innerWidth || doc.clientWidth || 0;
var viewportHeight = global.innerHeight || doc.clientHeight || 0;
var x = integer(event.clientX, 0);
var y = integer(event.clientY, 0);
invoke("pageClick", {
page_key: state.page.key,
page_name: state.page.name,
click_page_x: x,
click_page_y: y,
click_x_percent: viewportWidth ? Math.round(clamp(x / viewportWidth * 100, 0, 100)) : 0,
click_y_percent: viewportHeight ? Math.round(clamp(y / viewportHeight * 100, 0, 100)) : 0,
screen_width: integer(global.screen && global.screen.width, 0),
screen_height: integer(global.screen && global.screen.height, 0),
recommend_trace_id: "",
});
}
function trackNavigation(anchor) {
if (!anchor || (!anchor.hasAttribute("data-navigation-key") && !anchor.hasAttribute("data-nav-key")) ||
!anchor.getAttribute("href")) return;
var href = anchor.getAttribute("href");
if (!href || href.charAt(0) === "#" || /^javascript:/i.test(href)) return;
var label = stringValue(anchor.getAttribute("aria-label") || anchor.textContent).replace(/\s+/g, " ");
var navigationKey = stringValue(
anchor.getAttribute("data-navigation-key") || anchor.getAttribute("data-nav-key")
);
if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(navigationKey)) {
navigationKey = "";
try {
var url = new URL(href, global.location.href);
if (url.origin !== global.location.origin) {
navigationKey = "nav_external";
} else {
var targetPage = inferPage(url.pathname);
if (targetPage.key !== "internal_other") {
navigationKey = "nav_" + targetPage.key;
} else {
var firstSegment = stringValue(url.pathname.split("/").filter(Boolean)[0])
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
.slice(0, 48);
navigationKey = firstSegment ? "nav_" + firstSegment : "nav_home";
}
}
} catch (error) {
navigationKey = "nav_other";
}
}
invoke("navigation", {
navigation_key: navigationKey.toLowerCase(),
navigation_name: stringValue(
anchor.getAttribute("data-navigation-name") || anchor.getAttribute("data-nav-name"),
label || navigationKey
).slice(0, 255),
});
}
function adPayload(node) {
var id = stringValue(node && node.getAttribute("data-ad-id"));
if (!id) return null;
var adType = stringValue(node.getAttribute("data-ad-type"), "banner");
var allSlots = document.querySelectorAll('[data-track-ad="external"][data-ad-id]');
var slotPosition = Array.prototype.indexOf.call(allSlots, node) + 1;
if (slotPosition <= 0) slotPosition = 1;
var fallbackSlotKey = state.page.key + "_content_banner_" + slotPosition;
var fallbackSlotName = state.page.name + "内容横幅第" + slotPosition + "位";
if (adType === "interstitial") {
fallbackSlotKey = state.page.key + "_popup_" + slotPosition;
fallbackSlotName = state.page.name + "弹窗广告位";
}
return {
page_key: state.page.key,
page_name: state.page.name,
ad_slot_key: stringValue(
node.getAttribute("data-ad-slot-key"),
fallbackSlotKey
),
ad_slot_name: stringValue(
node.getAttribute("data-ad-slot-name"),
fallbackSlotName
),
ad_id: id,
creative_id: stringValue(node.getAttribute("data-creative-id")),
ad_type: adType,
};
}
function bindAds() {
var selector = '[data-track-ad="external"][data-ad-id]';
var nodes = document.querySelectorAll(selector);
function show(node) {
trackExternalAd(node, "show");
}
if (global.IntersectionObserver) {
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
show(entry.target);
observer.unobserve(entry.target);
});
}, { threshold: 0.1 });
function observe(node) {
if (!node || node.__trackerAdObserved) return;
node.__trackerAdObserved = true;
observer.observe(node);
}
Array.prototype.forEach.call(nodes, observe);
if (global.MutationObserver) {
new MutationObserver(function (records) {
records.forEach(function (record) {
Array.prototype.forEach.call(record.addedNodes || [], function (node) {
if (!node || node.nodeType !== 1) return;
if (node.matches && node.matches(selector)) observe(node);
if (node.querySelectorAll) {
Array.prototype.forEach.call(node.querySelectorAll(selector), observe);
}
});
});
}).observe(document.documentElement, { childList: true, subtree: true });
}
} else {
Array.prototype.forEach.call(nodes, show);
}
}
function trackExternalAd(node, eventType) {
if (!node || (eventType !== "show" && eventType !== "click")) return;
var payload = adPayload(node);
if (!payload) return;
var key = payload.ad_slot_key + "|" + payload.ad_id;
if (!state.adShown[key]) {
state.adShown[key] = true;
invoke("adImpression", payload);
}
if (eventType === "click" && node.getAttribute("data-ad-clickable") === "1") {
return invoke("adClick", payload);
}
}
function bindExternalAdEvents() {
document.addEventListener("external-ad:track", function (event) {
var detail = event && event.detail ? event.detail : {};
trackExternalAd(detail.element, stringValue(detail.eventType));
});
}
function trackAdvertising(node, eventType) {
if (!node || ["show", "click", "close"].indexOf(eventType) < 0) return;
invoke("advertising", {
event_type: eventType,
advertising_key: stringValue(node.getAttribute("data-advertising-key"), "dialog_ad"),
advertising_name: stringValue(node.getAttribute("data-advertising-name"), "弹窗广告"),
advertising_id: stringValue(node.getAttribute("data-advertising-id") || node.getAttribute("data-dialog-ad-id")),
});
}
function bindAdvertisingEvents() {
document.addEventListener("dialog-ad:track", function (event) {
var detail = event && event.detail ? event.detail : {};
var node = detail.element;
if (!node || !node.getAttribute) return;
trackAdvertising(node, stringValue(detail.eventType));
});
}
function dramaRoot() {
return document.querySelector("[data-track-drama]");
}
function dramaContext(root) {
root = root || dramaRoot();
if (!root) return null;
var data = root.dataset || {};
var dramaID = stringValue(data.dramaId);
var videoID = stringValue(data.videoId);
if (!dramaID || !videoID) return null;
return {
media_id: stringValue(data.mediaId),
drama_id: dramaID,
drama_title: stringValue(data.dramaTitle, "未命名短剧"),
episode: Math.max(1, integer(data.episode, 1)),
total_episode: Math.max(1, integer(data.totalEpisode, 1)),
video_id: videoID,
drama_type_id: stringValue(data.dramaTypeId, "default"),
drama_type_name: stringValue(data.dramaTypeName, "默认分类"),
drama_content_type: "short_drama",
recommend_trace_id: stringValue(data.recommendTraceId),
drama_tag_key: stringValue(data.dramaTagKey, "default"),
drama_tag_name: stringValue(data.dramaTagName, "默认标签"),
drama_duration: Math.max(0, integer(data.dramaDuration, 0)),
};
}
var dramaBehaviorNames = {
ep_view: "展示",
ep_play: "播放",
ep_pause: "暂停",
ep_forward: "快进",
ep_rewind: "快退",
ep_complete: "播放完成",
ep_share: "分享",
};
function videoPlaybackState(video) {
if (!video) return null;
if (!video.__trackerPlaybackState) {
video.__trackerPlaybackState = {
startedAt: 0,
elapsedMs: 0,
seekFrom: null,
lastPosition: Number(video.currentTime) || 0,
};
}
return video.__trackerPlaybackState;
}
function beginPlayback(video) {
var playback = videoPlaybackState(video);
if (playback && !playback.startedAt) playback.startedAt = Date.now();
}
function settlePlayback(video) {
var playback = videoPlaybackState(video);
if (!playback || !playback.startedAt) return;
playback.elapsedMs += Math.max(0, Date.now() - playback.startedAt);
playback.startedAt = 0;
}
function playbackSeconds(video, keepDuration) {
var playback = videoPlaybackState(video);
if (!playback) return 0;
var now = Date.now();
var activeMs = playback.startedAt ? Math.max(0, now - playback.startedAt) : 0;
var seconds = Math.max(0, Math.round((playback.elapsedMs + activeMs) / 1000));
if (!keepDuration) {
playback.elapsedMs = 0;
if (playback.startedAt) playback.startedAt = now;
}
return seconds;
}
function videoMetrics(video, context, keepDuration) {
var duration = video && isFinite(video.duration) ? Math.round(video.duration) : context.drama_duration;
var current = video && isFinite(video.currentTime) ? Math.round(video.currentTime) : 0;
return {
duration: Math.max(0, duration),
current: Math.max(0, current),
played: playbackSeconds(video, keepDuration),
progress: duration > 0 ? Math.round(clamp(current / duration * 100, 0, 100)) : 0,
speed: Math.max(1, Math.round(((video && video.playbackRate) || 1) * 100)),
};
}
function trackDramaEvent(behaviorKey, video, options) {
var context = dramaContext();
if (!context) return;
options = options || {};
var metrics = videoMetrics(video, context, options.keepPlaybackDuration === true);
context.drama_duration = metrics.duration;
context.play_duration = metrics.played;
context.play_progress = options.forceProgress != null
? Math.round(clamp(options.forceProgress, 0, 100))
: behaviorKey === "ep_complete" ? 100 : metrics.progress;
context.play_speed = metrics.speed;
context.play_behavior_key = behaviorKey;
context.play_behavior_name = dramaBehaviorNames[behaviorKey] || behaviorKey;
return invoke("dramaEvent", context);
}
function trackDramaStatusChange(targetEpisode, targetVideoID, behaviorOverride) {
var context = dramaContext();
if (!context) return;
var nextEpisode = Math.max(1, integer(targetEpisode, context.episode));
var videoID = stringValue(targetVideoID, context.video_id);
var behavior = stringValue(behaviorOverride);
var names = {
ep_auto_play: "自动播放",
ep_previous: "上一集",
ep_next: "下一集",
ep_switch: "切换剧集",
ep_unlock: "解锁",
};
if (!behavior) {
if (nextEpisode === context.episode && videoID === context.video_id) return;
behavior = "ep_switch";
if (nextEpisode === context.episode + 1) behavior = "ep_next";
else if (nextEpisode === context.episode - 1) behavior = "ep_previous";
}
if (!names[behavior]) return;
return invoke("dramaStatusChange", {
media_id: context.media_id,
drama_id: context.drama_id,
drama_title: context.drama_title,
episode: nextEpisode,
video_id: videoID,
pre_episode: context.episode,
drama_behavior_key: behavior,
drama_behavior_name: names[behavior],
});
}
function trackDramaUnlock() {
var context = dramaContext();
if (!context) return Promise.resolve({ sent: false, ok: false, reason: "no_drama_context" });
return trackDramaStatusChange(context.episode, context.video_id, "ep_unlock");
}
function isMainDramaVideo(root, video) {
var mode = video && video.__trackerDramaMode;
if (!mode && root) {
mode = root.getAttribute("data-play-mode") === "preview" ? "preview" : "main";
}
return !!(
root &&
video &&
video.tagName === "VIDEO" &&
root.contains(video) &&
mode !== "preview"
);
}
function rememberDramaVideoMode(root, video) {
if (!root || !video || video.tagName !== "VIDEO" || !root.contains(video)) return;
video.__trackerDramaMode = root.getAttribute("data-play-mode") === "preview" ? "preview" : "main";
}
function prepareDramaModeChange(nextMode) {
var root = dramaRoot();
var video = root && root.querySelector("video");
if (!root || !video) return;
var previousMode = video.__trackerDramaMode ||
(root.getAttribute("data-play-mode") === "preview" ? "preview" : "main");
nextMode = nextMode === "preview" ? "preview" : "main";
if (previousMode === nextMode) return;
if (previousMode === "main") {
var playback = video.__trackerPlaybackState;
var wasPlaying = !!(playback && (playback.startedAt || playback.elapsedMs > 0));
settlePlayback(video);
if (wasPlaying) trackDramaEvent("ep_pause", video);
}
video.__trackerPlaybackState = null;
video.__trackerDramaMode = nextMode;
}
function bindDramaEvents() {
var root = dramaRoot();
if (!root) return;
if (!state.dramaViewSent) {
state.dramaViewSent = true;
trackDramaEvent("ep_view", null);
}
document.addEventListener("play", function (event) {
rememberDramaVideoMode(root, event.target);
if (isMainDramaVideo(root, event.target)) {
trackDramaEvent("ep_play", event.target);
}
}, true);
document.addEventListener("playing", function (event) {
rememberDramaVideoMode(root, event.target);
if (isMainDramaVideo(root, event.target)) beginPlayback(event.target);
}, true);
document.addEventListener("loadedmetadata", function (event) {
rememberDramaVideoMode(root, event.target);
}, true);
document.addEventListener("waiting", function (event) {
if (isMainDramaVideo(root, event.target)) settlePlayback(event.target);
}, true);
document.addEventListener("pause", function (event) {
if (isMainDramaVideo(root, event.target) && !event.target.ended) {
var playback = event.target.__trackerPlaybackState;
var wasPlaying = !!(playback && (playback.startedAt || playback.elapsedMs > 0));
settlePlayback(event.target);
if (event.target.seeking || !wasPlaying) return;
trackDramaEvent("ep_pause", event.target);
}
}, true);
document.addEventListener("timeupdate", function (event) {
if (!isMainDramaVideo(root, event.target) || event.target.seeking) return;
videoPlaybackState(event.target).lastPosition = Number(event.target.currentTime) || 0;
}, true);
document.addEventListener("seeking", function (event) {
if (!isMainDramaVideo(root, event.target)) return;
var playback = videoPlaybackState(event.target);
if (playback.seekFrom != null) return;
settlePlayback(event.target);
playback.seekFrom = playback.lastPosition;
}, true);
document.addEventListener("seeked", function (event) {
if (!isMainDramaVideo(root, event.target)) return;
var playback = videoPlaybackState(event.target);
if (playback.seekFrom == null) return;
var from = playback.seekFrom;
var target = Number(event.target.currentTime) || 0;
playback.seekFrom = null;
playback.lastPosition = target;
if (Math.abs(target - from) >= 0.5) {
trackDramaEvent(target > from ? "ep_forward" : "ep_rewind", event.target);
}
if (!event.target.paused && !event.target.ended) beginPlayback(event.target);
}, true);
document.addEventListener("ended", function (event) {
if (isMainDramaVideo(root, event.target)) {
settlePlayback(event.target);
trackDramaEvent("ep_complete", event.target, { keepPlaybackDuration: true });
trackDramaEvent("ep_play", event.target, { forceProgress: 100 });
}
}, true);
function reportPendingDramaProgress() {
var video = root.querySelector("video");
if (!isMainDramaVideo(root, video)) return false;
var playback = video.__trackerPlaybackState;
if (!playback || (!playback.startedAt && playback.elapsedMs <= 0)) return false;
var pendingMs = playback.elapsedMs +
(playback.startedAt ? Math.max(0, Date.now() - playback.startedAt) : 0);
if (pendingMs < 500) return false;
settlePlayback(video);
trackDramaEvent("ep_play", video);
return true;
}
function flushSDKAfterEnqueue() {
if (!global.WebSDK || typeof global.WebSDK.flush !== "function") return;
function flush() {
Promise.resolve(global.WebSDK.flush()).catch(logError);
}
Promise.resolve().then(function () {
return Promise.resolve();
}).then(flush);
global.setTimeout(flush, 50);
global.setTimeout(flush, 200);
}
document.addEventListener("visibilitychange", function () {
var video = root.querySelector("video");
if (document.visibilityState === "hidden") {
if (reportPendingDramaProgress()) flushSDKAfterEnqueue();
if (isMainDramaVideo(root, video) && !video.paused && !video.ended) beginPlayback(video);
return;
}
if (document.visibilityState === "visible" && isMainDramaVideo(root, video) &&
!video.paused && !video.ended) {
beginPlayback(video);
}
});
global.addEventListener("beforeunload", function () {
if (reportPendingDramaProgress()) flushSDKAfterEnqueue();
});
global.addEventListener("pagehide", function () {
if (reportPendingDramaProgress()) flushSDKAfterEnqueue();
});
}
function bindSearchEvents() {
var container = document.querySelector("[data-search-results]");
if (!container) return;
var keyword = stringValue(container.getAttribute("data-search-keyword"));
if (!keyword) return;
var grid = container.querySelector("[data-search-grid]");
var cards = grid ? Array.prototype.slice.call(grid.querySelectorAll("[data-search-item-id]")) : [];
var resultCount = Math.max(0, integer(container.getAttribute("data-search-result-count"), cards.length));
var pageNum = Math.max(1, integer(container.getAttribute("data-search-page"), 1));
var pageSize = Math.max(1, integer(container.getAttribute("data-search-page-size"), cards.length || 1));
if (pageNum === 1) {
invoke("keywordSearch", {
keyword: keyword,
search_result_count: resultCount,
search_content_type: "",
search_trace_id: "",
search_id: "",
});
}
if (!grid) return;
grid.addEventListener("click", function (event) {
var target = event.target && event.target.closest ? event.target : null;
var resultLink = target && target.closest("[data-search-result-click]");
var card = resultLink && resultLink.closest("[data-search-item-id]");
if (!card || !grid.contains(card)) return;
var position = cards.indexOf(card);
if (position < 0) return;
invoke("keywordClick", {
keyword: keyword,
click_item_id: stringValue(card.getAttribute("data-search-item-id")),
click_item_type_key: "short_drama",
click_item_type_name: "短剧",
click_position: (pageNum - 1) * pageSize + position + 1,
search_trace_id: "",
});
}, true);
}
function bindClicks() {
document.addEventListener("click", function (event) {
var target = event.target && event.target.closest ? event.target : null;
if (!target) return;
var popupClose = target.closest(
"[data-dialog-ad-close],[data-ad-close],[data-register-close]," +
"[data-download-close],[data-login-close],[data-recharge-close]," +
"[data-play-coin-close],[data-setting-password-close],[data-drawer-close]"
);
if (!popupClose) trackPageClick(event);
var switcher = target.closest("[data-drama-switch]");
if (switcher) {
trackDramaStatusChange(switcher.getAttribute("data-target-episode"), switcher.getAttribute("data-target-video-id"));
}
if (target.closest("[data-share-page]") && dramaRoot()) {
var root = dramaRoot();
var video = root.querySelector("video");
trackDramaEvent("ep_share", isMainDramaVideo(root, video) ? video : null);
}
var ad = target.closest('[data-track-ad="external"][data-ad-clickable="1"][data-ad-id]');
if (ad) {
trackExternalAd(ad, "click");
}
trackNavigation(target.closest("a[data-navigation-key],a[data-nav-key]"));
}, true);
}
function onReady() {
whenDOMReady(function () {
Promise.resolve(loadCommonFields()).then(function () {
trackPageView();
});
bindClicks();
bindAds();
bindExternalAdEvents();
bindAdvertisingEvents();
bindDramaEvents();
bindSearchEvents();
try {
global.dispatchEvent(new CustomEvent("tracker:ready", { detail: state.page }));
} catch (error) {}
});
}
var COMMON_FIELD_KEYS = [
"sid",
"device_id",
"device",
"device_fingerprint",
"fp_version",
];
var __commonFields = null;
var __commonFieldsReady = null;
function emptyCommonFields() {
var out = {};
for (var i = 0; i < COMMON_FIELD_KEYS.length; i++) {
out[COMMON_FIELD_KEYS[i]] = "";
}
return out;
}
function normalizeCommonFields(fields) {
var out = emptyCommonFields();
fields = fields || {};
for (var i = 0; i < COMMON_FIELD_KEYS.length; i++) {
var k = COMMON_FIELD_KEYS[i];
if (fields[k] != null) out[k] = String(fields[k]);
}
return out;
}
function publishCommonFields(fields) {
__commonFields = normalizeCommonFields(fields);
try {
global.__trackerCommonFields = __commonFields;
} catch (e) {}
try {
if (typeof CustomEvent === "function") {
global.dispatchEvent(
new CustomEvent("tracker:common-fields", { detail: __commonFields })
);
}
} catch (e) {}
return __commonFields;
}
function loadCommonFields() {
if (__commonFieldsReady) return __commonFieldsReady;
__commonFieldsReady = new Promise(function (resolve) {
function finish(fields) {
resolve(publishCommonFields(fields));
}
if (!global.WebSDK || typeof global.WebSDK.getCommonFields !== "function") {
finish(emptyCommonFields());
return;
}
try {
Promise.resolve(global.WebSDK.getCommonFields(COMMON_FIELD_KEYS.slice()))
.then(function (fields) {
finish(fields || emptyCommonFields());
})
.catch(function (err) {
logError(err);
finish(emptyCommonFields());
});
} catch (err) {
logError(err);
finish(emptyCommonFields());
}
});
return __commonFieldsReady;
}
function getCommonFieldsCached(keys) {
return loadCommonFields().then(function (all) {
if (!Array.isArray(keys) || !keys.length) return all;
var out = {};
for (var i = 0; i < keys.length; i++) {
var k = String(keys[i] || "").trim();
if (k) out[k] = Object.prototype.hasOwnProperty.call(all, k) ? all[k] : "";
}
return out;
});
}
function getCommonFieldCached(key) {
var k = String(key || "").trim();
if (!k) return Promise.resolve("");
return getCommonFieldsCached([k]).then(function (m) {
return m[k] || "";
});
}
function encodeSDKCommonHeader(fields) {
var payload = normalizeCommonFields(fields);
try {
return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
} catch (e) {
logError(e);
return "";
}
}
function getSDKCommonHeader() {
return loadCommonFields().then(function (fields) {
return encodeSDKCommonHeader(fields);
});
}
global.trackerInit = init;
global.tracker = {
init: init,
pageView: trackPageView,
dramaEvent: trackDramaEvent,
dramaStatusChange: trackDramaStatusChange,
dramaUnlock: trackDramaUnlock,
prepareDramaModeChange: prepareDramaModeChange,
advertising: function (payload) { return invoke("advertising", payload || {}); },
externalAd: trackExternalAd,
getDramaContext: function () {
var context = dramaContext();
return context ? Object.assign({}, context) : {};
},
keywordSearch: function (payload) { return invoke("keywordSearch", payload || {}); },
keywordClick: function (payload) { return invoke("keywordClick", payload || {}); },
getCommonFields: getCommonFieldsCached,
getCommonField: getCommonFieldCached,
getCommonFieldsSync: function () { return __commonFields; },
encodeSDKCommonHeader: encodeSDKCommonHeader,
getSDKCommonHeader: getSDKCommonHeader,
flush: function () {
if (!global.WebSDK || typeof global.WebSDK.flush !== "function") return Promise.resolve();
return Promise.resolve(global.WebSDK.flush()).catch(logError);
},
setUid: function (uid) {
if (global.WebSDK && global.WebSDK.setUid) global.WebSDK.setUid(normalizeUid(uid));
},
setChannel: function (channel) {
if (global.WebSDK && global.WebSDK.setChannel) global.WebSDK.setChannel(currentChannel(channel));
},
setContext: function (context) {
if (global.WebSDK && global.WebSDK.setContext) global.WebSDK.setContext(context || {});
},
};
init(global.__TRACKER_BOOT__ || {});
})(window);