import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:get_storage/get_storage.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? meta; const MessageBubble({ Key? key, required this.isUser, required this.message, required this.timestamp, this.isRecognizing = false, this.hasImage = false, this.imagePath, this.meta, }) : 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 : isUser ? const EdgeInsets.fromLTRB( 12, 12, 16, 12) // 用户消息右侧多一点内边距 : const EdgeInsets.fromLTRB( 16, 12, 12, 12)) // AI消息左侧多一点内边距 : isUser ? const EdgeInsets.fromLTRB( 12, 12, 16, 12) // 用户消息右侧多一点内边距 : const EdgeInsets.fromLTRB( 16, 12, 12, 12), // AI消息左侧多一点内边距 decoration: BoxDecoration( color: _getBubbleColor(context), borderRadius: BorderRadius.circular(18).copyWith( topLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), topRight: !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: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[500], fontWeight: FontWeight.w400, ), ), ], ), ), //const SizedBox(width: 8), //if (isUser) _buildAvatar(isUser, context), ], ), ); } /// 获取气泡颜色 Color _getBubbleColor(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; if (isUser) { return isRecognizing ? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色 : Color(0xFF3B82F6); // 使用更明亮的蓝色 } else { return isDarkMode ? Color(0xFF2A2A2A) : Color(0xFFF1F5F9); // 根据主题调整AI消息背景色 } } /// 构建头像 Widget _buildAvatar(bool isUser, BuildContext context) { if (isUser) { final storage = GetStorage(); final localAvatarPath = storage.read('local_avatar') ?? ''; return Container( width: 36, height: 36, decoration: BoxDecoration( color: localAvatarPath.isEmpty ? Colors.grey[300] : null, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: localAvatarPath.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(18), child: Image.file( File(localAvatarPath), fit: BoxFit.cover, ), ) : Icon( Icons.person, color: Colors.grey[700], size: 20, ), ); } else { // AI头像保持不变 return Container( width: 36, height: 36, decoration: BoxDecoration( color: 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( Icons.smart_toy_outlined, color: 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 && meta != null) { // 判断消息类型,显示不同的卡片 if (meta!.keys.contains('card_music')) { final musicId = meta!['card_music']['id'].toString(); final musicName = meta!['card_music']['name'] as String; final musicUrl = meta!['card_music']['url'] as String; final musicSinger = meta!['card_music']['sgener'] as String; final musicImage = meta!['card_music']['image'] as String; //音乐卡片 return _buildMusicItem( context, musicId, musicName, musicUrl, musicSinger, musicImage); } else if (meta!.keys.contains('card_musiclist')) { final rawList = meta!['card_musiclist']['musics'] as List; final playlist = rawList.map((e) => Map.from(e)).toList(); //音乐播放列表 return _buildMusicContent(context, playlist); } else if (meta!.keys.contains('card_weather')) { final city = meta!['card_weather']['city'] as String; final rawList = meta!['card_weather']['dayweather'] as List; final daylist = rawList.map((e) => Map.from(e)).toList(); final now = meta!['card_weather']['day'] as Map; //天气 return _buildWeatherContent( context, city, now, daylist, ); } else if (meta!.keys.contains('card_travel')) { final rawList = meta!['card_travel']['items'] as List; final travellist = rawList.map((e) => Map.from(e)).toList(); // 旅游 return _buildTravelContent(context, travellist); } // 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 : (Theme.of(context).brightness == Brightness.dark ? Colors.white.withOpacity(0.9) : 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) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '以下是一些推荐的音乐:', style: TextStyle( color: isDarkMode ? Colors.white.withOpacity(0.9) : 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( '播放列表'.tr, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[300] : Colors.grey[700], ), ), ], ), const SizedBox(height: 10), // 动态生成音乐条目 ...playlist.asMap().entries.take(4).map((entry) { final index = entry.key; final item = entry.value; final id = item["id"]?.toString() ?? ""; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; final image = item["image"] ?? ""; return Column( children: [ _buildMusicItem(context, id, name, url, singer, image), if (index != playlist.length - 1) Divider( height: 1, thickness: 1, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[800] : 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, List> travellist) { 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.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), // 动态生成音乐条目 ...travellist.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final name = item["name"] ?? "未知歌曲"; //名字 final addr = item["addr"] ?? ""; //地址 final tag = item["tag"] ?? "未知歌手"; //标签 final rating = item["rating"] ?? ""; //评分 final distance = item["distance"] ?? ""; //距离 final images = item["images"] ?? []; // 图片列表 return Column( children: [ _buildLocationItem( context, name, addr, ), if (index != travellist.length - 1) Divider(height: 1, thickness: 1, color: Colors.grey[100]), ], ); }).toList(), // // 景点列表 // _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 id, String name, String url, String singer, String image) { final controller = Get.find(); // 获取控制器 final isDarkMode = Theme.of(context).brightness == Brightness.dark; Map song = { 'id': id, 'url': url, 'title': name, 'artist': singer, 'coverUrl': image, }; controller.musicStatus(); //刷新状态 return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDarkMode ? Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.03), blurRadius: 3, spreadRadius: 1, offset: const Offset(0, 1), ), ], ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), image: image.isNotEmpty ? DecorationImage( image: NetworkImage(image), fit: BoxFit.cover, ) : null, ), child: image.isEmpty ? Icon( Icons.music_note, size: 18, color: Theme.of(context).primaryColor, ) : null, ), 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(song); } } else { if (controller.currentUrl.value == url) { controller.musicResume(); } else { controller.musicPlay(song); } } }, icon: Icon( isThisPlaying ? Icons.pause_circle : Icons.play_circle, color: Theme.of(context).primaryColor, ), padding: EdgeInsets.zero, constraints: BoxConstraints(), iconSize: 36, ); }), ], ), ), ); } /// 构建地点项目 Widget _buildLocationItem( BuildContext context, String name, String description) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: isDarkMode ? Color(0xFF1A365D) : 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: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.grey[800], ), overflow: TextOverflow.ellipsis, ), SizedBox(height: 2), Text( description, style: TextStyle( fontSize: 12, color: isDarkMode ? Colors.grey[400] : 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) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '查询天气情况:', style: TextStyle( color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: isDarkMode ? LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF1E40AF), Color(0xFF1E3A8A)], ) : LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDarkMode ? 0.3 : 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} ${now["date"]}", style: const 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: const 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), ), ), ], ), ], ), ], ), const Icon( Icons.wb_sunny, size: 48, color: Colors.white, ), ], ), SizedBox(height: 16), Container( height: 1, color: Colors.white.withOpacity(isDarkMode ? 0.15 : 0.2), ), SizedBox(height: 16), const 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); IconData weatherIcon = getWeatherIcon(days[index]['weatherCondition']); String weekday = DateUtil.getWeekday(date, languageCode: "zh", short: true); String temp = "${days[index]['temperature']}°"; return _buildForecastDay( weekday, weatherIcon, temp, ); }, ), ), SizedBox(height: 16), Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.15) : 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, ), ), ], ); } /// 获取天气图标 IconData getWeatherIcon(String description) { description = description.trim(); if (["晴", "少云", "晴间多云"].contains(description)) { return Icons.wb_sunny; } if (["多云", "阴"].contains(description)) { return Icons.cloud; } if (description.contains("雾") || ["雾", "浓雾", "强浓雾", "轻雾", "大雾", "特强浓雾", "霾", "中度霾", "重度霾", "严重霾"] .contains(description)) { return Icons.blur_on; } if (description.contains("风") || [ "有风", "平静", "微风", "和风", "清风", "强风/劲风", "疾风", "大风", "烈风", "风暴", "狂爆风", "飓风", "热带风暴" ].contains(description)) { return Icons.air; } if (description.contains("雨") || [ "阵雨", "雷阵雨", "雷阵雨并伴有冰雹", "小雨", "中雨", "大雨", "暴雨", "大暴雨", "特大暴雨", "强阵雨", "强雷阵雨", "极端降雨", "毛毛雨/细雨", "雨", "小雨-中雨", "中雨-大雨", "大雨-暴雨", "暴雨-大暴雨", "大暴雨-特大暴雨" ].contains(description)) { return Icons.grain; // 或使用 community icon 包:weather_icons 中的 rain } if (description.contains("雪") || [ "雨雪天气", "雨夹雪", "阵雨夹雪", "冻雨", "雪", "阵雪", "小雪", "中雪", "大雪", "暴雪", "小雪-中雪", "中雪-大雪", "大雪-暴雪" ].contains(description)) { return Icons.ac_unit; } if (["浮尘", "扬沙", "沙尘暴", "强沙尘暴"].contains(description)) { return Icons.filter_drama; } if (["龙卷风"].contains(description)) { return Icons.waves; } if (["热"].contains(description)) { return Icons.local_fire_department; } if (["冷"].contains(description)) { return Icons.ac_unit; } // 默认或未知天气 return Icons.help_outline; } } /// 动画点组件 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, ), ], ), ); } }