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("/"))));
+});