Files
jinzhi/lib/features/web/presentation/mall_web_host.dart
T
2026-08-24 19:07:31 +08:00

132 lines
3.8 KiB
Dart

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<MallWebHost> createState() => _MallWebHostState();
}
class _MallWebHostState extends State<MallWebHost> {
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<void> _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),
),
],
),
);
}
}