@@ -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);
|
||||
@@ -0,0 +1,160 @@
|
||||
(function () {
|
||||
const DESKTOP_COLLAPSE_LIMIT = 10;
|
||||
const MOBILE_COLLAPSE_LIMIT = 8;
|
||||
|
||||
function initApplicationPage() {
|
||||
const page = document.querySelector(".application-page");
|
||||
if (!page) return;
|
||||
|
||||
const tabsNav = page.querySelector(".app-category-tabs");
|
||||
const tabs = page.querySelectorAll(".app-category-tab");
|
||||
const sections = page.querySelectorAll(".app-list-section[data-total]");
|
||||
if (!tabs.length || !sections.length) return;
|
||||
|
||||
let isProgrammaticScroll = false;
|
||||
let scrollTimer = null;
|
||||
|
||||
function isDesktop() {
|
||||
return window.matchMedia("(min-width: 768px)").matches;
|
||||
}
|
||||
|
||||
function getCollapseLimit() {
|
||||
return isDesktop() ? DESKTOP_COLLAPSE_LIMIT : MOBILE_COLLAPSE_LIMIT;
|
||||
}
|
||||
|
||||
function getScrollOffset() {
|
||||
const header = document.querySelector(".site-header");
|
||||
const barHeight = (header && header.getBoundingClientRect().height) || (isDesktop() ? 72 : 56);
|
||||
return barHeight + (isDesktop() ? 24 : 12);
|
||||
}
|
||||
|
||||
function getSectionAnchor(section) {
|
||||
return section.querySelector(".app-list-section__head") || section;
|
||||
}
|
||||
|
||||
function setActiveTab(activeTab) {
|
||||
tabs.forEach((tab) => {
|
||||
const isActive = tab === activeTab;
|
||||
tab.classList.toggle("app-category-tab--active", isActive);
|
||||
tab.setAttribute("aria-selected", isActive ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function updateActionButton(button, expanded, limit) {
|
||||
const label = button.querySelector(".app-list-section__more-label");
|
||||
const icon = button.querySelector(".app-list-section__more-icon");
|
||||
const total = Number(button.dataset.total) || 0;
|
||||
const hiddenCount = Math.max(total - limit, 0);
|
||||
|
||||
if (label) {
|
||||
label.textContent = expanded ? "收起" : `显示全部${hiddenCount}个站点`;
|
||||
}
|
||||
if (icon) {
|
||||
icon.classList.toggle("app-list-section__more-icon--up", expanded);
|
||||
}
|
||||
button.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
}
|
||||
|
||||
function updateCollapseSections() {
|
||||
const limit = getCollapseLimit();
|
||||
|
||||
page.querySelectorAll(".app-list-section[data-total]").forEach((section) => {
|
||||
const total = Number(section.dataset.total) || 0;
|
||||
const button = section.querySelector(".app-list-section__more");
|
||||
const needsCollapse = total > limit;
|
||||
|
||||
section.classList.toggle("app-list-section--collapsible", needsCollapse);
|
||||
|
||||
if (!needsCollapse) {
|
||||
section.classList.remove("app-list-section--expanded");
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.hidden = !needsCollapse;
|
||||
if (needsCollapse) {
|
||||
updateActionButton(button, section.classList.contains("app-list-section--expanded"), limit);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollTabIntoView(tab) {
|
||||
if (!tabsNav || !tab) return;
|
||||
const navRect = tabsNav.getBoundingClientRect();
|
||||
const tabRect = tab.getBoundingClientRect();
|
||||
const nextLeft = tabsNav.scrollLeft + (tabRect.left - navRect.left) - (navRect.width - tabRect.width) / 2;
|
||||
tabsNav.scrollTo({ left: Math.max(0, nextLeft), behavior: "smooth" });
|
||||
}
|
||||
|
||||
function scrollToSection(section) {
|
||||
if (!section) return;
|
||||
const anchor = getSectionAnchor(section);
|
||||
const top = anchor.getBoundingClientRect().top + window.scrollY - getScrollOffset();
|
||||
isProgrammaticScroll = true;
|
||||
clearTimeout(scrollTimer);
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
||||
scrollTimer = window.setTimeout(() => {
|
||||
isProgrammaticScroll = false;
|
||||
}, 800);
|
||||
}
|
||||
|
||||
tabs.forEach((tab, index) => {
|
||||
if (!tab.hasAttribute("aria-selected")) {
|
||||
tab.setAttribute("aria-selected", index === 0 ? "true" : "false");
|
||||
}
|
||||
|
||||
tab.addEventListener("click", () => {
|
||||
const targetId = tab.dataset.target;
|
||||
if (!targetId) return;
|
||||
|
||||
setActiveTab(tab);
|
||||
scrollTabIntoView(tab);
|
||||
scrollToSection(document.getElementById(targetId));
|
||||
});
|
||||
});
|
||||
|
||||
page.querySelectorAll(".app-list-section__more").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const section = button.closest(".app-list-section");
|
||||
if (!section || button.hidden) return;
|
||||
|
||||
const expanded = section.classList.toggle("app-list-section--expanded");
|
||||
updateActionButton(button, expanded, getCollapseLimit());
|
||||
});
|
||||
});
|
||||
|
||||
updateCollapseSections();
|
||||
window.addEventListener("resize", updateCollapseSections);
|
||||
|
||||
if (!("IntersectionObserver" in window)) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (isProgrammaticScroll) return;
|
||||
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
||||
|
||||
if (!visible.length) return;
|
||||
|
||||
const activeTab = page.querySelector(`.app-category-tab[data-target="${visible[0].target.id}"]`);
|
||||
if (activeTab) {
|
||||
setActiveTab(activeTab);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: `-${getScrollOffset()}px 0px -55% 0px`,
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1],
|
||||
}
|
||||
);
|
||||
|
||||
sections.forEach((section) => observer.observe(section));
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initApplicationPage);
|
||||
} else {
|
||||
initApplicationPage();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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 || "/";
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,224 @@
|
||||
(function () {
|
||||
var QUEUE_VERSION = "2";
|
||||
var DISMISSED_KEY = "dialogAdDismissed";
|
||||
var PROGRESS_KEY = "dialogAdProgress";
|
||||
var mask = document.querySelector("[data-dialog-ad]");
|
||||
var previousBodyOverflow = "";
|
||||
var hasDisplayedItem = false;
|
||||
if (!mask) return;
|
||||
|
||||
var items = Array.prototype.slice.call(mask.querySelectorAll("[data-dialog-ad-item]"));
|
||||
if (!items.length) return;
|
||||
|
||||
var queueSignature =
|
||||
QUEUE_VERSION +
|
||||
"|" +
|
||||
items
|
||||
.map(function (item, index) {
|
||||
var image = item.querySelector(".dialog-ad-image");
|
||||
return (
|
||||
item.getAttribute("data-dialog-ad-id") ||
|
||||
(image && image.getAttribute("data-dialog-ad-src")) ||
|
||||
String(index)
|
||||
);
|
||||
})
|
||||
.join("|");
|
||||
|
||||
function readSession(key) {
|
||||
try {
|
||||
return window.sessionStorage.getItem(key) || "";
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function writeSession(key, value) {
|
||||
try {
|
||||
window.sessionStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
// sessionStorage 被禁用时仍允许本页依次展示和关闭广告。
|
||||
}
|
||||
}
|
||||
|
||||
function removeSession(key) {
|
||||
try {
|
||||
window.sessionStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
// sessionStorage 被禁用时无需清理。
|
||||
}
|
||||
}
|
||||
|
||||
function isDismissed() {
|
||||
return readSession(DISMISSED_KEY) === queueSignature;
|
||||
}
|
||||
|
||||
function readProgress() {
|
||||
try {
|
||||
var progress = JSON.parse(readSession(PROGRESS_KEY));
|
||||
if (!progress || progress.signature !== queueSignature) return 0;
|
||||
var value = Number(progress.index);
|
||||
if (!Number.isFinite(value) || value < 0) return 0;
|
||||
return Math.floor(value);
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberProgress(index) {
|
||||
writeSession(
|
||||
PROGRESS_KEY,
|
||||
JSON.stringify({ signature: queueSignature, index: index }),
|
||||
);
|
||||
}
|
||||
|
||||
var currentIndex = readProgress();
|
||||
|
||||
function hideAllItems() {
|
||||
items.forEach(function (item) {
|
||||
item.hidden = true;
|
||||
item.setAttribute("hidden", "");
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveItem(index) {
|
||||
hideAllItems();
|
||||
var item = items[index];
|
||||
if (!item) return false;
|
||||
item.hidden = false;
|
||||
item.removeAttribute("hidden");
|
||||
return true;
|
||||
}
|
||||
|
||||
function openMask() {
|
||||
if (isDismissed()) return;
|
||||
if (!mask.classList.contains("is-open")) {
|
||||
previousBodyOverflow = document.body.style.overflow;
|
||||
mask.hidden = false;
|
||||
mask.removeAttribute("hidden");
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
function finishQueue() {
|
||||
if (hasDisplayedItem) {
|
||||
writeSession(DISMISSED_KEY, queueSignature);
|
||||
} else {
|
||||
removeSession(DISMISSED_KEY);
|
||||
}
|
||||
removeSession(PROGRESS_KEY);
|
||||
hideAllItems();
|
||||
mask.classList.remove("is-open");
|
||||
mask.hidden = true;
|
||||
mask.setAttribute("hidden", "");
|
||||
document.body.style.overflow = previousBodyOverflow;
|
||||
}
|
||||
|
||||
function showWhenAgeGateAllows() {
|
||||
if (isDismissed()) return;
|
||||
var ageGate = document.getElementById("eightPop");
|
||||
if (ageGate && ageGate.classList.contains("is-open")) {
|
||||
document.addEventListener("eightpop:closed", openMask, { once: true });
|
||||
return;
|
||||
}
|
||||
openMask();
|
||||
}
|
||||
|
||||
function waitForImage(image) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (image.complete) {
|
||||
if (image.naturalWidth > 0) resolve();
|
||||
else reject(new Error("image decode failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
var timeout = window.setTimeout(function () {
|
||||
cleanup();
|
||||
reject(new Error("image load timeout"));
|
||||
}, 12000);
|
||||
|
||||
function cleanup() {
|
||||
window.clearTimeout(timeout);
|
||||
image.removeEventListener("load", handleLoad);
|
||||
image.removeEventListener("error", handleError);
|
||||
}
|
||||
|
||||
function handleLoad() {
|
||||
cleanup();
|
||||
if (image.naturalWidth > 0) resolve();
|
||||
else reject(new Error("image decode failed"));
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
cleanup();
|
||||
reject(new Error("image load failed"));
|
||||
}
|
||||
|
||||
image.addEventListener("load", handleLoad, { once: true });
|
||||
image.addEventListener("error", handleError, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function loadDialogImage(image) {
|
||||
var source = image.getAttribute("data-dialog-ad-src") || "";
|
||||
if (!source) return Promise.reject(new Error("image source is empty"));
|
||||
|
||||
if (typeof window.processImage === "function") {
|
||||
return window.processImage(source, image).then(function (result) {
|
||||
if (!result) throw new Error("image decrypt failed");
|
||||
return waitForImage(image);
|
||||
});
|
||||
}
|
||||
|
||||
image.src = source;
|
||||
return waitForImage(image);
|
||||
}
|
||||
|
||||
function showItem(index) {
|
||||
if (index >= items.length) {
|
||||
finishQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
currentIndex = index;
|
||||
rememberProgress(currentIndex);
|
||||
var item = items[currentIndex];
|
||||
var image = item.querySelector(".dialog-ad-image");
|
||||
|
||||
function displayItem() {
|
||||
if (!setActiveItem(currentIndex)) return;
|
||||
hasDisplayedItem = true;
|
||||
showWhenAgeGateAllows();
|
||||
}
|
||||
|
||||
function skipBrokenItem() {
|
||||
showItem(currentIndex + 1);
|
||||
}
|
||||
|
||||
if (!image) return skipBrokenItem();
|
||||
loadDialogImage(image).then(displayItem).catch(skipBrokenItem);
|
||||
}
|
||||
|
||||
function showNextItem() {
|
||||
showItem(currentIndex + 1);
|
||||
}
|
||||
|
||||
mask.addEventListener("click", function (event) {
|
||||
if (event.target.closest("[data-dialog-ad-close]")) {
|
||||
showNextItem();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
showNextItem();
|
||||
}
|
||||
});
|
||||
|
||||
if (isDismissed()) return;
|
||||
if (currentIndex >= items.length) {
|
||||
finishQueue();
|
||||
return;
|
||||
}
|
||||
showItem(currentIndex);
|
||||
})();
|
||||
@@ -0,0 +1,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);
|
||||
@@ -0,0 +1,302 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("downloadAppDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var pcQRBox = mask.querySelector("[data-download-qr-box]");
|
||||
var h5QRBox = null;
|
||||
var h5URLEl = mask.querySelector("[data-download-url]");
|
||||
var downloadCTA = mask.querySelector("[data-download-app-cta]");
|
||||
var copyBtn = mask.querySelector("[data-download-copy]");
|
||||
var platformBtns = mask.querySelectorAll("[data-download-platform]");
|
||||
var copyTimer = null;
|
||||
var qrReady = false;
|
||||
var qrcodeLoading = null;
|
||||
|
||||
function loadQRCodeLib() {
|
||||
if (typeof window.QRCode === "function") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (qrcodeLoading) return qrcodeLoading;
|
||||
qrcodeLoading = new Promise(function (resolve, reject) {
|
||||
var s = document.createElement("script");
|
||||
s.src = "/static/common/qrcode.min.js";
|
||||
s.async = true;
|
||||
s.onload = function () { resolve(); };
|
||||
s.onerror = function () { reject(new Error("qrcode load failed")); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return qrcodeLoading;
|
||||
}
|
||||
|
||||
function getAppConfig() {
|
||||
return window.__APP_CONFIG__ || {};
|
||||
}
|
||||
|
||||
/** 二维码、跳转和复制链接统一使用 config/info.newAddressInfo.appAddr。 */
|
||||
function getAppURL() {
|
||||
var cfg = getAppConfig();
|
||||
var fromCfg = String((cfg && cfg.appAddr) || "").trim();
|
||||
if (fromCfg) return fromCfg;
|
||||
return String(mask.getAttribute("data-download-app-url") || "").trim();
|
||||
}
|
||||
|
||||
function isHTTPURL(value) {
|
||||
return /^https?:\/\//i.test(String(value || "").trim());
|
||||
}
|
||||
|
||||
function isMobileViewport() {
|
||||
return typeof window.matchMedia === "function"
|
||||
? window.matchMedia("(max-width: 767px)").matches
|
||||
: (window.innerWidth || 0) <= 767;
|
||||
}
|
||||
|
||||
function qrLevels() {
|
||||
var QR = window.QRCode;
|
||||
if (QR && QR.CorrectLevel) {
|
||||
return [QR.CorrectLevel.L, QR.CorrectLevel.M, QR.CorrectLevel.Q, QR.CorrectLevel.H];
|
||||
}
|
||||
return [1, 0, 3, 2];
|
||||
}
|
||||
|
||||
function paintQRModel(model, size) {
|
||||
if (!model || typeof model.getModuleCount !== "function") return null;
|
||||
var n = model.getModuleCount();
|
||||
if (!n) return null;
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
var ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
var cell = size / n;
|
||||
ctx.fillStyle = "#000000";
|
||||
for (var row = 0; row < n; row++) {
|
||||
for (var col = 0; col < n; col++) {
|
||||
if (model.isDark(row, col)) {
|
||||
ctx.fillRect(
|
||||
Math.floor(col * cell),
|
||||
Math.floor(row * cell),
|
||||
Math.ceil(cell),
|
||||
Math.ceil(cell)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function encodeQRCanvas(text, size) {
|
||||
var QR = window.QRCode;
|
||||
if (typeof QR !== "function") return null;
|
||||
var holder = document.createElement("div");
|
||||
holder.style.cssText =
|
||||
"position:fixed;left:0;top:0;width:" +
|
||||
size +
|
||||
"px;height:" +
|
||||
size +
|
||||
"px;opacity:0;pointer-events:none;z-index:-1;";
|
||||
document.body.appendChild(holder);
|
||||
var levels = qrLevels();
|
||||
try {
|
||||
for (var i = 0; i < levels.length; i++) {
|
||||
holder.innerHTML = "";
|
||||
try {
|
||||
var inst = new QR(holder, {
|
||||
text: text,
|
||||
width: size,
|
||||
height: size,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: levels[i],
|
||||
});
|
||||
var painted = paintQRModel(inst && inst._oQRCode, size);
|
||||
if (painted) return painted;
|
||||
var srcCanvas = holder.querySelector("canvas");
|
||||
if (srcCanvas) {
|
||||
var copy = document.createElement("canvas");
|
||||
copy.width = size;
|
||||
copy.height = size;
|
||||
copy.getContext("2d").drawImage(srcCanvas, 0, 0, size, size);
|
||||
return copy;
|
||||
}
|
||||
} catch (e) {
|
||||
/* try next level */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
if (holder.parentNode) holder.parentNode.removeChild(holder);
|
||||
}
|
||||
}
|
||||
|
||||
function renderQR(el, text, size) {
|
||||
if (!el || !text) return false;
|
||||
el.innerHTML = "";
|
||||
var img = document.createElement("img");
|
||||
img.alt = "下载二维码";
|
||||
img.title = "黄果短剧 App 下载二维码";
|
||||
img.width = size;
|
||||
img.height = size;
|
||||
var canvas = encodeQRCanvas(text, size);
|
||||
if (canvas) {
|
||||
try {
|
||||
img.src = canvas.toDataURL("image/png");
|
||||
el.appendChild(img);
|
||||
return true;
|
||||
} catch (e) {
|
||||
el.appendChild(canvas);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
img.src =
|
||||
"https://api.qrserver.com/v1/create-qr-code/?size=" +
|
||||
size +
|
||||
"x" +
|
||||
size +
|
||||
"&margin=0&data=" +
|
||||
encodeURIComponent(text);
|
||||
el.appendChild(img);
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncDownloadUI(force) {
|
||||
var url = getAppURL();
|
||||
if (!url) return;
|
||||
|
||||
if (force || !qrReady) {
|
||||
var pcOk = renderQR(pcQRBox, url, 164);
|
||||
var h5Ok = renderQR(h5QRBox, url, 164);
|
||||
qrReady = !!(pcOk || h5Ok);
|
||||
}
|
||||
|
||||
if (h5URLEl) {
|
||||
h5URLEl.textContent = "官方网址 " + url;
|
||||
if (isHTTPURL(url)) {
|
||||
h5URLEl.href = url;
|
||||
h5URLEl.removeAttribute("aria-disabled");
|
||||
} else {
|
||||
h5URLEl.href = "#";
|
||||
h5URLEl.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
}
|
||||
if (downloadCTA) {
|
||||
downloadCTA.href = isHTTPURL(url) ? url : "#";
|
||||
downloadCTA.toggleAttribute("aria-disabled", !isHTTPURL(url));
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(event) {
|
||||
if (event) event.preventDefault();
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
loadQRCodeLib()
|
||||
.catch(function () { /* fallback image QR still works */ })
|
||||
.then(function () {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
syncDownloadUI(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
function setPlatform(platform) {
|
||||
if (platform !== "ios" && platform !== "android") return;
|
||||
currentPlatform = platform;
|
||||
platformBtns.forEach(function (btn) {
|
||||
btn.classList.toggle(
|
||||
"is-active",
|
||||
btn.getAttribute("data-download-platform") === platform
|
||||
);
|
||||
});
|
||||
if (copyBtn) {
|
||||
copyBtn.textContent = "复制下载链接";
|
||||
copyBtn.classList.remove("is-copied");
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
var url = getAppURL();
|
||||
if (!url || !copyBtn) return;
|
||||
|
||||
function onSuccess() {
|
||||
copyBtn.textContent = "已复制";
|
||||
copyBtn.classList.add("is-copied");
|
||||
if (copyTimer) clearTimeout(copyTimer);
|
||||
copyTimer = setTimeout(function () {
|
||||
copyBtn.textContent = "复制下载链接";
|
||||
copyBtn.classList.remove("is-copied");
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(url).then(onSuccess).catch(function () {
|
||||
fallbackCopy(url, onSuccess);
|
||||
});
|
||||
return;
|
||||
}
|
||||
fallbackCopy(url, onSuccess);
|
||||
}
|
||||
|
||||
function fallbackCopy(text, onSuccess) {
|
||||
var input = document.createElement("input");
|
||||
input.value = text;
|
||||
input.setAttribute("readonly", "");
|
||||
input.style.position = "fixed";
|
||||
input.style.left = "-9999px";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var openTrigger = event.target.closest("[data-download-app-open]");
|
||||
if (openTrigger) {
|
||||
// 移动端由带 appAddr 的原生链接直接跳转,不展示二维码弹窗。
|
||||
if (isMobileViewport()) return;
|
||||
openDialog(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-download-app-close]")) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === mask) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
var platformBtn = event.target.closest("[data-download-platform]");
|
||||
if (platformBtn && mask.contains(platformBtn)) {
|
||||
setPlatform(platformBtn.getAttribute("data-download-platform"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-download-copy]")) {
|
||||
copyLink();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" && mask.classList.contains("is-open")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
window.openDownloadAppDialog = openDialog;
|
||||
window.closeDownloadAppDialog = closeDialog;
|
||||
})();
|
||||
@@ -0,0 +1,62 @@
|
||||
(function () {
|
||||
var SESSION_KEY = "ageConfirmed";
|
||||
var mask = document.getElementById("eightPop");
|
||||
if (!mask) return;
|
||||
|
||||
function getSessionValue(key) {
|
||||
try {
|
||||
return window.sessionStorage.getItem(key) || "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionValue(key, value) {
|
||||
try {
|
||||
window.sessionStorage.setItem(key, value);
|
||||
} catch (e) {
|
||||
// sessionStorage 不可用时,本页仍允许正常确认并关闭弹窗。
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
mask.hidden = false;
|
||||
mask.removeAttribute("hidden");
|
||||
mask.classList.add("is-open");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
mask.hidden = true;
|
||||
mask.setAttribute("hidden", "");
|
||||
document.body.style.overflow = "";
|
||||
document.dispatchEvent(new CustomEvent("eightpop:closed"));
|
||||
}
|
||||
|
||||
function confirmAge() {
|
||||
setSessionValue(SESSION_KEY, "1");
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function exitSite() {
|
||||
try {
|
||||
window.location.replace("about:blank");
|
||||
} catch (e) {
|
||||
window.location.href = "about:blank";
|
||||
}
|
||||
}
|
||||
|
||||
var confirmBtn = mask.querySelector("[data-eight-pop-confirm]");
|
||||
var exitBtns = mask.querySelectorAll("[data-eight-pop-exit]");
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener("click", confirmAge);
|
||||
}
|
||||
exitBtns.forEach(function (button) {
|
||||
button.addEventListener("click", exitSite);
|
||||
});
|
||||
|
||||
if (getSessionValue(SESSION_KEY) !== "1") {
|
||||
openDialog();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,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);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function fallbackCopy(value) {
|
||||
var input = document.createElement("textarea");
|
||||
input.value = value;
|
||||
input.setAttribute("readonly", "");
|
||||
input.style.position = "fixed";
|
||||
input.style.opacity = "0";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
var copied = document.execCommand("copy");
|
||||
input.remove();
|
||||
return copied;
|
||||
}
|
||||
|
||||
function copyValue(value) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(value);
|
||||
}
|
||||
return fallbackCopy(value) ? Promise.resolve() : Promise.reject(new Error("copy failed"));
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
var button = event.target.closest("[data-copy-value]");
|
||||
if (!button) return;
|
||||
var value = button.getAttribute("data-copy-value") || window.location.href;
|
||||
var original = button.textContent;
|
||||
copyValue(value).then(function () {
|
||||
button.textContent = "已复制";
|
||||
window.setTimeout(function () { button.textContent = original; }, 1600);
|
||||
}).catch(function () {
|
||||
button.textContent = "复制失败";
|
||||
window.setTimeout(function () { button.textContent = original; }, 1600);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,255 @@
|
||||
(function () {
|
||||
var H5_MQ = "(max-width: 767px)";
|
||||
var isH5 = !!(window.matchMedia && window.matchMedia(H5_MQ).matches);
|
||||
|
||||
var announcementTrack = document.querySelector(".home-announcement-marquee");
|
||||
if (announcementTrack) {
|
||||
var announcementCopies = announcementTrack.querySelectorAll(".home-announcement-copy");
|
||||
var announcementViewport = announcementTrack.parentElement;
|
||||
if (announcementCopies.length === 2 && announcementViewport) {
|
||||
var primaryAnnouncement = announcementCopies[0];
|
||||
var duplicateAnnouncement = announcementCopies[1];
|
||||
var announcementSourceNodes = Array.prototype.map.call(
|
||||
primaryAnnouncement.childNodes,
|
||||
function (node) {
|
||||
return node.cloneNode(true);
|
||||
}
|
||||
);
|
||||
|
||||
function fillAnnouncementTrack() {
|
||||
var rounds = 0;
|
||||
while (
|
||||
primaryAnnouncement.getBoundingClientRect().width <
|
||||
announcementViewport.clientWidth + 80 &&
|
||||
rounds < 20
|
||||
) {
|
||||
var repeated = document.createElement("span");
|
||||
repeated.setAttribute("aria-hidden", "true");
|
||||
repeated.className = "home-announcement-repeat";
|
||||
|
||||
var separator = document.createElement("span");
|
||||
separator.className = "home-announcement-separator";
|
||||
separator.textContent = " · ";
|
||||
repeated.appendChild(separator);
|
||||
|
||||
announcementSourceNodes.forEach(function (node) {
|
||||
repeated.appendChild(node.cloneNode(true));
|
||||
});
|
||||
primaryAnnouncement.appendChild(repeated);
|
||||
rounds += 1;
|
||||
}
|
||||
duplicateAnnouncement.innerHTML = primaryAnnouncement.innerHTML;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(fillAnnouncementTrack);
|
||||
if ("ResizeObserver" in window) {
|
||||
new ResizeObserver(fillAnnouncementTrack).observe(announcementViewport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// H5:首屏只抢 LCP 主图,海报交给 IntersectionObserver,避免弱网争抢带宽
|
||||
if (isH5) {
|
||||
document.querySelectorAll(".poster-cover-img[data-lazy-eager]").forEach(function (img) {
|
||||
img.removeAttribute("data-lazy-eager");
|
||||
});
|
||||
}
|
||||
|
||||
function loadHeroSlideImages(slide) {
|
||||
if (!slide || typeof loadLazyNode !== "function") return;
|
||||
var imgs = slide.querySelectorAll(".hero-image.lazy[data-src]:not([data-decrypted])");
|
||||
Array.prototype.forEach.call(imgs, function (img) {
|
||||
loadLazyNode(img);
|
||||
});
|
||||
}
|
||||
|
||||
var firstHeroImg = document.querySelector(
|
||||
".hero-card .hero-image.lazy[data-src]:not([data-decrypted])"
|
||||
);
|
||||
if (firstHeroImg && typeof loadLazyNode === "function") {
|
||||
loadLazyNode(firstHeroImg);
|
||||
} else if (firstHeroImg && typeof processImage === "function") {
|
||||
var src = firstHeroImg.getAttribute("data-src");
|
||||
if (src) {
|
||||
firstHeroImg.setAttribute("data-decrypted", "1");
|
||||
processImage(src, firstHeroImg);
|
||||
}
|
||||
}
|
||||
|
||||
var mainEl = document.getElementById("heroSwiper");
|
||||
var thumbsEl = document.getElementById("heroThumbs");
|
||||
if (!mainEl) return;
|
||||
|
||||
var slideCount = mainEl.querySelectorAll(".swiper-slide").length;
|
||||
if (slideCount === 0) return;
|
||||
|
||||
var swiperStarted = false;
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (typeof Swiper !== "undefined") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
if (existing) {
|
||||
existing.addEventListener("load", function () {
|
||||
resolve();
|
||||
});
|
||||
existing.addEventListener("error", reject);
|
||||
return;
|
||||
}
|
||||
var s = document.createElement("script");
|
||||
s.src = src;
|
||||
s.async = true;
|
||||
s.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
function loadStylesheet(href) {
|
||||
return new Promise(function (resolve) {
|
||||
if (document.querySelector('link[href="' + href + '"][data-swiper-css]')) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
var link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.setAttribute("data-swiper-css", "1");
|
||||
link.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
link.onerror = function () {
|
||||
resolve();
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureSwiperAssets() {
|
||||
var cssHref = "/static/common/swiper-bundle.min.css";
|
||||
var jsHref = "/static/common/swiper-bundle.min.js";
|
||||
// PC 已通过 media=(min-width:768px) 预加载 CSS;H5 在此按需注入
|
||||
var tasks = [loadScript(jsHref)];
|
||||
if (isH5) {
|
||||
tasks.unshift(loadStylesheet(cssHref));
|
||||
}
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
function initSwiper() {
|
||||
if (swiperStarted || typeof Swiper === "undefined") return;
|
||||
swiperStarted = true;
|
||||
mainEl.classList.remove("hero-swiper--h5-pending");
|
||||
|
||||
var thumbButtons = thumbsEl
|
||||
? Array.prototype.slice.call(thumbsEl.querySelectorAll(".hero-thumb"))
|
||||
: [];
|
||||
|
||||
function setActiveThumb(index) {
|
||||
thumbButtons.forEach(function (btn, i) {
|
||||
btn.classList.toggle("is-active", i === index);
|
||||
});
|
||||
}
|
||||
|
||||
var enableLoop = slideCount > 1;
|
||||
|
||||
var mainSwiper = new Swiper(mainEl, {
|
||||
loop: enableLoop,
|
||||
effect: "fade",
|
||||
fadeEffect: {
|
||||
crossFade: true,
|
||||
},
|
||||
speed: isH5 ? 650 : 900,
|
||||
autoplay: enableLoop
|
||||
? {
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: !isH5,
|
||||
waitForTransition: true,
|
||||
}
|
||||
: false,
|
||||
pagination: {
|
||||
el: mainEl.querySelector(".hero-dots"),
|
||||
clickable: true,
|
||||
bulletClass: "hero-dot",
|
||||
bulletActiveClass: "hero-dot--active",
|
||||
},
|
||||
on: {
|
||||
init: function () {
|
||||
loadHeroSlideImages(this.slides[this.activeIndex]);
|
||||
},
|
||||
slideChange: function () {
|
||||
setActiveThumb(this.realIndex);
|
||||
loadHeroSlideImages(this.slides[this.activeIndex]);
|
||||
if (isH5) return;
|
||||
var next = this.slides[this.activeIndex + 1];
|
||||
var prev = this.slides[this.activeIndex - 1];
|
||||
if (next) loadHeroSlideImages(next);
|
||||
if (prev) loadHeroSlideImages(prev);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var previousButton = mainEl.querySelector(".hero-thumb-prev");
|
||||
var nextButton = mainEl.querySelector(".hero-thumb-next");
|
||||
if (previousButton) {
|
||||
previousButton.addEventListener("click", function () {
|
||||
mainSwiper.slidePrev();
|
||||
});
|
||||
}
|
||||
if (nextButton) {
|
||||
nextButton.addEventListener("click", function () {
|
||||
mainSwiper.slideNext();
|
||||
});
|
||||
}
|
||||
|
||||
thumbButtons.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var index = Number(btn.getAttribute("data-index"));
|
||||
if (Number.isNaN(index)) return;
|
||||
if (enableLoop) {
|
||||
mainSwiper.slideToLoop(index);
|
||||
} else {
|
||||
mainSwiper.slideTo(index);
|
||||
}
|
||||
setActiveThumb(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startSwiper() {
|
||||
if (swiperStarted) return;
|
||||
ensureSwiperAssets()
|
||||
.then(initSwiper)
|
||||
.catch(function () {
|
||||
mainEl.classList.remove("hero-swiper--h5-pending");
|
||||
});
|
||||
}
|
||||
|
||||
if (isH5) {
|
||||
mainEl.classList.add("hero-swiper--h5-pending");
|
||||
|
||||
function scheduleSwiper() {
|
||||
startSwiper();
|
||||
}
|
||||
|
||||
// 用户滑动/点击 banner 时立即初始化;否则空闲后再加载,避开 LCP 争抢
|
||||
mainEl.addEventListener("touchstart", scheduleSwiper, { once: true, passive: true });
|
||||
mainEl.addEventListener("click", scheduleSwiper, { once: true });
|
||||
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(scheduleSwiper, { timeout: 3500 });
|
||||
} else {
|
||||
setTimeout(scheduleSwiper, 2200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// PC:立即加载并初始化 Swiper
|
||||
startSwiper();
|
||||
})();
|
||||
@@ -0,0 +1,230 @@
|
||||
(function () {
|
||||
var panel = document.querySelector("[data-mobile-search-landing]");
|
||||
var mobileMedia = window.matchMedia && window.matchMedia("(max-width: 767px)");
|
||||
if (!panel || !mobileMedia || !mobileMedia.matches) return;
|
||||
|
||||
var PLACEHOLDER = "/static/images/placeholder.png?v=2";
|
||||
var historySection = panel.querySelector("[data-mobile-search-history]");
|
||||
var historyList = panel.querySelector("[data-mobile-search-history-list]");
|
||||
var clearButton = panel.querySelector("[data-mobile-search-history-clear]");
|
||||
var hotSection = panel.querySelector("[data-mobile-search-hot]");
|
||||
var hotList = panel.querySelector("[data-mobile-search-hot-list]");
|
||||
var guessSection = panel.querySelector("[data-mobile-search-guess]");
|
||||
var guessList = panel.querySelector("[data-mobile-search-guess-list]");
|
||||
var changeButton = panel.querySelector("[data-mobile-search-guess-change]");
|
||||
var historyDivider = panel.querySelector('[data-mobile-search-divider="history"]');
|
||||
var hotDivider = panel.querySelector('[data-mobile-search-divider="hot"]');
|
||||
var hotLikeData = {};
|
||||
var guessOffset = 0;
|
||||
|
||||
function searchKeyword(keyword) {
|
||||
keyword = String(keyword || "").trim();
|
||||
if (!keyword) return;
|
||||
if (window.SearchHistory && typeof window.SearchHistory.add === "function") {
|
||||
window.SearchHistory.add(keyword);
|
||||
}
|
||||
window.location.href = "/search/" + encodeURIComponent(keyword) + "/";
|
||||
}
|
||||
|
||||
function syncDividers() {
|
||||
var hasHistory = historySection && !historySection.hidden;
|
||||
var hasHot = hotSection && !hotSection.hidden;
|
||||
var hasGuess = guessSection && !guessSection.hidden;
|
||||
if (historyDivider) historyDivider.hidden = !(hasHistory && (hasHot || hasGuess));
|
||||
if (hotDivider) hotDivider.hidden = !(hasHot && hasGuess);
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (!historyList || !historySection) return;
|
||||
var history = window.SearchHistory && typeof window.SearchHistory.get === "function"
|
||||
? window.SearchHistory.get()
|
||||
: [];
|
||||
historyList.textContent = "";
|
||||
historySection.hidden = history.length === 0;
|
||||
history.forEach(function (keyword) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "mobile-search-history-tag";
|
||||
button.textContent = keyword;
|
||||
button.addEventListener("click", function () {
|
||||
searchKeyword(keyword);
|
||||
});
|
||||
historyList.appendChild(button);
|
||||
});
|
||||
syncDividers();
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
var count = Math.max(0, Number(value) || 0);
|
||||
if (count >= 10000) {
|
||||
return (count / 10000).toFixed(1).replace(/\.0$/, "") + "w";
|
||||
}
|
||||
return String(count);
|
||||
}
|
||||
|
||||
function renderHotSearch() {
|
||||
if (!hotList || !hotSection) return;
|
||||
var source = Array.isArray(hotLikeData.hotSearch) ? hotLikeData.hotSearch : [];
|
||||
var items = source.filter(function (item) {
|
||||
return item && String(item.keyword || "").trim();
|
||||
}).slice(0, 6);
|
||||
hotList.textContent = "";
|
||||
hotSection.hidden = items.length === 0;
|
||||
items.forEach(function (item, index) {
|
||||
var button = document.createElement("button");
|
||||
var rank = document.createElement("span");
|
||||
var keyword = document.createElement("span");
|
||||
var count = document.createElement("span");
|
||||
var keywordText = String(item.keyword || "").trim();
|
||||
|
||||
button.type = "button";
|
||||
button.className = "mobile-search-hot-item";
|
||||
rank.className = "mobile-search-hot-rank";
|
||||
keyword.className = "mobile-search-hot-keyword";
|
||||
count.className = "mobile-search-hot-count";
|
||||
rank.textContent = String(index + 1);
|
||||
keyword.textContent = keywordText;
|
||||
count.textContent = formatCount(item.count);
|
||||
button.appendChild(rank);
|
||||
button.appendChild(keyword);
|
||||
button.appendChild(count);
|
||||
button.addEventListener("click", function () {
|
||||
searchKeyword(keywordText);
|
||||
});
|
||||
hotList.appendChild(button);
|
||||
});
|
||||
syncDividers();
|
||||
}
|
||||
|
||||
function detailHref(item) {
|
||||
var slug = String((item && item.slug) || "").trim();
|
||||
var id = Number(item && item.id);
|
||||
var key = slug || (id > 0 ? String(id) : "");
|
||||
return key ? "/detail/" + encodeURIComponent(key) + "/" : "#";
|
||||
}
|
||||
|
||||
function firstCardTag(item) {
|
||||
var tags = item && Array.isArray(item.tagNames) ? item.tagNames : [];
|
||||
var themes = item && Array.isArray(item.themeNames) ? item.themeNames : [];
|
||||
return String(tags[0] || themes[0] || "热门").trim();
|
||||
}
|
||||
|
||||
function setCardCover(image, rawURL, title) {
|
||||
var cover = String(rawURL || "").trim();
|
||||
image.className = "mobile-search-guess-image";
|
||||
image.alt = title;
|
||||
image.title = title;
|
||||
image.addEventListener("error", function () {
|
||||
image.classList.remove("lazy");
|
||||
image.removeAttribute("data-src");
|
||||
image.src = PLACEHOLDER;
|
||||
}, { once: true });
|
||||
if (cover && typeof window.shouldDecryptImage === "function" && window.shouldDecryptImage(cover)) {
|
||||
image.classList.add("lazy");
|
||||
image.src = PLACEHOLDER;
|
||||
image.setAttribute("data-src", cover);
|
||||
return;
|
||||
}
|
||||
image.src = cover || PLACEHOLDER;
|
||||
}
|
||||
|
||||
function createGuessCard(item) {
|
||||
var card = document.createElement("a");
|
||||
var cover = document.createElement("div");
|
||||
var image = document.createElement("img");
|
||||
var episode = document.createElement("span");
|
||||
var copy = document.createElement("div");
|
||||
var title = document.createElement("div");
|
||||
var desc = document.createElement("div");
|
||||
var tagRow = document.createElement("div");
|
||||
var tag = document.createElement("span");
|
||||
var titleText = String((item && item.title) || "精彩短剧");
|
||||
var totalEpisode = Math.max(0, Number(item && item.totalEpisode) || 0);
|
||||
|
||||
card.className = "mobile-search-guess-card";
|
||||
card.href = detailHref(item);
|
||||
card.setAttribute("aria-label", titleText);
|
||||
cover.className = "mobile-search-guess-cover";
|
||||
episode.className = "mobile-search-guess-episode";
|
||||
copy.className = "mobile-search-guess-copy";
|
||||
title.className = "mobile-search-guess-title";
|
||||
desc.className = "mobile-search-guess-desc";
|
||||
tagRow.className = "mobile-search-guess-tag-row";
|
||||
tag.className = "mobile-search-guess-tag";
|
||||
|
||||
setCardCover(image, (item && (item.cover || item.banner)) || "", titleText);
|
||||
episode.textContent = totalEpisode ? totalEpisode + "集" : "更新中";
|
||||
title.textContent = titleText;
|
||||
desc.textContent = String((item && item.desc) || "精彩内容等你来看");
|
||||
tag.textContent = firstCardTag(item);
|
||||
|
||||
cover.appendChild(image);
|
||||
cover.appendChild(episode);
|
||||
tagRow.appendChild(tag);
|
||||
copy.appendChild(title);
|
||||
copy.appendChild(desc);
|
||||
copy.appendChild(tagRow);
|
||||
card.appendChild(cover);
|
||||
card.appendChild(copy);
|
||||
return card;
|
||||
}
|
||||
|
||||
function guessItems() {
|
||||
var source = Array.isArray(hotLikeData.guessLike) ? hotLikeData.guessLike : [];
|
||||
return source.filter(function (item) {
|
||||
return item && (item.id || item.slug);
|
||||
});
|
||||
}
|
||||
|
||||
function renderGuessLike() {
|
||||
if (!guessList || !guessSection) return;
|
||||
var items = guessItems();
|
||||
var count = Math.min(4, items.length);
|
||||
guessList.textContent = "";
|
||||
guessSection.hidden = count === 0;
|
||||
for (var index = 0; index < count; index += 1) {
|
||||
guessList.appendChild(createGuessCard(items[(guessOffset + index) % items.length]));
|
||||
}
|
||||
if (count && typeof window.initLazyImages === "function") {
|
||||
window.initLazyImages(guessList);
|
||||
}
|
||||
syncDividers();
|
||||
}
|
||||
|
||||
function renderHotLike() {
|
||||
renderHotSearch();
|
||||
renderGuessLike();
|
||||
}
|
||||
|
||||
renderHistory();
|
||||
renderHotLike();
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function () {
|
||||
if (window.SearchHistory && typeof window.SearchHistory.clear === "function") {
|
||||
window.SearchHistory.clear();
|
||||
}
|
||||
renderHistory();
|
||||
});
|
||||
}
|
||||
|
||||
if (changeButton) {
|
||||
changeButton.addEventListener("click", function () {
|
||||
var items = guessItems();
|
||||
if (items.length > 4) guessOffset = (guessOffset + 4) % items.length;
|
||||
renderGuessLike();
|
||||
});
|
||||
}
|
||||
|
||||
if (window.HttpRequest && typeof window.HttpRequest.get === "function") {
|
||||
window.HttpRequest.get("/api/playlet/hot-like")
|
||||
.then(function (response) {
|
||||
hotLikeData = response && response.data ? response.data : {};
|
||||
renderHotLike();
|
||||
})
|
||||
.catch(function () {
|
||||
hotLikeData = {};
|
||||
renderHotLike();
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,43 @@
|
||||
(function () {
|
||||
if (window.__paginationJumpBound) {
|
||||
return;
|
||||
}
|
||||
window.__paginationJumpBound = true;
|
||||
|
||||
function clampPage(value, total) {
|
||||
var page = parseInt(value, 10);
|
||||
if (!page || page < 1) {
|
||||
return 0;
|
||||
}
|
||||
if (total > 0 && page > total) {
|
||||
return total;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
function buildPageURL(page) {
|
||||
var url = new URL(window.location.href);
|
||||
url.pathname = url.pathname.replace(/\/page\/\d+\/?$/, "").replace(/\/$/, "");
|
||||
url.searchParams.delete("page");
|
||||
if (page > 1) url.pathname += "/page/" + page;
|
||||
return url.pathname + url.search + url.hash;
|
||||
}
|
||||
|
||||
document.addEventListener("submit", function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.matches || !form.matches("[data-pagination-jump]")) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
var nav = form.closest("[data-pagination]");
|
||||
var input = form.querySelector(".pagination-jump-input");
|
||||
var total = nav ? parseInt(nav.getAttribute("data-total"), 10) : 0;
|
||||
var current = nav ? parseInt(nav.getAttribute("data-page"), 10) : 0;
|
||||
var page = clampPage(input && input.value, total);
|
||||
if (!page || page === current) {
|
||||
return;
|
||||
}
|
||||
window.location.href = buildPageURL(page);
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
(function () {
|
||||
var mask = document.getElementById("searchDialog");
|
||||
if (!mask) return;
|
||||
|
||||
var headerForm = document.querySelector("form.header-search");
|
||||
var headerInput = headerForm
|
||||
? headerForm.querySelector('input[name="q"]')
|
||||
: null;
|
||||
|
||||
var input = mask.querySelector('[data-search-dialog-input]');
|
||||
var form = mask.querySelector('[data-search-dialog-form]');
|
||||
|
||||
function isMobile() {
|
||||
try {
|
||||
return window.matchMedia("(max-width: 767px)").matches;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(initialValue) {
|
||||
if (initialValue != null && input) {
|
||||
input.value = String(initialValue || "");
|
||||
}
|
||||
mask.classList.add("is-open");
|
||||
mask.setAttribute("aria-hidden", "false");
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
if (input) input.focus();
|
||||
if (input && typeof input.select === "function") {
|
||||
input.select();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
mask.classList.remove("is-open");
|
||||
mask.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
window.openSearchDialog = openDialog;
|
||||
window.closeSearchDialog = closeDialog;
|
||||
|
||||
if (mask) {
|
||||
mask.addEventListener("click", function (e) {
|
||||
if (e.target !== mask) return;
|
||||
closeDialog();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (!mask.classList.contains("is-open")) return;
|
||||
if (e.key === "Escape") closeDialog();
|
||||
});
|
||||
|
||||
// 移动端:点击头部搜索栏 → 打开搜索弹窗,而不是直接跳转
|
||||
if (headerForm) {
|
||||
headerForm.addEventListener("submit", function (e) {
|
||||
if (!isMobile()) return;
|
||||
e.preventDefault();
|
||||
var v = headerInput ? headerInput.value : "";
|
||||
openDialog(v);
|
||||
});
|
||||
|
||||
headerForm.addEventListener("click", function (e) {
|
||||
if (!isMobile()) return;
|
||||
// 避免点到表单外或触发非预期行为
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var v = headerInput ? headerInput.value : "";
|
||||
openDialog(v);
|
||||
});
|
||||
}
|
||||
|
||||
// 提交时如果是空关键字,不跳转(避免触发接口报参数错误等情况)
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
var q = input ? String(input.value || "").trim() : "";
|
||||
if (!q) {
|
||||
e.preventDefault();
|
||||
if (input) input.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,76 @@
|
||||
(function () {
|
||||
var host = document.getElementById("toastHost");
|
||||
if (!host) return;
|
||||
|
||||
var ICONS = {
|
||||
info:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.5"/><path d="M8 7.2V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="5" r="0.9" fill="currentColor"/></svg>',
|
||||
success:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3.5 8.2l3 3.1 6-6.3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
error:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M5 5l6 6M11 5l-6 6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||
warning:
|
||||
'<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 4.2v5.2" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><circle cx="8" cy="11.4" r="0.95" fill="currentColor"/></svg>',
|
||||
};
|
||||
|
||||
var DEFAULT_DURATION = 2400;
|
||||
var MAX_VISIBLE = 3;
|
||||
|
||||
function normalizeType(type) {
|
||||
if (type === "success" || type === "error" || type === "warning" || type === "info") {
|
||||
return type;
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function showToast(message, options) {
|
||||
if (!message) return null;
|
||||
|
||||
var opts = typeof options === "string" ? { type: options } : options || {};
|
||||
var type = normalizeType(opts.type);
|
||||
var duration = typeof opts.duration === "number" ? opts.duration : DEFAULT_DURATION;
|
||||
|
||||
while (host.children.length >= MAX_VISIBLE) {
|
||||
var oldest = host.firstElementChild;
|
||||
if (oldest) oldest.remove();
|
||||
}
|
||||
|
||||
var item = document.createElement("div");
|
||||
item.className = "toast-item toast-item--" + type;
|
||||
item.setAttribute("role", "status");
|
||||
|
||||
var icon = document.createElement("span");
|
||||
icon.className = "toast-icon";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.innerHTML = ICONS[type] || ICONS.info;
|
||||
|
||||
var text = document.createElement("span");
|
||||
text.className = "toast-text";
|
||||
text.textContent = String(message);
|
||||
|
||||
item.appendChild(icon);
|
||||
item.appendChild(text);
|
||||
host.appendChild(item);
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
item.classList.add("is-visible");
|
||||
});
|
||||
|
||||
var hideTimer = setTimeout(function () {
|
||||
item.classList.add("is-leaving");
|
||||
item.classList.remove("is-visible");
|
||||
setTimeout(function () {
|
||||
if (item.parentNode) item.parentNode.removeChild(item);
|
||||
}, 260);
|
||||
}, Math.max(800, duration));
|
||||
|
||||
return {
|
||||
close: function () {
|
||||
clearTimeout(hideTimer);
|
||||
if (item.parentNode) item.parentNode.removeChild(item);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.showToast = showToast;
|
||||
})();
|
||||
@@ -0,0 +1,30 @@
|
||||
(function () {
|
||||
var scrollToTopButton = document.getElementById("scrollToTopButton");
|
||||
if (!scrollToTopButton) return;
|
||||
|
||||
var ticking = false;
|
||||
var lastVisible = null;
|
||||
|
||||
function updateVisibility() {
|
||||
ticking = false;
|
||||
// 用 visualViewport / 近似阈值,避免反复读 layout 几何属性触发强制重排
|
||||
var threshold = Math.max(window.innerHeight || 600, 600);
|
||||
var shouldShow = (window.scrollY || window.pageYOffset || 0) > threshold;
|
||||
if (shouldShow === lastVisible) return;
|
||||
lastVisible = shouldShow;
|
||||
scrollToTopButton.classList.toggle("is-visible", shouldShow);
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
window.requestAnimationFrame(updateVisibility);
|
||||
}
|
||||
|
||||
scrollToTopButton.addEventListener("click", function () {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.requestAnimationFrame(updateVisibility);
|
||||
})();
|
||||
@@ -0,0 +1,46 @@
|
||||
(function () {
|
||||
function setupRoot(root) {
|
||||
var track = root.querySelector(".square-promo__track");
|
||||
var group = root.querySelector(".square-promo__group");
|
||||
if (!track || !group) return;
|
||||
|
||||
var clone = root.querySelector(".square-promo__group--clone");
|
||||
if (clone) clone.remove();
|
||||
root.classList.remove("is-marquee");
|
||||
track.style.animationDuration = "";
|
||||
|
||||
if (group.scrollWidth <= root.clientWidth + 1) return;
|
||||
|
||||
clone = group.cloneNode(true);
|
||||
clone.classList.add("square-promo__group--clone");
|
||||
clone.setAttribute("aria-hidden", "true");
|
||||
clone.querySelectorAll("a").forEach(function (link) {
|
||||
link.setAttribute("tabindex", "-1");
|
||||
});
|
||||
track.appendChild(clone);
|
||||
root.classList.add("is-marquee");
|
||||
track.style.animationDuration =
|
||||
Math.max(18, Math.round(group.scrollWidth / 36)) + "s";
|
||||
if (typeof window.initLazyImages === "function") {
|
||||
window.initLazyImages(clone);
|
||||
}
|
||||
}
|
||||
|
||||
function setupAll() {
|
||||
document.querySelectorAll("[data-square-promo]").forEach(setupRoot);
|
||||
}
|
||||
|
||||
var resizeTimer = 0;
|
||||
|
||||
function onResize() {
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(setupAll, 160);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", setupAll, { once: true });
|
||||
} else {
|
||||
setupAll();
|
||||
}
|
||||
window.addEventListener("resize", onResize);
|
||||
})();
|
||||
Reference in New Issue
Block a user