343 lines
11 KiB
Dart
343 lines
11 KiB
Dart
|
// 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 转发给 Flutter(Bridge)
|
|||
|
(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 日志'),
|
|||
|
),
|
|||
|
],
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
),
|
|||
|
],
|
|||
|
);
|
|||
|
}
|
|||
|
}
|