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.

956 lines
30 KiB

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<AgentController> {
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<String, dynamic>.from(e)).toList();
// final playlist =
// mate!['card_musiclist']['musics'] as List<Map<String, String>>;
//音乐播放列表
return _buildMusicContent(context, playlist);
} else if (mate!.keys.contains('card_weather')) {
//天气
return _buildWeatherContent(context);
}
// 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<Map<String, dynamic>> 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<AgentController>(); // 获取控制器
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) {
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<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,
),
],
),
);
}
}