You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1241 lines
40 KiB

import 'package:common_utils/common_utils.dart';
import 'package:deep_voice/modules/agent/views/message_bubble_navigation.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<AgentController> {
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<String>('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<String, dynamic>.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<String, dynamic>.from(e)).toList();
final now = meta!['card_weather']['day'] as Map<String, dynamic>;
//天气
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<String, dynamic>.from(e)).toList();
// 旅游
return _buildTravelContent(context, travellist);
} else if (meta!.keys.contains('card_navigation')) {
final rawList = meta!['card_navigation']['items'] as List;
final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final rawNavigation = meta!['card_navigation']['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null;
print("导航实时数据 ${rawNavigation}");
// 导航
return navigationCard(context, modes: list, navigation: navigation);
}
// 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<AgentController>(); // 获取控制器
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Map<String, String> 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<String, dynamic> now, List<Map<String, dynamic>> 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<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = Tween<double>(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,
),
],
),
);
}
}