import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'dart:io'; import '../controllers/agent_controller.dart'; /// 消息气泡组件 class MessageBubble extends GetView { final bool isUser; final String message; final DateTime timestamp; final bool isRecognizing; final bool hasImage; final String? imagePath; final Map? mate; const MessageBubble({ Key? key, required this.isUser, required this.message, required this.timestamp, this.isRecognizing = false, this.hasImage = false, this.imagePath, this.mate, }) : 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: hasImage ? (message == '[图片]' ? EdgeInsets.zero : const EdgeInsets.all(12)) : 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 (hasImage && imagePath != null) { // 如果消息是默认的[图片]标记,只显示图片 if (message == '[图片]') { return ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.file( File(imagePath!), width: 200, height: 200, fit: BoxFit.cover, ), ); } // 如果消息是自定义文本,显示文本+图片 return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message, style: TextStyle( color: isUser ? Colors.white : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(imagePath!), width: 200, height: 200, fit: BoxFit.cover, ), ), ], ); } // 判断消息内容,显示特殊卡片 if (!isUser && mate != null) { if (mate!.keys.contains('card_music')) { final musicName = mate!['card_music']['name'] as String; final musicUrl = mate!['card_music']['url'] as String; final musicSinger = mate!['card_music']['sgener'] as String; //音乐卡片 return _buildMusicItem(context, musicName, musicUrl, musicSinger); } else if (mate!.keys.contains('card_musiclist')) { final rawList = mate!['card_musiclist']['musics'] as List; final playlist = rawList.map((e) => Map.from(e)).toList(); // final playlist = // mate!['card_musiclist']['musics'] as List>; //音乐播放列表 return _buildMusicContent(context, playlist); } else if (mate!.keys.contains('card_weather')) { final city = mate!['card_weather']['city'] as String; final rawList = mate!['card_weather']['dayweather'] as List; final daylist = rawList.map((e) => Map.from(e)).toList(); final now = daylist[0]; //天气 return _buildWeatherContent( context, city, now, daylist, ); } // 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, List> playlist) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const 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), // 动态生成音乐条目 ...playlist.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; return Column( children: [ _buildMusicItem(context, name, url, singer), if (index != playlist.length - 1) Divider(height: 1, thickness: 1, color: Colors.grey[100]), ], ); }).toList(), 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( BuildContext context, String name, String url, String singer) { final controller = Get.find(); // 获取控制器 controller.musicStatus(); //刷新状态 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( name, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.grey[800], ), overflow: TextOverflow.ellipsis, ), SizedBox(height: 2), Text( singer, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), ], ), ), Obx(() { final isThisPlaying = controller.isPlaying.value && controller.currentUrl.value == url; return IconButton( onPressed: () { if (controller.isPlaying.value) { if (controller.currentUrl.value == url) { controller.musicPause(); } else { controller.musicPlay(url); } } else { if (controller.currentUrl.value == url) { controller.musicResume(); } else { controller.musicPlay(url); } } }, icon: Icon( isThisPlaying ? Icons.pause_circle : 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, String city, Map now, List> days) { 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( city, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), SizedBox(height: 4), Text( now["weatherCondition"], style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), SizedBox(height: 12), Row( children: [ Text( "${now["temperature"]}°", style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: Colors.white, ), ), SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '最高: ${now["maxTemperature"]}°', style: TextStyle( fontSize: 12, color: Colors.white.withOpacity(0.9), ), ), Text( '最低: ${now["minTemperature"]}°', 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°'), // ], // ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate( days.length, (index) { String dateStr = days[index]['date']; DateTime? date = DateUtil.getDateTime(dateStr); String weekday = DateUtil.getWeekday(date); return _buildForecastDay( weekday, Icons.wb_sunny, days[index]['temperature'], ); }, ), ), SizedBox(height: 16), Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: const 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, ), ], ), ); } }