修改ui设计
This commit is contained in:
parent
114e18698f
commit
17bc32dcb2
163
CLAUDE.md
163
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<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工具链
|
||||
196
lib/main.dart
196
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<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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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: '删除分类',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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.0到0.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user