Browse Source

refactor(agent): 优化消息气泡布局和样式

- 调整消息气泡的内边距和圆角样式,提升视觉一致性
- 移除冗余的播放列表容器和按钮,简化UI结构
- 修复变量命名错误,将`mate`改为`meta`
weicu
lxm 1 year ago
parent
commit
4ed2d05d73
  1. 6
      .gitignore
  2. 52
      lib/modules/agent/views/agent_view.dart
  3. 421
      lib/modules/agent/views/message_bubble.dart
  4. 3
      lib/modules/home/controllers/home_controller.dart

6
.gitignore

@ -136,3 +136,9 @@ coverage/
*.js_ *.js_
*.js.deps *.js.deps
*.js.map *.js.map
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.h
linux/flutter/generated_plugins.cmake
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake

52
lib/modules/agent/views/agent_view.dart

@ -149,28 +149,32 @@ class AgentView extends StatelessWidget {
// 聊天内容 // 聊天内容
Expanded( Expanded(
child: Obx( child: Obx(
() => ListView.builder( () => Align(
controller: controller.scrollController, alignment: Alignment.topCenter,
reverse: true, child: ListView.builder(
padding: controller: controller.scrollController,
const EdgeInsets.symmetric(vertical: 16, horizontal: 16), shrinkWrap: true,
itemCount: controller.messages.length, reverse: true,
itemBuilder: (context, index) { padding:
final message = controller const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
.messages[controller.messages.length - 1 - index]; itemCount: controller.messages.length,
return Padding( itemBuilder: (context, index) {
padding: const EdgeInsets.only(bottom: 8), final message = controller
child: MessageBubble( .messages[controller.messages.length - 1 - index];
isUser: message.isUser, return Padding(
message: message.text, padding: const EdgeInsets.only(bottom: 8),
timestamp: message.timestamp, child: MessageBubble(
isRecognizing: message.isRecognizing, isUser: message.isUser,
hasImage: message.hasImage, message: message.text,
imagePath: message.imagePath, timestamp: message.timestamp,
mate: message.meta, isRecognizing: message.isRecognizing,
), hasImage: message.hasImage,
); imagePath: message.imagePath,
}, meta: message.meta,
),
);
},
),
), ),
), ),
), ),
@ -308,7 +312,7 @@ class AgentView extends StatelessWidget {
if (isTextMode) { if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化 // 文本输入模式 - 注意观察图片模式时的高度变化
return Container( return Container(
height: 44, //height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
@ -378,7 +382,7 @@ class AgentView extends StatelessWidget {
} }
}, },
child: Container( child: Container(
height: 44, height: 50,
padding: padding:
const EdgeInsets.symmetric(horizontal: 16), const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(

421
lib/modules/agent/views/message_bubble.dart

@ -15,7 +15,7 @@ class MessageBubble extends GetView<AgentController> {
final bool isRecognizing; final bool isRecognizing;
final bool hasImage; final bool hasImage;
final String? imagePath; final String? imagePath;
final Map? mate; final Map? meta;
const MessageBubble({ const MessageBubble({
Key? key, Key? key,
@ -25,7 +25,7 @@ class MessageBubble extends GetView<AgentController> {
this.isRecognizing = false, this.isRecognizing = false,
this.hasImage = false, this.hasImage = false,
this.imagePath, this.imagePath,
this.mate, this.meta,
}) : super(key: key); }) : super(key: key);
@override @override
@ -39,8 +39,8 @@ class MessageBubble extends GetView<AgentController> {
isUser ? MainAxisAlignment.end : MainAxisAlignment.start, isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!isUser) _buildAvatar(isUser, context), //if (!isUser) _buildAvatar(isUser, context),
const SizedBox(width: 8), //const SizedBox(width: 8),
Flexible( Flexible(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
@ -50,15 +50,23 @@ class MessageBubble extends GetView<AgentController> {
padding: hasImage padding: hasImage
? (message == '[图片]' ? (message == '[图片]'
? EdgeInsets.zero ? EdgeInsets.zero
: const EdgeInsets.all(12)) : isUser
: const EdgeInsets.all(12), ? 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( decoration: BoxDecoration(
color: _getBubbleColor(context), color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith( borderRadius: BorderRadius.circular(18).copyWith(
bottomLeft: isUser topLeft: isUser
? const Radius.circular(18) ? const Radius.circular(18)
: const Radius.circular(4), : const Radius.circular(4),
bottomRight: !isUser topRight: !isUser
? const Radius.circular(18) ? const Radius.circular(18)
: const Radius.circular(4), : const Radius.circular(4),
), ),
@ -86,8 +94,8 @@ class MessageBubble extends GetView<AgentController> {
], ],
), ),
), ),
const SizedBox(width: 8), //const SizedBox(width: 8),
if (isUser) _buildAvatar(isUser, context), //if (isUser) _buildAvatar(isUser, context),
], ],
), ),
); );
@ -231,28 +239,29 @@ class MessageBubble extends GetView<AgentController> {
} }
// 判断消息内容,显示特殊卡片 // 判断消息内容,显示特殊卡片
if (!isUser && mate != null) { if (!isUser && meta != null) {
if (mate!.keys.contains('card_music')) { // 判断消息类型,显示不同的卡片
final musicId = mate!['card_music']['id'].toString(); if (meta!.keys.contains('card_music')) {
final musicName = mate!['card_music']['name'] as String; final musicId = meta!['card_music']['id'].toString();
final musicUrl = mate!['card_music']['url'] as String; final musicName = meta!['card_music']['name'] as String;
final musicSinger = mate!['card_music']['sgener'] as String; final musicUrl = meta!['card_music']['url'] as String;
final musicImage = mate!['card_music']['image'] as String; final musicSinger = meta!['card_music']['sgener'] as String;
final musicImage = meta!['card_music']['image'] as String;
//音乐卡片 //音乐卡片
return _buildMusicItem( return _buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage); context, musicId, musicName, musicUrl, musicSinger, musicImage);
} else if (mate!.keys.contains('card_musiclist')) { } else if (meta!.keys.contains('card_musiclist')) {
final rawList = mate!['card_musiclist']['musics'] as List; final rawList = meta!['card_musiclist']['musics'] as List;
final playlist = final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表 //音乐播放列表
return _buildMusicContent(context, playlist); return _buildMusicContent(context, playlist);
} else if (mate!.keys.contains('card_weather')) { } else if (meta!.keys.contains('card_weather')) {
final city = mate!['card_weather']['city'] as String; final city = meta!['card_weather']['city'] as String;
final rawList = mate!['card_weather']['dayweather'] as List; final rawList = meta!['card_weather']['dayweather'] as List;
final daylist = final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final now = mate!['card_weather']['day'] as Map<String, dynamic>; final now = meta!['card_weather']['day'] as Map<String, dynamic>;
//天气 //天气
return _buildWeatherContent( return _buildWeatherContent(
context, context,
@ -321,122 +330,123 @@ class MessageBubble extends GetView<AgentController> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Container( // Container(
padding: const EdgeInsets.all(12), // padding: const EdgeInsets.all(12),
decoration: BoxDecoration( // decoration: BoxDecoration(
color: Colors.white, // color: Colors.white,
borderRadius: BorderRadius.circular(12), // borderRadius: BorderRadius.circular(12),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.black.withOpacity(0.03), // color: Colors.black.withOpacity(0.03),
blurRadius: 3, // blurRadius: 3,
spreadRadius: 1, // spreadRadius: 1,
offset: const Offset(0, 1), // offset: const Offset(0, 1),
), // ),
], // ],
), // ),
child: Column( // child:
crossAxisAlignment: CrossAxisAlignment.start, Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
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: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 动态生成音乐条目
...playlist.asMap().entries.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: 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: [ children: [
Expanded( Row(
child: OutlinedButton.icon( children: [
onPressed: () {}, Icon(
icon: Icon(Icons.play_arrow, size: 16), Icons.music_note,
label: Text('播放全部'), size: 16,
style: OutlinedButton.styleFrom( color: Theme.of(context).primaryColor.withOpacity(0.7),
foregroundColor: Theme.of(context).primaryColor, ),
side: BorderSide( const SizedBox(width: 6),
color: Theme.of(context).primaryColor.withOpacity(0.5)), Text(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), '播放列表'.tr,
shape: RoundedRectangleBorder( style: TextStyle(
borderRadius: BorderRadius.circular(16), fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
), ),
textStyle: TextStyle(fontSize: 12),
), ),
), ],
), ),
SizedBox(width: 8), const SizedBox(height: 10),
Expanded(
child: OutlinedButton.icon( // 动态生成音乐条目
...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: Colors.grey[100]),
],
);
}).toList(),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {}, onPressed: () {},
icon: Icon(Icons.shuffle, size: 16), style: TextButton.styleFrom(
label: Text('随机播放'), minimumSize: Size.zero,
style: OutlinedButton.styleFrom( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
foregroundColor: Colors.grey[700], tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: Colors.grey[400]!), ),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Text(
shape: RoundedRectangleBorder( '查看更多推荐',
borderRadius: BorderRadius.circular(16), style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
), ),
textStyle: TextStyle(fontSize: 12),
), ),
), ),
), ),
], ],
), ),
//),
//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),
// ),
// ),
// ),
// ],
// ),
], ],
); );
} }
@ -578,79 +588,102 @@ class MessageBubble extends GetView<AgentController> {
'coverUrl': image, 'coverUrl': image,
}; };
controller.musicStatus(); //刷新状态 controller.musicStatus(); //刷新状态
return Padding( return Container(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.all(12),
child: Row( decoration: BoxDecoration(
children: [ color: Colors.white,
Container( borderRadius: BorderRadius.circular(12),
width: 38, boxShadow: [
height: 38, BoxShadow(
decoration: BoxDecoration( color: Colors.black.withOpacity(0.03),
color: Colors.grey[100], blurRadius: 3,
borderRadius: BorderRadius.circular(8), spreadRadius: 1,
), offset: const Offset(0, 1),
child: Icon(
Icons.music_note,
size: 18,
color: Theme.of(context).primaryColor,
),
), ),
const SizedBox(width: 12), ],
Expanded( ),
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.symmetric(vertical: 8),
children: [ child: Row(
Text( children: [
name, Container(
style: TextStyle( width: 50,
fontSize: 13, height: 50,
fontWeight: FontWeight.w500, decoration: BoxDecoration(
color: Colors.grey[800], 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,
), ),
overflow: TextOverflow.ellipsis, SizedBox(height: 2),
), Text(
SizedBox(height: 2), singer,
Text( style: TextStyle(
singer, fontSize: 12,
style: TextStyle( color: Colors.grey[600],
fontSize: 12, ),
color: Colors.grey[600], overflow: TextOverflow.ellipsis,
), ),
overflow: TextOverflow.ellipsis, ],
), ),
],
), ),
), Obx(() {
Obx(() { final isThisPlaying = controller.isPlaying.value &&
final isThisPlaying = controller.isPlaying.value && controller.currentUrl.value == url;
controller.currentUrl.value == url;
return IconButton( return IconButton(
onPressed: () { onPressed: () {
if (controller.isPlaying.value) { if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) { if (controller.currentUrl.value == url) {
controller.musicPause(); controller.musicPause();
} else { } else {
controller.musicPlay(song); controller.musicPlay(song);
} }
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
} else { } else {
controller.musicPlay(song); if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(song);
}
} }
} },
}, icon: Icon(
icon: Icon( isThisPlaying ? Icons.pause_circle : Icons.play_circle,
isThisPlaying ? Icons.pause_circle : Icons.play_circle, color: Theme.of(context).primaryColor,
color: Theme.of(context).primaryColor, ),
), padding: EdgeInsets.zero,
padding: EdgeInsets.zero, constraints: BoxConstraints(),
constraints: BoxConstraints(), iconSize: 36,
iconSize: 24, );
); }),
}), ],
], ),
), ),
); );
} }

3
lib/modules/home/controllers/home_controller.dart

@ -103,6 +103,9 @@ class HomeController extends GetxController {
// 设置监听器监听蓝牙服务状态变化 // 设置监听器监听蓝牙服务状态变化
_setupStateListeners(); _setupStateListeners();
// 在初始化时自动触发一次刷新设备连接状态
refreshDeviceConnection();
} }
/// 设置状态监听器 /// 设置状态监听器

Loading…
Cancel
Save