Add minimal installable PWA support
test / pytest (push) Successful in 37s

- serve manifest and service worker from the app root for install compatibility
- add manifest metadata, service worker registration, and Apple touch icon links to the base template
- add install icon assets for Android, iOS, and desktop install flows
- document deployment and validation notes for the new PWA support
- cover the new endpoints and template output with tests
This commit is contained in:
2026-04-23 15:23:20 +02:00
parent 49a5452141
commit ed1e3311a5
10 changed files with 140 additions and 1 deletions
+17 -1
View File
@@ -1,7 +1,8 @@
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status
from fastapi.responses import RedirectResponse, Response
from fastapi.responses import FileResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlalchemy import func, or_
@@ -12,6 +13,7 @@ from app.images import process_upload
from app.models import Box, Item, SubItem
templates = Jinja2Templates(directory="app/templates")
STATIC_DIR = Path("app/static")
def _clean_text(value: str | None) -> str | None:
@@ -193,6 +195,20 @@ def create_app() -> FastAPI:
def root() -> RedirectResponse:
return RedirectResponse(url="/boxes", status_code=status.HTTP_302_FOUND)
@app.get("/manifest.webmanifest", include_in_schema=False)
def manifest() -> FileResponse:
return FileResponse(
path=STATIC_DIR / "manifest.webmanifest",
media_type="application/manifest+json",
)
@app.get("/service-worker.js", include_in_schema=False)
def service_worker() -> FileResponse:
return FileResponse(
path=STATIC_DIR / "service-worker.js",
media_type="application/javascript",
)
@app.get("/search")
def search_page(
request: Request,
Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+31
View File
@@ -0,0 +1,31 @@
{
"name": "搬家助手",
"short_name": "搬家助手",
"description": "用于记录搬家装箱内容并快速搜索的轻量工具。",
"start_url": "/boxes",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#f4f4f4",
"theme_color": "#0b57d0",
"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-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+7
View File
@@ -0,0 +1,7 @@
self.addEventListener("install", function () {
self.skipWaiting();
});
self.addEventListener("activate", function (event) {
event.waitUntil(self.clients.claim());
});
+14
View File
@@ -3,7 +3,15 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0b57d0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="搬家助手">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>{{ page_title or "搬家助手" }}</title>
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icons/icon-192.png" sizes="192x192" type="image/png">
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png" sizes="180x180">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
@@ -15,6 +23,12 @@
{% block content %}{% endblock %}
</main>
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("/service-worker.js");
});
}
document.addEventListener("click", function (event) {
const card = event.target.closest(".clickable-card[data-href]");
if (!card) return;