Files
nj/app/lib/widgets/responsive_scaffold.dart
iven ee5ce9bc56 feat(app): 初始化 Flutter 前端项目 (Phase F0)
- Flutter 3.44.0 + Dart 3.12.0
- 设计系统: 7色Token×浅/深模式, NotoSansSC/Caveat字体, 圆角10/16/22/28/pill, 三级阴影
- ResponsiveScaffold: 手机底部TabBar / 平板侧边Rail / 桌面三栏
- go_router 路由表: 13个页面 (5个Tab + 8个全屏页面)
- 13个功能模块占位页面 (home/calendar/mood/search/profile/editor/auth/class/teacher/parent/achievement/stickers/templates)
- 依赖: flutter_bloc, go_router, freezed, isar, dio, perfect_freehand, fl_chart
- 中国镜像: PUB_HOSTED_URL + FLUTTER_STORAGE_BASE_URL
- flutter analyze: No issues found
2026-06-01 00:17:16 +08:00

271 lines
7.4 KiB
Dart

// 暖记响应式骨架 — 三级自适应布局
// 手机: 底部 TabBar | 平板: 侧边导航 | 桌面: 三栏
import 'package:flutter/material.dart';
import '../core/constants/breakpoints.dart';
/// 暖记自适应 Scaffold
class ResponsiveScaffold extends StatefulWidget {
const ResponsiveScaffold({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
required this.body,
this.floatingActionButton,
this.appBarTitle,
this.secondaryBody,
});
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final Widget body;
final Widget? floatingActionButton;
final String? appBarTitle;
final Widget? secondaryBody;
@override
State<ResponsiveScaffold> createState() => _ResponsiveScaffoldState();
}
class _ResponsiveScaffoldState extends State<ResponsiveScaffold> {
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = Breakpoints.getDeviceType(width);
switch (deviceType) {
case DeviceType.mobile:
return _MobileLayout(
selectedIndex: widget.selectedIndex,
onDestinationSelected: widget.onDestinationSelected,
body: widget.body,
floatingActionButton: widget.floatingActionButton,
appBarTitle: widget.appBarTitle,
);
case DeviceType.tablet:
return _TabletLayout(
selectedIndex: widget.selectedIndex,
onDestinationSelected: widget.onDestinationSelected,
body: widget.body,
appBarTitle: widget.appBarTitle,
);
case DeviceType.desktop:
return _DesktopLayout(
selectedIndex: widget.selectedIndex,
onDestinationSelected: widget.onDestinationSelected,
body: widget.body,
secondaryBody: widget.secondaryBody,
appBarTitle: widget.appBarTitle,
);
}
}
}
// ===== 导航项定义 =====
final _navItems = [
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: const Icon(Icons.calendar_month_outlined),
selectedIcon: const Icon(Icons.calendar_month),
label: '日历',
),
NavigationDestination(
icon: const Icon(Icons.auto_awesome_outlined),
selectedIcon: const Icon(Icons.auto_awesome),
label: '心情',
),
NavigationDestination(
icon: const Icon(Icons.search_outlined),
selectedIcon: const Icon(Icons.search),
label: '搜索',
),
NavigationDestination(
icon: const Icon(Icons.person_outline),
selectedIcon: const Icon(Icons.person),
label: '我的',
),
];
const _railItems = [
NavigationRailDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: Text('首页'),
),
NavigationRailDestination(
icon: Icon(Icons.calendar_month_outlined),
selectedIcon: Icon(Icons.calendar_month),
label: Text('日历'),
),
NavigationRailDestination(
icon: Icon(Icons.auto_awesome_outlined),
selectedIcon: Icon(Icons.auto_awesome),
label: Text('心情'),
),
NavigationRailDestination(
icon: Icon(Icons.search_outlined),
selectedIcon: Icon(Icons.search),
label: Text('搜索'),
),
NavigationRailDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: Text('我的'),
),
];
// ===== 手机布局 — 底部 TabBar =====
class _MobileLayout extends StatelessWidget {
const _MobileLayout({
required this.selectedIndex,
required this.onDestinationSelected,
required this.body,
this.floatingActionButton,
this.appBarTitle,
});
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final Widget body;
final Widget? floatingActionButton;
final String? appBarTitle;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBarTitle != null
? AppBar(title: Text(appBarTitle!))
: null,
body: body,
floatingActionButton: floatingActionButton,
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: _navItems,
),
);
}
}
// ===== 平板布局 — 侧边 NavigationRail =====
class _TabletLayout extends StatelessWidget {
const _TabletLayout({
required this.selectedIndex,
required this.onDestinationSelected,
required this.body,
this.appBarTitle,
});
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final Widget body;
final String? appBarTitle;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBarTitle != null
? AppBar(title: Text(appBarTitle!))
: null,
body: Row(
children: [
NavigationRail(
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: _railItems,
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Icon(
Icons.edit_note_rounded,
size: 32,
color: Theme.of(context).colorScheme.primary,
),
),
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(child: body),
],
),
);
}
}
// ===== 桌面布局 — 三栏 =====
class _DesktopLayout extends StatelessWidget {
const _DesktopLayout({
required this.selectedIndex,
required this.onDestinationSelected,
required this.body,
this.secondaryBody,
this.appBarTitle,
});
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final Widget body;
final Widget? secondaryBody;
final String? appBarTitle;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBarTitle != null
? AppBar(title: Text(appBarTitle!))
: null,
body: Row(
children: [
NavigationRail(
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: _railItems,
extended: true,
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.edit_note_rounded,
size: 32,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Text(
'暖记',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontFamily: 'Caveat',
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
const VerticalDivider(thickness: 1, width: 1),
// 主内容区
Expanded(
flex: 3,
child: body,
),
// 第二面板(日记详情/预览)
if (secondaryBody != null) ...[
const VerticalDivider(thickness: 1, width: 1),
Expanded(
flex: 2,
child: secondaryBody!,
),
],
],
),
);
}
}