Download vetcopilot-app/src/utils/pushNotifications.ts from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 8.46 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/utils/pushNotifications.ts
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/utils/pushNotifications.ts
-
curl -L -o pushNotifications.ts https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/utils/pushNotifications.ts
8.46 kB
| /** | |
| * Push Notification 工具模块 | |
| * | |
| * 封装浏览器 Push API 的订阅、取消订阅、权限管理等逻辑。 | |
| * 所有后端通信委托给 src/api/index.ts 中的 push 相关 API 函数。 | |
| */ | |
| import { | |
| subscribePush, | |
| unsubscribePush, | |
| getVapidPublicKey, | |
| } from '../api/index' | |
| // ================================================================ | |
| // 常量 | |
| // ================================================================ | |
| /** Service Worker 注册路径(相对于 public 目录,构建后位于根路径) */ | |
| const SW_PATH = '/sw.js' | |
| /** 缓存的 VAPID 公钥(从后端动态获取,避免前后端密钥不匹配) */ | |
| let _cachedVapidPublicKey: string | null = null | |
| /** | |
| * 获取 VAPID 公钥(优先缓存,其次云端获取,兜底环境变量) | |
| */ | |
| async function _getVapidPublicKey(): Promise<string> { | |
| if (_cachedVapidPublicKey) return _cachedVapidPublicKey | |
| try { | |
| _cachedVapidPublicKey = await getVapidPublicKey() | |
| console.log('[Push] VAPID 公钥已从后端获取') | |
| return _cachedVapidPublicKey! | |
| } catch (err) { | |
| console.warn('[Push] 无法从后端获取 VAPID 公钥,回退到环境变量:', err) | |
| const fallback = import.meta.env.VITE_VAPID_PUBLIC_KEY | |
| if (fallback) { | |
| _cachedVapidPublicKey = fallback | |
| return fallback | |
| } | |
| throw new Error('VAPID 公钥不可用,推送通知功能暂无法使用') | |
| } | |
| } | |
| // ================================================================ | |
| // 工具函数 | |
| // ================================================================ | |
| /** | |
| * 将 URL-safe Base64 字符串转换为 Uint8Array | |
| * | |
| * Push API 的 applicationServerKey 需要 ArrayBuffer 格式, | |
| * 而 VAPID 公钥通常以 URL-safe Base64 存储,此函数完成该转换。 | |
| * | |
| * @param base64String - URL-safe Base64 编码的 VAPID 公钥 | |
| * @returns 对应的 Uint8Array | |
| */ | |
| export function urlBase64ToUint8Array(base64String: string): Uint8Array { | |
| // 补齐 Base64 标准长度所需的 padding | |
| const padding = '='.repeat((4 - (base64String.length % 4)) % 4) | |
| const base64 = (base64String + padding) | |
| .replace(/-/g, '+') | |
| .replace(/_/g, '/') | |
| const rawData = window.atob(base64) | |
| const outputArray = new Uint8Array(rawData.length) | |
| for (let i = 0; i < rawData.length; i++) { | |
| outputArray[i] = rawData.charCodeAt(i) | |
| } | |
| return outputArray | |
| } | |
| // ================================================================ | |
| // Service Worker 注册 | |
| // ================================================================ | |
| let swRegistration: ServiceWorkerRegistration | null = null | |
| /** | |
| * 获取或注册 Service Worker。 | |
| * | |
| * 已注册的实例会被缓存,多次调用返回同一个 registration。 | |
| */ | |
| export async function getSWRegistration(): Promise<ServiceWorkerRegistration | null> { | |
| if (swRegistration) { | |
| return swRegistration | |
| } | |
| if (!('serviceWorker' in navigator)) { | |
| console.warn('[Push] 当前浏览器不支持 Service Worker') | |
| return null | |
| } | |
| try { | |
| swRegistration = await navigator.serviceWorker.register(SW_PATH, { | |
| scope: '/', | |
| }) | |
| console.log('[Push] Service Worker 已注册:', swRegistration.scope) | |
| return swRegistration | |
| } catch (err) { | |
| console.error('[Push] Service Worker 注册失败:', err) | |
| return null | |
| } | |
| } | |
| // ================================================================ | |
| // 通知权限 | |
| // ================================================================ | |
| /** | |
| * 检查浏览器是否支持 Push Notification。 | |
| */ | |
| export function isPushSupported(): boolean { | |
| return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window | |
| } | |
| /** | |
| * 获取当前通知权限状态。 | |
| */ | |
| export function getNotificationPermission(): NotificationPermission { | |
| if (!('Notification' in window)) { | |
| return 'denied' | |
| } | |
| return Notification.permission | |
| } | |
| /** | |
| * 请求通知权限。 | |
| * | |
| * @returns 用户选择的权限状态 | |
| */ | |
| export async function requestNotificationPermission(): Promise<NotificationPermission> { | |
| if (!('Notification' in window)) { | |
| console.warn('[Push] 当前浏览器不支持 Notification API') | |
| return 'denied' | |
| } | |
| try { | |
| const permission = await Notification.requestPermission() | |
| console.log('[Push] 通知权限:', permission) | |
| return permission | |
| } catch (err) { | |
| console.error('[Push] 请求通知权限失败:', err) | |
| return 'denied' | |
| } | |
| } | |
| // ================================================================ | |
| // Push 订阅管理 | |
| // ================================================================ | |
| /** | |
| * 订阅 Push Notification。 | |
| * | |
| * 流程: | |
| * 1. 确认浏览器支持 & 获取 SW registration | |
| * 2. 使用 VAPID 公钥创建 push subscription | |
| * 3. 将 subscription 序列化后返回 | |
| * | |
| * @param publicKey - VAPID 公钥(Base64 字符串),不传则使用默认配置 | |
| * @returns 订阅对象 { endpoint, keys: { p256dh, auth } } | |
| */ | |
| export async function subscribeToPush( | |
| publicKey?: string, | |
| ): Promise<PushSubscriptionJSON> { | |
| if (!isPushSupported()) { | |
| throw new Error('当前浏览器不支持 Push Notification') | |
| } | |
| const registration = await getSWRegistration() | |
| if (!registration) { | |
| throw new Error('Service Worker 未注册,无法订阅推送') | |
| } | |
| const permission = getNotificationPermission() | |
| if (permission !== 'granted') { | |
| throw new Error('通知权限未授予,无法订阅推送') | |
| } | |
| const applicationServerKey = urlBase64ToUint8Array( | |
| await _getVapidPublicKey(), | |
| ) | |
| try { | |
| const subscription = await registration.pushManager.subscribe({ | |
| userVisibleOnly: true, | |
| applicationServerKey: applicationServerKey as unknown as BufferSource, | |
| }) | |
| console.log('[Push] 订阅成功:', subscription.endpoint) | |
| const subJSON = subscription.toJSON() | |
| return subJSON | |
| } catch (err) { | |
| console.error('[Push] 订阅失败:', err) | |
| throw new Error(`Push 订阅失败: ${err instanceof Error ? err.message : String(err)}`) | |
| } | |
| } | |
| /** | |
| * 取消 Push Notification 订阅。 | |
| * | |
| * @returns 是否成功取消 | |
| */ | |
| export async function unsubscribeFromPush(): Promise<boolean> { | |
| const registration = await getSWRegistration() | |
| if (!registration) { | |
| console.warn('[Push] Service Worker 未注册,无需取消订阅') | |
| return false | |
| } | |
| try { | |
| const subscription = await registration.pushManager.getSubscription() | |
| if (!subscription) { | |
| console.log('[Push] 当前没有活跃的推送订阅') | |
| return false | |
| } | |
| const result = await subscription.unsubscribe() | |
| console.log('[Push] 取消订阅:', result ? '成功' : '失败') | |
| return result | |
| } catch (err) { | |
| console.error('[Push] 取消订阅失败:', err) | |
| return false | |
| } | |
| } | |
| /** | |
| * 获取当前 Push Subscription。 | |
| * | |
| * @returns 当前订阅对象的 JSON 表示,无订阅时返回 null | |
| */ | |
| export async function getPushSubscription(): Promise<PushSubscriptionJSON | null> { | |
| const registration = await getSWRegistration() | |
| if (!registration) { | |
| return null | |
| } | |
| try { | |
| const subscription = await registration.pushManager.getSubscription() | |
| if (!subscription) { | |
| return null | |
| } | |
| return subscription.toJSON() | |
| } catch (err) { | |
| console.error('[Push] 获取订阅状态失败:', err) | |
| return null | |
| } | |
| } | |
| // ================================================================ | |
| // 后端同步 | |
| // ================================================================ | |
| /** | |
| * 将订阅对象注册到后端。 | |
| * | |
| * @param subscription - push 订阅 JSON 对象 | |
| */ | |
| export async function registerSubscriptionWithBackend( | |
| subscription: PushSubscriptionJSON, | |
| ): Promise<void> { | |
| if (!subscription.endpoint) { | |
| throw new Error('订阅对象缺少 endpoint') | |
| } | |
| try { | |
| await subscribePush({ | |
| endpoint: subscription.endpoint, | |
| keys: { | |
| p256dh: subscription.keys?.p256dh || '', | |
| auth: subscription.keys?.auth || '', | |
| }, | |
| }) | |
| console.log('[Push] 订阅已同步到后端') | |
| } catch (err) { | |
| console.error('[Push] 后端订阅注册失败:', err) | |
| throw err | |
| } | |
| } | |
| /** | |
| * 从后端取消注册指定端点的订阅。 | |
| * | |
| * @param endpoint - push 订阅的 endpoint URL | |
| */ | |
| export async function unregisterSubscriptionFromBackend( | |
| endpoint: string, | |
| ): Promise<void> { | |
| try { | |
| await unsubscribePush(endpoint) | |
| console.log('[Push] 后端订阅已取消') | |
| } catch (err) { | |
| console.error('[Push] 后端取消订阅失败:', err) | |
| throw err | |
| } | |
| } | |