feat: 添加推送通知功能,支持Web Push订阅和发送
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Bell, BellOff, Send } from 'lucide-react';
|
||||
import { urlBase64ToUint8Array } from '../utils/webPush';
|
||||
|
||||
export default function PushSubscription() {
|
||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
|
||||
const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window) {
|
||||
// Register SW if not already (though it might be done in layout or separate file)
|
||||
navigator.serviceWorker.ready.then(reg => {
|
||||
setRegistration(reg);
|
||||
reg.pushManager.getSubscription().then(sub => {
|
||||
if (sub) {
|
||||
setSubscription(sub);
|
||||
setIsSubscribed(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const subscribeUser = async () => {
|
||||
if (!registration) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/push/vapid-public-key');
|
||||
const { publicKey } = await response.json();
|
||||
const convertedVapidKey = urlBase64ToUint8Array(publicKey);
|
||||
|
||||
const sub = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: convertedVapidKey
|
||||
});
|
||||
|
||||
await fetch('/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(sub),
|
||||
});
|
||||
|
||||
setSubscription(sub);
|
||||
setIsSubscribed(true);
|
||||
alert('订阅成功!');
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe the user: ', error);
|
||||
alert('订阅失败,请检查控制台日志。');
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribeUser = async () => {
|
||||
if (!subscription) return;
|
||||
try {
|
||||
await subscription.unsubscribe();
|
||||
// Optionally notify backend to remove subscription
|
||||
setSubscription(null);
|
||||
setIsSubscribed(false);
|
||||
alert('已取消订阅。');
|
||||
} catch (error) {
|
||||
console.error('Error unsubscribing', error);
|
||||
}
|
||||
};
|
||||
|
||||
const sendTestPush = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/push/test', { method: 'POST' });
|
||||
if (!res.ok) throw new Error('Failed to send test push');
|
||||
alert('测试推送已发送,请检查通知!');
|
||||
} catch (error) {
|
||||
console.error('Failed to send test push', error);
|
||||
alert('发送测试推送失败');
|
||||
}
|
||||
};
|
||||
|
||||
if (!registration) {
|
||||
return null; // Service Worker not ready or not supported
|
||||
}
|
||||
|
||||
if (!isSubscribed) {
|
||||
return (
|
||||
<button
|
||||
onClick={subscribeUser}
|
||||
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 bg-gray-100 hover:bg-gray-200 dark:text-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
title="开启推送通知"
|
||||
>
|
||||
<BellOff className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">订阅通知</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 text-blue-600 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/40"
|
||||
>
|
||||
<Bell className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">已订阅</span>
|
||||
</button>
|
||||
|
||||
{isMenuOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
<div className="absolute right-0 top-full w-40 pt-1 z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
sendTestPush();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center"
|
||||
>
|
||||
<Send className="h-3 w-3 mr-2" />
|
||||
发送测试
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
unsubscribeUser();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center"
|
||||
>
|
||||
<BellOff className="h-3 w-3 mr-2" />
|
||||
取消订阅
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user