From a3bed57fbb28a89e2accabbd9c29490b908f849c Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Fri, 26 Dec 2025 21:17:01 +0800 Subject: [PATCH 01/12] Fix filter structure in fetchVndbQuotes and enhance screenshot validation in VndbPanel.vue - Updated the filter format in fetchVndbQuotes to directly use vnId for improved clarity. - Added a check in VndbPanel.vue to ensure the current game ID matches before processing screenshots, preventing unnecessary updates. --- src/api/search.ts | 2 +- src/components/VndbPanel.vue | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/search.ts b/src/api/search.ts index 4ac52dd..1d80039 100644 --- a/src/api/search.ts +++ b/src/api/search.ts @@ -598,7 +598,7 @@ export async function fetchVndbQuotes(vnId: string): Promise { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - filters: ['vn', '=', ['id', '=', vnId]], + filters: ['vn', '=', vnId], fields: 'id, quote, character{id, name, original}', results: 10, }), diff --git a/src/components/VndbPanel.vue b/src/components/VndbPanel.vue index fea07df..631a722 100644 --- a/src/components/VndbPanel.vue +++ b/src/components/VndbPanel.vue @@ -730,8 +730,13 @@ watch(() => searchStore.vndbInfo, async (newInfo) => { // 检查缓存的截图是否已加载 if (newInfo?.screenshots && newInfo.screenshots.length > 0) { + const vnIdForScreenshots = newInfo.id nextTick(() => { requestAnimationFrame(() => { + // 检查是否仍是同一个游戏 + if (currentVnId.value !== vnIdForScreenshots) { + return + } const screenshotImgs = modalRef.value?.querySelectorAll('img[alt*="截图"]') if (screenshotImgs) { for (let i = 0; i < screenshotImgs.length; i++) { From 1e192cef3c92b5b8ad7d245eae5ac7c509f33e96 Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Fri, 26 Dec 2025 21:21:19 +0800 Subject: [PATCH 02/12] Enhance service worker caching strategy and update fetchVndbQuotes filter structure - Updated service worker to improve caching of static resources, including additional file types and core resources for faster loading. - Refactored fetchVndbQuotes to modify filter structure for better clarity and functionality. --- public/sw.js | 93 +++++++++++++++++++++++++++++++++++++---------- src/api/search.ts | 2 +- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/public/sw.js b/public/sw.js index 9b702bb..832126c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,13 +1,28 @@ -// Service Worker - PWA 支持(简化版) -// 离线时仅显示联网提示,不缓存离线内容 +// Service Worker - PWA 支持(增强缓存版) +// 尽可能多地缓存文件以提升加载速度 const SW_VERSION = self.__SW_VERSION__ || Date.now().toString(36); const CACHE_NAME = `searchgal-cache-${SW_VERSION}`; -// 仅缓存静态资源以提升加载速度(非离线使用) +// 缓存静态资源(尽可能多) const CACHEABLE_PATTERNS = [ - /\/assets\/.*\.(js|css)(\?.*)?$/i, // Vite 构建的静态资源 + /\/assets\/.*\.(js|css|mjs)(\?.*)?$/i, // Vite 构建的静态资源 /\.(woff2?|ttf|otf|eot)(\?.*)?$/i, // 字体 - /\.(png|jpg|jpeg|gif|webp|svg|ico)(\?.*)?$/i, // 图片 + /\.(png|jpg|jpeg|gif|webp|svg|ico|avif)(\?.*)?$/i, // 图片 + /\.(mp3|wav|ogg|m4a|aac|flac)(\?.*)?$/i, // 音频(snd-lib 音效) + /\.(mp4|webm|ogv)(\?.*)?$/i, // 视频 + /\.(json|xml|txt)(\?.*)?$/i, // 数据文件 + /\.(wasm)(\?.*)?$/i, // WebAssembly + /\/manifest\.json$/i, // PWA manifest + /\/favicon/i, // Favicon 相关 + /\/apple-touch-icon/i, // iOS 图标 + /\/android-chrome/i, // Android 图标 +]; + +// 预缓存的核心资源(安装时缓存) +const PRECACHE_URLS = [ + '/', + '/index.html', + '/manifest.json', ]; // 永不缓存 @@ -16,14 +31,26 @@ const NO_CACHE_PATTERNS = [ /\/sw\.js$/, /sockjs-node/, /__vite/, + /hot-update/, + /\.map$/, // Source maps ]; // ============================================ -// 安装事件 +// 安装事件 - 预缓存核心资源 // ============================================ -self.addEventListener('install', () => { +self.addEventListener('install', (event) => { console.log(`[SW] Installing version ${SW_VERSION}`); - self.skipWaiting(); + + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + console.log('[SW] Precaching core resources'); + return cache.addAll(PRECACHE_URLS).catch((err) => { + console.warn('[SW] Precache failed:', err); + }); + }) + .then(() => self.skipWaiting()) + ); }); // ============================================ @@ -78,23 +105,30 @@ self.addEventListener('fetch', (event) => { // 跳过永不缓存的模式 if (NO_CACHE_PATTERNS.some((p) => p.test(url.href))) return; - // 仅处理同源请求 - if (url.origin !== location.origin) return; + // 同源请求 + if (url.origin === location.origin) { + // HTML 页面请求:网络优先,离线时显示提示 + if (request.destination === 'document' || url.pathname === '/' || url.pathname.endsWith('.html')) { + event.respondWith(handlePageRequest(request)); + return; + } - // HTML 页面请求:网络优先,离线时显示提示 - if (request.destination === 'document' || url.pathname === '/' || url.pathname.endsWith('.html')) { - event.respondWith(handlePageRequest(request)); + // 可缓存的静态资源:缓存优先(加速) + if (CACHEABLE_PATTERNS.some((p) => p.test(url.pathname) || p.test(url.href))) { + event.respondWith(cacheFirst(request)); + return; + } + + // 同源其他资源:网络优先,但也缓存 + event.respondWith(networkFirst(request)); return; } - // 可缓存的静态资源:缓存优先(加速) - if (CACHEABLE_PATTERNS.some((p) => p.test(url.pathname))) { - event.respondWith(cacheFirst(request)); + // 跨域资源:仅缓存可缓存的静态资源(如 CDN 资源) + if (CACHEABLE_PATTERNS.some((p) => p.test(url.pathname) || p.test(url.href))) { + event.respondWith(cacheFirstCrossOrigin(request)); return; } - - // 其他资源:网络优先 - event.respondWith(networkFirst(request)); }); /** @@ -146,6 +180,27 @@ async function networkFirst(request) { } } +/** + * 缓存优先(跨域资源) + */ +async function cacheFirstCrossOrigin(request) { + const cached = await caches.match(request); + if (cached) return cached; + + try { + // 跨域请求需要设置 mode + const response = await fetch(request, { mode: 'cors', credentials: 'omit' }); + if (response.ok && response.type !== 'opaque') { + const cache = await caches.open(CACHE_NAME); + cache.put(request, response.clone()); + } + return response; + } catch { + // 跨域失败时返回空响应 + return new Response('', { status: 503 }); + } +} + /** * 离线提示页面 */ diff --git a/src/api/search.ts b/src/api/search.ts index 1d80039..4ac52dd 100644 --- a/src/api/search.ts +++ b/src/api/search.ts @@ -598,7 +598,7 @@ export async function fetchVndbQuotes(vnId: string): Promise { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - filters: ['vn', '=', vnId], + filters: ['vn', '=', ['id', '=', vnId]], fields: 'id, quote, character{id, name, original}', results: 10, }), From 56d4178da8c0d932606277f1382d0c62c6cbab93 Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Fri, 26 Dec 2025 21:29:32 +0800 Subject: [PATCH 03/12] Implement parallel VNDB data fetching and improve scrolling behavior in SearchHeader.vue - Added functionality to fetch VNDB data concurrently when in game search mode, ensuring data consistency during rapid search term changes. - Introduced a flag to manage scrolling behavior, allowing for smoother user experience by scrolling to results only after a minimum number of platform results are available. - Enhanced error handling for VNDB requests to prevent disruption of the main search process. --- src/components/SearchHeader.vue | 46 +++++++++++++++++++++++++-------- src/components/VndbPanel.vue | 7 ++--- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/components/SearchHeader.vue b/src/components/SearchHeader.vue index c0ecd0f..fe47e53 100644 --- a/src/components/SearchHeader.vue +++ b/src/components/SearchHeader.vue @@ -590,6 +590,8 @@ watch(() => searchStore.customApi, (newApi) => { } }) +let hasScrolledToResults = false + async function handleSearch() { if (!searchQuery.value.trim()) {return} @@ -597,6 +599,7 @@ async function handleSearch() { searchStore.clearResults() searchStore.isSearching = true searchStore.errorMessage = '' + hasScrolledToResults = false // 重置滚动标志 const searchParams = new URLSearchParams() searchParams.set('game', searchQuery.value.trim()) @@ -605,6 +608,19 @@ async function handleSearch() { searchParams.set('api', customApi.value.trim()) } + // 在 game 模式下,搜索开始时就并行发起 VNDB 请求 + const queryForVndb = searchQuery.value.trim() + if (searchMode.value === 'game') { + fetchVndbData(queryForVndb).then((vndbData) => { + // 检查搜索词是否仍匹配(防止快速切换搜索时数据错乱) + if (vndbData && searchStore.searchQuery === queryForVndb) { + searchStore.vndbInfo = vndbData + } + }).catch(() => { + // VNDB 请求失败不影响主搜索 + }) + } + try { await searchGameStream(searchParams, { onTotal: (total) => { @@ -614,11 +630,11 @@ async function handleSearch() { searchStore.searchProgress = { current, total } }, onPlatformResult: (data) => { - const isFirstResult = searchStore.platformResults.size === 0 searchStore.setPlatformResult(data.name, data) - // 第一个结果出现时滚动到结果区域 - if (isFirstResult) { + // 等待至少 3 个平台结果后滚动到结果区域(只滚动一次) + if (!hasScrolledToResults && searchStore.platformResults.size >= 3) { + hasScrolledToResults = true // 使用 requestAnimationFrame + setTimeout 确保 DOM 已更新 window.requestAnimationFrame(() => { setTimeout(() => { @@ -642,6 +658,22 @@ async function handleSearch() { searchStore.isSearching = false playCelebration() // 搜索完成音效 + // 如果结果不足 3 个但有结果,且还没滚动过,则现在滚动 + if (!hasScrolledToResults && searchStore.platformResults.size > 0) { + hasScrolledToResults = true + window.requestAnimationFrame(() => { + setTimeout(() => { + const resultsEl = document.getElementById('results') + if (resultsEl) { + const headerOffset = 80 + const elementPosition = resultsEl.getBoundingClientRect().top + const offsetPosition = elementPosition + window.pageYOffset - headerOffset + window.scrollTo({ top: offsetPosition, behavior: 'smooth' }) + } + }, 50) + }) + } + // 保存搜索历史 const resultCount = searchStore.totalResults saveSearchHistory({ @@ -657,14 +689,6 @@ async function handleSearch() { playCaution() // 错误音效 }, }) - - // 获取 VNDB 数据 - if (searchMode.value === 'game') { - const vndbData = await fetchVndbData(searchQuery.value.trim()) - if (vndbData) { - searchStore.vndbInfo = vndbData - } - } } catch (error) { searchStore.errorMessage = error instanceof Error ? error.message : '搜索失败' diff --git a/src/components/VndbPanel.vue b/src/components/VndbPanel.vue index 631a722..548acb0 100644 --- a/src/components/VndbPanel.vue +++ b/src/components/VndbPanel.vue @@ -730,11 +730,12 @@ watch(() => searchStore.vndbInfo, async (newInfo) => { // 检查缓存的截图是否已加载 if (newInfo?.screenshots && newInfo.screenshots.length > 0) { - const vnIdForScreenshots = newInfo.id + const vnIdForScreenshots = newInfo?.id nextTick(() => { requestAnimationFrame(() => { - // 检查是否仍是同一个游戏 - if (currentVnId.value !== vnIdForScreenshots) { + // 只有当有有效的游戏 ID 时才进行竞态检查 + // 如果没有 ID,则无法进行有意义的检查,直接处理截图 + if (vnIdForScreenshots && currentVnId.value !== vnIdForScreenshots) { return } const screenshotImgs = modalRef.value?.querySelectorAll('img[alt*="截图"]') From 62c0abd4ac7bcb666cf68867a841ffa8ed15825a Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Fri, 26 Dec 2025 21:47:36 +0800 Subject: [PATCH 04/12] Refactor modals to use consistent structure and remove window management features - Updated CommentsModal.vue, SettingsModal.vue, and VndbPanel.vue to change the comment, settings, and VNDB panels from floating windows to modal dialogs. - Removed the WindowResizeHandles component and related window management logic to simplify the modal implementation. - Enhanced the styling and structure of modals for improved consistency across the application. --- public/sw.js | 19 ++- src/components/CommentsModal.vue | 82 +--------- src/components/SettingsModal.vue | 85 +--------- src/components/VndbPanel.vue | 77 +-------- src/components/WindowResizeHandles.vue | 60 ------- src/composables/useWindowManager.ts | 212 ------------------------- 6 files changed, 28 insertions(+), 507 deletions(-) delete mode 100644 src/components/WindowResizeHandles.vue delete mode 100644 src/composables/useWindowManager.ts diff --git a/public/sw.js b/public/sw.js index 832126c..781855a 100644 --- a/public/sw.js +++ b/public/sw.js @@ -41,15 +41,18 @@ const NO_CACHE_PATTERNS = [ self.addEventListener('install', (event) => { console.log(`[SW] Installing version ${SW_VERSION}`); + // 预缓存失败不应阻止 service worker 安装 + const precachePromise = caches.open(CACHE_NAME) + .then((cache) => { + console.log('[SW] Precaching core resources'); + return cache.addAll(PRECACHE_URLS); + }) + .catch((err) => { + console.warn('[SW] Precache failed:', err); + }); + event.waitUntil( - caches.open(CACHE_NAME) - .then((cache) => { - console.log('[SW] Precaching core resources'); - return cache.addAll(PRECACHE_URLS).catch((err) => { - console.warn('[SW] Precache failed:', err); - }); - }) - .then(() => self.skipWaiting()) + precachePromise.then(() => self.skipWaiting()) ); }); diff --git a/src/components/CommentsModal.vue b/src/components/CommentsModal.vue index 8843cb0..fc14d81 100644 --- a/src/components/CommentsModal.vue +++ b/src/components/CommentsModal.vue @@ -1,6 +1,6 @@