Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* 认证相关前端封装:注册 / 登录 / 游客登录走本站 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;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
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 {
|
||||
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 (e2) {}
|
||||
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) !== "/";
|
||||
}
|
||||
|
||||
/** 注册账号:打开登录弹窗;无弹窗时跳首页登录入口 */
|
||||
function promptRegisteredRelogin() {
|
||||
toast("登录已过期,请重新登录", "warning");
|
||||
var redirect = "";
|
||||
try {
|
||||
redirect = (global.location.pathname || "/") + (global.location.search || "");
|
||||
} catch (e2) {
|
||||
redirect = "/";
|
||||
}
|
||||
var url = "/login/";
|
||||
if (isSafeRedirect(redirect) && redirect !== "/" && redirect.indexOf("/?auth=") !== 0) {
|
||||
url = "/login/return" + (global.location.pathname || "/");
|
||||
}
|
||||
if (typeof global.openLoginDialog === "function") {
|
||||
try {
|
||||
var opened = global.openLoginDialog();
|
||||
// dialogLoader 桩函数返回 Promise:加载失败时再走跳转兜底
|
||||
if (opened && typeof opened.then === "function") {
|
||||
opened.catch(function () {
|
||||
global.location.href = url;
|
||||
});
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
global.location.href = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口返回 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;
|
||||
}
|
||||
|
||||
global.AuthAPI = {
|
||||
TOKEN_EXPIRED_CODE: TOKEN_EXPIRED_CODE,
|
||||
register: register,
|
||||
login: login,
|
||||
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,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,142 @@
|
||||
/**
|
||||
* 前端请求封装。
|
||||
* - 登录态走 cookie(credentials: include)
|
||||
* - 浏览器无法改写 User-Agent;X-Real-IP 由服务端反代上游时注入
|
||||
* - 直连上游时若需自定义头,请走本站 Go 代理接口
|
||||
*/
|
||||
(function (global) {
|
||||
var DEFAULT_TIMEOUT_MS = 30000;
|
||||
|
||||
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,
|
||||
* }} [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 fetchOpts = {
|
||||
method: method,
|
||||
headers: headers,
|
||||
credentials: "include",
|
||||
signal: controller ? controller.signal : undefined,
|
||||
};
|
||||
if (body != null) fetchOpts.body = body;
|
||||
|
||||
var timeoutMs =
|
||||
typeof options.timeout === "number" ? options.timeout : DEFAULT_TIMEOUT_MS;
|
||||
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,160 @@
|
||||
(function () {
|
||||
const DESKTOP_COLLAPSE_LIMIT = 10;
|
||||
const MOBILE_COLLAPSE_LIMIT = 8;
|
||||
|
||||
function initApplicationPage() {
|
||||
const page = document.querySelector(".application-page");
|
||||
if (!page) return;
|
||||
|
||||
const tabsNav = page.querySelector(".app-category-tabs");
|
||||
const tabs = page.querySelectorAll(".app-category-tab");
|
||||
const sections = page.querySelectorAll(".app-list-section[data-total]");
|
||||
if (!tabs.length || !sections.length) return;
|
||||
|
||||
let isProgrammaticScroll = false;
|
||||
let scrollTimer = null;
|
||||
|
||||
function isDesktop() {
|
||||
return window.matchMedia("(min-width: 768px)").matches;
|
||||
}
|
||||
|
||||
function getCollapseLimit() {
|
||||
return isDesktop() ? DESKTOP_COLLAPSE_LIMIT : MOBILE_COLLAPSE_LIMIT;
|
||||
}
|
||||
|
||||
function getScrollOffset() {
|
||||
const header = document.querySelector(".site-header");
|
||||
const barHeight = (header && header.getBoundingClientRect().height) || (isDesktop() ? 72 : 56);
|
||||
return barHeight + (isDesktop() ? 24 : 12);
|
||||
}
|
||||
|
||||
function getSectionAnchor(section) {
|
||||
return section.querySelector(".app-list-section__head") || section;
|
||||
}
|
||||
|
||||
function setActiveTab(activeTab) {
|
||||
tabs.forEach((tab) => {
|
||||
const isActive = tab === activeTab;
|
||||
tab.classList.toggle("app-category-tab--active", isActive);
|
||||
tab.setAttribute("aria-selected", isActive ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function updateActionButton(button, expanded, limit) {
|
||||
const label = button.querySelector(".app-list-section__more-label");
|
||||
const icon = button.querySelector(".app-list-section__more-icon");
|
||||
const total = Number(button.dataset.total) || 0;
|
||||
const hiddenCount = Math.max(total - limit, 0);
|
||||
|
||||
if (label) {
|
||||
label.textContent = expanded ? "收起" : `显示全部${hiddenCount}个站点`;
|
||||
}
|
||||
if (icon) {
|
||||
icon.classList.toggle("app-list-section__more-icon--up", expanded);
|
||||
}
|
||||
button.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
}
|
||||
|
||||
function updateCollapseSections() {
|
||||
const limit = getCollapseLimit();
|
||||
|
||||
page.querySelectorAll(".app-list-section[data-total]").forEach((section) => {
|
||||
const total = Number(section.dataset.total) || 0;
|
||||
const button = section.querySelector(".app-list-section__more");
|
||||
const needsCollapse = total > limit;
|
||||
|
||||
section.classList.toggle("app-list-section--collapsible", needsCollapse);
|
||||
|
||||
if (!needsCollapse) {
|
||||
section.classList.remove("app-list-section--expanded");
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.hidden = !needsCollapse;
|
||||
if (needsCollapse) {
|
||||
updateActionButton(button, section.classList.contains("app-list-section--expanded"), limit);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollTabIntoView(tab) {
|
||||
if (!tabsNav || !tab) return;
|
||||
const navRect = tabsNav.getBoundingClientRect();
|
||||
const tabRect = tab.getBoundingClientRect();
|
||||
const nextLeft = tabsNav.scrollLeft + (tabRect.left - navRect.left) - (navRect.width - tabRect.width) / 2;
|
||||
tabsNav.scrollTo({ left: Math.max(0, nextLeft), behavior: "smooth" });
|
||||
}
|
||||
|
||||
function scrollToSection(section) {
|
||||
if (!section) return;
|
||||
const anchor = getSectionAnchor(section);
|
||||
const top = anchor.getBoundingClientRect().top + window.scrollY - getScrollOffset();
|
||||
isProgrammaticScroll = true;
|
||||
clearTimeout(scrollTimer);
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
||||
scrollTimer = window.setTimeout(() => {
|
||||
isProgrammaticScroll = false;
|
||||
}, 800);
|
||||
}
|
||||
|
||||
tabs.forEach((tab, index) => {
|
||||
if (!tab.hasAttribute("aria-selected")) {
|
||||
tab.setAttribute("aria-selected", index === 0 ? "true" : "false");
|
||||
}
|
||||
|
||||
tab.addEventListener("click", () => {
|
||||
const targetId = tab.dataset.target;
|
||||
if (!targetId) return;
|
||||
|
||||
setActiveTab(tab);
|
||||
scrollTabIntoView(tab);
|
||||
scrollToSection(document.getElementById(targetId));
|
||||
});
|
||||
});
|
||||
|
||||
page.querySelectorAll(".app-list-section__more").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const section = button.closest(".app-list-section");
|
||||
if (!section || button.hidden) return;
|
||||
|
||||
const expanded = section.classList.toggle("app-list-section--expanded");
|
||||
updateActionButton(button, expanded, getCollapseLimit());
|
||||
});
|
||||
});
|
||||
|
||||
updateCollapseSections();
|
||||
window.addEventListener("resize", updateCollapseSections);
|
||||
|
||||
if (!("IntersectionObserver" in window)) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (isProgrammaticScroll) return;
|
||||
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
||||
|
||||
if (!visible.length) return;
|
||||
|
||||
const activeTab = page.querySelector(`.app-category-tab[data-target="${visible[0].target.id}"]`);
|
||||
if (activeTab) {
|
||||
setActiveTab(activeTab);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: `-${getScrollOffset()}px 0px -55% 0px`,
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1],
|
||||
}
|
||||
);
|
||||
|
||||
sections.forEach((section) => observer.observe(section));
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initApplicationPage);
|
||||
} else {
|
||||
initApplicationPage();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,307 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("coinDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var balanceEls = mask.querySelectorAll("[data-coin-balance]");
|
||||
var costEls = mask.querySelectorAll("[data-coin-cost]");
|
||||
var payBtn = mask.querySelector("[data-coin-dialog-pay]");
|
||||
var loading = false;
|
||||
var paying = false;
|
||||
|
||||
function formatCoins(n) {
|
||||
var num = Number(n);
|
||||
if (!Number.isFinite(num) || num < 0) num = 0;
|
||||
return String(Math.floor(num));
|
||||
}
|
||||
|
||||
function setBalance(n) {
|
||||
var text = formatCoins(n);
|
||||
balanceEls.forEach(function (el) {
|
||||
el.textContent = text;
|
||||
});
|
||||
}
|
||||
|
||||
function setCost(n) {
|
||||
var text = formatCoins(n);
|
||||
costEls.forEach(function (el) {
|
||||
el.textContent = text;
|
||||
});
|
||||
}
|
||||
|
||||
function toast(message, type) {
|
||||
if (typeof window.showToast === "function") {
|
||||
window.showToast(message, type);
|
||||
return;
|
||||
}
|
||||
window.alert(message);
|
||||
}
|
||||
|
||||
function openLogin() {
|
||||
var loginOpen = document.querySelector("[data-login-dialog-open]");
|
||||
if (loginOpen) loginOpen.click();
|
||||
}
|
||||
|
||||
function goToCoinRecharge() {
|
||||
window.location.href = "/deposit/coin/";
|
||||
}
|
||||
|
||||
function isInsufficientBalance(res) {
|
||||
if (!res) return false;
|
||||
if (Number(res.code) === 20004) return true;
|
||||
var message = String(res.tip || res.msg || "");
|
||||
return /(?:金币|余额).{0,6}不足|不足.{0,6}(?:金币|余额)/.test(message);
|
||||
}
|
||||
|
||||
function resolvePlayletId(trigger) {
|
||||
if (trigger) {
|
||||
var fromTrigger = Number(trigger.getAttribute("data-playlet-id") || 0);
|
||||
if (fromTrigger > 0) return fromTrigger;
|
||||
}
|
||||
var fromMask = Number(mask.getAttribute("data-playlet-id") || 0);
|
||||
if (fromMask > 0) return fromMask;
|
||||
var playPage =
|
||||
document.querySelector("[data-play-page]") ||
|
||||
document.querySelector(".play-page");
|
||||
if (playPage) {
|
||||
var fromPage = Number(playPage.getAttribute("data-playlet-id") || 0);
|
||||
if (fromPage > 0) return fromPage;
|
||||
}
|
||||
var player = document.querySelector("[data-play-player]");
|
||||
if (player) {
|
||||
var fromPlayer = Number(player.getAttribute("data-playlet-id") || 0);
|
||||
if (fromPlayer > 0) return fromPlayer;
|
||||
}
|
||||
var likeBtn = document.querySelector("[data-playlet-id]");
|
||||
if (likeBtn) {
|
||||
var fromBtn = Number(likeBtn.getAttribute("data-playlet-id") || 0);
|
||||
if (fromBtn > 0) return fromBtn;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function resolvePlayletSlug(trigger) {
|
||||
if (trigger) {
|
||||
var fromTrigger = String(trigger.getAttribute("data-playlet-slug") || "").trim();
|
||||
if (fromTrigger) return fromTrigger;
|
||||
}
|
||||
var fromMask = String(mask.getAttribute("data-playlet-slug") || "").trim();
|
||||
if (fromMask) return fromMask;
|
||||
var playPage =
|
||||
document.querySelector("[data-play-page]") ||
|
||||
document.querySelector(".play-page");
|
||||
if (playPage) {
|
||||
var fromPage = String(playPage.getAttribute("data-playlet-slug") || "").trim();
|
||||
if (fromPage) return fromPage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildPlayHref(playletId, chapterId) {
|
||||
if (!playletId || !chapterId) return "";
|
||||
var slug = resolvePlayletSlug(null);
|
||||
var key = slug || String(playletId);
|
||||
return "/play/" + encodeURIComponent(key) + "/" + chapterId + "/";
|
||||
}
|
||||
|
||||
function isOnPlayPage() {
|
||||
return !!(
|
||||
document.querySelector("[data-play-page]") ||
|
||||
document.querySelector(".play-page")
|
||||
);
|
||||
}
|
||||
|
||||
function afterUnlockSuccess(playletId, chapterId, data) {
|
||||
var detail = {
|
||||
playletId: playletId,
|
||||
chapterId: chapterId,
|
||||
balance: data && typeof data.balance === "number" ? data.balance : null,
|
||||
canWatch: !!(data && data.canWatch),
|
||||
unlocked: !!(data && data.unlocked),
|
||||
};
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("play:chapter-unlocked", { detail: detail })
|
||||
);
|
||||
if (
|
||||
isOnPlayPage() &&
|
||||
typeof window.playAfterChapterUnlock === "function"
|
||||
) {
|
||||
window.playAfterChapterUnlock(detail);
|
||||
return;
|
||||
}
|
||||
var href = buildPlayHref(playletId, chapterId);
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
function setPaying(on) {
|
||||
paying = !!on;
|
||||
if (!payBtn) return;
|
||||
payBtn.disabled = paying;
|
||||
payBtn.classList.toggle("is-loading", paying);
|
||||
payBtn.setAttribute("aria-busy", paying ? "true" : "false");
|
||||
}
|
||||
|
||||
function fetchBalance() {
|
||||
if (!window.HttpRequest) {
|
||||
setBalance(0);
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
return window.HttpRequest.get("/api/auth/getInfo")
|
||||
.then(function (res) {
|
||||
if (!res || res.code !== 200 || !res.data) {
|
||||
if (res && res.code === 401) {
|
||||
openLogin();
|
||||
}
|
||||
throw new Error((res && (res.tip || res.msg)) || "获取余额失败");
|
||||
}
|
||||
var recharge = res.data.recharge;
|
||||
setBalance(recharge);
|
||||
if (window.CookieUtil && CookieUtil.setAuthSession) {
|
||||
CookieUtil.setAuthSession(res.data);
|
||||
}
|
||||
return Number(recharge) || 0;
|
||||
})
|
||||
.catch(function (err) {
|
||||
setBalance(0);
|
||||
toast((err && err.message) || "获取余额失败", "error");
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function unlockAndPlay() {
|
||||
if (paying) return;
|
||||
if (!window.HttpRequest) {
|
||||
toast("网络模块未就绪", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
var chapterId = Number(mask.getAttribute("data-chapter-id") || 0);
|
||||
var playletId = Number(mask.getAttribute("data-playlet-id") || 0);
|
||||
var cost = Number(mask.getAttribute("data-single-coins") || 0);
|
||||
var balance = Number(
|
||||
(balanceEls[0] && balanceEls[0].textContent) || 0
|
||||
);
|
||||
|
||||
if (!chapterId || !playletId) {
|
||||
toast("缺少剧集信息,请刷新后重试", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isFinite(balance) && Number.isFinite(cost) && balance < cost) {
|
||||
goToCoinRecharge();
|
||||
return;
|
||||
}
|
||||
|
||||
setPaying(true);
|
||||
window.HttpRequest.post("/api/playlet/chapter/unlock", {
|
||||
chapterId: chapterId,
|
||||
playletId: playletId,
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res || res.code !== 200) {
|
||||
if (res && res.code === 401) {
|
||||
closeDialog();
|
||||
openLogin();
|
||||
return;
|
||||
}
|
||||
if (isInsufficientBalance(res)) {
|
||||
goToCoinRecharge();
|
||||
return;
|
||||
}
|
||||
throw new Error((res && (res.tip || res.msg)) || "解锁失败");
|
||||
}
|
||||
var data = res.data || {};
|
||||
if (typeof data.balance === "number") {
|
||||
setBalance(data.balance);
|
||||
if (window.CookieUtil && CookieUtil.setAuthSession) {
|
||||
CookieUtil.setAuthSession({ recharge: data.balance });
|
||||
}
|
||||
}
|
||||
toast((res && res.tip) || "解锁成功", "success");
|
||||
closeDialog();
|
||||
afterUnlockSuccess(
|
||||
Number(data.playletId) || playletId,
|
||||
Number(data.chapterId) || chapterId,
|
||||
data
|
||||
);
|
||||
})
|
||||
.catch(function (err) {
|
||||
toast((err && err.message) || "解锁失败,请稍后重试", "error");
|
||||
})
|
||||
.finally(function () {
|
||||
setPaying(false);
|
||||
});
|
||||
}
|
||||
|
||||
function openDialog(event) {
|
||||
if (event) event.preventDefault();
|
||||
var trigger =
|
||||
event && event.target && event.target.closest
|
||||
? event.target.closest("[data-coin-dialog-open]")
|
||||
: null;
|
||||
var singleCoins = trigger
|
||||
? Number(trigger.getAttribute("data-single-coins") || 0)
|
||||
: 0;
|
||||
setCost(singleCoins);
|
||||
if (trigger) {
|
||||
mask.setAttribute(
|
||||
"data-chapter-id",
|
||||
trigger.getAttribute("data-chapter-id") || ""
|
||||
);
|
||||
mask.setAttribute("data-single-coins", String(singleCoins || 0));
|
||||
mask.setAttribute(
|
||||
"data-playlet-id",
|
||||
String(resolvePlayletId(trigger) || "")
|
||||
);
|
||||
mask.setAttribute("data-playlet-slug", resolvePlayletSlug(trigger));
|
||||
} else if (!mask.getAttribute("data-playlet-id")) {
|
||||
mask.setAttribute("data-playlet-id", String(resolvePlayletId(null) || ""));
|
||||
}
|
||||
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
fetchBalance().finally(function () {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-coin-dialog-open]")) {
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-coin-dialog-close]")) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-coin-dialog-pay]")) {
|
||||
event.preventDefault();
|
||||
unlockAndPlay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === mask) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
window.openCoinDialog = openDialog;
|
||||
window.closeCoinDialog = closeDialog;
|
||||
})();
|
||||
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* 充值页:套餐 / 支付方式互斥选择,按套餐 payMethods 刷新;提交走 VIP/金币下单。
|
||||
* VIP: POST /api/vip/pay 金币: POST /api/coin/buy
|
||||
* 支付方式严格使用接口返回的 payMethods,不造假默认渠道。
|
||||
*/
|
||||
(function () {
|
||||
var submitting = false;
|
||||
var confirmMask = document.querySelector("[data-deposit-confirm]");
|
||||
var confirmMethods = document.querySelector("[data-deposit-confirm-methods]");
|
||||
var confirmSubmit = document.querySelector("[data-deposit-confirm-submit]");
|
||||
var bodyOverflowBeforeConfirm = "";
|
||||
|
||||
function toast(message, type) {
|
||||
if (window.AuthAPI && typeof AuthAPI.toast === "function") {
|
||||
AuthAPI.toast(message, type || "info");
|
||||
return;
|
||||
}
|
||||
if (typeof showToast === "function") showToast(message, type || "info");
|
||||
}
|
||||
|
||||
function bindExclusive(root, selector, activeClass) {
|
||||
var scope = root || document;
|
||||
var nodes = scope.querySelectorAll(selector);
|
||||
if (!nodes.length) return;
|
||||
nodes.forEach(function (node) {
|
||||
node.addEventListener("click", function () {
|
||||
nodes.forEach(function (item) {
|
||||
item.classList.remove(activeClass);
|
||||
});
|
||||
node.classList.add(activeClass);
|
||||
syncPayTypeAcross(node.getAttribute("data-pay-type") || "");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncPayTypeAcross(payType) {
|
||||
if (!payType) return;
|
||||
document.querySelectorAll("[data-pay-type]").forEach(function (el) {
|
||||
var match = String(el.getAttribute("data-pay-type") || "") === String(payType);
|
||||
if (el.classList.contains("deposit-pay-btn")) {
|
||||
el.classList.toggle("deposit-pay-btn--active", match);
|
||||
}
|
||||
if (el.classList.contains("deposit-mobile-pay")) {
|
||||
el.classList.toggle("deposit-mobile-pay--active", match);
|
||||
}
|
||||
if (el.classList.contains("deposit-confirm-method")) {
|
||||
el.classList.toggle("is-active", match);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parsePayMethods(raw) {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
var list = JSON.parse(raw);
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list
|
||||
.map(function (item) {
|
||||
return {
|
||||
type: String((item && item.type) || "").trim(),
|
||||
typeName: String((item && (item.typeName || item.type)) || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter(function (item) {
|
||||
return item.type || item.typeName;
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function currentPayType() {
|
||||
var active =
|
||||
document.querySelector(".deposit-confirm-method.is-active[data-pay-type]") ||
|
||||
document.querySelector(".deposit-pay-btn--active[data-pay-type]") ||
|
||||
document.querySelector(".deposit-mobile-pay--active[data-pay-type]");
|
||||
return active ? String(active.getAttribute("data-pay-type") || "") : "";
|
||||
}
|
||||
|
||||
function currentPackage() {
|
||||
return (
|
||||
document.querySelector(".deposit-mobile-pkg--active[data-package-id]") ||
|
||||
document.querySelector(".deposit-package--active[data-package-id]")
|
||||
);
|
||||
}
|
||||
|
||||
function renderPayButtons(container, className, activeClass, methods, preferType) {
|
||||
if (!container) return;
|
||||
var list = Array.isArray(methods) ? methods : [];
|
||||
if (!list.length) {
|
||||
container.innerHTML = '<div class="deposit-package-empty">暂无可用支付方式</div>';
|
||||
return;
|
||||
}
|
||||
var prefer = preferType || "";
|
||||
var activeType = prefer;
|
||||
if (!list.some(function (item) { return item.type === prefer; })) {
|
||||
activeType = list[0] ? list[0].type : "";
|
||||
}
|
||||
container.innerHTML = list
|
||||
.map(function (item) {
|
||||
var isActive = item.type === activeType;
|
||||
return (
|
||||
'<button class="' +
|
||||
className +
|
||||
(isActive ? " " + activeClass : "") +
|
||||
'" type="button" data-pay-type="' +
|
||||
escapeAttr(item.type) +
|
||||
'">' +
|
||||
escapeHtml(item.typeName || item.type) +
|
||||
"</button>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
bindExclusive(container, "." + className.split(" ").join("."), activeClass);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function payMethodVisual(item) {
|
||||
var value = String((item && (item.typeName || item.type)) || "").toLowerCase();
|
||||
if (/支付宝|alipay/.test(value)) {
|
||||
return { kind: "alipay", icon: "/static/images/figma-dialogs/alipay.svg" };
|
||||
}
|
||||
if (/微信|wechat|weixin|wxpay/.test(value)) {
|
||||
return { kind: "wechat", icon: "/static/images/figma-dialogs/wechat.svg" };
|
||||
}
|
||||
if (/usdt|tether/.test(value)) {
|
||||
return { kind: "usdt", icon: "/static/images/figma-dialogs/usdt.svg" };
|
||||
}
|
||||
return { kind: "other", icon: "" };
|
||||
}
|
||||
|
||||
function renderConfirmMethods(methods, preferType) {
|
||||
if (!confirmMethods) return "";
|
||||
var list = Array.isArray(methods) ? methods : [];
|
||||
var activeType = preferType || (list[0] && list[0].type) || "";
|
||||
if (!list.some(function (item) { return item.type === activeType; })) {
|
||||
activeType = (list[0] && list[0].type) || "";
|
||||
}
|
||||
confirmMethods.innerHTML = list.map(function (item) {
|
||||
var visual = payMethodVisual(item);
|
||||
var icon = visual.icon
|
||||
? '<img src="' + visual.icon + '" alt="" />'
|
||||
: '<span aria-hidden="true">¥</span>';
|
||||
return '<button class="deposit-confirm-method' +
|
||||
(item.type === activeType ? ' is-active' : '') +
|
||||
'" type="button" data-pay-type="' + escapeAttr(item.type) + '">' +
|
||||
'<span class="deposit-confirm-method-copy"><span class="deposit-confirm-method-icon deposit-confirm-method-icon--' + visual.kind + '">' + icon + '</span>' +
|
||||
'<span class="deposit-confirm-method-name">' + escapeHtml(item.typeName || item.type) + '</span></span>' +
|
||||
'<span class="deposit-confirm-method-check" aria-hidden="true">✓</span></button>';
|
||||
}).join("");
|
||||
confirmMethods.querySelectorAll(".deposit-confirm-method").forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
syncPayTypeAcross(button.getAttribute("data-pay-type") || "");
|
||||
});
|
||||
});
|
||||
syncPayTypeAcross(activeType);
|
||||
return activeType;
|
||||
}
|
||||
|
||||
function hasPayMethods(el) {
|
||||
var target = el || currentPackage();
|
||||
if (!target) return false;
|
||||
return parsePayMethods(target.getAttribute("data-pay-methods")).length > 0;
|
||||
}
|
||||
|
||||
function syncPayMethods(el) {
|
||||
var methods = parsePayMethods(el.getAttribute("data-pay-methods"));
|
||||
var prefer = currentPayType();
|
||||
renderPayButtons(
|
||||
document.querySelector("[data-deposit-pay-pc]"),
|
||||
"deposit-pay-btn",
|
||||
"deposit-pay-btn--active",
|
||||
methods,
|
||||
prefer
|
||||
);
|
||||
renderPayButtons(
|
||||
document.querySelector("[data-deposit-pay-mobile]"),
|
||||
"deposit-mobile-pay",
|
||||
"deposit-mobile-pay--active",
|
||||
methods,
|
||||
prefer
|
||||
);
|
||||
}
|
||||
|
||||
function syncPackageActive(kind, packageId) {
|
||||
document.querySelectorAll(".deposit-package, .deposit-mobile-pkg").forEach(function (el) {
|
||||
var match =
|
||||
String(el.getAttribute("data-package-id") || "") === String(packageId) &&
|
||||
String(el.getAttribute("data-package-kind") || "") === String(kind);
|
||||
el.classList.toggle("deposit-package--active", match && el.classList.contains("deposit-package"));
|
||||
el.classList.toggle(
|
||||
"deposit-mobile-pkg--active",
|
||||
match && el.classList.contains("deposit-mobile-pkg")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSubmitButtons(price, loading) {
|
||||
var canPay = hasPayMethods(currentPackage());
|
||||
var disabled = !!loading || !canPay;
|
||||
var pkg = currentPackage();
|
||||
var packageName = pkg ? String(pkg.getAttribute("data-package-name") || "").trim() : "";
|
||||
var text;
|
||||
if (loading) {
|
||||
text = "支付中...";
|
||||
} else if (!canPay) {
|
||||
text = "暂无支付方式";
|
||||
} else {
|
||||
text = "立即支付 ¥" + (price || "0");
|
||||
}
|
||||
document.querySelectorAll("[data-deposit-submit]").forEach(function (btn) {
|
||||
btn.disabled = disabled;
|
||||
btn.classList.toggle("is-loading", !!loading);
|
||||
btn.classList.toggle("is-disabled", !canPay && !loading);
|
||||
if (btn.classList.contains("deposit-pc-submit")) {
|
||||
btn.textContent = loading ? "支付中..." : canPay ? "立即支付 ¥" + (price || "0") : "暂无支付方式";
|
||||
} else {
|
||||
btn.textContent = loading ? "支付中..." : canPay ? "支付 ¥" + (price || "0") + " 充值金币" : text;
|
||||
}
|
||||
});
|
||||
document.querySelectorAll("[data-deposit-selected-note]").forEach(function (node) {
|
||||
node.textContent = packageName
|
||||
? "已选择 " + packageName + " · 点击后选择支付方式"
|
||||
: "请选择金币套餐";
|
||||
});
|
||||
}
|
||||
|
||||
function onPackageClick(el) {
|
||||
var kind = el.getAttribute("data-package-kind") || "";
|
||||
var id = el.getAttribute("data-package-id") || "";
|
||||
var price = el.getAttribute("data-price") || "0";
|
||||
syncPackageActive(kind, id);
|
||||
syncPayMethods(el);
|
||||
updateSubmitButtons(price, false);
|
||||
}
|
||||
|
||||
function openPayUrl(data) {
|
||||
var url = data && data.payUrl ? String(data.payUrl).trim() : "";
|
||||
if (!url) {
|
||||
toast("未获取到支付链接", "error");
|
||||
return;
|
||||
}
|
||||
var mode = String((data && data.payUrlMode) || "").toLowerCase();
|
||||
var openNew =
|
||||
!!(data && data.isOpenNewBrowser) ||
|
||||
mode === "new" ||
|
||||
mode === "blank" ||
|
||||
mode === "external";
|
||||
if (openNew) {
|
||||
var win = window.open(url, "_blank");
|
||||
if (!win) {
|
||||
window.location.href = url;
|
||||
}
|
||||
return;
|
||||
}
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function ensureLoggedIn() {
|
||||
if (window.CookieUtil && CookieUtil.isLoggedIn()) return true;
|
||||
toast("请先登录后再支付", "error");
|
||||
var loginURL = "/login/return/deposit/";
|
||||
if (typeof window.openLoginDialog === "function") {
|
||||
var opened = window.openLoginDialog();
|
||||
if (opened && typeof opened.then === "function") {
|
||||
opened.catch(function () {
|
||||
window.location.href = loginURL;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
window.location.href = loginURL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function closePayConfirm() {
|
||||
if (!confirmMask) return;
|
||||
confirmMask.classList.remove("is-open");
|
||||
confirmMask.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = bodyOverflowBeforeConfirm;
|
||||
}
|
||||
|
||||
function openPayConfirm() {
|
||||
if (submitting || !confirmMask) return;
|
||||
if (!ensureLoggedIn()) return;
|
||||
var pkg = currentPackage();
|
||||
if (!pkg) {
|
||||
toast("请选择套餐", "error");
|
||||
return;
|
||||
}
|
||||
var methods = parsePayMethods(pkg.getAttribute("data-pay-methods"));
|
||||
if (!methods.length) {
|
||||
toast("当前套餐暂无可用支付方式", "error");
|
||||
return;
|
||||
}
|
||||
var packageName = String(pkg.getAttribute("data-package-name") || "金币套餐").trim();
|
||||
var price = String(pkg.getAttribute("data-price") || "0").trim();
|
||||
var nameEl = confirmMask.querySelector("[data-deposit-confirm-name]");
|
||||
var priceEl = confirmMask.querySelector("[data-deposit-confirm-price]");
|
||||
if (nameEl) nameEl.textContent = packageName;
|
||||
if (priceEl) priceEl.textContent = price;
|
||||
renderConfirmMethods(methods, currentPayType());
|
||||
bodyOverflowBeforeConfirm = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
confirmMask.classList.add("is-open");
|
||||
confirmMask.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
function submitPay() {
|
||||
if (submitting) return;
|
||||
if (!window.HttpRequest) {
|
||||
toast("请求组件未加载", "error");
|
||||
return;
|
||||
}
|
||||
if (!ensureLoggedIn()) return;
|
||||
|
||||
var pkg = currentPackage();
|
||||
if (!pkg) {
|
||||
toast("请选择套餐", "error");
|
||||
return;
|
||||
}
|
||||
var packageId = Number(pkg.getAttribute("data-package-id") || 0);
|
||||
var kind = String(pkg.getAttribute("data-package-kind") || "");
|
||||
var price = pkg.getAttribute("data-price") || "0";
|
||||
var payMode = currentPayType();
|
||||
if (!packageId || (kind !== "vip" && kind !== "coin")) {
|
||||
toast("套餐无效,请重新选择", "error");
|
||||
return;
|
||||
}
|
||||
if (!payMode) {
|
||||
toast("请选择支付方式", "error");
|
||||
return;
|
||||
}
|
||||
if (!hasPayMethods(pkg)) {
|
||||
toast("当前套餐暂无可用支付方式", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
var api = kind === "coin" ? "/api/coin/buy" : "/api/vip/pay";
|
||||
submitting = true;
|
||||
updateSubmitButtons(price, true);
|
||||
if (confirmSubmit) {
|
||||
confirmSubmit.disabled = true;
|
||||
confirmSubmit.textContent = "支付中...";
|
||||
}
|
||||
|
||||
HttpRequest.post(api, { packageId: packageId, payMode: payMode })
|
||||
.then(function (res) {
|
||||
if (!res || res.code !== 200 || !res.data) {
|
||||
var err = new Error((res && (res.tip || res.msg)) || "发起支付失败");
|
||||
err.code = res && res.code;
|
||||
throw err;
|
||||
}
|
||||
openPayUrl(res.data);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err && err.code === 401) {
|
||||
toast("请先登录后再支付", "error");
|
||||
if (typeof window.openLoginDialog === "function") {
|
||||
var opened = window.openLoginDialog();
|
||||
if (opened && typeof opened.then === "function") {
|
||||
opened.catch(function () {
|
||||
window.location.href = "/login/return/deposit/";
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
toast((err && err.message) || "发起支付失败", "error");
|
||||
})
|
||||
.finally(function () {
|
||||
submitting = false;
|
||||
updateSubmitButtons(price, false);
|
||||
if (confirmSubmit) {
|
||||
confirmSubmit.disabled = false;
|
||||
confirmSubmit.textContent = "去支付";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindExclusive(document, ".deposit-pay-btn", "deposit-pay-btn--active");
|
||||
bindExclusive(document, ".deposit-mobile-pay", "deposit-mobile-pay--active");
|
||||
|
||||
document.querySelectorAll(".deposit-package[data-package-id]").forEach(function (node) {
|
||||
node.addEventListener("click", function () {
|
||||
onPackageClick(node);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".deposit-mobile-pkg[data-package-id]").forEach(function (node) {
|
||||
node.addEventListener("click", function () {
|
||||
onPackageClick(node);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-deposit-submit]").forEach(function (btn) {
|
||||
btn.addEventListener("click", openPayConfirm);
|
||||
});
|
||||
|
||||
if (confirmSubmit) confirmSubmit.addEventListener("click", submitPay);
|
||||
document.querySelectorAll("[data-deposit-confirm-close]").forEach(function (button) {
|
||||
button.addEventListener("click", closePayConfirm);
|
||||
});
|
||||
if (confirmMask) {
|
||||
confirmMask.addEventListener("click", function (event) {
|
||||
if (event.target === confirmMask) closePayConfirm();
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && confirmMask && confirmMask.classList.contains("is-open")) {
|
||||
closePayConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var active =
|
||||
document.querySelector(".deposit-mobile-pkg--active[data-price]") ||
|
||||
document.querySelector(".deposit-package--active[data-price]");
|
||||
if (active) {
|
||||
syncPayMethods(active);
|
||||
updateSubmitButtons(active.getAttribute("data-price") || "0", false);
|
||||
} else {
|
||||
updateSubmitButtons("0", false);
|
||||
}
|
||||
|
||||
// /deposit/coin 时滚动到金币充值区域(套餐默认选中由 SSR 完成)
|
||||
(function scrollPreferCoin() {
|
||||
if (!/\/deposit\/coin\/?$/i.test(window.location.pathname || "")) return;
|
||||
var isMobile = window.matchMedia("(max-width: 767px)").matches;
|
||||
var target = isMobile
|
||||
? document.getElementById("depositCoinMobileTitle")
|
||||
: document.getElementById("depositCoinGroup");
|
||||
if (!target) return;
|
||||
setTimeout(function () {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}, 80);
|
||||
})();
|
||||
|
||||
function formatRecharge(value) {
|
||||
var n = Number(value);
|
||||
if (!isFinite(n) || n < 0) n = 0;
|
||||
return String(Math.floor(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function syncDepositRecharge(value) {
|
||||
var text = formatRecharge(value);
|
||||
document.querySelectorAll("[data-deposit-recharge]").forEach(function (el) {
|
||||
el.textContent = text;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshDepositWallet() {
|
||||
if (!window.CookieUtil || !CookieUtil.isLoggedIn() || !window.HttpRequest) {
|
||||
return;
|
||||
}
|
||||
var session = CookieUtil.getAuthSession();
|
||||
if (session && session.recharge != null && session.recharge !== "") {
|
||||
syncDepositRecharge(session.recharge);
|
||||
}
|
||||
HttpRequest.get("/api/auth/getInfo")
|
||||
.then(function (res) {
|
||||
if (!res || res.code !== 200 || !res.data) return;
|
||||
if (CookieUtil.setAuthSession) CookieUtil.setAuthSession(res.data);
|
||||
syncDepositRecharge(res.data.recharge);
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
refreshDepositWallet();
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
(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();
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
function finishQueue() {
|
||||
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.getElementById("eightPop");
|
||||
if (ageGate && ageGate.classList.contains("is-open")) {
|
||||
document.addEventListener("eightpop: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;
|
||||
rememberProgress(currentIndex);
|
||||
var item = items[currentIndex];
|
||||
var image = item.querySelector(".dialog-ad-image");
|
||||
|
||||
function displayItem() {
|
||||
if (!setActiveItem(currentIndex)) return;
|
||||
hasDisplayedItem = true;
|
||||
showWhenAgeGateAllows();
|
||||
}
|
||||
|
||||
function skipBrokenItem() {
|
||||
showItem(currentIndex + 1);
|
||||
}
|
||||
|
||||
if (!image) return skipBrokenItem();
|
||||
loadDialogImage(image).then(displayItem).catch(skipBrokenItem);
|
||||
}
|
||||
|
||||
function showNextItem() {
|
||||
showItem(currentIndex + 1);
|
||||
}
|
||||
|
||||
mask.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-dialog-ad-close]")) {
|
||||
showNextItem();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
showNextItem();
|
||||
}
|
||||
});
|
||||
|
||||
if (isDismissed()) return;
|
||||
if (currentIndex >= items.length) {
|
||||
finishQueue();
|
||||
return;
|
||||
}
|
||||
showItem(currentIndex);
|
||||
})();
|
||||
@@ -0,0 +1,243 @@
|
||||
(function (global) {
|
||||
var definitions = {
|
||||
download: {
|
||||
endpoint: "/ui/dialog/download",
|
||||
elementId: "downloadAppDialog",
|
||||
css: "/static/css/downloadAppDialog.css?v=2",
|
||||
js: "/static/js/pages/downloadAppDialog.js?v=4",
|
||||
open: "openDownloadAppDialog",
|
||||
},
|
||||
login: {
|
||||
endpoint: "/ui/dialog/login",
|
||||
elementId: "loginDialog",
|
||||
css: "/static/css/loginDialog.css?v=2",
|
||||
js: "/static/js/pages/loginDialog.js?v=3",
|
||||
open: "openLoginDialog",
|
||||
},
|
||||
register: {
|
||||
endpoint: "/ui/dialog/register",
|
||||
elementId: "registerDialog",
|
||||
css: "/static/css/registerDialog.css?v=2",
|
||||
js: "/static/js/pages/registerDialog.js?v=2",
|
||||
open: "openRegisterDialog",
|
||||
},
|
||||
};
|
||||
var pending = Object.create(null);
|
||||
var stubOpeners = Object.create(null);
|
||||
|
||||
function hasRealOpener(name) {
|
||||
var definition = definitions[name];
|
||||
return (
|
||||
!!definition &&
|
||||
typeof global[definition.open] === "function" &&
|
||||
global[definition.open] !== stubOpeners[name]
|
||||
);
|
||||
}
|
||||
|
||||
function loadStylesheet(href) {
|
||||
var existing = document.querySelector('link[rel="stylesheet"][href="' + href + '"]');
|
||||
if (existing) return Promise.resolve();
|
||||
return new Promise(function (resolve, reject) {
|
||||
var link = document.createElement("link");
|
||||
var settled = false;
|
||||
function finish(ok, err) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (ok) resolve();
|
||||
else reject(err || new Error("dialog stylesheet failed"));
|
||||
}
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.onload = function () {
|
||||
finish(true);
|
||||
};
|
||||
link.onerror = function () {
|
||||
finish(false, new Error("dialog stylesheet failed"));
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
// 部分浏览器对缓存 CSS 不触发 onload,避免一直卡住弹窗
|
||||
requestAnimationFrame(function () {
|
||||
try {
|
||||
if (link.sheet) finish(true);
|
||||
} catch (e) {}
|
||||
});
|
||||
setTimeout(function () {
|
||||
finish(true);
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function loadScript(src) {
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
if (existing) {
|
||||
if (existing.getAttribute("data-loaded") === "1") return Promise.resolve();
|
||||
return new Promise(function (resolve, reject) {
|
||||
existing.addEventListener("load", resolve, { once: true });
|
||||
existing.addEventListener("error", function () {
|
||||
reject(new Error("dialog script failed"));
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
var script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.onload = function () {
|
||||
script.setAttribute("data-loaded", "1");
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function () {
|
||||
reject(new Error("dialog script failed"));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchFragment(definition) {
|
||||
if (document.getElementById(definition.elementId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return fetch(definition.endpoint, {
|
||||
credentials: "same-origin",
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
}).then(function (response) {
|
||||
if (!response.ok) throw new Error("dialog fragment failed");
|
||||
return response.text();
|
||||
}).then(function (html) {
|
||||
if (document.getElementById(definition.elementId)) return;
|
||||
var host = document.createElement("div");
|
||||
host.innerHTML = html;
|
||||
var dialog = host.querySelector("#" + definition.elementId);
|
||||
if (!dialog) throw new Error("dialog fragment is invalid");
|
||||
document.body.appendChild(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDialog(name) {
|
||||
var definition = definitions[name];
|
||||
if (!definition) return Promise.reject(new Error("unknown dialog"));
|
||||
if (document.getElementById(definition.elementId) && hasRealOpener(name)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (pending[name]) return pending[name];
|
||||
|
||||
pending[name] = loadStylesheet(definition.css)
|
||||
.then(function () {
|
||||
return fetchFragment(definition);
|
||||
})
|
||||
.then(function () {
|
||||
if (hasRealOpener(name)) return;
|
||||
return loadScript(definition.js);
|
||||
})
|
||||
.then(function () {
|
||||
if (!document.getElementById(definition.elementId) || !hasRealOpener(name)) {
|
||||
throw new Error("dialog opener is unavailable");
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
delete pending[name];
|
||||
throw error;
|
||||
});
|
||||
return pending[name];
|
||||
}
|
||||
|
||||
function loginFallbackURL() {
|
||||
var redirect = "";
|
||||
try {
|
||||
redirect = global.location.pathname || "/";
|
||||
} catch (e) {
|
||||
redirect = "/";
|
||||
}
|
||||
var url = "/login/";
|
||||
if (
|
||||
typeof redirect === "string" &&
|
||||
redirect.charAt(0) === "/" &&
|
||||
redirect.charAt(1) !== "/" &&
|
||||
redirect !== "/" &&
|
||||
redirect.indexOf("/?auth=") !== 0
|
||||
) {
|
||||
url = "/login/return" + redirect;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function openDialog(name, value) {
|
||||
return ensureDialog(name)
|
||||
.then(function () {
|
||||
var opener = global[definitions[name].open];
|
||||
if (typeof opener !== "function" || opener === stubOpeners[name]) {
|
||||
throw new Error("dialog opener is unavailable");
|
||||
}
|
||||
return opener(value);
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (name === "login") {
|
||||
try {
|
||||
var params = new URLSearchParams(global.location.search || "");
|
||||
// 已在登录入口仍失败时避免死循环跳转
|
||||
if (params.get("auth") !== "login") {
|
||||
global.location.href = loginFallbackURL();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(definitions).forEach(function (name) {
|
||||
var apiName = definitions[name].open;
|
||||
var stub = function (value) {
|
||||
return openDialog(name, value);
|
||||
};
|
||||
stubOpeners[name] = stub;
|
||||
if (typeof global[apiName] !== "function") global[apiName] = stub;
|
||||
});
|
||||
|
||||
function triggerName(target) {
|
||||
if (!target || !target.closest) return "";
|
||||
if (target.closest("[data-download-app-open]")) return "download";
|
||||
if (target.closest("[data-register-dialog-open], a[href=\"#register\"], a[href=\"/register/\"]")) {
|
||||
return "register";
|
||||
}
|
||||
if (target.closest("[data-login-dialog-open], a[href=\"#login\"], a[href=\"/login/\"]")) {
|
||||
return "login";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isMobileViewport() {
|
||||
return typeof global.matchMedia === "function"
|
||||
? global.matchMedia("(max-width: 767px)").matches
|
||||
: (global.innerWidth || 0) <= 767;
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var name = triggerName(event.target);
|
||||
if (!name) return;
|
||||
// 移动端下载入口保留原生链接行为,直接访问 appAddr,不加载下载弹窗。
|
||||
if (name === "download" && isMobileViewport()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
|
||||
openDialog(name).catch(function () {});
|
||||
}, true);
|
||||
|
||||
function openFromLocation() {
|
||||
var params = new URLSearchParams(global.location.search || "");
|
||||
var auth = params.get("auth");
|
||||
if (auth === "login" || auth === "register") {
|
||||
openDialog(auth).catch(function () {});
|
||||
return;
|
||||
}
|
||||
if (global.location.hash === "#login") openDialog("login").catch(function () {});
|
||||
if (global.location.hash === "#register") openDialog("register").catch(function () {});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", openFromLocation, { once: true });
|
||||
} else {
|
||||
openFromLocation();
|
||||
}
|
||||
|
||||
global.ensureDialog = ensureDialog;
|
||||
})(window);
|
||||
@@ -0,0 +1,302 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("downloadAppDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var pcQRBox = mask.querySelector("[data-download-qr-box]");
|
||||
var h5QRBox = null;
|
||||
var h5URLEl = mask.querySelector("[data-download-url]");
|
||||
var downloadCTA = mask.querySelector("[data-download-app-cta]");
|
||||
var copyBtn = mask.querySelector("[data-download-copy]");
|
||||
var platformBtns = mask.querySelectorAll("[data-download-platform]");
|
||||
var copyTimer = null;
|
||||
var qrReady = false;
|
||||
var qrcodeLoading = null;
|
||||
|
||||
function loadQRCodeLib() {
|
||||
if (typeof window.QRCode === "function") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (qrcodeLoading) return qrcodeLoading;
|
||||
qrcodeLoading = new Promise(function (resolve, reject) {
|
||||
var s = document.createElement("script");
|
||||
s.src = "/static/common/qrcode.min.js";
|
||||
s.async = true;
|
||||
s.onload = function () { resolve(); };
|
||||
s.onerror = function () { reject(new Error("qrcode load failed")); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return qrcodeLoading;
|
||||
}
|
||||
|
||||
function getAppConfig() {
|
||||
return window.__APP_CONFIG__ || {};
|
||||
}
|
||||
|
||||
/** 二维码、跳转和复制链接统一使用 config/info.newAddressInfo.appAddr。 */
|
||||
function getAppURL() {
|
||||
var cfg = getAppConfig();
|
||||
var fromCfg = String((cfg && cfg.appAddr) || "").trim();
|
||||
if (fromCfg) return fromCfg;
|
||||
return String(mask.getAttribute("data-download-app-url") || "").trim();
|
||||
}
|
||||
|
||||
function isHTTPURL(value) {
|
||||
return /^https?:\/\//i.test(String(value || "").trim());
|
||||
}
|
||||
|
||||
function isMobileViewport() {
|
||||
return typeof window.matchMedia === "function"
|
||||
? window.matchMedia("(max-width: 767px)").matches
|
||||
: (window.innerWidth || 0) <= 767;
|
||||
}
|
||||
|
||||
function qrLevels() {
|
||||
var QR = window.QRCode;
|
||||
if (QR && QR.CorrectLevel) {
|
||||
return [QR.CorrectLevel.L, QR.CorrectLevel.M, QR.CorrectLevel.Q, QR.CorrectLevel.H];
|
||||
}
|
||||
return [1, 0, 3, 2];
|
||||
}
|
||||
|
||||
function paintQRModel(model, size) {
|
||||
if (!model || typeof model.getModuleCount !== "function") return null;
|
||||
var n = model.getModuleCount();
|
||||
if (!n) return null;
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
var ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
var cell = size / n;
|
||||
ctx.fillStyle = "#000000";
|
||||
for (var row = 0; row < n; row++) {
|
||||
for (var col = 0; col < n; col++) {
|
||||
if (model.isDark(row, col)) {
|
||||
ctx.fillRect(
|
||||
Math.floor(col * cell),
|
||||
Math.floor(row * cell),
|
||||
Math.ceil(cell),
|
||||
Math.ceil(cell)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function encodeQRCanvas(text, size) {
|
||||
var QR = window.QRCode;
|
||||
if (typeof QR !== "function") return null;
|
||||
var holder = document.createElement("div");
|
||||
holder.style.cssText =
|
||||
"position:fixed;left:0;top:0;width:" +
|
||||
size +
|
||||
"px;height:" +
|
||||
size +
|
||||
"px;opacity:0;pointer-events:none;z-index:-1;";
|
||||
document.body.appendChild(holder);
|
||||
var levels = qrLevels();
|
||||
try {
|
||||
for (var i = 0; i < levels.length; i++) {
|
||||
holder.innerHTML = "";
|
||||
try {
|
||||
var inst = new QR(holder, {
|
||||
text: text,
|
||||
width: size,
|
||||
height: size,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: levels[i],
|
||||
});
|
||||
var painted = paintQRModel(inst && inst._oQRCode, size);
|
||||
if (painted) return painted;
|
||||
var srcCanvas = holder.querySelector("canvas");
|
||||
if (srcCanvas) {
|
||||
var copy = document.createElement("canvas");
|
||||
copy.width = size;
|
||||
copy.height = size;
|
||||
copy.getContext("2d").drawImage(srcCanvas, 0, 0, size, size);
|
||||
return copy;
|
||||
}
|
||||
} catch (e) {
|
||||
/* try next level */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
if (holder.parentNode) holder.parentNode.removeChild(holder);
|
||||
}
|
||||
}
|
||||
|
||||
function renderQR(el, text, size) {
|
||||
if (!el || !text) return false;
|
||||
el.innerHTML = "";
|
||||
var img = document.createElement("img");
|
||||
img.alt = "下载二维码";
|
||||
img.title = "黄果短剧 App 下载二维码";
|
||||
img.width = size;
|
||||
img.height = size;
|
||||
var canvas = encodeQRCanvas(text, size);
|
||||
if (canvas) {
|
||||
try {
|
||||
img.src = canvas.toDataURL("image/png");
|
||||
el.appendChild(img);
|
||||
return true;
|
||||
} catch (e) {
|
||||
el.appendChild(canvas);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
img.src =
|
||||
"https://api.qrserver.com/v1/create-qr-code/?size=" +
|
||||
size +
|
||||
"x" +
|
||||
size +
|
||||
"&margin=0&data=" +
|
||||
encodeURIComponent(text);
|
||||
el.appendChild(img);
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncDownloadUI(force) {
|
||||
var url = getAppURL();
|
||||
if (!url) return;
|
||||
|
||||
if (force || !qrReady) {
|
||||
var pcOk = renderQR(pcQRBox, url, 164);
|
||||
var h5Ok = renderQR(h5QRBox, url, 164);
|
||||
qrReady = !!(pcOk || h5Ok);
|
||||
}
|
||||
|
||||
if (h5URLEl) {
|
||||
h5URLEl.textContent = "官方网址 " + url;
|
||||
if (isHTTPURL(url)) {
|
||||
h5URLEl.href = url;
|
||||
h5URLEl.removeAttribute("aria-disabled");
|
||||
} else {
|
||||
h5URLEl.href = "#";
|
||||
h5URLEl.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
}
|
||||
if (downloadCTA) {
|
||||
downloadCTA.href = isHTTPURL(url) ? url : "#";
|
||||
downloadCTA.toggleAttribute("aria-disabled", !isHTTPURL(url));
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(event) {
|
||||
if (event) event.preventDefault();
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
loadQRCodeLib()
|
||||
.catch(function () { /* fallback image QR still works */ })
|
||||
.then(function () {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
syncDownloadUI(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function setPlatform(platform) {
|
||||
if (platform !== "ios" && platform !== "android") return;
|
||||
currentPlatform = platform;
|
||||
platformBtns.forEach(function (btn) {
|
||||
btn.classList.toggle(
|
||||
"is-active",
|
||||
btn.getAttribute("data-download-platform") === platform
|
||||
);
|
||||
});
|
||||
if (copyBtn) {
|
||||
copyBtn.textContent = "复制下载链接";
|
||||
copyBtn.classList.remove("is-copied");
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
var url = getAppURL();
|
||||
if (!url || !copyBtn) return;
|
||||
|
||||
function onSuccess() {
|
||||
copyBtn.textContent = "已复制";
|
||||
copyBtn.classList.add("is-copied");
|
||||
if (copyTimer) clearTimeout(copyTimer);
|
||||
copyTimer = setTimeout(function () {
|
||||
copyBtn.textContent = "复制下载链接";
|
||||
copyBtn.classList.remove("is-copied");
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(url).then(onSuccess).catch(function () {
|
||||
fallbackCopy(url, onSuccess);
|
||||
});
|
||||
return;
|
||||
}
|
||||
fallbackCopy(url, onSuccess);
|
||||
}
|
||||
|
||||
function fallbackCopy(text, onSuccess) {
|
||||
var input = document.createElement("input");
|
||||
input.value = text;
|
||||
input.setAttribute("readonly", "");
|
||||
input.style.position = "fixed";
|
||||
input.style.left = "-9999px";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var openTrigger = event.target.closest("[data-download-app-open]");
|
||||
if (openTrigger) {
|
||||
// 移动端由带 appAddr 的原生链接直接跳转,不展示二维码弹窗。
|
||||
if (isMobileViewport()) return;
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-download-app-close]")) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === mask) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
var platformBtn = event.target.closest("[data-download-platform]");
|
||||
if (platformBtn && mask.contains(platformBtn)) {
|
||||
setPlatform(platformBtn.getAttribute("data-download-platform"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-download-copy]")) {
|
||||
copyLink();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
window.openDownloadAppDialog = openDialog;
|
||||
window.closeDownloadAppDialog = closeDialog;
|
||||
})();
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 修改密码:POST /api/auth/profile { oldPassword, newPassword }
|
||||
*/
|
||||
(function () {
|
||||
var form = document.querySelector("[data-edit-password-form]");
|
||||
if (!form) return;
|
||||
|
||||
var currentInput = form.querySelector("[data-edit-password-current]");
|
||||
var newInput = form.querySelector("[data-edit-password-new]");
|
||||
var confirmInput = form.querySelector("[data-edit-password-confirm]");
|
||||
var submitBtn = form.querySelector("[data-edit-password-submit]");
|
||||
|
||||
document.querySelectorAll("[data-password-toggle]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var id = btn.getAttribute("data-password-toggle");
|
||||
var input = id ? document.getElementById(id) : null;
|
||||
if (!input) return;
|
||||
var show = input.type === "password";
|
||||
input.type = show ? "text" : "password";
|
||||
btn.textContent = show ? "隐藏" : "显示";
|
||||
});
|
||||
});
|
||||
|
||||
function getField(name) {
|
||||
return form.querySelector('[data-edit-password-field="' + name + '"]');
|
||||
}
|
||||
|
||||
function setFieldError(name, message) {
|
||||
var field = getField(name);
|
||||
if (!field) return;
|
||||
var error = field.querySelector("[data-edit-password-error]");
|
||||
field.classList.toggle("is-error", !!message);
|
||||
if (error) {
|
||||
if (message) {
|
||||
error.hidden = false;
|
||||
error.textContent = message;
|
||||
} else {
|
||||
error.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
setFieldError("current", "");
|
||||
setFieldError("new", "");
|
||||
setFieldError("confirm", "");
|
||||
}
|
||||
|
||||
function isValidLength(value) {
|
||||
return value.length >= 4 && value.length <= 16;
|
||||
}
|
||||
|
||||
function toast(message, type) {
|
||||
if (window.AuthAPI) AuthAPI.toast(message, type);
|
||||
else if (typeof showToast === "function") showToast(message, type);
|
||||
}
|
||||
|
||||
function validate() {
|
||||
clearErrors();
|
||||
var current = (currentInput && currentInput.value) || "";
|
||||
var next = (newInput && newInput.value) || "";
|
||||
var confirm = (confirmInput && confirmInput.value) || "";
|
||||
var ok = true;
|
||||
|
||||
if (!current) {
|
||||
setFieldError("current", "请输入当前密码");
|
||||
ok = false;
|
||||
} else if (!isValidLength(current)) {
|
||||
setFieldError("current", "当前密码需为 4–16 个字符");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!next) {
|
||||
setFieldError("new", "请输入新密码");
|
||||
ok = false;
|
||||
} else if (!isValidLength(next)) {
|
||||
setFieldError("new", "新密码需为 4–16 个字符");
|
||||
ok = false;
|
||||
} else if (next === current) {
|
||||
setFieldError("new", "新密码不能与当前密码相同");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
setFieldError("confirm", "请再次输入新密码");
|
||||
ok = false;
|
||||
} else if (confirm !== next) {
|
||||
setFieldError("confirm", "两次输入的新密码不一致");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!ok) return null;
|
||||
return { oldPassword: current, newPassword: next };
|
||||
}
|
||||
|
||||
[currentInput, newInput, confirmInput].forEach(function (input) {
|
||||
if (!input) return;
|
||||
input.addEventListener("input", function () {
|
||||
var field = input.closest("[data-edit-password-field]");
|
||||
if (!field) return;
|
||||
var name = field.getAttribute("data-edit-password-field");
|
||||
setFieldError(name, "");
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var payload = validate();
|
||||
if (!payload) return;
|
||||
if (!window.AuthAPI || !window.HttpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
|
||||
AuthAPI.updateProfile(payload)
|
||||
.then(function () {
|
||||
toast("密码修改成功", "success");
|
||||
setTimeout(function () {
|
||||
window.location.href = "/setting/";
|
||||
}, 400);
|
||||
})
|
||||
.catch(function (err) {
|
||||
var tip = (err && err.message) || "修改失败,请稍后重试";
|
||||
if (/旧|当前|原密码|old/i.test(tip)) {
|
||||
setFieldError("current", tip);
|
||||
return;
|
||||
}
|
||||
if (/新密码|new/i.test(tip)) {
|
||||
setFieldError("new", tip);
|
||||
return;
|
||||
}
|
||||
toast(tip, "error");
|
||||
})
|
||||
.finally(function () {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,68 @@
|
||||
(function () {
|
||||
var form = document.querySelector("[data-edit-profile-form]");
|
||||
var nickInput = document.querySelector("[data-edit-profile-nickname]");
|
||||
var bioInput = document.querySelector("[data-edit-profile-bio]");
|
||||
var submitBtn = document.querySelector("[data-edit-profile-submit]");
|
||||
if (!form) return;
|
||||
|
||||
var originalNick = nickInput ? String(nickInput.value || "").trim() : "";
|
||||
var originalBio = bioInput ? String(bioInput.value || "").trim() : "";
|
||||
|
||||
function toast(message, type) {
|
||||
if (window.AuthAPI) AuthAPI.toast(message, type);
|
||||
else if (typeof showToast === "function") showToast(message, type);
|
||||
}
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
if (!window.AuthAPI || !window.HttpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nick = nickInput ? String(nickInput.value || "").trim() : "";
|
||||
var bio = bioInput ? String(bioInput.value || "").trim() : "";
|
||||
if (!nick) {
|
||||
toast("请输入昵称", "error");
|
||||
return;
|
||||
}
|
||||
if (nick.length > 20) {
|
||||
toast("昵称最多 20 个字符", "error");
|
||||
return;
|
||||
}
|
||||
if (bio.length > 120) {
|
||||
toast("个人简介最多 120 个字符", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
var nickChanged = nick !== originalNick;
|
||||
var bioChanged = bio !== originalBio;
|
||||
if (!nickChanged && !bioChanged) {
|
||||
toast("暂无修改", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
if (nickChanged) payload.nickName = nick;
|
||||
if (bioChanged) payload.introduction = bio;
|
||||
if (!payload.nickName && !payload.introduction) {
|
||||
toast(bioChanged ? "请填写个人简介或修改昵称" : "暂无修改", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
|
||||
AuthAPI.updateProfile(payload)
|
||||
.then(function () {
|
||||
toast("保存成功", "success");
|
||||
setTimeout(function () {
|
||||
window.location.href = "/setting/";
|
||||
}, 400);
|
||||
})
|
||||
.catch(function (err) {
|
||||
toast((err && err.message) || "保存失败", "error");
|
||||
})
|
||||
.finally(function () {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,62 @@
|
||||
(function () {
|
||||
var SESSION_KEY = "ageConfirmed";
|
||||
var mask = document.getElementById("eightPop");
|
||||
if (!mask) return;
|
||||
|
||||
function getSessionValue(key) {
|
||||
try {
|
||||
return window.sessionStorage.getItem(key) || "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionValue(key, value) {
|
||||
try {
|
||||
window.sessionStorage.setItem(key, value);
|
||||
} catch (e) {
|
||||
// sessionStorage 不可用时,本页仍允许正常确认并关闭弹窗。
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
mask.hidden = false;
|
||||
mask.removeAttribute("hidden");
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
mask.hidden = true;
|
||||
mask.setAttribute("hidden", "");
|
||||
document.body.style.overflow = "";
|
||||
document.dispatchEvent(new CustomEvent("eightpop:closed"));
|
||||
}
|
||||
|
||||
function confirmAge() {
|
||||
setSessionValue(SESSION_KEY, "1");
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function exitSite() {
|
||||
try {
|
||||
window.location.replace("about:blank");
|
||||
} catch (e) {
|
||||
window.location.href = "about:blank";
|
||||
}
|
||||
}
|
||||
|
||||
var confirmBtn = mask.querySelector("[data-eight-pop-confirm]");
|
||||
var exitBtns = mask.querySelectorAll("[data-eight-pop-exit]");
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener("click", confirmAge);
|
||||
}
|
||||
exitBtns.forEach(function (button) {
|
||||
button.addEventListener("click", exitSite);
|
||||
});
|
||||
|
||||
if (getSessionValue(SESSION_KEY) !== "1") {
|
||||
openDialog();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,668 @@
|
||||
/**
|
||||
* 意见反馈:校验 → 可选上传截图(最多 3 张,单张 ≤5MB)→ POST /api/feedback/add
|
||||
* 上游 type: 1-功能建议、2-播放问题、3-账号问题、4-其他
|
||||
*/
|
||||
(function () {
|
||||
var MAX_FILES = 3;
|
||||
var MAX_SIZE = 5 * 1024 * 1024;
|
||||
var ACCEPT_TYPES = { "image/png": 1, "image/jpeg": 1, "image/jpg": 1 };
|
||||
var CITY_OPTIONS = {
|
||||
"北京市": "北京市",
|
||||
"天津市": "天津市",
|
||||
"河北省": "石家庄市|唐山市|秦皇岛市|邯郸市|邢台市|保定市|张家口市|承德市|沧州市|廊坊市|衡水市",
|
||||
"山西省": "太原市|大同市|阳泉市|长治市|晋城市|朔州市|晋中市|运城市|忻州市|临汾市|吕梁市",
|
||||
"内蒙古自治区": "呼和浩特市|包头市|乌海市|赤峰市|通辽市|鄂尔多斯市|呼伦贝尔市|巴彦淖尔市|乌兰察布市|兴安盟|锡林郭勒盟|阿拉善盟",
|
||||
"辽宁省": "沈阳市|大连市|鞍山市|抚顺市|本溪市|丹东市|锦州市|营口市|阜新市|辽阳市|盘锦市|铁岭市|朝阳市|葫芦岛市",
|
||||
"吉林省": "长春市|吉林市|四平市|辽源市|通化市|白山市|松原市|白城市|延边朝鲜族自治州",
|
||||
"黑龙江省": "哈尔滨市|齐齐哈尔市|鸡西市|鹤岗市|双鸭山市|大庆市|伊春市|佳木斯市|七台河市|牡丹江市|黑河市|绥化市|大兴安岭地区",
|
||||
"上海市": "上海市",
|
||||
"江苏省": "南京市|无锡市|徐州市|常州市|苏州市|南通市|连云港市|淮安市|盐城市|扬州市|镇江市|泰州市|宿迁市",
|
||||
"浙江省": "杭州市|宁波市|温州市|嘉兴市|湖州市|绍兴市|金华市|衢州市|舟山市|台州市|丽水市",
|
||||
"安徽省": "合肥市|芜湖市|蚌埠市|淮南市|马鞍山市|淮北市|铜陵市|安庆市|黄山市|滁州市|阜阳市|宿州市|六安市|亳州市|池州市|宣城市",
|
||||
"福建省": "福州市|厦门市|莆田市|三明市|泉州市|漳州市|南平市|龙岩市|宁德市",
|
||||
"江西省": "南昌市|景德镇市|萍乡市|九江市|新余市|鹰潭市|赣州市|吉安市|宜春市|抚州市|上饶市",
|
||||
"山东省": "济南市|青岛市|淄博市|枣庄市|东营市|烟台市|潍坊市|济宁市|泰安市|威海市|日照市|临沂市|德州市|聊城市|滨州市|菏泽市",
|
||||
"河南省": "郑州市|开封市|洛阳市|平顶山市|安阳市|鹤壁市|新乡市|焦作市|濮阳市|许昌市|漯河市|三门峡市|南阳市|商丘市|信阳市|周口市|驻马店市|济源市",
|
||||
"湖北省": "武汉市|黄石市|十堰市|宜昌市|襄阳市|鄂州市|荆门市|孝感市|荆州市|黄冈市|咸宁市|随州市|恩施土家族苗族自治州|仙桃市|潜江市|天门市|神农架林区",
|
||||
"湖南省": "长沙市|株洲市|湘潭市|衡阳市|邵阳市|岳阳市|常德市|张家界市|益阳市|郴州市|永州市|怀化市|娄底市|湘西土家族苗族自治州",
|
||||
"广东省": "广州市|韶关市|深圳市|珠海市|汕头市|佛山市|江门市|湛江市|茂名市|肇庆市|惠州市|梅州市|汕尾市|河源市|阳江市|清远市|东莞市|中山市|潮州市|揭阳市|云浮市",
|
||||
"广西壮族自治区": "南宁市|柳州市|桂林市|梧州市|北海市|防城港市|钦州市|贵港市|玉林市|百色市|贺州市|河池市|来宾市|崇左市",
|
||||
"海南省": "海口市|三亚市|三沙市|儋州市",
|
||||
"重庆市": "重庆市",
|
||||
"四川省": "成都市|自贡市|攀枝花市|泸州市|德阳市|绵阳市|广元市|遂宁市|内江市|乐山市|南充市|眉山市|宜宾市|广安市|达州市|雅安市|巴中市|资阳市|阿坝藏族羌族自治州|甘孜藏族自治州|凉山彝族自治州",
|
||||
"贵州省": "贵阳市|六盘水市|遵义市|安顺市|毕节市|铜仁市|黔西南布依族苗族自治州|黔东南苗族侗族自治州|黔南布依族苗族自治州",
|
||||
"云南省": "昆明市|曲靖市|玉溪市|保山市|昭通市|丽江市|普洱市|临沧市|楚雄彝族自治州|红河哈尼族彝族自治州|文山壮族苗族自治州|西双版纳傣族自治州|大理白族自治州|德宏傣族景颇族自治州|怒江傈僳族自治州|迪庆藏族自治州",
|
||||
"西藏自治区": "拉萨市|日喀则市|昌都市|林芝市|山南市|那曲市|阿里地区",
|
||||
"陕西省": "西安市|铜川市|宝鸡市|咸阳市|渭南市|延安市|汉中市|榆林市|安康市|商洛市",
|
||||
"甘肃省": "兰州市|嘉峪关市|金昌市|白银市|天水市|武威市|张掖市|平凉市|酒泉市|庆阳市|定西市|陇南市|临夏回族自治州|甘南藏族自治州",
|
||||
"青海省": "西宁市|海东市|海北藏族自治州|黄南藏族自治州|海南藏族自治州|果洛藏族自治州|玉树藏族自治州|海西蒙古族藏族自治州",
|
||||
"宁夏回族自治区": "银川市|石嘴山市|吴忠市|固原市|中卫市",
|
||||
"新疆维吾尔自治区": "乌鲁木齐市|克拉玛依市|吐鲁番市|哈密市|昌吉回族自治州|博尔塔拉蒙古自治州|巴音郭楞蒙古自治州|阿克苏地区|克孜勒苏柯尔克孜自治州|喀什地区|和田地区|伊犁哈萨克自治州|塔城地区|阿勒泰地区",
|
||||
"香港特别行政区": "香港岛|九龙|新界",
|
||||
"澳门特别行政区": "澳门半岛|氹仔|路环",
|
||||
"台湾省": "台北市|新北市|桃园市|台中市|台南市|高雄市|基隆市|新竹市|嘉义市|新竹县|苗栗县|彰化县|南投县|云林县|嘉义县|屏东县|宜兰县|花莲县|台东县|澎湖县|金门县|连江县",
|
||||
"海外": "海外",
|
||||
};
|
||||
|
||||
var root = document.querySelector("[data-feedback-select]");
|
||||
var form = document.querySelector("[data-feedback-form]");
|
||||
var typeInput = document.querySelector("[data-feedback-type]");
|
||||
var contentInput = document.querySelector("[data-feedback-content]");
|
||||
var contactInput = document.querySelector("[data-feedback-contact]");
|
||||
var fileInput = document.querySelector("[data-feedback-file]");
|
||||
var fileText = document.querySelector("[data-feedback-file-text]");
|
||||
var uploadEl = document.querySelector("[data-feedback-upload]");
|
||||
var previewList = document.querySelector("[data-feedback-preview-list]");
|
||||
var submitBtn = document.querySelector("[data-feedback-submit]");
|
||||
var counter = document.querySelector("[data-feedback-counter]");
|
||||
var typeChips = document.querySelectorAll("[data-feedback-type-option]");
|
||||
var choiceGroups = document.querySelectorAll("[data-feedback-choice-group]");
|
||||
var provinceSelect = document.querySelector("[data-feedback-province]");
|
||||
var citySelect = document.querySelector("[data-feedback-city]");
|
||||
var operatorSelect = document.querySelector("[data-feedback-operator]");
|
||||
|
||||
var trigger = root ? root.querySelector("[data-feedback-select-trigger]") : null;
|
||||
var valueEl = root ? root.querySelector("[data-feedback-select-value]") : null;
|
||||
var menu = root ? root.querySelector("[data-feedback-select-menu]") : null;
|
||||
var options = root ? root.querySelectorAll(".feedback-select-option") : [];
|
||||
var placeholder = valueEl ? valueEl.textContent : "";
|
||||
var defaultSubmitText = submitBtn ? submitBtn.textContent : "提交反馈";
|
||||
/** @type {{ id: string, file: File, url: string }[]} */
|
||||
var selectedFiles = [];
|
||||
var fileSeq = 0;
|
||||
|
||||
function setSelectPlaceholderState(select) {
|
||||
if (!select) return;
|
||||
select.classList.toggle("is-placeholder", !select.value);
|
||||
}
|
||||
|
||||
function closeCustomSelect(wrap, restoreFocus) {
|
||||
if (!wrap) return;
|
||||
var custom = wrap._feedbackSelect;
|
||||
if (!custom) return;
|
||||
wrap.classList.remove("is-open");
|
||||
custom.menu.hidden = true;
|
||||
custom.trigger.setAttribute("aria-expanded", "false");
|
||||
if (restoreFocus) custom.trigger.focus();
|
||||
}
|
||||
|
||||
function closeAllCustomSelects(except) {
|
||||
document.querySelectorAll(".feedback-select-wrap.is-open").forEach(function (wrap) {
|
||||
if (wrap !== except) closeCustomSelect(wrap, false);
|
||||
});
|
||||
}
|
||||
|
||||
function focusCustomOption(menu, index) {
|
||||
var items = menu.querySelectorAll(".feedback-select-menu-option");
|
||||
if (!items.length) return;
|
||||
var nextIndex = Math.max(0, Math.min(index, items.length - 1));
|
||||
items[nextIndex].focus();
|
||||
}
|
||||
|
||||
function openCustomSelect(wrap) {
|
||||
if (!wrap) return;
|
||||
var custom = wrap._feedbackSelect;
|
||||
if (!custom || custom.select.disabled) return;
|
||||
closeAllCustomSelects(wrap);
|
||||
wrap.classList.add("is-open");
|
||||
custom.menu.hidden = false;
|
||||
custom.trigger.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
function buildCustomSelectMenu(select) {
|
||||
var wrap = select && select._feedbackSelectWrap;
|
||||
var custom = wrap && wrap._feedbackSelect;
|
||||
if (!custom) return;
|
||||
custom.menu.innerHTML = "";
|
||||
|
||||
Array.prototype.forEach.call(select.options, function (nativeOption) {
|
||||
if (nativeOption.disabled) return;
|
||||
var option = document.createElement("button");
|
||||
option.type = "button";
|
||||
option.className = "feedback-select-menu-option";
|
||||
option.textContent = nativeOption.textContent;
|
||||
option.setAttribute("role", "option");
|
||||
option.setAttribute("data-value", nativeOption.value);
|
||||
option.setAttribute("aria-selected", nativeOption.selected ? "true" : "false");
|
||||
option.classList.toggle("is-selected", nativeOption.selected);
|
||||
|
||||
option.addEventListener("click", function () {
|
||||
select.value = nativeOption.value;
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
closeCustomSelect(wrap, true);
|
||||
});
|
||||
option.addEventListener("keydown", function (event) {
|
||||
var items = Array.prototype.slice.call(
|
||||
custom.menu.querySelectorAll(".feedback-select-menu-option"),
|
||||
);
|
||||
var index = items.indexOf(option);
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
focusCustomOption(custom.menu, index + 1);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
focusCustomOption(custom.menu, index - 1);
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
focusCustomOption(custom.menu, 0);
|
||||
} else if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
focusCustomOption(custom.menu, items.length - 1);
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeCustomSelect(wrap, true);
|
||||
}
|
||||
});
|
||||
custom.menu.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function syncCustomSelect(select) {
|
||||
var wrap = select && select._feedbackSelectWrap;
|
||||
var custom = wrap && wrap._feedbackSelect;
|
||||
if (!custom) return;
|
||||
var selected = select.options[select.selectedIndex];
|
||||
custom.trigger.textContent = selected
|
||||
? selected.textContent
|
||||
: select.getAttribute("aria-label") || "请选择";
|
||||
custom.trigger.disabled = select.disabled;
|
||||
custom.trigger.classList.toggle("is-placeholder", !select.value);
|
||||
wrap.classList.toggle("is-disabled", select.disabled);
|
||||
if (select.disabled) closeCustomSelect(wrap, false);
|
||||
buildCustomSelectMenu(select);
|
||||
}
|
||||
|
||||
function initCustomSelect(select, index) {
|
||||
if (!select || select._feedbackSelectWrap) return;
|
||||
var wrap = document.createElement("div");
|
||||
var triggerButton = document.createElement("button");
|
||||
var customMenu = document.createElement("div");
|
||||
var menuId = (select.id || "feedback-select-" + index) + "-menu";
|
||||
|
||||
wrap.className = "feedback-select-wrap";
|
||||
triggerButton.type = "button";
|
||||
triggerButton.className = "feedback-select-trigger";
|
||||
triggerButton.setAttribute("aria-haspopup", "listbox");
|
||||
triggerButton.setAttribute("aria-expanded", "false");
|
||||
triggerButton.setAttribute("aria-controls", menuId);
|
||||
triggerButton.setAttribute("aria-label", select.getAttribute("aria-label") || "请选择");
|
||||
customMenu.className = "feedback-select-menu";
|
||||
customMenu.id = menuId;
|
||||
customMenu.setAttribute("role", "listbox");
|
||||
customMenu.hidden = true;
|
||||
|
||||
select.parentNode.insertBefore(wrap, select);
|
||||
wrap.appendChild(select);
|
||||
wrap.appendChild(triggerButton);
|
||||
wrap.appendChild(customMenu);
|
||||
select.classList.add("feedback-select-native");
|
||||
select.setAttribute("tabindex", "-1");
|
||||
select.setAttribute("aria-hidden", "true");
|
||||
select._feedbackSelectWrap = wrap;
|
||||
wrap._feedbackSelect = { select: select, trigger: triggerButton, menu: customMenu };
|
||||
|
||||
triggerButton.addEventListener("click", function () {
|
||||
if (wrap.classList.contains("is-open")) closeCustomSelect(wrap, false);
|
||||
else openCustomSelect(wrap);
|
||||
});
|
||||
triggerButton.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
||||
event.preventDefault();
|
||||
openCustomSelect(wrap);
|
||||
var selectedIndex = 0;
|
||||
var items = customMenu.querySelectorAll(".feedback-select-menu-option");
|
||||
items.forEach(function (item, itemIndex) {
|
||||
if (item.classList.contains("is-selected")) selectedIndex = itemIndex;
|
||||
});
|
||||
focusCustomOption(customMenu, selectedIndex);
|
||||
});
|
||||
select.addEventListener("change", function () {
|
||||
syncCustomSelect(select);
|
||||
});
|
||||
syncCustomSelect(select);
|
||||
}
|
||||
|
||||
function initCustomSelects() {
|
||||
[provinceSelect, citySelect, operatorSelect].forEach(initCustomSelect);
|
||||
}
|
||||
|
||||
function renderCityOptions(province) {
|
||||
if (!citySelect) return;
|
||||
citySelect.innerHTML = "";
|
||||
var placeholderOption = document.createElement("option");
|
||||
placeholderOption.value = "";
|
||||
placeholderOption.textContent = "城市(选填)";
|
||||
placeholderOption.selected = true;
|
||||
citySelect.appendChild(placeholderOption);
|
||||
|
||||
var citySource = CITY_OPTIONS[province] || "";
|
||||
var cities = citySource ? citySource.split("|") : [];
|
||||
cities.forEach(function (city) {
|
||||
var option = document.createElement("option");
|
||||
option.value = city;
|
||||
option.textContent = city;
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
citySelect.disabled = cities.length === 0;
|
||||
setSelectPlaceholderState(citySelect);
|
||||
syncCustomSelect(citySelect);
|
||||
}
|
||||
|
||||
[provinceSelect, citySelect, operatorSelect].forEach(function (select) {
|
||||
if (!select) return;
|
||||
setSelectPlaceholderState(select);
|
||||
select.addEventListener("change", function () {
|
||||
setSelectPlaceholderState(select);
|
||||
});
|
||||
});
|
||||
|
||||
if (provinceSelect) {
|
||||
provinceSelect.addEventListener("change", function () {
|
||||
renderCityOptions(provinceSelect.value);
|
||||
});
|
||||
}
|
||||
renderCityOptions(provinceSelect ? provinceSelect.value : "");
|
||||
initCustomSelects();
|
||||
|
||||
function toast(message, type) {
|
||||
if (window.AuthAPI && typeof AuthAPI.toast === "function") {
|
||||
AuthAPI.toast(message, type);
|
||||
return;
|
||||
}
|
||||
if (typeof showToast === "function") {
|
||||
showToast(message, type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function setSubmitting(loading) {
|
||||
if (!submitBtn) return;
|
||||
submitBtn.disabled = !!loading;
|
||||
submitBtn.classList.toggle("is-loading", !!loading);
|
||||
submitBtn.textContent = loading ? "提交中..." : defaultSubmitText;
|
||||
}
|
||||
|
||||
function updateUploadState() {
|
||||
var count = selectedFiles.length;
|
||||
if (uploadEl) uploadEl.classList.toggle("is-full", count >= MAX_FILES);
|
||||
if (previewList) previewList.hidden = count === 0;
|
||||
if (fileText) {
|
||||
fileText.textContent =
|
||||
count > 0
|
||||
? "已选 " + count + "/" + MAX_FILES + " 个文件"
|
||||
: "支持jpg/png 最多3个文件";
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreviews() {
|
||||
if (!previewList) return;
|
||||
previewList.innerHTML = "";
|
||||
selectedFiles.forEach(function (item) {
|
||||
var li = document.createElement("li");
|
||||
li.className = "feedback-upload-item";
|
||||
li.setAttribute("data-file-id", item.id);
|
||||
|
||||
var img = document.createElement("img");
|
||||
img.src = item.url;
|
||||
img.alt = item.file.name || "截图预览";
|
||||
|
||||
var removeBtn = document.createElement("button");
|
||||
removeBtn.type = "button";
|
||||
removeBtn.className = "feedback-upload-remove";
|
||||
removeBtn.setAttribute("aria-label", "移除图片");
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removeFile(item.id);
|
||||
});
|
||||
|
||||
li.appendChild(img);
|
||||
li.appendChild(removeBtn);
|
||||
previewList.appendChild(li);
|
||||
});
|
||||
updateUploadState();
|
||||
}
|
||||
|
||||
function clearFiles() {
|
||||
selectedFiles.forEach(function (item) {
|
||||
if (item.url) URL.revokeObjectURL(item.url);
|
||||
});
|
||||
selectedFiles = [];
|
||||
if (fileInput) fileInput.value = "";
|
||||
renderPreviews();
|
||||
}
|
||||
|
||||
function removeFile(id) {
|
||||
var next = [];
|
||||
selectedFiles.forEach(function (item) {
|
||||
if (item.id === id) {
|
||||
if (item.url) URL.revokeObjectURL(item.url);
|
||||
} else {
|
||||
next.push(item);
|
||||
}
|
||||
});
|
||||
selectedFiles = next;
|
||||
if (fileInput) fileInput.value = "";
|
||||
renderPreviews();
|
||||
}
|
||||
|
||||
function isAcceptedImage(file) {
|
||||
if (!file) return false;
|
||||
if (file.type && ACCEPT_TYPES[file.type]) return true;
|
||||
var name = String(file.name || "").toLowerCase();
|
||||
return /\.(png|jpe?g)$/.test(name);
|
||||
}
|
||||
|
||||
function addFiles(fileList) {
|
||||
if (!fileList || !fileList.length) return;
|
||||
var remain = MAX_FILES - selectedFiles.length;
|
||||
if (remain <= 0) {
|
||||
toast("最多上传 " + MAX_FILES + " 张图片", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
var files = Array.prototype.slice.call(fileList, 0);
|
||||
var added = 0;
|
||||
var skippedSize = 0;
|
||||
var skippedType = 0;
|
||||
var skippedLimit = 0;
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
if (added >= remain) {
|
||||
skippedLimit += files.length - i;
|
||||
break;
|
||||
}
|
||||
var file = files[i];
|
||||
if (!isAcceptedImage(file)) {
|
||||
skippedType += 1;
|
||||
continue;
|
||||
}
|
||||
if (file.size > MAX_SIZE) {
|
||||
skippedSize += 1;
|
||||
continue;
|
||||
}
|
||||
fileSeq += 1;
|
||||
selectedFiles.push({
|
||||
id: "f" + fileSeq,
|
||||
file: file,
|
||||
url: URL.createObjectURL(file),
|
||||
});
|
||||
added += 1;
|
||||
}
|
||||
|
||||
if (skippedType) toast("仅支持 PNG / JPG 图片", "error");
|
||||
if (skippedSize) toast("单张图片不能超过 5MB", "error");
|
||||
if (skippedLimit) toast("最多上传 " + MAX_FILES + " 张图片", "error");
|
||||
|
||||
renderPreviews();
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (!root) return;
|
||||
root.classList.add("is-open");
|
||||
if (menu) menu.hidden = false;
|
||||
if (trigger) trigger.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
if (!root) return;
|
||||
root.classList.remove("is-open");
|
||||
if (menu) menu.hidden = true;
|
||||
if (trigger) trigger.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
if (!root) return;
|
||||
if (root.classList.contains("is-open")) closeMenu();
|
||||
else openMenu();
|
||||
}
|
||||
|
||||
function selectOption(option) {
|
||||
var value = option.getAttribute("data-value") || "";
|
||||
var label = option.textContent.trim();
|
||||
if (typeInput) typeInput.value = value;
|
||||
if (valueEl) {
|
||||
valueEl.textContent = label;
|
||||
valueEl.classList.toggle("is-placeholder", !value);
|
||||
}
|
||||
options.forEach(function (item) {
|
||||
var active = item === option;
|
||||
item.classList.toggle("is-active", active);
|
||||
item.setAttribute("aria-selected", active ? "true" : "false");
|
||||
});
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
if (trigger) {
|
||||
trigger.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
toggleMenu();
|
||||
});
|
||||
}
|
||||
|
||||
options.forEach(function (option) {
|
||||
option.setAttribute("aria-selected", "false");
|
||||
option.addEventListener("click", function () {
|
||||
selectOption(option);
|
||||
});
|
||||
});
|
||||
|
||||
typeChips.forEach(function (chip) {
|
||||
chip.addEventListener("click", function () {
|
||||
typeChips.forEach(function (item) {
|
||||
item.classList.toggle("is-active", item === chip);
|
||||
});
|
||||
if (typeInput) {
|
||||
var nextType = chip.getAttribute("data-value") || "4";
|
||||
typeInput.value = nextType;
|
||||
typeInput.setAttribute("value", nextType);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
choiceGroups.forEach(function (group) {
|
||||
group.querySelectorAll(".feedback-chip").forEach(function (chip) {
|
||||
chip.addEventListener("click", function () {
|
||||
group.querySelectorAll(".feedback-chip").forEach(function (item) {
|
||||
item.classList.toggle("is-active", item === chip);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function updateCounter() {
|
||||
if (!counter || !contentInput) return;
|
||||
counter.textContent = String(contentInput.value.length) + "/500";
|
||||
}
|
||||
|
||||
if (contentInput) contentInput.addEventListener("input", updateCounter);
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (root && !root.contains(event.target)) closeMenu();
|
||||
if (!event.target.closest(".feedback-select-wrap")) closeAllCustomSelects(null);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") closeMenu();
|
||||
if (event.key === "Escape") closeAllCustomSelects(null);
|
||||
});
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", function () {
|
||||
addFiles(fileInput.files);
|
||||
fileInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
function requireLogin() {
|
||||
if (window.CookieUtil && CookieUtil.isLoggedIn()) return true;
|
||||
toast("请先登录后再提交反馈", "error");
|
||||
var loginURL = "/login/return/feedback/";
|
||||
if (typeof window.openLoginDialog === "function") {
|
||||
var opened = window.openLoginDialog();
|
||||
if (opened && typeof opened.then === "function") {
|
||||
opened.catch(function () {
|
||||
window.location.href = loginURL;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
window.location.href = loginURL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function uploadAllImages() {
|
||||
if (!selectedFiles.length) return Promise.resolve([]);
|
||||
if (!window.AuthAPI || typeof AuthAPI.uploadImage !== "function") {
|
||||
return Promise.reject(new Error("上传组件未加载"));
|
||||
}
|
||||
var urls = [];
|
||||
var chain = Promise.resolve();
|
||||
selectedFiles.forEach(function (item) {
|
||||
chain = chain.then(function () {
|
||||
return AuthAPI.uploadImage(item.file).then(function (data) {
|
||||
var url = (data && (data.url || data.sourceNameFs)) || "";
|
||||
if (!url) throw new Error("上传失败");
|
||||
urls.push(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
return chain.then(function () {
|
||||
return urls;
|
||||
});
|
||||
}
|
||||
|
||||
if (!form) return;
|
||||
|
||||
if (typeInput && typeChips.length) {
|
||||
var initialTypeChip = document.querySelector("[data-feedback-type-option].is-active") || typeChips[0];
|
||||
typeInput.value = initialTypeChip.getAttribute("data-value") || "2";
|
||||
}
|
||||
updateUploadState();
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!window.HttpRequest) {
|
||||
return;
|
||||
}
|
||||
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
|
||||
if (!requireLogin()) return;
|
||||
|
||||
var activeTypeChip = document.querySelector("[data-feedback-type-option].is-active");
|
||||
var type = parseInt(
|
||||
activeTypeChip
|
||||
? activeTypeChip.getAttribute("data-value")
|
||||
: typeInput && typeInput.value
|
||||
? typeInput.value
|
||||
: "",
|
||||
10,
|
||||
);
|
||||
if (!(type >= 1 && type <= 4)) {
|
||||
toast("请选择问题类型", "error");
|
||||
openMenu();
|
||||
if (valueEl) {
|
||||
valueEl.textContent = placeholder;
|
||||
valueEl.classList.add("is-placeholder");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var content = contentInput ? String(contentInput.value || "").trim() : "";
|
||||
if (!content) {
|
||||
toast("请填写问题描述", "error");
|
||||
if (contentInput) contentInput.focus();
|
||||
return;
|
||||
}
|
||||
if (content.length > 500) {
|
||||
toast("问题描述最多 500 字", "error");
|
||||
return;
|
||||
}
|
||||
if (selectedFiles.length > MAX_FILES) {
|
||||
toast("最多上传 " + MAX_FILES + " 张图片", "error");
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < selectedFiles.length; i++) {
|
||||
if (selectedFiles[i].file.size > MAX_SIZE) {
|
||||
toast("单张图片不能超过 5MB", "error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var contact = contactInput ? String(contactInput.value || "").trim() : "";
|
||||
if (!contact) {
|
||||
toast("请填写联系方式", "error");
|
||||
if (contactInput) contactInput.focus();
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
|
||||
uploadAllImages()
|
||||
.then(function (screenshots) {
|
||||
var body = {
|
||||
type: type,
|
||||
content: content,
|
||||
};
|
||||
if (contact) body.contact = contact;
|
||||
if (screenshots && screenshots.length) body.screenshots = screenshots;
|
||||
return HttpRequest.post("/api/feedback/add", 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;
|
||||
}
|
||||
toast((res && res.tip) || "提交成功", "success");
|
||||
form.reset();
|
||||
renderCityOptions("");
|
||||
setSelectPlaceholderState(provinceSelect);
|
||||
setSelectPlaceholderState(operatorSelect);
|
||||
syncCustomSelect(provinceSelect);
|
||||
syncCustomSelect(operatorSelect);
|
||||
if (typeInput) typeInput.value = "";
|
||||
if (valueEl) {
|
||||
valueEl.textContent = placeholder;
|
||||
valueEl.classList.add("is-placeholder");
|
||||
}
|
||||
options.forEach(function (item) {
|
||||
item.classList.remove("is-active");
|
||||
item.setAttribute("aria-selected", "false");
|
||||
});
|
||||
typeChips.forEach(function (item, index) {
|
||||
item.classList.toggle("is-active", index === 0);
|
||||
});
|
||||
if (typeInput && typeChips.length) {
|
||||
typeInput.value = typeChips[0].getAttribute("data-value") || "2";
|
||||
}
|
||||
choiceGroups.forEach(function (group) {
|
||||
var groupChips = group.querySelectorAll(".feedback-chip");
|
||||
groupChips.forEach(function (item, index) {
|
||||
item.classList.toggle("is-active", index === 0 && group.hasAttribute("data-default-first"));
|
||||
});
|
||||
});
|
||||
updateCounter();
|
||||
clearFiles();
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err && err.code === 401) {
|
||||
toast("请先登录后再提交反馈", "error");
|
||||
if (typeof window.openLoginDialog === "function") {
|
||||
var opened = window.openLoginDialog();
|
||||
if (opened && typeof opened.then === "function") {
|
||||
opened.catch(function () {
|
||||
window.location.href = "/login/return/feedback/";
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
toast((err && err.message) || "提交失败", "error");
|
||||
})
|
||||
.finally(function () {
|
||||
setSubmitting(false);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,8 @@
|
||||
(function () {
|
||||
var scrollTopButtons = document.querySelectorAll(".home-mobile-footer-top");
|
||||
scrollTopButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,568 @@
|
||||
(function () {
|
||||
var SEARCH_HISTORY_KEY = "searchHistory";
|
||||
var SEARCH_HISTORY_LIMIT = 10;
|
||||
|
||||
function readSearchHistory() {
|
||||
try {
|
||||
var stored = window.localStorage.getItem(SEARCH_HISTORY_KEY);
|
||||
var parsed = stored ? JSON.parse(stored) : [];
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed
|
||||
.filter(function (item) {
|
||||
return typeof item === "string" && item.trim() !== "";
|
||||
})
|
||||
.map(function (item) {
|
||||
return item.trim();
|
||||
})
|
||||
.slice(0, SEARCH_HISTORY_LIMIT);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveSearchHistory(keyword) {
|
||||
keyword = String(keyword || "").trim();
|
||||
if (!keyword) return readSearchHistory();
|
||||
|
||||
var compareKeyword = keyword.toLocaleLowerCase();
|
||||
var history = readSearchHistory().filter(function (item) {
|
||||
return item.toLocaleLowerCase() !== compareKeyword;
|
||||
});
|
||||
history.unshift(keyword);
|
||||
history = history.slice(0, SEARCH_HISTORY_LIMIT);
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history));
|
||||
} catch (_) {
|
||||
// 隐私模式或存储空间不可用时不阻断搜索跳转。
|
||||
}
|
||||
return history;
|
||||
}
|
||||
|
||||
window.SearchHistory = {
|
||||
key: SEARCH_HISTORY_KEY,
|
||||
limit: SEARCH_HISTORY_LIMIT,
|
||||
get: readSearchHistory,
|
||||
add: saveSearchHistory,
|
||||
clear: function () {
|
||||
try {
|
||||
window.localStorage.removeItem(SEARCH_HISTORY_KEY);
|
||||
} catch (_) {
|
||||
// localStorage 不可用时保持静默。
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!document.querySelector('meta[name="viewport"]')) {
|
||||
var viewport = document.createElement("meta");
|
||||
viewport.name = "viewport";
|
||||
viewport.content = "width=device-width, initial-scale=1.0";
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
|
||||
var mobileMenu = document.getElementById("homeMobileMenu");
|
||||
var mobileMenuButton = document.querySelector(".header-menu-button");
|
||||
var mobileMenuCloseButtons = mobileMenu
|
||||
? mobileMenu.querySelectorAll("[data-home-menu-close]")
|
||||
: [];
|
||||
|
||||
function setMobileMenu(open) {
|
||||
if (!mobileMenu || !mobileMenuButton) return;
|
||||
mobileMenu.hidden = !open;
|
||||
mobileMenuButton.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
document.body.classList.toggle("home-mobile-menu-open", open);
|
||||
if (open) {
|
||||
var closeButton = mobileMenu.querySelector(".home-mobile-menu-close");
|
||||
if (closeButton) closeButton.focus({ preventScroll: true });
|
||||
} else {
|
||||
mobileMenuButton.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (mobileMenu && mobileMenuButton) {
|
||||
mobileMenuButton.addEventListener("click", function () {
|
||||
setMobileMenu(true);
|
||||
});
|
||||
Array.prototype.forEach.call(mobileMenuCloseButtons, function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
setMobileMenu(false);
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && !mobileMenu.hidden) setMobileMenu(false);
|
||||
});
|
||||
}
|
||||
|
||||
var DEFAULT_AVATAR = "/static/images/avatar.png";
|
||||
var PLACEHOLDER = "/static/images/placeholder.png?v=2";
|
||||
|
||||
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");
|
||||
if (src && typeof shouldDecryptImage === "function" && shouldDecryptImage(src)) {
|
||||
img.classList.add("lazy");
|
||||
img.src = PLACEHOLDER;
|
||||
img.setAttribute("data-src", src);
|
||||
} else {
|
||||
img.classList.remove("lazy");
|
||||
img.removeAttribute("data-src");
|
||||
img.src = src || DEFAULT_AVATAR;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDecrypt(root) {
|
||||
if (typeof window.initLazyImages === "function") {
|
||||
window.initLazyImages(root || document);
|
||||
}
|
||||
}
|
||||
|
||||
/** 用 cookie 登录态同步 header-account */
|
||||
function parseExpireTime(value) {
|
||||
if (value == null || value === "") return NaN;
|
||||
if (typeof value === "number") {
|
||||
return value < 1e12 ? value * 1000 : value;
|
||||
}
|
||||
var text = String(value).trim();
|
||||
if (!text) return NaN;
|
||||
if (/^\d+$/.test(text)) {
|
||||
var num = Number(text);
|
||||
return num < 1e12 ? num * 1000 : num;
|
||||
}
|
||||
return Date.parse(text);
|
||||
}
|
||||
|
||||
// vipType≠0 且 currentVipExpire 未过期(与 91ly 一致)
|
||||
function isVipActive(user) {
|
||||
if (!user) return false;
|
||||
var type = Number(user.vipType);
|
||||
if (!type || type === 0) return false;
|
||||
var expireMs = parseExpireTime(user.currentVipExpire);
|
||||
if (!isFinite(expireMs) || expireMs <= 0) return false;
|
||||
return expireMs > Date.now();
|
||||
}
|
||||
|
||||
function syncHeaderVip(session) {
|
||||
var active = isVipActive(session);
|
||||
document.querySelectorAll("[data-header-vip], [data-header-vip-sep]").forEach(function (el) {
|
||||
el.hidden = !active;
|
||||
});
|
||||
}
|
||||
|
||||
function syncHeaderAccount() {
|
||||
var account = document.querySelector("[data-header-account]");
|
||||
if (!account || !window.CookieUtil) return;
|
||||
|
||||
// 有 token 即展示个人信息(含游客)
|
||||
var loggedIn = CookieUtil.isLoggedIn();
|
||||
var session = loggedIn ? CookieUtil.getAuthSession() : null;
|
||||
var nameEl = account.querySelector("[data-header-username]");
|
||||
|
||||
if (loggedIn && session) {
|
||||
var displayName =
|
||||
session.nickName || session.accountNo || session.email || "用户";
|
||||
var avatar = usableAvatar(session.avatar);
|
||||
|
||||
account.href = "/user-center/";
|
||||
account.removeAttribute("data-login-dialog-open");
|
||||
|
||||
var guest = account.querySelector("[data-header-guest-avatar]");
|
||||
if (guest) guest.remove();
|
||||
|
||||
var photo = account.querySelector("[data-header-avatar]");
|
||||
if (!photo) {
|
||||
photo = document.createElement("img");
|
||||
photo.className = "account-avatar-photo";
|
||||
photo.setAttribute("data-header-avatar", "");
|
||||
photo.alt = displayName + "的头像";
|
||||
account.insertBefore(photo, nameEl || account.firstChild);
|
||||
}
|
||||
setAvatarImg(photo, avatar);
|
||||
|
||||
if (nameEl) {
|
||||
nameEl.textContent = displayName;
|
||||
nameEl.classList.add("account-label--logged");
|
||||
}
|
||||
|
||||
refreshDecrypt(account.parentElement || document);
|
||||
syncHeaderVip(session);
|
||||
return;
|
||||
}
|
||||
|
||||
account.href = "#login";
|
||||
account.setAttribute("data-login-dialog-open", "");
|
||||
|
||||
var loggedPhoto = account.querySelector("[data-header-avatar]");
|
||||
if (loggedPhoto) loggedPhoto.remove();
|
||||
if (!account.querySelector("[data-header-guest-avatar]")) {
|
||||
var wrap = document.createElement("div");
|
||||
wrap.className = "account-avatar";
|
||||
wrap.setAttribute("data-header-guest-avatar", "");
|
||||
wrap.innerHTML =
|
||||
'<img class="account-avatar-ring" src="' +
|
||||
DEFAULT_AVATAR +
|
||||
'" alt="未登录用户头像" />' +
|
||||
'<div class="account-avatar-body"></div>' +
|
||||
'<div class="account-avatar-head"></div>';
|
||||
account.insertBefore(wrap, nameEl || account.firstChild);
|
||||
}
|
||||
if (nameEl) {
|
||||
nameEl.textContent = "未登录";
|
||||
nameEl.classList.remove("account-label--logged");
|
||||
}
|
||||
|
||||
syncHeaderVip(null);
|
||||
}
|
||||
|
||||
syncHeaderAccount();
|
||||
window.syncHeaderAccount = syncHeaderAccount;
|
||||
document.addEventListener("auth:login", function () {
|
||||
syncHeaderAccount();
|
||||
});
|
||||
document.addEventListener("auth:logout", function () {
|
||||
syncHeaderAccount();
|
||||
});
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (event) {
|
||||
var el = event.target;
|
||||
if (el && el.nodeType === 3) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (!el || !el.closest) return;
|
||||
var tag = el.closest(".poster-card-tag[data-tag-href], .ranking-card-tag[data-tag-href], .detail-tag[data-tag-href]");
|
||||
if (!tag) return;
|
||||
var href = tag.getAttribute("data-tag-href");
|
||||
if (!href) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
|
||||
window.location.href = href;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
function initDesktopSearchPopover() {
|
||||
var form = document.querySelector('form.header-search[action="/search/"]');
|
||||
var input = form && form.querySelector('input[name="q"]');
|
||||
var popover = document.querySelector("[data-header-search-popover]");
|
||||
if (!form || !input || !popover) return;
|
||||
|
||||
var desktopMedia = window.matchMedia("(min-width: 768px)");
|
||||
var historyList = popover.querySelector("[data-search-history-list]");
|
||||
var hotList = popover.querySelector("[data-search-hot-list]");
|
||||
var guessList = popover.querySelector("[data-search-guess-list]");
|
||||
var historySection = popover.querySelector(".header-search-history");
|
||||
var hotSection = popover.querySelector(".header-search-hot");
|
||||
var guessSection = popover.querySelector(".header-search-guess");
|
||||
var dividers = popover.querySelectorAll(".header-search-popover-divider");
|
||||
var clearButton = popover.querySelector("[data-search-history-clear]");
|
||||
var closeButton = popover.querySelector("[data-header-search-close]");
|
||||
var changeButton = popover.querySelector("[data-search-guess-change]");
|
||||
var hotLikeData = null;
|
||||
var hotLikeRequest = null;
|
||||
var guessOffset = 0;
|
||||
var searchActive = false;
|
||||
|
||||
function syncPopoverVisibility() {
|
||||
var hasHistory = historySection && !historySection.hidden;
|
||||
var hasHot = hotSection && !hotSection.hidden;
|
||||
var hasGuess = guessSection && !guessSection.hidden;
|
||||
var hasContent = hasHistory || hasHot || hasGuess;
|
||||
|
||||
if (dividers[0]) dividers[0].hidden = !(hasHistory && (hasHot || hasGuess));
|
||||
if (dividers[1]) dividers[1].hidden = !(hasHot && hasGuess);
|
||||
|
||||
popover.hidden = !(searchActive && hasContent);
|
||||
form.classList.toggle("is-search-open", searchActive && hasContent);
|
||||
input.setAttribute("aria-expanded", searchActive && hasContent ? "true" : "false");
|
||||
if (!popover.hidden) positionPopover();
|
||||
}
|
||||
|
||||
function searchKeyword(keyword) {
|
||||
keyword = String(keyword || "").trim();
|
||||
if (!keyword) return;
|
||||
saveSearchHistory(keyword);
|
||||
window.location.href = "/search/" + encodeURIComponent(keyword) + "/";
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!historyList) return;
|
||||
historyList.textContent = "";
|
||||
var history = readSearchHistory();
|
||||
if (historySection) historySection.hidden = history.length === 0;
|
||||
history.forEach(function (keyword) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "header-search-history-tag";
|
||||
button.textContent = keyword;
|
||||
button.addEventListener("click", function () {
|
||||
searchKeyword(keyword);
|
||||
});
|
||||
historyList.appendChild(button);
|
||||
});
|
||||
syncPopoverVisibility();
|
||||
}
|
||||
|
||||
function normalizeHotItems(data) {
|
||||
var hot = data && Array.isArray(data.hotSearch) ? data.hotSearch : [];
|
||||
return hot
|
||||
.map(function (item) {
|
||||
return { keyword: String((item && item.keyword) || "").trim() };
|
||||
})
|
||||
.filter(function (item) {
|
||||
return item.keyword !== "";
|
||||
})
|
||||
.slice(0, 6);
|
||||
}
|
||||
|
||||
function renderHotSearch() {
|
||||
if (!hotList) return;
|
||||
hotList.textContent = "";
|
||||
var items = normalizeHotItems(hotLikeData);
|
||||
if (hotSection) hotSection.hidden = items.length === 0;
|
||||
items.forEach(function (item, index) {
|
||||
var button = document.createElement("button");
|
||||
var rank = document.createElement("span");
|
||||
var keyword = document.createElement("span");
|
||||
button.type = "button";
|
||||
button.className = "header-search-hot-item";
|
||||
rank.className = "header-search-hot-rank";
|
||||
keyword.className = "header-search-hot-keyword";
|
||||
rank.textContent = String(index + 1);
|
||||
keyword.textContent = item.keyword;
|
||||
button.appendChild(rank);
|
||||
button.appendChild(keyword);
|
||||
button.addEventListener("click", function () {
|
||||
searchKeyword(item.keyword);
|
||||
});
|
||||
hotList.appendChild(button);
|
||||
});
|
||||
syncPopoverVisibility();
|
||||
}
|
||||
|
||||
function detailHref(item) {
|
||||
var slug = String((item && item.slug) || "").trim();
|
||||
var id = Number(item && item.id);
|
||||
var key = slug || (id > 0 ? String(id) : "");
|
||||
return key ? "/detail/" + encodeURIComponent(key) + "/" : "#";
|
||||
}
|
||||
|
||||
function firstCardTag(item) {
|
||||
var tags = item && Array.isArray(item.tagNames) ? item.tagNames : [];
|
||||
var themes = item && Array.isArray(item.themeNames) ? item.themeNames : [];
|
||||
return String(tags[0] || themes[0] || "热门").trim();
|
||||
}
|
||||
|
||||
function setCardCover(img, rawURL, title) {
|
||||
var cover = String(rawURL || "").trim();
|
||||
var imageTitle = String(title || "精彩短剧").trim();
|
||||
img.alt = imageTitle;
|
||||
img.title = imageTitle;
|
||||
img.addEventListener("error", function () {
|
||||
img.classList.remove("lazy");
|
||||
img.removeAttribute("data-src");
|
||||
img.src = PLACEHOLDER;
|
||||
}, { once: true });
|
||||
if (cover && typeof window.shouldDecryptImage === "function" && window.shouldDecryptImage(cover)) {
|
||||
img.className = "lazy";
|
||||
img.src = PLACEHOLDER;
|
||||
img.setAttribute("data-src", cover);
|
||||
return;
|
||||
}
|
||||
img.src = cover || PLACEHOLDER;
|
||||
}
|
||||
|
||||
function createGuessCard(item) {
|
||||
var card = document.createElement("a");
|
||||
var cover = document.createElement("div");
|
||||
var image = document.createElement("img");
|
||||
var episode = document.createElement("span");
|
||||
var copy = document.createElement("div");
|
||||
var title = document.createElement("div");
|
||||
var desc = document.createElement("div");
|
||||
var tagRow = document.createElement("div");
|
||||
var tag = document.createElement("span");
|
||||
var totalEpisode = Math.max(0, Number(item && item.totalEpisode) || 0);
|
||||
|
||||
card.className = "header-search-guess-card";
|
||||
card.href = detailHref(item);
|
||||
card.setAttribute("aria-label", String((item && item.title) || "查看短剧"));
|
||||
cover.className = "header-search-guess-cover";
|
||||
episode.className = "header-search-guess-episode";
|
||||
copy.className = "header-search-guess-copy";
|
||||
title.className = "header-search-guess-title";
|
||||
desc.className = "header-search-guess-desc";
|
||||
tagRow.className = "header-search-guess-tag-row";
|
||||
tag.className = "header-search-guess-tag";
|
||||
|
||||
setCardCover(image, item && item.cover, item && item.title);
|
||||
episode.textContent = totalEpisode ? totalEpisode + "集" : "更新中";
|
||||
title.textContent = String((item && item.title) || "精彩短剧");
|
||||
desc.textContent = String((item && item.desc) || "精彩内容等你来看");
|
||||
tag.textContent = firstCardTag(item);
|
||||
|
||||
cover.appendChild(image);
|
||||
cover.appendChild(episode);
|
||||
tagRow.appendChild(tag);
|
||||
copy.appendChild(title);
|
||||
copy.appendChild(desc);
|
||||
copy.appendChild(tagRow);
|
||||
card.appendChild(cover);
|
||||
card.appendChild(copy);
|
||||
return card;
|
||||
}
|
||||
|
||||
function currentGuessBatch(items) {
|
||||
if (items.length <= 4) return items.slice(0, 4);
|
||||
var batch = [];
|
||||
for (var index = 0; index < 4; index += 1) {
|
||||
batch.push(items[(guessOffset + index) % items.length]);
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
function renderGuessLike() {
|
||||
if (!guessList) return;
|
||||
guessList.textContent = "";
|
||||
var items = hotLikeData && Array.isArray(hotLikeData.guessLike)
|
||||
? hotLikeData.guessLike.filter(function (item) { return item && (item.id || item.slug); })
|
||||
: [];
|
||||
if (guessSection) guessSection.hidden = items.length === 0;
|
||||
currentGuessBatch(items).forEach(function (item) {
|
||||
guessList.appendChild(createGuessCard(item));
|
||||
});
|
||||
if (items.length) refreshDecrypt(guessList);
|
||||
syncPopoverVisibility();
|
||||
}
|
||||
|
||||
function renderHotLike() {
|
||||
renderHotSearch();
|
||||
renderGuessLike();
|
||||
}
|
||||
|
||||
function loadHotLike() {
|
||||
if (hotLikeData) {
|
||||
renderHotLike();
|
||||
return Promise.resolve(hotLikeData);
|
||||
}
|
||||
if (hotLikeRequest) return hotLikeRequest;
|
||||
if (hotSection) hotSection.hidden = true;
|
||||
if (guessSection) guessSection.hidden = true;
|
||||
syncPopoverVisibility();
|
||||
if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") {
|
||||
hotLikeData = {};
|
||||
renderHotLike();
|
||||
return Promise.resolve(hotLikeData);
|
||||
}
|
||||
hotLikeRequest = window.HttpRequest.get("/api/playlet/hot-like")
|
||||
.then(function (response) {
|
||||
hotLikeData = response && response.data ? response.data : {};
|
||||
renderHotLike();
|
||||
return hotLikeData;
|
||||
})
|
||||
.catch(function () {
|
||||
hotLikeData = {};
|
||||
renderHotLike();
|
||||
return hotLikeData;
|
||||
});
|
||||
return hotLikeRequest;
|
||||
}
|
||||
|
||||
function positionPopover() {
|
||||
if (popover.hidden || !desktopMedia.matches) return;
|
||||
var rect = form.getBoundingClientRect();
|
||||
var header = form.closest(".site-header");
|
||||
var headerRect = header ? header.getBoundingClientRect() : rect;
|
||||
var viewportPadding = 24;
|
||||
var width = popover.offsetWidth;
|
||||
var left = rect.right - width;
|
||||
var top = Math.max(rect.bottom + 12, headerRect.bottom + 12);
|
||||
left = Math.max(viewportPadding, Math.min(left, window.innerWidth - width - viewportPadding));
|
||||
popover.style.left = Math.round(left) + "px";
|
||||
popover.style.top = Math.round(top) + "px";
|
||||
popover.style.maxHeight = Math.max(240, window.innerHeight - top - 16) + "px";
|
||||
}
|
||||
|
||||
function setPopover(open, options) {
|
||||
options = options || {};
|
||||
searchActive = Boolean(open && desktopMedia.matches);
|
||||
if (searchActive) {
|
||||
renderHistory();
|
||||
if (hotLikeData) renderHotLike();
|
||||
else loadHotLike();
|
||||
} else if (options.blur) {
|
||||
input.blur();
|
||||
}
|
||||
syncPopoverVisibility();
|
||||
}
|
||||
|
||||
input.addEventListener("focus", function () {
|
||||
setPopover(true);
|
||||
});
|
||||
input.addEventListener("click", function () {
|
||||
setPopover(true);
|
||||
});
|
||||
clearButton.addEventListener("click", function () {
|
||||
window.SearchHistory.clear();
|
||||
renderHistory();
|
||||
input.focus({ preventScroll: true });
|
||||
});
|
||||
closeButton.addEventListener("click", function () {
|
||||
setPopover(false, { blur: true });
|
||||
});
|
||||
changeButton.addEventListener("click", function () {
|
||||
var items = hotLikeData && Array.isArray(hotLikeData.guessLike) ? hotLikeData.guessLike : [];
|
||||
if (items.length > 4) guessOffset = (guessOffset + 4) % items.length;
|
||||
renderGuessLike();
|
||||
});
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
if (!searchActive || form.contains(event.target) || popover.contains(event.target)) return;
|
||||
setPopover(false);
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && searchActive) {
|
||||
setPopover(false, { blur: true });
|
||||
}
|
||||
});
|
||||
window.addEventListener("resize", function () {
|
||||
if (!desktopMedia.matches) {
|
||||
setPopover(false);
|
||||
return;
|
||||
}
|
||||
positionPopover();
|
||||
});
|
||||
window.addEventListener("scroll", positionPopover, { passive: true });
|
||||
if (typeof desktopMedia.addEventListener === "function") {
|
||||
desktopMedia.addEventListener("change", function (event) {
|
||||
if (!event.matches) setPopover(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initDesktopSearchPopover();
|
||||
|
||||
document.addEventListener("submit", function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.matches || !form.matches('form[action="/search/"]')) return;
|
||||
var input = form.querySelector('input[name="q"]');
|
||||
var keyword = String((input && input.value) || "").trim();
|
||||
if (!keyword) return;
|
||||
saveSearchHistory(keyword);
|
||||
event.preventDefault();
|
||||
window.location.href = "/search/" + encodeURIComponent(keyword) + "/";
|
||||
});
|
||||
|
||||
if (document.body && document.body.classList.contains("search-layout")) {
|
||||
var currentSearchInput = document.querySelector('.header-search input[name="q"]');
|
||||
saveSearchHistory(currentSearchInput && currentSearchInput.value);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 观看历史页:清空记录
|
||||
*/
|
||||
(function () {
|
||||
var clearBtns = document.querySelectorAll("[data-history-clear]");
|
||||
var confirmMask = document.querySelector("[data-history-confirm]");
|
||||
var confirmCancelBtns = document.querySelectorAll(
|
||||
"[data-history-confirm-cancel]"
|
||||
);
|
||||
var confirmOkBtn = document.querySelector("[data-history-confirm-ok]");
|
||||
var clearing = false;
|
||||
|
||||
function toast(message, type) {
|
||||
if (window.AuthAPI && typeof window.AuthAPI.toast === "function") {
|
||||
window.AuthAPI.toast(message, type);
|
||||
}
|
||||
}
|
||||
|
||||
function openClearConfirm() {
|
||||
if (!confirmMask) return;
|
||||
confirmMask.hidden = false;
|
||||
confirmMask.removeAttribute("hidden");
|
||||
confirmMask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
function closeClearConfirm() {
|
||||
if (!confirmMask) return;
|
||||
confirmMask.classList.remove("is-open");
|
||||
confirmMask.hidden = true;
|
||||
confirmMask.setAttribute("hidden", "");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
if (clearing) return;
|
||||
if (!window.HttpRequest) {
|
||||
toast("清空失败,请稍后重试", "error");
|
||||
return;
|
||||
}
|
||||
clearing = true;
|
||||
window.HttpRequest.post("/api/playlet/history/clear")
|
||||
.then(function (res) {
|
||||
if (!res || res.code !== 200) {
|
||||
throw new Error((res && (res.tip || res.msg)) || "清空失败");
|
||||
}
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(function (err) {
|
||||
clearing = false;
|
||||
toast((err && err.message) || "清空失败,请稍后重试", "error");
|
||||
});
|
||||
}
|
||||
|
||||
clearBtns.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
openClearConfirm();
|
||||
});
|
||||
});
|
||||
|
||||
confirmCancelBtns.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
closeClearConfirm();
|
||||
});
|
||||
});
|
||||
|
||||
if (confirmMask) {
|
||||
confirmMask.addEventListener("click", function (e) {
|
||||
if (e.target === confirmMask) closeClearConfirm();
|
||||
});
|
||||
}
|
||||
|
||||
if (confirmOkBtn) {
|
||||
confirmOkBtn.addEventListener("click", function () {
|
||||
closeClearConfirm();
|
||||
clearHistory();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key !== "Escape") return;
|
||||
if (!confirmMask || !confirmMask.classList.contains("is-open")) return;
|
||||
closeClearConfirm();
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function fallbackCopy(value) {
|
||||
var input = document.createElement("textarea");
|
||||
input.value = value;
|
||||
input.setAttribute("readonly", "");
|
||||
input.style.position = "fixed";
|
||||
input.style.opacity = "0";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
var copied = document.execCommand("copy");
|
||||
input.remove();
|
||||
return copied;
|
||||
}
|
||||
|
||||
function copyValue(value) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(value);
|
||||
}
|
||||
return fallbackCopy(value) ? Promise.resolve() : Promise.reject(new Error("copy failed"));
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var button = event.target.closest("[data-copy-value]");
|
||||
if (!button) return;
|
||||
var value = button.getAttribute("data-copy-value") || window.location.href;
|
||||
var original = button.textContent;
|
||||
copyValue(value).then(function () {
|
||||
button.textContent = "已复制";
|
||||
window.setTimeout(function () { button.textContent = original; }, 1600);
|
||||
}).catch(function () {
|
||||
button.textContent = "复制失败";
|
||||
window.setTimeout(function () { button.textContent = original; }, 1600);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,255 @@
|
||||
(function () {
|
||||
var H5_MQ = "(max-width: 767px)";
|
||||
var isH5 = !!(window.matchMedia && window.matchMedia(H5_MQ).matches);
|
||||
|
||||
var announcementTrack = document.querySelector(".home-announcement-marquee");
|
||||
if (announcementTrack) {
|
||||
var announcementCopies = announcementTrack.querySelectorAll(".home-announcement-copy");
|
||||
var announcementViewport = announcementTrack.parentElement;
|
||||
if (announcementCopies.length === 2 && announcementViewport) {
|
||||
var primaryAnnouncement = announcementCopies[0];
|
||||
var duplicateAnnouncement = announcementCopies[1];
|
||||
var announcementSourceNodes = Array.prototype.map.call(
|
||||
primaryAnnouncement.childNodes,
|
||||
function (node) {
|
||||
return node.cloneNode(true);
|
||||
}
|
||||
);
|
||||
|
||||
function fillAnnouncementTrack() {
|
||||
var rounds = 0;
|
||||
while (
|
||||
primaryAnnouncement.getBoundingClientRect().width <
|
||||
announcementViewport.clientWidth + 80 &&
|
||||
rounds < 20
|
||||
) {
|
||||
var repeated = document.createElement("span");
|
||||
repeated.setAttribute("aria-hidden", "true");
|
||||
repeated.className = "home-announcement-repeat";
|
||||
|
||||
var separator = document.createElement("span");
|
||||
separator.className = "home-announcement-separator";
|
||||
separator.textContent = " · ";
|
||||
repeated.appendChild(separator);
|
||||
|
||||
announcementSourceNodes.forEach(function (node) {
|
||||
repeated.appendChild(node.cloneNode(true));
|
||||
});
|
||||
primaryAnnouncement.appendChild(repeated);
|
||||
rounds += 1;
|
||||
}
|
||||
duplicateAnnouncement.innerHTML = primaryAnnouncement.innerHTML;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(fillAnnouncementTrack);
|
||||
if ("ResizeObserver" in window) {
|
||||
new ResizeObserver(fillAnnouncementTrack).observe(announcementViewport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// H5:首屏只抢 LCP 主图,海报交给 IntersectionObserver,避免弱网争抢带宽
|
||||
if (isH5) {
|
||||
document.querySelectorAll(".poster-cover-img[data-lazy-eager]").forEach(function (img) {
|
||||
img.removeAttribute("data-lazy-eager");
|
||||
});
|
||||
}
|
||||
|
||||
function loadHeroSlideImages(slide) {
|
||||
if (!slide || typeof loadLazyNode !== "function") return;
|
||||
var imgs = slide.querySelectorAll(".hero-image.lazy[data-src]:not([data-decrypted])");
|
||||
Array.prototype.forEach.call(imgs, function (img) {
|
||||
loadLazyNode(img);
|
||||
});
|
||||
}
|
||||
|
||||
var firstHeroImg = document.querySelector(
|
||||
".hero-card .hero-image.lazy[data-src]:not([data-decrypted])"
|
||||
);
|
||||
if (firstHeroImg && typeof loadLazyNode === "function") {
|
||||
loadLazyNode(firstHeroImg);
|
||||
} else if (firstHeroImg && typeof processImage === "function") {
|
||||
var src = firstHeroImg.getAttribute("data-src");
|
||||
if (src) {
|
||||
firstHeroImg.setAttribute("data-decrypted", "1");
|
||||
processImage(src, firstHeroImg);
|
||||
}
|
||||
}
|
||||
|
||||
var mainEl = document.getElementById("heroSwiper");
|
||||
var thumbsEl = document.getElementById("heroThumbs");
|
||||
if (!mainEl) return;
|
||||
|
||||
var slideCount = mainEl.querySelectorAll(".swiper-slide").length;
|
||||
if (slideCount === 0) return;
|
||||
|
||||
var swiperStarted = false;
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (typeof Swiper !== "undefined") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
if (existing) {
|
||||
existing.addEventListener("load", function () {
|
||||
resolve();
|
||||
});
|
||||
existing.addEventListener("error", reject);
|
||||
return;
|
||||
}
|
||||
var s = document.createElement("script");
|
||||
s.src = src;
|
||||
s.async = true;
|
||||
s.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
function loadStylesheet(href) {
|
||||
return new Promise(function (resolve) {
|
||||
if (document.querySelector('link[href="' + href + '"][data-swiper-css]')) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
var link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.setAttribute("data-swiper-css", "1");
|
||||
link.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
link.onerror = function () {
|
||||
resolve();
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureSwiperAssets() {
|
||||
var cssHref = "/static/common/swiper-bundle.min.css";
|
||||
var jsHref = "/static/common/swiper-bundle.min.js";
|
||||
// PC 已通过 media=(min-width:768px) 预加载 CSS;H5 在此按需注入
|
||||
var tasks = [loadScript(jsHref)];
|
||||
if (isH5) {
|
||||
tasks.unshift(loadStylesheet(cssHref));
|
||||
}
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
function initSwiper() {
|
||||
if (swiperStarted || typeof Swiper === "undefined") return;
|
||||
swiperStarted = true;
|
||||
mainEl.classList.remove("hero-swiper--h5-pending");
|
||||
|
||||
var thumbButtons = thumbsEl
|
||||
? Array.prototype.slice.call(thumbsEl.querySelectorAll(".hero-thumb"))
|
||||
: [];
|
||||
|
||||
function setActiveThumb(index) {
|
||||
thumbButtons.forEach(function (btn, i) {
|
||||
btn.classList.toggle("is-active", i === index);
|
||||
});
|
||||
}
|
||||
|
||||
var enableLoop = slideCount > 1;
|
||||
|
||||
var mainSwiper = new Swiper(mainEl, {
|
||||
loop: enableLoop,
|
||||
effect: "fade",
|
||||
fadeEffect: {
|
||||
crossFade: true,
|
||||
},
|
||||
speed: isH5 ? 650 : 900,
|
||||
autoplay: enableLoop
|
||||
? {
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: !isH5,
|
||||
waitForTransition: true,
|
||||
}
|
||||
: false,
|
||||
pagination: {
|
||||
el: mainEl.querySelector(".hero-dots"),
|
||||
clickable: true,
|
||||
bulletClass: "hero-dot",
|
||||
bulletActiveClass: "hero-dot--active",
|
||||
},
|
||||
on: {
|
||||
init: function () {
|
||||
loadHeroSlideImages(this.slides[this.activeIndex]);
|
||||
},
|
||||
slideChange: function () {
|
||||
setActiveThumb(this.realIndex);
|
||||
loadHeroSlideImages(this.slides[this.activeIndex]);
|
||||
if (isH5) return;
|
||||
var next = this.slides[this.activeIndex + 1];
|
||||
var prev = this.slides[this.activeIndex - 1];
|
||||
if (next) loadHeroSlideImages(next);
|
||||
if (prev) loadHeroSlideImages(prev);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var previousButton = mainEl.querySelector(".hero-thumb-prev");
|
||||
var nextButton = mainEl.querySelector(".hero-thumb-next");
|
||||
if (previousButton) {
|
||||
previousButton.addEventListener("click", function () {
|
||||
mainSwiper.slidePrev();
|
||||
});
|
||||
}
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener("click", function () {
|
||||
mainSwiper.slideNext();
|
||||
});
|
||||
}
|
||||
|
||||
thumbButtons.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var index = Number(btn.getAttribute("data-index"));
|
||||
if (Number.isNaN(index)) return;
|
||||
if (enableLoop) {
|
||||
mainSwiper.slideToLoop(index);
|
||||
} else {
|
||||
mainSwiper.slideTo(index);
|
||||
}
|
||||
setActiveThumb(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startSwiper() {
|
||||
if (swiperStarted) return;
|
||||
ensureSwiperAssets()
|
||||
.then(initSwiper)
|
||||
.catch(function () {
|
||||
mainEl.classList.remove("hero-swiper--h5-pending");
|
||||
});
|
||||
}
|
||||
|
||||
if (isH5) {
|
||||
mainEl.classList.add("hero-swiper--h5-pending");
|
||||
|
||||
function scheduleSwiper() {
|
||||
startSwiper();
|
||||
}
|
||||
|
||||
// 用户滑动/点击 banner 时立即初始化;否则空闲后再加载,避开 LCP 争抢
|
||||
mainEl.addEventListener("touchstart", scheduleSwiper, { once: true, passive: true });
|
||||
mainEl.addEventListener("click", scheduleSwiper, { once: true });
|
||||
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(scheduleSwiper, { timeout: 3500 });
|
||||
} else {
|
||||
setTimeout(scheduleSwiper, 2200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// PC:立即加载并初始化 Swiper
|
||||
startSwiper();
|
||||
})();
|
||||
@@ -0,0 +1,236 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("loginDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var form = mask.querySelector("[data-login-dialog-form]");
|
||||
var accountInput = mask.querySelector("[data-login-account]");
|
||||
var passwordInput = mask.querySelector("[data-login-password]");
|
||||
var submitBtn = mask.querySelector("[data-login-submit]");
|
||||
var defaultSubmitText = (submitBtn && submitBtn.textContent) || "登录";
|
||||
|
||||
function setSubmitting(loading) {
|
||||
if (!submitBtn) return;
|
||||
submitBtn.disabled = !!loading;
|
||||
submitBtn.classList.toggle("is-loading", !!loading);
|
||||
submitBtn.setAttribute("aria-busy", loading ? "true" : "false");
|
||||
submitBtn.textContent = loading ? "登录中..." : defaultSubmitText;
|
||||
}
|
||||
|
||||
function getField(name) {
|
||||
return mask.querySelector('[data-login-field="' + name + '"]');
|
||||
}
|
||||
|
||||
function setFieldError(name, message) {
|
||||
var field = getField(name);
|
||||
if (!field) return;
|
||||
var control = field.querySelector("[data-login-control]");
|
||||
var error = field.querySelector("[data-login-error]");
|
||||
if (control) control.classList.add("is-error");
|
||||
if (error) {
|
||||
error.textContent = message;
|
||||
error.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFieldError(name) {
|
||||
var field = getField(name);
|
||||
if (!field) return;
|
||||
var control = field.querySelector("[data-login-control]");
|
||||
var error = field.querySelector("[data-login-error]");
|
||||
if (control) control.classList.remove("is-error");
|
||||
if (error) error.hidden = true;
|
||||
}
|
||||
|
||||
function clearAllErrors() {
|
||||
clearFieldError("account");
|
||||
clearFieldError("password");
|
||||
}
|
||||
|
||||
function isValidLength(value) {
|
||||
return value.length >= 4 && value.length <= 16;
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
var account = (accountInput && accountInput.value.trim()) || "";
|
||||
var password = (passwordInput && passwordInput.value) || "";
|
||||
var valid = true;
|
||||
|
||||
clearAllErrors();
|
||||
|
||||
if (!account) {
|
||||
setFieldError("account", "账号格式不正确,请输入有效账号");
|
||||
valid = false;
|
||||
} else if (!window.AuthAPI || !AuthAPI.isValidLoginAccount(account)) {
|
||||
setFieldError("account", "账号格式不正确,请输入有效账号");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setFieldError("password", "密码错误,请检查后重新输入");
|
||||
valid = false;
|
||||
} else if (!isValidLength(password)) {
|
||||
setFieldError("password", "密码错误,请检查后重新输入");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) return null;
|
||||
return { account: account, password: password };
|
||||
}
|
||||
|
||||
function mapServerError(err) {
|
||||
var tip = (err && err.message) || "登录失败,请稍后重试";
|
||||
if (/密码|password/i.test(tip)) {
|
||||
setFieldError("password", tip);
|
||||
return;
|
||||
}
|
||||
if (/账号|账户|用户|邮箱|account|email|不存在|错误/i.test(tip)) {
|
||||
setFieldError("account", tip);
|
||||
return;
|
||||
}
|
||||
if (window.AuthAPI) AuthAPI.toast(tip, "error");
|
||||
else if (typeof showToast === "function") showToast(tip, "error");
|
||||
}
|
||||
|
||||
function openDialog(event) {
|
||||
if (event) event.preventDefault();
|
||||
if (typeof window.closeRegisterDialog === "function") {
|
||||
window.closeRegisterDialog();
|
||||
}
|
||||
clearAllErrors();
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
return true;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
clearAllErrors();
|
||||
if (!document.getElementById("registerDialog") || !document.getElementById("registerDialog").classList.contains("is-open")) {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginErrors(errors) {
|
||||
clearAllErrors();
|
||||
if (!errors) return;
|
||||
if (errors.account) setFieldError("account", errors.account);
|
||||
if (errors.password) setFieldError("password", errors.password);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
|
||||
var payload = validateForm();
|
||||
if (!payload) return;
|
||||
if (!window.AuthAPI || !window.HttpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
AuthAPI.login(payload)
|
||||
.then(function () {
|
||||
AuthAPI.toast("登录成功", "success");
|
||||
closeDialog();
|
||||
setTimeout(function () {
|
||||
goAfterAuth();
|
||||
}, 400);
|
||||
})
|
||||
.catch(function (err) {
|
||||
setSubmitting(false);
|
||||
mapServerError(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isSafeRedirect(path) {
|
||||
return typeof path === "string" && path.charAt(0) === "/" && path.charAt(1) !== "/";
|
||||
}
|
||||
|
||||
function goAfterAuth() {
|
||||
var bodyRedirect = document.body && document.body.getAttribute("data-auth-redirect");
|
||||
if (isSafeRedirect(bodyRedirect)) {
|
||||
window.location.href = bodyRedirect;
|
||||
return;
|
||||
}
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var redirect = params.get("redirect");
|
||||
if (isSafeRedirect(redirect)) {
|
||||
window.location.href = redirect;
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function consumeAuthQuery(kind) {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get("auth") !== kind) return false;
|
||||
params.delete("auth");
|
||||
var qs = params.toString();
|
||||
var next = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
|
||||
window.history.replaceState(null, "", next);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (accountInput) {
|
||||
accountInput.addEventListener("input", function () {
|
||||
clearFieldError("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener("input", function () {
|
||||
clearFieldError("password");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var openEl = event.target.closest("[data-login-dialog-open]");
|
||||
if (openEl) {
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
var loginLink = event.target.closest('a[href="/login/"], a[href="#login"]');
|
||||
if (loginLink) {
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-login-dialog-close]")) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-login-dialog-eye]")) {
|
||||
var btn = event.target.closest("[data-login-dialog-eye]");
|
||||
var input = document.getElementById("loginDialogPassword");
|
||||
var img = btn.querySelector("img");
|
||||
if (!input || !img) return;
|
||||
var show = input.type === "password";
|
||||
input.type = show ? "text" : "password";
|
||||
img.src = show ? "/static/images/eye_open.svg" : "/static/images/eye_close.svg";
|
||||
btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === mask) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
window.openLoginDialog = openDialog;
|
||||
window.closeLoginDialog = closeDialog;
|
||||
window.setLoginDialogErrors = setLoginErrors;
|
||||
|
||||
if ((document.body && document.body.getAttribute("data-auth-mode") === "login") || consumeAuthQuery("login")) {
|
||||
openDialog();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,230 @@
|
||||
(function () {
|
||||
var panel = document.querySelector("[data-mobile-search-landing]");
|
||||
var mobileMedia = window.matchMedia && window.matchMedia("(max-width: 767px)");
|
||||
if (!panel || !mobileMedia || !mobileMedia.matches) return;
|
||||
|
||||
var PLACEHOLDER = "/static/images/placeholder.png?v=2";
|
||||
var historySection = panel.querySelector("[data-mobile-search-history]");
|
||||
var historyList = panel.querySelector("[data-mobile-search-history-list]");
|
||||
var clearButton = panel.querySelector("[data-mobile-search-history-clear]");
|
||||
var hotSection = panel.querySelector("[data-mobile-search-hot]");
|
||||
var hotList = panel.querySelector("[data-mobile-search-hot-list]");
|
||||
var guessSection = panel.querySelector("[data-mobile-search-guess]");
|
||||
var guessList = panel.querySelector("[data-mobile-search-guess-list]");
|
||||
var changeButton = panel.querySelector("[data-mobile-search-guess-change]");
|
||||
var historyDivider = panel.querySelector('[data-mobile-search-divider="history"]');
|
||||
var hotDivider = panel.querySelector('[data-mobile-search-divider="hot"]');
|
||||
var hotLikeData = {};
|
||||
var guessOffset = 0;
|
||||
|
||||
function searchKeyword(keyword) {
|
||||
keyword = String(keyword || "").trim();
|
||||
if (!keyword) return;
|
||||
if (window.SearchHistory && typeof window.SearchHistory.add === "function") {
|
||||
window.SearchHistory.add(keyword);
|
||||
}
|
||||
window.location.href = "/search/" + encodeURIComponent(keyword) + "/";
|
||||
}
|
||||
|
||||
function syncDividers() {
|
||||
var hasHistory = historySection && !historySection.hidden;
|
||||
var hasHot = hotSection && !hotSection.hidden;
|
||||
var hasGuess = guessSection && !guessSection.hidden;
|
||||
if (historyDivider) historyDivider.hidden = !(hasHistory && (hasHot || hasGuess));
|
||||
if (hotDivider) hotDivider.hidden = !(hasHot && hasGuess);
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!historyList || !historySection) return;
|
||||
var history = window.SearchHistory && typeof window.SearchHistory.get === "function"
|
||||
? window.SearchHistory.get()
|
||||
: [];
|
||||
historyList.textContent = "";
|
||||
historySection.hidden = history.length === 0;
|
||||
history.forEach(function (keyword) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "mobile-search-history-tag";
|
||||
button.textContent = keyword;
|
||||
button.addEventListener("click", function () {
|
||||
searchKeyword(keyword);
|
||||
});
|
||||
historyList.appendChild(button);
|
||||
});
|
||||
syncDividers();
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
var count = Math.max(0, Number(value) || 0);
|
||||
if (count >= 10000) {
|
||||
return (count / 10000).toFixed(1).replace(/\.0$/, "") + "w";
|
||||
}
|
||||
return String(count);
|
||||
}
|
||||
|
||||
function renderHotSearch() {
|
||||
if (!hotList || !hotSection) return;
|
||||
var source = Array.isArray(hotLikeData.hotSearch) ? hotLikeData.hotSearch : [];
|
||||
var items = source.filter(function (item) {
|
||||
return item && String(item.keyword || "").trim();
|
||||
}).slice(0, 6);
|
||||
hotList.textContent = "";
|
||||
hotSection.hidden = items.length === 0;
|
||||
items.forEach(function (item, index) {
|
||||
var button = document.createElement("button");
|
||||
var rank = document.createElement("span");
|
||||
var keyword = document.createElement("span");
|
||||
var count = document.createElement("span");
|
||||
var keywordText = String(item.keyword || "").trim();
|
||||
|
||||
button.type = "button";
|
||||
button.className = "mobile-search-hot-item";
|
||||
rank.className = "mobile-search-hot-rank";
|
||||
keyword.className = "mobile-search-hot-keyword";
|
||||
count.className = "mobile-search-hot-count";
|
||||
rank.textContent = String(index + 1);
|
||||
keyword.textContent = keywordText;
|
||||
count.textContent = formatCount(item.count);
|
||||
button.appendChild(rank);
|
||||
button.appendChild(keyword);
|
||||
button.appendChild(count);
|
||||
button.addEventListener("click", function () {
|
||||
searchKeyword(keywordText);
|
||||
});
|
||||
hotList.appendChild(button);
|
||||
});
|
||||
syncDividers();
|
||||
}
|
||||
|
||||
function detailHref(item) {
|
||||
var slug = String((item && item.slug) || "").trim();
|
||||
var id = Number(item && item.id);
|
||||
var key = slug || (id > 0 ? String(id) : "");
|
||||
return key ? "/detail/" + encodeURIComponent(key) + "/" : "#";
|
||||
}
|
||||
|
||||
function firstCardTag(item) {
|
||||
var tags = item && Array.isArray(item.tagNames) ? item.tagNames : [];
|
||||
var themes = item && Array.isArray(item.themeNames) ? item.themeNames : [];
|
||||
return String(tags[0] || themes[0] || "热门").trim();
|
||||
}
|
||||
|
||||
function setCardCover(image, rawURL, title) {
|
||||
var cover = String(rawURL || "").trim();
|
||||
image.className = "mobile-search-guess-image";
|
||||
image.alt = title;
|
||||
image.title = title;
|
||||
image.addEventListener("error", function () {
|
||||
image.classList.remove("lazy");
|
||||
image.removeAttribute("data-src");
|
||||
image.src = PLACEHOLDER;
|
||||
}, { once: true });
|
||||
if (cover && typeof window.shouldDecryptImage === "function" && window.shouldDecryptImage(cover)) {
|
||||
image.classList.add("lazy");
|
||||
image.src = PLACEHOLDER;
|
||||
image.setAttribute("data-src", cover);
|
||||
return;
|
||||
}
|
||||
image.src = cover || PLACEHOLDER;
|
||||
}
|
||||
|
||||
function createGuessCard(item) {
|
||||
var card = document.createElement("a");
|
||||
var cover = document.createElement("div");
|
||||
var image = document.createElement("img");
|
||||
var episode = document.createElement("span");
|
||||
var copy = document.createElement("div");
|
||||
var title = document.createElement("div");
|
||||
var desc = document.createElement("div");
|
||||
var tagRow = document.createElement("div");
|
||||
var tag = document.createElement("span");
|
||||
var titleText = String((item && item.title) || "精彩短剧");
|
||||
var totalEpisode = Math.max(0, Number(item && item.totalEpisode) || 0);
|
||||
|
||||
card.className = "mobile-search-guess-card";
|
||||
card.href = detailHref(item);
|
||||
card.setAttribute("aria-label", titleText);
|
||||
cover.className = "mobile-search-guess-cover";
|
||||
episode.className = "mobile-search-guess-episode";
|
||||
copy.className = "mobile-search-guess-copy";
|
||||
title.className = "mobile-search-guess-title";
|
||||
desc.className = "mobile-search-guess-desc";
|
||||
tagRow.className = "mobile-search-guess-tag-row";
|
||||
tag.className = "mobile-search-guess-tag";
|
||||
|
||||
setCardCover(image, (item && (item.cover || item.banner)) || "", titleText);
|
||||
episode.textContent = totalEpisode ? totalEpisode + "集" : "更新中";
|
||||
title.textContent = titleText;
|
||||
desc.textContent = String((item && item.desc) || "精彩内容等你来看");
|
||||
tag.textContent = firstCardTag(item);
|
||||
|
||||
cover.appendChild(image);
|
||||
cover.appendChild(episode);
|
||||
tagRow.appendChild(tag);
|
||||
copy.appendChild(title);
|
||||
copy.appendChild(desc);
|
||||
copy.appendChild(tagRow);
|
||||
card.appendChild(cover);
|
||||
card.appendChild(copy);
|
||||
return card;
|
||||
}
|
||||
|
||||
function guessItems() {
|
||||
var source = Array.isArray(hotLikeData.guessLike) ? hotLikeData.guessLike : [];
|
||||
return source.filter(function (item) {
|
||||
return item && (item.id || item.slug);
|
||||
});
|
||||
}
|
||||
|
||||
function renderGuessLike() {
|
||||
if (!guessList || !guessSection) return;
|
||||
var items = guessItems();
|
||||
var count = Math.min(4, items.length);
|
||||
guessList.textContent = "";
|
||||
guessSection.hidden = count === 0;
|
||||
for (var index = 0; index < count; index += 1) {
|
||||
guessList.appendChild(createGuessCard(items[(guessOffset + index) % items.length]));
|
||||
}
|
||||
if (count && typeof window.initLazyImages === "function") {
|
||||
window.initLazyImages(guessList);
|
||||
}
|
||||
syncDividers();
|
||||
}
|
||||
|
||||
function renderHotLike() {
|
||||
renderHotSearch();
|
||||
renderGuessLike();
|
||||
}
|
||||
|
||||
renderHistory();
|
||||
renderHotLike();
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function () {
|
||||
if (window.SearchHistory && typeof window.SearchHistory.clear === "function") {
|
||||
window.SearchHistory.clear();
|
||||
}
|
||||
renderHistory();
|
||||
});
|
||||
}
|
||||
|
||||
if (changeButton) {
|
||||
changeButton.addEventListener("click", function () {
|
||||
var items = guessItems();
|
||||
if (items.length > 4) guessOffset = (guessOffset + 4) % items.length;
|
||||
renderGuessLike();
|
||||
});
|
||||
}
|
||||
|
||||
if (window.HttpRequest && typeof window.HttpRequest.get === "function") {
|
||||
window.HttpRequest.get("/api/playlet/hot-like")
|
||||
.then(function (response) {
|
||||
hotLikeData = response && response.data ? response.data : {};
|
||||
renderHotLike();
|
||||
})
|
||||
.catch(function () {
|
||||
hotLikeData = {};
|
||||
renderHotLike();
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,43 @@
|
||||
(function () {
|
||||
if (window.__paginationJumpBound) {
|
||||
return;
|
||||
}
|
||||
window.__paginationJumpBound = true;
|
||||
|
||||
function clampPage(value, total) {
|
||||
var page = parseInt(value, 10);
|
||||
if (!page || page < 1) {
|
||||
return 0;
|
||||
}
|
||||
if (total > 0 && page > total) {
|
||||
return total;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
function buildPageURL(page) {
|
||||
var url = new URL(window.location.href);
|
||||
url.pathname = url.pathname.replace(/\/page\/\d+\/?$/, "").replace(/\/$/, "");
|
||||
url.searchParams.delete("page");
|
||||
if (page > 1) url.pathname += "/page/" + page;
|
||||
return url.pathname + url.search + url.hash;
|
||||
}
|
||||
|
||||
document.addEventListener("submit", function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.matches || !form.matches("[data-pagination-jump]")) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
var nav = form.closest("[data-pagination]");
|
||||
var input = form.querySelector(".pagination-jump-input");
|
||||
var total = nav ? parseInt(nav.getAttribute("data-total"), 10) : 0;
|
||||
var current = nav ? parseInt(nav.getAttribute("data-page"), 10) : 0;
|
||||
var page = clampPage(input && input.value, total);
|
||||
if (!page || page === current) {
|
||||
return;
|
||||
}
|
||||
window.location.href = buildPageURL(page);
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,213 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("registerDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var form = mask.querySelector("[data-register-dialog-form]");
|
||||
var accountInput = mask.querySelector("[data-register-account]");
|
||||
var passwordInput = mask.querySelector("[data-register-password]");
|
||||
var confirmInput = mask.querySelector("[data-register-password-confirm]");
|
||||
var submitBtn = mask.querySelector("[data-register-submit]");
|
||||
var defaultSubmitText = (submitBtn && submitBtn.textContent) || "立即注册";
|
||||
|
||||
function setSubmitting(loading) {
|
||||
if (!submitBtn) return;
|
||||
submitBtn.disabled = !!loading;
|
||||
submitBtn.classList.toggle("is-loading", !!loading);
|
||||
submitBtn.setAttribute("aria-busy", loading ? "true" : "false");
|
||||
submitBtn.textContent = loading ? "注册中..." : defaultSubmitText;
|
||||
}
|
||||
|
||||
function getField(name) {
|
||||
return mask.querySelector('[data-register-field="' + name + '"]');
|
||||
}
|
||||
|
||||
function setFieldError(name, message) {
|
||||
var field = getField(name);
|
||||
if (!field) return;
|
||||
var control = field.querySelector("[data-register-control]");
|
||||
var error = field.querySelector("[data-register-error]");
|
||||
if (control) control.classList.add("is-error");
|
||||
if (error) {
|
||||
error.textContent = message;
|
||||
error.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFieldError(name) {
|
||||
var field = getField(name);
|
||||
if (!field) return;
|
||||
var control = field.querySelector("[data-register-control]");
|
||||
var error = field.querySelector("[data-register-error]");
|
||||
if (control) control.classList.remove("is-error");
|
||||
if (error) error.hidden = true;
|
||||
}
|
||||
|
||||
function clearAllErrors() {
|
||||
clearFieldError("account");
|
||||
clearFieldError("password");
|
||||
clearFieldError("password_confirm");
|
||||
}
|
||||
|
||||
function isValidLength(value) {
|
||||
return value.length >= 4 && value.length <= 16;
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
var account = (accountInput && accountInput.value.trim()) || "";
|
||||
var password = (passwordInput && passwordInput.value) || "";
|
||||
var confirm = (confirmInput && confirmInput.value) || "";
|
||||
var valid = true;
|
||||
|
||||
clearAllErrors();
|
||||
|
||||
if (!account) {
|
||||
setFieldError("account", "请输入账号");
|
||||
valid = false;
|
||||
} else if (!isValidLength(account)) {
|
||||
setFieldError("account", "账号需为 4–16 个字符");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setFieldError("password", "请输入密码");
|
||||
valid = false;
|
||||
} else if (!isValidLength(password)) {
|
||||
setFieldError("password", "密码需为 4–16 个字符");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
setFieldError("password_confirm", "请再次输入密码");
|
||||
valid = false;
|
||||
} else if (confirm !== password) {
|
||||
setFieldError("password_confirm", "两次输入的密码不一致");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) return null;
|
||||
return { accountNo: account, password: password };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (window.AuthAPI) AuthAPI.toast(tip, "error");
|
||||
else if (typeof showToast === "function") showToast(tip, "error");
|
||||
}
|
||||
|
||||
function openDialog(event) {
|
||||
if (event) event.preventDefault();
|
||||
if (typeof window.closeLoginDialog === "function") {
|
||||
window.closeLoginDialog();
|
||||
}
|
||||
clearAllErrors();
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
return true;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
clearAllErrors();
|
||||
if (!document.getElementById("loginDialog") || !document.getElementById("loginDialog").classList.contains("is-open")) {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
if (submitBtn && submitBtn.classList.contains("is-loading")) return;
|
||||
var payload = validateForm();
|
||||
if (!payload) return;
|
||||
if (!window.AuthAPI || !window.HttpRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
AuthAPI.register(payload)
|
||||
.then(function () {
|
||||
AuthAPI.toast("注册成功", "success");
|
||||
closeDialog();
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 400);
|
||||
})
|
||||
.catch(function (err) {
|
||||
setSubmitting(false);
|
||||
mapServerError(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function consumeAuthQuery(kind) {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get("auth") !== kind) return false;
|
||||
params.delete("auth");
|
||||
var qs = params.toString();
|
||||
var next = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
|
||||
window.history.replaceState(null, "", next);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (accountInput) {
|
||||
accountInput.addEventListener("input", function () {
|
||||
clearFieldError("account");
|
||||
});
|
||||
}
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener("input", function () {
|
||||
clearFieldError("password");
|
||||
});
|
||||
}
|
||||
if (confirmInput) {
|
||||
confirmInput.addEventListener("input", function () {
|
||||
clearFieldError("password_confirm");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var openEl = event.target.closest("[data-register-dialog-open]");
|
||||
if (openEl) {
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
var registerLink = event.target.closest('a[href="/register/"], a[href="#register"]');
|
||||
if (registerLink) {
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-register-dialog-close]")) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === mask) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
window.openRegisterDialog = openDialog;
|
||||
window.closeRegisterDialog = closeDialog;
|
||||
|
||||
if ((document.body && document.body.getAttribute("data-auth-mode") === "register") || consumeAuthQuery("register")) {
|
||||
openDialog();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,89 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("searchDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var headerForm = document.querySelector("form.header-search");
|
||||
var headerInput = headerForm
|
||||
? headerForm.querySelector('input[name="q"]')
|
||||
: null;
|
||||
|
||||
var input = mask.querySelector('[data-search-dialog-input]');
|
||||
var form = mask.querySelector('[data-search-dialog-form]');
|
||||
|
||||
function isMobile() {
|
||||
try {
|
||||
return window.matchMedia("(max-width: 767px)").matches;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(initialValue) {
|
||||
if (initialValue != null && input) {
|
||||
input.value = String(initialValue || "");
|
||||
}
|
||||
mask.classList.add("is-open");
|
||||
mask.setAttribute("aria-hidden", "false");
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
if (input) input.focus();
|
||||
if (input && typeof input.select === "function") {
|
||||
input.select();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
mask.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
window.openSearchDialog = openDialog;
|
||||
window.closeSearchDialog = closeDialog;
|
||||
|
||||
if (mask) {
|
||||
mask.addEventListener("click", function (e) {
|
||||
if (e.target !== mask) return;
|
||||
closeDialog();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (!mask.classList.contains("is-open")) return;
|
||||
if (e.key === "Escape") closeDialog();
|
||||
});
|
||||
|
||||
// 移动端:点击头部搜索栏 → 打开搜索弹窗,而不是直接跳转
|
||||
if (headerForm) {
|
||||
headerForm.addEventListener("submit", function (e) {
|
||||
if (!isMobile()) return;
|
||||
e.preventDefault();
|
||||
var v = headerInput ? headerInput.value : "";
|
||||
openDialog(v);
|
||||
});
|
||||
|
||||
headerForm.addEventListener("click", function (e) {
|
||||
if (!isMobile()) return;
|
||||
// 避免点到表单外或触发非预期行为
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var v = headerInput ? headerInput.value : "";
|
||||
openDialog(v);
|
||||
});
|
||||
}
|
||||
|
||||
// 提交时如果是空关键字,不跳转(避免触发接口报参数错误等情况)
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
var q = input ? String(input.value || "").trim() : "";
|
||||
if (!q) {
|
||||
e.preventDefault();
|
||||
if (input) input.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
(function () {
|
||||
document.querySelectorAll(".setting-switch").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var on = btn.classList.toggle("setting-switch--on");
|
||||
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
});
|
||||
});
|
||||
|
||||
var logoutBtn = document.querySelector("[data-auth-logout]");
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener("click", function () {
|
||||
function done() {
|
||||
if (window.AuthAPI && typeof AuthAPI.markGuestLogout === "function") {
|
||||
AuthAPI.markGuestLogout();
|
||||
}
|
||||
if (window.CookieUtil && typeof CookieUtil.clearAuth === "function") {
|
||||
CookieUtil.clearAuth();
|
||||
}
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
} catch (e) {}
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
if (window.HttpRequest) {
|
||||
HttpRequest.post("/api/auth/logout", {})
|
||||
.catch(function () {})
|
||||
.then(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,76 @@
|
||||
(function () {
|
||||
var host = document.getElementById("toastHost");
|
||||
if (!host) return;
|
||||
|
||||
var ICONS = {
|
||||
info:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.5"/><path d="M8 7.2V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="5" r="0.9" fill="currentColor"/></svg>',
|
||||
success:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3.5 8.2l3 3.1 6-6.3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
error:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M5 5l6 6M11 5l-6 6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||
warning:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 4.2v5.2" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><circle cx="8" cy="11.4" r="0.95" fill="currentColor"/></svg>',
|
||||
};
|
||||
|
||||
var DEFAULT_DURATION = 2400;
|
||||
var MAX_VISIBLE = 3;
|
||||
|
||||
function normalizeType(type) {
|
||||
if (type === "success" || type === "error" || type === "warning" || type === "info") {
|
||||
return type;
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function showToast(message, options) {
|
||||
if (!message) return null;
|
||||
|
||||
var opts = typeof options === "string" ? { type: options } : options || {};
|
||||
var type = normalizeType(opts.type);
|
||||
var duration = typeof opts.duration === "number" ? opts.duration : DEFAULT_DURATION;
|
||||
|
||||
while (host.children.length >= MAX_VISIBLE) {
|
||||
var oldest = host.firstElementChild;
|
||||
if (oldest) oldest.remove();
|
||||
}
|
||||
|
||||
var item = document.createElement("div");
|
||||
item.className = "toast-item toast-item--" + type;
|
||||
item.setAttribute("role", "status");
|
||||
|
||||
var icon = document.createElement("span");
|
||||
icon.className = "toast-icon";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.innerHTML = ICONS[type] || ICONS.info;
|
||||
|
||||
var text = document.createElement("span");
|
||||
text.className = "toast-text";
|
||||
text.textContent = String(message);
|
||||
|
||||
item.appendChild(icon);
|
||||
item.appendChild(text);
|
||||
host.appendChild(item);
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
item.classList.add("is-visible");
|
||||
});
|
||||
|
||||
var hideTimer = setTimeout(function () {
|
||||
item.classList.add("is-leaving");
|
||||
item.classList.remove("is-visible");
|
||||
setTimeout(function () {
|
||||
if (item.parentNode) item.parentNode.removeChild(item);
|
||||
}, 260);
|
||||
}, Math.max(800, duration));
|
||||
|
||||
return {
|
||||
close: function () {
|
||||
clearTimeout(hideTimer);
|
||||
if (item.parentNode) item.parentNode.removeChild(item);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.showToast = showToast;
|
||||
})();
|
||||
@@ -0,0 +1,161 @@
|
||||
(function () {
|
||||
function parseExpireTime(value) {
|
||||
if (value == null || value === "") return NaN;
|
||||
if (typeof value === "number") {
|
||||
return value < 1e12 ? value * 1000 : value;
|
||||
}
|
||||
var text = String(value).trim();
|
||||
if (!text) return NaN;
|
||||
if (/^\d+$/.test(text)) {
|
||||
var num = Number(text);
|
||||
return num < 1e12 ? num * 1000 : num;
|
||||
}
|
||||
return Date.parse(text);
|
||||
}
|
||||
|
||||
function isVipActive(user) {
|
||||
if (!user) return false;
|
||||
var type = Number(user.vipType);
|
||||
if (!type || type === 0) return false;
|
||||
var expireMs = parseExpireTime(user.currentVipExpire);
|
||||
if (!isFinite(expireMs) || expireMs <= 0) return false;
|
||||
return expireMs > Date.now();
|
||||
}
|
||||
|
||||
function vipTypeLabel(vipType) {
|
||||
var type = Number(vipType);
|
||||
if (type === 1) return "月卡会员";
|
||||
if (type === 2) return "年卡会员";
|
||||
return "普通会员";
|
||||
}
|
||||
|
||||
function vipDisplayLabel(user) {
|
||||
var name = user && String(user.showCardName || "").trim();
|
||||
if (name) return name;
|
||||
return vipTypeLabel(user && user.vipType);
|
||||
}
|
||||
|
||||
function vipExpireText(raw) {
|
||||
var ms = parseExpireTime(raw);
|
||||
if (!isFinite(ms) || ms <= 0) return "";
|
||||
var d = new Date(ms);
|
||||
var y = d.getFullYear();
|
||||
var m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
var day = String(d.getDate()).padStart(2, "0");
|
||||
return "有效期至 " + y + "/" + m + "/" + day;
|
||||
}
|
||||
|
||||
function formatRecharge(value) {
|
||||
var n = Number(value);
|
||||
if (!isFinite(n) || n < 0) n = 0;
|
||||
return String(Math.floor(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function syncUserCenterRecharge(value) {
|
||||
var text = formatRecharge(
|
||||
value != null
|
||||
? value
|
||||
: CookieUtil.getAuthSession() && CookieUtil.getAuthSession().recharge,
|
||||
);
|
||||
var pc = document.querySelector("[data-user-center-recharge]");
|
||||
if (pc) pc.textContent = text;
|
||||
var hint = document.querySelector("[data-user-center-recharge-hint]");
|
||||
if (hint) hint.textContent = text + "金币";
|
||||
}
|
||||
|
||||
function syncUserCenterID(value) {
|
||||
var id = String(value == null ? "" : value).trim();
|
||||
if (!id) return;
|
||||
document.querySelectorAll("[data-user-center-id]").forEach(function (el) {
|
||||
el.textContent = el.getAttribute("data-id-layout") === "pc"
|
||||
? "ID:" + id
|
||||
: "ID " + id;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshUserCenterWallet() {
|
||||
if (!window.CookieUtil || !CookieUtil.isLoggedIn()) return;
|
||||
syncUserCenterRecharge();
|
||||
if (!window.HttpRequest) return;
|
||||
HttpRequest.get("/api/auth/getInfo")
|
||||
.then(function (res) {
|
||||
if (!res || res.code !== 200 || !res.data) return;
|
||||
if (CookieUtil.setAuthSession) CookieUtil.setAuthSession(res.data);
|
||||
syncUserCenterID(res.data.userId);
|
||||
syncUserCenterRecharge(res.data.recharge);
|
||||
syncUserCenterVip();
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
function syncUserCenterVip() {
|
||||
if (!window.CookieUtil || !CookieUtil.isLoggedIn()) return;
|
||||
var session = CookieUtil.getAuthSession();
|
||||
var active = isVipActive(session);
|
||||
var label = active ? vipDisplayLabel(session) : "";
|
||||
var expire = active
|
||||
? vipExpireText(session && session.currentVipExpire)
|
||||
: "";
|
||||
|
||||
var pc = document.querySelector("[data-user-center-vip]");
|
||||
if (pc) {
|
||||
pc.classList.toggle("user-center-vip-card--active", active);
|
||||
var status = pc.querySelector("[data-vip-status]");
|
||||
var expireEl = pc.querySelector("[data-vip-expire]");
|
||||
var action = pc.querySelector("[data-vip-action]");
|
||||
if (status) {
|
||||
status.textContent = active ? "尊享 VIP 会员" : "尚未开通会员";
|
||||
}
|
||||
if (expireEl) {
|
||||
if (active && expire) {
|
||||
expireEl.hidden = false;
|
||||
expireEl.style.display = "";
|
||||
expireEl.textContent = expire;
|
||||
} else {
|
||||
expireEl.hidden = true;
|
||||
expireEl.style.display = "none";
|
||||
expireEl.textContent = "";
|
||||
}
|
||||
}
|
||||
if (action) {
|
||||
if (active) {
|
||||
action.hidden = true;
|
||||
action.style.display = "none";
|
||||
} else {
|
||||
action.hidden = false;
|
||||
action.style.display = "";
|
||||
action.textContent = "立即开通 →";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mobile = document.querySelector("[data-user-center-mobile-vip]");
|
||||
if (mobile) {
|
||||
mobile.classList.toggle("user-center-mobile-vip--active", active);
|
||||
var title = mobile.querySelector("[data-mobile-vip-title]");
|
||||
var desc = mobile.querySelector("[data-mobile-vip-desc]");
|
||||
var btn = mobile.querySelector("[data-mobile-vip-btn]");
|
||||
if (title) title.textContent = active ? label : "开通黄果短剧会员";
|
||||
if (desc) {
|
||||
desc.textContent = active
|
||||
? expire || "全场免费看 · 热门短剧抢先看"
|
||||
: "全场免费看 · 热门短剧抢先看";
|
||||
}
|
||||
if (btn) {
|
||||
btn.hidden = false;
|
||||
btn.style.display = "";
|
||||
if (active) {
|
||||
btn.textContent = "续费会员";
|
||||
} else {
|
||||
btn.textContent = "立即开通";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncUserCenterVip();
|
||||
refreshUserCenterWallet();
|
||||
window.syncUserCenterVip = syncUserCenterVip;
|
||||
window.syncUserCenterID = syncUserCenterID;
|
||||
window.syncUserCenterRecharge = syncUserCenterRecharge;
|
||||
})();
|
||||
@@ -0,0 +1,52 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("vipDialog");
|
||||
if (!mask) return;
|
||||
|
||||
function openDialog(event) {
|
||||
if (event) event.preventDefault();
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function isVipLockedEpisode(target) {
|
||||
var episode = target.closest(
|
||||
".detail-episode, .play-ep, .play-ep-sheet-item, .detail-h5-ep-item"
|
||||
);
|
||||
if (!episode) return false;
|
||||
// 金币解锁走 coinDialog,不在此打开 VIP 弹窗
|
||||
if (episode.hasAttribute("data-coin-dialog-open")) return false;
|
||||
return !!episode.querySelector(
|
||||
".detail-episode-lock[src*='vip'], .detail-episode-vip, .play-ep-lock[src*='vip'], .play-ep-sheet-lock[src*='vip'], .detail-h5-ep-lock[src*='vip']"
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-vip-dialog-open]") || isVipLockedEpisode(event.target)) {
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-vip-dialog-close]")) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === mask) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
window.openVipDialog = openDialog;
|
||||
window.closeVipDialog = closeDialog;
|
||||
})();
|
||||
@@ -0,0 +1,30 @@
|
||||
(function () {
|
||||
var scrollToTopButton = document.getElementById("scrollToTopButton");
|
||||
if (!scrollToTopButton) return;
|
||||
|
||||
var ticking = false;
|
||||
var lastVisible = null;
|
||||
|
||||
function updateVisibility() {
|
||||
ticking = false;
|
||||
// 用 visualViewport / 近似阈值,避免反复读 layout 几何属性触发强制重排
|
||||
var threshold = Math.max(window.innerHeight || 600, 600);
|
||||
var shouldShow = (window.scrollY || window.pageYOffset || 0) > threshold;
|
||||
if (shouldShow === lastVisible) return;
|
||||
lastVisible = shouldShow;
|
||||
scrollToTopButton.classList.toggle("is-visible", shouldShow);
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
window.requestAnimationFrame(updateVisibility);
|
||||
}
|
||||
|
||||
scrollToTopButton.addEventListener("click", function () {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.requestAnimationFrame(updateVisibility);
|
||||
})();
|
||||
@@ -0,0 +1,46 @@
|
||||
(function () {
|
||||
function setupRoot(root) {
|
||||
var track = root.querySelector(".square-promo__track");
|
||||
var group = root.querySelector(".square-promo__group");
|
||||
if (!track || !group) return;
|
||||
|
||||
var clone = root.querySelector(".square-promo__group--clone");
|
||||
if (clone) clone.remove();
|
||||
root.classList.remove("is-marquee");
|
||||
track.style.animationDuration = "";
|
||||
|
||||
if (group.scrollWidth <= root.clientWidth + 1) return;
|
||||
|
||||
clone = group.cloneNode(true);
|
||||
clone.classList.add("square-promo__group--clone");
|
||||
clone.setAttribute("aria-hidden", "true");
|
||||
clone.querySelectorAll("a").forEach(function (link) {
|
||||
link.setAttribute("tabindex", "-1");
|
||||
});
|
||||
track.appendChild(clone);
|
||||
root.classList.add("is-marquee");
|
||||
track.style.animationDuration =
|
||||
Math.max(18, Math.round(group.scrollWidth / 36)) + "s";
|
||||
if (typeof window.initLazyImages === "function") {
|
||||
window.initLazyImages(clone);
|
||||
}
|
||||
}
|
||||
|
||||
function setupAll() {
|
||||
document.querySelectorAll("[data-square-promo]").forEach(setupRoot);
|
||||
}
|
||||
|
||||
var resizeTimer = 0;
|
||||
|
||||
function onResize() {
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(setupAll, 160);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", setupAll, { once: true });
|
||||
} else {
|
||||
setupAll();
|
||||
}
|
||||
window.addEventListener("resize", onResize);
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user