Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-22 14:47:17 +07:00
co-authored by Claude Opus 5.5
commit 3639d2184a
574 changed files with 61690 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* 前端请求封装。
* - 游客会话走 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;
});
})
.finally(function () {
if (timer) clearTimeout(timer);
});
}
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();
}
})();
+914
View File
@@ -0,0 +1,914 @@
(function () {
var section = document.querySelector("[data-dh-episodes]");
if (!section) return;
var ranges = section.querySelectorAll(".dh-range");
var episodes = section.querySelectorAll(".dh-ep");
if (!episodes.length) return;
if (!ranges.length) return;
function applyRange(start, end) {
episodes.forEach(function (el) {
var n = Number(el.getAttribute("data-ep"));
var inRange = !Number.isNaN(n) && n >= start && n <= end;
el.classList.toggle("is-range-hidden", !inRange);
});
}
function activate(btn) {
ranges.forEach(function (item) {
item.classList.toggle("is-active", item === btn);
});
applyRange(Number(btn.dataset.rangeStart), Number(btn.dataset.rangeEnd));
}
ranges.forEach(function (btn) {
btn.addEventListener("click", function () {
activate(btn);
});
});
var active = section.querySelector(".dh-range.is-active") || ranges[0];
activate(active);
})();
// 简介超出行数时显示「展开 / 收起」
(function () {
var intro = document.querySelector("[data-dh-intro]");
var toggle = document.querySelector("[data-dh-intro-toggle]");
if (!intro || !toggle) return;
function sync() {
if (intro.classList.contains("is-expanded")) return;
toggle.hidden = intro.scrollHeight <= intro.clientHeight + 1;
}
toggle.addEventListener("click", function () {
var expanded = intro.classList.toggle("is-expanded");
toggle.textContent = expanded ? "收起" : "展开";
});
window.addEventListener("resize", sync);
sync();
})();
(function () {
var shareBtns = document.querySelectorAll("[data-detail-share]");
if (!shareBtns.length) return;
function toast(message, type) {
if (typeof window.showToast === "function") {
window.showToast(message, type);
return;
}
window.alert(message);
}
function resolvePlayletId(btn) {
var id = Number(btn.getAttribute("data-playlet-id") || 0);
if (id > 0) return id;
var host =
btn.closest("[data-playlet-id]") ||
document.querySelector("[data-play-player][data-playlet-id]") ||
document.querySelector("[data-playlet-id]");
if (host) {
id = Number(host.getAttribute("data-playlet-id") || 0);
}
return id > 0 ? id : 0;
}
function reportForwardHeat(playletId) {
if (!playletId || !window.HttpRequest) return;
window.HttpRequest.post("/api/playlet/forward/heat", { id: playletId }).catch(
function () {
/* 加热失败不影响分享体验 */
}
);
}
function fallbackCopy(text) {
var area = document.createElement("textarea");
area.value = text;
area.setAttribute("readonly", "");
area.style.cssText = "position:fixed;left:-9999px;top:0;opacity:0;";
document.body.appendChild(area);
area.focus();
area.select();
area.setSelectionRange(0, area.value.length);
var ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {
ok = false;
}
document.body.removeChild(area);
return ok;
}
function copyPageUrl() {
var url = window.location.href;
if (navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext) {
return navigator.clipboard.writeText(url).then(
function () {
return true;
},
function () {
return fallbackCopy(url);
}
);
}
return Promise.resolve(fallbackCopy(url));
}
shareBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var playletId = resolvePlayletId(btn);
copyPageUrl()
.then(function (ok) {
if (ok) {
toast("链接已复制", "success");
reportForwardHeat(playletId);
} else {
toast("复制失败,请手动复制链接", "error");
}
})
.catch(function () {
toast("复制失败,请手动复制链接", "error");
});
});
});
})();
(function () {
// 付费剧集:不再站内解锁,统一引导下载 APP
var lockedEpisodes = document.querySelectorAll("[data-episode-locked]");
if (!lockedEpisodes.length) return;
lockedEpisodes.forEach(function (el) {
el.addEventListener("click", function (e) {
e.preventDefault();
if (typeof window.openDownloadApp === "function") {
window.openDownloadApp();
}
});
});
})();
(function () {
var grid = document.querySelector("[data-related-grid]");
var btn = document.querySelector("[data-related-refresh]");
if (!grid || !btn) return;
var section = document.querySelector("[data-related-section]");
var mobileMedia = window.matchMedia && window.matchMedia("(max-width: 767px)");
var isMobileDetail =
document.body.classList.contains("is-detail-page") &&
mobileMedia &&
mobileMedia.matches;
if (isMobileDetail) {
var PLACEHOLDER = "/static/images/placeholder.png?v=2";
var guessLikeData = [];
var guessOffset = 0;
var pageSize = 4;
grid.textContent = "";
btn.hidden = true;
if (section) section.hidden = true;
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 = "poster-cover-img";
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("div");
var episodeText = document.createElement("div");
var body = document.createElement("div");
var title = document.createElement("div");
var subtitle = document.createElement("div");
var meta = 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 = "poster-card";
card.href = detailHref(item);
card.setAttribute("aria-label", titleText);
cover.className = "poster-cover";
episode.className = "episode-badge";
episodeText.className = "text-white-xs";
body.className = "poster-card-body";
title.className = "poster-card-title";
subtitle.className = "poster-card-subtitle";
meta.className = "poster-card-meta";
tag.className = "poster-card-tag";
setCardCover(image, (item && (item.cover || item.banner)) || "", titleText);
episodeText.textContent = totalEpisode ? totalEpisode + "集" : "更新中";
title.textContent = titleText;
subtitle.textContent = String((item && item.desc) || "精彩内容等你来看");
tag.textContent = firstCardTag(item);
episode.appendChild(episodeText);
cover.appendChild(image);
cover.appendChild(episode);
meta.appendChild(tag);
body.appendChild(title);
body.appendChild(subtitle);
body.appendChild(meta);
card.appendChild(cover);
card.appendChild(body);
return card;
}
function renderMobileGuessLike() {
var count = Math.min(pageSize, guessLikeData.length);
grid.textContent = "";
if (section) section.hidden = count === 0;
btn.hidden = guessLikeData.length <= pageSize;
for (var index = 0; index < count; index += 1) {
grid.appendChild(
createGuessCard(guessLikeData[(guessOffset + index) % guessLikeData.length])
);
}
if (count && typeof window.initLazyImages === "function") {
window.initLazyImages(grid);
}
}
btn.addEventListener("click", function () {
if (guessLikeData.length > pageSize) {
guessOffset = (guessOffset + pageSize) % guessLikeData.length;
}
renderMobileGuessLike();
});
function loadMobileGuessLike() {
if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") {
renderMobileGuessLike();
return;
}
window.HttpRequest.get("/api/playlet/hot-like")
.then(function (response) {
var source = response && response.data && response.data.guessLike;
guessLikeData = Array.isArray(source)
? source.filter(function (item) {
return item && (item.id || item.slug);
})
: [];
guessOffset = 0;
renderMobileGuessLike();
})
.catch(function () {
guessLikeData = [];
renderMobileGuessLike();
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadMobileGuessLike, { once: true });
} else {
loadMobileGuessLike();
}
return;
}
var cards = Array.prototype.slice.call(grid.querySelectorAll(".poster-card"));
if (cards.length <= 6) {
btn.hidden = true;
return;
}
var pageSize = 6;
var page = 0;
function render() {
var start = (page * pageSize) % cards.length;
cards.forEach(function (card) {
card.style.display = "none";
});
for (var i = 0; i < pageSize; i++) {
var card = cards[(start + i) % cards.length];
card.style.display = "";
grid.appendChild(card);
}
}
render();
btn.addEventListener("click", function () {
page += 1;
render();
});
})();
(function () {
// 注意:本文件在页面底部同步加载,早于 header 中 defer 的 request.js。
// 不能在此用 `if (!HttpRequest) return`,否则评论初始化会整段跳过。
// 实际请求在 DOMContentLoaded(HttpRequest 已就绪)后再发起。
var DEFAULT_AVATAR = "/static/images/avatar.png";
var PLACEHOLDER = "/static/images/placeholder.png?v=2";
function toast(message, type) {
if (typeof window.showToast === "function") {
window.showToast(message, type);
return;
}
window.alert(message);
}
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatCommentTime(raw) {
if (!raw) return "";
var d = new Date(raw);
if (Number.isNaN(d.getTime())) return String(raw);
return (
d.getFullYear() +
"/" +
(d.getMonth() + 1) +
"/" +
d.getDate() +
" " +
String(d.getHours()).padStart(2, "0") +
":" +
String(d.getMinutes()).padStart(2, "0")
);
}
function formatRelativeTime(raw) {
if (!raw) return "";
var d = new Date(raw);
if (Number.isNaN(d.getTime())) return String(raw);
var diff = Date.now() - d.getTime();
if (diff < 0) diff = 0;
var minutes = Math.floor(diff / 60000);
if (minutes < 1) return "刚刚";
if (minutes < 60) return minutes + "分钟前";
var hours = Math.floor(minutes / 60);
if (hours < 24) return hours + "小时前";
var days = Math.floor(hours / 24);
if (days < 30) return days + "天前";
return formatCommentTime(raw);
}
function avatarURL(url) {
url = String(url || "").trim();
return url || DEFAULT_AVATAR;
}
function replyToggleLabel(expanded, count) {
count = Number(count || 0);
if (expanded) return "收起回复";
return "查看全部 " + count + " 条回复 ›";
}
function renderDetailComment(item, playPcMode) {
var avatar = avatarURL(item.userAvatar);
var avatarHtml = item.userAvatar
? '<img class="detail-comment-avatar lazy" src="' +
PLACEHOLDER +
'" data-src="' +
escapeHtml(avatar) +
'" alt="' +
escapeHtml((item.userName || "匿名用户") + "的头像") +
'" />'
: '<img class="detail-comment-avatar" src="' + DEFAULT_AVATAR + '" alt="匿名用户头像" />';
var textHtml = escapeHtml(item.text || "");
if (item.replyUserName) {
textHtml =
'<span class="detail-comment-reply-to">回复 @' +
escapeHtml(item.replyUserName) +
"</span> " +
textHtml;
}
var children = Array.isArray(item.children) ? item.children : [];
var replyCount = Number(item.commentCount || children.length || 0);
var repliesHtml = "";
if (children.length) {
var collapsed = children.length > 1;
repliesHtml =
'<div class="detail-comment-replies">' +
children
.map(function (child, index) {
var childText = escapeHtml(child.text || "");
if (child.replyUserName) {
childText =
'<span class="detail-comment-reply-to">@' +
escapeHtml(child.replyUserName) +
"</span> " +
childText;
}
return (
'<div class="detail-comment-reply-item" data-comment-id="' +
escapeHtml(child.id) +
'"' +
(collapsed && index > 0
? ' data-reply-collapsed-item hidden'
: "") +
">" +
'<span class="detail-comment-reply-name">' +
escapeHtml(child.userName || "匿名用户") +
":</span>" +
'<span class="detail-comment-reply-text">' +
childText +
"</span>" +
"</div>"
);
})
.join("") +
(replyCount > 1
? '<button class="detail-comment-replies-toggle" type="button" data-comment-replies-toggle data-reply-count="' +
escapeHtml(replyCount) +
'" aria-expanded="false">' +
replyToggleLabel(false, replyCount) +
"</button>"
: "") +
"</div>";
} else if (playPcMode && replyCount > 1) {
repliesHtml =
'<div class="detail-comment-replies"><button class="detail-comment-replies-toggle" type="button" data-comment-replies-toggle data-reply-count="' +
escapeHtml(replyCount) +
'" aria-expanded="false">' +
replyToggleLabel(false, replyCount) +
"</button></div>";
}
return (
'<div class="detail-comment" data-comment-id="' +
escapeHtml(item.id) +
'">' +
avatarHtml +
'<div class="detail-comment-body">' +
'<div class="detail-comment-name">' +
escapeHtml(item.userName || "匿名用户") +
"</div>" +
'<div class="detail-comment-text">' +
textHtml +
"</div>" +
'<div class="detail-comment-meta">' +
'<span class="detail-comment-time">' +
escapeHtml(formatCommentTime(item.createdAt)) +
"</span>" +
"</div>" +
repliesHtml +
"</div></div>"
);
}
function renderPlayComment(item) {
var avatar = avatarURL(item.userAvatar);
var commentId = item.id;
var children = Array.isArray(item.children) ? item.children : [];
var replyCount = Number(item.commentCount || children.length || 0);
var avatarHtml = item.userAvatar
? '<img class="play-comment-avatar lazy" src="' +
PLACEHOLDER +
'" data-src="' +
escapeHtml(avatar) +
'" alt="' +
escapeHtml((item.userName || "匿名用户") + "的头像") +
'" />'
: '<img class="play-comment-avatar" src="' + DEFAULT_AVATAR + '" alt="匿名用户头像" />';
var textHtml = escapeHtml(item.text || "");
if (item.replyUserName) {
textHtml =
'<span class="play-comment-reply-to">回复 @' +
escapeHtml(item.replyUserName) +
"</span> " +
textHtml;
}
var repliesHtml = "";
if (children.length) {
repliesHtml =
'<div class="play-comment-replies">' +
children
.map(function (child) {
return (
'<div class="play-comment-reply-line" data-comment-id="' +
escapeHtml(child.id) +
'">' +
'<span class="play-comment-reply-name">' +
escapeHtml(child.userName || "匿名用户") +
"</span>" +
(child.replyUserName
? '<span class="play-comment-reply-badge">回复</span><span class="play-comment-reply-name">' +
escapeHtml(child.replyUserName) +
"</span>"
: "") +
'<span class="play-comment-reply-text">' +
escapeHtml(child.text || "") +
"</span>" +
"</div>"
);
})
.join("") +
(replyCount > children.length
? '<button class="play-comment-replies-all" type="button" data-comment-replies="' +
escapeHtml(commentId) +
'">全部' +
escapeHtml(replyCount) +
"条评论</button>"
: "") +
"</div>";
} else if (replyCount > 0) {
repliesHtml =
'<button class="play-comment-more-replies" type="button" data-comment-replies="' +
escapeHtml(commentId) +
'">全部' +
escapeHtml(replyCount) +
'条评论<img src="/static/images/arrow_right.svg" alt="查看回复" /></button>';
}
return (
'<div class="play-comment-item" data-comment-id="' +
escapeHtml(commentId) +
'">' +
avatarHtml +
'<div class="play-comment-body">' +
'<div class="play-comment-name">' +
escapeHtml(item.userName || "匿名用户") +
"</div>" +
'<div class="play-comment-text">' +
textHtml +
"</div>" +
'<div class="play-comment-meta">' +
'<div class="play-comment-meta-left">' +
'<span class="play-comment-time">' +
escapeHtml(formatRelativeTime(item.createdAt)) +
"</span>" +
"</div>" +
"</div>" +
repliesHtml +
"</div></div>"
);
}
function renderComment(item, sheetMode, playPcMode) {
return sheetMode
? renderPlayComment(item)
: renderDetailComment(item, playPcMode);
}
function triggerLazy(root) {
if (typeof window.initLazyImages === "function") {
window.initLazyImages(root || document);
return;
}
if (typeof window.__lazyLoadImages === "function") {
window.__lazyLoadImages();
return;
}
if (window.LazyLoad && typeof window.LazyLoad.update === "function") {
window.LazyLoad.update();
}
}
function initCommentSection(section) {
// PC 播放页评论区仅桌面初始化;H5 sheet 始终初始化(避免宽屏加载后缩到手机仍卡在「加载中」)
if (section.classList.contains("play-comments")) {
if (window.matchMedia("(max-width: 767px)").matches) return;
}
var sheetRoot = section.closest("[data-play-comment-sheet]");
var sheetMode = !!sheetRoot || section.classList.contains("play-comment-sheet-body");
var playPcMode = section.classList.contains("play-comments");
var scope = sheetRoot || section;
var listEl = section.querySelector("[data-comment-list]");
var moreBtn = section.querySelector("[data-comment-more]");
var titleEl = section.querySelector("[data-comment-title]");
var sortBtns = scope.querySelectorAll("[data-comment-sort]");
var sheetTitle = sheetRoot
? sheetRoot.querySelector("[data-play-comment-title]")
: document.querySelector("[data-play-comment-title]");
var sideCount = document.querySelector("[data-play-comment-count]");
if (section.getAttribute("data-comment-inited") === "1") return;
section.setAttribute("data-comment-inited", "1");
var objectType = Number(section.getAttribute("data-comment-object-type") || 10);
var pageSize = Number(section.getAttribute("data-comment-size") || 10);
var sortDefault = Number(section.getAttribute("data-comment-sort-default") || 0);
var sortOrder = sortDefault === 2 || sortDefault === 1 ? sortDefault : sheetMode ? 2 : 1;
var page = 0;
var total = 0;
var loading = false;
function getObjectId() {
return Number(section.getAttribute("data-comment-object-id") || 0);
}
function commentItemSelector() {
return sheetMode ? ".play-comment-item" : ".detail-comment";
}
function updateTitleCount() {
if (titleEl) {
titleEl.textContent = total > 0 ? "评论 (" + total + ")" : "评论";
}
if (sheetTitle) {
sheetTitle.textContent = total > 0 ? total + "条评论" : "评论";
}
if (sideCount) {
sideCount.textContent = String(total);
}
}
function syncSortActive() {
sortBtns.forEach(function (item) {
var active = Number(item.getAttribute("data-comment-sort")) === sortOrder;
item.classList.toggle("detail-sort-btn--active", active);
item.classList.toggle("play-comment-sort-btn--active", active);
});
}
function setMoreVisible(show) {
if (!moreBtn) return;
if (show) {
moreBtn.hidden = false;
moreBtn.removeAttribute("hidden");
moreBtn.disabled = false;
} else {
moreBtn.hidden = true;
moreBtn.setAttribute("hidden", "");
moreBtn.disabled = false;
}
}
function showEmpty(text) {
var emptyClass = sheetMode
? "section-empty play-comment-empty"
: "section-empty";
listEl.innerHTML =
'<div class="' +
emptyClass +
'" data-comment-empty>' +
escapeHtml(text || "暂无评论") +
"</div>";
setMoreVisible(false);
}
function fetchPage(nextPage, replace) {
var objectId = getObjectId();
if (!objectId || loading) return Promise.resolve();
// detail.js 若早于 defer 的 request.js 执行,直接 get 会同步抛错且 loading 无法复位
if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") {
if (replace) showEmpty("加载失败,请稍后重试");
return Promise.resolve();
}
loading = true;
if (moreBtn) moreBtn.disabled = true;
return window.HttpRequest.get("/api/comment/list", {
params: {
objectId: objectId,
objectType: objectType,
pageNum: nextPage,
pageSize: pageSize,
sortOrder: sortOrder,
},
})
.then(function (res) {
if (!res || res.code !== 200 || !res.data) {
throw new Error((res && (res.tip || res.msg)) || "加载失败");
}
var rows = res.data.list || [];
total = Number(res.data.totalCount != null ? res.data.totalCount : total);
page = Number(res.data.pageNum || nextPage);
updateTitleCount();
if (replace) {
if (!rows.length) {
showEmpty("暂无评论");
return;
}
listEl.innerHTML = rows
.map(function (row) {
return renderComment(row, sheetMode, playPcMode);
})
.join("");
} else if (rows.length) {
var empty = listEl.querySelector(
"[data-comment-empty],[data-comment-loading]"
);
if (empty) empty.remove();
listEl.insertAdjacentHTML(
"beforeend",
rows
.map(function (row) {
return renderComment(row, sheetMode, playPcMode);
})
.join("")
);
}
triggerLazy(listEl);
var loaded = listEl.querySelectorAll(commentItemSelector()).length;
var hasMore =
rows.length > 0 &&
rows.length >= pageSize &&
(total <= 0 || loaded < total);
setMoreVisible(hasMore);
})
.catch(function (err) {
if (replace || !listEl.querySelector(commentItemSelector())) {
showEmpty((err && err.message) || "加载失败,请稍后重试");
} else {
toast((err && err.message) || "加载失败,请稍后重试", "error");
if (moreBtn) moreBtn.disabled = false;
}
})
.finally(function () {
loading = false;
});
}
syncSortActive();
sortBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var next = Number(btn.getAttribute("data-comment-sort") || 1);
if (next === sortOrder || loading) return;
sortOrder = next;
syncSortActive();
listEl.innerHTML =
'<div class="section-empty' +
(sheetMode ? " play-comment-empty" : "") +
'" data-comment-loading>评论加载中…</div>';
setMoreVisible(false);
fetchPage(1, true);
});
});
if (scope) {
scope.addEventListener("click", function (e) {
var repliesToggle =
e.target && e.target.closest
? e.target.closest("[data-comment-replies-toggle]")
: null;
if (!repliesToggle || !scope.contains(repliesToggle)) return;
e.preventDefault();
var repliesBox = repliesToggle.closest(".detail-comment-replies");
if (!repliesBox) return;
var expanded = repliesToggle.getAttribute("aria-expanded") === "true";
expanded = !expanded;
repliesToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
repliesToggle.textContent = replyToggleLabel(
expanded,
repliesToggle.getAttribute("data-reply-count")
);
var hiddenItems = repliesBox.querySelectorAll("[data-reply-collapsed-item]");
Array.prototype.forEach.call(hiddenItems, function (el) {
el.hidden = !expanded;
});
});
}
if (moreBtn) {
moreBtn.addEventListener("click", function () {
fetchPage(page + 1, false);
});
}
if (!listEl || !getObjectId()) {
if (listEl) showEmpty("暂无评论");
return;
}
section.addEventListener("play:reload-comments", function () {
listEl.innerHTML =
'<div class="section-empty' +
(sheetMode ? " play-comment-empty" : "") +
'" data-comment-loading>评论加载中…</div>';
setMoreVisible(false);
fetchPage(1, true);
});
fetchPage(1, true);
}
// 等 DOMContentLoaded:保证 header 中 defer 的 request.js 已执行
function bootCommentSections() {
if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") {
// 极端情况:再等 window.load 一次
if (!bootCommentSections._retried) {
bootCommentSections._retried = true;
window.addEventListener("load", bootCommentSections, { once: true });
}
return;
}
document.querySelectorAll(".detail-comments").forEach(initCommentSection);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bootCommentSections, { once: true });
} else {
bootCommentSections();
}
})();
/** H5 返回:避免 play ↔ detail 互相压栈导致死循环 */
(function () {
var nodes = document.querySelectorAll("[data-nav-back], .play-h5-back, .detail-h5-back");
if (!nodes.length) return;
function pathOf(url) {
try {
return new URL(url, window.location.origin).pathname || "";
} catch (e) {
return "";
}
}
function isPlayPath(pathname) {
return pathname === "/play";
}
function isDetailPath(pathname) {
return pathname === "/detail";
}
function resolveFallback(el) {
var fallback = el.getAttribute("data-nav-fallback") || "";
if (fallback) return fallback;
var href = el.getAttribute("href") || "";
if (href && href.indexOf("javascript:") !== 0) return href;
return "/";
}
nodes.forEach(function (el) {
el.addEventListener("click", function (e) {
var fallback = resolveFallback(el);
var ref = document.referrer || "";
var sameOrigin = !!ref && ref.indexOf(window.location.origin) === 0;
var refPath = sameOrigin ? pathOf(ref) : "";
var here = window.location.pathname || "";
// 详情页若上一页是播放页,history.back 会回到播放页形成循环
if (isDetailPath(here) && isPlayPath(refPath)) {
e.preventDefault();
window.location.href = fallback || "/";
return;
}
// 播放页:有同站来源时回退历史(通常回到详情),否则走详情/首页兜底
if (isPlayPath(here)) {
e.preventDefault();
if (sameOrigin && refPath && ref !== window.location.href) {
window.history.back();
return;
}
window.location.href = fallback || "/";
return;
}
// 其它带 data-nav-back 的页面
if (sameOrigin && refPath && ref !== window.location.href) {
e.preventDefault();
window.history.back();
return;
}
var href = el.getAttribute("href") || "";
if (!href || href.indexOf("javascript:") === 0) {
e.preventDefault();
window.location.href = fallback || "/";
}
});
});
})();
+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);
})();
+195
View File
@@ -0,0 +1,195 @@
(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",
},
};
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 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) {
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";
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 appDownloadURL() {
var el = document.querySelector("[data-app-download-url]");
var url = el ? (el.getAttribute("data-app-download-url") || "").trim() : "";
return url || "/download-app/";
}
// 付费内容统一入口:移动端直接跳下载地址,PC 弹下载二维码。
function openDownloadApp() {
if (isMobileViewport()) {
global.location.href = appDownloadURL();
return Promise.resolve();
}
return openDialog("download").catch(function () {
global.location.href = appDownloadURL();
});
}
global.ensureDialog = ensureDialog;
global.openDownloadApp = openDownloadApp;
})(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;
})();
+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();
}
})();
+638
View File
@@ -0,0 +1,638 @@
/**
* 意见反馈:校验 → 可选上传截图(最多 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 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;
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) {
toast((err && err.message) || "提交失败", "error");
})
.finally(function () {
setSubmitting(false);
});
});
})();
+45
View File
@@ -0,0 +1,45 @@
(function () {
var scrollTopButtons = document.querySelectorAll(".home-mobile-footer-top");
scrollTopButtons.forEach(function (button) {
button.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: "smooth" });
});
});
// 页脚进入视口后,把悬浮按钮(下载 App / 回到顶部)顶到页脚上方,避免遮住页脚内容。
var footers = document.querySelectorAll(".footer-box, .home-mobile-footer");
if (!footers.length) return;
var root = document.documentElement;
var ticking = false;
var lastLift = -1;
function visibleFooter() {
for (var i = 0; i < footers.length; i++) {
if (footers[i].offsetHeight > 0) return footers[i];
}
return null;
}
function update() {
ticking = false;
var footer = visibleFooter();
var lift = 0;
if (footer) {
var top = footer.getBoundingClientRect().top;
lift = Math.max(0, Math.round((window.innerHeight || 0) - top));
}
if (lift === lastLift) return;
lastLift = lift;
root.style.setProperty("--footer-lift", lift + "px");
}
function schedule() {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(update);
}
window.addEventListener("scroll", schedule, { passive: true });
window.addEventListener("resize", schedule);
schedule();
})();
+438
View File
@@ -0,0 +1,438 @@
(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);
});
}
function refreshDecrypt(root) {
if (typeof window.initLazyImages === "function") {
window.initLazyImages(root || document);
}
}
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]");
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);
}
})();
+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();
})();
+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
+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();
}
});
}
})();
+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;
})();
+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);
})();