rtc_prd/airhub_app/lib/pages/profile/notification_page.dart
seaislee1209 f9666d4aa3 feat: UI规范化 + 故事吸入动画 + 音乐页面优化
- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化
- 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失)
- 书架页: bookPop弹出+粒子效果(三段式动画完整链路)
- 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局
- 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化
- 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌
- 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面
- 阶段总结文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:34:53 +08:00

399 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:airhub_app/theme/design_tokens.dart';
import 'package:airhub_app/widgets/animated_gradient_background.dart';
/// 消息通知页面 — 还原 notifications.html
class NotificationPage extends StatefulWidget {
const NotificationPage({super.key});
@override
State<NotificationPage> createState() => _NotificationPageState();
}
class _NotificationPageState extends State<NotificationPage> {
/// 当前展开的通知 index-1 表示全部折叠,手风琴模式)
int _expandedIndex = -1;
/// 已读标记index set
final Set<int> _readIndices = {};
/// 示例通知数据
final List<_NotificationData> _notifications = [
_NotificationData(
type: _NotifType.system,
icon: Icons.warning_amber_rounded,
title: '系统更新',
time: '10:30',
desc: 'Airhub V1.2.0 版本更新已准备就绪',
detail: 'Airhub V1.2.0 版本更新说明:\n\n'
'• 新增"喂养指南"功能,现在您可以查看详细的电子宠物养成手册了。\n'
'• 优化了设备连接的稳定性,修复了部分机型搜索不到设备的问题。\n'
'• 提升了整体界面的流畅度,增加了更多微交互动画。\n\n'
'建议您连接 Wi-Fi 后进行更新,以获得最佳体验。',
isUnread: true,
),
_NotificationData(
type: _NotifType.activity,
emojiIcon: '🎁',
title: '新春活动',
time: '昨天',
desc: '领取您的新春限定水豚皮肤"招财进宝"',
detail: '🎉 新春限定皮肤上线啦!\n\n'
'为了庆祝即将到来的春节,我们特别推出了水豚的"招财进宝"限定皮肤。\n\n'
'活动亮点:\n'
'• 限定版红色唐装外观\n'
'• 专属的春节互动音效\n'
'• 限时免费领取的节庆道具\n\n'
'活动截止日期2月15日',
isUnread: false,
),
_NotificationData(
type: _NotifType.system,
icon: Icons.person_add_alt_1_outlined,
title: '新设备绑定',
time: '1月20日',
desc: '您的新设备"Airhub_5G"已成功绑定',
detail: '恭喜!您已成功绑定新设备 Airhub_5G。\n\n'
'接下来的几步可以帮助您快速上手:\n'
'• 前往角色记忆页面,注入您喜欢的角色人格。\n'
'• 进入设置页面配置您的偏好设置。\n'
'• 查看帮助中心的入门指南,解锁更多互动玩法。\n\n'
'祝您开启一段奇妙的 AI 陪伴旅程!',
isUnread: false,
),
];
void _toggleNotification(int index) {
setState(() {
if (_expandedIndex == index) {
_expandedIndex = -1; // 折叠
} else {
_expandedIndex = index; // 展开,并标记已读
_readIndices.add(index);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
const AnimatedGradientBackground(),
Column(
children: [
_buildHeader(context),
Expanded(
child: 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: _notifications.isEmpty
? _buildEmptyState()
: _buildNotificationList(context),
),
),
],
),
],
),
);
}
// ─── Header ───
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: BoxDecoration(
color: const 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 _buildNotificationList(BuildContext context) {
return 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(index);
},
);
}
// ─── 单条通知卡片 ───
Widget _buildNotificationCard(int index) {
final notif = _notifications[index];
final isExpanded = _expandedIndex == index;
final isUnread = notif.isUnread && !_readIndices.contains(index);
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) // 0.85 opacity
: const Color(0xB3FFFFFF), // 0.7 opacity
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0x66FFFFFF), // rgba(255,255,255,0.4)
),
boxShadow: const [AppShadows.card],
),
child: Column(
children: [
// ── 卡片头部(可点击) ──
GestureDetector(
onTap: () => _toggleNotification(index),
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: [
Text(
notif.title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
Text(
notif.time,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
const SizedBox(height: 4),
Text(
notif.desc,
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), // rgba(249, 250, 251, 0.5)
border: Border(
top: BorderSide(
color: Color(0x0D000000), // rgba(0,0,0,0.05)
),
),
),
padding: const EdgeInsets.all(20),
child: Text(
notif.detail,
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(_NotificationData notif) {
final isSystem = notif.type == _NotifType.system;
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSystem
? const Color(0xFFEFF6FF) // 蓝色系统背景
: const Color(0xFFFFF7ED), // 橙色活动背景
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
child: notif.emojiIcon != null
? Text(
notif.emojiIcon!,
style: const TextStyle(fontSize: 18),
)
: Icon(
notif.icon ?? Icons.info_outline,
size: 20,
color: isSystem
? const Color(0xFF3B82F6)
: const Color(0xFFF97316),
),
);
}
}
// ─── 数据模型 ───
enum _NotifType { system, activity }
class _NotificationData {
final _NotifType type;
final IconData? icon;
final String? emojiIcon;
final String title;
final String time;
final String desc;
final String detail;
final bool isUnread;
_NotificationData({
required this.type,
this.icon,
this.emojiIcon,
required this.title,
required this.time,
required this.desc,
required this.detail,
this.isUnread = false,
});
}