Initial commit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fish03
2026-09-23 14:43:16 +07:00
co-authored by Claude Opus 5.5
commit bef9f8b4ed
663 changed files with 81305 additions and 0 deletions
+454
View File
@@ -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);
+204
View File
@@ -0,0 +1,204 @@
/**
* Cookie 读写(登录 token / sid 等统一落 cookie)
*/
(function (global) {
function encode(value) {
return encodeURIComponent(String(value == null ? "" : value));
}
function decode(value) {
try {
// 兼容服务端历史 QueryEscape:空格被编成 "+",decodeURIComponent 不会还原
return decodeURIComponent(String(value || "").replace(/\+/g, "%20"));
} catch (e) {
return value;
}
}
function getCookie(name) {
if (!name) return "";
var key = encodeURIComponent(name) + "=";
var parts = String(document.cookie || "").split(";");
for (var i = 0; i < parts.length; i++) {
var part = parts[i].replace(/^\s+/, "");
if (part.indexOf(key) === 0) {
return decode(part.slice(key.length));
}
}
return "";
}
/**
* @param {string} name
* @param {string} value
* @param {{ days?: number, path?: string, sameSite?: string, secure?: boolean }} [options]
*/
function setCookie(name, value, options) {
options = options || {};
var days = options.days;
var path = options.path || "/";
var sameSite = options.sameSite || "Lax";
var parts = [
encodeURIComponent(name) + "=" + encode(value),
"path=" + path,
"SameSite=" + sameSite,
];
if (typeof days === "number") {
var expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
parts.push("expires=" + expires.toUTCString());
}
if (options.secure || (global.location && global.location.protocol === "https:")) {
parts.push("Secure");
}
document.cookie = parts.join("; ");
}
function removeCookie(name, path) {
setCookie(name, "", { days: -1, path: path || "/" });
}
var AUTH = {
TOKEN: "token",
SID: "sid",
S_TOKEN: "_s_token_",
USER_ID: "userId",
ACCOUNT_NO: "accountNo",
NICK_NAME: "nickName",
INTRODUCTION: "introduction",
AVATAR: "avatar",
EMAIL: "email",
INVITE_CODE: "inviteCode",
RECHARGE: "recharge",
VIP_TYPE: "vipType",
VIP_EXPIRE: "currentVipExpire",
SHOW_CARD_ID: "showCardId",
SHOW_CARD_NAME: "showCardName",
USER_TYPE: "userType",
IS_REGISTER: "isRegister",
};
var AUTH_KEYS = [
AUTH.TOKEN,
AUTH.USER_ID,
AUTH.ACCOUNT_NO,
AUTH.NICK_NAME,
AUTH.INTRODUCTION,
AUTH.AVATAR,
AUTH.EMAIL,
AUTH.INVITE_CODE,
AUTH.RECHARGE,
AUTH.VIP_TYPE,
AUTH.VIP_EXPIRE,
AUTH.SHOW_CARD_ID,
AUTH.SHOW_CARD_NAME,
AUTH.USER_TYPE,
AUTH.IS_REGISTER,
"_token_",
AUTH.SID,
AUTH.S_TOKEN,
];
function getToken() {
return getCookie(AUTH.TOKEN) || getCookie("_token_") || "";
}
function setToken(token, days) {
setCookie(AUTH.TOKEN, token || "", { days: typeof days === "number" ? days : 7 });
}
function getSid() {
return getCookie(AUTH.SID) || "";
}
function setSid(sid, days) {
setCookie(AUTH.SID, sid || "", { days: typeof days === "number" ? days : 7 });
}
function setAuthSession(user, days) {
if (!user) return;
var d = typeof days === "number" ? days : 7;
if (user.token) setToken(user.token, d);
if (user.userId != null) setCookie(AUTH.USER_ID, String(user.userId), { days: d });
if (user.accountNo) setCookie(AUTH.ACCOUNT_NO, user.accountNo, { days: d });
if (user.nickName) setCookie(AUTH.NICK_NAME, user.nickName, { days: d });
if (user.introduction != null) setCookie(AUTH.INTRODUCTION, user.introduction || "", { days: d });
if (user.avatar != null) setCookie(AUTH.AVATAR, user.avatar || "", { days: d });
if (user.email != null) setCookie(AUTH.EMAIL, user.email || "", { days: d });
if (user.inviteCode != null) setCookie(AUTH.INVITE_CODE, user.inviteCode || "", { days: d });
// 仅在响应显式带 recharge 时写入(含 0),避免局部更新把余额抹成 0
if (Object.prototype.hasOwnProperty.call(user, "recharge")) {
setCookie(AUTH.RECHARGE, String(Number(user.recharge) || 0), { days: d });
}
if (user.vipType != null) setCookie(AUTH.VIP_TYPE, String(user.vipType), { days: d });
if (user.currentVipExpire != null) {
setCookie(AUTH.VIP_EXPIRE, user.currentVipExpire || "", { days: d });
}
if (Object.prototype.hasOwnProperty.call(user, "showCardId")) {
setCookie(AUTH.SHOW_CARD_ID, String(user.showCardId != null ? user.showCardId : ""), { days: d });
}
if (Object.prototype.hasOwnProperty.call(user, "showCardName")) {
setCookie(AUTH.SHOW_CARD_NAME, user.showCardName || "", { days: d });
}
if (user.type != null) setCookie(AUTH.USER_TYPE, String(user.type), { days: d });
if (Object.prototype.hasOwnProperty.call(user, "isRegister") || user.type != null) {
var registered = !!(user.isRegister || Number(user.type) === 1);
setCookie(AUTH.IS_REGISTER, registered ? "1" : "0", { days: d });
}
}
function getAuthSession() {
var token = getToken();
if (!token) return null;
return {
token: token,
userId: getCookie(AUTH.USER_ID),
accountNo: getCookie(AUTH.ACCOUNT_NO),
nickName: getCookie(AUTH.NICK_NAME),
introduction: getCookie(AUTH.INTRODUCTION),
avatar: getCookie(AUTH.AVATAR),
email: getCookie(AUTH.EMAIL),
inviteCode: getCookie(AUTH.INVITE_CODE),
recharge: getCookie(AUTH.RECHARGE),
vipType: getCookie(AUTH.VIP_TYPE),
currentVipExpire: getCookie(AUTH.VIP_EXPIRE),
showCardId: getCookie(AUTH.SHOW_CARD_ID),
showCardName: getCookie(AUTH.SHOW_CARD_NAME),
type: getCookie(AUTH.USER_TYPE),
isRegister: getCookie(AUTH.IS_REGISTER) === "1",
};
}
function clearAuth() {
for (var i = 0; i < AUTH_KEYS.length; i++) {
removeCookie(AUTH_KEYS[i]);
}
}
function isLoggedIn() {
return !!getToken();
}
/** 是否正式注册账号(非游客) */
function isRegistered() {
if (!isLoggedIn()) return false;
if (getCookie(AUTH.IS_REGISTER) === "1") return true;
return Number(getCookie(AUTH.USER_TYPE)) === 1;
}
global.CookieUtil = {
get: getCookie,
set: setCookie,
remove: removeCookie,
AUTH: AUTH,
getToken: getToken,
setToken: setToken,
getSid: getSid,
setSid: setSid,
setAuthSession: setAuthSession,
getAuthSession: getAuthSession,
clearAuth: clearAuth,
isLoggedIn: isLoggedIn,
isRegistered: isRegistered,
};
})(typeof window !== "undefined" ? window : this);
+142
View File
@@ -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);
+160
View File
@@ -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();
}
})();
+307
View File
@@ -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;
})();
+481
View File
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function escapeAttr(text) {
return escapeHtml(text).replace(/'/g, "&#39;");
}
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
+224
View File
@@ -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);
})();
+243
View File
@@ -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);
+302
View File
@@ -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;
})();
+139
View File
@@ -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;
});
});
})();
+68
View File
@@ -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;
});
});
})();
+62
View File
@@ -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();
}
})();
+668
View File
@@ -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);
});
});
})();
+8
View File
@@ -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" });
});
});
})();
+568
View File
@@ -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);
}
})();
+85
View File
@@ -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();
});
})();
+37
View File
@@ -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);
});
});
})();
+255
View File
@@ -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();
})();
+236
View File
@@ -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();
}
})();
+230
View File
@@ -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();
});
}
})();
+43
View File
@@ -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
+213
View File
@@ -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();
}
})();
+89
View File
@@ -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();
}
});
}
})();
+34
View File
@@ -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();
}
});
}
})();
+76
View File
@@ -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;
})();
+161
View File
@@ -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;
})();
+52
View File
@@ -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;
})();
+30
View File
@@ -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);
})();
+46
View File
@@ -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