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
This commit is contained in:
iven
2026-06-01 00:17:16 +08:00
parent 3d9896a676
commit ee5ce9bc56
90 changed files with 3933 additions and 0 deletions

19
app/lib/app.dart Normal file
View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'core/theme/app_theme.dart';
import 'core/routing/app_router.dart';
class NuanjiApp extends StatelessWidget {
const NuanjiApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: '暖记',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: ThemeMode.system,
routerConfig: appRouter,
);
}
}

View File

@@ -0,0 +1,24 @@
// 暖记响应式断点
// 手机 < 600 | 平板 600-1024 | 桌面 > 1024
class Breakpoints {
Breakpoints._();
/// 手机最大宽度
static const double mobile = 600;
/// 平板最大宽度
static const double tablet = 1024;
/// 触摸目标最小尺寸 (WCAG + Apple HIG)
static const double touchTarget = 44;
/// 判断设备类型
static DeviceType getDeviceType(double width) {
if (width < mobile) return DeviceType.mobile;
if (width < tablet) return DeviceType.tablet;
return DeviceType.desktop;
}
}
enum DeviceType { mobile, tablet, desktop }

View File

@@ -0,0 +1,41 @@
// 暖记设计常量 — 间距 / 动画 / 触摸目标
import 'package:flutter/animation.dart';
class DesignTokens {
DesignTokens._();
// ===== 间距 =====
static const double spacing4 = 4;
static const double spacing8 = 8;
static const double spacing12 = 12;
static const double spacing16 = 16;
static const double spacing20 = 20;
static const double spacing24 = 24;
static const double spacing32 = 32;
static const double spacing48 = 48;
// ===== 动画时长 =====
static const Duration animFast = Duration(milliseconds: 150);
static const Duration animNormal = Duration(milliseconds: 300);
static const Duration animSlow = Duration(milliseconds: 500);
// ===== 弹性曲线 cubic-bezier(0.34, 1.56, 0.64, 1) =====
static const Curve warmCurve = Curves.easeOutBack;
// ===== 列表/网格 =====
static const int journalGridCrossAxisCountMobile = 2;
static const int journalGridCrossAxisCountTablet = 3;
static const int journalGridCrossAxisCountDesktop = 4;
// ===== 日记限制 =====
static const int maxTagsPerJournal = 10;
static const int maxTitleLength = 100;
static const int maxStrokesPerElement = 5000;
static const int maxUndoSteps = 50;
// ===== 班级码 =====
static const int classCodeLength = 6;
static const int classCodeMaxAttempts = 5;
static const int classCodeLockoutMinutes = 30;
}

View File

@@ -0,0 +1,181 @@
// 暖记路由表 — go_router 20 页面
export '../../widgets/responsive_scaffold.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/responsive_scaffold.dart';
import '../../features/home/views/home_page.dart';
import '../../features/calendar/views/calendar_page.dart';
import '../../features/mood/views/mood_page.dart';
import '../../features/search/views/search_page.dart';
import '../../features/profile/views/profile_page.dart';
import '../../features/editor/views/editor_page.dart';
import '../../features/auth/views/login_page.dart';
import '../../features/class_/views/class_page.dart';
import '../../features/teacher/views/teacher_page.dart';
import '../../features/parent/views/parent_page.dart';
import '../../features/achievement/views/achievement_page.dart';
import '../../features/stickers/views/sticker_library_page.dart';
import '../../features/templates/views/template_gallery_page.dart';
// Shell 分支键
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
/// 暖记路由配置
final appRouter = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/home',
debugLogDiagnostics: true,
routes: [
// 认证路由(无 Shell
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
// 主 Shell 路由(底部导航 + 侧边导航)
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
// 根据当前路径计算选中的 tab index
final index = _selectedIndexFromLocation(state.uri.path);
return _AppShell(
selectedIndex: index,
child: child,
);
},
routes: [
// Tab 0: 首页日记流
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
// Tab 1: 日历
GoRoute(
path: '/calendar',
name: 'calendar',
builder: (context, state) => const CalendarPage(),
),
// Tab 2: 心情
GoRoute(
path: '/mood',
name: 'mood',
builder: (context, state) => const MoodPage(),
),
// Tab 3: 搜索
GoRoute(
path: '/search',
name: 'search',
builder: (context, state) => const SearchPage(),
),
// Tab 4: 个人中心
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => const ProfilePage(),
),
],
),
// 全屏页面(无底部导航)
GoRoute(
path: '/editor',
name: 'editor',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) {
final journalId = state.uri.queryParameters['id'];
return EditorPage(journalId: journalId);
},
),
GoRoute(
path: '/class',
name: 'class',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const ClassPage(),
),
GoRoute(
path: '/teacher',
name: 'teacher',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const TeacherPage(),
),
GoRoute(
path: '/parent',
name: 'parent',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const ParentPage(),
),
GoRoute(
path: '/achievements',
name: 'achievements',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const AchievementPage(),
),
GoRoute(
path: '/stickers',
name: 'stickers',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const StickerLibraryPage(),
),
GoRoute(
path: '/templates',
name: 'templates',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const TemplateGalleryPage(),
),
],
);
/// 路径 → Tab index 映射
int _selectedIndexFromLocation(String location) {
if (location.startsWith('/calendar')) return 1;
if (location.startsWith('/mood')) return 2;
if (location.startsWith('/search')) return 3;
if (location.startsWith('/profile')) return 4;
return 0; // 默认首页
}
/// App Shell — 包裹 ResponsiveScaffold
class _AppShell extends StatelessWidget {
const _AppShell({
required this.selectedIndex,
required this.child,
});
final int selectedIndex;
final Widget child;
@override
Widget build(BuildContext context) {
return ResponsiveScaffold(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
switch (index) {
case 0:
context.go('/home');
case 1:
context.go('/calendar');
case 2:
context.go('/mood');
case 3:
context.go('/search');
case 4:
context.go('/profile');
}
},
body: child,
floatingActionButton: selectedIndex == 0
? FloatingActionButton(
onPressed: () => context.go('/editor'),
child: const Icon(Icons.edit_rounded),
)
: null,
);
}
}

View File

@@ -0,0 +1,128 @@
// 暖记色彩系统 — 7 色 × 浅色/深色模式
// 设计规格 v1.2
import 'package:flutter/material.dart';
/// 暖记色彩 Token
class AppColors {
AppColors._();
// ===== 浅色模式 =====
/// 奶油白背景 #FFF8F0
static const Color bgLight = Color(0xFFFFF8F0);
/// 珊瑚色主色 #E07A5F
static const Color accent = Color(0xFFE07A5F);
/// 鼠尾草绿 #81B29A
static const Color secondary = Color(0xFF81B29A);
/// 暖金 #F2CC8F
static const Color tertiary = Color(0xFFF2CC8F);
/// 主文字 #2D2420
static const Color fgLight = Color(0xFF2D2420);
/// 卡片背景 #FFFFFF
static const Color surfaceLight = Color(0xFFFFFFFF);
/// 玫瑰粉 #D4A5A5
static const Color rose = Color(0xFFD4A5A5);
// ===== 深色模式 =====
/// 深色背景 #1A1614
static const Color bgDark = Color(0xFF1A1614);
/// 深色珊瑚 #E8907A
static const Color accentDark = Color(0xFFE8907A);
/// 深色鼠尾草 #8FBF9E
static const Color secondaryDark = Color(0xFF8FBF9E);
/// 深色暖金 #D4B878
static const Color tertiaryDark = Color(0xFFD4B878);
/// 深色主文字 #F0E8DF
static const Color fgDark = Color(0xFFF0E8DF);
/// 深色卡片 #2A2520
static const Color surfaceDark = Color(0xFF2A2520);
/// 深色玫瑰 #C4A0A0
static const Color roseDark = Color(0xFFC4A0A0);
// ===== 功能色 =====
/// 错误红
static const Color error = Color(0xFFD32F2F);
/// 成功绿
static const Color success = Color(0xFF4CAF50);
/// 警告黄
static const Color warning = Color(0xFFFFA726);
/// 信息蓝
static const Color info = Color(0xFF42A5F5);
// ===== 心情颜色映射 =====
/// 心情 → 颜色
static const Map<String, Color> moodColors = {
'happy': Color(0xFFFFD93D), // 😊 开心 — 暖黄
'calm': Color(0xFF81B29A), // 😌 平静 — 鼠尾草绿
'sad': Color(0xFF7B9CC4), // 😢 难过 — 灰蓝
'angry': Color(0xFFE07A5F), // 😠 生气 — 珊瑚
'thinking': Color(0xFFB8A9C9),// 🤔 思考 — 淡紫
};
// ===== 浅色主题色彩方案 =====
static const _light = ColorScheme(
brightness: Brightness.light,
primary: accent,
onPrimary: Colors.white,
primaryContainer: Color(0xFFFFE0D6),
onPrimaryContainer: fgLight,
secondary: secondary,
onSecondary: Colors.white,
secondaryContainer: Color(0xFFD4E8DC),
onSecondaryContainer: fgLight,
tertiary: tertiary,
onTertiary: fgLight,
error: error,
onError: Colors.white,
surface: surfaceLight,
onSurface: fgLight,
surfaceContainerHighest: bgLight,
);
// ===== 深色主题色彩方案 =====
static const _dark = ColorScheme(
brightness: Brightness.dark,
primary: accentDark,
onPrimary: fgDark,
primaryContainer: Color(0xFF5A2E22),
onPrimaryContainer: Color(0xFFFFD6CC),
secondary: secondaryDark,
onSecondary: fgDark,
secondaryContainer: Color(0xFF2A4A38),
onSecondaryContainer: Color(0xFFD4E8DC),
tertiary: tertiaryDark,
onTertiary: fgDark,
error: Color(0xFFEF5350),
onError: fgDark,
surface: surfaceDark,
onSurface: fgDark,
surfaceContainerHighest: bgDark,
);
/// 获取浅色色彩方案
static ColorScheme lightScheme() => _light;
/// 获取深色色彩方案
static ColorScheme darkScheme() => _dark;
}

View File

@@ -0,0 +1,28 @@
// 暖记圆角系统
// 设计规格: 10 / 16 / 22 / 28 / pill
import 'package:flutter/material.dart';
class AppRadius {
AppRadius._();
/// 小圆角 10px — 按钮、输入框
static const double sm = 10;
static BorderRadius get smBorder => BorderRadius.circular(sm);
/// 中圆角 16px — 卡片、弹窗
static const double md = 16;
static BorderRadius get mdBorder => BorderRadius.circular(md);
/// 大圆角 22px — 大卡片、底部面板
static const double lg = 22;
static BorderRadius get lgBorder => BorderRadius.circular(lg);
/// 超大圆角 28px — 模态框、全屏面板
static const double xl = 28;
static BorderRadius get xlBorder => BorderRadius.circular(xl);
/// 胶囊型 — 标签、Chip
static const double pill = 100;
static BorderRadius get pillBorder => BorderRadius.circular(pill);
}

View File

@@ -0,0 +1,49 @@
// 暖记阴影系统 — soft / medium / float
import 'package:flutter/material.dart';
class AppShadows {
AppShadows._();
/// 柔和阴影 — 卡片默认
static List<BoxShadow> soft(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.3)
: const Color(0xFF2D2420).withValues(alpha: 0.08),
offset: const Offset(0, 2),
blurRadius: 8,
),
];
}
/// 中等阴影 — 浮动元素、FAB
static List<BoxShadow> medium(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.4)
: const Color(0xFF2D2420).withValues(alpha: 0.12),
offset: const Offset(0, 4),
blurRadius: 16,
),
];
}
/// 浮动阴影 — 弹窗、底部面板
static List<BoxShadow> floating(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.5)
: const Color(0xFF2D2420).withValues(alpha: 0.16),
offset: const Offset(0, 8),
blurRadius: 24,
),
];
}
}

View File

@@ -0,0 +1,159 @@
// 暖记主题入口 — 浅色/深色 ThemeData
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'app_colors.dart';
import 'app_typography.dart';
import 'app_radius.dart';
class AppTheme {
AppTheme._();
/// 浅色主题
static ThemeData light() => _buildTheme(AppColors.lightScheme());
/// 深色主题
static ThemeData dark() => _buildTheme(AppColors.darkScheme());
static ThemeData _buildTheme(ColorScheme colorScheme) {
final isLight = colorScheme.brightness == Brightness.light;
final textTheme = isLight
? AppTypography.lightTextTheme()
: AppTypography.darkTextTheme();
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: textTheme,
scaffoldBackgroundColor: isLight ? AppColors.bgLight : AppColors.bgDark,
// AppBar
appBarTheme: AppBarTheme(
elevation: 0,
scrolledUnderElevation: 1,
centerTitle: true,
backgroundColor: isLight ? AppColors.bgLight : AppColors.bgDark,
foregroundColor: colorScheme.onSurface,
titleTextStyle: textTheme.titleLarge,
),
// Card
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: AppRadius.mdBorder,
side: BorderSide(
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
),
),
color: colorScheme.surface,
),
// ElevatedButton
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: AppRadius.smBorder,
),
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
textStyle: textTheme.labelLarge,
),
),
// OutlinedButton
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: AppRadius.smBorder,
),
side: BorderSide(color: colorScheme.primary),
textStyle: textTheme.labelLarge,
),
),
// InputDecoration
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
border: OutlineInputBorder(
borderRadius: AppRadius.smBorder,
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: AppRadius.smBorder,
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: AppRadius.smBorder,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
// BottomNavigationBar
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurface.withValues(alpha: 0.5),
backgroundColor: colorScheme.surface,
elevation: 8,
),
// Chip
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(
borderRadius: AppRadius.pillBorder,
),
side: BorderSide.none,
),
// FloatingActionButton
floatingActionButtonTheme: FloatingActionButtonThemeData(
shape: RoundedRectangleBorder(
borderRadius: AppRadius.mdBorder,
),
elevation: 4,
),
// Page transitions — 弹性曲线
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: _WarmCurveBuilder(),
TargetPlatform.iOS: const CupertinoPageTransitionsBuilder(),
},
),
);
}
}
/// 暖记弹性页面转场: cubic-bezier(0.34, 1.56, 0.64, 1)
class _WarmCurveBuilder extends PageTransitionsBuilder {
const _WarmCurveBuilder();
static const Curve _warmCurve = Curves.easeOutBack;
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: animation.drive(
Tween(begin: const Offset(1.0, 0.0), end: Offset.zero)
.chain(CurveTween(curve: _warmCurve)),
),
child: child,
);
}
}

View File

@@ -0,0 +1,114 @@
// 暖记字体系统 — Noto Sans SC + Caveat
import 'package:flutter/material.dart';
class AppTypography {
AppTypography._();
/// 字体族
static const String displayFont = 'Caveat'; // 手写风格(标题装饰)
static const String bodyFont = 'NotoSansSC'; // 正文(中文优先)
static const String monoFont = 'JetBrains Mono'; // 等宽(暂用系统回退)
/// 浅色主题文字主题
static TextTheme lightTextTheme() => TextTheme(
// 大标题 — 手写风格
displayLarge: TextStyle(
fontFamily: displayFont,
fontSize: 57,
height: 1.12,
fontWeight: FontWeight.w700,
),
displayMedium: TextStyle(
fontFamily: displayFont,
fontSize: 45,
height: 1.16,
fontWeight: FontWeight.w700,
),
displaySmall: TextStyle(
fontFamily: displayFont,
fontSize: 36,
height: 1.22,
fontWeight: FontWeight.w700,
),
// 标题 — 正文衬线
headlineLarge: TextStyle(
fontFamily: bodyFont,
fontSize: 32,
height: 1.25,
fontWeight: FontWeight.w700,
),
headlineMedium: TextStyle(
fontFamily: bodyFont,
fontSize: 28,
height: 1.29,
fontWeight: FontWeight.w600,
),
headlineSmall: TextStyle(
fontFamily: bodyFont,
fontSize: 24,
height: 1.33,
fontWeight: FontWeight.w600,
),
// 副标题
titleLarge: TextStyle(
fontFamily: bodyFont,
fontSize: 22,
height: 1.27,
fontWeight: FontWeight.w600,
),
titleMedium: TextStyle(
fontFamily: bodyFont,
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.w600,
),
titleSmall: TextStyle(
fontFamily: bodyFont,
fontSize: 14,
height: 1.43,
fontWeight: FontWeight.w600,
),
// 正文
bodyLarge: TextStyle(
fontFamily: bodyFont,
fontSize: 16,
height: 1.5,
fontWeight: FontWeight.w400,
),
bodyMedium: TextStyle(
fontFamily: bodyFont,
fontSize: 14,
height: 1.43,
fontWeight: FontWeight.w400,
),
bodySmall: TextStyle(
fontFamily: bodyFont,
fontSize: 12,
height: 1.33,
fontWeight: FontWeight.w400,
),
// 标签
labelLarge: TextStyle(
fontFamily: bodyFont,
fontSize: 14,
height: 1.43,
fontWeight: FontWeight.w500,
),
labelMedium: TextStyle(
fontFamily: bodyFont,
fontSize: 12,
height: 1.33,
fontWeight: FontWeight.w500,
),
labelSmall: TextStyle(
fontFamily: bodyFont,
fontSize: 11,
height: 1.45,
fontWeight: FontWeight.w500,
),
);
/// 深色主题文字主题
static TextTheme darkTextTheme() => lightTextTheme();
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class AchievementPage extends StatelessWidget {
const AchievementPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('成就 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('登录 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class CalendarPage extends StatelessWidget {
const CalendarPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('日历 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class ClassPage extends StatelessWidget {
const ClassPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('班级 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class EditorPage extends StatelessWidget {
final String? journalId;
const EditorPage({super.key, this.journalId});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
journalId != null
? '编辑日记 ($journalId) - 占位页面'
: '新建日记 - 占位页面',
),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('首页 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class MoodPage extends StatelessWidget {
const MoodPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('心情 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class ParentPage extends StatelessWidget {
const ParentPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('家长 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('我的 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class SearchPage extends StatelessWidget {
const SearchPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('搜索 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class StickerLibraryPage extends StatelessWidget {
const StickerLibraryPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('贴纸库 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class TeacherPage extends StatelessWidget {
const TeacherPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('教师 - 占位页面'),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class TemplateGalleryPage extends StatelessWidget {
const TemplateGalleryPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('模板画廊 - 占位页面'),
),
);
}
}

7
app/lib/main.dart Normal file
View File

@@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
import 'app.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const NuanjiApp());
}

View File

@@ -0,0 +1,270 @@
// 暖记响应式骨架 — 三级自适应布局
// 手机: 底部 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!,
),
],
],
),
);
}
}