rtc_prd/airhub_app/lib/pages/profile/notification_page.dart
seaislee1209 f26627a83f fix: 修复页面渐隐过大 + MP3进度条拖动 + notification字段修复
- 修复设置子页面/帮助页/喂养指南顶部渐隐区域过大(12%→5%)导致首行文字过淡
- 修复4首预设音乐(卡皮巴拉系列)因ID3标签过大导致进度条无法拖动
- 修复notification_page中notif.detail→notif.content字段名错误
- 新增测试生成的故事和音频文件

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 11:28:49 +08:00

396 lines
14 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,
),
),
],
],
),
],
),
),
),
// ── 展开详情区域(只动画高度,宽度始终满宽,避免文字竖排) ──
ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: isExpanded
? 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,
),
),
)
: const SizedBox(width: double.infinity, height: 0),
),
),
],
),
),
);
}
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;
}
}
}