feat:新增安卓 pwa 支持
This commit is contained in:
38
app/components/PwaRegister.jsx
Normal file
38
app/components/PwaRegister.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在客户端注册 Service Worker,满足 Android Chrome PWA 安装条件(需 HTTPS + manifest + SW)。
|
||||||
|
* 仅在生产环境且浏览器支持时注册。
|
||||||
|
*/
|
||||||
|
export default function PwaRegister() {
|
||||||
|
useEffect(() => {// 检测核心能力
|
||||||
|
const isPwaSupported =
|
||||||
|
'serviceWorker' in navigator &&
|
||||||
|
'BeforeInstallPromptEvent' in window;
|
||||||
|
console.log('PWA 支持:', isPwaSupported);
|
||||||
|
if (
|
||||||
|
typeof window === 'undefined' ||
|
||||||
|
!('serviceWorker' in navigator) ||
|
||||||
|
process.env.NODE_ENV !== 'production'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register('/sw.js', { scope: '/', updateViaCache: 'none' })
|
||||||
|
.then((reg) => {
|
||||||
|
reg.addEventListener('updatefound', () => {
|
||||||
|
const newWorker = reg.installing;
|
||||||
|
newWorker?.addEventListener('statechange', () => {
|
||||||
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
|
// 可选:提示用户刷新以获取新版本
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
37
app/components/ThemeColorSync.jsx
Normal file
37
app/components/ThemeColorSync.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const THEME_COLORS = {
|
||||||
|
dark: '#0f172a',
|
||||||
|
light: '#ffffff',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getThemeColor() {
|
||||||
|
if (typeof document === 'undefined') return THEME_COLORS.dark;
|
||||||
|
const theme = document.documentElement.getAttribute('data-theme');
|
||||||
|
return THEME_COLORS[theme] ?? THEME_COLORS.dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyThemeColor() {
|
||||||
|
const meta = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (meta) meta.setAttribute('content', getThemeColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前亮/暗主题同步 PWA theme-color meta,使 Android 状态栏与页面主题一致。
|
||||||
|
* 监听 document.documentElement 的 data-theme 变化并更新 meta。
|
||||||
|
*/
|
||||||
|
export default function ThemeColorSync() {
|
||||||
|
useEffect(() => {
|
||||||
|
applyThemeColor();
|
||||||
|
const observer = new MutationObserver(() => applyThemeColor());
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-theme'],
|
||||||
|
});
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import AnalyticsGate from './components/AnalyticsGate';
|
import AnalyticsGate from './components/AnalyticsGate';
|
||||||
|
import PwaRegister from './components/PwaRegister';
|
||||||
|
import ThemeColorSync from './components/ThemeColorSync';
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@@ -19,6 +21,9 @@ export default function RootLayout({ children }) {
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default"/>
|
<meta name="apple-mobile-web-app-status-bar-style" content="default"/>
|
||||||
<link rel="apple-touch-icon" href="/Icon-60@3x.png?v=1"/>
|
<link rel="apple-touch-icon" href="/Icon-60@3x.png?v=1"/>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/Icon-60@3x.png?v=1"/>
|
<link rel="apple-touch-icon" sizes="180x180" href="/Icon-60@3x.png?v=1"/>
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
{/* 初始为暗色;ThemeColorSync 会按 data-theme 同步为亮/暗 */}
|
||||||
|
<meta name="theme-color" content="#0f172a" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
{/* 尽早设置 data-theme,减少首屏主题闪烁;与 suppressHydrationWarning 配合避免服务端/客户端 html 属性不一致报错 */}
|
{/* 尽早设置 data-theme,减少首屏主题闪烁;与 suppressHydrationWarning 配合避免服务端/客户端 html 属性不一致报错 */}
|
||||||
<script
|
<script
|
||||||
@@ -28,6 +33,8 @@ export default function RootLayout({ children }) {
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<ThemeColorSync />
|
||||||
|
<PwaRegister />
|
||||||
<AnalyticsGate GA_ID={GA_ID} />
|
<AnalyticsGate GA_ID={GA_ID} />
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
18
public/sw.js
Normal file
18
public/sw.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// 最小 Service Worker,满足 Android Chrome「添加到主屏幕」的安装条件
|
||||||
|
const CACHE_NAME = 'jigubao-v1';
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request).catch(() => {
|
||||||
|
return new Response('', { status: 503, statusText: 'Service Unavailable' });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user