|
|
|
@ -31,7 +31,15 @@ String formatDuration(String secondsStr) { |
|
|
|
return parts.join(''); |
|
|
|
} |
|
|
|
|
|
|
|
//导航卡片 |
|
|
|
/// 导航卡片组件 |
|
|
|
/// |
|
|
|
/// 各元素含义分析: |
|
|
|
/// - [start]: 起点坐标 (经纬度格式) |
|
|
|
/// - [end]: 终点坐标 (经纬度格式) |
|
|
|
/// - [destination]: 目的地名称显示 |
|
|
|
/// - [modes]: 导航方式列表,包含驾车、步行、公交等选项 |
|
|
|
/// - [navigation]: 当前导航状态信息(可选) |
|
|
|
/// - [height]: 卡片高度 |
|
|
|
Widget navigationCard( |
|
|
|
BuildContext context, { |
|
|
|
required String start, |
|
|
|
@ -39,72 +47,168 @@ Widget navigationCard( |
|
|
|
required String destination, |
|
|
|
required List<Map<String, dynamic>> modes, |
|
|
|
Map<String, dynamic>? navigation, |
|
|
|
double height = 300, |
|
|
|
double? height, // 改为可选参数,允许自适应高度 |
|
|
|
}) { |
|
|
|
int currentIndex = 0; |
|
|
|
final controller = Get.find<AgentController>(); // 获取控制器 |
|
|
|
final controller = Get.find<AgentController>(); |
|
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
|
|
|
|
|
return Container( |
|
|
|
height: height.h, |
|
|
|
height: height?.h ?? 350.h, // 如果没有指定高度,使用350作为默认高度 |
|
|
|
decoration: BoxDecoration( |
|
|
|
borderRadius: BorderRadius.circular(16.r), |
|
|
|
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
blurRadius: 8, |
|
|
|
color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.12), |
|
|
|
offset: const Offset(0, 4), |
|
|
|
blurRadius: 12, |
|
|
|
color: Colors.black.withValues(alpha: isDarkMode ? 0.4 : 0.08), |
|
|
|
offset: const Offset(0, 2), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: StatefulBuilder( |
|
|
|
builder: (context, setInnerState) { |
|
|
|
return Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// 路线图片 |
|
|
|
ClipRRect( |
|
|
|
borderRadius: |
|
|
|
BorderRadius.vertical(top: Radius.circular(16.r)), |
|
|
|
child: AnimatedSwitcher( |
|
|
|
duration: const Duration(milliseconds: 300), |
|
|
|
child: Image.network( |
|
|
|
modes[currentIndex]["image"] as String, |
|
|
|
key: ValueKey<String>(modes[currentIndex]["ntype"] as String), |
|
|
|
height: 160.h, |
|
|
|
width: double.infinity, |
|
|
|
fit: BoxFit.cover, |
|
|
|
// 顶部:路线预览图片区域 |
|
|
|
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( |
|
|
|
'导航中', |
|
|
|
style: TextStyle( |
|
|
|
color: Colors.white, |
|
|
|
fontSize: 12.sp, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
// 导航方式切换按钮 |
|
|
|
Padding( |
|
|
|
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w), |
|
|
|
// 中部:导航方式选择器 |
|
|
|
Container( |
|
|
|
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), |
|
|
|
child: Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
|
|
children: List.generate(modes.length, (index) { |
|
|
|
final mode = modes[index]; |
|
|
|
final isSelected = index == currentIndex; |
|
|
|
return GestureDetector( |
|
|
|
onTap: modes[index]['enable'] as bool |
|
|
|
? () { |
|
|
|
setInnerState(() => currentIndex = index); |
|
|
|
} |
|
|
|
: null, |
|
|
|
child: SizedBox( |
|
|
|
width: 50.w, |
|
|
|
child: Text( |
|
|
|
(modes[index]["ntype"] as String).tr, |
|
|
|
textAlign: TextAlign.center, |
|
|
|
style: TextStyle( |
|
|
|
color: modes[index]['enable'] |
|
|
|
? isSelected |
|
|
|
? Theme.of(context).primaryColor |
|
|
|
: isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87 |
|
|
|
: Colors.grey, |
|
|
|
fontSize: 12.sp, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
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, |
|
|
|
), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
Text( |
|
|
|
(mode["ntype"] as String).tr, |
|
|
|
textAlign: TextAlign.center, |
|
|
|
style: TextStyle( |
|
|
|
color: isEnabled |
|
|
|
? (isSelected |
|
|
|
? Theme.of(context).primaryColor |
|
|
|
: (isDarkMode |
|
|
|
? Colors.white70 |
|
|
|
: Colors.black87)) |
|
|
|
: Colors.grey, |
|
|
|
fontSize: 11.sp, |
|
|
|
fontWeight: isSelected |
|
|
|
? FontWeight.w600 |
|
|
|
: FontWeight.w400, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
@ -113,43 +217,191 @@ Widget navigationCard( |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
// 距离和耗时 |
|
|
|
Padding( |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 8.w), |
|
|
|
child: Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
|
|
// 底部:目的地信息和操作按钮 |
|
|
|
Container( |
|
|
|
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h), |
|
|
|
child: Stack( |
|
|
|
children: [ |
|
|
|
Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text(destination, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87)), |
|
|
|
Text( |
|
|
|
'${formatDistance(modes[currentIndex]["distance"])}-约${formatDuration(modes[currentIndex]["duration"])}', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 15.sp, |
|
|
|
color: Theme.of(context).primaryColor)), |
|
|
|
if (navigation != null) |
|
|
|
Text( |
|
|
|
"导航中... ${navigation["distanceText"]}进入${navigation["nextRoad"]}", |
|
|
|
style: TextStyle( |
|
|
|
// 目的地和距离时间信息 |
|
|
|
Padding( |
|
|
|
padding: EdgeInsets.only(right: 70.w), // 为右下角按钮留出空间 |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// 路径显示:当前位置 → 目的地 |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
// 起点图标 |
|
|
|
Icon( |
|
|
|
Icons.my_location, |
|
|
|
size: 16.sp, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
), |
|
|
|
SizedBox(width: 6.w), |
|
|
|
// 当前位置文本 |
|
|
|
Text( |
|
|
|
'当前位置', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
color: isDarkMode |
|
|
|
? Colors.white70 |
|
|
|
: Colors.black54, |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(width: 8.w), |
|
|
|
// 箭头图标 |
|
|
|
Icon( |
|
|
|
Icons.arrow_forward, |
|
|
|
size: 16.sp, |
|
|
|
color: isDarkMode |
|
|
|
? Colors.white54 |
|
|
|
: Colors.black45, |
|
|
|
), |
|
|
|
SizedBox(width: 8.w), |
|
|
|
// 目的地图标 |
|
|
|
Icon( |
|
|
|
Icons.location_on, |
|
|
|
size: 16.sp, |
|
|
|
color: Colors.red, |
|
|
|
), |
|
|
|
SizedBox(width: 6.w), |
|
|
|
// 目的地名称 |
|
|
|
Expanded( |
|
|
|
child: Text( |
|
|
|
destination, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16.sp, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode |
|
|
|
? Colors.white |
|
|
|
: Colors.black87, |
|
|
|
), |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
// 距离和时间信息 |
|
|
|
Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
// 距离信息 |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
Icon( |
|
|
|
Icons.straighten, |
|
|
|
size: 14.sp, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
), |
|
|
|
SizedBox(width: 4.w), |
|
|
|
Text( |
|
|
|
formatDistance( |
|
|
|
modes[currentIndex]["distance"]), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
SizedBox(height: 4.h), |
|
|
|
// 时间信息 |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
Icon( |
|
|
|
Icons.access_time, |
|
|
|
size: 14.sp, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
), |
|
|
|
SizedBox(width: 4.w), |
|
|
|
Text( |
|
|
|
'约${formatDuration(modes[currentIndex]["duration"])}', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14.sp, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
// 导航状态信息 |
|
|
|
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"]}进入${navigation["nextRoad"]}", |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12.sp, |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600])) |
|
|
|
], |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
CircleAvatar( |
|
|
|
backgroundColor: Theme.of(context).primaryColor, |
|
|
|
radius: 18.r, |
|
|
|
child: IconButton( |
|
|
|
icon: Icon(Icons.turn_left, |
|
|
|
color: Colors.white, size: 20.sp), |
|
|
|
onPressed: () { |
|
|
|
controller.openNavigation(start, end, ""); |
|
|
|
}, |
|
|
|
// 开始导航按钮 - 绝对定位到右下角 |
|
|
|
Positioned( |
|
|
|
right: 0, |
|
|
|
bottom: 0, |
|
|
|
child: 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.openNavigation(start, end, ""); |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
padding: EdgeInsets.symmetric( |
|
|
|
horizontal: 16.w, |
|
|
|
vertical: 12.h, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.navigation, |
|
|
|
color: Colors.white, |
|
|
|
size: 20.sp, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
) |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
@ -159,3 +411,25 @@ Widget navigationCard( |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// 根据导航类型获取对应图标 |
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
|