rtc_prd/airhub_app/lib/pages/profile/notification_page.dart
2026-02-09 18:03:55 +08:00

392 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:airhub_app/theme/design_tokens.dart';
import 'package:airhub_app/widgets/animated_gradient_background.dart';
import 'package:airhub_app/features/notification/domain/entities/app_notification.dart';
import 'package:airhub_app/features/notification/presentation/controllers/notification_controller.dart';
/// 消息通知页面 — 接入真实 API
class NotificationPage extends ConsumerStatefulWidget {
const NotificationPage({super.key});
@override
ConsumerState<NotificationPage> createState() => _NotificationPageState();
}
class _NotificationPageState extends ConsumerState<NotificationPage> {
/// 当前展开的通知 idnull 表示全部折叠)
int? _expandedId;
void _toggleNotification(AppNotification notif) {
setState(() {
if (_expandedId == notif.id) {
_expandedId = null;
} else {
_expandedId = notif.id;
// Mark as read when expanded
if (!notif.isRead) {
ref.read(notificationControllerProvider.notifier).markAsRead(notif.id);
}
}
});
}
@override
Widget build(BuildContext context) {
final notificationsAsync = ref.watch(notificationControllerProvider);
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
const AnimatedGradientBackground(),
Column(
children: [
_buildHeader(context),
Expanded(
child: notificationsAsync.when(
loading: () => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
error: (error, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'加载失败',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () => ref.read(notificationControllerProvider.notifier).refresh(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Text('重试', style: TextStyle(color: Colors.white)),
),
),
],
),
),
data: (notifications) {
if (notifications.isEmpty) {
return _buildEmptyState();
}
return ShaderMask(
shaderCallback: (Rect rect) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.black,
Colors.transparent,
],
stops: [0.0, 0.03, 0.95, 1.0],
).createShader(rect);
},
blendMode: BlendMode.dstIn,
child: ListView.builder(
padding: EdgeInsets.only(
top: 8,
left: 20,
right: 20,
bottom: 40 + MediaQuery.of(context).padding.bottom,
),
itemCount: notifications.length,
itemBuilder: (context, index) {
return _buildNotificationCard(notifications[index]);
},
),
);
},
),
),
],
),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 20,
left: AppSpacing.lg,
right: AppSpacing.lg,
bottom: AppSpacing.md,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.iconBtnBg,
borderRadius: BorderRadius.circular(AppRadius.button),
),
child: const Icon(
Icons.arrow_back_ios_new,
color: AppColors.textPrimary,
size: 18,
),
),
),
Text('消息通知', style: AppTextStyles.title),
const SizedBox(width: 40),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: Color(0x1A9CA3AF),
shape: BoxShape.circle,
),
child: const Icon(
Icons.notifications_none_rounded,
size: 48,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 20),
const Text(
'暂无新消息',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
const Text(
'新的通知会在这里显示',
style: TextStyle(
fontSize: 13,
color: AppColors.textHint,
),
),
],
),
);
}
Widget _buildNotificationCard(AppNotification notif) {
final isExpanded = _expandedId == notif.id;
final isUnread = !notif.isRead;
final timeStr = _formatTime(notif.createdAt);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: isExpanded
? const Color(0xD9FFFFFF)
: const Color(0xB3FFFFFF),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0x66FFFFFF)),
boxShadow: const [AppShadows.card],
),
child: Column(
children: [
GestureDetector(
onTap: () => _toggleNotification(notif),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNotifIcon(notif),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
notif.title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Text(
timeStr,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
const SizedBox(height: 4),
Text(
notif.description,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF6B7280),
height: 1.5,
),
),
],
),
),
const SizedBox(width: 8),
Column(
children: [
AnimatedRotation(
turns: isExpanded ? 0.25 : 0,
duration: const Duration(milliseconds: 300),
child: const Icon(
Icons.chevron_right,
color: AppColors.textHint,
size: 20,
),
),
if (isUnread) ...[
const SizedBox(height: 6),
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppColors.notificationDot,
shape: BoxShape.circle,
),
),
],
],
),
],
),
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Color(0x80F9FAFB),
border: Border(
top: BorderSide(color: Color(0x0D000000)),
),
),
padding: const EdgeInsets.all(20),
child: Text(
notif.content,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF374151),
height: 1.7,
),
),
),
crossFadeState: isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300),
sizeCurve: Curves.easeInOut,
),
],
),
),
);
}
Widget _buildNotifIcon(AppNotification notif) {
final isSystem = notif.type == 'system';
final isActivity = notif.type == 'activity';
IconData icon;
if (isSystem) {
icon = Icons.info_outline;
} else if (isActivity) {
icon = Icons.card_giftcard;
} else {
icon = Icons.devices;
}
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSystem
? const Color(0xFFEFF6FF)
: isActivity
? const Color(0xFFFFF7ED)
: const Color(0xFFF0FDF4),
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
child: notif.imageUrl.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
notif.imageUrl,
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Icon(
icon,
size: 20,
color: isSystem ? const Color(0xFF3B82F6) : const Color(0xFFF97316),
),
),
)
: Icon(
icon,
size: 20,
color: isSystem
? const Color(0xFF3B82F6)
: isActivity
? const Color(0xFFF97316)
: const Color(0xFF22C55E),
),
);
}
String _formatTime(String createdAt) {
try {
final dt = DateTime.parse(createdAt);
final now = DateTime.now();
final diff = now.difference(dt);
if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
if (diff.inHours < 24) return '${diff.inHours}小时前';
if (diff.inDays < 2) return '昨天';
if (diff.inDays < 7) return '${diff.inDays}天前';
return '${dt.month}${dt.day}';
} catch (_) {
return createdAt;
}
}
}