mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-03-30 07:39:45 +08:00
250714
This commit is contained in:
93
index.html
93
index.html
@@ -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"
|
||||
|
||||
551
src/main.js
551
src/main.js
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user