feat: 添加 H5 线路切换功能;支持动态加载和切换不同线路的 URL
This commit is contained in:
293
lib/main.dart
293
lib/main.dart
@@ -94,17 +94,22 @@ class H5ShellPage extends StatefulWidget {
|
||||
|
||||
class _H5ShellPageState extends State<H5ShellPage> {
|
||||
late final WebViewController _controller;
|
||||
late final List<H5Line> _h5Lines;
|
||||
|
||||
int _progress = 0;
|
||||
String? _loadError;
|
||||
bool _showShellCover = true;
|
||||
bool _shellBrandingLoaded = false;
|
||||
int _currentLineIndex = 0;
|
||||
Timer? _shellCoverFallbackTimer;
|
||||
late ShellBranding _shellBranding;
|
||||
|
||||
H5Line get _currentLine => _h5Lines[_currentLineIndex];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_h5Lines = AppConfig.h5Lines;
|
||||
_shellBranding = widget.initialShellBranding;
|
||||
_controller = _buildController();
|
||||
unawaited(_loadHome());
|
||||
@@ -125,10 +130,14 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
setState(() => _progress = progress);
|
||||
}
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
onPageStarted: (url) {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
final lineIndex = AppConfig.lineIndexForUrl(url);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (lineIndex != null) {
|
||||
_currentLineIndex = lineIndex;
|
||||
}
|
||||
_loadError = null;
|
||||
_progress = 0;
|
||||
_showShellCover = true;
|
||||
@@ -169,7 +178,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
}
|
||||
|
||||
String _freshHomeUrl() {
|
||||
return AppConfig.homeUrl();
|
||||
return AppConfig.homeUrl(lineIndex: _currentLineIndex);
|
||||
}
|
||||
|
||||
Future<void> _handlePageFinished() async {
|
||||
@@ -256,10 +265,14 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
|
||||
Future<void> _loadUrl(String url) async {
|
||||
final targetUrl = AppConfig.canonicalizeMainFrameUrl(url);
|
||||
final lineIndex = AppConfig.lineIndexForUrl(targetUrl);
|
||||
|
||||
if (mounted) {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
setState(() {
|
||||
if (lineIndex != null) {
|
||||
_currentLineIndex = lineIndex;
|
||||
}
|
||||
_loadError = null;
|
||||
_progress = 0;
|
||||
_showShellCover = true;
|
||||
@@ -269,6 +282,41 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
await _controller.loadRequest(Uri.parse(targetUrl));
|
||||
}
|
||||
|
||||
Future<void> _loadLine(int index) async {
|
||||
final safeIndex = index < 0 || index >= _h5Lines.length ? 0 : index;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentLineIndex = safeIndex;
|
||||
});
|
||||
}
|
||||
|
||||
await _loadUrl(_h5Lines[safeIndex].url);
|
||||
}
|
||||
|
||||
void _showLineSwitcher() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return _LineSwitcherSheet(
|
||||
lines: _h5Lines,
|
||||
currentIndex: _currentLineIndex,
|
||||
onSelect: (index) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
if (index != _currentLineIndex) {
|
||||
unawaited(_loadLine(index));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<NavigationDecision> _handleNavigationRequest(
|
||||
NavigationRequest request,
|
||||
) async {
|
||||
@@ -306,6 +354,10 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
|
||||
final showLineSwitch =
|
||||
!_showShellCover && _loadError == null && bottomInset == 0;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
@@ -337,6 +389,16 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
message: _loadError!,
|
||||
onRetry: () => unawaited(_loadHome()),
|
||||
),
|
||||
if (showLineSwitch)
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 14,
|
||||
child: _LineSwitchBar(
|
||||
currentLine: _currentLine,
|
||||
onSwitch: _showLineSwitcher,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -386,6 +448,233 @@ class _ErrorPanel extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _LineSwitchBar extends StatelessWidget {
|
||||
const _LineSwitchBar({
|
||||
required this.currentLine,
|
||||
required this.onSwitch,
|
||||
});
|
||||
|
||||
final H5Line currentLine;
|
||||
final VoidCallback onSwitch;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Material(
|
||||
color: Colors.white.withValues(alpha: 0.94),
|
||||
elevation: 8,
|
||||
shadowColor: const Color(0xFF0F4C81).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: InkWell(
|
||||
onTap: onSwitch,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(color: const Color(0xFFE1EAF5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.sync_rounded,
|
||||
size: 19,
|
||||
color: _shellSubText,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'当前: ',
|
||||
style: TextStyle(
|
||||
color: _shellSubText,
|
||||
fontSize: 15,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
currentLine.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF17233D),
|
||||
fontSize: 16,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'切换',
|
||||
style: TextStyle(
|
||||
color: _shellAccent,
|
||||
fontSize: 15,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LineSwitcherSheet extends StatelessWidget {
|
||||
const _LineSwitcherSheet({
|
||||
required this.lines,
|
||||
required this.currentIndex,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
final List<H5Line> lines;
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onSelect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sheetHeight = MediaQuery.sizeOf(context).height * 0.58;
|
||||
final maxHeight = sheetHeight > 420 ? 420.0 : sheetHeight;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'切换线路',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF17233D),
|
||||
fontSize: 18,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: lines.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final line = lines[index];
|
||||
final selected = index == currentIndex;
|
||||
|
||||
return _LineOptionTile(
|
||||
line: line,
|
||||
selected: selected,
|
||||
onTap: () => onSelect(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LineOptionTile extends StatelessWidget {
|
||||
const _LineOptionTile({
|
||||
required this.line,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final H5Line line;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: selected ? const Color(0xFFEFF7FF) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 64),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: selected ? _shellAccent : const Color(0xFFE1EAF5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
selected
|
||||
? Icons.radio_button_checked_rounded
|
||||
: Icons.radio_button_unchecked_rounded,
|
||||
color: selected ? _shellAccent : _shellSubText,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
line.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF17233D),
|
||||
fontSize: 16,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
line.displayAddress,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: _shellSubText,
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: _shellSubText,
|
||||
size: 22,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShellFallback extends StatelessWidget {
|
||||
const _ShellFallback({required this.progress});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user