import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; /// 消息气泡组件 class MessageBubble extends StatelessWidget { final bool isUser; final String message; final DateTime timestamp; final bool isRecognizing; const MessageBubble({ Key? key, required this.isUser, required this.message, required this.timestamp, this.isRecognizing = false, }) : super(key: key); @override Widget build(BuildContext context) { final timeFormat = DateFormat('HH:mm'); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) _buildAvatar(isUser, context), const SizedBox(width: 8), Flexible( child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _getBubbleColor(context), borderRadius: BorderRadius.circular(18).copyWith( bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), bottomRight: !isUser ? const Radius.circular(18) : const Radius.circular(4), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: _buildMessageContent(context), ), const SizedBox(height: 4), // 识别中的消息不显示时间 if (!isRecognizing) Text( timeFormat.format(timestamp), style: TextStyle( fontSize: 10, color: Colors.grey[500], fontWeight: FontWeight.w400, ), ), ], ), ), const SizedBox(width: 8), if (isUser) _buildAvatar(isUser, context), ], ), ); } /// 获取气泡颜色 Color _getBubbleColor(BuildContext context) { if (isUser) { return isRecognizing ? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色 : Color(0xFF3B82F6); // 使用更明亮的蓝色 } else { return Color(0xFFF1F5F9); // 更淡的灰色背景 } } /// 构建头像 Widget _buildAvatar(bool isUser, BuildContext context) { return Container( width: 36, height: 36, decoration: BoxDecoration( color: isUser ? Colors.grey[300] : Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Icon( isUser ? Icons.person : Icons.smart_toy_outlined, color: isUser ? Colors.grey[700] : Colors.white, size: 20, ), ); } /// 构建消息内容 Widget _buildMessageContent(BuildContext context) { // 如果是识别中的消息,添加特殊样式 if (isRecognizing) { return Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( message, style: const TextStyle( color: Colors.white, fontStyle: FontStyle.italic, fontSize: 14, height: 1.4, ), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), _buildTypingIndicator(), ], ); } // 判断消息内容,显示特殊卡片 if (!isUser) { if (message.contains('播放列表') || message.contains('音乐')) { return _buildMusicContent(context); } else if (message.contains('景点') || message.contains('旅游')) { return _buildTravelContent(context); } else if (message.contains('天气') || message.contains('气温') || message.contains('预报')) { return _buildWeatherContent(context); } } return Text( message, style: TextStyle( color: isUser ? Colors.white : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ); } /// 构建输入中的指示器 Widget _buildTypingIndicator() { return SizedBox( width: 40, height: 20, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildDot(100), _buildDot(300), _buildDot(500), ], ), ); } /// 构建动画点 Widget _buildDot(int milliseconds) { return _AnimatedDot(delayMilliseconds: milliseconds); } /// 构建音乐内容卡片 Widget _buildMusicContent(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '以下是一些推荐的音乐:', style: TextStyle( color: Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.03), blurRadius: 3, spreadRadius: 1, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.music_note, size: 16, color: Theme.of(context).primaryColor.withOpacity(0.7), ), const SizedBox(width: 6), Text( '雨天时光 · 播放列表', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), ], ), const SizedBox(height: 10), // 歌曲示例 _buildMusicItem('雨声与钢琴曲', '纯音乐', context), Divider(height: 1, thickness: 1, color: Colors.grey[100]), _buildMusicItem('Rainy Day', 'Coldplay', context), Divider(height: 1, thickness: 1, color: Colors.grey[100]), _buildMusicItem('雨天的爵士咖啡馆', '爵士合集', context), const SizedBox(height: 8), Center( child: TextButton( onPressed: () {}, style: TextButton.styleFrom( minimumSize: Size.zero, padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( '查看更多推荐', style: TextStyle( fontSize: 12, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w500, ), ), ), ), ], ), ), const SizedBox(height: 10), Row( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: OutlinedButton.icon( onPressed: () {}, icon: Icon(Icons.play_arrow, size: 16), label: Text('播放全部'), style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).primaryColor, side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: TextStyle(fontSize: 12), ), ), ), SizedBox(width: 8), Expanded( child: OutlinedButton.icon( onPressed: () {}, icon: Icon(Icons.shuffle, size: 16), label: Text('随机播放'), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey[700], side: BorderSide(color: Colors.grey[400]!), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: TextStyle(fontSize: 12), ), ), ), ], ), ], ); } /// 构建旅游内容卡片 Widget _buildTravelContent(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '以下是一些推荐的景点:', style: TextStyle( color: Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.03), blurRadius: 3, spreadRadius: 1, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.place, size: 16, color: Color(0xFFE11D48), ), const SizedBox(width: 6), Text( '热门景点 · 东京', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.grey[700], ), ), ], ), const SizedBox(height: 10), // 景点列表 _buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context), Divider(height: 1, thickness: 1, color: Colors.grey[100]), _buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context), Divider(height: 1, thickness: 1, color: Colors.grey[100]), _buildLocationItem('浅草寺', '东京最古老的寺庙', context), const SizedBox(height: 8), Center( child: TextButton( onPressed: () {}, style: TextButton.styleFrom( minimumSize: Size.zero, padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( '查看更多景点', style: TextStyle( fontSize: 12, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w500, ), ), ), ), ], ), ), const SizedBox(height: 10), Row( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: OutlinedButton.icon( onPressed: () {}, icon: Icon(Icons.map, size: 16), label: Text('查看地图'), style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).primaryColor, side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: TextStyle(fontSize: 12), ), ), ), SizedBox(width: 8), Expanded( child: OutlinedButton.icon( onPressed: () {}, icon: Icon(Icons.bookmark_border, size: 16), label: Text('保存到收藏'), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey[700], side: BorderSide(color: Colors.grey[400]!), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: TextStyle(fontSize: 12), ), ), ), ], ), ], ); } /// 构建音乐项目 Widget _buildMusicItem(String title, String artist, BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.music_note, size: 18, color: Theme.of(context).primaryColor, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey[800], ), overflow: TextOverflow.ellipsis, ), SizedBox(height: 2), Text( artist, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), ], ), ), IconButton( onPressed: () {}, icon: Icon(Icons.play_circle, color: Theme.of(context).primaryColor), padding: EdgeInsets.zero, constraints: BoxConstraints(), iconSize: 24, ), ], ), ); } /// 构建地点项目 Widget _buildLocationItem(String name, String description, BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.place, size: 18, color: Color(0xFFE11D48), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey[800], ), overflow: TextOverflow.ellipsis, ), SizedBox(height: 2), Text( description, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), ], ), ), IconButton( onPressed: () {}, icon: Icon(Icons.arrow_forward, color: Colors.grey[400]), padding: EdgeInsets.zero, constraints: BoxConstraints(), iconSize: 20, ), ], ), ); } /// 构建天气内容卡片 Widget _buildWeatherContent(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '当前天气情况:', style: TextStyle( color: Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '北京', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), SizedBox(height: 4), Text( '晴朗', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), SizedBox(height: 12), Row( children: [ Text( '25°', style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: Colors.white, ), ), SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '最高: 29°', style: TextStyle( fontSize: 12, color: Colors.white.withOpacity(0.9), ), ), Text( '最低: 18°', style: TextStyle( fontSize: 12, color: Colors.white.withOpacity(0.9), ), ), ], ), ], ), ], ), Icon( Icons.wb_sunny, size: 48, color: Colors.white, ), ], ), SizedBox(height: 16), Container( height: 1, color: Colors.white.withOpacity(0.2), ), SizedBox(height: 16), Text( '未来预报', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildForecastDay('周一', Icons.wb_sunny, '28°'), _buildForecastDay('周二', Icons.cloud, '25°'), _buildForecastDay('周三', Icons.wb_cloudy, '26°'), _buildForecastDay('周四', Icons.grain, '24°'), _buildForecastDay('周五', Icons.wb_sunny, '27°'), ], ), SizedBox(height: 16), Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( Icons.info_outline, size: 16, color: Colors.white, ), SizedBox(width: 6), Flexible( child: Text( '今日紫外线指数较高,外出请做好防晒', style: TextStyle( fontSize: 12, color: Colors.white, ), ), ), ], ), ), ], ), ), const SizedBox(height: 10), Row( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: OutlinedButton.icon( onPressed: () {}, icon: Icon(Icons.location_on, size: 16), label: Text('更多地区'), style: OutlinedButton.styleFrom( foregroundColor: Theme.of(context).primaryColor, side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: TextStyle(fontSize: 12), ), ), ), SizedBox(width: 8), Expanded( child: OutlinedButton.icon( onPressed: () {}, icon: Icon(Icons.calendar_today, size: 16), label: Text('15天预报'), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey[700], side: BorderSide(color: Colors.grey[400]!), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: TextStyle(fontSize: 12), ), ), ), ], ), ], ); } /// 构建单日天气预报 Widget _buildForecastDay(String day, IconData icon, String temp) { return Column( children: [ Text( day, style: TextStyle( fontSize: 12, color: Colors.white.withOpacity(0.9), ), ), SizedBox(height: 4), Icon( icon, size: 20, color: Colors.white, ), SizedBox(height: 4), Text( temp, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white, ), ), ], ); } } /// 动画点组件 class _AnimatedDot extends StatefulWidget { final int delayMilliseconds; const _AnimatedDot({required this.delayMilliseconds}); @override _AnimatedDotState createState() => _AnimatedDotState(); } class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), ); _animation = Tween(begin: 0, end: 6).animate( CurvedAnimation( parent: _controller, curve: Interval(0.0, 0.5, curve: Curves.easeOut), reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn), ), )..addListener(() { setState(() {}); }); Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () { if (mounted) { _controller.repeat(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( width: 6, height: 6, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(3), boxShadow: [ BoxShadow( color: Colors.white.withOpacity(0.5), blurRadius: _animation.value, spreadRadius: _animation.value / 2, ), ], ), ); } }