From 20f63c67cbf80fe9143af2f3e9eb0c7dbed1d7f7 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 16:37:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20PWA=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20=E2=80=94=20=E5=8F=AF=E5=AE=89=E8=A3=85=E5=88=B0?= =?UTF-8?q?=E4=B8=BB=E5=B1=8F=E5=B9=95=E3=80=81=E7=A6=BB=E7=BA=BF=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E3=80=81=E5=88=98=E6=B5=B7=E5=B1=8F=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ROADMAP.md | 8 +-- public/apple-touch-icon.png | Bin 0 -> 3724 bytes public/icon-192x192.png | Bin 0 -> 4052 bytes public/icon-512x512.png | Bin 0 -> 14959 bytes public/sw.js | 68 ++++++++++++++++++++++ scripts/generate-pwa-icons.mjs | 37 ++++++++++++ src/app/layout.tsx | 5 ++ src/app/manifest.ts | 32 ++++++++++ src/app/offline/page.tsx | 23 ++++++++ src/components/ServiceWorkerRegistrar.tsx | 13 +++++ 10 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/icon-192x192.png create mode 100644 public/icon-512x512.png create mode 100644 public/sw.js create mode 100644 scripts/generate-pwa-icons.mjs create mode 100644 src/app/manifest.ts create mode 100644 src/app/offline/page.tsx create mode 100644 src/components/ServiceWorkerRegistrar.tsx diff --git a/ROADMAP.md b/ROADMAP.md index 498edc5..1f08ee8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,10 +33,10 @@ - 抽中后打卡确认(拍照上传,形成回忆) - "本周契约执行率" 统计 -### PWA 支持 -- 添加 Web App Manifest,支持"添加到主屏幕" -- Service Worker 离线缓存基础页面 -- `viewport-fit=cover` 适配刘海屏 +### ~~PWA 支持~~(已完成) +- ~~添加 Web App Manifest,支持"添加到主屏幕"~~ +- ~~Service Worker 离线缓存基础页面~~ +- ~~`viewport-fit=cover` 适配刘海屏~~ ### 盲盒房间删除 / 退出 - 房间创建者可删除房间(级联清理成员 & 想法) diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0c1d427667612ddb61f8a46c3e04a761f47bf396 GIT binary patch literal 3724 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 { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(PRECACHE_URLS)) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => caches.delete(key)) + ) + ) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + + if (request.method !== "GET") return; + + const url = new URL(request.url); + + // API calls: network-only + if (url.pathname.startsWith("/api/")) return; + + // Static assets (_next/static, icons, fonts): cache-first + if ( + url.pathname.startsWith("/_next/static/") || + url.pathname.match(/\.(png|jpg|svg|ico|woff2?)$/) + ) { + event.respondWith( + caches.match(request).then( + (cached) => + cached || + fetch(request).then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + return response; + }) + ) + ); + return; + } + + // HTML pages: network-first, fallback to cache, then offline page + event.respondWith( + fetch(request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + return response; + }) + .catch(() => caches.match(request).then((cached) => cached || caches.match("/offline"))) + ); +}); diff --git a/scripts/generate-pwa-icons.mjs b/scripts/generate-pwa-icons.mjs new file mode 100644 index 0000000..07a1676 --- /dev/null +++ b/scripts/generate-pwa-icons.mjs @@ -0,0 +1,37 @@ +import sharp from "sharp"; +import { mkdirSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const publicDir = join(__dirname, "..", "public"); + +const ACCENT = "#10b981"; +const BG_DARK = "#030712"; + +function buildSvg(size) { + const fontSize = Math.round(size * 0.32); + const radius = Math.round(size * 0.18); + return ` + + NW + `; +} + +const sizes = [192, 512]; + +for (const size of sizes) { + const svg = Buffer.from(buildSvg(size)); + const out = join(publicDir, `icon-${size}x${size}.png`); + await sharp(svg).resize(size, size).png().toFile(out); + console.log(`✓ ${out}`); +} + +const appleSvg = Buffer.from(buildSvg(180)); +const appleOut = join(publicDir, "apple-touch-icon.png"); +await sharp(appleSvg).resize(180, 180).png().toFile(appleOut); +console.log(`✓ ${appleOut}`); + +console.log("\nPWA icons generated."); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dbb8c48..25e975b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next"; import { Geist } from "next/font/google"; import "./globals.css"; import GlobalUserBadge from "@/components/GlobalUserBadge"; +import ServiceWorkerRegistrar from "@/components/ServiceWorkerRegistrar"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -19,6 +20,8 @@ export const viewport: Viewport = { initialScale: 1, maximumScale: 1, userScalable: false, + viewportFit: "cover", + themeColor: "#10b981", }; const themeScript = `(function(){try{var t=localStorage.getItem("nowhatever-theme")||"system";var r=t;if(t==="system")r=window.matchMedia("(prefers-color-scheme:light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",r)}catch(e){}})()`; @@ -32,8 +35,10 @@ export default function RootLayout({