fix:第一轮跑

This commit is contained in:
jingyun committed 2026-08-24 19:07:31 +08:00
1 parent e834ec7586
commit 33235f450b
56 files changed
+4357 -210

No files matched your search

+226 -90
View File
@@ -1,13 +1,18 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../features/mall/application/mall_tab_config.dart';
import '../../features/web/application/mall_web_host_controller.dart';
import '../../features/web/presentation/mall_web_host.dart';
import '../theme/app_icons.dart';
import '../widgets/adaptive.dart';
import 'app_router.dart';
class TabShell extends StatefulWidget {
class TabShell extends ConsumerStatefulWidget {
const TabShell({
super.key,
required this.navigationShell,
@@ -18,16 +23,29 @@ class TabShell extends StatefulWidget {
final String location;
@override
State<TabShell> createState() => _TabShellState();
ConsumerState<TabShell> createState() => _TabShellState();
}
class _TabShellState extends State<TabShell> {
class _TabShellState extends ConsumerState<TabShell> {
static const double _compactHeaderThreshold = 34;
bool _showCompactHeader = false;
int get _index => widget.navigationShell.currentIndex;
@override
void initState() {
super.initState();
ref.listenManual<MallTabConfig>(mallTabConfigProvider, (_, next) {
if (!next.isVisible) return;
final uri = next.attributedUri(
clickId: '',
isDark: Theme.of(context).brightness == Brightness.dark,
);
if (uri != null) MallWebHostController.instance.preload(uri.toString());
});
}
@override
void didUpdateWidget(covariant TabShell oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -36,9 +54,28 @@ class _TabShellState extends State<TabShell> {
}
}
Future<void> _openMall(MallTabConfig config) async {
final isDark = Theme.of(context).brightness == Brightness.dark;
final actual = await ref
.read(mallTabConfigProvider.notifier)
.ensureLoaded();
if (!actual.isVisible || !mounted) return;
final clickId = await ref
.read(mallTabConfigProvider.notifier)
.recordClick();
final uri = actual.attributedUri(clickId: clickId ?? '', isDark: isDark);
if (uri == null) return;
if (actual.opensInApp) {
MallWebHostController.instance.open(uri.toString());
return;
}
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final mall = ref.watch(mallTabConfigProvider);
final header = _headerForPath(widget.location);
final canPop = GoRouter.of(context).canPop();
@@ -47,111 +84,144 @@ class _TabShellState extends State<TabShell> {
resizeToAvoidBottomInset: false,
body: Stack(
children: [
Scaffold(
backgroundColor: cs.background,
resizeToAvoidBottomInset: false,
appBar: AppBar(
PopScope(
canPop: !MallWebHostController.instance.state.value.visible,
onPopInvokedWithResult: (didPop, _) {
if (!didPop &&
MallWebHostController.instance.state.value.visible) {
MallWebHostController.instance.handleBack();
}
},
child: Scaffold(
backgroundColor: cs.background,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
toolbarHeight: 32,
leading: canPop
? IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.chevron_left, size: 20),
)
: null,
title: AnimatedOpacity(
opacity: _showCompactHeader ? 1 : 0,
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
header.title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: cs.foreground,
height: 1.1,
letterSpacing: 0,
),
),
if (header.subtitle != null) ...[
const SizedBox(height: 2),
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: cs.background,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
toolbarHeight: 32,
leading: canPop
? IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.chevron_left, size: 20),
)
: null,
title: AnimatedOpacity(
opacity: _showCompactHeader ? 1 : 0,
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
header.subtitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
header.title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: cs.mutedForeground,
fontSize: 15,
fontWeight: FontWeight.w700,
color: cs.foreground,
height: 1.1,
letterSpacing: 0,
),
),
if (header.subtitle != null) ...[
const SizedBox(height: 2),
Text(
header.subtitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: cs.mutedForeground,
height: 1.1,
letterSpacing: 0,
),
),
],
],
],
),
),
),
),
body: ColoredBox(
color: cs.background,
child: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: widget.navigationShell,
body: ColoredBox(
color: cs.background,
child: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: widget.navigationShell,
),
),
),
bottomNavigationBar: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: cs.background,
border: Border(
top: BorderSide(color: cs.border, width: 1),
bottomNavigationBar: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: cs.background,
border: Border(
top: BorderSide(color: cs.border, width: 1),
),
),
),
child: SizedBox(
height: 62,
// 平板:tab 行限宽 600 居中(demo .nav .tabs)
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: Adaptive.tabsMaxWidth,
),
child: Row(
children: [
for (var i = 0; i < _labels.length; i++)
Expanded(
child: _TabButton(
icon: i == _index
? _filledIcons[i]
: _outlineIcons[i],
label: _labels[i],
selected: i == _index,
onTap: () =>
widget.navigationShell.goBranch(
i,
initialLocation: i == _index,
),
child: SizedBox(
height: 62,
// 平板:tab 行限宽 600 居中(demo .nav .tabs)
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: Adaptive.tabsMaxWidth,
),
child: Row(
children: [
for (var i = 0; i < _labels.length; i++)
Expanded(
child: _TabButton(
icon: i == _index
? _filledIcons[i]
: _outlineIcons[i],
label: _labels[i],
selected: i == _index,
onTap: () =>
widget.navigationShell.goBranch(
i,
initialLocation: i == _index,
),
),
),
),
],
],
),
),
),
),
),
),
],
],
),
),
),
),
if (mall.isVisible)
Positioned(
bottom: 39,
left: 0,
right: 0,
child: Center(
child: _MallDockButton(
title: mall.title,
badge: mall.badgeText,
onTap: () => _openMall(mall),
),
),
),
ValueListenableBuilder<MallWebHostState>(
valueListenable: MallWebHostController.instance.state,
builder: (context, state, _) => Positioned.fill(
child: Offstage(
offstage: !state.visible,
child: MallWebHost(
onExit: MallWebHostController.instance.dismiss,
),
),
),
),
@@ -188,6 +258,72 @@ class _TabShellState extends State<TabShell> {
}
}
class _MallDockButton extends StatelessWidget {
const _MallDockButton({
required this.title,
required this.badge,
required this.onTap,
});
final String title;
final String badge;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return Semantics(
button: true,
label: title,
child: Material(
color: cs.primary,
borderRadius: BorderRadius.circular(24),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(24),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.shopping_bag_outlined,
size: 18,
color: cs.primaryForeground,
),
const SizedBox(width: 6),
Text(
title,
style: TextStyle(
color: cs.primaryForeground,
fontWeight: FontWeight.w700,
),
),
if (badge.isNotEmpty) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 1,
),
decoration: BoxDecoration(
color: cs.background,
borderRadius: BorderRadius.circular(8),
),
child: Text(
badge,
style: TextStyle(fontSize: 10, color: cs.foreground),
),
),
],
],
),
),
),
),
);
}
}
class _TabButton extends StatelessWidget {
const _TabButton({
required this.icon,