Files
video-gen/video-gen-app/src/components/NotificationPopup.tsx
T
2026-06-15 17:50:47 +08:00

180 lines
7.4 KiB
TypeScript

import React, { useEffect, useState, useCallback } from 'react';
import { Tag, Typography, Button } from 'antd';
import { BellOutlined, ThunderboltOutlined, GiftOutlined, StarOutlined, CloseOutlined } from '@ant-design/icons';
import { getNotifications, markNotificationRead } from '../api';
import { useAuthStore } from '../store/useAuthStore';
interface Notification {
id: string;
title: string;
content: string;
type: string;
isRead: boolean;
createdAt: string;
}
const typeConfig: Record<string, { gradient: string; icon: React.ReactNode; label: string }> = {
system: { gradient: 'linear-gradient(135deg, #6366f1, #818cf8)', icon: <ThunderboltOutlined />, label: '系统通知' },
credit: { gradient: 'linear-gradient(135deg, #f59e0b, #fbbf24)', icon: <StarOutlined />, label: '积分通知' },
promo: { gradient: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', icon: <GiftOutlined />, label: '活动通知' },
};
const NotificationPopup: React.FC = () => {
const [visible, setVisible] = useState(false);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const refreshUser = useAuthStore((state) => state.refreshUser);
const fetchNotifications = useCallback(async () => {
try {
const result = await getNotifications();
const data = result.items || [];
const unread = data.filter((n: Notification) => !n.isRead);
setNotifications(unread);
if (unread.length > 0 && !visible) {
setVisible(true);
setCurrentIndex(0);
}
} catch { /* ignore */ }
}, [visible]);
useEffect(() => {
fetchNotifications();
const timer = setInterval(fetchNotifications, 30000);
return () => clearInterval(timer);
}, []);
const handleAcknowledge = async () => {
const current = notifications[currentIndex];
if (current) {
try { await markNotificationRead(current.id); } catch { /* ignore */ }
}
if (currentIndex < notifications.length - 1) {
setCurrentIndex(currentIndex + 1);
} else {
setVisible(false);
setNotifications([]);
setCurrentIndex(0);
// 刷新用户信息(包括积分)
try { await refreshUser(); } catch { /* ignore */ }
}
};
const handleClose = async () => {
const current = notifications[currentIndex];
if (current) {
try { await markNotificationRead(current.id); } catch { /* ignore */ }
}
setVisible(false);
setNotifications([]);
setCurrentIndex(0);
};
if (!visible || notifications.length === 0) return null;
const current = notifications[currentIndex];
const tc = typeConfig[current.type] || typeConfig.system;
return (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(15,15,35,0.6)', backdropFilter: 'blur(8px)',
}} onClick={handleClose}>
<div onClick={e => e.stopPropagation()} style={{
width: 420, borderRadius: 20, overflow: 'hidden',
boxShadow: '0 32px 80px rgba(0,0,0,0.35)',
animation: 'slideUp 0.35s cubic-bezier(0.16,1,0.3,1)',
}}>
{/* Header */}
<div style={{
background: tc.gradient,
padding: '28px 28px 24px',
position: 'relative', overflow: 'hidden',
}}>
<div style={{ position: 'absolute', right: -20, top: -20, width: 120, height: 120, borderRadius: '50%', background: 'rgba(255,255,255,0.1)' }} />
<div style={{ position: 'absolute', right: 50, bottom: -30, width: 80, height: 80, borderRadius: '50%', background: 'rgba(255,255,255,0.06)' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{
width: 48, height: 48, borderRadius: 14,
background: 'rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
backdropFilter: 'blur(4px)', fontSize: 22, color: '#fff',
}}>{tc.icon}</div>
<div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginBottom: 2 }}>新消息通知</div>
<div style={{ color: '#fff', fontSize: 17, fontWeight: 700 }}>{current.title}</div>
</div>
</div>
<div onClick={handleClose} style={{
width: 28, height: 28, borderRadius: 8,
background: 'rgba(255,255,255,0.15)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'background 0.2s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.25)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.15)'; }}
>
<CloseOutlined style={{ color: '#fff', fontSize: 12 }} />
</div>
</div>
</div>
{/* Body */}
<div style={{ background: '#fff', padding: '24px 28px 28px' }}>
<div style={{
padding: '18px 20px', background: '#f8fafc', borderRadius: 14,
marginBottom: 20, border: '1px solid #f0f0f5',
}}>
<div style={{ fontSize: 14, color: '#334155', lineHeight: 1.8 }}>
{current.content}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag color={tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6'}
style={{ borderRadius: 6, border: 'none', padding: '2px 10px', fontSize: 12 }}>
{tc.label}
</Tag>
{notifications.length > 1 && (
<span style={{ fontSize: 12, color: '#94a3b8' }}>{currentIndex + 1} / {notifications.length}</span>
)}
</div>
<Button type="primary" onClick={handleAcknowledge} style={{
borderRadius: 10, fontWeight: 600, height: 38,
background: tc.gradient, border: 'none',
paddingLeft: 28, paddingRight: 28,
boxShadow: `0 6px 16px ${tc.gradient.includes('#6366f1') ? 'rgba(99,102,241,0.3)' : tc.gradient.includes('#f59e0b') ? 'rgba(245,158,11,0.3)' : 'rgba(139,92,246,0.3)'}`,
}}>
我已知晓
</Button>
</div>
{/* Dots */}
{notifications.length > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, marginTop: 18 }}>
{notifications.map((_, i) => (
<div key={i} style={{
width: i === currentIndex ? 22 : 6, height: 6, borderRadius: 3,
background: i === currentIndex ? (tc.gradient.includes('#6366f1') ? '#6366f1' : tc.gradient.includes('#f59e0b') ? '#f59e0b' : '#8b5cf6') : '#e2e8f0',
transition: 'all 0.3s ease',
}} />
))}
</div>
)}
</div>
</div>
<style>{`
@keyframes slideUp {
from { opacity: 0; transform: translateY(24px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
`}</style>
</div>
);
};
export default NotificationPopup;