mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-03-15 04:53:18 +08:00
feat: integrate PWA support with vite-plugin-pwa and remove legacy service worker files
- Added vite-plugin-pwa for enhanced PWA capabilities, including automatic service worker registration and caching strategies. - Updated manifest configuration directly in vite.config.ts, removing the need for a separate manifest.json file. - Removed legacy service worker and associated versioning plugin, streamlining the PWA setup. - Adjusted UI components to reflect the new update handling mechanism for service workers.
This commit is contained in:
1
env.d.ts
vendored
1
env.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-pwa/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
@@ -112,8 +112,7 @@
|
||||
<link rel="alternate icon" type="image/png" sizes="16x16" href="/logo.svg" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/logo.svg" />
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<!-- PWA Meta Tags (manifest 由 vite-plugin-pwa 自动注入) -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
10
package.json
10
package.json
@@ -23,17 +23,19 @@
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"vite": "^7.3.0",
|
||||
"vue-tsc": "^3.2.1"
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vue-tsc": "^3.2.1",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/noto-sans-sc": "^5.2.8",
|
||||
"@tanstack/vue-virtual": "^3.13.13",
|
||||
"artalk": "^2.9.1",
|
||||
"lucide-vue-next": "^0.562.0",
|
||||
"pinia": "^3.0.4",
|
||||
"prismjs": "^1.30.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"@tanstack/vue-virtual": "^3.13.13"
|
||||
"vue-prism-editor": "2.0.0-alpha.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2"
|
||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||
}
|
||||
|
||||
3144
pnpm-lock.yaml
generated
3144
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"name": "SearchGal - Galgame 聚合搜索",
|
||||
"short_name": "SearchGal",
|
||||
"description": "多平台 Galgame 资源聚合搜索引擎",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff5fa",
|
||||
"theme_color": "#ff1493",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"lang": "zh-CN",
|
||||
"dir": "ltr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"categories": ["entertainment", "games", "utilities"],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "1920x1080",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "游戏模式搜索",
|
||||
"short_name": "游戏搜索",
|
||||
"description": "快速搜索游戏资源",
|
||||
"url": "/?mode=game",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "高级模式搜索",
|
||||
"short_name": "高级搜索",
|
||||
"description": "高级工具和资源搜索",
|
||||
"url": "/?mode=advanced",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"sizes": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/",
|
||||
"method": "GET",
|
||||
"enctype": "application/x-www-form-urlencoded",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
157
public/sw.js
157
public/sw.js
@@ -1,157 +0,0 @@
|
||||
// Service Worker - SearchGal PWA
|
||||
// 版本由构建工具注入
|
||||
const VERSION = self.__SW_VERSION__ || Date.now().toString(36)
|
||||
const CACHE = `searchgal-${VERSION}`
|
||||
|
||||
// 缓存规则
|
||||
const STATIC_EXT = /\.(js|css|mjs|woff2?|ttf|png|jpg|jpeg|gif|webp|svg|ico|avif|wasm)$/i
|
||||
const SKIP = /\/(api|sw\.js|__vite|hot-update)|\.map$/
|
||||
|
||||
// ============================================
|
||||
// 生命周期
|
||||
// ============================================
|
||||
|
||||
self.addEventListener('install', (e) => {
|
||||
console.log(`[SW] Install v${VERSION}`)
|
||||
e.waitUntil(self.skipWaiting())
|
||||
})
|
||||
|
||||
self.addEventListener('activate', (e) => {
|
||||
console.log(`[SW] Activate v${VERSION}`)
|
||||
e.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(
|
||||
keys.filter((k) => k.startsWith('searchgal-') && k !== CACHE)
|
||||
.map((k) => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
)
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// 消息
|
||||
// ============================================
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
const { type } = e.data || {}
|
||||
|
||||
if (type === 'GET_VERSION') {
|
||||
e.ports[0]?.postMessage({ version: VERSION })
|
||||
} else if (type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
} else if (type === 'CLEAR_CACHE') {
|
||||
e.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(keys.map((k) => caches.delete(k))))
|
||||
.then(() => e.ports[0]?.postMessage({ success: true }))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// 请求拦截
|
||||
// ============================================
|
||||
|
||||
self.addEventListener('fetch', (e) => {
|
||||
const { request } = e
|
||||
const url = new URL(request.url)
|
||||
|
||||
// 跳过:非 GET、非 HTTP、跨域、特殊路径
|
||||
if (
|
||||
request.method !== 'GET' ||
|
||||
!url.protocol.startsWith('http') ||
|
||||
url.origin !== location.origin ||
|
||||
SKIP.test(url.href)
|
||||
) return
|
||||
|
||||
const isDocument = request.destination === 'document'
|
||||
const isStatic = STATIC_EXT.test(url.pathname)
|
||||
|
||||
if (isDocument) {
|
||||
// HTML:网络优先,离线显示提示
|
||||
e.respondWith(networkFirst(request, true))
|
||||
} else if (isStatic) {
|
||||
// 静态资源:缓存优先
|
||||
e.respondWith(cacheFirst(request))
|
||||
} else {
|
||||
// 其他:网络优先
|
||||
e.respondWith(networkFirst(request, false))
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// 缓存策略
|
||||
// ============================================
|
||||
|
||||
async function cacheFirst(req) {
|
||||
const cached = await caches.match(req)
|
||||
if (cached) return cached
|
||||
|
||||
try {
|
||||
const res = await fetch(req)
|
||||
if (res.ok) {
|
||||
const cache = await caches.open(CACHE)
|
||||
cache.put(req, res.clone())
|
||||
}
|
||||
return res
|
||||
} catch {
|
||||
return new Response('', { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
async function networkFirst(req, showOffline) {
|
||||
try {
|
||||
const res = await fetch(req)
|
||||
if (res.ok) {
|
||||
const cache = await caches.open(CACHE)
|
||||
cache.put(req, res.clone())
|
||||
}
|
||||
return res
|
||||
} catch {
|
||||
const cached = await caches.match(req)
|
||||
if (cached) return cached
|
||||
return showOffline ? offlinePage() : new Response('', { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 离线页面
|
||||
// ============================================
|
||||
|
||||
function offlinePage() {
|
||||
return new Response(`<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>离线 - SearchGal</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:system-ui,-apple-system,sans-serif;background:linear-gradient(135deg,#fff5fa,#ffe4f0);padding:1.5rem}
|
||||
.c{text-align:center;max-width:360px}
|
||||
.i{font-size:4rem;margin-bottom:1rem;animation:f 2s ease-in-out infinite}
|
||||
@keyframes f{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
||||
h1{font-size:1.5rem;margin-bottom:.5rem;background:linear-gradient(135deg,#ff1493,#d946ef);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
||||
p{color:#666;line-height:1.5;margin-bottom:1.5rem}
|
||||
button{padding:.875rem 2rem;background:linear-gradient(135deg,#ff1493,#d946ef);color:#fff;border:none;border-radius:9999px;font-size:.9rem;font-weight:500;cursor:pointer;box-shadow:0 4px 12px rgba(255,20,147,.3);transition:transform .2s,box-shadow .2s}
|
||||
button:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(255,20,147,.4)}
|
||||
button:active{transform:translateY(0)}
|
||||
.h{margin-top:1.5rem;font-size:.8rem;color:#999}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="c">
|
||||
<div class="i">🌐</div>
|
||||
<h1>需要网络连接</h1>
|
||||
<p>SearchGal 是在线搜索服务,请连接网络后使用</p>
|
||||
<button onclick="location.reload()">重新连接</button>
|
||||
<p class="h">检查网络是否正常</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`, {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/html;charset=utf-8' }
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`[SW] Ready v${VERSION}`)
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Vite 插件:Service Worker 版本注入
|
||||
* 构建时自动将版本号注入到 sw.js
|
||||
*/
|
||||
|
||||
import type { Plugin } from 'vite'
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
interface Options {
|
||||
/** sw.js 路径(相对于输出目录) */
|
||||
swPath?: string
|
||||
/** 是否包含 git commit hash */
|
||||
includeGitHash?: boolean
|
||||
}
|
||||
|
||||
/** 获取 git 短 hash,失败返回空 */
|
||||
function getGitHash(): string {
|
||||
try {
|
||||
return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成版本号:时间戳base36[-gitHash] */
|
||||
function generateVersion(includeGitHash: boolean): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const gitHash = includeGitHash ? getGitHash() : ''
|
||||
return gitHash ? `${timestamp}-${gitHash}` : timestamp
|
||||
}
|
||||
|
||||
export function swVersionPlugin(options: Options = {}): Plugin {
|
||||
const { swPath = 'sw.js', includeGitHash = true } = options
|
||||
|
||||
let version = ''
|
||||
let outDir = 'dist'
|
||||
|
||||
return {
|
||||
name: 'sw-version',
|
||||
|
||||
configResolved(config) {
|
||||
outDir = config.build.outDir
|
||||
},
|
||||
|
||||
buildStart() {
|
||||
version = generateVersion(includeGitHash)
|
||||
console.info(`\n📦 SW Version: ${version}\n`)
|
||||
},
|
||||
|
||||
closeBundle() {
|
||||
const filePath = resolve(outDir, swPath)
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
console.warn(`[sw-version] ${swPath} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
// 替换版本占位符
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
.replace(/self\.__SW_VERSION__/g, `'${version}'`)
|
||||
|
||||
writeFileSync(filePath, content)
|
||||
console.info(`✅ SW version: ${version}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default swVersionPlugin
|
||||
13
src/App.vue
13
src/App.vue
@@ -56,10 +56,7 @@
|
||||
<ImageViewer />
|
||||
|
||||
<!-- SW 更新提示 -->
|
||||
<UpdateToast
|
||||
:is-visible="uiStore.showUpdateToast"
|
||||
:on-update="handleSwUpdate"
|
||||
/>
|
||||
<UpdateToast />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,14 +118,6 @@ function openSettings() {
|
||||
uiStore.toggleSettingsModal()
|
||||
}
|
||||
|
||||
// 处理 SW 更新
|
||||
function handleSwUpdate() {
|
||||
// SW 更新逻辑由 main.ts 的 controllerchange 事件处理
|
||||
// 这里只需要关闭提示并发送消息给 SW
|
||||
uiStore.setShowUpdateToast(false)
|
||||
navigator.serviceWorker.controller?.postMessage({ type: 'SKIP_WAITING' })
|
||||
}
|
||||
|
||||
// 处理历史记录选择
|
||||
function handleHistorySelect(item: { query: string; mode: 'game' | 'patch' }) {
|
||||
// 同步设置 store(用于其他地方读取)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
|
||||
<!-- 输入框 -->
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
:placeholder="searchMode === 'game' ? '搜索游戏...' : '搜索补丁...'"
|
||||
@@ -502,6 +503,7 @@ const historyStore = useHistoryStore()
|
||||
const searchQuery = ref('')
|
||||
const customApi = ref('')
|
||||
const searchMode = ref<'game' | 'patch'>('game')
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null)
|
||||
let cleanupURLListener: (() => void) | null = null
|
||||
let searchStartTime = 0
|
||||
|
||||
@@ -951,6 +953,11 @@ function searchWithParams(query: string, mode: 'game' | 'patch') {
|
||||
mode: mode,
|
||||
api: customApi.value,
|
||||
})
|
||||
|
||||
// 自动对焦到输入框
|
||||
setTimeout(() => {
|
||||
searchInputRef.value?.focus()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -8,79 +8,111 @@
|
||||
leave-to-class="opacity-0 translate-y-4 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
v-if="needRefresh"
|
||||
class="fixed bottom-4 left-1/2 -translate-x-1/2 z-[100] px-4 py-3 rounded-2xl bg-gradient-to-r from-[#ff1493] to-[#d946ef] text-white shadow-xl shadow-pink-500/30 flex items-center gap-3 max-w-[90vw] sm:max-w-md"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<RefreshCw :size="18" class="animate-spin" />
|
||||
<RefreshCw :size="18" :class="{ 'animate-spin': isUpdating }" />
|
||||
</div>
|
||||
|
||||
<!-- 文字内容 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-semibold text-sm">发现新版本</p>
|
||||
<p class="text-xs text-white/80 truncate">
|
||||
{{ countdown > 0 ? `${countdown} 秒后自动更新...` : '正在更新...' }}
|
||||
<template v-if="isUpdating">
|
||||
正在更新...
|
||||
</template>
|
||||
<template v-else-if="countdown > 0">
|
||||
{{ countdown }} 秒后自动更新...
|
||||
</template>
|
||||
<template v-else>
|
||||
准备更新...
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 立即更新按钮 -->
|
||||
<button
|
||||
v-if="!isUpdating && countdown > 0"
|
||||
class="flex-shrink-0 px-3 py-1.5 rounded-full bg-white/20 hover:bg-white/30 text-xs font-medium transition-colors"
|
||||
@click="handleUpdate"
|
||||
>
|
||||
立即更新
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
||||
import { playNotification } from '@/composables/useSound'
|
||||
|
||||
const props = defineProps<{
|
||||
isVisible: boolean
|
||||
onUpdate: () => void
|
||||
}>()
|
||||
const UPDATE_COUNTDOWN = 5 // 秒
|
||||
|
||||
const countdown = ref(5)
|
||||
let timer: number | null = null
|
||||
const countdown = ref(0)
|
||||
const isUpdating = ref(false)
|
||||
let countdownTimer: number | null = null
|
||||
|
||||
const {
|
||||
needRefresh,
|
||||
updateServiceWorker,
|
||||
} = useRegisterSW({
|
||||
immediate: true,
|
||||
onRegisteredSW(swUrl, r) {
|
||||
console.info(`[SW] Registered: ${swUrl}`)
|
||||
// 定期检查更新
|
||||
if (r) {
|
||||
setInterval(() => {
|
||||
void r.update()
|
||||
}, 5 * 60 * 1000) // 每 5 分钟检查一次
|
||||
}
|
||||
},
|
||||
onRegisterError(error) {
|
||||
console.error('[SW] Registration error:', error)
|
||||
},
|
||||
})
|
||||
|
||||
// 当有更新时启动倒计时
|
||||
watch(needRefresh, (need) => {
|
||||
if (need) {
|
||||
playNotification()
|
||||
startCountdown()
|
||||
}
|
||||
})
|
||||
|
||||
function startCountdown() {
|
||||
// 先清除可能存在的旧定时器,避免创建多个并发定时器
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
countdown.value = UPDATE_COUNTDOWN
|
||||
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
}
|
||||
|
||||
playNotification()
|
||||
countdown.value = 5
|
||||
timer = window.setInterval(() => {
|
||||
countdownTimer = window.setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
props.onUpdate()
|
||||
clearInterval(countdownTimer!)
|
||||
countdownTimer = null
|
||||
handleUpdate()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isVisible) {
|
||||
startCountdown()
|
||||
function handleUpdate() {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
})
|
||||
isUpdating.value = true
|
||||
// 更新 SW 并刷新页面
|
||||
void updateServiceWorker(true)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 isVisible 变化
|
||||
watch(() => props.isVisible, (visible) => {
|
||||
if (visible) {
|
||||
startCountdown()
|
||||
} else if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
62
src/main.ts
62
src/main.ts
@@ -75,7 +75,7 @@ app.mount('#app')
|
||||
// Pinia Stores 初始化
|
||||
// ============================================
|
||||
|
||||
// 获取 UI Store 用于 SW 更新通知
|
||||
// 获取 UI Store
|
||||
const uiStore = useUIStore()
|
||||
const statsStore = useStatsStore()
|
||||
|
||||
@@ -86,61 +86,7 @@ uiStore.init()
|
||||
statsStore.incrementPageView()
|
||||
|
||||
// ============================================
|
||||
// Service Worker 注册与更新
|
||||
// Service Worker (vite-plugin-pwa)
|
||||
// ============================================
|
||||
|
||||
// 显示更新提示 - 使用 UIStore 管理(由 UpdateToast 组件渲染)
|
||||
function showUpdateToast() {
|
||||
uiStore.setShowUpdateToast(true)
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
void (async () => {
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.register('/sw.js')
|
||||
console.info('[SW] Registered')
|
||||
|
||||
// 新版本检测
|
||||
reg.addEventListener('updatefound', () => {
|
||||
const worker = reg.installing
|
||||
if (!worker) {
|
||||
return
|
||||
}
|
||||
|
||||
worker.addEventListener('statechange', () => {
|
||||
// 新 SW 安装完成且有旧 SW 控制页面 = 有更新
|
||||
if (worker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
console.info('[SW] Update available')
|
||||
// 显示更新提示(UpdateToast 组件处理倒计时和更新)
|
||||
showUpdateToast()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 新 SW 激活后刷新页面
|
||||
let refreshing = false
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (refreshing) {
|
||||
return
|
||||
}
|
||||
refreshing = true
|
||||
console.info('[SW] New version activated, reloading...')
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
// 定期检查更新(5 分钟)
|
||||
setInterval(() => { void reg.update().catch(() => { /* 静默处理 */ }) }, 5 * 60 * 1000)
|
||||
|
||||
// 页面可见时检查更新
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void reg.update().catch(() => { /* 静默处理 */ })
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
// SW 注册由 vite-plugin-pwa 自动处理
|
||||
// 更新提示由 UpdateToast 组件处理
|
||||
|
||||
@@ -4,13 +4,13 @@ import type { SearchHistory } from '@/utils/persistence'
|
||||
import {
|
||||
loadSearchHistory,
|
||||
saveSearchHistory as persistSearchHistory,
|
||||
saveAllSearchHistory,
|
||||
clearSearchHistory as clearPersistedSearchHistory,
|
||||
} from '@/utils/persistence'
|
||||
|
||||
export const useHistoryStore = defineStore('history', () => {
|
||||
// 状态
|
||||
const searchHistory = ref<SearchHistory[]>([])
|
||||
const maxHistoryItems = ref(50) // 最多保存 50 条历史
|
||||
|
||||
// 计算属性
|
||||
const recentHistory = computed(() =>
|
||||
@@ -57,11 +57,6 @@ export const useHistoryStore = defineStore('history', () => {
|
||||
// 添加到开头
|
||||
searchHistory.value.unshift(newItem)
|
||||
|
||||
// 限制数量
|
||||
if (searchHistory.value.length > maxHistoryItems.value) {
|
||||
searchHistory.value = searchHistory.value.slice(0, maxHistoryItems.value)
|
||||
}
|
||||
|
||||
// 持久化
|
||||
persistSearchHistory(newItem)
|
||||
}
|
||||
@@ -87,10 +82,8 @@ export const useHistoryStore = defineStore('history', () => {
|
||||
}
|
||||
|
||||
function saveHistory() {
|
||||
// 由于持久化工具只能添加单条,这里重新保存所有历史
|
||||
searchHistory.value.forEach(item => {
|
||||
persistSearchHistory(item)
|
||||
})
|
||||
// 覆盖保存整个历史列表
|
||||
saveAllSearchHistory(searchHistory.value)
|
||||
}
|
||||
|
||||
function getHistoryStats() {
|
||||
@@ -128,7 +121,6 @@ export const useHistoryStore = defineStore('history', () => {
|
||||
return {
|
||||
// 状态
|
||||
searchHistory,
|
||||
maxHistoryItems,
|
||||
|
||||
// 计算属性
|
||||
recentHistory,
|
||||
|
||||
@@ -79,9 +79,6 @@ export const useUIStore = defineStore('ui', () => {
|
||||
// 加载状态
|
||||
const isLoading = ref(false)
|
||||
const loadingMessage = ref('')
|
||||
|
||||
// SW 更新状态
|
||||
const showUpdateToast = ref(false)
|
||||
|
||||
// 滚动位置(用于恢复)
|
||||
const scrollPosition = ref(0)
|
||||
@@ -466,11 +463,6 @@ export const useUIStore = defineStore('ui', () => {
|
||||
}, { passive: true })
|
||||
}
|
||||
|
||||
// 显示 SW 更新提示
|
||||
function setShowUpdateToast(show: boolean) {
|
||||
showUpdateToast.value = show
|
||||
}
|
||||
|
||||
// 清除会话状态(用于完全重置)
|
||||
function clearSessionState() {
|
||||
sessionStorage.removeItem(SESSION_KEY)
|
||||
@@ -493,7 +485,6 @@ export const useUIStore = defineStore('ui', () => {
|
||||
backgroundImageLoaded,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
showUpdateToast,
|
||||
toasts,
|
||||
isKeyboardHelpOpen,
|
||||
scrollPosition,
|
||||
@@ -526,7 +517,6 @@ export const useUIStore = defineStore('ui', () => {
|
||||
setBackgroundImage,
|
||||
setBackgroundImageLoaded,
|
||||
setLoading,
|
||||
setShowUpdateToast,
|
||||
showToast,
|
||||
removeToast,
|
||||
clearToasts,
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { PlatformData, VndbInfo } from '@/stores/search'
|
||||
|
||||
const STORAGE_KEY = 'searchgal_state'
|
||||
const STORAGE_VERSION = '1.0'
|
||||
const MAX_HISTORY_SIZE = 10 // 最多保存 10 条搜索历史
|
||||
|
||||
export interface SearchState {
|
||||
version: string
|
||||
@@ -106,11 +105,6 @@ export function saveSearchHistory(history: SearchHistory): void {
|
||||
// 添加新搜索到开头
|
||||
historyList.unshift(history)
|
||||
|
||||
// 限制历史记录数量
|
||||
if (historyList.length > MAX_HISTORY_SIZE) {
|
||||
historyList = historyList.slice(0, MAX_HISTORY_SIZE)
|
||||
}
|
||||
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(historyList))
|
||||
} catch (error) {
|
||||
// 静默处理
|
||||
@@ -149,6 +143,18 @@ export function clearSearchHistory(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖保存整个搜索历史列表
|
||||
*/
|
||||
export function saveAllSearchHistory(historyList: SearchHistory[]): void {
|
||||
try {
|
||||
const HISTORY_KEY = 'searchgal_history'
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(historyList))
|
||||
} catch (error) {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 localStorage 使用情况
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { swVersionPlugin } from './scripts/sw-version-plugin';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
@@ -26,10 +26,97 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
// 构建时自动注入 SW 版本号
|
||||
swVersionPlugin({
|
||||
swPath: 'sw.js',
|
||||
includeGitHash: true,
|
||||
// PWA 配置
|
||||
VitePWA({
|
||||
registerType: 'prompt', // 提示用户更新
|
||||
includeAssets: ['logo.svg', 'robots.txt'],
|
||||
manifest: {
|
||||
name: 'SearchGal - Galgame 聚合搜索',
|
||||
short_name: 'SearchGal',
|
||||
description: '多平台 Galgame 资源聚合搜索引擎',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#fff5fa',
|
||||
theme_color: '#ff1493',
|
||||
orientation: 'portrait-primary',
|
||||
scope: '/',
|
||||
lang: 'zh-CN',
|
||||
dir: 'ltr',
|
||||
icons: [
|
||||
{
|
||||
src: '/logo.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
categories: ['entertainment', 'games', 'utilities'],
|
||||
shortcuts: [
|
||||
{
|
||||
name: '游戏模式搜索',
|
||||
short_name: '游戏搜索',
|
||||
description: '快速搜索游戏资源',
|
||||
url: '/?mode=game',
|
||||
icons: [{ src: '/logo.svg', sizes: 'any' }],
|
||||
},
|
||||
{
|
||||
name: '补丁模式搜索',
|
||||
short_name: '补丁搜索',
|
||||
description: '搜索游戏补丁资源',
|
||||
url: '/?mode=patch',
|
||||
icons: [{ src: '/logo.svg', sizes: 'any' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
// 预缓存所有构建产物
|
||||
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
|
||||
// 运行时缓存策略
|
||||
runtimeCaching: [
|
||||
{
|
||||
// 字体文件 - 缓存优先
|
||||
urlPattern: /^https:\/\/fonts\.(googleapis|gstatic)\.com\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'google-fonts',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 年
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 图片 API - 网络优先
|
||||
urlPattern: /^https:\/\/api\.illlights\.com\/.*/i,
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'background-images',
|
||||
expiration: {
|
||||
maxEntries: 20,
|
||||
maxAgeSeconds: 60 * 60 * 24, // 1 天
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// VNDB API - 网络优先
|
||||
urlPattern: /^https:\/\/api\.vndb\.org\/.*/i,
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'vndb-api',
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 30, // 30 分钟
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
// 离线时的导航回退
|
||||
navigateFallback: null, // 不使用默认回退,由 offlineFallback 处理
|
||||
},
|
||||
// 开发环境启用 SW(便于测试)
|
||||
devOptions: {
|
||||
enabled: false, // 开发时禁用,避免干扰
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user