feat: 添加 PWA 支持 — 可安装到主屏幕、离线缓存、刘海屏适配

This commit is contained in:
2026-02-26 16:37:40 +08:00
parent 31003110e1
commit 20f63c67cb
10 changed files with 182 additions and 4 deletions
+5
View File
@@ -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({
<html lang="zh-CN" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</head>
<body className={`${geistSans.variable} font-sans antialiased`}>
<ServiceWorkerRegistrar />
<GlobalUserBadge />
{children}
</body>
+32
View File
@@ -0,0 +1,32 @@
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "NoWhatever — 别说随便",
short_name: "NoWhatever",
description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!",
start_url: "/",
display: "standalone",
background_color: "#030712",
theme_color: "#10b981",
orientation: "portrait",
icons: [
{
src: "/icon-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/icon-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "/icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
};
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { WifiOff } from "lucide-react";
export default function OfflinePage() {
return (
<div className="min-h-dvh flex flex-col items-center justify-center px-6 bg-background text-foreground">
<div className="w-16 h-16 rounded-2xl bg-surface flex items-center justify-center mb-6">
<WifiOff className="w-8 h-8 text-muted" />
</div>
<h1 className="text-xl font-bold text-heading mb-2"></h1>
<p className="text-secondary text-center mb-8">
</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-accent text-white rounded-xl font-medium active:scale-95 transition-transform"
>
</button>
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
"use client";
import { useEffect } from "react";
export default function ServiceWorkerRegistrar() {
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").catch(() => {});
}
}, []);
return null;
}