This commit is contained in:
AdingApkgg
2025-07-14 05:00:32 +08:00
parent ecd98261e0
commit ca4381fc38
2 changed files with 427 additions and 217 deletions

View File

@@ -12,7 +12,7 @@
"Segoe UI Symbol";
line-height: 1.6;
background: fixed #f0bbbb
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' viewBox='0 0 192 192'%3E%3Cpath fill='%23000000' fill-opacity='0.05' d='M192 15v2a11 11 0 0 0-11 11c0 1.94 1.16 4.75 2.53 6.11l2.36 2.36a6.93 6.93 0 0 1 1.22 7.56l-.43.84a8.08 8.08 0 0 1-6.66 4.13H145v35.02a6.1 6.1 0 0 0 3.03 4.87l.84.43c1.58.79 4 .4 5.24-.85l2.36-2.36a12.04 12.04 0 0 1 7.51-3.11 13 13 0 1 1 .02 26 12 12 0 0 1-7.53-3.11l-2.36-2.36a4.93 4.93 0 0 0-5.24-.85l-.84.43a6.1 6.1 0 0 0-3.03 4.87V143h35.02a8.08 8.08 0 0 1 6.66 4.13l.43.84a6.91 6.91 0 0 1-1.22 7.56l-2.36 2.36A10.06 10.06 0 0 0 181 164a11 11 0 0 0 11 11v2a13 13 0 0 1-13-13 12 12 0 0 1 3.11-7.53l2.36-2.36a4.93 4.93 0 0 0 .85-5.24l-.43-.84a6.1 6.1 0 0 0-4.87-3.03H145v35.02a8.08 8.08 0 0 1-4.13 6.66l-.84.43a6.91 6.91 0 0 1-7.56-1.22l-2.36-2.36A10.06 10.06 0 0 0 124 181a11 11 0 0 0-11 11h-2a13 13 0 0 1 13-13c2.47 0 5.79 1.37 7.53 3.11l2.36 2.36a4.94 4.94 0 0 0 5.24.85l.84-.43a6.1 6.1 0 0 0 3.03-4.87V145h-35.02a8.08 8.08 0 0 1-6.66-4.13l-.43-.84a6.91 6.91 0 0 1 1.22-7.56l2.36-2.36A10.06 10.06 0 0 0 107 124a11 11 0 0 0-22 0c0 1.94 1.16 4.75 2.53 6.11l2.36 2.36a6.93 6.93 0 0 1 1.22 7.56l-.43.84a8.08 8.08 0 0 1-6.66 4.13H49v35.02a6.1 6.1 0 0 0 3.03 4.87l.84.43c1.58.79 4 .4 5.24-.85l2.36-2.36a12.04 12.04 0 0 1 7.51-3.11A13 13 0 0 1 81 192h-2a11 11 0 0 0-11-11c-1.94 0-4.75 1.16-6.11 2.53l-2.36 2.36a6.93 6.93 0 0 1-7.56 1.22l-.84-.43a8.08 8.08 0 0 1-4.13-6.66V145H11.98a6.1 6.1 0 0 0-4.87 3.03l-.43.84c-.79 1.58-.4 4 .85 5.24l2.36 2.36a12.04 12.04 0 0 1 3.11 7.51A13 13 0 0 1 0 177v-2a11 11 0 0 0 11-11c0-1.94-1.16-4.75-2.53-6.11l-2.36-2.36a6.93 6.93 0 0 1-1.22-7.56l.43-.84a8.08 8.08 0 0 1 6.66-4.13H47v-35.02a6.1 6.1 0 0 0-3.03-4.87l-.84-.43c-1.59-.8-4-.4-5.24.85l-2.36 2.36A12 12 0 0 1 28 109a13 13 0 1 1 0-26c2.47 0 5.79 1.37 7.53 3.11l2.36 2.36a4.94 4.94 0 0 0 5.24.85l.84-.43A6.1 6.1 0 0 0 47 84.02V49H11.98a8.08 8.08 0 0 1-6.66-4.13l-.43-.84a6.91 6.91 0 0 1 1.22-7.56l2.36-2.36A10.06 10.06 0 0 0 11 28 11 11 0 0 0 0 17v-2a13 13 0 0 1 13 13c0 2.47-1.37 5.79-3.11 7.53l-2.36 2.36a4.94 4.94 0 0 0-.85 5.24l.43.84A6.1 6.1 0 0 0 11.98 47H47V11.98a8.08 8.08 0 0 1 4.13-6.66l.84-.43a6.91 6.91 0 0 1 7.56 1.22l2.36 2.36A10.06 10.06 0 0 0 68 11 11 11 0 0 0 79 0h2a13 13 0 0 1-13 13 12 12 0 0 1-7.53-3.11l-2.36-2.36a4.93 4.93 0 0 0-5.24-.85l-.84.43A6.1 6.1 0 0 0 49 11.98V47h35.02a8.08 8.08 0 0 1 6.66 4.13l.43.84a6.91 6.91 0 0 1-1.22 7.56l-2.36 2.36A10.06 10.06 0 0 0 85 68a11 11 0 0 0 22 0c0-1.94-1.16-4.75-2.53-6.11l-2.36-2.36a6.93 6.93 0 0 1-1.22-7.56l.43-.84a8.08 8.08 0 0 1 6.66-4.13H143V11.98a6.1 6.1 0 0 0-3.03-4.87l-.84-.43c-1.59-.8-4-.4-5.24.85l-2.36 2.36A12 12 0 0 1 124 13a13 13 0 0 1-13-13h2a11 11 0 0 0 11 11c1.94 0 4.75-1.16 6.11-2.53l2.36-2.36a6.93 6.93 0 0 1 7.56-1.22l.84.43a8.08 8.08 0 0 1 4.13 6.66V47h35.02a6.1 6.1 0 0 0 4.87-3.03l.43-.84c.8-1.59.4-4-.85-5.24l-2.36-2.36A12 12 0 0 1 179 28a13 13 0 0 1 13-13zM84.02 143a6.1 6.1 0 0 0 4.87-3.03l.43-.84c.8-1.59.4-4-.85-5.24l-2.36-2.36A12 12 0 0 1 83 124a13 13 0 1 1 26 0c0 2.47-1.37 5.79-3.11 7.53l-2.36 2.36a4.94 4.94 0 0 0-.85 5.24l.43.84a6.1 6.1 0 0 0 4.87 3.03H143v-35.02a8.08 8.08 0 0 1 4.13-6.66l.84-.43a6.91 6.91 0 0 1 7.56 1.22l2.36 2.36A10.06 10.06 0 0 0 164 107a11 11 0 0 0 0-22c-1.94 0-4.75 1.16-6.11 2.53l-2.36 2.36a6.93 6.93 0 0 1-7.56 1.22l-.84-.43a8.08 8.08 0 0 1-4.13-6.66V49h-35.02a6.1 6.1 0 0 0-4.87 3.03l-.43.84c-.79 1.58-.4 4 .85 5.24l2.36 2.36a12.04 12.04 0 0 1 3.11 7.51A13 13 0 1 1 83 68a12 12 0 0 1 3.11-7.53l2.36-2.36a4.93 4.93 0 0 0 .85-5.24l-.43-.84A6.1 6.1 0 0 0 84.02 49H49v35.02a8.08 8.08 0 0 1-4.13 6.66l-.84.43a6.91 6.91 0 0 1-7.56-1.22l-2.36-2.36A10.06 10.06 0 0 0 28 85a11 11 0 0 0 0 22c1.94 0 4.75-1.16 6.11-2.53l2.36-2.36a6.93 6.93 0 0 1 7.56-1.22l.84.43a8.08 8.08 0 0 1 4.13 6.66V143h35.02z'%3E%3C/path%3E%3C/svg%3E")
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' viewBox='0 0 192 192'%3E%3Cpath fill='%23000000' fill-opacity='0.05' d='M192 15v2a11 11 0 0 0-11 11c0 1.94 1.16 4.75 2.53 6.11l2.36 2.36a6.93 6.93 0 0 1 1.22 7.56l-.43.84a8.08 8.08 0 0 1-6.66 4.13H145v35.02a6.1 6.1 0 0 0 3.03 4.87l.84.43c1.58.79 4 .4 5.24-.85l2.36-2.36a12.04 12.04 0 0 1 7.51-3.11 13 13 0 1 1 .02 26 12 12 0 0 1-7.53-3.11l-2.36-2.36a4.93 4.93 0 0 0-5.24-.85l-.84.43a6.1 6.1 0 0 0-3.03 4.87V143h35.02a8.08 8.08 0 0 1 6.66 4.13l.43.84a6.91 6.91 0 0 1-1.22 7.56l-2.36 2.36A10.06 10.06 0 0 0 181 164a11 11 0 0 0 11 11v2a13 13 0 0 1-13-13 12 12 0 0 1 3.11-7.53l2.36-2.36a4.94 4.94 0 0 0-.85-5.24l-.43-.84a6.1 6.1 0 0 0-4.87-3.03H145v35.02a8.08 8.08 0 0 1-4.13 6.66l-.84.43a6.91 6.91 0 0 1-7.56-1.22l-2.36-2.36A10.06 10.06 0 0 0 124 181a11 11 0 0 0-11 11h-2a13 13 0 0 1 13-13c2.47 0 5.79 1.37 7.53 3.11l2.36 2.36a4.94 4.94 0 0 0 5.24.85l.84-.43a6.1 6.1 0 0 0 3.03-4.87V145h-35.02a8.08 8.08 0 0 1-6.66-4.13l-.43-.84a6.91 6.91 0 0 1 1.22-7.56l2.36-2.36A10.06 10.06 0 0 0 107 124a11 11 0 0 0-22 0c0 1.94 1.16 4.75 2.53 6.11l2.36 2.36a6.93 6.93 0 0 1 1.22 7.56l-.43.84a8.08 8.08 0 0 1-6.66 4.13H49v35.02a6.1 6.1 0 0 0 3.03 4.87l.84.43c1.58.79 4 .4 5.24-.85l2.36-2.36a12.04 12.04 0 0 1 7.51-3.11A13 13 0 1 1 81 192h-2a11 11 0 0 0-11-11c-1.94 0-4.75 1.16-6.11 2.53l-2.36 2.36a6.93 6.93 0 0 1-7.56 1.22l-.84-.43a8.08 8.08 0 0 1-4.13-6.66V145H11.98a6.1 6.1 0 0 0-4.87 3.03l-.43.84c-.79 1.58-.4 4 .85 5.24l2.36 2.36a12.04 12.04 0 0 1 3.11 7.51A13 13 0 1 1 0 177v-2a11 11 0 0 0 11-11c0-1.94-1.16-4.75-2.53-6.11l-2.36-2.36a6.93 6.93 0 0 1-1.22-7.56l.43-.84a8.08 8.08 0 0 1 6.66-4.13H47v-35.02a6.1 6.1 0 0 0-3.03-4.87l-.84-.43c-1.59-.8-4-.4-5.24.85l-2.36 2.36A12 12 0 0 1 28 109a13 13 0 1 1 0-26c2.47 0 5.79 1.37 7.53 3.11l2.36 2.36a4.94 4.94 0 0 0 5.24.85l.84-.43A6.1 6.1 0 0 0 47 84.02V49H11.98a8.08 8.08 0 0 1-6.66-4.13l-.43-.84a6.91 6.91 0 0 1 1.22-7.56l2.36-2.36A10.06 10.06 0 0 0 11 28 11 11 0 0 0 0 17v-2a13 13 0 0 1 13 13c0 2.47-1.37 5.79-3.11 7.53l-2.36 2.36a4.94 4.94 0 0 0-.85 5.24l.43.84A6.1 6.1 0 0 0 11.98 47H47V11.98a8.08 8.08 0 0 1 4.13-6.66l.84-.43a6.91 6.91 0 0 1 7.56 1.22l2.36 2.36A10.06 10.06 0 0 0 68 11 11 11 0 0 0 79 0h2a13 13 0 0 1-13 13 12 12 0 0 1-7.53-3.11l-2.36-2.36a4.93 4.93 0 0 0-5.24-.85l-.84.43A6.1 6.1 0 0 0 49 11.98V47h35.02a8.08 8.08 0 0 1 6.66 4.13l.43.84a6.91 6.91 0 0 1-1.22 7.56l-2.36 2.36A10.06 10.06 0 0 0 85 68a11 11 0 0 0 22 0c0-1.94-1.16-4.75-2.53-6.11l-2.36-2.36a6.93 6.93 0 0 1-1.22-7.56l.43-.84a8.08 8.08 0 0 1 6.66-4.13H143V11.98a6.1 6.1 0 0 0-3.03-4.87l-.84-.43c-1.59-.8-4-.4-5.24.85l-2.36 2.36A12 12 0 0 1 124 13a13 13 0 0 1-13-13h2a11 11 0 0 0 11 11c1.94 0 4.75-1.16 6.11-2.53l2.36-2.36a6.93 6.93 0 0 1 7.56-1.22l.84.43a8.08 8.08 0 0 1 4.13 6.66V47h35.02a6.1 6.1 0 0 0 4.87-3.03l.43-.84c.8-1.59.4-4-.85-5.24l-2.36-2.36A12 12 0 0 1 179 28a13 13 0 0 1 13-13zM84.02 143a6.1 6.1 0 0 0 4.87-3.03l.43-.84c.8-1.59.4-4-.85-5.24l-2.36-2.36A12 12 0 0 1 83 124a13 13 0 1 1 26 0c0 2.47-1.37 5.79-3.11 7.53l-2.36 2.36a4.94 4.94 0 0 0-.85 5.24l.43.84a6.1 6.1 0 0 0 4.87 3.03H143v-35.02a8.08 8.08 0 0 1 4.13-6.66l.84-.43a6.91 6.91 0 0 1 7.56 1.22l2.36 2.36A10.06 10.06 0 0 0 164 107a11 11 0 0 0 0-22c-1.94 0-4.75 1.16-6.11 2.53l-2.36 2.36a6.93 6.93 0 0 1-7.56 1.22l-.84-.43a8.08 8.08 0 0 1-4.13-6.66V49h-35.02a6.1 6.1 0 0 0-4.87 3.03l-.43.84c-.79 1.58-.4 4 .85 5.24l2.36 2.36a12.04 12.04 0 0 1 3.11 7.51A13 13 0 1 1 83 68a12 12 0 0 1 3.11-7.53l2.36-2.36a4.93 4.93 0 0 0 .85-5.24l-.43-.84A6.1 6.1 0 0 0 84.02 49H49v35.02a8.08 8.08 0 0 1-4.13 6.66l-.84.43a6.91 6.91 0 0 1-7.56-1.22l-2.36-2.36A10.06 10.06 0 0 0 28 85a11 11 0 0 0 0 22c1.94 0 4.75-1.16 6.11-2.53l2.36-2.36a6.93 6.93 0 0 1 7.56-1.22l.84.43a8.08 8.08 0 0 1 4.13 6.66V143h35.02z'%3E%3C/path%3E%3C/svg%3E")
center;
min-height: 100vh;
overflow-wrap: break-word;
@@ -213,6 +213,90 @@
</form>
<div class="error text-red-600 font-semibold px-12" id="error"></div>
<div class="results mt-2 px-6" id="results"></div>
<section class="w-full max-w-4xl mx-auto mt-8">
<div class="bg-white/95 rounded-[8px] p-6 text-gray-700">
<h2
class="text-xl font-bold text-indigo-700 mb-4 inline-flex items-center gap-2"
>
<i class="fas fa-info-circle text-blue-500"></i> 咱家的使用须知
</h2>
<ul class="list-disc list-inside space-y-2 text-sm">
<li>
首先,衷心感谢
<a
href="https://saop.cc/"
target="_blank"
class="font-semibold text-indigo-600 hover:underline"
>@Asuna</a
>
大佬提供的服务器和技术支持!没有大佬的魔法,咱可跑不起来!
</li>
<li>
本程序纯属<strong>爱发电</strong>,仅供绅士们交流学习使用,务必请大家<strong
>支持正版 Galgame</strong
>!入正不亏哦!
</li>
<li>
本站只做互联网内容的<strong>聚合搬运工</strong>,搜索结果均来自第三方站点,下载前请各位自行判断<strong>资源安全性</strong>,以免翻车。
</li>
<li>
如果想体验“魔法”搜索,记得启用<strong>“魔搜”</strong>,解锁更多神秘站点!
</li>
<li>
搜索时请注意关键词长度!<strong>关键词太短</strong>可能搜不全(部分站点只显示首批结果),<strong>太长</strong>则可能无法精准匹配。建议尝试<strong>适当的关键词</strong>,效果更佳~
</li>
<li>
本程序每次查询完毕即断开连接,<strong>严禁任何形式的爆破或恶意爬取</strong>,做个文明的绅士!
</li>
<li>
万一某个站点搜索挂了,先看看自己的魔法是否到位,也可能是站点维护了,或者咱这边的<strong>爬虫失效</strong>了。
</li>
<li>
关于站点标签:
<span
class="inline-block px-2 py-0.5 rounded-full bg-green-200 text-green-800 text-xs font-medium"
>绿色</span
>
代表免登录<strong>直冲</strong>
<span
class="inline-block px-2 py-0.5 rounded-full bg-yellow-200 text-yellow-800 text-xs font-medium"
>金色</span
>
表示需要<strong>魔法加持</strong>才能访问;
<span
class="inline-block px-2 py-0.5 rounded-full bg-gray-200 text-gray-800 text-xs font-medium"
>白色</span
>
通常意味着需要<strong>登录/回复</strong>等额外操作才能拿到资源。
</li>
<li>
目前收录的站点多为 PC 平台资源,大部分站点提供 OneDrive
或直链下载,速度上比某些国内盘要<strong>给力</strong>不少!
</li>
<li>
为了支持各 Galgame
站点能长久运营,还请各位把浏览器的<strong>广告屏蔽插件</strong>关掉,或将这些站点加入白名单。大家建站不易,小小的支持也是大大的动力!
</li>
<li>
<span class="font-bold text-red-600"
>郑重呼吁:请务必支持 Galgame 正版!让爱与梦想延续!</span
>
</li>
<li>
如果您觉得咱这小工具好用,请移步
<a
href="https://github.com/Moe-Sakura"
target="_blank"
class="text-blue-600 hover:underline font-semibold"
>GitHub</a
>
给本项目点个免费的
<strong>Star</strong>
吧,秋梨膏!你的支持就是咱最大的动力,比心~
</li>
</ul>
</div>
</section>
<div
class="footer text-xs text-gray-500 mt-4 text-center border-t border-gray-100 pt-4 px-12 pb-4 rounded-b-lg"
>
@@ -234,7 +318,12 @@
class="inline-flex items-center gap-1 text-xs text-gray-400 bg-gray-100 rounded-[8px] px-2 py-1 font-mono select-none"
>
<i class="fas fa-code-branch"></i>
2025/07/09 v15
<a
href="https://github.com/Moe-Sakura/SearchGal/blob/main/version.md"
target="_blank"
class="text-gray-600 hover:underline"
>250714</a
>
</span>
<a
href="https://github.com/Moe-Sakura"

View File

@@ -1,34 +1,164 @@
function isMagicAccessChecked() {
const magicCheckbox = document.getElementById("magicAccess");
return magicCheckbox && magicCheckbox.checked;
}
// -- 全局常量与状态 --
const ITEMS_PER_PAGE = 10;
const platformResults = new Map(); // 使用 Map 存储每个平台的结果和当前页码
function isMagicPlatform(result) {
return result && result.color === "gold";
}
quicklink.listen({ priority: true });
Artalk.init({
el: "#Comments",
pageKey: "https://searchgal.homes",
server: "https://artalk.saop.cc",
site: "Galgame 聚合搜索",
});
const form = document.getElementById("searchForm");
// -- 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");
/**
* 页面加载后初始化
*/
window.addEventListener("DOMContentLoaded", () => {
if (searchBtn) searchBtn.disabled = false;
// --- 主要修改点 ---
// 默认勾选“启用魔法”复选框
const magicCheckbox = document.getElementById("magicAccess");
if (magicCheckbox && !magicCheckbox.checked) magicCheckbox.checked = true;
if (magicCheckbox) {
magicCheckbox.checked = true;
}
// --- 修改结束 ---
// 初始化 quicklink 和 Artalk
quicklink.listen({ priority: true });
Artalk.init({
el: "#Comments",
pageKey: "https://searchgal.homes",
server: "https://artalk.saop.cc",
site: "Galgame 聚合搜索",
});
// 为表单和分页按钮绑定事件监听器
if (searchForm) {
searchForm.addEventListener("submit", handleSearchSubmit);
}
if (resultsDiv) {
resultsDiv.addEventListener("click", handlePaginationClick);
}
});
function renderPlatform(result, withAnimation = true) {
/**
* 统一处理搜索表单提交
* @param {Event} e 事件对象
*/
async function handleSearchSubmit(e) {
e.preventDefault();
clearUI();
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;
if (!gameName) {
showError("游戏名称不能为空");
return;
}
setLoadingState(true);
const searchParams = {
gameName,
zypassword,
magic,
patchMode: searchMode === "patch",
};
try {
let totalTasks = 0;
let isFirstResult = true;
await 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, isFirstResult);
resultsDiv.appendChild(platformCard);
isFirstResult = false;
},
onDone: () => {
if (searchBtnText) searchBtnText.textContent = "搜索完成!";
setTimeout(() => setLoadingState(false), 1200);
},
onError: (err) => {
showError(err.message);
setLoadingState(false);
},
});
} catch (err) {
showError(err.message || "发生未知错误");
setLoadingState(false);
}
}
/**
* 【关键修复】处理分页按钮点击(事件委托)
* @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);
}
}
/**
* 根据平台结果数据创建 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",
@@ -64,79 +194,118 @@ function renderPlatform(result, withAnimation = true) {
const colorKey =
result.color && colorMap[result.color] ? result.color : "default";
const color = colorMap[colorKey];
let html = `<div class="${
withAnimation ? "animate__animated animate__fadeInUp" : ""
} mb-6 rounded-xl shadow-lg rounded-t-2xl ${color.bg} border ${
color.border
} overflow-hidden">`;
let domain = "";
let home = "";
let home = "",
domain = "";
if (result.items && result.items.length > 0) {
try {
const urlObj = new URL(result.items[0].url);
domain = urlObj.hostname;
home = urlObj.origin;
} catch {
domain = "";
home = "";
}
}
let tag = "";
if (isMagicPlatform(result)) {
tag =
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-yellow-400 text-white align-middle">需要魔法</span>';
} else if (colorKey === "lime") {
tag =
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-lime-400 text-white align-middle">无需登录</span>';
} else if (colorKey === "red") {
tag =
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-red-400 text-white align-middle">错误</span>';
} else if (colorKey === "white") {
tag =
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-gray-300 text-gray-700 align-middle">需要登录</span>';
} else if (colorKey === "default") {
tag =
'<span class="ml-2 px-2 py-0.5 rounded text-xs font-bold bg-indigo-200 text-indigo-700 align-middle">综合</span>';
}
html += `<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>`;
if (result.error) {
html += `<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>`;
const url = new URL(result.items[0].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) {
html += '<ol class="divide-y divide-gray-100">';
for (const item of result.items) {
let path = "";
let decodedPath = "";
try {
const urlObj = new URL(item.url);
path = urlObj.pathname + (urlObj.search || "");
decodedPath = decodeURIComponent(path);
} catch {
path = "";
decodedPath = "";
}
html += `<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="truncate">${item.name}</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>`;
}
html += "</ol>";
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="truncate">${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) {
html += '<div class="px-5 py-3 text-gray-400 italic">暂无结果</div>';
itemsHtml = '<div class="px-5 py-3 text-gray-400 italic">暂无结果</div>';
}
html += "</div>";
return html;
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">
<button class="prev-page-btn bg-indigo-500 text-white px-4 py-2 rounded hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 ${
prevDisabled ? "opacity-50 cursor-not-allowed" : ""
}" data-platform="${result.name}" ${
prevDisabled ? "disabled" : ""
}>上一页</button>
<span class="text-gray-600 self-center">第 ${currentPage} 页 / 共 ${totalPages} 页</span>
<button class="next-page-btn bg-indigo-500 text-white px-4 py-2 rounded hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 ${
nextDisabled ? "opacity-50 cursor-not-allowed" : ""
}" data-platform="${result.name}" ${
nextDisabled ? "disabled" : ""
}>下一页</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.className = `mb-6 rounded-xl shadow-lg rounded-t-2xl ${
color.bg
} border ${color.border} overflow-hidden ${
withAnimation ? "animate__animated animate__fadeInUp" : ""
}`;
cardElement.innerHTML = cardHtml;
return cardElement;
}
/**
* 重置/清空UI界面
*/
function clearUI() {
resultsDiv.innerHTML = "";
errorDiv.textContent = "";
@@ -144,177 +313,129 @@ function clearUI() {
progressBar.style.width = "0%";
progressBar.style.opacity = "0";
}
if (searchBtnText) searchBtnText.textContent = "开始搜索";
platformResults.clear();
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
clearUI();
setTimeout(async () => {
const game = form.game.value.trim();
const zypassword = form.zypassword.value.trim();
const searchMode = form.searchMode.value;
const patchMode = searchMode === "patch";
const magic = isMagicAccessChecked();
if (!game) {
errorDiv.textContent = "游戏名称不能为空";
return;
/**
* 显示错误信息
* @param {string} message 错误消息
*/
function showError(message) {
errorDiv.textContent = message;
}
/**
* 设置UI的加载状态
* @param {boolean} isLoading 是否正在加载
*/
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;
}
if (searchBtn) {
searchBtn.disabled = true;
searchBtn.classList.add("active");
}
const iconEl = searchBtnText && searchBtnText.previousElementSibling;
let oldIconClass = "";
if (iconEl && iconEl.tagName === "I") {
oldIconClass = iconEl.className;
iconEl.className = "fas fa-spinner fa-spin";
}
let firstResult = true;
let total = 0;
searchBtn.disabled = true;
searchBtn.classList.add("active");
searchIcon.className = "fas fa-spinner fa-spin";
if (searchBtnText) searchBtnText.textContent = "正在初始化...";
if (progressBar) {
progressBar.style.width = "0%";
progressBar.style.opacity = "1";
progressBar.classList.remove("hidden");
}
if (searchBtnText) searchBtnText.textContent = "正在搜索...";
const searchParams = {
gameName: game,
zypassword,
patchMode,
magic,
};
const restoreIcon = () => {
if (iconEl && oldIconClass) iconEl.className = oldIconClass;
};
try {
await searchGameStream(searchParams, {
onProgress: (progress) => {
total = progress.total || total;
if (progressBar && total) {
const percent = Math.min(
100,
Math.round((progress.completed / total) * 100)
);
progressBar.style.width = percent + "%";
progressBar.style.opacity = "1";
}
if (searchBtnText)
searchBtnText.textContent = `进度: ${progress.completed} / ${progress.total}`;
},
onResult: (result) => {
const temp = document.createElement("div");
temp.innerHTML = renderPlatform(result, firstResult);
const card = temp.firstElementChild;
if (card) {
card.classList.remove("animate__fadeInUp");
card.classList.remove("animate__fadeInDown");
void card.offsetWidth;
card.classList.add("animate__animated");
card.classList.add(
firstResult ? "animate__fadeInDown" : "animate__fadeInUp"
);
resultsDiv.insertBefore(card, resultsDiv.firstChild);
}
firstResult = false;
},
onDone: () => {
if (progressBar) {
progressBar.style.width = "100%";
setTimeout(() => {
progressBar.style.opacity = "0";
}, 800);
}
if (searchBtnText) searchBtnText.textContent = "搜索完成!";
setTimeout(() => {
if (searchBtnText) searchBtnText.textContent = "开始搜索";
restoreIcon();
}, 1200);
if (searchBtn) {
searchBtn.disabled = false;
searchBtn.classList.remove("active");
}
},
onError: (err) => {
errorDiv.textContent = err.message || "发生未知错误";
if (progressBar) progressBar.style.opacity = "0";
if (searchBtnText) searchBtnText.textContent = "开始搜索";
restoreIcon();
if (searchBtn) {
searchBtn.disabled = false;
searchBtn.classList.remove("active");
}
},
});
} catch (err) {
errorDiv.textContent = err.message || "发生未知错误";
if (progressBar) progressBar.style.opacity = "0";
if (searchBtnText) searchBtnText.textContent = "开始搜索";
restoreIcon();
if (searchBtn) {
searchBtn.disabled = false;
searchBtn.classList.remove("active");
}
} else {
searchBtn.disabled = false;
searchBtn.classList.remove("active");
searchIcon.className = originalIconClass;
if (searchBtnText) searchBtnText.textContent = "开始搜索";
if (progressBar) {
setTimeout(() => {
progressBar.style.opacity = "0";
}, 800);
}
}, 0);
});
}
}
/**
* 调用后端流式搜索API
* @param {object} params - 搜索参数
* @param {string} params.gameName - 游戏名
* @param {boolean} [params.magic=false] - 是否开启魔法搜索
* @param {string} [params.zypassword=''] - Zypassword
* @param {boolean} [params.patchMode=false] - 是否为补丁搜索模式
* @param {object} callbacks - 回调函数集合
* @param {function} callbacks.onTotal - 接收到总数时调用
* @param {function} callbacks.onProgress - 接收到进度时调用
* @param {function} callbacks.onResult - 接收到单个结果时调用
* @param {function} callbacks.onDone - 全部完成时调用
* @param {function} callbacks.onError - 发生错误时调用
*/
async function searchGameStream(
{ gameName, zypassword = "", patchMode = false, magic = false },
{ onProgress, onResult, onDone, onError }
{ gameName, magic = false, zypassword = "", patchMode = false },
{ onTotal, onProgress, onResult, onDone, onError }
) {
const site = "api.searchgal.homes";
const protocol = "https";
const url = patchMode
? `https://${site}/search-patch`
: `https://${site}/search-gal`;
? `${protocol}://${site}/search-patch`
: `${protocol}://${site}/search-gal`;
const formData = new FormData();
formData.append("game", gameName);
formData.append("magic", magic ? "true" : "false");
if (zypassword) formData.append("zypassword", zypassword);
formData.append("magic", String(magic));
if (zypassword) {
formData.append("zypassword", zypassword);
}
try {
const response = await fetch(url, {
method: "POST",
body: formData,
});
if (!response.ok) {
let errorData = {};
try {
errorData = await response.json();
} catch {}
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.error || `HTTP error! status: ${response.status}`
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;
let data;
try {
data = JSON.parse(line);
} catch (e) {
throw new Error("数据解析失败: " + e.message);
}
if (data.total) {
} else if (data.progress && onProgress) {
onProgress(data.progress);
if (data.result && onResult) {
onResult(data.result);
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;
}
} else if (data.done && onDone) {
onDone();
return;
} catch (e) {
console.error("无法解析JSON行:", line, e);
}
}
}
} catch (error) {
throw error;
if (onError) {
onError(error);
} else {
throw error;
}
}
}