417 lines
9.5 KiB
Vue
417 lines
9.5 KiB
Vue
<template>
|
|
<div v-if="showPrompt" class="pwa-install-prompt">
|
|
<q-banner class="install-banner" rounded>
|
|
<template v-slot:avatar>
|
|
<q-icon name="get_app" color="primary" size="md" />
|
|
</template>
|
|
|
|
<div class="banner-content">
|
|
<div class="banner-title">安裝 Drama Ling 應用程式</div>
|
|
<div class="banner-description">
|
|
在桌面安裝應用程式,享受更好的學習體驗:
|
|
</div>
|
|
|
|
<ul class="features-list">
|
|
<li>離線學習功能</li>
|
|
<li>快速啟動和存取</li>
|
|
<li>推播通知提醒</li>
|
|
<li>全螢幕沉浸體驗</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<template v-slot:action>
|
|
<div class="banner-actions">
|
|
<q-btn
|
|
color="primary"
|
|
label="立即安裝"
|
|
@click="installApp"
|
|
:loading="isInstalling"
|
|
no-caps
|
|
/>
|
|
<q-btn
|
|
flat
|
|
label="稍後提醒"
|
|
@click="postponeInstall"
|
|
no-caps
|
|
class="q-ml-sm"
|
|
/>
|
|
<q-btn
|
|
flat
|
|
icon="close"
|
|
@click="dismissPermanently"
|
|
size="sm"
|
|
class="q-ml-sm"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</q-banner>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { useQuasar } from 'quasar'
|
|
|
|
const $q = useQuasar()
|
|
|
|
// 響應式數據
|
|
const showPrompt = ref(false)
|
|
const isInstalling = ref(false)
|
|
const deferredPrompt = ref<any>(null)
|
|
|
|
// PWA 安裝狀態
|
|
const isStandalone = ref(false)
|
|
const isInstalled = ref(false)
|
|
|
|
// 檢查 PWA 安裝狀態
|
|
const checkPWAStatus = () => {
|
|
// 檢查是否已在獨立模式下運行 (已安裝)
|
|
isStandalone.value = window.matchMedia('(display-mode: standalone)').matches ||
|
|
('standalone' in window.navigator && (window.navigator as any).standalone === true)
|
|
|
|
// 檢查用戶偏好設定
|
|
const userPreference = localStorage.getItem('pwa-install-preference')
|
|
const lastPromptTime = localStorage.getItem('pwa-last-prompt')
|
|
|
|
// 如果已安裝或用戶已永久拒絕,不顯示提示
|
|
if (isStandalone.value || userPreference === 'never') {
|
|
isInstalled.value = true
|
|
return false
|
|
}
|
|
|
|
// 如果用戶選擇稍後提醒,檢查是否已過足夠時間 (7天)
|
|
if (userPreference === 'later' && lastPromptTime) {
|
|
const daysSincePrompt = (Date.now() - parseInt(lastPromptTime)) / (1000 * 60 * 60 * 24)
|
|
if (daysSincePrompt < 7) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// 處理 beforeinstallprompt 事件
|
|
const handleBeforeInstallPrompt = (e: Event) => {
|
|
// 阻止瀏覽器預設的安裝提示
|
|
e.preventDefault()
|
|
|
|
// 儲存事件以供後續使用
|
|
deferredPrompt.value = e
|
|
|
|
// 檢查是否應該顯示自訂提示
|
|
if (checkPWAStatus()) {
|
|
showPrompt.value = true
|
|
}
|
|
}
|
|
|
|
// 安裝應用程式
|
|
const installApp = async () => {
|
|
if (!deferredPrompt.value) {
|
|
// 如果沒有 beforeinstallprompt 事件,顯示手動安裝指引
|
|
showManualInstallInstructions()
|
|
return
|
|
}
|
|
|
|
isInstalling.value = true
|
|
|
|
try {
|
|
// 顯示安裝提示
|
|
const result = await deferredPrompt.value.prompt()
|
|
|
|
// 等待用戶回應
|
|
const choiceResult = await deferredPrompt.value.userChoice
|
|
|
|
if (choiceResult.outcome === 'accepted') {
|
|
// 用戶接受安裝
|
|
$q.notify({
|
|
type: 'positive',
|
|
message: '應用程式安裝成功!',
|
|
icon: 'check_circle',
|
|
timeout: 3000
|
|
})
|
|
|
|
// 記錄安裝成功
|
|
localStorage.setItem('pwa-install-preference', 'installed')
|
|
localStorage.setItem('pwa-install-time', Date.now().toString())
|
|
|
|
showPrompt.value = false
|
|
} else {
|
|
// 用戶拒絕安裝
|
|
$q.notify({
|
|
type: 'info',
|
|
message: '你可以稍後從瀏覽器選單安裝應用程式',
|
|
icon: 'info',
|
|
timeout: 3000
|
|
})
|
|
}
|
|
|
|
// 清除 deferredPrompt
|
|
deferredPrompt.value = null
|
|
|
|
} catch (error) {
|
|
console.error('安裝失敗:', error)
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: '安裝過程發生錯誤',
|
|
icon: 'error',
|
|
timeout: 3000
|
|
})
|
|
} finally {
|
|
isInstalling.value = false
|
|
}
|
|
}
|
|
|
|
// 稍後提醒
|
|
const postponeInstall = () => {
|
|
localStorage.setItem('pwa-install-preference', 'later')
|
|
localStorage.setItem('pwa-last-prompt', Date.now().toString())
|
|
showPrompt.value = false
|
|
|
|
$q.notify({
|
|
type: 'info',
|
|
message: '我們會在 7 天後再次提醒你',
|
|
icon: 'schedule',
|
|
timeout: 3000
|
|
})
|
|
}
|
|
|
|
// 永久拒絕
|
|
const dismissPermanently = () => {
|
|
localStorage.setItem('pwa-install-preference', 'never')
|
|
showPrompt.value = false
|
|
|
|
$q.notify({
|
|
type: 'info',
|
|
message: '你可以隨時從設定中啟用安裝提示',
|
|
icon: 'settings',
|
|
timeout: 3000
|
|
})
|
|
}
|
|
|
|
// 顯示手動安裝指引
|
|
const showManualInstallInstructions = () => {
|
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
|
const isAndroid = /Android/.test(navigator.userAgent)
|
|
|
|
let instructions = ''
|
|
|
|
if (isIOS) {
|
|
instructions = '點擊 Safari 底部的分享按鈕,然後選擇「加入主畫面」'
|
|
} else if (isAndroid) {
|
|
instructions = '點擊瀏覽器選單中的「安裝應用程式」或「加到主畫面」'
|
|
} else {
|
|
instructions = '點擊網址列右側的安裝圖示,或從瀏覽器選單選擇「安裝」'
|
|
}
|
|
|
|
$q.dialog({
|
|
title: '手動安裝應用程式',
|
|
message: instructions,
|
|
html: true,
|
|
ok: {
|
|
label: '我知道了',
|
|
color: 'primary'
|
|
}
|
|
})
|
|
}
|
|
|
|
// 檢查應用程式是否已成功安裝
|
|
const handleAppInstalled = () => {
|
|
console.log('PWA 安裝成功')
|
|
|
|
$q.notify({
|
|
type: 'positive',
|
|
message: '歡迎使用 Drama Ling 應用程式!',
|
|
icon: 'celebration',
|
|
timeout: 5000,
|
|
actions: [{
|
|
label: '開始學習',
|
|
color: 'white',
|
|
handler: () => {
|
|
// 可以跳轉到學習頁面
|
|
}
|
|
}]
|
|
})
|
|
|
|
// 記錄安裝狀態
|
|
localStorage.setItem('pwa-install-preference', 'installed')
|
|
localStorage.setItem('pwa-install-time', Date.now().toString())
|
|
|
|
showPrompt.value = false
|
|
isInstalled.value = true
|
|
}
|
|
|
|
// 重置安裝提示 (用於測試或設定)
|
|
const resetInstallPrompt = () => {
|
|
localStorage.removeItem('pwa-install-preference')
|
|
localStorage.removeItem('pwa-last-prompt')
|
|
localStorage.removeItem('pwa-install-time')
|
|
|
|
if (checkPWAStatus() && deferredPrompt.value) {
|
|
showPrompt.value = true
|
|
}
|
|
}
|
|
|
|
// 生命週期
|
|
onMounted(() => {
|
|
// 監聽 PWA 安裝相關事件
|
|
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
|
window.addEventListener('appinstalled', handleAppInstalled)
|
|
|
|
// 初始化檢查
|
|
setTimeout(() => {
|
|
if (checkPWAStatus()) {
|
|
// 如果沒有 beforeinstallprompt 事件但符合顯示條件,顯示簡化版提示
|
|
if (!deferredPrompt.value) {
|
|
const visitCount = parseInt(localStorage.getItem('visit-count') || '0')
|
|
if (visitCount >= 3) { // 訪問 3 次後顯示提示
|
|
showPrompt.value = true
|
|
}
|
|
}
|
|
}
|
|
}, 3000) // 頁面載入 3 秒後顯示
|
|
|
|
// 增加訪問計數
|
|
const visitCount = parseInt(localStorage.getItem('visit-count') || '0')
|
|
localStorage.setItem('visit-count', (visitCount + 1).toString())
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
|
|
window.removeEventListener('appinstalled', handleAppInstalled)
|
|
})
|
|
|
|
// 暴露方法供外部使用
|
|
defineExpose({
|
|
installApp,
|
|
resetInstallPrompt,
|
|
isInstalled,
|
|
isStandalone
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.pwa-install-prompt {
|
|
position: fixed;
|
|
bottom: $space-4;
|
|
left: $space-4;
|
|
right: $space-4;
|
|
z-index: 1000;
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
|
|
@media (max-width: 768px) {
|
|
bottom: $space-3;
|
|
left: $space-3;
|
|
right: $space-3;
|
|
}
|
|
}
|
|
|
|
.install-banner {
|
|
background: linear-gradient(135deg, $primary-teal 0%, $secondary-purple 100%);
|
|
color: white;
|
|
border-radius: $radius-xl;
|
|
box-shadow: $shadow-xl;
|
|
backdrop-filter: blur(10px);
|
|
|
|
:deep(.q-banner__content) {
|
|
padding: $space-4;
|
|
}
|
|
|
|
:deep(.q-banner__avatar) {
|
|
margin-right: $space-4;
|
|
align-self: flex-start;
|
|
margin-top: $space-1;
|
|
}
|
|
}
|
|
|
|
.banner-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.banner-title {
|
|
font-size: $text-lg;
|
|
font-weight: 700;
|
|
margin-bottom: $space-2;
|
|
color: white;
|
|
}
|
|
|
|
.banner-description {
|
|
font-size: $text-base;
|
|
color: rgba(white, 0.9);
|
|
margin-bottom: $space-3;
|
|
}
|
|
|
|
.features-list {
|
|
margin: 0;
|
|
padding-left: $space-5;
|
|
|
|
li {
|
|
font-size: $text-sm;
|
|
color: rgba(white, 0.8);
|
|
margin-bottom: $space-1;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.banner-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: $space-2;
|
|
align-items: stretch;
|
|
margin-top: $space-4;
|
|
|
|
@media (min-width: 600px) {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
margin-top: 0;
|
|
}
|
|
|
|
.q-btn {
|
|
min-width: 100px;
|
|
|
|
@media (max-width: 599px) {
|
|
width: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 動畫效果
|
|
.pwa-install-prompt {
|
|
animation: slideUp 0.5s ease-out;
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
transform: translateY(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
// 響應式調整
|
|
@media (max-width: 480px) {
|
|
.banner-content {
|
|
.banner-title {
|
|
font-size: $text-base;
|
|
}
|
|
|
|
.banner-description {
|
|
font-size: $text-sm;
|
|
}
|
|
|
|
.features-list {
|
|
padding-left: $space-4;
|
|
|
|
li {
|
|
font-size: $text-xs;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style> |