From ed1e3311a501e980ddbaa923b6e2fead57b39c93 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Thu, 23 Apr 2026 15:23:20 +0200 Subject: [PATCH] Add minimal installable PWA support - 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 --- README.md | 38 +++++++++++++++++++++++++ app/main.py | 18 +++++++++++- app/static/icons/apple-touch-icon.png | Bin 0 -> 837 bytes app/static/icons/icon-192.png | Bin 0 -> 890 bytes app/static/icons/icon-512-maskable.png | Bin 0 -> 2981 bytes app/static/icons/icon-512.png | Bin 0 -> 3076 bytes app/static/manifest.webmanifest | 31 ++++++++++++++++++++ app/static/service-worker.js | 7 +++++ app/templates/base.html | 14 +++++++++ tests/test_app.py | 33 +++++++++++++++++++++ 10 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 app/static/icons/apple-touch-icon.png create mode 100644 app/static/icons/icon-192.png create mode 100644 app/static/icons/icon-512-maskable.png create mode 100644 app/static/icons/icon-512.png create mode 100644 app/static/manifest.webmanifest create mode 100644 app/static/service-worker.js 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 0000000000000000000000000000000000000000..2d8c5e1daccb336a350b738bce9331ed5dcd7c58 GIT binary patch literal 837 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD4M^IaWiw)6VCM03aSW-L^Y-rE!Yd9E4G*o` zS_~~y9T-a-7}XYXU3U~!F>zqmJ21tO@9_l}z5<5wTd~r6>*mGmo^{cBerf%ukB>e* zd&eNCKkpYyf|}!GrHmjJ->wCof+m_xYOWWSaLf`x2v+V)x+p*Y-r9(13(Vv5maQwF z^Yd$<;^wPsHY{h+O-YmFt^Oewb>@u8t;yo8OL$mwtu`O>2v;-FoZW0TH!?Fj9>~Nr z6x}8>_S%mJ_opS!U0XZz0>~6Es40gXXI}!EeR+y|fbUA>xJ%m-=;l&{rjz} z(Thm&*H=nk{eJ7I_u{7b%lOwc1Ih7bKjnB0VN_H*07wtYd@7Btd^$VHUh~?j&h&Oru-jo}iD!z>05VTMd51z(0GD1y~HuN<O_X}$w`2cU*cVR*x|Ch^!E-`|CI?DL;p-~M=w z>cc=|{T-Lv{JlN@`oj9^$2+Vi-(XDR zIc)j*O5bkR-TVpPJZ(z%-YVRlb)8YC^wYek?PYg#Z^L;;_U*B$gpL@>R zugBk~@73GLd|*EZ_C&R1A;Y=9KP1znW;$DgQd+JB!*<6tnI-?;Ff8%q1je*f+L^ms zWF1&yF0HAYUB#`!q?hFH=U(D2iBouF38B1(W$sHE$|wA!O67A$0H6e2O-2`^1b##1})=2FS! zfvZWTcE}6GP@y}vik}@}7ZYPwII~MkVc*!*W0!|Hh?InPv|UeTR0T>j%e*(vQ5OSs zG(spJN5;-Mek)z!D3~5m4*S6ckyJ6)uk?O%0L~ZPNTu0rOt~D7*LQSh+Kr7jb-qV> zX-5+6Rwt<|Ue$|Ts&r--=Z2MlsO_Qyr`79RG;Gz5qti?SKM>)ewyI}$+_p&-lXL(d zO-(+(5s0>nj#D4<$0ITlf-1G^gyuanf?%>(xh z%Hd5(y@RJl6M|BXguC`!>&~WyHxCef4KlO--~=OZ8_<_Pqizeu__36ZaRo$(%pc*M zlq1-;E8TiSGbR&fT+=ywOD0Ao#mlN2^C$g4)E~Gp{mELQuXfLo3({vr4Kyg0_DDXx z&f*GgcM(IMn^PhYa(j+>UJq)a*%#)Z1Kw8yaM8-$+6?N zGWGwf~}dtl-ewoO8@9uC*@h5jr5HTnf6ckR_hE3W0g?SAkMdDPcRyLswrKbK7;hpC%q+cRYKlD z6az#b7Vg?t<+97(zH4pm3(WNZL>zbxWc;7Q0aIiD6qN7P`S{Oyn0tn{n?=hW7PhI{j)QVXluhi@qhLT=j$sD_6$HZ`vOBr zRtW2kA9(QE5v#CX(D*9)1I*K77F$xS8fcQ2#eoOkRJg*Bu^N#e8E9qe<6U#2CFCg7vW!--iJDhn>Er2U$77*-E;M$+aT#A6Ao*?EAU1&H(NkL zakHc|Ak>$JhX^$4qFIj3IMU{4x&I9GpXJEDG<;3^5cRli{DzoKu~F3@VaI+4caYVz literal 0 HcmV?d00001 diff --git a/app/static/icons/icon-512.png b/app/static/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d9333b5e41408678e46ca13e8d38b2c38c740661 GIT binary patch literal 3076 zcmeHJdoqnZ7tJ^a_dBxmza3Ki*&TsO>ZsG zI5@xD(~bLGey;oGa2pr5IYM4>JgcW^7aU>6`gb_?Rw|{sSWvM3Q$_cB^16CPS|4B+ z7#4K^BBdsPrVfC%5rDxG0Bb*h@6Q5Y);Iu9bcrLM@j%HMUwe^ugX;CQE4je^b4D&j zLBcj-mbADii}Ik{R1WE7f}_+yaFF9_o?W-$?1n`%FdSE95NZc{bl4Y{Z4f%p0D24< ziO5d9=?&~L2gr*!e#emvAt~DM*fB$S>I-ZzlgN!dFZrtr^3q=cJ9229nJEMj&9T)R zSpofc>I)kUPJOg2dM9F|)7%6)h6q@W|ACmT2rCvD!};q3tYWz=CQyw{4IwCo0M&8L zJ!Xag-1^xLx@JJ2+;Ra>+!Qk?Mn{e3VbkY`-(&?@s&sOi4ve<($PEYrcWzuzWi*w# zmVAfX8&H9q%2harOILyV=$;v>ic8hiY^;L;F0&{lAT#JSeG{+*Oz`2|G>%EF>X!@o5Fk{4igY-hQBjvl6K1Kg?Y$tb@V zNR?KXtwB^J&{FO_2`S~1XK|vcD5WldJnb`2`drgzXJz&|zG{1-TO!FUX1J&6FveQq z8!YOM#lO(2l<1WnN=a9Zr;pnzA4#&LXDE0#m;NZlQd-@yLh%I4W@>V>3B8t4A3Ro& zpCqn7n4{ZeO5vL(UU1aH8gQaL%ff4g2hU6tN9cFDG)?n4%)No(9+IU{b24~k`Bzh7 zJsM-OD5P6UVhdfpF}VUm#eSxvhb$*VNNh2N;TS={P>~{$d6WFQD5RIvVgSQ?CRztA z6$hA()qGX9r@m<=76#aBfxVwEKu~t&g60`ZW*Mg@Wu0p9DZ_c^y z^57*5(E8eA6yg-5V4*K>xF%Zh5+-TV=@!1 zV4><^Vmwf`Ph7$!Sgqjy@|y-#_>7(cJ-i`V%7+XTBMo@Hg7`jHlS?{*!;%+GGnPU9Icvyk;;Ig* w`>{o}P#b27=N8SUBXpL#B_AgKBUXCas=W$Zd)M=_Co+NH@3YCf!i#$3AEc8FZU6uP literal 0 HcmV?d00001 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 %}