添加PWA支持,更新配置文件以集成next-pwa,新增相关文件到.gitignore,优化layout和page组件以支持PWA功能,提升用户体验。

This commit is contained in:
zihanjian
2025-06-23 15:44:00 +08:00
parent d79c2c4b88
commit f5ce1df803
19 changed files with 5227 additions and 214 deletions

View File

@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import "./globals.css";
@@ -16,6 +16,29 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "七卡瓦拼豆底稿生成器 | Perler Beads Generator",
description: "上传图片,调整精细度,一键生成像素画图纸,简单实用的像素画生成工具",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "拼豆生成器",
},
icons: {
icon: [
{ url: "/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/icon-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: [
{ url: "/icon-192x192.png", sizes: "192x192", type: "image/png" },
],
},
};
export const viewport: Viewport = {
themeColor: "#000000",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({

View File

@@ -2,6 +2,7 @@
import React, { useState, useRef, ChangeEvent, DragEvent, useEffect, useMemo, useCallback } from 'react';
import Script from 'next/script';
import InstallPWA from '../components/InstallPWA';
// 导入像素化工具和类型
import {
@@ -1749,6 +1750,9 @@ export default function Home() {
{/* 添加自定义动画样式 */}
<style dangerouslySetInnerHTML={{ __html: floatAnimation }} />
{/* PWA 安装按钮 */}
<InstallPWA />
{/* ++ 修改:添加 onLoad 回调函数 ++ */}
<Script
async

141
src/app/pwa-debug/page.tsx Normal file
View File

@@ -0,0 +1,141 @@
'use client';
import { useEffect, useState } from 'react';
export default function PWADebug() {
const [debugInfo, setDebugInfo] = useState<{
manifest: object | null | { error: string };
serviceWorker: object | null;
https: boolean;
standalone: boolean;
installable: boolean;
installPromptSupported?: boolean;
}>({
manifest: null,
serviceWorker: null,
https: false,
standalone: false,
installable: false,
});
useEffect(() => {
const checkPWA = async () => {
const info: {
manifest?: object | null | { error: string };
serviceWorker?: object | null;
https?: boolean;
standalone?: boolean;
installable?: boolean;
installPromptSupported?: boolean;
} = {};
// 检查 HTTPS
info.https = window.location.protocol === 'https:' || window.location.hostname === 'localhost';
// 检查 Service Worker
if ('serviceWorker' in navigator) {
try {
const registrations = await navigator.serviceWorker.getRegistrations();
info.serviceWorker = {
supported: true,
registrations: registrations.length,
active: registrations.some(reg => reg.active),
};
} catch (e) {
info.serviceWorker = { error: e instanceof Error ? e.message : 'Unknown error' };
}
} else {
info.serviceWorker = { supported: false };
}
// 检查 manifest
const manifestLink = document.querySelector('link[rel="manifest"]');
if (manifestLink) {
try {
const response = await fetch(manifestLink.getAttribute('href') || '');
const manifest = await response.json();
info.manifest = manifest;
} catch (e) {
info.manifest = { error: e instanceof Error ? e.message : 'Unknown error' };
}
} else {
info.manifest = { error: 'No manifest link found' };
}
// 检查是否独立模式
info.standalone = window.matchMedia('(display-mode: standalone)').matches;
// 检查 beforeinstallprompt
info.installPromptSupported = 'onbeforeinstallprompt' in window;
setDebugInfo({
manifest: info.manifest || null,
serviceWorker: info.serviceWorker || null,
https: info.https || false,
standalone: info.standalone || false,
installable: info.installable || false,
installPromptSupported: info.installPromptSupported,
});
};
checkPWA();
}, []);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8">PWA </h1>
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4"></h2>
<ul className="space-y-2">
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.https ? 'bg-green-500' : 'bg-red-500'}`}></span>
HTTPS: {debugInfo.https ? '是' : '否'} ({typeof window !== 'undefined' ? window.location.protocol : 'N/A'})
</li>
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.serviceWorker ? 'bg-green-500' : 'bg-red-500'}`}></span>
Service Worker: {JSON.stringify(debugInfo.serviceWorker, null, 2)}
</li>
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.standalone ? 'bg-green-500' : 'bg-gray-400'}`}></span>
: {debugInfo.standalone ? '是' : '否'}
</li>
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.installPromptSupported ? 'bg-green-500' : 'bg-red-500'}`}></span>
: {debugInfo.installPromptSupported ? '支持' : '不支持'}
</li>
</ul>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4">Manifest </h2>
<pre className="bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-auto">
{JSON.stringify(debugInfo.manifest, null, 2)}
</pre>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2 text-gray-600 dark:text-gray-300">
<p><strong>iOS Safari:</strong></p>
<ol className="list-decimal list-inside ml-4">
<li></li>
<li>&ldquo;&rdquo;</li>
<li>&ldquo;&rdquo;</li>
</ol>
<p className="mt-4"><strong>Android Chrome/Edge:</strong></p>
<ol className="list-decimal list-inside ml-4">
<li></li>
<li>&ldquo;&rdquo;&ldquo;&rdquo;</li>
<li>&ldquo;&rdquo;&ldquo;&rdquo;</li>
</ol>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import { useEffect, useState } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export default function InstallPWA() {
const [supportsPWA, setSupportsPWA] = useState(false);
const [promptInstall, setPromptInstall] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
const handler = (e: BeforeInstallPromptEvent) => {
e.preventDefault();
console.log('PWA 安装提示已准备');
setSupportsPWA(true);
setPromptInstall(e);
};
window.addEventListener('beforeinstallprompt', handler as EventListener);
// 检查是否已安装
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
return () => window.removeEventListener('beforeinstallprompt', handler as EventListener);
}, []);
const onClick = async (evt: React.MouseEvent) => {
evt.preventDefault();
if (!promptInstall) {
return;
}
promptInstall.prompt();
const { outcome } = await promptInstall.userChoice;
if (outcome === 'accepted') {
setPromptInstall(null);
setSupportsPWA(false);
}
};
if (isInstalled) {
return null;
}
if (!supportsPWA) {
return null;
}
return (
<button
className="fixed bottom-6 right-6 bg-gradient-to-r from-purple-500 to-blue-500 text-white px-6 py-3 rounded-full shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center gap-2 z-50"
onClick={onClick}
aria-label="安装应用"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m0 0l-4-4m4 4l4-4M5 12h14" />
</svg>
</button>
);
}