9 changed files with 569 additions and 33 deletions
@ -0,0 +1,447 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/agent_controller.dart'; |
|||
|
|||
String formatDistance(String distanceStr) { |
|||
final distance = double.tryParse(distanceStr); |
|||
if (distance == null) return distanceStr; // 如果无法解析,则返回原始字符串 |
|||
|
|||
if (distance >= 1000) { |
|||
final km = distance / 1000; |
|||
return '${km.toStringAsFixed(1)} KM'; |
|||
} else { |
|||
return '${distance.toStringAsFixed(0)} M'; |
|||
} |
|||
} |
|||
|
|||
String formatDuration(String secondsStr) { |
|||
int totalSeconds = int.tryParse(secondsStr) ?? 0; |
|||
Duration duration = Duration(seconds: totalSeconds); |
|||
|
|||
int hours = duration.inHours; |
|||
int minutes = duration.inMinutes.remainder(60); |
|||
int seconds = duration.inSeconds.remainder(60); |
|||
|
|||
List<String> parts = []; |
|||
if (hours > 0) parts.add('$hours${'hours'.tr}'); // 小时 |
|||
if (minutes > 0) parts.add('$minutes${'minutes'.tr}'); // 分钟 |
|||
//不需要秒 |
|||
//if (seconds > 0 || parts.isEmpty) parts.add('$seconds${'seconds'.tr}'); // 秒 |
|||
|
|||
return parts.join(''); |
|||
} |
|||
|
|||
/// 导航卡片组件 |
|||
/// |
|||
/// 各元素含义分析: |
|||
/// - [start]: 起点坐标 (经纬度格式) |
|||
/// - [end]: 终点坐标 (经纬度格式) |
|||
/// - [destination]: 目的地名称显示 |
|||
/// - [modes]: 导航方式列表,包含驾车、步行、公交等选项 |
|||
/// - [navigation]: 当前导航状态信息(可选) |
|||
/// - [height]: 卡片高度 |
|||
Widget googlenavigationCard( |
|||
BuildContext context, { |
|||
required String start, |
|||
required String end, |
|||
required String destination, |
|||
required List<Map<String, dynamic>> modes, |
|||
Map<String, dynamic>? navigation, |
|||
double? height, // 改为可选参数,允许自适应高度 |
|||
}) { |
|||
int currentIndex = 0; |
|||
final controller = Get.find<AgentController>(); |
|||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|||
|
|||
return Container( |
|||
height: height?.h ?? 350.h, // 如果没有指定高度,使用350作为默认高度 |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(16.r), |
|||
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
blurRadius: 12, |
|||
color: Colors.black.withValues(alpha: isDarkMode ? 0.4 : 0.08), |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: StatefulBuilder( |
|||
builder: (context, setInnerState) { |
|||
return Column( |
|||
children: [ |
|||
// 顶部:路线预览图片区域 |
|||
Expanded( |
|||
flex: 4, // 增加图片区域的flex值,让图片占更多空间 |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), |
|||
child: Stack( |
|||
children: [ |
|||
// 路线图片 |
|||
AnimatedSwitcher( |
|||
duration: const Duration(milliseconds: 300), |
|||
child: Image.network( |
|||
modes[currentIndex]["image"] as String, |
|||
key: ValueKey<String>( |
|||
modes[currentIndex]["ntype"] as String), |
|||
height: double.infinity, |
|||
width: double.infinity, |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return Container( |
|||
color: isDarkMode |
|||
? Colors.grey[800] |
|||
: Colors.grey[200], |
|||
child: Icon( |
|||
Icons.map_outlined, |
|||
size: 48.sp, |
|||
color: isDarkMode |
|||
? Colors.grey[600] |
|||
: Colors.grey[400], |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
// 导航状态覆盖层 |
|||
if (navigation != null) |
|||
Positioned( |
|||
top: 12.h, |
|||
left: 12.w, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 8.w, vertical: 4.h), |
|||
decoration: BoxDecoration( |
|||
color: |
|||
Theme.of(context).primaryColor.withOpacity(0.9), |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon( |
|||
Icons.navigation, |
|||
color: Colors.white, |
|||
size: 14.sp, |
|||
), |
|||
SizedBox(width: 4.w), |
|||
Text( |
|||
'navigating'.tr, // 导航中 |
|||
style: TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 12.sp, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
|
|||
// 中部:导航方式选择器 |
|||
Container( |
|||
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), |
|||
child: Row( |
|||
children: List.generate(modes.length, (index) { |
|||
final mode = modes[index]; |
|||
final isSelected = index == currentIndex; |
|||
final isEnabled = mode['enable'] as bool; |
|||
|
|||
return Expanded( |
|||
child: GestureDetector( |
|||
onTap: isEnabled |
|||
? () { |
|||
setInnerState(() => currentIndex = index); |
|||
} |
|||
: null, |
|||
child: Container( |
|||
margin: EdgeInsets.symmetric(horizontal: 4.w), |
|||
padding: EdgeInsets.symmetric( |
|||
vertical: 8.h, horizontal: 12.w), |
|||
decoration: BoxDecoration( |
|||
color: isSelected |
|||
? Theme.of(context).primaryColor.withOpacity(0.1) |
|||
: Colors.transparent, |
|||
borderRadius: BorderRadius.circular(8.r), |
|||
border: Border.all( |
|||
color: isSelected |
|||
? Theme.of(context).primaryColor |
|||
: (isDarkMode |
|||
? Colors.grey[700]! |
|||
: Colors.grey[300]!), |
|||
width: isSelected ? 2 : 1, |
|||
), |
|||
), |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon( |
|||
_getNavigationIcon(mode["ntype"] as String), |
|||
color: isEnabled |
|||
? (isSelected |
|||
? Theme.of(context).primaryColor |
|||
: (isDarkMode |
|||
? Colors.white70 |
|||
: Colors.black87)) |
|||
: Colors.grey, |
|||
size: 20.sp, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
}), |
|||
), |
|||
), |
|||
|
|||
// 底部:目的地信息和操作按钮 |
|||
Container( |
|||
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 第一层:起点 → 目的地信息(独占整个宽度) |
|||
Row( |
|||
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 |
|||
children: [ |
|||
// 起点图标 - 统一图标容器宽度,确保对齐 |
|||
SizedBox( |
|||
width: 16.sp, // 固定图标容器宽度 |
|||
child: Icon( |
|||
Icons.my_location, |
|||
size: 15.sp, // 统一图标大小 |
|||
color: Theme.of(context).primaryColor, |
|||
), |
|||
), |
|||
SizedBox(width: 6.w), // 统一间距 |
|||
// 当前位置文本 |
|||
Text( |
|||
'currentLocation'.tr, // 当前位置 |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
fontWeight: FontWeight.w500, |
|||
color: isDarkMode ? Colors.white70 : Colors.black54, |
|||
height: 1.3, // 设置行高,提升多行文本可读性 |
|||
), |
|||
softWrap: true, // 启用软换行 |
|||
overflow: TextOverflow.visible, // 确保文本完全显示 |
|||
), |
|||
SizedBox(width: 8.w), |
|||
// 箭头图标 |
|||
Icon( |
|||
Icons.arrow_forward, |
|||
size: 15.sp, // 统一图标大小 |
|||
color: isDarkMode ? Colors.white54 : Colors.black45, |
|||
), |
|||
SizedBox(width: 8.w), |
|||
// 目的地图标 |
|||
Icon( |
|||
Icons.location_on, |
|||
size: 15.sp, // 统一图标大小 |
|||
color: Colors.red, |
|||
), |
|||
SizedBox(width: 6.w), // 统一间距 |
|||
// 目的地名称 - 使用Expanded确保自动换行 |
|||
Expanded( |
|||
child: Text( |
|||
destination, |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDarkMode ? Colors.white : Colors.black87, |
|||
height: 1.2, // 设置行高,提升多行文本可读性 |
|||
), |
|||
softWrap: true, // 启用软换行 |
|||
overflow: TextOverflow.visible, // 确保文本完全显示 |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
SizedBox(height: 4.h), // 第一层和第二层之间的间距 |
|||
|
|||
// 第二层:距离时间信息(左侧)+ 导航按钮(右侧) |
|||
Row( |
|||
crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐 |
|||
children: [ |
|||
// 左侧:距离和时间信息 |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 距离信息 |
|||
Row( |
|||
crossAxisAlignment: |
|||
CrossAxisAlignment.center, // 确保垂直居中对齐 |
|||
children: [ |
|||
// 距离图标 - 统一图标容器宽度,确保与第一层对齐 |
|||
SizedBox( |
|||
width: 16.sp, // 与第一层图标容器宽度一致 |
|||
child: Icon( |
|||
Icons.straighten, |
|||
size: 15.sp, // 统一图标大小 |
|||
color: Theme.of(context).primaryColor, |
|||
), |
|||
), |
|||
SizedBox(width: 6.w), // 与第一层间距一致 |
|||
Flexible( |
|||
child: Text( |
|||
formatDistance( |
|||
modes[currentIndex]["distance"]), |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: Theme.of(context).primaryColor, |
|||
fontWeight: FontWeight.w500, |
|||
height: 1.3, // 设置行高,提升多行文本可读性 |
|||
), |
|||
softWrap: true, // 启用软换行 |
|||
overflow: TextOverflow.visible, // 确保文本完全显示 |
|||
), |
|||
), |
|||
], |
|||
), |
|||
SizedBox(height: 4.h), |
|||
// 时间信息 |
|||
Row( |
|||
crossAxisAlignment: |
|||
CrossAxisAlignment.center, // 确保垂直居中对齐 |
|||
children: [ |
|||
// 时间图标 - 统一图标容器宽度,确保与第一层对齐 |
|||
SizedBox( |
|||
width: 16.sp, // 与第一层图标容器宽度一致 |
|||
child: Icon( |
|||
Icons.access_time, |
|||
size: 15.sp, // 统一图标大小 |
|||
color: Theme.of(context).primaryColor, |
|||
), |
|||
), |
|||
SizedBox(width: 6.w), // 与第一层间距一致 |
|||
Flexible( |
|||
child: Text( |
|||
'${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约 |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: Theme.of(context).primaryColor, |
|||
fontWeight: FontWeight.w500, |
|||
height: 1.3, // 设置行高,提升多行文本可读性 |
|||
), |
|||
softWrap: true, // 启用软换行 |
|||
overflow: TextOverflow.visible, // 确保文本完全显示 |
|||
), |
|||
), |
|||
], |
|||
), |
|||
// 导航状态信息 |
|||
if (navigation != null) ...[ |
|||
SizedBox(height: 6.h), |
|||
Container( |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 8.w, vertical: 4.h), |
|||
decoration: BoxDecoration( |
|||
color: Theme.of(context) |
|||
.primaryColor |
|||
.withOpacity(0.1), |
|||
borderRadius: BorderRadius.circular(6.r), |
|||
), |
|||
child: Text( |
|||
"${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入 |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
color: Theme.of(context).primaryColor, |
|||
fontWeight: FontWeight.w500, |
|||
height: 1.3, // 设置行高,提升多行文本可读性 |
|||
), |
|||
softWrap: true, // 启用软换行 |
|||
overflow: TextOverflow.visible, // 确保文本完全显示 |
|||
), |
|||
), |
|||
], |
|||
], |
|||
), |
|||
), |
|||
|
|||
SizedBox(width: 16.w), // 左右内容之间的间距 |
|||
|
|||
// 右侧:开始导航按钮 |
|||
Container( |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
colors: [ |
|||
Theme.of(context).primaryColor, |
|||
Theme.of(context).primaryColor.withOpacity(0.8), |
|||
], |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
), |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Theme.of(context) |
|||
.primaryColor |
|||
.withOpacity(0.3), |
|||
blurRadius: 8, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Material( |
|||
color: Colors.transparent, |
|||
child: InkWell( |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
onTap: () { |
|||
controller.openGoogleMap( |
|||
end, modes[currentIndex]["ntype"] as String); |
|||
}, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 16.w, |
|||
vertical: 12.h, |
|||
), |
|||
child: Icon( |
|||
Icons.navigation, |
|||
color: Colors.white, |
|||
size: 20.sp, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 根据导航类型获取对应图标 |
|||
IconData _getNavigationIcon(String ntype) { |
|||
switch (ntype.toLowerCase()) { |
|||
case 'driving': |
|||
case '驾车': |
|||
return Icons.directions_car; |
|||
case 'walking': |
|||
case '步行': |
|||
return Icons.directions_walk; |
|||
case 'transit': |
|||
case 'tsransit': // 修正拼写错误 |
|||
case '公交': |
|||
return Icons.directions_transit; |
|||
case 'bicycling': // 修正为正确的英文值 |
|||
case 'cycling': |
|||
case '骑行': |
|||
return Icons.directions_bike; |
|||
default: |
|||
return Icons.navigation; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue