import 'dart:async'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../application/mall_web_host_controller.dart'; /// 常驻在 Tab Shell 上层的商城 H5 宿主。 class MallWebHost extends StatefulWidget { const MallWebHost({super.key, required this.onExit}); final VoidCallback onExit; @override State createState() => _MallWebHostState(); } class _MallWebHostState extends State { final _host = MallWebHostController.instance; WebViewController? _controller; String? _loadedUrl; String? _error; int _progress = 0; @override void initState() { super.initState(); _host.state.addListener(_sync); _host.attachBackHandler(_goBack); _sync(); } @override void dispose() { _host.state.removeListener(_sync); _host.detachBackHandler(); super.dispose(); } void _sync() { final url = _host.state.value.url; if (url != null && url != _loadedUrl) _load(url); if (mounted) setState(() {}); } void _load(String rawUrl) { final uri = Uri.tryParse(rawUrl); if (uri == null || (uri.scheme != 'https' && uri.scheme != 'http')) { _error = '商城链接不可用'; return; } _error = null; _progress = 0; _loadedUrl = rawUrl; _controller ??= WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( 'JinzhiBridge', onMessageReceived: (message) { if (message.message == 'close') widget.onExit(); }, ) ..setNavigationDelegate( NavigationDelegate( onProgress: (value) { if (mounted) setState(() => _progress = value); }, onWebResourceError: (error) { if (error.isForMainFrame == false) return; if (mounted) setState(() => _error = error.description); }, onNavigationRequest: (request) async { final target = Uri.tryParse(request.url); if (target == null || target.scheme == 'http' || target.scheme == 'https') { return NavigationDecision.navigate; } if (await canLaunchUrl(target)) { await launchUrl(target, mode: LaunchMode.externalApplication); } return NavigationDecision.prevent; }, ), ); unawaited(_controller!.loadRequest(uri)); } Future _goBack() async { final controller = _controller; if (controller != null && await controller.canGoBack()) { await controller.goBack(); } } @override Widget build(BuildContext context) { final current = _host.state.value; if (current.url == null) return const SizedBox.shrink(); return ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, child: Stack( children: [ if (_controller != null) Positioned.fill(child: WebViewWidget(controller: _controller!)), if (_error != null) Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.wifi_off_rounded, size: 32), const SizedBox(height: 12), const Text('商城加载失败'), const SizedBox(height: 8), TextButton( onPressed: () => _load(_loadedUrl ?? ''), child: const Text('重新加载'), ), ], ), ), if (_error == null && _progress < 100) const Align( alignment: Alignment.topCenter, child: LinearProgressIndicator(minHeight: 2), ), ], ), ); } }