feat: PWA支持(manifest+Service Worker缓存+图标,可添加到主屏幕/离线/增量更新)

This commit is contained in:
gouki
2026-08-02 15:01:26 +00:00
parent f79d021307
commit cc7b2c67fe
7 changed files with 104 additions and 1 deletions
+11 -1
View File
@@ -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 动态读取,便于验证自动更新)"""
+13
View File
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ app_name }}</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#2563eb">
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="资产管理">
<script src="/static/js/tailwind.js"></script>
<script>tailwind.config = { darkMode: 'class' };</script>
<script src="/static/js/vue.global.prod.js"></script>
@@ -19,5 +25,12 @@
<div id="app"></div>
<script src="/static/js/api.js"></script>
<script src="/static/js/app.js"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js').catch(function () {});
});
}
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

+17
View File
@@ -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" }
]
}
+63
View File
@@ -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;
})
);
});