import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// Shared HTTPS WebView host. Mall-specific residency and attribution are /// layered above this page so ordinary H5 links never receive mall state. class AppWebPage extends StatefulWidget { const AppWebPage({ super.key, required this.initialUri, this.hideTitle = false, this.allowedHosts = const {}, }); final Uri initialUri; final bool hideTitle; final Set allowedHosts; @override State createState() => _AppWebPageState(); } class _AppWebPageState extends State { late final WebViewController _controller; var _progress = 0; String? _error; String _title = ''; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( 'JinzhiBridge', onMessageReceived: (message) { if (message.message == 'close' && mounted) context.pop(); }, ) ..setNavigationDelegate( NavigationDelegate( onProgress: (progress) => setState(() => _progress = progress), onPageFinished: (_) async { final title = await _controller.getTitle(); if (mounted && title != null && title.trim().isNotEmpty) { setState(() => _title = title.trim()); } }, onWebResourceError: (error) { if (error.isForMainFrame == true && mounted) { setState(() => _error = error.description); } }, onNavigationRequest: _onNavigationRequest, ), ) ..loadRequest(widget.initialUri); } NavigationDecision _onNavigationRequest(NavigationRequest request) { final uri = Uri.tryParse(request.url); if (uri == null || !_isHttp(uri)) return NavigationDecision.prevent; if (_allowsInApp(uri)) return NavigationDecision.navigate; launchUrl(uri, mode: LaunchMode.externalApplication); return NavigationDecision.prevent; } bool _allowsInApp(Uri uri) { final host = uri.host.toLowerCase(); if (host == widget.initialUri.host.toLowerCase()) return true; return widget.allowedHosts.any( (allowed) => host == allowed || host.endsWith('.$allowed'), ); } bool _isHttp(Uri uri) => uri.hasAuthority && (uri.scheme == 'https' || uri.scheme == 'http'); Future _onPop() async { if (await _controller.canGoBack()) { await _controller.goBack(); return false; } return true; } @override Widget build(BuildContext context) { final body = _error == null ? Stack( children: [ WebViewWidget(controller: _controller), if (_progress < 100) LinearProgressIndicator(value: _progress / 100), ], ) : Center( child: Padding( padding: const EdgeInsets.all(24), child: Text('页面加载失败\n$_error', textAlign: TextAlign.center), ), ); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) async { if (!didPop && await _onPop() && context.mounted) context.pop(); }, child: Scaffold( appBar: widget.hideTitle ? null : AppBar(title: Text(_title.isEmpty ? '加载中' : _title)), body: body, ), ); } } Uri? parseSafeWebUri(String? raw) { final uri = Uri.tryParse(raw?.trim() ?? ''); if (uri == null || !uri.hasAuthority) return null; return uri.scheme == 'https' || uri.scheme == 'http' ? uri : null; }