diff --git a/packages/distribute/app.vue b/packages/distribute/app.vue index b4585ef..ff1a8c8 100644 --- a/packages/distribute/app.vue +++ b/packages/distribute/app.vue @@ -2,6 +2,7 @@ diff --git a/packages/distribute/components/PWAInstallBanner.vue b/packages/distribute/components/PWAInstallBanner.vue new file mode 100644 index 0000000..0d601a9 --- /dev/null +++ b/packages/distribute/components/PWAInstallBanner.vue @@ -0,0 +1,117 @@ + + + diff --git a/packages/distribute/components/PWAInstallButton.vue b/packages/distribute/components/PWAInstallButton.vue new file mode 100644 index 0000000..6a6b953 --- /dev/null +++ b/packages/distribute/components/PWAInstallButton.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/distribute/composables/usePWAInstall.ts b/packages/distribute/composables/usePWAInstall.ts new file mode 100644 index 0000000..1ac2f0a --- /dev/null +++ b/packages/distribute/composables/usePWAInstall.ts @@ -0,0 +1,58 @@ +import { ref, onMounted } from 'vue' + +export function usePWAInstall() { + const deferredPrompt = ref(null) + const canInstall = ref(false) + const isInstalled = ref(false) + + onMounted(() => { + // 检查是否已安装 + if (window.matchMedia('(display-mode: standalone)').matches) { + isInstalled.value = true + return + } + + // 监听安装提示事件 + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault() + deferredPrompt.value = e + canInstall.value = true + }) + + // 监听安装成功事件 + window.addEventListener('appinstalled', () => { + deferredPrompt.value = null + canInstall.value = false + isInstalled.value = true + }) + }) + + async function install() { + if (!deferredPrompt.value) { + return false + } + + try { + await deferredPrompt.value.prompt() + const { outcome } = await deferredPrompt.value.userChoice + + if (outcome === 'accepted') { + deferredPrompt.value = null + canInstall.value = false + return true + } + + return false + } + catch (error) { + console.error('安装失败:', error) + return false + } + } + + return { + canInstall, + isInstalled, + install, + } +} diff --git a/packages/distribute/i18n/locales/en-US.json b/packages/distribute/i18n/locales/en-US.json index 1ec7658..9d384fc 100644 --- a/packages/distribute/i18n/locales/en-US.json +++ b/packages/distribute/i18n/locales/en-US.json @@ -13,5 +13,8 @@ "iosDownloads": "iOS Downloads", "androidDownloads": "Android Downloads", "noAppsFound": "No apps found", - "downloadApp": "Download App" + "downloadApp": "Download App", + "installPWA": "Install App Store", + "installed": "Installed", + "installDesc": "Install on your home screen and use it like a native app" } diff --git a/packages/distribute/i18n/locales/zh-CN.json b/packages/distribute/i18n/locales/zh-CN.json index 0caca73..22d3c39 100644 --- a/packages/distribute/i18n/locales/zh-CN.json +++ b/packages/distribute/i18n/locales/zh-CN.json @@ -13,5 +13,8 @@ "iosDownloads": "iOS 下载", "androidDownloads": "Android 下载", "noAppsFound": "没有找到应用", - "downloadApp": "下载应用" + "downloadApp": "下载应用", + "installPWA": "安装应用商店", + "installed": "已安装", + "installDesc": "安装到主屏幕,像原生应用一样使用" } diff --git a/packages/distribute/nuxt.config.ts b/packages/distribute/nuxt.config.ts index ab22b5a..eb6b296 100644 --- a/packages/distribute/nuxt.config.ts +++ b/packages/distribute/nuxt.config.ts @@ -4,10 +4,84 @@ export default defineNuxtConfig({ '@nuxt/ui', '@nuxtjs/i18n', '@nuxt/eslint', + '@vite-pwa/nuxt', ], devtools: { enabled: true }, + pwa: { + registerType: 'autoUpdate', + manifest: { + name: 'Riwa App 下载', + short_name: 'Riwa应用商店', + description: 'Riwa App 下载 - iOS, Android, H5', + theme_color: '#3b82f6', + background_color: '#ffffff', + display: 'standalone', + scope: '/', + start_url: '/', + icons: [ + { + src: '/favicon.svg', + sizes: '512x512', + type: 'image/svg+xml', + purpose: 'any', + }, + { + src: '/favicon.svg', + sizes: '192x192', + type: 'image/svg+xml', + }, + ], + }, + workbox: { + navigateFallback: '/', + globPatterns: ['**/*.{js,css,html,png,svg,ico}'], + cleanupOutdatedCaches: true, + runtimeCaching: [ + { + urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'google-fonts-cache', + expiration: { + maxEntries: 10, + maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + { + urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'gstatic-fonts-cache', + expiration: { + maxEntries: 10, + maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + ], + }, + client: { + installPrompt: true, + periodicSyncForUpdates: 3600, + }, + devOptions: { + enabled: true, + type: 'module', + }, + injectManifest: { + globPatterns: ['**/*.{js,css,html,png,svg,ico}'], + }, + }, + css: [ '~/assets/css/main.css', '~/assets/css/animations.css', diff --git a/packages/distribute/package.json b/packages/distribute/package.json index b913852..4fa302a 100644 --- a/packages/distribute/package.json +++ b/packages/distribute/package.json @@ -9,7 +9,7 @@ "generate": "nuxt generate", "preview": "nuxt preview", "postinstall": "nuxt prepare", - "deploy": "pnpx wrangler pages deploy dist --project-name=appstore --branch=main" + "deploy": "pnpx wrangler pages deploy .output/public --project-name=appstore --branch=main" }, "dependencies": { "@nuxt/ui": "^4.3.0", @@ -18,6 +18,7 @@ }, "devDependencies": { "@nuxt/eslint": "^1.12.1", + "@vite-pwa/nuxt": "^1.1.0", "typescript": "~5.9.3" } } diff --git a/packages/distribute/pages/index.vue b/packages/distribute/pages/index.vue index 61c2d51..f4ff5f7 100644 --- a/packages/distribute/pages/index.vue +++ b/packages/distribute/pages/index.vue @@ -137,7 +137,12 @@ useHead({ - + + +
+ +
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02ad953..46ef687 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,6 +250,9 @@ importers: '@nuxt/eslint': specifier: ^1.12.1 version: 1.12.1(@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint-plugin-format@1.1.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@vite-pwa/nuxt': + specifier: ^1.1.0 + version: 1.1.0(magicast@0.5.1)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0) typescript: specifier: ~5.9.3 version: 5.9.3 @@ -3374,6 +3377,14 @@ packages: engines: {node: '>=18'} hasBin: true + '@vite-pwa/nuxt@1.1.0': + resolution: {integrity: sha512-OKrqHg9PHCqp9dlrtCaLlh55V0xEG/zkXjvpl2nE+6IB3xW8mqnH0hXYc1pjN7qv0JzB+lbCfWxFsg5EZvAjWA==} + peerDependencies: + '@vite-pwa/assets-generator': ^1.0.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + '@vitejs/plugin-legacy@7.2.1': resolution: {integrity: sha512-CaXb/y0mlfu7jQRELEJJc2/5w2bX2m1JraARgFnvSB2yfvnCNJVWWlqAo6WjnKoepOwKx8gs0ugJThPLKCOXIg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -12359,6 +12370,19 @@ snapshots: - rollup - supports-color + '@vite-pwa/nuxt@1.1.0(magicast@0.5.1)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0)': + dependencies: + '@nuxt/kit': 3.20.2(magicast@0.5.1) + pathe: 1.1.2 + ufo: 1.6.1 + vite-plugin-pwa: 1.2.0(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0) + transitivePeerDependencies: + - magicast + - supports-color + - vite + - workbox-build + - workbox-window + '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5