From 17bc32dcb200e86021efd93ebac2e9ed6df5357a Mon Sep 17 00:00:00 2001 From: daodaoshi Date: Sat, 30 Aug 2025 18:11:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9ui=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 163 +++++------ lib/main.dart | 196 +++++++++++-- lib/pages/add_photo_page.dart | 308 ++++++++++++------- lib/pages/categories_page.dart | 299 +++++++++++++------ lib/pages/photo_page.dart | 520 ++++++++++++++++++++++++--------- test/widget_test.dart | 30 -- 6 files changed, 1056 insertions(+), 460 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/CLAUDE.md b/CLAUDE.md index 5ee4b82..a71a2ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,119 +4,118 @@ ## 项目概览 -**SnapWish** - 一款使用中文界面的 Flutter 照片管理应用。该应用允许用户将照片组织到分类中、添加标签并管理他们的照片收藏。 +**SnapWish** - 一款使用中文界面的 Flutter 照片管理应用,允许用户将照片组织到分类中、添加标签并管理照片收藏。 -## 架构结构 +## 架构与结构 -该应用采用标准的 Flutter Material Design 架构: -- **主入口**: `lib/main.dart` - 包含根组件和导航设置 -- **导航**: 底部导航栏包含 2 个标签页(照片、分类) -- **悬浮按钮**: 通用添加照片按钮 -- **页面**: 位于 `lib/pages/` 目录 +### 核心架构 +- **Material 3 设计系统** 配合深色主题 +- **三个主界面**: 照片流、分类管理、添加照片表单 +- **底部导航栏** 包含2个标签页(照片、分类) +- **悬浮按钮** 用于通用照片添加 +- **命名路由** `/add-photo` 用于导航 -## 核心组件 - -### 主结构 (`lib/main.dart`) -- **SnapWishApp**: 根 MaterialApp 组件,包含主题配置 -- **MainPage**: 管理底部导航的有状态组件 -- **路由**: `/add-photo` 路由用于添加新照片 - -### 页面 (`lib/pages/`) -- **PhotoPage** (`photo_page.dart`): 照片网格显示和照片详情查看器 -- **CategoriesPage** (`categories_page.dart`): 文件夹/分类管理,包含创建/删除功能 -- **AddPhotoPage** (`add_photo_page.dart`): 照片上传表单,包含分类选择和标签功能 +### 文件结构 +``` +lib/ +├── main.dart # 根应用,包含主题和导航 +├── pages/ +│ ├── photo_page.dart # 照片网格和详情查看器 +│ ├── categories_page.dart # 分类CRUD操作 +│ └── add_photo_page.dart # 照片上传和元数据管理 +└── ... +``` ## 开发命令 -### 构建与运行 +### 基础命令 ```bash # 安装依赖 flutter pub get -# 运行开发服务器 +# 运行开发 flutter run -# 构建 APK -flutter build apk +# 构建命令 +flutter build apk # Android APK +flutter build ios # iOS应用 +flutter build web # Web构建 -# 构建 iOS 应用 -flutter build ios - -# 构建 Web 应用 -flutter build web +# 代码质量 +flutter analyze # 静态分析 +flutter test # 运行所有测试 +flutter format . # 代码格式化 ``` -### 测试与代码质量 +### 热重载与开发 ```bash -# 运行所有测试 -flutter test - -# 运行特定测试文件 -flutter test test/widget_test.dart - -# 代码分析 -flutter analyze - -# 代码格式化 -flutter format . +flutter run --hot # 带热重载的开发模式 +flutter pub outdated # 检查过时包 +flutter pub upgrade # 更新依赖 ``` -### 开发工作流 -```bash -# 开发期间热重载 -flutter run --hot - -# 检查过时包 -flutter pub outdated - -# 升级包 -flutter pub upgrade -``` - -## 代码模式 +## 关键技术模式 ### 状态管理 -- 使用 **StatefulWidget** 进行本地状态管理 -- 使用 **setState** 进行 UI 更新 -- 目前使用模拟数据(标有 TODO 注释) +- **StatefulWidget** 用于本地状态管理 +- **setState** 用于UI更新 +- **AnimationController** 用于按压动画(照片卡片) +- **SingleTickerProviderStateMixin** 用于流畅动画 -### 导航 -- **命名路由** 用于主要导航 (`/add-photo`) -- **Navigator.push** 用于照片详情查看 -- **BottomNavigationBar** 用于主标签页切换 +### UI模式 +- **CustomScrollView + Slivers** 用于灵活滚动 +- **ShaderMask** 用于文字渐变效果 +- **BoxDecoration** 带阴影和渐变效果 +- **GestureDetector** 用于高级触摸处理 +- **AnimationController** 配合Tween动画 -### UI 组件 -- **Material 3** 设计系统 (`useMaterial3: true`) -- **中文界面**(标签为中文) -- **响应式网格布局** 用于照片显示 -- **对话框式** 交互用于分类管理 +### 导航模式 +- **BottomNavigationBar** 自定义样式 +- **命名路由** (`/add-photo`) 便于维护 +- **PageView** 用于照片详情浏览 +- **基于对话框的交互** 用于CRUD操作 -### 数据结构模式 -- **模拟数据** 目前以 Lists/Maps 实现 -- **分类结构**: `{name: String, count: int, isDefault: bool}` -- **标签管理**: 字符串列表支持 # 前缀 +### 数据结构 +- **分类**: `{'name': String, 'count': int, 'isDefault': bool}` +- **标签**: `List` 支持#前缀 +- **照片**: 目前为模拟数据,带TODO占位符 -## 关键 TODO 项目 +## 开发注意事项 -代码库包含多个 TODO 注释,指示计划功能: -- 用真实照片数据替换模拟数据 -- 实现从设备选择照片功能 -- 添加照片保存功能 -- 实现收藏/分享/删除功能 -- 按分类筛选照片 +### 已实现的关键功能 +- 深色主题配合品牌色(#FF8A1F渐变) +- 响应式照片卡片,4:5宽高比 +- 分类管理,带创建/删除验证 +- 标签系统,支持#前缀 +- 照片详情查看器,带PageView +- 自定义底部导航配合FAB集成 + +### TODO项目(可在代码中搜索) +- 用真实照片存储替换模拟数据 +- 集成图片选择器用于设备照片 +- 实现照片保存功能 +- 添加收藏/分享/删除功能 +- 实现基于分类的照片筛选 - 集成真实文件夹/分类数据 -## 当前限制 - -- 无实际照片存储/检索(仅模拟数据) +### 当前限制 +- 无实际照片存储/检索 - 无相机集成 - 无持久化存储 -- 无图像缓存或优化 +- 无图像缓存/优化 - 仅基础错误处理 -## 开发环境 +### 设计系统 +- **背景色**: #0F1113 +- **表面色**: #1A1D1F +- **主色**: #FF8A1F +- **次色**: #FFBE3D +- **文字**: #EDEDED +- **次要文字**: #9CA3AF +## 开发环境 - **Flutter**: 3.2.5+ - **Dart**: 3.2.5+ -- **目标平台**: iOS、Android(支持 Web) -- **构建工具**: 标准 Flutter 工具链 \ No newline at end of file +- **平台**: iOS、Android、Web +- **语言**: 中文界面,带详细中文代码注释 +- **架构**: 标准Flutter工具链 \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f9d5a4a..631f343 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,10 +3,14 @@ import 'pages/photo_page.dart'; import 'pages/categories_page.dart'; import 'pages/add_photo_page.dart'; +/// 应用入口函数 +/// Flutter应用从main()函数开始执行,runApp()启动整个应用 void main() { runApp(const SnapWishApp()); } +/// 根应用组件 +/// 负责全局配置:主题、路由、国际化等 class SnapWishApp extends StatelessWidget { const SnapWishApp({super.key}); @@ -14,11 +18,49 @@ class SnapWishApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'SnapWish', + // 全局主题配置 - 使用深色模式 theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + brightness: Brightness.dark, + scaffoldBackgroundColor: const Color(0xFF0F1113), + primaryColor: const Color(0xFFFF8A1F), useMaterial3: true, + colorScheme: ColorScheme.dark( + primary: const Color(0xFFFF8A1F), + secondary: const Color(0xFFFFBE3D), + surface: const Color(0xFF1A1D1F), + background: const Color(0xFF0F1113), + onBackground: const Color(0xFFEDEDED), + onSurface: const Color(0xFFEDEDED), + onSurfaceVariant: const Color(0xFF9CA3AF), + ), + appBarTheme: const AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: false, + titleTextStyle: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFFFF9A2A), + ), + iconTheme: IconThemeData( + color: Color(0xFFEDEDED), + ), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Colors.transparent, + elevation: 8, + sizeConstraints: BoxConstraints.tightFor(width: 64, height: 64), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Color(0xFF1A1D1F), + selectedItemColor: Color(0xFFFFFFFF), + unselectedItemColor: Color(0xFF9CA3AF), + type: BottomNavigationBarType.fixed, + elevation: 0, + ), ), home: const MainPage(), + // 路由配置 - 命名路由便于管理 routes: { '/add-photo': (context) => const AddPhotoPage(), }, @@ -26,6 +68,8 @@ class SnapWishApp extends StatelessWidget { } } +/// 主页控制器 +/// 管理底部导航和页面切换的核心组件 class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -34,41 +78,143 @@ class MainPage extends StatefulWidget { } class _MainPageState extends State { + // 当前选中的页面索引 int _currentIndex = 0; - final List _pages = [ - const PhotoPage(), - const CategoriesPage(), + // 页面列表 - 使用const构造函数提升性能 + final List _pages = const [ + PhotoPage(), + CategoriesPage(), ]; @override Widget build(BuildContext context) { return Scaffold( + // 允许body延伸到bottomNavigationBar下方,实现沉浸式效果 + extendBody: true, + + // 当前显示的页面 body: _pages[_currentIndex], - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.pushNamed(context, '/add-photo'); - }, - child: const Icon(Icons.add), - ), - bottomNavigationBar: NavigationBar( - selectedIndex: _currentIndex, - onDestinationSelected: (index) { - setState(() { - _currentIndex = index; - }); - }, - destinations: const [ - NavigationDestination( - icon: Icon(Icons.photo_library), - label: '照片', - ), - NavigationDestination( - icon: Icon(Icons.folder), - label: '分类', + + // 悬浮按钮 - 使用自定义容器实现渐变效果 + floatingActionButton: _buildGradientFAB(), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + + // 底部导航栏 - 自定义样式 + bottomNavigationBar: _buildCustomBottomNav(), + ); + } + + /// 构建渐变悬浮按钮 + /// 技巧:使用Container+BoxDecoration实现复杂渐变和阴影效果 + Widget _buildGradientFAB() { + return Container( + width: 64, + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + // 线性渐变背景 + gradient: const LinearGradient( + colors: [Color(0xFFFF8A1F), Color(0xFFFFBE3D)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + // 阴影效果增强立体感 + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.35), + blurRadius: 24, + offset: const Offset(0, 8), ), ], ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(32), + onTap: () { + // 使用命名路由跳转,便于维护 + Navigator.pushNamed(context, '/add-photo'); + }, + // 水波纹效果增强交互体验 + child: const Icon( + Icons.add, + color: Colors.white, + size: 24, + ), + ), + ), + ); + } + + /// 构建自定义底部导航栏 + /// 技巧:使用Container+BoxDecoration实现圆角和阴影 + Widget _buildCustomBottomNav() { + return Container( + height: 72, + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1A1D1F), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.35), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: (index) { + // 使用setState触发页面重建 + setState(() { + _currentIndex = index; + }); + }, + backgroundColor: const Color(0xFF1A1D1F), + selectedItemColor: Colors.white, + unselectedItemColor: const Color(0xFF9CA3AF), + type: BottomNavigationBarType.fixed, + elevation: 0, + showSelectedLabels: false, + showUnselectedLabels: false, + items: [ + BottomNavigationBarItem( + icon: _buildHomeIcon(), + label: '照片', + ), + BottomNavigationBarItem( + icon: const Icon(Icons.grid_view_rounded, size: 26), + label: '分类', + ), + ], + ), + ), + ); + } + + /// 构建带通知图标的首页按钮 + /// 技巧:使用Stack叠加小圆点通知 + Widget _buildHomeIcon() { + return Stack( + children: [ + const Icon(Icons.home_rounded, size: 26), + Positioned( + top: 0, + right: 0, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFFFF4D4F), + shape: BoxShape.circle, + ), + ), + ), + ], ); } } diff --git a/lib/pages/add_photo_page.dart b/lib/pages/add_photo_page.dart index d0a950a..1c0aa86 100644 --- a/lib/pages/add_photo_page.dart +++ b/lib/pages/add_photo_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +/// 添加照片页面 - 照片上传和元数据管理 +/// 提供照片选择、分类选择、标签添加等功能 +/// 使用StatefulWidget管理复杂的表单状态 class AddPhotoPage extends StatefulWidget { const AddPhotoPage({super.key}); @@ -7,7 +10,10 @@ class AddPhotoPage extends StatefulWidget { State createState() => _AddPhotoPageState(); } +/// 添加照片页面状态管理 +/// 技巧:使用多个Controller管理不同输入字段 class _AddPhotoPageState extends State { + // 表单状态管理 String? _selectedCategory; final TextEditingController _tagsController = TextEditingController(); final List _tags = []; @@ -21,13 +27,16 @@ class _AddPhotoPageState extends State { super.dispose(); } + /// 选择照片 + /// TODO: 集成图片选择器(image_picker)实现真实功能 void _selectPhoto() { - // TODO: 实现选择本地照片功能 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('选择照片功能待实现')), ); } + /// 添加标签 + /// 技巧:自动去除空格和重复标签,保持数据整洁 void _addTag(String text) { final tag = text.trim(); if (tag.isNotEmpty && !_tags.contains(tag)) { @@ -38,12 +47,15 @@ class _AddPhotoPageState extends State { } } + /// 移除标签 void _removeTag(String tag) { setState(() { _tags.remove(tag); }); } + /// 保存照片 + /// 技巧:表单验证确保必要字段已填写 void _savePhoto() { if (_selectedCategory == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -52,7 +64,7 @@ class _AddPhotoPageState extends State { return; } - // TODO: 实现保存照片功能 + // TODO: 实现真实保存逻辑 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('照片已保存到 $_selectedCategory')), ); @@ -62,109 +74,209 @@ class _AddPhotoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('添加照片'), - actions: [ - TextButton( - onPressed: _savePhoto, - child: const Text('保存', style: TextStyle(color: Colors.white)), + appBar: _buildAppBar(), + body: _buildBody(), + ); + } + + /// 构建应用栏 + /// 技巧:使用actions添加保存按钮,保持界面简洁 + AppBar _buildAppBar() { + return AppBar( + title: const Text('添加照片'), + actions: [ + TextButton( + onPressed: _savePhoto, + child: const Text( + '保存', + style: TextStyle(color: Colors.white, fontSize: 16), ), + ), + ], + ); + } + + /// 构建页面主体 + /// 技巧:使用SingleChildScrollView确保键盘弹出时内容可滚动 + Widget _buildBody() { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPhotoSelector(), + const SizedBox(height: 24), + _buildCategorySelector(), + const SizedBox(height: 24), + _buildTagsSection(), ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 照片预览区域 - GestureDetector( - onTap: _selectPhoto, - child: Container( - height: 200, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[400]!), - ), - child: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey), - SizedBox(height: 8), - Text('点击选择照片', style: TextStyle(color: Colors.grey)), - ], - ), + ); + } + + /// 构建照片选择器 + /// 技巧:使用GestureDetector实现点击区域,提升用户体验 + Widget _buildPhotoSelector() { + return GestureDetector( + onTap: _selectPhoto, + child: Container( + height: 200, + decoration: BoxDecoration( + color: const Color(0xFF1A1D1F), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFF374151)), + ), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_photo_alternate, + size: 48, + color: Color(0xFF9CA3AF), + ), + SizedBox(height: 8), + Text( + '点击选择照片', + style: TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 16, ), ), - ), - const SizedBox(height: 24), - - // 文件夹选择 - const Text('选择文件夹', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - DropdownButtonFormField( - value: _selectedCategory, - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: '请选择文件夹', - ), - items: _categories.map((category) { - return DropdownMenuItem( - value: category, - child: Text(category), - ); - }).toList(), - onChanged: (value) { - setState(() { - _selectedCategory = value; - }); - }, - ), - const SizedBox(height: 24), - - // 标签输入 - const Text('添加标签', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - TextField( - controller: _tagsController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: '输入标签(支持 #标签 格式)', - suffixIcon: IconButton( - icon: const Icon(Icons.add), - onPressed: () => _addTag(_tagsController.text), - ), - ), - onSubmitted: _addTag, - onChanged: (value) { - // 处理 # 开头的标签输入 - if (value.endsWith(' ') || value.endsWith('#')) { - final parts = value.split('#'); - if (parts.length > 1) { - final tag = parts.last.trim(); - if (tag.isNotEmpty) { - _addTag(tag); - } - } - } - }, - ), - const SizedBox(height: 12), - - // 标签列表 - Wrap( - spacing: 8, - runSpacing: 4, - children: _tags.map((tag) => Chip( - label: Text('#$tag'), - onDeleted: () => _removeTag(tag), - deleteIcon: const Icon(Icons.close, size: 16), - )).toList(), - ), - ], + ], + ), ), ), ); } + + /// 构建分类选择器 + /// 技巧:使用DropdownButtonFormField提供下拉选择体验 + Widget _buildCategorySelector() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '选择文件夹', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: _selectedCategory, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + hintText: '请选择文件夹', + filled: true, + fillColor: const Color(0xFF1A1D1F), + ), + dropdownColor: const Color(0xFF1A1D1F), + items: _categories.map((category) { + return DropdownMenuItem( + value: category, + child: Text(category, style: const TextStyle(color: Colors.white)), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedCategory = value; + }); + }, + ), + ], + ); + } + + /// 构建标签区域 + /// 技巧:使用Wrap实现标签的流式布局 + Widget _buildTagsSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '添加标签', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + _buildTagInput(), + const SizedBox(height: 12), + _buildTagList(), + ], + ); + } + + /// 构建标签输入框 + /// 技巧:使用TextField的多种事件处理实现智能标签输入 + Widget _buildTagInput() { + return TextField( + controller: _tagsController, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + hintText: '输入标签(支持 #标签 格式)', + filled: true, + fillColor: const Color(0xFF1A1D1F), + suffixIcon: IconButton( + icon: const Icon(Icons.add, color: Color(0xFFFF8A1F)), + onPressed: () => _addTag(_tagsController.text), + ), + ), + style: const TextStyle(color: Colors.white), + onSubmitted: _addTag, + onChanged: _handleTagInputChange, + ); + } + + /// 处理标签输入变化 + /// 技巧:智能识别#符号,支持多种输入格式 + void _handleTagInputChange(String value) { + // 支持空格或回车分隔标签 + if (value.endsWith(' ') || value.endsWith('\n')) { + final tag = value.trim(); + if (tag.isNotEmpty) { + _addTag(tag.replaceAll('#', '')); + } + } + } + + /// 构建标签列表 + /// 技巧:使用Chip组件实现可删除的标签 + Widget _buildTagList() { + if (_tags.isEmpty) { + return const Text( + '暂无标签,可添加多个标签方便搜索', + style: TextStyle(color: Color(0xFF9CA3AF), fontSize: 14), + ); + } + + return Wrap( + spacing: 8, + runSpacing: 8, + children: _tags.map((tag) => _buildTagChip(tag)).toList(), + ); + } + + /// 构建单个标签Chip + /// 技巧:自定义Chip样式,提升视觉体验 + Widget _buildTagChip(String tag) { + return Chip( + label: Text( + '#$tag', + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + backgroundColor: const Color(0xFF374151), + deleteIcon: const Icon(Icons.close, size: 16, color: Colors.white), + onDeleted: () => _removeTag(tag), + ); + } } \ No newline at end of file diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index a14ed44..5f46d34 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +/// 分类页面 - 照片分类管理 +/// 提供创建、删除、查看分类的功能 +/// 使用StatefulWidget管理分类数据状态 class CategoriesPage extends StatefulWidget { const CategoriesPage({super.key}); @@ -7,8 +10,11 @@ class CategoriesPage extends StatefulWidget { State createState() => _CategoriesPageState(); } +/// 分类页面状态管理 +/// 技巧:使用List存储分类数据,便于后续API对接 class _CategoriesPageState extends State { // TODO: 替换为真实文件夹数据 + // 数据结构:name-分类名称, count-照片数量, isDefault-是否为默认分类 final List> _categories = [ {'name': '默认分类', 'count': 15, 'isDefault': true}, {'name': '人像', 'count': 8, 'isDefault': false}, @@ -16,46 +22,67 @@ class _CategoriesPageState extends State { {'name': '建筑', 'count': 5, 'isDefault': false}, ]; + /// 创建新分类 + /// 技巧:使用TextEditingController管理输入框状态 void _createCategory() { final controller = TextEditingController(); + showDialog( context: context, - builder: (context) => AlertDialog( - title: const Text('创建新文件夹'), - content: TextField( - controller: controller, - decoration: const InputDecoration( - hintText: '输入文件夹名称', - border: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - TextButton( - onPressed: () { - if (controller.text.isNotEmpty) { - setState(() { - _categories.add({ - 'name': controller.text, - 'count': 0, - 'isDefault': false, - }); - }); - Navigator.pop(context); - } - }, - child: const Text('创建'), - ), - ], - ), + builder: (context) => _buildCreateCategoryDialog(controller), ); } + /// 构建创建分类对话框 + /// 技巧:分离UI构建逻辑,提高代码可读性 + Widget _buildCreateCategoryDialog(TextEditingController controller) { + return AlertDialog( + backgroundColor: const Color(0xFF1A1D1F), + title: const Text('创建新文件夹'), + content: TextField( + controller: controller, + decoration: const InputDecoration( + hintText: '输入文件夹名称', + border: OutlineInputBorder(), + hintStyle: TextStyle(color: Color(0xFF9CA3AF)), + ), + style: const TextStyle(color: Colors.white), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消', style: TextStyle(color: Color(0xFF9CA3AF))), + ), + TextButton( + onPressed: () => _handleCreateCategory(controller.text), + child: const Text('创建', style: TextStyle(color: Color(0xFFFF8A1F))), + ), + ], + ); + } + + /// 处理创建分类逻辑 + /// 技巧:输入验证确保数据有效性 + void _handleCreateCategory(String name) { + if (name.trim().isEmpty) return; + + setState(() { + _categories.add({ + 'name': name.trim(), + 'count': 0, + 'isDefault': false, + }); + }); + Navigator.pop(context); + } + + /// 删除分类 + /// 技巧:删除前进行二次确认,提升用户体验 void _deleteCategory(int index) { final category = _categories[index]; + + // 默认分类不允许删除 if (category['isDefault']) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('默认分类不能删除')), @@ -65,78 +92,162 @@ class _CategoriesPageState extends State { showDialog( context: context, - builder: (context) => AlertDialog( - title: const Text('删除文件夹'), - content: Text('确定要删除"${category['name']}"吗?\n${category['count']}张照片将被移至默认分类。'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - TextButton( - onPressed: () { - setState(() { - // 将照片移至默认分类 - _categories[0]['count'] += category['count']; - _categories.removeAt(index); - }); - Navigator.pop(context); - }, - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('删除'), - ), - ], + builder: (context) => _buildDeleteCategoryDialog(category, index), + ); + } + + /// 构建删除分类对话框 + Widget _buildDeleteCategoryDialog(Map category, int index) { + return AlertDialog( + backgroundColor: const Color(0xFF1A1D1F), + title: const Text('删除文件夹'), + content: Text( + '确定要删除"${category['name']}"吗?\n${category['count']}张照片将被移至默认分类。', + style: const TextStyle(color: Colors.white), ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消', style: TextStyle(color: Color(0xFF9CA3AF))), + ), + TextButton( + onPressed: () => _handleDeleteCategory(index), + style: TextButton.styleFrom(foregroundColor: const Color(0xFFFF4D4F)), + child: const Text('删除'), + ), + ], + ); + } + + /// 处理删除分类逻辑 + /// 技巧:删除时将照片移至默认分类,避免数据丢失 + void _handleDeleteCategory(int index) { + final category = _categories[index]; + + setState(() { + // 将照片数量累加到默认分类 + _categories[0]['count'] += category['count']; + _categories.removeAt(index); + }); + Navigator.pop(context); + } + + /// 处理分类点击 + /// TODO: 跳转到该分类的照片列表页面 + void _handleCategoryTap(Map category) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('打开 ${category['name']}')), ); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('分类'), - actions: [ - IconButton( - icon: const Icon(Icons.create_new_folder), - onPressed: _createCategory, - ), - ], + appBar: _buildAppBar(), + body: _buildCategoryList(), + ); + } + + /// 构建应用栏 + /// 技巧:使用AppBar的actions属性添加功能按钮 + AppBar _buildAppBar() { + return AppBar( + title: const Text('分类'), + actions: [ + IconButton( + icon: const Icon(Icons.create_new_folder, color: Color(0xFFFF8A1F)), + onPressed: _createCategory, + tooltip: '创建新分类', + ), + ], + ); + } + + /// 构建分类列表 + /// 技巧:使用ListView.builder实现高效长列表 + Widget _buildCategoryList() { + if (_categories.isEmpty) { + return const Center( + child: Text('暂无分类,点击右上角创建'), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _categories.length, + itemBuilder: (context, index) => _buildCategoryCard(_categories[index], index), + ); + } + + /// 构建分类卡片 + /// 技巧:使用Card+ListTile组合实现优雅的列表项 + Widget _buildCategoryCard(Map category, int index) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: const Color(0xFF1A1D1F), + borderRadius: BorderRadius.circular(16), ), - body: ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: _categories.length, - itemBuilder: (context, index) { - final category = _categories[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - leading: CircleAvatar( - backgroundColor: category['isDefault'] - ? Colors.blue - : Colors.deepPurple, - child: const Icon(Icons.folder, color: Colors.white), - ), - title: Text( - category['name'], - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text('${category['count']} 张照片'), - trailing: category['isDefault'] - ? null - : IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => _deleteCategory(index), - ), - onTap: () { - // TODO: 打开该分类的照片列表 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('打开 ${category['name']}')), - ); - }, - ), - ); - }, + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: _buildCategoryIcon(category), + title: _buildCategoryTitle(category), + subtitle: _buildCategorySubtitle(category), + trailing: _buildCategoryActions(category, index), + onTap: () => _handleCategoryTap(category), ), ); } + + /// 构建分类图标 + /// 技巧:根据分类类型使用不同颜色的图标 + Widget _buildCategoryIcon(Map category) { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: category['isDefault'] + ? const Color(0xFF3B82F6) + : const Color(0xFF8B5CF6), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.folder, color: Colors.white, size: 24), + ); + } + + /// 构建分类标题 + Widget _buildCategoryTitle(Map category) { + return Text( + category['name'], + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white, + ), + ); + } + + /// 构建分类副标题 + Widget _buildCategorySubtitle(Map category) { + return Text( + '${category['count']} 张照片', + style: const TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 14, + ), + ); + } + + /// 构建分类操作按钮 + Widget? _buildCategoryActions(Map category, int index) { + if (category['isDefault']) { + return null; // 默认分类无操作按钮 + } + + return IconButton( + icon: const Icon(Icons.delete_outline, color: Color(0xFFFF4D4F)), + onPressed: () => _deleteCategory(index), + tooltip: '删除分类', + ); + } } \ No newline at end of file diff --git a/lib/pages/photo_page.dart b/lib/pages/photo_page.dart index 7045725..8fea157 100644 --- a/lib/pages/photo_page.dart +++ b/lib/pages/photo_page.dart @@ -1,75 +1,328 @@ import 'package:flutter/material.dart'; +/// 照片页面 - 应用主页面 +/// 使用CustomScrollView实现弹性滚动和复杂布局 class PhotoPage extends StatelessWidget { const PhotoPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('照片'), + // 透明AppBar区域,让内容延伸到状态栏 + extendBodyBehindAppBar: true, + body: CustomScrollView( + slivers: [ + const PhotoAppBar(), + const PhotoFeed(), + ], ), - body: const PhotoGrid(), ); } } -class PhotoGrid extends StatelessWidget { - const PhotoGrid({super.key}); +/// 自定义SliverAppBar +/// 技巧:使用SliverAppBar实现滚动时的动态效果 +class PhotoAppBar extends StatelessWidget { + const PhotoAppBar({super.key}); @override Widget build(BuildContext context) { - // TODO: 替换为真实照片数据 - final mockPhotos = List.generate(20, (index) => '照片 ${index + 1}'); - - return GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 4, - mainAxisSpacing: 4, - childAspectRatio: 1, - ), - itemCount: mockPhotos.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PhotoDetailPage( - photos: mockPhotos, - initialIndex: index, - ), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.photo, size: 40, color: Colors.grey), - const SizedBox(height: 4), - Text( - mockPhotos[index], - style: const TextStyle(fontSize: 12), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), + return SliverAppBar( + backgroundColor: Colors.transparent, + elevation: 0, + pinned: true, // 滚动时固定在顶部 + floating: true, // 向下滚动时立即显示 + snap: false, + expandedHeight: 0, + title: ShaderMask( + // 技巧:使用ShaderMask实现文字渐变效果 + shaderCallback: (bounds) => const LinearGradient( + colors: [Color(0xFFFF8A1F), Color(0xFFFFBE3D)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(bounds), + child: const Text( + 'SnapWish,', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, // 会被shader覆盖 ), - ); - }, + ), + ), ); } } +/// 照片信息流 +/// 使用SliverList实现高效的长列表渲染 +class PhotoFeed extends StatelessWidget { + const PhotoFeed({super.key}); + + @override + Widget build(BuildContext context) { + // 模拟数据 - 实际应用中应从API获取 + final mockPhotos = []; // 空数组触发空状态 + + if (mockPhotos.isEmpty) { + return const SliverFillRemaining( + child: EmptyState(), + ); + } + + return SliverPadding( + // 水平内边距,确保内容不贴边 + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return const PhotoCard(); + }, + childCount: mockPhotos.length, + ), + ), + ); + } +} + +/// 空状态组件 +/// 良好的空状态设计提升用户体验 +class EmptyState extends StatelessWidget { + const EmptyState({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Text( + '还没有添加任何图片', + style: TextStyle( + fontSize: 14, + color: Color(0xFF9CA3AF), + letterSpacing: 0.5, // 字间距让文字更美观 + ), + ), + ); + } +} + +/// 照片卡片组件 - 核心UI元素 +/// 使用StatefulWidget管理收藏状态 +class PhotoCard extends StatefulWidget { + const PhotoCard({super.key}); + + @override + State createState() => _PhotoCardState(); +} + +/// 照片卡片状态管理 +/// 技巧:使用AnimationController实现按压动画 +class _PhotoCardState extends State + with SingleTickerProviderStateMixin { + // 收藏状态 + bool _isFavorite = false; + + // 动画控制器 + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + // 初始化动画控制器,时长200ms + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + // 创建缩放动画,从1.0到0.98 + _scaleAnimation = Tween( + begin: 1.0, + end: 0.98, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + /// 切换收藏状态 + void _toggleFavorite() { + setState(() { + _isFavorite = !_isFavorite; + }); + } + + /// 处理卡片点击 + void _handleCardTap() { + // TODO: 实现跳转到详情页 + debugPrint('跳转到照片详情页'); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final cardWidth = screenWidth - 32; // 考虑左右16dp内边距 + final cardHeight = cardWidth * 4 / 5; // 4:5的宽高比 + + return GestureDetector( + // 按压动画触发 + onTapDown: (_) => _controller.forward(), + onTapUp: (_) { + _controller.reverse(); + _handleCardTap(); + }, + onTapCancel: () => _controller.reverse(), + // 双击收藏 + onDoubleTap: _toggleFavorite, + child: ScaleTransition( + scale: _scaleAnimation, + child: Container( + margin: const EdgeInsets.only(bottom: 14), + width: cardWidth, + height: cardHeight, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.35), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Stack( + children: [ + // 图片占位符 + _buildImagePlaceholder(), + + // 底部渐变遮罩,提升文字可读性 + _buildGradientOverlay(), + + // 收藏按钮 + _buildFavoriteButton(), + + // 书签装饰 + _buildBookmarkDecoration(), + + // 标签文字 + _buildTags(), + ], + ), + ), + ), + ), + ); + } + + /// 构建图片占位符 + Widget _buildImagePlaceholder() { + return Container( + color: Colors.grey[800], + child: const Center( + child: Icon(Icons.photo, size: 80, color: Colors.grey), + ), + ); + } + + /// 构建底部渐变遮罩 + Widget _buildGradientOverlay() { + return Positioned( + bottom: 0, + left: 0, + right: 0, + height: 80, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black.withOpacity(0), // 透明 + Colors.black.withOpacity(0.6), // 60%不透明 + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ); + } + + /// 构建收藏按钮 + Widget _buildFavoriteButton() { + return Positioned( + top: 12, + right: 12, + child: GestureDetector( + onTap: _toggleFavorite, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.35), + shape: BoxShape.circle, + ), + child: Icon( + _isFavorite ? Icons.favorite : Icons.favorite_border, + color: _isFavorite ? const Color(0xFFFF5A5F) : Colors.white, + size: 20, + ), + ), + ), + ); + } + + /// 构建书签装饰 + Widget _buildBookmarkDecoration() { + return Positioned( + bottom: 12, + left: 12, + child: Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 1, + ), + ), + ), + ); + } + + /// 构建标签文字 + Widget _buildTags() { + return const Positioned( + bottom: 12, + right: 12, + child: Text( + '#家居 #盆栽 #庭院', + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w600, + shadows: [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + ), + ); + } +} + +/// 照片详情页面 +/// 提供全屏浏览和交互功能 class PhotoDetailPage extends StatefulWidget { final List photos; final int initialIndex; @@ -85,7 +338,9 @@ class PhotoDetailPage extends StatefulWidget { } class _PhotoDetailPageState extends State { + // 当前照片索引 late int _currentIndex; + // 页面控制器,用于控制PageView late PageController _pageController; @override @@ -101,6 +356,13 @@ class _PhotoDetailPageState extends State { super.dispose(); } + /// 显示操作反馈 + void _showActionFeedback(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -109,92 +371,88 @@ class _PhotoDetailPageState extends State { backgroundColor: Colors.transparent, elevation: 0, actions: [ - IconButton( - icon: const Icon(Icons.favorite_border), - onPressed: () { - // TODO: 实现收藏功能 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('收藏功能待实现')), - ); - }, - ), - IconButton( - icon: const Icon(Icons.share), - onPressed: () { - // TODO: 实现分享功能 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('分享功能待实现')), - ); - }, - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - // TODO: 实现删除功能 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('删除功能待实现')), - ); - }, - ), + _buildActionButton(Icons.favorite_border, '收藏功能待实现'), + _buildActionButton(Icons.share, '分享功能待实现'), + _buildActionButton(Icons.delete, '删除功能待实现'), ], ), body: Stack( children: [ - PageView.builder( - controller: _pageController, - itemCount: widget.photos.length, - onPageChanged: (index) { - setState(() { - _currentIndex = index; - }); - }, - itemBuilder: (context, index) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height * 0.7, - color: Colors.grey[800], - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.photo, size: 100, color: Colors.white), - const SizedBox(height: 20), - Text( - widget.photos[index], - style: const TextStyle( - color: Colors.white, - fontSize: 24, - ), - ), - ], - ), - ), - ), - ], - ), - ); - }, - ), - Positioned( - bottom: 20, - left: 0, - right: 0, - child: Center( - child: Text( - '${_currentIndex + 1} / ${widget.photos.length}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - ), + // 照片浏览区域 + _buildPhotoViewer(), + // 页码指示器 + _buildPageIndicator(), ], ), ); } + + /// 构建操作按钮 + Widget _buildActionButton(IconData icon, String feedback) { + return IconButton( + icon: Icon(icon), + onPressed: () => _showActionFeedback(feedback), + ); + } + + /// 构建照片查看器 + Widget _buildPhotoViewer() { + return PageView.builder( + controller: _pageController, + itemCount: widget.photos.length, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + }); + }, + itemBuilder: (context, index) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.7, + color: Colors.grey[800], + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.photo, size: 100, color: Colors.white), + SizedBox(height: 20), + Text( + '照片占位符', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + /// 构建页码指示器 + Widget _buildPageIndicator() { + return Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: Text( + '${_currentIndex + 1} / ${widget.photos.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + ); + } } \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 83d8723..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:snap_wish/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}