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