添加PWA支持,更新配置文件以集成next-pwa,新增相关文件到.gitignore,优化layout和page组件以支持PWA功能,提升用户体验。
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
141
src/app/pwa-debug/page.tsx
Normal 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>选择“添加到主屏幕”</li>
|
||||
<li>点击“添加”</li>
|
||||
</ol>
|
||||
|
||||
<p className="mt-4"><strong>Android Chrome/Edge:</strong></p>
|
||||
<ol className="list-decimal list-inside ml-4">
|
||||
<li>点击菜单(三个点)</li>
|
||||
<li>选择“添加到主屏幕”或“安装应用”</li>
|
||||
<li>点击“添加”或“安装”</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/InstallPWA.tsx
Normal file
66
src/components/InstallPWA.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user