trip_route_card.dart 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_map/flutter_map.dart';
  4. import 'package:latlong2/latlong.dart';
  5. import '../../core/i18n/app_localizations.dart';
  6. import '../../core/theme/app_colors_extension.dart';
  7. class TripRouteCard extends StatelessWidget {
  8. final String originLabel;
  9. final String destLabel;
  10. final double? originLat;
  11. final double? originLng;
  12. final double? destLat;
  13. final double? destLng;
  14. const TripRouteCard({
  15. super.key,
  16. required this.originLabel,
  17. required this.destLabel,
  18. this.originLat,
  19. this.originLng,
  20. this.destLat,
  21. this.destLng,
  22. });
  23. bool get _hasCoords =>
  24. originLat != null &&
  25. originLng != null &&
  26. destLat != null &&
  27. destLng != null &&
  28. (originLat! != 0 || originLng! != 0) &&
  29. (destLat! != 0 || destLng! != 0);
  30. LatLng get _origin => LatLng(originLat!, originLng!);
  31. LatLng get _dest => LatLng(destLat!, destLng!);
  32. @override
  33. Widget build(BuildContext context) {
  34. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  35. return Column(
  36. mainAxisSize: MainAxisSize.min,
  37. children: [
  38. _buildTextCard(context, colors),
  39. if (_hasCoords) ...[const SizedBox(height: 20), _buildMapCard(colors)],
  40. ],
  41. );
  42. }
  43. Widget _buildTextCard(BuildContext context, AppColorsExtension colors) {
  44. return Column(
  45. children: [
  46. GestureDetector(
  47. onLongPress: () => _copyToClipboard(context, originLabel),
  48. child: _bubble(
  49. originLabel,
  50. Icons.trip_origin,
  51. colors.primary,
  52. colors,
  53. ),
  54. ),
  55. Padding(
  56. padding: const EdgeInsets.only(left: 9),
  57. child: Column(
  58. children: List.generate(
  59. 3,
  60. (_) => Padding(
  61. padding: const EdgeInsets.symmetric(vertical: 3),
  62. child: Container(
  63. width: 2,
  64. height: 2,
  65. decoration: BoxDecoration(
  66. color: colors.textPlaceholder,
  67. shape: BoxShape.circle,
  68. ),
  69. ),
  70. ),
  71. ),
  72. ),
  73. ),
  74. GestureDetector(
  75. onLongPress: () => _copyToClipboard(context, destLabel),
  76. child: _bubble(destLabel, Icons.location_on, colors.danger, colors),
  77. ),
  78. ],
  79. );
  80. }
  81. void _copyToClipboard(BuildContext context, String text) {
  82. final l10n = AppLocalizations.of(context);
  83. Clipboard.setData(ClipboardData(text: text));
  84. ScaffoldMessenger.of(context).showSnackBar(
  85. SnackBar(
  86. content: Center(child: Text(l10n.get('copiedToClipboard'))),
  87. duration: const Duration(seconds: 1),
  88. behavior: SnackBarBehavior.floating,
  89. width: 120,
  90. ),
  91. );
  92. }
  93. Widget _bubble(
  94. String label,
  95. IconData icon,
  96. Color color,
  97. AppColorsExtension colors,
  98. ) {
  99. return Container(
  100. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  101. decoration: BoxDecoration(
  102. color: color.withValues(alpha: 0.08),
  103. borderRadius: BorderRadius.circular(8),
  104. ),
  105. child: Row(
  106. children: [
  107. Icon(icon, size: 18, color: color),
  108. const SizedBox(width: 10),
  109. Expanded(
  110. child: Text(
  111. label,
  112. maxLines: 2,
  113. overflow: TextOverflow.ellipsis,
  114. style: TextStyle(
  115. fontSize: 14,
  116. fontWeight: FontWeight.w500,
  117. color: colors.textPrimary,
  118. ),
  119. ),
  120. ),
  121. ],
  122. ),
  123. );
  124. }
  125. Widget _buildMapCard(AppColorsExtension colors) {
  126. final dist = const Distance().as(LengthUnit.Kilometer, _origin, _dest);
  127. final mid = LatLng(
  128. (_origin.latitude + _dest.latitude) / 2,
  129. (_origin.longitude + _dest.longitude) / 2,
  130. );
  131. final distText = dist >= 1
  132. ? '${dist.toStringAsFixed(1)}km'
  133. : '${(dist * 1000).toInt()}m';
  134. return Container(
  135. height: 200,
  136. decoration: BoxDecoration(
  137. borderRadius: BorderRadius.circular(12),
  138. border: Border.all(color: colors.border, width: 0.5),
  139. ),
  140. clipBehavior: Clip.antiAlias,
  141. child: FlutterMap(
  142. options: MapOptions(
  143. initialCameraFit: CameraFit.bounds(
  144. bounds: LatLngBounds.fromPoints([_origin, _dest]),
  145. padding: const EdgeInsets.all(40),
  146. ),
  147. ),
  148. children: [
  149. TileLayer(
  150. urlTemplate:
  151. 'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
  152. subdomains: const ['1', '2', '3', '4'],
  153. userAgentPackageName: 'com.amtxts.tboss_oa_module',
  154. ),
  155. PolylineLayer(
  156. polylines: [
  157. Polyline(
  158. points: [_origin, _dest],
  159. color: colors.primary.withValues(alpha: 0.8),
  160. strokeWidth: 3,
  161. ),
  162. ],
  163. ),
  164. MarkerLayer(
  165. markers: [
  166. Marker(
  167. point: _origin,
  168. width: 180,
  169. height: 32,
  170. child: _mapLabel(
  171. originLabel,
  172. Icons.trip_origin,
  173. colors.primary,
  174. colors.textPrimary,
  175. ),
  176. ),
  177. Marker(
  178. point: _dest,
  179. width: 180,
  180. height: 32,
  181. child: _mapLabel(
  182. destLabel,
  183. Icons.location_on,
  184. colors.danger,
  185. colors.textPrimary,
  186. ),
  187. ),
  188. Marker(
  189. point: mid,
  190. width: 80,
  191. height: 24,
  192. child: _distBadge(distText, colors),
  193. ),
  194. ],
  195. ),
  196. ],
  197. ),
  198. );
  199. }
  200. Widget _distBadge(String text, AppColorsExtension colors) {
  201. return Container(
  202. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  203. alignment: Alignment.center,
  204. decoration: BoxDecoration(
  205. color: colors.primary.withValues(alpha: 0.85),
  206. borderRadius: BorderRadius.circular(10),
  207. ),
  208. child: Text(
  209. text,
  210. textAlign: TextAlign.center,
  211. style: const TextStyle(
  212. fontSize: 10,
  213. color: Colors.white,
  214. fontWeight: FontWeight.w600,
  215. ),
  216. ),
  217. );
  218. }
  219. Widget _mapLabel(
  220. String text,
  221. IconData icon,
  222. Color iconColor,
  223. Color textColor,
  224. ) {
  225. return Container(
  226. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  227. decoration: BoxDecoration(
  228. color: Colors.white,
  229. borderRadius: BorderRadius.circular(4),
  230. boxShadow: [
  231. BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 4),
  232. ],
  233. ),
  234. child: Row(
  235. mainAxisSize: MainAxisSize.min,
  236. children: [
  237. Icon(icon, size: 12, color: iconColor),
  238. const SizedBox(width: 2),
  239. Flexible(
  240. child: Text(
  241. text,
  242. maxLines: 1,
  243. overflow: TextOverflow.ellipsis,
  244. style: TextStyle(fontSize: 10, color: textColor),
  245. ),
  246. ),
  247. ],
  248. ),
  249. );
  250. }
  251. }