From 921dd61b59fa04fcaaa793b52f078ba1907ea918 Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Sun, 14 Dec 2025 09:20:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E4=B8=8E=E4=BC=98=E5=8C=96=E6=9C=8D=E5=8A=A1=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E8=80=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 移除 `vue-router` 依赖,简化项目结构。 * 更新 `package.json` 和 `pnpm-lock.yaml`,确保依赖项的整洁性。 * 优化服务工作者 `sw.js`,引入缓存策略和版本管理,提升 PWA 性能。 * 更新多个组件的样式,增强用户界面的视觉一致性。 * 调整 `vite.config.ts`,引入新的插件以支持服务工作者版本管理。 --- package.json | 3 +- pnpm-lock.yaml | 18 -- public/sw.js | 239 ++++++++++++++----- scripts/sw-version-plugin.ts | 125 ++++++++++ src/App.vue | 60 +---- src/components/CommentsModal.vue | 72 +++--- src/components/FloatingButtons.vue | 186 +++++---------- src/components/SearchHistoryModal.vue | 294 ++++++++---------------- src/components/SettingsModal.vue | 49 ++-- src/components/UpdateToast.vue | 6 + src/components/VndbPanel.vue | 66 ++++-- src/composables/useKeyboardShortcuts.ts | 48 ++-- src/composables/useProgress.ts | 19 -- src/main.ts | 57 +++-- src/router/index.ts | 88 ------- vite.config.ts | 6 + 16 files changed, 647 insertions(+), 689 deletions(-) create mode 100644 scripts/sw-version-plugin.ts delete mode 100644 src/router/index.ts diff --git a/package.json b/package.json index b828039..445e567 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "nprogress": "^0.2.0", "pinia": "^3.0.4", "quicklink": "^3.0.1", - "vue": "^3.5.25", - "vue-router": "^4.6.4" + "vue": "^3.5.25" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a76b801..cc9a0d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: vue: specifier: ^3.5.25 version: 3.5.25(typescript@5.9.3) - vue-router: - specifier: ^4.6.4 - version: 4.6.4(vue@3.5.25(typescript@5.9.3)) devDependencies: '@eslint/js': specifier: ^9.39.2 @@ -644,9 +641,6 @@ packages: '@vue/compiler-ssr@3.5.25': resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} - '@vue/devtools-api@6.6.4': - resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-api@7.7.8': resolution: {integrity: sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==} @@ -1371,11 +1365,6 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 - vue-router@4.6.4: - resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} - peerDependencies: - vue: ^3.5.0 - vue-tsc@3.1.8: resolution: {integrity: sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA==} hasBin: true @@ -1884,8 +1873,6 @@ snapshots: '@vue/compiler-dom': 3.5.25 '@vue/shared': 3.5.25 - '@vue/devtools-api@6.6.4': {} - '@vue/devtools-api@7.7.8': dependencies: '@vue/devtools-kit': 7.7.8 @@ -2567,11 +2554,6 @@ snapshots: transitivePeerDependencies: - supports-color - vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)): - dependencies: - '@vue/devtools-api': 6.6.4 - vue: 3.5.25(typescript@5.9.3) - vue-tsc@3.1.8(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.26 diff --git a/public/sw.js b/public/sw.js index 1a5c903..91f6df7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,109 +1,228 @@ // Service Worker - PWA 支持 -const SW_VERSION = '2.1.0'; -const CACHE_NAME = `searchgal-cache-v${SW_VERSION}`; -const STATIC_CACHE = [ +// 版本由构建时自动注入,如未注入则使用时间戳 +const SW_VERSION = self.__SW_VERSION__ || Date.now().toString(36); +const CACHE_NAME = `searchgal-cache-${SW_VERSION}`; + +// 静态资源缓存列表(核心资源) +const CORE_ASSETS = [ '/', '/index.html', '/manifest.json', '/favicon.svg', ]; -// 安装事件 +// 缓存策略配置 +const CACHE_STRATEGIES = { + // 缓存优先(适用于静态资源) + cacheFirst: ['script', 'style', 'font'], + // 网络优先(适用于动态内容) + networkFirst: ['document'], + // 仅网络(适用于 API 请求) + networkOnly: [], + // 过期时间(毫秒) + maxAge: { + image: 7 * 24 * 60 * 60 * 1000, // 图片 7 天 + script: 24 * 60 * 60 * 1000, // 脚本 1 天 + style: 24 * 60 * 60 * 1000, // 样式 1 天 + font: 30 * 24 * 60 * 60 * 1000, // 字体 30 天 + }, +}; + +// 安装事件 - 预缓存核心资源 self.addEventListener('install', (event) => { + console.log(`[SW] Installing version ${SW_VERSION}`); + event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - return cache.addAll(STATIC_CACHE); + caches.open(CACHE_NAME) + .then((cache) => { + console.log('[SW] Pre-caching core assets'); + return cache.addAll(CORE_ASSETS); + }) + .then(() => { + console.log('[SW] Installation complete'); }) ); - // 立即激活新版本 + + // 立即激活新版本,不等待旧版本关闭 self.skipWaiting(); }); // 激活事件 - 清理旧缓存 self.addEventListener('activate', (event) => { + console.log(`[SW] Activating version ${SW_VERSION}`); + event.waitUntil( - caches.keys().then((cacheNames) => { + caches.keys() + .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { - if (cacheName !== CACHE_NAME) { + // 删除所有不匹配当前版本的缓存 + if (cacheName.startsWith('searchgal-cache-') && cacheName !== CACHE_NAME) { + console.log(`[SW] Deleting old cache: ${cacheName}`); return caches.delete(cacheName); } }) ); }) + .then(() => { + console.log('[SW] Activation complete'); + }) ); + // 立即接管所有客户端 self.clients.claim(); }); -// 监听消息 - 用于版本检查 +// 消息处理 self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'GET_VERSION') { - event.ports[0].postMessage({ version: SW_VERSION }); - } + const { type, payload } = event.data || {}; - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting(); + switch (type) { + case 'GET_VERSION': + // 返回当前版本 + event.ports[0]?.postMessage({ + version: SW_VERSION, + cacheSize: null, // 可扩展:返回缓存大小 + }); + break; + + case 'SKIP_WAITING': + // 跳过等待,立即激活 + self.skipWaiting(); + break; + + case 'CLEAR_CACHE': + // 清除所有缓存 + event.waitUntil( + caches.keys().then((names) => { + return Promise.all(names.map((name) => caches.delete(name))); + }).then(() => { + event.ports[0]?.postMessage({ success: true }); + }) + ); + break; + + case 'GET_CACHE_INFO': + // 获取缓存信息 + event.waitUntil( + caches.keys().then(async (names) => { + const info = {}; + for (const name of names) { + const cache = await caches.open(name); + const keys = await cache.keys(); + info[name] = keys.length; + } + event.ports[0]?.postMessage({ caches: info, current: CACHE_NAME }); + }) + ); + break; } }); -// 拦截请求 +// 请求拦截 self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); - // 只缓存同源请求 + // 跳过非 GET 请求 + if (request.method !== 'GET') { + return; + } + + // 跳过 chrome-extension 等非 http(s) 请求 + if (!url.protocol.startsWith('http')) { + return; + } + + // 跳过 API 请求(不缓存动态数据) + if (url.pathname.startsWith('/api/') || url.hostname.includes('api.')) { + return; + } + + // 外部资源:网络优先 if (url.origin !== location.origin) { - // 对于外部资源,使用网络优先策略 - event.respondWith( - fetch(request).catch(() => { - return caches.match(request); - }) - ); + event.respondWith(networkFirst(request)); return; } - // 对于静态资源,使用缓存优先策略 - if ( - request.destination === 'script' || - request.destination === 'style' || - request.destination === 'image' || - request.destination === 'font' - ) { - event.respondWith( - caches.match(request).then((response) => { - if (response) { + // 根据资源类型选择策略 + const destination = request.destination; + + if (CACHE_STRATEGIES.cacheFirst.includes(destination)) { + // 静态资源:缓存优先 + 后台更新 + event.respondWith(staleWhileRevalidate(request)); + } else if (destination === 'image') { + // 图片:缓存优先(更长的过期时间) + event.respondWith(cacheFirst(request)); + } else { + // 其他(HTML等):网络优先 + event.respondWith(networkFirst(request)); + } +}); + +/** + * 缓存优先策略 + * 优先返回缓存,缓存不存在时从网络获取并缓存 + */ +async function cacheFirst(request) { + const cached = await caches.match(request); + if (cached) { + return cached; + } + + try { + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(CACHE_NAME); + cache.put(request, response.clone()); + } return response; - } - return fetch(request).then((response) => { - // 缓存新的资源 - if (response.status === 200) { - const responseClone = response.clone(); - caches.open(CACHE_NAME).then((cache) => { - cache.put(request, responseClone); - }); + } catch (error) { + // 网络失败时返回离线页面(如果有) + return new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); + } +} + +/** + * 网络优先策略 + * 优先从网络获取,网络失败时返回缓存 + */ +async function networkFirst(request) { + try { + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(CACHE_NAME); + cache.put(request, response.clone()); } return response; - }); - }) - ); - return; + } catch (error) { + const cached = await caches.match(request); + if (cached) { + return cached; + } + return new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); } +} - // 对于 HTML 页面,使用网络优先策略 - event.respondWith( - fetch(request) - .then((response) => { - if (response.status === 200) { - const responseClone = response.clone(); - caches.open(CACHE_NAME).then((cache) => { - cache.put(request, responseClone); - }); +/** + * Stale-While-Revalidate 策略 + * 立即返回缓存,同时在后台更新缓存 + */ +async function staleWhileRevalidate(request) { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + + // 后台更新缓存 + const fetchPromise = fetch(request).then((response) => { + if (response.ok) { + cache.put(request, response.clone()); } return response; - }) - .catch(() => { - return caches.match(request); - }) - ); -}); + }).catch(() => null); + + // 有缓存则立即返回,否则等待网络 + return cached || fetchPromise || new Response('Offline', { status: 503 }); +} + +// 输出版本信息 +console.log(`[SW] Service Worker loaded, version: ${SW_VERSION}`); diff --git a/scripts/sw-version-plugin.ts b/scripts/sw-version-plugin.ts new file mode 100644 index 0000000..8f0287e --- /dev/null +++ b/scripts/sw-version-plugin.ts @@ -0,0 +1,125 @@ +/** + * Vite 插件:Service Worker 版本自动注入 + * + * 在构建时自动将版本信息注入到 sw.js 中 + * 版本格式:构建时间戳的 base36 编码(如 "m5x7k9a") + */ + +import type { Plugin } from 'vite' +import { readFileSync, writeFileSync, existsSync } from 'fs' +import { resolve } from 'path' +import { execSync } from 'child_process' + +interface SwVersionPluginOptions { + /** sw.js 文件路径(相对于 public 目录) */ + swPath?: string + /** 是否包含 git commit hash */ + includeGitHash?: boolean + /** 自定义版本前缀 */ + prefix?: string +} + +/** + * 生成版本号 + * 格式:时间戳base36 + 可选的git短hash + */ +function generateVersion(includeGitHash: boolean, prefix: string): string { + const timestamp = Date.now().toString(36) + + let gitHash = '' + if (includeGitHash) { + try { + gitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim() + } catch { + // git 不可用时忽略 + } + } + + const parts = [prefix, timestamp, gitHash].filter(Boolean) + return parts.join('-') +} + +/** + * 获取构建信息 + */ +function getBuildInfo(): Record { + const info: Record = { + buildTime: new Date().toISOString(), + nodeVersion: process.version, + } + + try { + info.gitBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim() + info.gitCommit = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim() + } catch { + // git 不可用时忽略 + } + + return info +} + +export function swVersionPlugin(options: SwVersionPluginOptions = {}): Plugin { + const { + swPath = 'sw.js', + includeGitHash = true, + prefix = '', + } = options + + let version: string + let outDir: string + + return { + name: 'sw-version-plugin', + + // 配置阶段获取输出目录 + configResolved(config) { + outDir = config.build.outDir + }, + + // 构建开始时生成版本号 + buildStart() { + version = generateVersion(includeGitHash, prefix) + const buildInfo = getBuildInfo() + + console.log('\n📦 SW Version Plugin') + console.log(` Version: ${version}`) + console.log(` Build Time: ${buildInfo.buildTime}`) + if (buildInfo.gitCommit) { + console.log(` Git: ${buildInfo.gitBranch}@${buildInfo.gitCommit}`) + } + console.log('') + }, + + // 构建完成后注入版本到 sw.js + closeBundle() { + const swFilePath = resolve(outDir, swPath) + + if (!existsSync(swFilePath)) { + console.warn(`[sw-version-plugin] Warning: ${swPath} not found in ${outDir}`) + return + } + + let content = readFileSync(swFilePath, 'utf-8') + + // 注入版本号 + // 方式1:替换 self.__SW_VERSION__ + content = content.replace( + /self\.__SW_VERSION__/g, + `'${version}'`, + ) + + // 方式2:替换旧的硬编码版本(如果有) + content = content.replace( + /const SW_VERSION = ['"][^'"]*['"]/, + `const SW_VERSION = '${version}'`, + ) + + writeFileSync(swFilePath, content) + + console.log(`✅ SW version injected: ${version}`) + }, + } +} + +export default swVersionPlugin + diff --git a/src/App.vue b/src/App.vue index d491e76..f51d402 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,7 +18,7 @@
- + @@ -29,7 +29,7 @@ :is-open="uiStore.isSettingsModalOpen" :custom-api="searchStore.customApi" :custom-c-s-s="uiStore.customCSS" - @close="navigateToHome" + @close="uiStore.isSettingsModalOpen = false" @save="saveSettings" /> @@ -43,8 +43,7 @@ diff --git a/src/components/SearchHistoryModal.vue b/src/components/SearchHistoryModal.vue index 9d7644b..1c5d0b4 100644 --- a/src/components/SearchHistoryModal.vue +++ b/src/components/SearchHistoryModal.vue @@ -1,189 +1,127 @@