mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-04-07 09:29:33 +08:00
856 lines
29 KiB
JavaScript
856 lines
29 KiB
JavaScript
// -- 全局常量与状态 --
|
|
const ITEMS_PER_PAGE = 10;
|
|
const platformResults = new Map();
|
|
|
|
// -- DOM 元素获取 --
|
|
const searchForm = document.getElementById("searchForm");
|
|
const resultsDiv = document.getElementById("results");
|
|
const errorDiv = document.getElementById("error");
|
|
const progressBar = document.getElementById("progressBar");
|
|
const searchBtn = document.getElementById("searchBtn");
|
|
const searchBtnText = document.getElementById("searchBtnText");
|
|
const searchIcon = searchBtn?.querySelector("i");
|
|
const customApiInput = document.getElementById("customApi");
|
|
|
|
let siteNavigationDiv;
|
|
let toggleNavButton;
|
|
let navLinksContainer;
|
|
let isNavCollapsed = false; // Track navigation state for mobile (now largely ignored for mobile)
|
|
let isMobileView = false; // Track if we are in mobile view
|
|
let siteNavOriginalTop = 0; // Store the original top position of the navigation bar (less relevant for fixed bottom)
|
|
let scrollbarWidth = 0; // Store calculated scrollbar width
|
|
|
|
// Scroll to top functionality
|
|
const scrollToTopBtn = document.getElementById("scrollToTopBtn");
|
|
const scrollToCommentsBtn = document.getElementById("scrollToCommentsBtn"); // Get the comments button
|
|
|
|
window.addEventListener("scroll", () => {
|
|
if (window.scrollY > 200) {
|
|
// Show buttons after scrolling down 200px
|
|
scrollToTopBtn.classList.add("flex");
|
|
scrollToTopBtn.classList.remove("hidden");
|
|
scrollToCommentsBtn.classList.add("flex"); // Show comments button
|
|
scrollToCommentsBtn.classList.remove("hidden");
|
|
} else {
|
|
scrollToTopBtn.classList.add("hidden");
|
|
scrollToTopBtn.classList.remove("flex");
|
|
scrollToCommentsBtn.classList.add("hidden"); // Hide comments button
|
|
scrollToCommentsBtn.classList.remove("flex");
|
|
}
|
|
});
|
|
|
|
// Initialize button visibility on page load (in case user refreshes scrolled down)
|
|
// This also handles the case where the user might load the page already scrolled
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
if (window.scrollY > 200) {
|
|
scrollToTopBtn.classList.add("flex");
|
|
scrollToTopBtn.classList.remove("hidden");
|
|
scrollToCommentsBtn.classList.add("flex");
|
|
scrollToCommentsBtn.classList.remove("hidden");
|
|
} else {
|
|
scrollToTopBtn.classList.add("hidden");
|
|
scrollToTopBtn.classList.remove("flex");
|
|
scrollToCommentsBtn.classList.add("hidden");
|
|
scrollToCommentsBtn.classList.remove("flex");
|
|
}
|
|
});
|
|
|
|
scrollToTopBtn.addEventListener("click", () => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: "smooth",
|
|
});
|
|
});
|
|
|
|
// Smooth scroll to comments
|
|
scrollToCommentsBtn.addEventListener("click", (e) => {
|
|
e.preventDefault(); // Prevent default anchor jump
|
|
const commentsSection = document.getElementById("Comments");
|
|
if (commentsSection) {
|
|
commentsSection.scrollIntoView({
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 页面加载后初始化
|
|
*/
|
|
window.addEventListener("DOMContentLoaded", () => {
|
|
if (searchBtn) searchBtn.disabled = false;
|
|
|
|
const magicCheckbox = document.getElementById("magicAccess");
|
|
if (magicCheckbox) {
|
|
magicCheckbox.checked = true;
|
|
}
|
|
|
|
quicklink.listen({ priority: true });
|
|
Artalk.init({
|
|
el: "#Comments",
|
|
pageKey: "https://searchgal.homes", // Original domain from user's file
|
|
server: "https://artalk.saop.cc",
|
|
site: "Galgame 聚合搜索",
|
|
});
|
|
|
|
siteNavigationDiv = document.createElement("div");
|
|
siteNavigationDiv.id = "siteNavigation";
|
|
// Initial classes are minimal, updateNavigationLayout will set full classes
|
|
siteNavigationDiv.className =
|
|
"z-20 flex flex-col items-center animate__animated animate__fadeInUp animate__faster";
|
|
document.body.appendChild(siteNavigationDiv);
|
|
|
|
navLinksContainer = document.createElement("div");
|
|
// Changed to fixed height, horizontal scroll, no wrap
|
|
// Increased horizontal padding (px-2 to px-4) to prevent focus ring clipping
|
|
navLinksContainer.className =
|
|
"nav-links-container flex flex-nowrap overflow-x-auto gap-2 items-center w-full h-12 px-4";
|
|
siteNavigationDiv.appendChild(navLinksContainer);
|
|
|
|
toggleNavButton = document.createElement("button");
|
|
toggleNavButton.id = "toggleNavButton";
|
|
// Always hidden as mobile collapse is removed
|
|
toggleNavButton.className = "hidden"; // Always hidden
|
|
toggleNavButton.innerHTML = '<i class="fas fa-chevron-down"></i>';
|
|
siteNavigationDiv.appendChild(toggleNavButton);
|
|
|
|
siteNavigationDiv.addEventListener("click", (e) => {
|
|
const targetLink = e.target.closest('a[href^="#"]');
|
|
if (targetLink) {
|
|
e.preventDefault();
|
|
const targetId = targetLink.getAttribute("href").substring(1);
|
|
const targetElement = document.getElementById(targetId);
|
|
if (targetElement) {
|
|
const viewportHeight = window.innerHeight;
|
|
const scrollOffset = viewportHeight * 0.25;
|
|
|
|
const elementPosition =
|
|
targetElement.getBoundingClientRect().top + window.scrollY;
|
|
window.scrollTo({
|
|
top: elementPosition - scrollOffset,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Calculate scrollbar width once
|
|
scrollbarWidth = getScrollbarWidth();
|
|
|
|
// Initial layout update and event listeners
|
|
updateNavigationLayout();
|
|
updateSiteNavigation(); // Call to set initial visibility
|
|
adjustBodyPaddingForScrollbar(); // Adjust padding on initial load
|
|
|
|
if (searchForm) {
|
|
searchForm.addEventListener("submit", handleSearchSubmit);
|
|
}
|
|
if (resultsDiv) {
|
|
resultsDiv.addEventListener("click", handlePaginationClick);
|
|
}
|
|
|
|
window.addEventListener(
|
|
"resize",
|
|
debounce(() => {
|
|
scrollbarWidth = getScrollbarWidth(); // Recalculate scrollbar width on resize
|
|
adjustBodyPaddingForScrollbar(); // Adjust padding after resize
|
|
updateNavigationLayout(); // Update layout (fixed/absolute, bottom/top)
|
|
updateSiteNavigationWidth(); // Update width
|
|
}, 200)
|
|
);
|
|
|
|
window.addEventListener("scroll", debounce(handleScroll, 10));
|
|
});
|
|
|
|
/**
|
|
* Calculates the width of the scrollbar.
|
|
* @returns {number} The width of the scrollbar in pixels.
|
|
*/
|
|
function getScrollbarWidth() {
|
|
// Create a temporary div to measure scrollbar width
|
|
const outer = document.createElement("div");
|
|
outer.style.visibility = "hidden";
|
|
outer.style.overflow = "scroll"; // Force scrollbar
|
|
outer.style.msOverflowStyle = "scrollbar"; // For IE11
|
|
document.body.appendChild(outer);
|
|
|
|
const inner = document.createElement("div");
|
|
outer.appendChild(inner);
|
|
|
|
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
|
|
|
outer.parentNode.removeChild(outer);
|
|
|
|
return scrollbarWidth;
|
|
}
|
|
|
|
/**
|
|
* Adjusts body padding to prevent content shifting when scrollbar appears/disappears.
|
|
*/
|
|
function adjustBodyPaddingForScrollbar() {
|
|
if (isMobileView) {
|
|
// Do not adjust for mobile, as scrollbars are often overlaid
|
|
document.body.style.paddingRight = "";
|
|
return;
|
|
}
|
|
|
|
const hasScrollbar = document.body.scrollHeight > window.innerHeight;
|
|
const currentPaddingRight =
|
|
parseInt(getComputedStyle(document.body).paddingRight, 10) || 0;
|
|
|
|
if (hasScrollbar && currentPaddingRight === 0) {
|
|
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
} else if (!hasScrollbar && currentPaddingRight === scrollbarWidth) {
|
|
document.body.style.paddingRight = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理页面滚动事件 (不再用于桌面导航栏固定,仅用于滚动条调整)
|
|
*/
|
|
function handleScroll() {
|
|
adjustBodyPaddingForScrollbar();
|
|
}
|
|
|
|
/**
|
|
* 根据屏幕宽度调整导航栏布局 (fixed bottom)
|
|
*/
|
|
function updateNavigationLayout() {
|
|
if (!siteNavigationDiv || !resultsDiv) return;
|
|
|
|
const breakpoint = 768; // Tailwind's 'md' breakpoint
|
|
isMobileView = window.innerWidth < breakpoint;
|
|
|
|
// Clear all existing classes to ensure a clean slate
|
|
siteNavigationDiv.className = "";
|
|
|
|
// Apply base classes for fixed bottom, centered, full width, padding
|
|
siteNavigationDiv.classList.add(
|
|
"z-20",
|
|
"flex",
|
|
"flex-col",
|
|
"items-center",
|
|
"animate__animated",
|
|
"animate__fadeInUp",
|
|
"animate__faster",
|
|
"fixed",
|
|
"bottom-0", // Stays at the very bottom
|
|
"w-full", // Full width
|
|
"px-2" // Consistent padding - changed from 'p-2' to 'px-2' to avoid top/bottom padding affecting height calculation for resultsDiv margin
|
|
);
|
|
|
|
// Apply responsive max-width and rounding
|
|
if (isMobileView) {
|
|
siteNavigationDiv.classList.add("rounded-t-xl", "max-w-md");
|
|
} else {
|
|
// For desktop, remove max-width and make it truly full viewport width
|
|
siteNavigationDiv.classList.remove("max-w-4xl"); // Remove previous max-width
|
|
siteNavigationDiv.classList.add("rounded-t-xl"); // Keep rounded top for desktop
|
|
}
|
|
|
|
// Ensure toggle button is hidden and nav links are always visible
|
|
toggleNavButton.classList.add("hidden");
|
|
navLinksContainer.classList.remove("hidden", "animate__fadeIn");
|
|
|
|
// Update resultsDiv margin to account for fixed bottom navigation
|
|
updateSiteNavigationWidth();
|
|
|
|
siteNavOriginalTop =
|
|
siteNavigationDiv.getBoundingClientRect().top + window.scrollY; // Still update, though less critical
|
|
adjustBodyPaddingForScrollbar();
|
|
}
|
|
|
|
/**
|
|
* 统一处理搜索表单提交
|
|
* @param {Event} e 事件对象
|
|
*/
|
|
async function handleSearchSubmit(e) {
|
|
e.preventDefault();
|
|
platformResults.clear();
|
|
clearUI();
|
|
|
|
resultsDiv.classList.add(
|
|
"animate__animated",
|
|
"animate__fadeOut",
|
|
"animate__faster"
|
|
);
|
|
resultsDiv.addEventListener(
|
|
"animationend",
|
|
function handler() {
|
|
this.classList.remove(
|
|
"animate__animated",
|
|
"animate__fadeOut",
|
|
"animate__faster"
|
|
);
|
|
this.removeEventListener("animationend", handler);
|
|
|
|
const formData = new FormData(searchForm);
|
|
const gameName = formData.get("game").trim();
|
|
const zypassword = formData.get("zypassword").trim();
|
|
const searchMode = formData.get("searchMode");
|
|
const magic = document.getElementById("magicAccess")?.checked || false;
|
|
const customApi = customApiInput ? customApiInput.value.trim() : "";
|
|
|
|
if (!gameName) {
|
|
showError("游戏名称不能为空");
|
|
setLoadingState(false);
|
|
return;
|
|
}
|
|
|
|
setLoadingState(true);
|
|
|
|
const searchParams = {
|
|
gameName,
|
|
zypassword,
|
|
magic,
|
|
patchMode: searchMode === "patch",
|
|
customApi,
|
|
};
|
|
|
|
let totalTasks = 0;
|
|
|
|
searchGameStream(searchParams, {
|
|
onTotal: (total) => {
|
|
totalTasks = total;
|
|
},
|
|
onProgress: (progress) => {
|
|
if (progressBar && totalTasks > 0) {
|
|
const percent = Math.min(
|
|
100,
|
|
Math.round((progress.completed / totalTasks) * 100)
|
|
);
|
|
progressBar.style.width = `${percent}%`;
|
|
}
|
|
if (searchBtnText) {
|
|
searchBtnText.textContent = `进度: ${progress.completed} / ${totalTasks}`;
|
|
}
|
|
},
|
|
onResult: (result) => {
|
|
platformResults.set(result.name, {
|
|
...result,
|
|
items: result.items || [],
|
|
currentPage: 1,
|
|
});
|
|
const platformCard = createPlatformCard(result, true);
|
|
resultsDiv.appendChild(platformCard);
|
|
updateSiteNavigation(); // Update navigation visibility and content
|
|
},
|
|
onDone: () => {
|
|
if (searchBtnText) searchBtnText.textContent = "搜索完成!";
|
|
setTimeout(() => setLoadingState(false), 1200);
|
|
updateNavigationLayout();
|
|
siteNavOriginalTop =
|
|
siteNavigationDiv.getBoundingClientRect().top + window.scrollY;
|
|
handleScroll();
|
|
adjustBodyPaddingForScrollbar(); // Adjust padding after content is rendered
|
|
},
|
|
onError: (err) => {
|
|
showError(err.message);
|
|
setLoadingState(false);
|
|
},
|
|
}).catch((err) => {
|
|
showError(err.message || "发生未知错误");
|
|
setLoadingState(false);
|
|
});
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 处理分页按钮点击(事件委托)
|
|
* @param {Event} e 点击事件
|
|
*/
|
|
function handlePaginationClick(e) {
|
|
const button = e.target.closest(".prev-page-btn, .next-page-btn");
|
|
if (!button || button.disabled) return;
|
|
|
|
const platformName = button.dataset.platform;
|
|
const platformData = platformResults.get(platformName);
|
|
|
|
if (!platformData) {
|
|
console.error(`错误:找不到平台 "${platformName}" 的数据。`);
|
|
return;
|
|
}
|
|
|
|
const isNext = button.classList.contains("next-page-btn");
|
|
const totalPages = Math.ceil(platformData.items.length / ITEMS_PER_PAGE);
|
|
let newPage = platformData.currentPage + (isNext ? 1 : -1);
|
|
|
|
if (newPage < 1 || newPage > totalPages) return;
|
|
|
|
platformData.currentPage = newPage;
|
|
platformResults.set(platformName, platformData);
|
|
|
|
const oldCard = resultsDiv.querySelector(
|
|
`div[data-platform="${platformName}"]`
|
|
);
|
|
if (oldCard) {
|
|
const newCard = createPlatformCard(platformData, false);
|
|
oldCard.replaceWith(newCard);
|
|
newCard.classList.add(
|
|
"animate__animated",
|
|
"animate__fadeIn",
|
|
"animate__faster"
|
|
);
|
|
newCard.addEventListener(
|
|
"animationend",
|
|
function handler() {
|
|
this.classList.remove(
|
|
"animate__animated",
|
|
"animate__fadeIn",
|
|
"animate__faster"
|
|
);
|
|
this.removeEventListener("animationend", handler);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
adjustBodyPaddingForScrollbar(); // Adjust padding after pagination
|
|
}
|
|
|
|
/**
|
|
* 动态设置导航栏宽度和桌面端定位
|
|
*/
|
|
function updateSiteNavigationWidth() {
|
|
if (!siteNavigationDiv) return;
|
|
|
|
const firstCard = resultsDiv.querySelector("[data-platform]");
|
|
if (firstCard) {
|
|
// The width is now primarily controlled by Tailwind's 'w-full' and removed 'max-w-*' for desktop
|
|
siteNavigationDiv.style.width = ""; // Ensure no inline width overrides Tailwind
|
|
siteNavigationDiv.style.left = ""; // Ensure no inline left overrides Tailwind centering
|
|
siteNavigationDiv.style.transform = ""; // Ensure no inline transform overrides Tailwind centering
|
|
|
|
// Set marginBottom for resultsDiv as nav is at the bottom
|
|
resultsDiv.style.marginBottom = `${siteNavigationDiv.offsetHeight}px`; // Removed +24
|
|
resultsDiv.style.marginTop = ""; // Clear any lingering top margin
|
|
} else {
|
|
siteNavigationDiv.style.width = "";
|
|
siteNavigationDiv.style.left = "";
|
|
siteNavigationDiv.style.top = "";
|
|
siteNavigationDiv.classList.add("hidden");
|
|
resultsDiv.style.marginTop = "";
|
|
resultsDiv.style.marginBottom = ""; // Clear margin if nav is hidden
|
|
}
|
|
// siteNavOriginalTop is less relevant now as nav is always fixed at bottom
|
|
}
|
|
|
|
/**
|
|
* 更新导航链接并控制可见性
|
|
*/
|
|
function updateSiteNavigation() {
|
|
if (!siteNavigationDiv || !navLinksContainer || !toggleNavButton) return;
|
|
|
|
navLinksContainer.innerHTML = "";
|
|
|
|
const colorMap = {
|
|
lime: {
|
|
text: "text-lime-800",
|
|
bg: "bg-lime-200",
|
|
hoverBg: "hover:bg-lime-300",
|
|
},
|
|
white: {
|
|
text: "text-gray-800",
|
|
bg: "bg-gray-200",
|
|
hoverBg: "hover:bg-gray-300",
|
|
},
|
|
gold: {
|
|
text: "text-yellow-800",
|
|
bg: "bg-yellow-200",
|
|
hoverBg: "hover:bg-yellow-300",
|
|
},
|
|
red: {
|
|
text: "text-red-800",
|
|
bg: "bg-red-200",
|
|
hoverBg: "hover:bg-red-300",
|
|
},
|
|
default: {
|
|
text: "text-indigo-800",
|
|
bg: "bg-indigo-200",
|
|
hoverBg: "hover:bg-indigo-300",
|
|
},
|
|
};
|
|
|
|
const sortedPlatformNames = Array.from(platformResults.keys()).sort();
|
|
|
|
// Unified visibility control for siteNavigationDiv
|
|
if (sortedPlatformNames.length === 0) {
|
|
siteNavigationDiv.classList.add("hidden");
|
|
toggleNavButton.classList.add("hidden");
|
|
isNavCollapsed = false; // Reset collapse state
|
|
resultsDiv.style.marginBottom = ""; // Reset margin if nav hidden
|
|
console.log(
|
|
"updateSiteNavigation: No results, hiding nav. platformResults.size:",
|
|
platformResults.size
|
|
);
|
|
return;
|
|
} else {
|
|
siteNavigationDiv.classList.remove("hidden");
|
|
siteNavigationDiv.classList.add("animate__fadeInUp"); // Re-add entrance animation
|
|
siteNavigationDiv.classList.remove("animate__fadeOutDown"); // Ensure fadeOut is removed
|
|
console.log(
|
|
"updateSiteNavigation: Results present, showing nav. platformResults.size:",
|
|
platformResults.size
|
|
);
|
|
}
|
|
|
|
sortedPlatformNames.forEach((name) => {
|
|
const platform = platformResults.get(name);
|
|
const link = document.createElement("a");
|
|
link.href = `#${name}`;
|
|
|
|
const colorKey =
|
|
platform.color && colorMap[platform.color] ? platform.color : "default";
|
|
const colorClasses = colorMap[colorKey];
|
|
|
|
link.className = `text-sm px-3 py-1 rounded-full transition-all duration-200 ease-in-out whitespace-nowrap
|
|
${colorClasses.text} ${colorClasses.bg} ${colorClasses.hoverBg}
|
|
hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-400`;
|
|
link.textContent = platform.name;
|
|
navLinksContainer.appendChild(link);
|
|
});
|
|
|
|
updateNavigationLayout();
|
|
}
|
|
|
|
/**
|
|
* 根据平台结果数据创建 HTML 卡片元素
|
|
* @param {object} result - 单个平台的结果数据
|
|
* @param {boolean} withAnimation - 是否应用入场动画
|
|
* @returns {HTMLElement}
|
|
*/
|
|
function createPlatformCard(result, withAnimation = true) {
|
|
const currentPage = result.currentPage || 1;
|
|
const colorMap = {
|
|
lime: {
|
|
bg: "bg-lime-100",
|
|
text: "text-lime-700",
|
|
icon: "text-lime-400",
|
|
border: "border-lime-200",
|
|
},
|
|
white: {
|
|
bg: "bg-gray-50",
|
|
text: "text-gray-500",
|
|
icon: "text-gray-300",
|
|
border: "border-gray-100",
|
|
},
|
|
gold: {
|
|
bg: "bg-yellow-100",
|
|
text: "text-yellow-700",
|
|
icon: "text-yellow-400",
|
|
border: "border-yellow-200",
|
|
},
|
|
red: {
|
|
bg: "bg-red-100",
|
|
text: "text-red-700",
|
|
icon: "text-red-400",
|
|
border: "border-red-200",
|
|
},
|
|
default: {
|
|
bg: "bg-gradient-to-br from-indigo-100 via-white to-pink-50",
|
|
text: "text-indigo-700",
|
|
icon: "text-indigo-400",
|
|
border: "border-gray-100",
|
|
},
|
|
};
|
|
const colorKey =
|
|
result.color && colorMap[result.color] ? result.color : "default";
|
|
const color = colorMap[colorKey];
|
|
|
|
let home = "",
|
|
domain = "";
|
|
if (result.items && result.items.length > 0) {
|
|
try {
|
|
const url = new URL(item.url);
|
|
home = url.origin;
|
|
domain = url.hostname;
|
|
} catch {}
|
|
}
|
|
|
|
const tags = {
|
|
gold: '<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-yellow-400 text-white align-middle">需要魔法</span>',
|
|
lime: '<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-lime-400 text-white align-middle">无需登录</span>',
|
|
red: '<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-red-400 text-white align-middle">错误</span>',
|
|
white:
|
|
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-gray-300 text-gray-700 align-middle">需要登录</span>',
|
|
default:
|
|
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-indigo-200 text-indigo-700 align-middle">综合</span>',
|
|
};
|
|
const tag = tags[colorKey] || "";
|
|
|
|
let itemsHtml = "";
|
|
if (result.items && result.items.length > 0) {
|
|
const start = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
const end = start + ITEMS_PER_PAGE;
|
|
const paginatedItems = result.items.slice(start, end);
|
|
|
|
itemsHtml = `<ol class="divide-y divide-gray-100" data-items>
|
|
${paginatedItems
|
|
.map((item) => {
|
|
let decodedPath = "";
|
|
try {
|
|
const urlObj = new URL(item.url);
|
|
decodedPath = decodeURIComponent(
|
|
urlObj.pathname + (urlObj.search || "")
|
|
);
|
|
} catch {}
|
|
const displayName =
|
|
item.name === ".bzEmpty" || !item.name
|
|
? "未知文件"
|
|
: item.name;
|
|
return `<li class="group transition hover:bg-indigo-50 flex flex-col px-5 py-3">
|
|
<a href="${item.url}" target="_blank" class="font-medium text-gray-800 group-hover:text-indigo-700 text-sm flex items-center gap-1" title="访问具体页面">
|
|
<span class="flex-1 min-w-0 break-words">${displayName}</span>
|
|
<i class="fas fa-arrow-up-right-from-square text-gray-300 group-hover:text-indigo-400 ml-1"></i>
|
|
</a>
|
|
<span class="text-xs text-gray-400 mt-0.5 ml-1 break-all block w-full">${decodedPath}</span>
|
|
</li>`;
|
|
})
|
|
.join("")}
|
|
</ol>`;
|
|
} else if (!result.error) {
|
|
itemsHtml = '<div class="px-5 py-3 text-gray-400 italic">暂无结果</div>';
|
|
}
|
|
|
|
let paginationHtml = "";
|
|
if (result.items.length > ITEMS_PER_PAGE) {
|
|
const totalPages = Math.ceil(result.items.length / ITEMS_PER_PAGE);
|
|
const prevDisabled = currentPage === 1;
|
|
const nextDisabled = currentPage === totalPages;
|
|
paginationHtml = `<div class="px-5 py-3 flex justify-center gap-2 items-center">
|
|
<button class="prev-page-btn bg-indigo-500 text-white p-2 rounded-full hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 transition-all duration-200 ease-in-out ${
|
|
prevDisabled
|
|
? "opacity-50 cursor-not-allowed"
|
|
: "hover:scale-110 active:scale-90"
|
|
}" data-platform="${result.name}" ${
|
|
prevDisabled ? "disabled" : ""
|
|
}><i class="fas fa-chevron-left text-sm"></i></button>
|
|
<span class="text-sm font-semibold text-indigo-700 px-3 py-1 bg-indigo-100 rounded-full shadow-sm">
|
|
${currentPage} / ${totalPages}
|
|
</span>
|
|
<button class="next-page-btn bg-indigo-500 text-white p-2 rounded-full hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 transition-all duration-200 ease-in-out ${
|
|
nextDisabled
|
|
? "opacity-50 cursor-not-allowed"
|
|
: "hover:scale-110 active:scale-90"
|
|
}" data-platform="${result.name}" ${
|
|
nextDisabled ? "disabled" : ""
|
|
}><i class="fas fa-chevron-right text-sm"></i></button>
|
|
</div>`;
|
|
}
|
|
|
|
const cardHtml = `
|
|
<div class="flex items-center gap-2 px-5 py-3 bg-white/80 border-b ${
|
|
color.border
|
|
}">
|
|
<i class="fas fa-dice-d6 ${color.icon}"></i>
|
|
<a href="${home}" target="_blank" class="flex items-center gap-2 group/link outline-none focus:ring-2 focus:ring-indigo-300 rounded" title="访问具体页面">
|
|
<span class="text-lg font-bold ${
|
|
color.text
|
|
} group-hover/link:text-indigo-800">${
|
|
result.name
|
|
}${tag}</span>
|
|
<span class="text-xs text-gray-400 group-hover/link:text-indigo-400 ml-2">${domain}</span>
|
|
</a>
|
|
</div>
|
|
${
|
|
result.error
|
|
? `<div class="px-5 py-3 text-red-500 font-semibold flex items-center gap-2"><i class='fas fa-exclamation-circle'></i> ${result.error}</div>`
|
|
: ""
|
|
}
|
|
${itemsHtml}
|
|
${paginationHtml}
|
|
`;
|
|
|
|
const cardElement = document.createElement("div");
|
|
cardElement.dataset.platform = result.name;
|
|
cardElement.id = result.name;
|
|
cardElement.className = `mb-6 rounded-xl shadow-lg rounded-t-2xl ${
|
|
color.bg
|
|
} border ${color.border} overflow-hidden ${
|
|
withAnimation ? "animate__animated animate__fadeInUp animate__faster" : ""
|
|
}`;
|
|
cardElement.innerHTML = cardHtml;
|
|
|
|
return cardElement;
|
|
}
|
|
|
|
/**
|
|
* 重置/清空UI界面
|
|
*/
|
|
function clearUI() {
|
|
resultsDiv.innerHTML = "";
|
|
|
|
isNavCollapsed = false;
|
|
updateNavigationLayout(); // This will call updateSiteNavigation which will hide the nav if platformResults is empty
|
|
|
|
siteNavigationDiv.style.width = "";
|
|
resultsDiv.style.marginTop = "";
|
|
resultsDiv.style.marginBottom = ""; // Clear bottom margin as well
|
|
siteNavigationDiv.classList.remove("fixed-nav"); // Ensure fixed-nav class is removed
|
|
|
|
errorDiv.textContent = "";
|
|
if (progressBar) {
|
|
progressBar.style.width = "0%";
|
|
progressBar.style.opacity = "0";
|
|
progressBar.classList.remove("animate__fadeIn", "animate__fadeOut");
|
|
}
|
|
adjustBodyPaddingForScrollbar(); // Adjust padding after clearing UI
|
|
}
|
|
|
|
function showError(message) {
|
|
errorDiv.textContent = message;
|
|
errorDiv.classList.add(
|
|
"animate__animated",
|
|
"animate__shakeX",
|
|
"animate__faster"
|
|
);
|
|
errorDiv.addEventListener(
|
|
"animationend",
|
|
function handler() {
|
|
this.classList.remove(
|
|
"animate__animated",
|
|
"animate__shakeX",
|
|
"animate__faster"
|
|
);
|
|
this.removeEventListener("animationend", handler);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
|
|
function setLoadingState(isLoading) {
|
|
if (!searchBtn || !searchIcon) return;
|
|
|
|
const originalIconClass = searchIcon.dataset.originalClass || "fas fa-search";
|
|
if (isLoading) {
|
|
if (!searchIcon.dataset.originalClass) {
|
|
searchIcon.dataset.originalClass = searchIcon.className;
|
|
}
|
|
searchBtn.disabled = true;
|
|
searchBtn.classList.add("active");
|
|
searchIcon.className = "fas fa-spinner fa-spin";
|
|
if (searchBtnText) searchBtnText.textContent = "正在初始化...";
|
|
if (progressBar) {
|
|
progressBar.style.opacity = "1";
|
|
progressBar.classList.remove("hidden", "animate__fadeOut");
|
|
progressBar.classList.add("animate__animated", "animate__fadeIn");
|
|
}
|
|
} else {
|
|
searchBtn.disabled = false;
|
|
searchBtn.classList.remove("active");
|
|
searchIcon.className = originalIconClass;
|
|
if (searchBtnText) searchBtnText.textContent = "开始搜索";
|
|
if (progressBar) {
|
|
progressBar.classList.remove("animate__fadeIn");
|
|
progressBar.classList.add("animate__fadeOut");
|
|
progressBar.addEventListener(
|
|
"animationend",
|
|
function handler() {
|
|
this.style.opacity = "0";
|
|
this.classList.remove("animate__animated", "animate__fadeOut");
|
|
this.removeEventListener("animationend", handler);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function searchGameStream(
|
|
{
|
|
gameName,
|
|
magic = false,
|
|
zypassword = "",
|
|
patchMode = false,
|
|
customApi = "",
|
|
},
|
|
{ onTotal, onProgress, onResult, onDone, onError }
|
|
) {
|
|
const defaultSite = "api.searchgal.homes";
|
|
const protocol = "https";
|
|
|
|
let baseUrl = "";
|
|
|
|
if (customApi) {
|
|
try {
|
|
const urlObj = new URL(customApi);
|
|
baseUrl = urlObj.origin;
|
|
} catch (e) {
|
|
onError(new Error("自定义 API 地址无效。请确保其是有效的 URL。"));
|
|
return;
|
|
}
|
|
} else {
|
|
baseUrl = `${protocol}://${defaultSite}`;
|
|
}
|
|
|
|
const url = patchMode ? `${baseUrl}/search-patch` : `${baseUrl}/search-gal`;
|
|
|
|
const formData = new FormData();
|
|
formData.append("game", gameName);
|
|
formData.append("magic", String(magic));
|
|
if (zypassword) {
|
|
formData.append("zypassword", zypassword);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
errorData.error || `HTTP 错误!状态码: ${response.status}`
|
|
);
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop();
|
|
|
|
for (const line of lines) {
|
|
if (line.trim() === "") continue;
|
|
try {
|
|
const data = JSON.parse(line);
|
|
if (data.total && onTotal) {
|
|
onTotal(data.total);
|
|
} else if (data.progress) {
|
|
if (onProgress) onProgress(data.progress);
|
|
if (data.result && onResult) onResult(data.result);
|
|
} else if (data.done && onDone) {
|
|
onDone();
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error("无法解析JSON行:", line, e);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (onError) {
|
|
onError(error);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
function debounce(func, delay) {
|
|
let timeout;
|
|
return function () {
|
|
const context = this;
|
|
const args = arguments;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), delay);
|
|
};
|
|
}
|