/** * PushNotificationToggle - 推送通知开关组件 * * 显示当前推送订阅状态,提供订阅/取消订阅的切换按钮。 * 可在设置页面或任意位置嵌入使用。 * * 交互流程: * 1. 检查浏览器支持 → 不支持则显示提示 * 2. 检查通知权限 → 未授权则引导用户授权 * 3. 检查当前订阅状态 → 显示对应按钮 * 4. 点击按钮 → 执行订阅/取消订阅 + 后端同步 */ import React, { useState, useEffect, useCallback } from 'react' import { useTranslation } from '../i18n' import { isPushSupported, getNotificationPermission, requestNotificationPermission, subscribeToPush, unsubscribeFromPush, getPushSubscription, registerSubscriptionWithBackend, unregisterSubscriptionFromBackend, } from '../utils/pushNotifications' // ================================================================ // 状态枚举 // ================================================================ type SubscriptionStatus = | 'unsupported' // 浏览器不支持 Push | 'loading' // 正在检查/执行操作 | 'denied' // 通知权限被拒绝 | 'unsubscribed' // 未订阅 | 'subscribed' // 已订阅 // ================================================================ // PushNotificationToggle 组件 // ================================================================ export default function PushNotificationToggle() { const { t } = useTranslation() const [status, setStatus] = useState('loading') const [error, setError] = useState(null) const [subscriptionEndpoint, setSubscriptionEndpoint] = useState(null) // ---------------------------------------------------------- // 初始化:检查当前状态 // ---------------------------------------------------------- useEffect(() => { checkStatus() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const checkStatus = useCallback(async () => { setError(null) // 1. 检查浏览器支持 if (!isPushSupported()) { setStatus('unsupported') return } // 2. 检查通知权限 const permission = getNotificationPermission() if (permission === 'denied') { setStatus('denied') return } // 3. 检查当前订阅 setStatus('loading') try { const sub = await getPushSubscription() if (sub) { setSubscriptionEndpoint(sub.endpoint || null) setStatus('subscribed') } else { setSubscriptionEndpoint(null) setStatus('unsubscribed') } } catch { setSubscriptionEndpoint(null) setStatus('unsubscribed') } }, []) // ---------------------------------------------------------- // 开启通知 // ---------------------------------------------------------- const handleEnable = useCallback(async () => { setError(null) setStatus('loading') try { console.log('[PushToggle] 开始开启推送通知...') // 先请求通知权限(如果尚未授予) let permission = getNotificationPermission() if (permission !== 'granted') { permission = await requestNotificationPermission() } if (permission !== 'granted') { console.warn('[PushToggle] 通知权限未授予') setStatus('denied') setError(t('components.push.permissionNotGranted')) return } // 订阅 Push(调用浏览器 PushManager.subscribe) console.log('[PushToggle] 调用 subscribeToPush...') const subscription = await subscribeToPush() console.log('[PushToggle] 浏览器订阅成功:', subscription.endpoint) // 立刻更新 UI 状态为已订阅(不等后端同步完成) setSubscriptionEndpoint(subscription.endpoint || null) setStatus('subscribed') console.log('[PushToggle] UI 已切换为已订阅状态') // 后台同步到后端(不阻塞 UI) try { console.log('[PushToggle] 同步订阅到后端...') await registerSubscriptionWithBackend(subscription) console.log('[PushToggle] 后端订阅注册成功') } catch (backendErr) { console.warn('[PushToggle] 后端同步失败(浏览器订阅已生效):', backendErr) setError(t('components.push.backendSyncFailed')) } } catch (err) { console.error('[PushToggle] 开启失败:', err) const message = err instanceof Error ? err.message : t('components.push.enableFailed') setError(message) // 回退到检查当前实际订阅状态 await checkStatus() } }, [checkStatus]) // ---------------------------------------------------------- // 关闭通知 // ---------------------------------------------------------- const handleDisable = useCallback(async () => { setError(null) setStatus('loading') try { // 获取当前端点用于后端取消注册 const sub = await getPushSubscription() const endpoint = sub?.endpoint || subscriptionEndpoint // 取消浏览器 Push 订阅 await unsubscribeFromPush() // 从后端取消注册 if (endpoint) { try { await unregisterSubscriptionFromBackend(endpoint) } catch { // 后端取消失败不影响前端状态 console.warn('[PushToggle] 后端取消订阅失败,已在前端取消') } } setSubscriptionEndpoint(null) setStatus('unsubscribed') } catch (err) { const message = err instanceof Error ? err.message : t('components.push.disableFailed') setError(message) await checkStatus() } }, [subscriptionEndpoint, checkStatus]) // ---------------------------------------------------------- // 渲染:不支持 // ---------------------------------------------------------- if (status === 'unsupported') { return (

{t('components.push.notSupported')}

{t('components.push.browserNotSupported')}

) } // ---------------------------------------------------------- // 渲染:加载中 // ---------------------------------------------------------- if (status === 'loading') { return (

{t('components.push.checking')}

) } // ---------------------------------------------------------- // 渲染:权限被拒绝 // ---------------------------------------------------------- if (status === 'denied') { return (

{t('components.push.permissionDenied')}

{t('components.push.permissionDeniedHint')}

) } // ---------------------------------------------------------- // 渲染:已订阅 / 未订阅 // ---------------------------------------------------------- return (
{/* 标题行 */}
{/* 状态图标 */} {status === 'subscribed' ? (
) : (
)} {/* 状态文字 */}

{status === 'subscribed' ? t('components.push.enabled') : t('components.push.disabled')}

{status === 'subscribed' ? t('components.push.enabledDesc') : t('components.push.disabledDesc')}

{/* 切换按钮 */} {status === 'subscribed' ? ( ) : ( )}
{/* 错误提示 */} {error && (

{error}

)}
) }