212 lines
5.8 KiB
Dart
212 lines
5.8 KiB
Dart
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<TabShell> createState() => _TabShellState();
|
|
}
|
|
|
|
class _TabShellState extends State<TabShell> {
|
|
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<ScrollNotification>(
|
|
onNotification: _handleScrollNotification,
|
|
child: Stack(children: [Positioned.fill(child: widget.child)]),
|
|
),
|
|
),
|
|
bottomNavigationBar: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: cs.background,
|
|
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.foreground : cs.mutedForeground;
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
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;
|
|
}
|