fix:对齐金值的报名验证,商城入口逻辑对齐
This commit is contained in:
1 parent
3b089e2205
commit
7b1852d9a6
20 files changed
+682
-116
No files matched your search
@@ -9,13 +9,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../features/mall/application/mall_entry_controller.dart';
|
||||
import '../../features/mall/application/mall_entry_promo_store.dart';
|
||||
import '../../features/mall/application/mall_tab_config.dart';
|
||||
import '../../features/mall/presentation/widgets/mall_entry_promo.dart';
|
||||
import '../../features/ads/application/startup_ad_service.dart';
|
||||
import '../../features/web/application/mall_bridge_state.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/app_toast.dart';
|
||||
import 'app_router.dart';
|
||||
import '../storage/storage_service.dart';
|
||||
|
||||
class TabShell extends ConsumerStatefulWidget {
|
||||
const TabShell({
|
||||
@@ -47,6 +51,10 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
late final AnimationController _mallTransition;
|
||||
late final CurvedAnimation _mallProgress;
|
||||
late final ProviderSubscription<MallTabConfig> _mallConfigSubscription;
|
||||
late final MallEntryPromoStore _mallEntryPromoStore;
|
||||
late bool _mallEntryTipConsumed;
|
||||
late bool _mallHomeBadgeSeen;
|
||||
bool _showMallHomeBadgeForPresentation = false;
|
||||
bool _userScrollSession = false;
|
||||
ScrollDirection _userScrollDirection = ScrollDirection.idle;
|
||||
double _directionAccum = 0;
|
||||
@@ -57,6 +65,9 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mallEntryPromoStore = MallEntryPromoStore(StorageService.to.preferences);
|
||||
_mallEntryTipConsumed = _mallEntryPromoStore.hasOpenedMall;
|
||||
_mallHomeBadgeSeen = _mallEntryPromoStore.hasSeenHomeBadge;
|
||||
MallWebHostController.instance.state.addListener(_syncMallHost);
|
||||
MallBridgeController.instance.state.addListener(_refreshMallDock);
|
||||
_minimizeController = AnimationController(
|
||||
@@ -130,10 +141,22 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
final state = MallWebHostController.instance.state.value;
|
||||
if (!mounted) return;
|
||||
final changed = _mallActive != state.visible;
|
||||
final showHomeBadge =
|
||||
changed &&
|
||||
state.visible &&
|
||||
!_mallHomeBadgeSeen &&
|
||||
ref.read(mallTabConfigProvider).showsEntryBadge;
|
||||
setState(() {
|
||||
_mallPresented |= state.url != null;
|
||||
_mallActive = state.visible;
|
||||
if (changed) {
|
||||
_showMallHomeBadgeForPresentation = showHomeBadge;
|
||||
if (showHomeBadge) _mallHomeBadgeSeen = true;
|
||||
}
|
||||
});
|
||||
if (showHomeBadge) {
|
||||
unawaited(_mallEntryPromoStore.markHomeBadgeSeen());
|
||||
}
|
||||
if (changed) {
|
||||
if (state.visible) {
|
||||
_mallTransition.forward();
|
||||
@@ -148,6 +171,8 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
}
|
||||
|
||||
Future<void> _openMall(MallTabConfig config) async {
|
||||
await _consumeMallEntryTip(config);
|
||||
if (!mounted) return;
|
||||
final result = await ref
|
||||
.read(mallEntryControllerProvider)
|
||||
.open(context, initialConfig: config);
|
||||
@@ -159,6 +184,20 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _consumeMallEntryTip(MallTabConfig config) async {
|
||||
if (!config.showsEntryTip || _mallEntryTipConsumed) return;
|
||||
setState(() => _mallEntryTipConsumed = true);
|
||||
await _mallEntryPromoStore.markMallOpened();
|
||||
}
|
||||
|
||||
bool get _isPrimaryTabRoot => switch (widget.location) {
|
||||
AppRoutes.assets ||
|
||||
AppRoutes.market ||
|
||||
AppRoutes.news ||
|
||||
AppRoutes.profile => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TabShell oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -173,6 +212,7 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
final mall = ref.watch(mallTabConfigProvider);
|
||||
final startupAdResolved = ref.watch(startupAdResolvedProvider);
|
||||
final isQiyuKeyboardActive =
|
||||
MallWebHostController.instance.state.value.isQiyuKeyboardActive;
|
||||
final header = _headerForPath(widget.location);
|
||||
@@ -312,6 +352,14 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
minimize: _minimizeFactor,
|
||||
currentIndex: _index,
|
||||
mall: mall,
|
||||
showMallEntryTip:
|
||||
startupAdResolved &&
|
||||
_isPrimaryTabRoot &&
|
||||
!_mallActive &&
|
||||
!_mallEntryTipConsumed &&
|
||||
mall.showsEntryTip,
|
||||
showMallHomeBadge:
|
||||
_showMallHomeBadgeForPresentation && mall.showsEntryBadge,
|
||||
onTabTap: (index) => widget.navigationShell.goBranch(
|
||||
index,
|
||||
initialLocation: index == _index,
|
||||
@@ -437,6 +485,8 @@ class _MorphingNavigationDock extends StatelessWidget {
|
||||
required this.minimize,
|
||||
required this.currentIndex,
|
||||
required this.mall,
|
||||
required this.showMallEntryTip,
|
||||
required this.showMallHomeBadge,
|
||||
required this.onTabTap,
|
||||
required this.onMallTap,
|
||||
});
|
||||
@@ -445,6 +495,8 @@ class _MorphingNavigationDock extends StatelessWidget {
|
||||
final Animation<double> minimize;
|
||||
final int currentIndex;
|
||||
final MallTabConfig mall;
|
||||
final bool showMallEntryTip;
|
||||
final bool showMallHomeBadge;
|
||||
final ValueChanged<int> onTabTap;
|
||||
final VoidCallback onMallTap;
|
||||
|
||||
@@ -545,6 +597,7 @@ class _MorphingNavigationDock extends StatelessWidget {
|
||||
opacity: (1 - t * 2).clamp(0.0, 1.0),
|
||||
child: _MallLauncherButton(
|
||||
mall: mall,
|
||||
showPromoTip: showMallEntryTip,
|
||||
onTap: onMallTap,
|
||||
),
|
||||
),
|
||||
@@ -570,6 +623,9 @@ class _MorphingNavigationDock extends StatelessWidget {
|
||||
.tabs[index]
|
||||
.$1,
|
||||
),
|
||||
promoBadgeIndex: 0,
|
||||
promoBadgeVisible: showMallHomeBadge,
|
||||
promoBadgeLabel: mall.entryBadgeText,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -591,37 +647,48 @@ class _MorphingNavigationDock extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _MallLauncherButton extends StatelessWidget {
|
||||
const _MallLauncherButton({required this.mall, required this.onTap});
|
||||
const _MallLauncherButton({
|
||||
required this.mall,
|
||||
required this.showPromoTip,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final MallTabConfig mall;
|
||||
final bool showPromoTip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: mall.title,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [cs.primary.withValues(alpha: 0.78), cs.primary],
|
||||
return MallEntryPromoTooltip(
|
||||
visible: showPromoTip,
|
||||
title: mall.entryTipTitle,
|
||||
body: mall.entryTipBody,
|
||||
onCardTap: onTap,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: mall.title,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [cs.primary.withValues(alpha: 0.78), cs.primary],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.shopping_bag,
|
||||
size: 25,
|
||||
color: cs.primaryForeground,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.shopping_bag,
|
||||
size: 25,
|
||||
color: cs.primaryForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -651,6 +718,9 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
||||
required this.selectedIndex,
|
||||
required this.onTap,
|
||||
this.minimize = 0,
|
||||
this.promoBadgeIndex,
|
||||
this.promoBadgeVisible = false,
|
||||
this.promoBadgeLabel = '',
|
||||
});
|
||||
|
||||
static const double _inset = 6;
|
||||
@@ -660,6 +730,9 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
final double minimize;
|
||||
final int? promoBadgeIndex;
|
||||
final bool promoBadgeVisible;
|
||||
final String promoBadgeLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -676,6 +749,7 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
||||
final indicatorLeft = _inset + safeIndex * compactWidth;
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 260),
|
||||
@@ -697,13 +771,19 @@ class _ExpandingDockTabStrip extends StatelessWidget {
|
||||
duration: const Duration(milliseconds: 260),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: index == safeIndex ? selectedWidth : compactWidth,
|
||||
child: _ExpandingDockTab(
|
||||
item: items[index],
|
||||
selected: index == safeIndex,
|
||||
labelFactor: index == safeIndex
|
||||
? (1 - minimize).clamp(0.0, 1.0)
|
||||
: 0,
|
||||
onTap: () => onTap(index),
|
||||
child: MallEntryPromoBadge(
|
||||
visible: index == promoBadgeIndex && promoBadgeVisible,
|
||||
label: promoBadgeLabel,
|
||||
top: -2,
|
||||
right: 0,
|
||||
child: _ExpandingDockTab(
|
||||
item: items[index],
|
||||
selected: index == safeIndex,
|
||||
labelFactor: index == safeIndex
|
||||
? (1 - minimize).clamp(0.0, 1.0)
|
||||
: 0,
|
||||
onTap: () => onTap(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -859,19 +939,28 @@ class _MallDockSurface extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.background.withValues(alpha: 0.82),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
// Clip only the frosted background. The mall promotion badge is
|
||||
// intentionally positioned above the pill and must remain visible.
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
border: Border.all(color: cs.border.withValues(alpha: 0.8)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.background.withValues(alpha: 0.82),
|
||||
borderRadius: BorderRadius.circular(height / 2),
|
||||
border: Border.all(color: cs.border.withValues(alpha: 0.8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user