// lib/widgets/map_preview_widget.dart import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/tools/asset_server.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// MapPreviewWidget:只读缩略地图(不能交互),显示传入的点位(markers)。 /// points 格式示例: /// [ { "longitude": 116.397428, "latitude": 39.90923, "iconPath": "/static/marker50.png" }, ... ] class MapPreviewWidget extends StatefulWidget { const MapPreviewWidget({ Key? key, required this.points, this.width = 160, this.height = 160, this.borderRadius = 6, this.showBorder = true, this.defaultLongitude = 116.397428, this.defaultLatitude = 39.90923, this.defaultZoom = 15, }) : super(key: key); final List> points; final double width; final double height; final double borderRadius; final bool showBorder; /// 当 points 为空时使用的默认中心(北京) final double defaultLongitude; final double defaultLatitude; final int defaultZoom; @override State createState() => _MapPreviewWidgetState(); } class _MapPreviewWidgetState extends State { late final WebViewController _controller; bool _loading = true; Uri? _baseUri; @override void initState() { super.initState(); // 创建 controller(无需 JS channel,因为不需要回调) _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate(NavigationDelegate( onPageFinished: (url) async { // 页面加载完成后注入“居中 + 阻止交互”的脚本 await _injectCenterAndBlock(); setState(() { _loading = false; }); }, onWebResourceError: (err) { debugPrint('[MapPreview] WebResourceError: $err'); }, )); // 启动本地 server 并加载 page _initServerAndLoad(); } Future _initServerAndLoad() async { _baseUri = await AssetServer().start(); // 单例 asset server // 把 points JSON 编码到 URL(map.html 已能处理 point 参数) final jsonPoints = jsonEncode(widget.points); final encoded = Uri.encodeComponent(jsonPoints); final uri = Uri.parse('${_baseUri.toString()}/map.html?point=$encoded&t=${DateTime.now().millisecondsSinceEpoch}'); debugPrint('[MapPreview] load url: $uri'); try { await _controller.loadRequest(uri); } catch (e) { debugPrint('[MapPreview] loadRequest failed: $e'); } } // 计算点位平均中心(如果没有点则返回 null) Map? _computeAverageCenter() { double sumLon = 0.0, sumLat = 0.0; int count = 0; for (final p in widget.points) { try { final lon = _toDouble(p['longitude'] ?? p['longitue'] ?? p['long']); final lat = _toDouble(p['latitude'] ?? p['lat']); if (lon != null && lat != null) { sumLon += lon; sumLat += lat; count++; } } catch (_) {} } if (count == 0) return null; return {'longitude': sumLon / count, 'latitude': sumLat / count}; } double? _toDouble(dynamic v) { if (v == null) return null; if (v is double) return v; if (v is int) return v.toDouble(); if (v is String) { final cleaned = v.replaceAll(RegExp(r'[^\d\.\-]'), ''); return double.tryParse(cleaned); } return null; } // 注入 JS:设置中心并插入一个透明的覆盖层阻止所有页面内交互 Future _injectCenterAndBlock() async { final center = _computeAverageCenter(); final lon = center != null ? center['longitude']! : widget.defaultLongitude; final lat = center != null ? center['latitude']! : widget.defaultLatitude; final zoom = widget.defaultZoom; // JS:尝试用几种不同地图 API 的方式设置中心与缩放(兼容性考虑), // 然后在页面上加一个透明 div 覆盖层,阻止交互(pointerEvents 会拦截) final js = ''' (function(){ try { var lon = ${lon.toString()}; var lat = ${lat.toString()}; var zoom = ${zoom.toString()}; // try custom setter try { if (typeof window.setMapCenter === 'function') { window.setMapCenter(lon, lat, zoom); } } catch(e) {} // Try T.Map style (used in your map.html) try { if (typeof map !== 'undefined' && map) { if (typeof map.centerAndZoom === 'function' && typeof T !== 'undefined') { try { map.centerAndZoom(new T.LngLat(lon, lat), zoom); } catch(e) {} } else if (typeof map.setCenter === 'function') { try { map.setCenter(new BMap.Point(lon, lat)); if (typeof map.setZoom === 'function') map.setZoom(zoom); } catch(e) {} } } } catch(e) {} // Add a full-page transparent blocker DIV to prevent interactions try { // avoid duplicate block if (!document.getElementById('flutter_preview_block')) { var block = document.createElement('div'); block.id = 'flutter_preview_block'; block.style.position = 'fixed'; block.style.left = '0'; block.style.top = '0'; block.style.width = '100%'; block.style.height = '100%'; block.style.zIndex = '2147483647'; // very high block.style.background = 'transparent'; // pointerEvents 'auto' means this div captures events and prevents underlying map from receiving them block.style.pointerEvents = 'auto'; // ensure it doesn't block CSS visuals (transparent) document.documentElement.appendChild(block); } } catch (e) { console.log('preview block insert err', e); } } catch(e) { console.log('preview inject error', e); } })(); '''; try { await _controller.runJavaScript(js); debugPrint('[MapPreview] injected center + blocker'); } catch (e) { debugPrint('[MapPreview] inject error: $e'); } } @override void dispose() { // 不停止单例 AssetServer(可能被其它页面复用) super.dispose(); } @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(widget.borderRadius), child: Container( width: widget.width, height: widget.height, decoration: widget.showBorder ? BoxDecoration( color: Colors.white, border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(widget.borderRadius), ) : null, child: Stack( children: [ // WebView(我们不使用 AbsorbPointer,因为 JS 覆盖层会阻止页面内交互) WebViewWidget(controller: _controller), if (_loading) const Positioned.fill( child: Center( child: SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2)), ), ), ], ), ), ); } }