flutter_integrated_whb/lib/customWidget/BaiDuMap/BaiduMapWebView.dart

343 lines
11 KiB
Dart
Raw 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.

// 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 日志'),
),
],
),
),
),
),
),
],
);
}
}