diff --git a/README.md b/README.md index 9d9e139..5796374 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,29 @@ - Box / Item / SubItem 基础 CRUD - Box / Item / SubItem 单图上传、替换、删除、展示 - Box / Item / SubItem 全局搜索 +- 最小 PWA 安装支持(主屏幕 / 桌面安装) - Docker / Compose 长期运行 - SQLite 数据持久化 - 基础自动化测试 +## PWA 安装支持 + +当前版本在不改变 FastAPI + Jinja2 SSR 结构的前提下,补充了最小可维护的 PWA 能力: + +- 提供根路径 `manifest.webmanifest` +- 提供根路径 `service-worker.js` +- 在基础模板中注入 `manifest`、`theme-color`、`apple-touch-icon` 和安装相关 meta +- 支持 Android Chrome 添加到主屏幕 +- 支持 iPhone Safari 添加到主屏幕 +- 支持桌面 Chrome / Edge 安装为独立 app 窗口 + +当前新增的安装图标尺寸: + +- `180x180`:Apple touch icon +- `192x192`:Android / Chromium 安装图标 +- `512x512`:高分辨率安装图标 +- `512x512`:maskable 图标 + ## 当前数据模型 这个项目不是无限树结构,而是固定最多 3 级: @@ -110,6 +129,9 @@ Box 这一阶段仍然没有实现以下内容: +- 离线访问 +- 离线缓存策略 +- 离线数据同步 - 多图上传 - OCR - AI 识别物品 @@ -223,6 +245,22 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 10000 http://localhost:10000 ``` +本地开发验证 PWA 时,页面与安装元数据可以直接检查;如果要完整验证桌面安装体验,优先在 HTTPS 或受信任反向代理环境下测试。 + +## PWA 部署注意事项 + +- 生产环境应使用 HTTPS;Android Chrome 和桌面 Chrome / Edge 的安装能力通常要求安全上下文 +- `manifest.webmanifest` 需要返回 `application/manifest+json` +- `service-worker.js` 需要从站点根路径返回,保证作用域覆盖整个应用 +- 如果前面有 nginx 或其他反向代理,不要拦截或改写这两个根路径资源 +- iPhone Safari 的“添加到主屏幕”主要依赖 meta 和 `apple-touch-icon`,不包含离线能力 + +## PWA 简单验收 + +1. Android Chrome:打开站点,确认浏览器菜单或地址栏出现“添加到主屏幕”或“安装应用”。 +2. iPhone Safari:打开站点,点击分享菜单,确认可见“添加到主屏幕”。 +3. Desktop Chrome / Edge:打开站点,确认地址栏或菜单中出现“安装应用”。 + 本地默认数据库位置: ```text diff --git a/app/main.py b/app/main.py index 71dbca8..4c11cdf 100644 --- a/app/main.py +++ b/app/main.py @@ -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, diff --git a/app/static/icons/apple-touch-icon.png b/app/static/icons/apple-touch-icon.png new file mode 100644 index 0000000..2d8c5e1 Binary files /dev/null and b/app/static/icons/apple-touch-icon.png differ diff --git a/app/static/icons/icon-192.png b/app/static/icons/icon-192.png new file mode 100644 index 0000000..483ae33 Binary files /dev/null and b/app/static/icons/icon-192.png differ diff --git a/app/static/icons/icon-512-maskable.png b/app/static/icons/icon-512-maskable.png new file mode 100644 index 0000000..d4b89f9 Binary files /dev/null and b/app/static/icons/icon-512-maskable.png differ diff --git a/app/static/icons/icon-512.png b/app/static/icons/icon-512.png new file mode 100644 index 0000000..d9333b5 Binary files /dev/null and b/app/static/icons/icon-512.png differ diff --git a/app/static/manifest.webmanifest b/app/static/manifest.webmanifest new file mode 100644 index 0000000..4b88e1c --- /dev/null +++ b/app/static/manifest.webmanifest @@ -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" + } + ] +} \ No newline at end of file diff --git a/app/static/service-worker.js b/app/static/service-worker.js new file mode 100644 index 0000000..294c0f8 --- /dev/null +++ b/app/static/service-worker.js @@ -0,0 +1,7 @@ +self.addEventListener("install", function () { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 0d18852..72325e8 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -3,7 +3,15 @@ + + + + + {{ page_title or "搬家助手" }} + + + @@ -15,6 +23,12 @@ {% block content %}{% endblock %}