rtc_prd/airhub_app/lib/pages/profile/notification_page.dart
seaislee1209 8f5fb32b37 feat(story,music,server): 豆包故事生成 + 历史数据持久化 + 封面占位
- 接入火山引擎豆包 Chat API 生成儿童故事(SSE 流式进度)
- 新增 /api/stories 接口加载历史故事到书架
- 新增 /api/playlist 接口加载历史歌曲到唱片架
- 书架排序:预设故事在前,AI 生成在后
- AI 生成的故事显示"暂无封面"淡紫渐变占位
- 保存故事时传回真实标题+内容(不再用 mock)
- 修复 Windows GBK 编码导致的中文乱码问题
- 新增 MusicGenerationService 单例管理音乐生成
- 音乐页心情卡片 UI 重做 + 歌词可读性优化
- 添加豆包 API 参考文档和故事创作 prompt

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:11:58 +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,
),
),
],
],
),
],
),
),
),
// ── 展开详情区域(只动画高度,宽度始终满宽,避免文字竖排) ──
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.detail,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF374151),
height: 1.7,
),
),
)
: const SizedBox(width: double.infinity, height: 0),
),
),
],
),
),
);
}
// ─── 通知图标 ───
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,
});
}