修改ui设计

This commit is contained in:
daodaoshi 2025-08-30 18:11:40 +08:00
parent 114e18698f
commit 17bc32dcb2
6 changed files with 1056 additions and 460 deletions

163
CLAUDE.md
View File

@ -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<String>` 支持#前缀
- **照片**: 目前为模拟数据带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 工具链
- **平台**: iOS、Android、Web
- **语言**: 中文界面,带详细中文代码注释
- **架构**: 标准Flutter工具链

View File

@ -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<MainPage> {
//
int _currentIndex = 0;
final List<Widget> _pages = [
const PhotoPage(),
const CategoriesPage(),
// - 使const构造函数提升性能
final List<Widget> _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,
),
),
),
],
);
}
}

View File

@ -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<AddPhotoPage> createState() => _AddPhotoPageState();
}
///
/// 使Controller管理不同输入字段
class _AddPhotoPageState extends State<AddPhotoPage> {
//
String? _selectedCategory;
final TextEditingController _tagsController = TextEditingController();
final List<String> _tags = [];
@ -21,13 +27,16 @@ class _AddPhotoPageState extends State<AddPhotoPage> {
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<AddPhotoPage> {
}
}
///
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<AddPhotoPage> {
return;
}
// TODO:
// TODO:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('照片已保存到 $_selectedCategory')),
);
@ -62,109 +74,209 @@ class _AddPhotoPageState extends State<AddPhotoPage> {
@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<String>(
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<String>(
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),
);
}
}

View File

@ -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<CategoriesPage> createState() => _CategoriesPageState();
}
///
/// 使List<Map>便API对接
class _CategoriesPageState extends State<CategoriesPage> {
// TODO:
// name-, count-, isDefault-
final List<Map<String, dynamic>> _categories = [
{'name': '默认分类', 'count': 15, 'isDefault': true},
{'name': '人像', 'count': 8, 'isDefault': false},
@ -16,46 +22,67 @@ class _CategoriesPageState extends State<CategoriesPage> {
{'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<CategoriesPage> {
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> category) {
return Text(
category['name'],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
),
);
}
///
Widget _buildCategorySubtitle(Map<String, dynamic> category) {
return Text(
'${category['count']} 张照片',
style: const TextStyle(
color: Color(0xFF9CA3AF),
fontSize: 14,
),
);
}
///
Widget? _buildCategoryActions(Map<String, dynamic> category, int index) {
if (category['isDefault']) {
return null; //
}
return IconButton(
icon: const Icon(Icons.delete_outline, color: Color(0xFFFF4D4F)),
onPressed: () => _deleteCategory(index),
tooltip: '删除分类',
);
}
}

View File

@ -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<PhotoCard> createState() => _PhotoCardState();
}
///
/// 使AnimationController实现按压动画
class _PhotoCardState extends State<PhotoCard>
with SingleTickerProviderStateMixin {
//
bool _isFavorite = false;
//
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
// 200ms
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
// 1.00.98
_scaleAnimation = Tween<double>(
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<String> photos;
final int initialIndex;
@ -85,7 +338,9 @@ class PhotoDetailPage extends StatefulWidget {
}
class _PhotoDetailPageState extends State<PhotoDetailPage> {
//
late int _currentIndex;
// PageView
late PageController _pageController;
@override
@ -101,6 +356,13 @@ class _PhotoDetailPageState extends State<PhotoDetailPage> {
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<PhotoDetailPage> {
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,
),
),
),
);
}
}

View File

@ -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);
});
}