import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../theme/app_icons.dart'; class TabShell extends StatefulWidget { const TabShell({super.key, required this.child, required this.location}); final Widget child; final String location; @override State createState() => _TabShellState(); } class _TabShellState extends State { static const double _compactHeaderThreshold = 34; bool _showCompactHeader = false; static const _tabs = ['/assets', '/market', '/news', '/profile']; int get _index => _tabs.indexWhere((t) => widget.location.startsWith(t)).clamp(0, 3); @override void didUpdateWidget(covariant TabShell oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.location != widget.location && _showCompactHeader) { _showCompactHeader = false; } } @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final header = _headerForPath(widget.location); final canPop = GoRouter.of(context).canPop(); return Scaffold( backgroundColor: cs.background, 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.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), 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( onNotification: _handleScrollNotification, child: Stack(children: [Positioned.fill(child: widget.child)]), ), ), bottomNavigationBar: DecoratedBox( decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: cs.border, width: 1)), ), child: SafeArea( top: false, child: SizedBox( height: 62, child: Row( children: [ for (var i = 0; i < _tabs.length; i++) Expanded( child: _TabButton( icon: i == _index ? _filledIcons[i] : _outlineIcons[i], label: _labels[i], selected: i == _index, onTap: () => context.go(_tabs[i]), ), ), ], ), ), ), ), ); } bool _handleScrollNotification(ScrollNotification notification) { if (notification.metrics.axis != Axis.vertical) { return false; } final next = notification.metrics.pixels > _compactHeaderThreshold; if (next != _showCompactHeader && mounted) { setState(() => _showCompactHeader = next); } return false; } } class _TabButton extends StatelessWidget { const _TabButton({ required this.icon, required this.label, required this.selected, required this.onTap, }); final IconData icon; final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final color = selected ? cs.primary : cs.mutedForeground; return InkWell( onTap: onTap, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (selected) Container( width: 22, height: 2.5, margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( color: cs.primary, borderRadius: const BorderRadius.vertical( bottom: Radius.circular(3), ), ), ) else const SizedBox(height: 8.5), Icon(icon, size: 22, color: color), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 11, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, color: color, letterSpacing: 0, ), ), ], ), ); } } const _filledIcons = [ AppIcons.assetsFill, AppIcons.marketFill, AppIcons.newsFill, AppIcons.meFill, ]; const _outlineIcons = [ AppIcons.assetsLine, AppIcons.marketLine, AppIcons.newsLine, AppIcons.meLine, ]; const _labels = ['资产', '行情', '资讯', '我的']; _ShellHeader _headerForPath(String path) { return switch (path) { '/market' => const _ShellHeader('行情', ''), '/news' => const _ShellHeader('资讯', ''), '/profile' => const _ShellHeader('我的'), _ => const _ShellHeader('金值', ''), }; } class _ShellHeader { const _ShellHeader(this.title, [this.subtitle]); final String title; final String? subtitle; }