132 lines
3.8 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|