QinGang_interested/lib/customWidget/BaiDuMap/map_preview_widget.dart

212 lines
7.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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<Map<String, dynamic>> 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<MapPreviewWidget> createState() => _MapPreviewWidgetState();
}
class _MapPreviewWidgetState extends State<MapPreviewWidget> {
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<void> _initServerAndLoad() async {
_baseUri = await AssetServer().start(); // 单例 asset server
// 把 points JSON 编码到 URLmap.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<String, double>? _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<void> _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)),
),
),
],
),
),
);
}
}