import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:recording_tool/app/router/app_navigator.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; class WebviewPage extends StatefulWidget { final String? url; final String? title; final String? assetPath; final bool? canPop; const WebviewPage({ super.key, this.url, this.title, this.assetPath, this.canPop, }); @override State createState() => WebviewPageState(); } class WebviewPageState extends State { static const String _javaScriptChannelName = 'AppBridge'; WebViewController? _controller; bool _isLoading = true; String? _errorMessage; @override void initState() { super.initState(); _initController(); } @override void dispose() { final controller = _controller; _controller = null; if (controller != null) { unawaited(_disposeWebViewController(controller)); } super.dispose(); } Future _initController() async { if ((widget.url?.isEmpty ?? true) && widget.assetPath == null) { return; } final params = _createPlatformParams(); late final WebViewController controller; controller = WebViewController.fromPlatformCreationParams(params) ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(Colors.transparent) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { debugPrint('webview - progress: $progress'); }, onPageStarted: (String url) { debugPrint('webview - page started: $url'); if (!mounted) return; setState(() { _isLoading = true; _errorMessage = null; }); }, onPageFinished: (String url) async { debugPrint('webview - page finished: $url'); // await _injectPostMessageBridge(controller); await _injectH5NeedData(controller); if (!mounted) return; setState(() => _isLoading = false); }, onHttpError: (HttpResponseError error) { debugPrint( 'webview - http error: ${error.response?.uri}, code: ${error.response?.statusCode}', ); }, onWebResourceError: (WebResourceError error) { debugPrint( 'webview error: ${error.description}, code: ${error.errorCode}, url: ${error.url}', ); if (!mounted || error.isForMainFrame != true) return; setState(() { _isLoading = false; _errorMessage = error.description; }); }, onNavigationRequest: (NavigationRequest request) { return NavigationDecision.navigate; }, ), ); controller.addJavaScriptChannel( _javaScriptChannelName, onMessageReceived: (JavaScriptMessage message) { _onMessageReceived(message); }, ); if (controller.platform is AndroidWebViewController) { AndroidWebViewController.enableDebugging(kDebugMode); await (controller.platform as AndroidWebViewController) .setMediaPlaybackRequiresUserGesture(false); } try { if (widget.assetPath != null) { await controller.loadFlutterAsset(widget.assetPath!); } else { final targetUrl = widget.url ?? ''; await controller.loadRequest(Uri.parse(targetUrl)); } } catch (e) { debugPrint('webview - load failed: $e'); if (!mounted) return; setState(() { _isLoading = false; _errorMessage = e.toString(); }); return; } if (!mounted) { unawaited(_disposeWebViewController(controller)); return; } setState(() => _controller = controller); } Future _disposeWebViewController(WebViewController controller) async { await _runWebViewCleanupStep( 'reset navigation delegate', () => controller.setNavigationDelegate(NavigationDelegate()), ); await _runWebViewCleanupStep( 'remove javascript channel', () => controller.removeJavaScriptChannel(_javaScriptChannelName), ); await _runWebViewCleanupStep( 'load blank page', () => controller.loadRequest(Uri.parse('about:blank')), ); await _runWebViewCleanupStep('clear cache', controller.clearCache); await _runWebViewCleanupStep( 'clear local storage', controller.clearLocalStorage, ); await _runWebViewCleanupStep( 'clear cookies', () => WebViewCookieManager().clearCookies(), ); } Future _runWebViewCleanupStep( String step, Future Function() cleanup, ) async { try { debugPrint('webview - dispose cleanup at $step '); await cleanup(); } catch (e) { debugPrint('webview - dispose cleanup failed at $step: $e'); } } void _onMessageReceived(JavaScriptMessage message) { debugPrint('收到来自 WebView 的消息: ${message.message}'); final jsonMap = json.decode(message.message); if (jsonMap['type'] != null && jsonMap['type'] == 'navigator_pop') { Navigator.of(context).maybePop(); } } /// 脚本注入传输给h5 数据 Future _injectH5NeedData(WebViewController controller) async { final token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjgxNDI4NTczNTY2NjY4ODAwLCJkYXRhIjp7fSwic3ViIjoiODE0Mjg1NzM1NjY2Njg4MDAiLCJleHAiOjE3ODM5MzM1ODQsIm5iZiI6MTc4MzMyODc4NCwiaWF0IjoxNzgzMzI4Nzg0fQ.r1wYfvpV-5HIgwUuvq1Or3jPgjc3AhfJmoV1PNP23-Y'; try { await controller.runJavaScript(''' (function () { if (window.__appInstallData) { return; } window.__appInstallData = ${true}; window.AppBridge.token = '$token'; })(); '''); } catch (e) { debugPrint('webview - inject postMessage bridge failed: $e'); } } PlatformWebViewControllerCreationParams _createPlatformParams() { if (WebViewPlatform.instance is WebKitWebViewPlatform) { return WebKitWebViewControllerCreationParams( allowsInlineMediaPlayback: true, mediaTypesRequiringUserAction: const {}, ); } return const PlatformWebViewControllerCreationParams(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, extendBodyBehindAppBar: true, // appBar: myAppBar(context: context), body: _buildBody(), ); } Widget _buildBody() { if (_errorMessage != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( '页面加载失败\n$_errorMessage', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 14), ), ), ); } final controller = _controller; if (controller == null) { return const Center(child: CircularProgressIndicator()); } return PopScope( canPop: widget.canPop ?? true, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; final ok = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("提示"), content: const Text("是否返回上一页?"), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text("取消"), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: const Text("确定"), ), ], ), ); if (ok == true) { AppNavigator.pop(); } }, child: Scaffold( body: Column( children: [ SizedBox(height: 48), Expanded( child: Stack( children: [ Positioned.fill(child: WebViewWidget(controller: controller)), if (_isLoading) const Center(child: CircularProgressIndicator()), ], ), ), ], ), ), ); } }