147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Download, X, Smartphone } from "lucide-react"
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||
|
||
export function PWAInstaller() {
|
||
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
|
||
const [isInstallable, setIsInstallable] = useState(false)
|
||
const [isInstalled, setIsInstalled] = useState(false)
|
||
const [showPrompt, setShowPrompt] = useState(false)
|
||
|
||
useEffect(() => {
|
||
// 检查是否已安装
|
||
if (typeof window !== "undefined") {
|
||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|
||
const isIOSStandalone = (window.navigator as any).standalone === true
|
||
|
||
if (isStandalone || isIOSStandalone) {
|
||
setIsInstalled(true)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 监听 beforeinstallprompt 事件
|
||
const handleBeforeInstallPrompt = (e: Event) => {
|
||
e.preventDefault()
|
||
setDeferredPrompt(e)
|
||
setIsInstallable(true)
|
||
|
||
// 延迟显示提示(给用户一些时间浏览应用)
|
||
setTimeout(() => {
|
||
const hasSeenPrompt = localStorage.getItem('pwa-install-prompt-seen')
|
||
if (!hasSeenPrompt) {
|
||
setShowPrompt(true)
|
||
}
|
||
}, 30000) // 30秒后显示
|
||
}
|
||
|
||
// 监听安装完成事件
|
||
const handleAppInstalled = () => {
|
||
setIsInstalled(true)
|
||
setShowPrompt(false)
|
||
setIsInstallable(false)
|
||
}
|
||
|
||
if (typeof window !== "undefined") {
|
||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||
window.addEventListener('appinstalled', handleAppInstalled)
|
||
|
||
return () => {
|
||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
||
window.removeEventListener('appinstalled', handleAppInstalled)
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
const handleInstallClick = async () => {
|
||
if (!deferredPrompt) return
|
||
|
||
// 显示安装提示
|
||
deferredPrompt.prompt()
|
||
|
||
// 等待用户响应
|
||
const { outcome } = await deferredPrompt.userChoice
|
||
|
||
if (outcome === 'accepted') {
|
||
console.log('用户接受了安装')
|
||
} else {
|
||
console.log('用户拒绝了安装')
|
||
}
|
||
|
||
setDeferredPrompt(null)
|
||
setShowPrompt(false)
|
||
localStorage.setItem('pwa-install-prompt-seen', 'true')
|
||
}
|
||
|
||
const handleDismiss = () => {
|
||
setShowPrompt(false)
|
||
localStorage.setItem('pwa-install-prompt-seen', 'true')
|
||
}
|
||
|
||
if (isInstalled) {
|
||
return null
|
||
}
|
||
|
||
if (!isInstallable) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{/* 浮动安装提示 */}
|
||
{showPrompt && (
|
||
<div className="fixed bottom-4 right-4 z-50 max-w-sm animate-in slide-in-from-bottom-5">
|
||
<Card className="shadow-lg border-2 border-primary">
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Smartphone className="h-5 w-5 text-primary" />
|
||
<CardTitle className="text-base">安装应用</CardTitle>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-6 w-6"
|
||
onClick={handleDismiss}
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
<CardDescription className="text-xs">
|
||
将家谱应用添加到主屏幕,随时随地访问
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<Button
|
||
onClick={handleInstallClick}
|
||
className="w-full"
|
||
size="sm"
|
||
>
|
||
<Download className="mr-2 h-4 w-4" />
|
||
立即安装
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{/* 设置页面的安装按钮 */}
|
||
{!showPrompt && (
|
||
<Alert className="cursor-pointer hover:bg-muted/50 transition-colors" onClick={handleInstallClick}>
|
||
<Download className="h-4 w-4" />
|
||
<AlertDescription className="flex items-center justify-between">
|
||
<span>将应用安装到设备</span>
|
||
<Button size="sm" variant="outline">
|
||
安装
|
||
</Button>
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
</>
|
||
)
|
||
}
|