diff --git a/app/main.py b/app/main.py index 95fca83..ad1e7f0 100644 --- a/app/main.py +++ b/app/main.py @@ -11,7 +11,7 @@ from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from jinja2 import Environment, FileSystemLoader from sqlmodel import Session @@ -64,6 +64,16 @@ def index() -> HTMLResponse: return HTMLResponse(html) +@app.get("/sw.js", include_in_schema=False) +def service_worker() -> FileResponse: + """Service Worker(置于根路径以使 scope 覆盖全站)""" + return FileResponse( + STATIC_DIR / "sw.js", + media_type="application/javascript", + headers={"Service-Worker-Allowed": "/"}, + ) + + @app.get("/health") def health_check() -> dict: """健康检查(version 动态读取,便于验证自动更新)""" diff --git a/app/templates/index.html b/app/templates/index.html index 73d53a6..68d22e1 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -4,6 +4,12 @@ {{ app_name }} + + + + + + @@ -19,5 +25,12 @@
+ diff --git a/static/icons/icon-1024.png b/static/icons/icon-1024.png new file mode 100644 index 0000000..9a0b306 Binary files /dev/null and b/static/icons/icon-1024.png differ diff --git a/static/icons/icon-192.png b/static/icons/icon-192.png new file mode 100644 index 0000000..78a65c3 Binary files /dev/null and b/static/icons/icon-192.png differ diff --git a/static/icons/icon-512.png b/static/icons/icon-512.png new file mode 100644 index 0000000..497c43b Binary files /dev/null and b/static/icons/icon-512.png differ diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..5d7f343 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "VPS 资产管理系统", + "short_name": "资产管理", + "description": "个人数字资产管理:VPS、域名、AI 账号、Cloudflare,续费提醒与状态监控", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#2563eb", + "orientation": "any", + "lang": "zh-CN", + "icons": [ + { "src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000..f1f6391 --- /dev/null +++ b/static/sw.js @@ -0,0 +1,63 @@ +/* vps-manager Service Worker + * 缓存策略: + * - 预缓存核心静态资源,安装即可离线打开 + * - 静态资源与页面:stale-while-revalidate(先返回缓存秒开,后台自动拉取最新版本) + * - /api/* 数据请求:始终走网络,保证数据实时 + * 更新机制:业务代码更新后,后台自动同步到缓存,下次访问生效; + * 若需强制刷新缓存,递增 CACHE_NAME 即可清理旧缓存。 + */ +const CACHE_NAME = 'vps-manager-v1'; + +const PRECACHE_URLS = [ + '/', + '/static/js/vue.global.prod.js', + '/static/js/tailwind.js', + '/static/js/api.js', + '/static/js/app.js', + '/static/manifest.json', + '/static/icons/icon-192.png', + '/static/icons/icon-512.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(PRECACHE_URLS)) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + // 数据接口走网络,不缓存 + if (url.pathname.startsWith('/api/')) return; + + event.respondWith( + caches.open(CACHE_NAME).then(async (cache) => { + const cached = await cache.match(request); + const network = fetch(request) + .then((response) => { + if (response && response.ok) cache.put(request, response.clone()); + return response; + }) + .catch(() => cached); + return cached || network; + }) + ); +});