From cb97a45051d0e19e6809dd7c393425d3dfb251c3 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 26 Jul 2026 21:57:39 +0800 Subject: [PATCH] Add PWA install badge for desktop and tablet (manifest, SW, Add to Home Screen). Co-authored-by: Cursor --- backend/app/main.py | 35 ++++++ frontend/index.html | 19 +++- frontend/public/icons/apple-touch-icon.png | Bin 0 -> 974 bytes frontend/public/icons/favicon-32.png | Bin 0 -> 210 bytes frontend/public/icons/icon-192.png | Bin 0 -> 1060 bytes frontend/public/icons/icon-512.png | Bin 0 -> 4209 bytes frontend/public/manifest.webmanifest | 34 ++++++ frontend/public/sw.js | 38 +++++++ frontend/src/App.tsx | 2 + frontend/src/components/InstallBadge.tsx | 118 +++++++++++++++++++++ frontend/src/main.tsx | 3 + frontend/src/pages/Login.tsx | 8 +- frontend/src/pwa/install.ts | 34 ++++++ frontend/src/styles/app.css | 69 +++++++++++- 14 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 frontend/public/icons/apple-touch-icon.png create mode 100644 frontend/public/icons/favicon-32.png create mode 100644 frontend/public/icons/icon-192.png create mode 100644 frontend/public/icons/icon-512.png create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/public/sw.js create mode 100644 frontend/src/components/InstallBadge.tsx create mode 100644 frontend/src/pwa/install.ts diff --git a/backend/app/main.py b/backend/app/main.py index b7cdb0d..ed83f19 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -128,6 +128,41 @@ _DIST = resolve_frontend_dist() if (_DIST / "assets").is_dir(): app.mount("/assets", StaticFiles(directory=str(_DIST / "assets")), name="assets") +_ICONS = _DIST / "icons" +if _ICONS.is_dir(): + app.mount("/icons", StaticFiles(directory=str(_ICONS)), name="icons") + + +def _dist_file(name: str) -> Path: + return _DIST / name + + +@app.get("/manifest.webmanifest") +async def web_manifest(): + path = _dist_file("manifest.webmanifest") + if not path.exists(): + raise HTTPException(status_code=404, detail="manifest missing") + return FileResponse( + path, + media_type="application/manifest+json", + headers={"Cache-Control": "no-cache"}, + ) + + +@app.get("/sw.js") +async def service_worker(): + path = _dist_file("sw.js") + if not path.exists(): + raise HTTPException(status_code=404, detail="service worker missing") + return FileResponse( + path, + media_type="application/javascript", + headers={ + "Cache-Control": "no-cache", + "Service-Worker-Allowed": "/", + }, + ) + @app.get("/") async def index_page(): diff --git a/frontend/index.html b/frontend/index.html index dc4bf04..8d79815 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,24 @@ - + + + + + + + + + + + + OKX 自动化对冲系统 diff --git a/frontend/public/icons/apple-touch-icon.png b/frontend/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7e38155c12513171c9cb1e95ba809630b524d9e6 GIT binary patch literal 974 zcma#4u&@m9bB`&C$&0BlOpM86U|`_&^l%9R(px~7gAGX9o@Fy)U|{a?ba4!+xb^n# z!CYlW9u|jxyfOtp-5;%)bI`J*x$NY-sxR@Eb$WT6z0Y2szyH5Wkdc?b0@nsH#weBy z5Eg`RAOc_xSinln{(k&_QCX&1KHWDC;Y@5YFDi}{Fw1sJ)g^o1x12 z^?;7&3W$>Tk_J~6F5nFkH(bE$XAUI1{egsgK9F!}cR0M}c<%zoYneP56TV6_OQqQ| z^KM>e-o&<(+3bSB>wi)iGZywS@)}=pIGiIV?P#8~?~l~_bE;flN<~htSnQyF@7%df zpM4&)eb|3ziDd7hn#sDEVUHKzysYT?S?c|dYW@|oA~jxo6XklwR&u9Ey11HSi=)$D zwpWuEB<{P`U_l759 z#^S1I-dppf9rpKD-8mPae)p0CGw+w9%)d3w7BsR+Wq^ohSy!|jch7CS*8t&z1tBuc zwIGunxGw;80ckMd{Puns5|^2Gi~bMhx7`2a4mRCXj)2PKUhHu-2C}!L_8LTAdohje zR$T`7d0iX!Te@%0^DbEUZLwp(;Wz1b8`z{?#Ht?PF}Y%}{s>QrK$fu$kJPK*v2&Pt zP0#ozv;DHjD&}VX`?Bbkg8G>^ix;flf7^VK+4_PD{!_LvmYtG3-|Pnrl{ZU@Z43K6 zojzRoAZ#})__=XjgKhfmf4|J<$S^;-LMPqpFPFJo8&#%iy`;D@O@WmaHDr-k059 z6z;#v<#{fbXDiFN_p7Dd-8+?+`=v9!Gn5K4{JK@glg@UFFFpJCF9&DdVwsE;iG3h% z6fRiUX2}Qi;qk^csb>}!5}IW=rLtx`hA7(L*vG=V&HaLsqx0S0HmN!p8?5J70Nqt~ z{Z*UPg|!=QKZ|DH#ai|{CfiZ{k(u{`^N-DpL4@=IU3-?Oc}LbcgK`8c&j>DHh2LsR^Dgjv*Dd-cCQw zdq9E5xz>HvmA~pQ!|pE7aad|4|H#YoiQ6Oh6omwt=fRR04BnS&dsCM%?dhyaoS=Bl zLBV+jW238$Q9ILzAFWIs1wXD!ZBUE4w&q%+?indPkpqvaxDM>D_7)e|TfL2&_k(qdzPnB)br-?pE8dD`ttJzopr07Di~QUCw| literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..a8bca0c9c6250cc24a4072d109e3560218d58a65 GIT binary patch literal 1060 zcma#4u&@m9bB`&C$&0BlOpM86U|`_&^l%9R(g#48gAGUq-N|8RU|@da>EaktaqI2f zgT99Y1XwQYb$43y-|IyeUjsLM!VRT?&5M*#+ z5Fo(#dH(zL$17*ZGJI5RXJ$xkXJS~|&Irdx*&%EY4Z=^`8Nf6$`)T_EsRk#f08R%^ z#}%v#Dg>@D1z2!oF=|M$Y-JGPX^LX#Vs+GQ*cRl#(9nMIEHi_*+59MmX<2bw8FViG z$zqI{6UmeinRbigz@#(V#Tt&zDAi@)HY{ckx&KC*QNtc+$Q1de%ez;wEco!PP^RIN zbHU-bEC!A3e`A;(ek$%r^}k%u8Jf0x73+qL0qd9+GR!*lKB7z}Tc_d4q9w{5EE8h> zO{5hkEufiz3Qd}n5ebVK{2 zsq!G^fz(@X`5DfIocp(x;SL+)gSQvNmi=LfkYZr?v8CT%h4F#bgE#T42SN|rtatv& z_+eoV!#1V@(HFCBTQRI-I`?n;^m=^_!>PvnyYB(fpXJt@0G52H@ zgJwF{)AP48MHx@#{hHLZ1XX`Uux64`d zx>eDJXNGgHFl)gEWk-%l-$Ik&fe`Oj^IcP`%xXM3;0dZ2vI z6nO>?ELcDUAr51MXmqy36gjY1e-amioSQhqo(eC98(oLaFm@bIEMw?kP+)4{U{E5+ cI1yjRxVAII@RjDtG*Hxey85}Sb4q9e0F8Bb=>Px# literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c77ad6e0ee7e1e9ce3a247002c4838c7a508bd7d GIT binary patch literal 4209 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 NIVm-9|HSNy{{V{1=OX|B literal 0 HcmV?d00001 diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..b64978e --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,34 @@ +{ + "name": "OKX 自动化对冲系统", + "short_name": "对冲系统", + "description": "ETH 永续+期权对冲监控与策略控制台", + "start_url": "/plan", + "scope": "/", + "display": "standalone", + "display_override": ["standalone", "minimal-ui", "browser"], + "orientation": "any", + "background_color": "#0b0e11", + "theme_color": "#0b0e11", + "lang": "zh-CN", + "categories": ["finance", "productivity"], + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..368c2d8 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,38 @@ +/* Minimal service worker: makes the app installable on desktop & tablet. */ +const CACHE = "eth-hedge-shell-v1"; +const PRECACHE = ["/", "/plan", "/manifest.webmanifest", "/icons/icon-192.png"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()), + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))), + ).then(() => self.clients.claim()), + ); +}); + +self.addEventListener("fetch", (event) => { + const req = event.request; + if (req.method !== "GET") return; + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; + // Never cache API / health / websocket-ish paths + if (url.pathname.startsWith("/api") || url.pathname === "/health") return; + + event.respondWith( + fetch(req) + .then((res) => { + if (res.ok && (url.pathname.startsWith("/assets") || url.pathname === "/")) { + const copy = res.clone(); + caches.open(CACHE).then((cache) => cache.put(req, copy)); + } + return res; + }) + .catch(() => caches.match(req).then((hit) => hit || caches.match("/"))), + ); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a8c4591..2e1c23e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; import { NavLink, Navigate, Route, Routes } from "react-router-dom"; import { clearSession, getToken, getUsername } from "./api/client"; +import InstallBadge from "./components/InstallBadge"; import LoginPage from "./pages/Login"; import PlanPage from "./pages/Plan"; import TradesPage from "./pages/Trades"; @@ -14,6 +15,7 @@ function Shell({ children }: { children: ReactNode }) {
OKX 自动化对冲系统
+