QinGang_interested/lib/customWidget/BaiDuMap/map_preview_widget.dart

212 lines
7.0 KiB
Dart
Raw Normal View History

2026-04-10 17:25:59 +08:00
// 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)),
),
),
],
),
),
);
}
}