import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors_extension.dart'; class TripRouteCard extends StatelessWidget { final String originLabel; final String destLabel; final double? originLat; final double? originLng; final double? destLat; final double? destLng; const TripRouteCard({ super.key, required this.originLabel, required this.destLabel, this.originLat, this.originLng, this.destLat, this.destLng, }); bool get _hasCoords => originLat != null && originLng != null && destLat != null && destLng != null && (originLat! != 0 || originLng! != 0) && (destLat! != 0 || destLng! != 0) && // 起终点重合会使 flutter_map 计算出 Infinity/NaN zoom 而崩溃(瓦片层 round 报错),隐藏地图 (originLat != destLat || originLng != destLng); LatLng get _origin => LatLng(originLat!, originLng!); LatLng get _dest => LatLng(destLat!, destLng!); @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; return Column( mainAxisSize: MainAxisSize.min, children: [ _buildTextCard(context, colors), if (_hasCoords) ...[const SizedBox(height: 20), _buildMapCard(colors)], ], ); } Widget _buildTextCard(BuildContext context, AppColorsExtension colors) { return Column( children: [ GestureDetector( onLongPress: () => _copyToClipboard(context, originLabel), child: _bubble( originLabel, Icons.trip_origin, colors.primary, colors, ), ), Padding( padding: const EdgeInsets.only(left: 9), child: Column( children: List.generate( 3, (_) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Container( width: 2, height: 2, decoration: BoxDecoration( color: colors.textPlaceholder, shape: BoxShape.circle, ), ), ), ), ), ), GestureDetector( onLongPress: () => _copyToClipboard(context, destLabel), child: _bubble(destLabel, Icons.location_on, colors.danger, colors), ), ], ); } void _copyToClipboard(BuildContext context, String text) { final l10n = AppLocalizations.of(context); Clipboard.setData(ClipboardData(text: text)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Center(child: Text(l10n.get('copiedToClipboard'))), duration: const Duration(seconds: 1), behavior: SnackBarBehavior.floating, width: 120, ), ); } Widget _bubble( String label, IconData icon, Color color, AppColorsExtension colors, ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: color.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(icon, size: 18, color: color), const SizedBox(width: 10), Expanded( child: Text( label, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: colors.textPrimary, ), ), ), ], ), ); } Widget _buildMapCard(AppColorsExtension colors) { final dist = const Distance().as(LengthUnit.Kilometer, _origin, _dest); final mid = LatLng( (_origin.latitude + _dest.latitude) / 2, (_origin.longitude + _dest.longitude) / 2, ); final distText = dist >= 1 ? '${dist.toStringAsFixed(1)}km' : '${(dist * 1000).toInt()}m'; return Container( height: 200, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: colors.border, width: 0.5), ), clipBehavior: Clip.antiAlias, child: FlutterMap( options: MapOptions( initialCameraFit: CameraFit.bounds( bounds: LatLngBounds.fromPoints([_origin, _dest]), padding: const EdgeInsets.all(40), // 高德瓦片最大 18 级:两点极近时 fit 出的 zoom 超限会导致瓦片 404,整图空白 maxZoom: 18, ), ), children: [ TileLayer( urlTemplate: 'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', subdomains: const ['1', '2', '3', '4'], userAgentPackageName: 'com.amtxts.tboss_oa_module', ), PolylineLayer( polylines: [ Polyline( points: [_origin, _dest], color: colors.primary.withValues(alpha: 0.8), strokeWidth: 3, ), ], ), MarkerLayer( markers: [ Marker( point: _origin, width: 180, height: 32, child: _mapLabel( originLabel, Icons.trip_origin, colors.primary, colors.textPrimary, ), ), Marker( point: _dest, width: 180, height: 32, child: _mapLabel( destLabel, Icons.location_on, colors.danger, colors.textPrimary, ), ), Marker( point: mid, width: 80, height: 24, child: _distBadge(distText, colors), ), ], ), ], ), ); } Widget _distBadge(String text, AppColorsExtension colors) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), alignment: Alignment.center, decoration: BoxDecoration( color: colors.primary.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(10), ), child: Text( text, textAlign: TextAlign.center, style: const TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.w600, ), ), ); } Widget _mapLabel( String text, IconData icon, Color iconColor, Color textColor, ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 4), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: iconColor), const SizedBox(width: 2), Flexible( child: Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 10, color: textColor), ), ), ], ), ); } }