From 153f184c38cb9b9f232cce05386697fe9a402510 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 18 Sep 2026 21:41:29 +0900 Subject: [PATCH] Fix PWA installation support --- app/globals.css | 2 ++ app/layout.tsx | 1 + app/pwa-register.tsx | 24 ++++++++++++++++++++++-- public/icon-192.png | Bin 0 -> 1125 bytes public/icon-512.png | Bin 0 -> 3526 bytes public/manifest.webmanifest | 6 +++++- public/sw.js | 15 +++++++++++---- 7 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 public/icon-192.png create mode 100644 public/icon-512.png diff --git a/app/globals.css b/app/globals.css index 2b5246e..20a1360 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,6 +2,8 @@ @import "tw-animate-css"; @import "../vendor/shadcn-tailwind-4.13.0.css"; +.pwa-install{position:fixed;z-index:60;right:1rem;bottom:1rem;display:flex;align-items:center;gap:.75rem;max-width:min(28rem,calc(100vw - 2rem));padding:.8rem .9rem;color:#eff6ff;background:rgba(15,23,42,.94);border:1px solid rgba(147,197,253,.45);border-radius:.85rem;box-shadow:0 12px 30px rgba(15,23,42,.28);font-size:.875rem}.pwa-install button{flex:none;padding:.45rem .7rem;border-radius:.55rem;border:0;background:#60a5fa;color:#0f172a;font-weight:700;cursor:pointer}.pwa-ios{color:#bfdbfe;white-space:nowrap}@media(max-width:520px){.pwa-install{left:.75rem;right:.75rem;bottom:.75rem;flex-wrap:wrap}.pwa-ios{white-space:normal}} + @utility scrollbar-thin { scrollbar-width: thin; } diff --git a/app/layout.tsx b/app/layout.tsx index 34ad7ea..72091f4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,6 +8,7 @@ export const metadata: Metadata = { icons: { icon: "/favicon.svg", shortcut: "/favicon.svg", + apple: "/icon-192.png", }, manifest: "/manifest.webmanifest", applicationName: "大学英语六级 · 90天打卡", diff --git a/app/pwa-register.tsx b/app/pwa-register.tsx index efda7f1..38bc645 100644 --- a/app/pwa-register.tsx +++ b/app/pwa-register.tsx @@ -1,3 +1,23 @@ "use client"; -import { useEffect } from "react"; -export default function PwaRegister() { useEffect(() => { if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js").catch(() => undefined); }, []); return null; } +import { useEffect, useState } from "react"; + +type InstallPrompt = Event & { prompt: () => Promise; userChoice: Promise<{ outcome: "accepted" | "dismissed" }> }; + +export default function PwaRegister() { + const [prompt, setPrompt] = useState(null); + const [isIos, setIsIos] = useState(false); + + useEffect(() => { + if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js", { updateViaCache: "none" }).catch(() => undefined); + setIsIos(/iphone|ipad|ipod/i.test(navigator.userAgent)); + const receivePrompt = (event: Event) => { event.preventDefault(); setPrompt(event as InstallPrompt); }; + window.addEventListener("beforeinstallprompt", receivePrompt); + return () => window.removeEventListener("beforeinstallprompt", receivePrompt); + }, []); + + if (!prompt && !isIos) return null; + return
+ 把打卡站点安装到桌面,打开更快捷。 + {prompt ? : Safari 点“分享”→“添加到主屏幕”} +
; +} diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..7273a8df126e3051e7cb8a1a58a07b3bff2af855 GIT binary patch literal 1125 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 JT#M(I{2T2hAG81f literal 0 HcmV?d00001 diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index aadb0e7..0fff5ca 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -8,5 +8,9 @@ "display": "standalone", "background_color": "#eef4ff", "theme_color": "#1d4ed8", - "icons": [{ "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }] + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }, + { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" } + ] } diff --git a/public/sw.js b/public/sw.js index 5da3fc5..6c1499f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,10 +1,17 @@ -const CACHE = "cet6-shell-v1"; -const APP_SHELL = ["/", "/tasks", "/stats", "/review", "/manifest.webmanifest", "/favicon.svg"]; +const CACHE = "cet6-shell-v2"; +const APP_SHELL = ["/", "/manifest.webmanifest", "/favicon.svg", "/icon-192.png", "/icon-512.png"]; self.addEventListener("install", event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(APP_SHELL)).then(() => self.skipWaiting()))); -self.addEventListener("activate", event => event.waitUntil(self.clients.claim())); +self.addEventListener("activate", event => event.waitUntil( + caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))).then(() => self.clients.claim()) +)); self.addEventListener("fetch", event => { const request = event.request; if (request.method !== "GET" || new URL(request.url).origin !== location.origin) return; if (request.url.includes("/api/")) return; - event.respondWith(fetch(request).then(response => { const copy = response.clone(); caches.open(CACHE).then(cache => cache.put(request, copy)); return response; }).catch(() => caches.match(request).then(found => found || caches.match("/")))); + event.respondWith(fetch(request).then(response => { + if (response.ok && new URL(request.url).pathname.startsWith("/_next/")) { + const copy = response.clone(); caches.open(CACHE).then(cache => cache.put(request, copy)); + } + return response; + }).catch(() => caches.match(request).then(found => found || caches.match("/")))); });