Files
2026-06-18 15:02:28 +08:00

256 lines
7.9 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.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';
import '../widgets/adaptive.dart';
import 'app_router.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,
body: Stack(
children: [
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: 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 < _tabs.length; i++)
Expanded(
child: _TabButton(
icon: i == _index
? _filledIcons[i]
: _outlineIcons[i],
label: _labels[i],
selected: i == _index,
onTap: () => context.go(_tabs[i]),
),
),
],
),
),
),
),
),
],
),
),
),
if (!kReleaseMode)
Positioned(
right: 16,
bottom: 88,
child: FilledButton(
onPressed: () => context.push(AppRoutes.fancyDioInspector),
style: FilledButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: cs.primaryForeground,
minimumSize: const Size(60, 60),
shape: const CircleBorder(),
padding: EdgeInsets.zero,
),
child: const Text('请求'),
),
),
],
),
);
}
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;
}