flutter_integrated_whb/lib/customWidget/BaiDuMap/BaiduMapWebView.dart

343 lines
11 KiB
Dart
Raw Normal View History

2025-08-19 11:06:16 +08:00
// baidu_map_webview_debug.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// 更健壮的 BaiduMapWebView带 JS 错误回传、ready 检测、超时/重试)
class BaiduMapWebView extends StatefulWidget {
final String ak; // 请确保这是 web (JS API) 可用的 AK
final double latitude;
final double longitude;
final int zoom; // 对应 uniapp 的 scale
final List<Map<String, dynamic>> covers;
final ValueChanged<Map<String, dynamic>>? onMarkerTap;
final Duration readyTimeout;
const BaiduMapWebView({
Key? key,
required this.ak,
required this.latitude,
required this.longitude,
this.zoom = 13,
this.covers = const [],
this.onMarkerTap,
this.readyTimeout = const Duration(seconds: 8),
}) : super(key: key);
@override
State<BaiduMapWebView> createState() => _BaiduMapWebViewState();
}
class _BaiduMapWebViewState extends State<BaiduMapWebView> {
late final WebViewController _controller;
bool _pageLoaded = false;
bool _mapReady = false;
String? _lastJsMessage;
Timer? _readyTimer;
bool _showError = false;
String _errorText = '';
String _html(String ak) {
// HTML 模板:创建 map定义 setCenter / setMarkersFromFlutter转发 console 与 onerror
return '''
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta charset="utf-8"/>
<style>html,body,#map{height:100%;margin:0;padding:0;background:#f0f0f0}</style>
</head>
<body>
<div id="map"></div>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=$ak"></script>
<script>
// 把 console.log、console.error、window.onerror 转发给 FlutterBridge
(function(){
function send(kind, msg) {
try { Bridge.postMessage(JSON.stringify({__bridge: true, kind: kind, msg: String(msg)})); } catch(e) {}
}
var _log = console.log;
console.log = function(){
try{ send('log', Array.prototype.slice.call(arguments).join(' ')); }catch(e){}
_log && _log.apply(console, arguments);
};
var _err = console.error;
console.error = function(){
try{ send('error', Array.prototype.slice.call(arguments).join(' ')); }catch(e){}
_err && _err.apply(console, arguments);
};
window.onerror = function(msg, url, line, col, err) {
try{ send('onerror', msg + ' at ' + url + ':' + line + ':' + col + ' -> ' + (err && err.stack? err.stack:'') ); }catch(e){}
};
})();
// 初始化地图
var map;
try {
map = new BMap.Map("map");
map.enableScrollWheelZoom(true);
} catch(e) {
console.error('Map init error', e);
}
function setCenter(lat, lng, zoom) {
try {
if(!map) { console.error('map undefined in setCenter'); return; }
var p = new BMap.Point(lng, lat);
map.centerAndZoom(p, zoom || map.getZoom());
} catch(e) {
console.error('setCenter error', e);
}
}
var markers = [];
function clearMarkers(){
try {
for(var i=0;i<markers.length;i++){
map.removeOverlay(markers[i]);
}
markers = [];
} catch(e) { console.error('clearMarkers error', e); }
}
function setMarkersFromFlutter(coversJson){
try {
if(!map) { console.error('map undefined in setMarkersFromFlutter'); return; }
var arr = JSON.parse(coversJson || '[]');
clearMarkers();
for (var i = 0; i < arr.length; i++) {
var it = arr[i];
if(!it || it.longitude==null || it.latitude==null) continue;
var pt = new BMap.Point(parseFloat(it.longitude), parseFloat(it.latitude));
var marker;
if (it.icon && it.icon.length > 0) {
// 图片过大可能失败,谨慎使用 base64 大图
var myIcon = new BMap.Icon(it.icon, new BMap.Size(36,36));
marker = new BMap.Marker(pt, {icon: myIcon});
} else {
marker = new BMap.Marker(pt);
}
try {
marker._meta = it.data || it;
} catch(e) { marker._meta = {}; }
(function(m){
m.addEventListener('click', function(){
try { Bridge.postMessage(JSON.stringify(m._meta)); } catch(e) {}
});
})(marker);
markers.push(marker);
map.addOverlay(marker);
}
} catch (e) {
console.error('setMarkersFromFlutter parse error', e);
}
}
// 地图初始化完成后发送 ready
function notifyReady(){
try{ Bridge.postMessage(JSON.stringify({__mapReady:true})); }catch(e){}
}
// 等待 map 实例, 然后 center 并通知 ready
(function waitMap(){
try {
if(map && typeof map.centerAndZoom === 'function') {
// initial center (Flutter will call setCenter after load, but still notify)
notifyReady();
} else {
setTimeout(waitMap, 300);
}
} catch(e) {
console.error('waitMap error', e);
}
})();
// 导出 API
window.setMarkersFromFlutter = setMarkersFromFlutter;
window.setCenter = setCenter;
</script>
</body>
</html>
''';
}
String _escapeForJs(String s) => s.replaceAll(r'\', r'\\').replaceAll("'", r"\\'").replaceAll('\n', r' ');
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('Bridge', onMessageReceived: _onJsMessage)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
_pageLoaded = true;
_startReadyTimer();
// inject initial center & markers AFTER slight delay to allow JS to define functions
Future.delayed(const Duration(milliseconds: 300), () async {
await _controller.runJavaScript('setCenter(${widget.latitude}, ${widget.longitude}, ${widget.zoom});');
final coversJson = jsonEncode(widget.covers);
await _controller.runJavaScript("setMarkersFromFlutter('${_escapeForJs(coversJson)}');");
});
},
onWebResourceError: (err) {
_reportError('WebResourceError: ${err.description}');
},
))
..loadHtmlString(_html(widget.ak));
}
void _startReadyTimer() {
_readyTimer?.cancel();
_readyTimer = Timer(widget.readyTimeout, () {
if (!mounted) return;
if (!_mapReady) {
setState(() {
_showError = true;
_errorText = '地图初始化超时,可能 AK 无效或网络受限。请检查 AK需为百度 JS API Key与网络。';
});
}
});
}
void _onJsMessage(JavaScriptMessage msg) {
_lastJsMessage = msg.message;
// 解析 message
try {
final m = jsonDecode(msg.message);
if (m is Map && m.containsKey('__mapReady') && m['__mapReady'] == true) {
// JS 报告地图已 ready
_readyTimer?.cancel();
if (mounted) {
setState(() {
_mapReady = true;
_showError = false;
_errorText = '';
});
}
return;
}
} catch (_) {
// not json meta, maybe marker data or console log
}
// handle console / error wrapper shape: {__bridge:true, kind:..., msg:...}
try {
final decoded = jsonDecode(msg.message);
if (decoded is Map && decoded['__bridge'] == true) {
final kind = decoded['kind'];
final mm = decoded['msg'];
if (kind == 'error' || kind == 'onerror') {
_reportError('JS error: $mm');
} else {
// console log: keep last message (debug only)
debugPrint('JS log: $mm');
}
return;
}
} catch (_) {}
// otherwise treat as marker click payload (likely non-meta JSON)
try {
final payload = jsonDecode(msg.message) as Map<String, dynamic>;
if (widget.onMarkerTap != null) widget.onMarkerTap!(payload);
} catch (e) {
// not JSON? ignore
debugPrint('Unrecognized JS message: ${msg.message}');
}
}
void _reportError(String text) {
debugPrint(text);
if (!mounted) return;
setState(() {
_showError = true;
_errorText = text;
});
}
@override
void didUpdateWidget(covariant BaiduMapWebView oldWidget) {
super.didUpdateWidget(oldWidget);
if (_pageLoaded) {
if (oldWidget.latitude != widget.latitude ||
oldWidget.longitude != widget.longitude ||
oldWidget.zoom != widget.zoom) {
_controller.runJavaScript('setCenter(${widget.latitude}, ${widget.longitude}, ${widget.zoom});');
}
if (!listEquals(oldWidget.covers, widget.covers)) {
final coversJson = jsonEncode(widget.covers);
_controller.runJavaScript("setMarkersFromFlutter('${_escapeForJs(coversJson)}');");
}
}
}
@override
void dispose() {
_readyTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(controller: _controller),
if (!_mapReady && !_showError)
const Center(child: CircularProgressIndicator()),
if (_showError)
Positioned.fill(
child: Container(
color: Colors.white70,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('地图加载失败', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(_errorText, textAlign: TextAlign.center),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
setState(() {
_showError = false;
_mapReady = false;
});
// 重新加载页面
_controller.loadHtmlString(_html(widget.ak));
},
child: const Text('重试'),
),
const SizedBox(height: 6),
ElevatedButton(
onPressed: () {
// 把最后一条 js message 展示出来,便于你贴错误给我
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('JS 消息'),
content: SingleChildScrollView(child: Text(_lastJsMessage ?? '')),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
],
),
);
},
child: const Text('查看 JS 日志'),
),
],
),
),
),
),
),
],
);
}
}