diff --git a/app/layout.tsx b/app/layout.tsx index 303c161..34ad7ea 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; +import PwaRegister from "./pwa-register"; export const metadata: Metadata = { title: "大学英语六级 · 90天打卡", @@ -8,6 +9,9 @@ export const metadata: Metadata = { icon: "/favicon.svg", shortcut: "/favicon.svg", }, + manifest: "/manifest.webmanifest", + applicationName: "大学英语六级 · 90天打卡", + themeColor: "#1d4ed8", }; export default function RootLayout({ @@ -17,7 +21,7 @@ export default function RootLayout({ }>) { return ( - {children} + {children} ); } diff --git a/app/pwa-register.tsx b/app/pwa-register.tsx new file mode 100644 index 0000000..efda7f1 --- /dev/null +++ b/app/pwa-register.tsx @@ -0,0 +1,3 @@ +"use client"; +import { useEffect } from "react"; +export default function PwaRegister() { useEffect(() => { if ("serviceWorker" in navigator) navigator.serviceWorker.register("/sw.js").catch(() => undefined); }, []); return null; } diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..aadb0e7 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,12 @@ +{ + "name": "大学英语六级 · 90天打卡", + "short_name": "CET-6 打卡", + "description": "CET-6 90 天复习计划、云端打卡与复盘。", + "lang": "zh-CN", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#eef4ff", + "theme_color": "#1d4ed8", + "icons": [{ "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..5da3fc5 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,10 @@ +const CACHE = "cet6-shell-v1"; +const APP_SHELL = ["/", "/tasks", "/stats", "/review", "/manifest.webmanifest", "/favicon.svg"]; +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("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("/")))); +});