- 接入火山引擎豆包 Chat API 生成儿童故事(SSE 流式进度) - 新增 /api/stories 接口加载历史故事到书架 - 新增 /api/playlist 接口加载历史歌曲到唱片架 - 书架排序:预设故事在前,AI 生成在后 - AI 生成的故事显示"暂无封面"淡紫渐变占位 - 保存故事时传回真实标题+内容(不再用 mock) - 修复 Windows GBK 编码导致的中文乱码问题 - 新增 MusicGenerationService 单例管理音乐生成 - 音乐页心情卡片 UI 重做 + 歌词可读性优化 - 添加豆包 API 参考文档和故事创作 prompt Co-authored-by: Cursor <cursoragent@cursor.com>
399 lines
13 KiB
Dart
399 lines
13 KiB
Dart
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,
|
||
});
|
||
}
|