Browse Source

refactor(agent): 重构旅游卡片组件并优化代码格式

将旅游卡片相关逻辑从 `message_bubble.dart` 中提取到独立的 `travel_card.dart` 文件中,提高代码可维护性。同时优化了代码格式,包括缩进、空格和换行,提升代码可读性。
weicu
lxm 1 year ago
parent
commit
3fada1193c
  1. 38
      lib/modules/agent/controllers/agent_controller.dart
  2. 230
      lib/modules/agent/views/message_bubble.dart
  3. 211
      lib/modules/agent/views/travel_card.dart

38
lib/modules/agent/controllers/agent_controller.dart

@ -180,11 +180,11 @@ class AgentController extends GetxController {
void _addWelcomeMessage() { void _addWelcomeMessage() {
messages.add(Message( messages.add(Message(
isUser: false, isUser: false,
text: '你好!我是Emma,我能帮你:\n\n' text: '你好!我是**Emma**,我能帮你:\n\n'
'• 回答问题和提供信息\n' '• 答问题和提供信息\n\n'
'• 实时语音翻译\n' '• 实时语音翻译\n\n'
'• 推荐音乐和播客\n' '• 推荐音乐和播客\n\n'
'• 语音备忘录和提醒\n' '• 语音备忘录和提醒\n\n'
'• 控制耳机功能\n\n' '• 控制耳机功能\n\n'
'有什么我可以帮你的吗?', '有什么我可以帮你的吗?',
)); ));
@ -395,12 +395,12 @@ class AgentController extends GetxController {
Logger.e(TAG, '解析meta数据失败: $e'); Logger.e(TAG, '解析meta数据失败: $e');
} }
} }
if(metaResult.isNotEmpty){ if (metaResult.isNotEmpty) {
// 判断是否为新的回复或响应ID是否改变 // 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) { if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息 // 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult)); messages
.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false; _isNewAssistantResponse = false;
_currentResponseId = responseId; _currentResponseId = responseId;
} else { } else {
@ -535,24 +535,22 @@ class AgentController extends GetxController {
try { try {
// 创建待请求权限列表 // 创建待请求权限列表
List<Permission> permissionsToRequest = []; List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求 // Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone); permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
// iOS 只需要麦克风权限 // iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone); permissionsToRequest.add(Permission.microphone);
} }
// 批量请求所有需要的权限 // 批量请求所有需要的权限
final statuses = await permissionsToRequest.request(); final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予 // 检查是否所有权限都已授予
bool allGranted = true; bool allGranted = true;
Permission? deniedPermission; Permission? deniedPermission;
statuses.forEach((permission, status) { statuses.forEach((permission, status) {
if (!status.isGranted) { if (!status.isGranted) {
allGranted = false; allGranted = false;
@ -560,33 +558,33 @@ class AgentController extends GetxController {
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status'); Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
} }
}); });
// 单独检查麦克风权限状态,因为这是主要需求 // 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status; final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true // 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) { if (allGranted || micStatus.isGranted) {
return true; return true;
} }
// 处理特定权限被永久拒绝的情况 // 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) { if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true); _showPermissionDialog(true);
return false; return false;
} }
// 普通拒绝情况 // 普通拒绝情况
if (deniedPermission != null) { if (deniedPermission != null) {
_showPermissionDialog(false); _showPermissionDialog(false);
} }
return false; return false;
} catch (e) { } catch (e) {
Logger.e(TAG, '检查麦克风权限失败: $e'); Logger.e(TAG, '检查麦克风权限失败: $e');
return false; return false;
} }
} }
// 获取Android SDK版本 // 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async { Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {

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

@ -2,6 +2,7 @@ import 'package:common_utils/common_utils.dart';
import 'package:deep_voice/modules/agent/views/message_bubble_navigation.dart'; import 'package:deep_voice/modules/agent/views/message_bubble_navigation.dart';
import 'weather_card.dart'; import 'weather_card.dart';
import 'music_card.dart'; import 'music_card.dart';
import 'travel_card.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -210,13 +211,16 @@ class MessageBubble extends GetView<AgentController> {
if (hasImage && imagePath != null) { if (hasImage && imagePath != null) {
// 如果消息是默认的[图片]标记,只显示图片 // 如果消息是默认的[图片]标记,只显示图片
if (message == '[图片]') { if (message == '[图片]') {
return ClipRRect( return Container(
borderRadius: BorderRadius.circular(16), padding: const EdgeInsets.all(8),
child: Image.file( child: ClipRRect(
File(imagePath!), borderRadius: BorderRadius.circular(12),
width: 200, child: Image.file(
height: 200, File(imagePath!),
fit: BoxFit.cover, width: 200,
height: 200,
fit: BoxFit.cover,
),
), ),
); );
} }
@ -284,7 +288,7 @@ class MessageBubble extends GetView<AgentController> {
final travellist = final travellist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// 旅游 // 旅游
return _buildTravelContent(context, travellist); return buildTravelContent(context, travellist);
} else if (meta!.keys.contains('card_navigation')) { } else if (meta!.keys.contains('card_navigation')) {
final rawList = meta!['card_navigation']['items'] as List; final rawList = meta!['card_navigation']['items'] as List;
final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList(); final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList();
@ -413,216 +417,6 @@ class MessageBubble extends GetView<AgentController> {
Widget _buildDot(int milliseconds) { Widget _buildDot(int milliseconds) {
return _AnimatedDot(delayMilliseconds: milliseconds); return _AnimatedDot(delayMilliseconds: milliseconds);
} }
/// 构建旅游内容卡片
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 _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,
),
],
),
);
}
} }
/// 动画点组件 /// 动画点组件

211
lib/modules/agent/views/travel_card.dart

@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
/// 构建旅游内容卡片
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 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,
),
],
),
);
}
Loading…
Cancel
Save