5.1 KiB
5.1 KiB
【Demo】笔记图片选择页(多图选择 + 缩略图网格 + 删除角标)
源文件:
lib/pages/demo/note_picker_demo.dart| 分类:组件封装 一句话:学生上传笔记前的"选图 + 预览 + 确认"页 —— 网格缩略图、右下角加号、底部提交按钮。
一、这个 Demo 解决什么问题
笔记上传前要先选图、预览、确认。本页是一个真实的多图选择页:
选图(PickerService 弹层)→ 网格缩略图预览 → 删除不满意的 → 底部提交(预留上传接口)
它演示了三个可复用的套路:
- 加号格子:GridView 比图片多一格,点它弹选图层;
- 删除角标:每张图右上角一个小圆叉(Stack + Positioned);
- 双端缩略图:同一份代码在原生 / Web 都能显示缩略图(
PickerService.buildThumb)。
对照 Vue:相当于"el-upload 多图模式 + 预览删除"的前端部分,上传接口那步留了 TODO。
二、怎么运行 / 演示内容
runApp(const MaterialApp(home: NotePickerPage()));
- 点加号 → 弹「拍照/相册」底部弹层(原生是微信风格相册,可多选;Web 降级 image_picker);
- 选完图回到网格,右上角小叉可删;
- 选满 9 张后加号自动消失;
- 提交按钮显示
上传笔记(n/9),未选图时点击提示"请先选择笔记图片"。
三、核心知识点(新手向)
1. 网格多一格 = 加号位
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 3 列
mainAxisSpacing: 10.h,
crossAxisSpacing: 10.w,
),
itemCount: _images.length + 1, // 关键:多一格给"加号"
itemBuilder: (context, index) {
if (index == _images.length) { // 最后一格
if (_images.length >= _maxCount) {
return const SizedBox.shrink(); // 满了:占位空格
}
return _buildAddCell(); // 没满:加号
}
return _buildImageCell(index); // 前面是已选图片
},
)
2. 删除角标 = Stack + Positioned
Stack(
fit: StackFit.expand,
children: [
ClipRRect(borderRadius: BorderRadius.circular(10.r), child: thumb),
Positioned(
top: 4.h, right: 4.w, // 钉在右上角
child: GestureDetector(
onTap: () => _onRemoveTap(index),
child: Container( // 20x20 黑色半透明圆
width: 20.w, height: 20.h,
decoration: const BoxDecoration(
color: Colors.black54, shape: BoxShape.circle),
child: const Icon(Icons.close, size: 12, color: Colors.white),
),
),
),
],
)
3. 双端缩略图:永远走服务层,别自己判断平台
Widget _buildThumb(PickedImage image) {
return PickerService.buildThumb(image);
// 原生 → AssetEntityImage(实体缩略图,秒开不卡顿)
// Web → Image.memory(字节)
}
4. 提交按钮的"禁用态"套路
FilledButton(
onPressed: _uploading ? null : _onSubmit, // null = 禁用(置灰)
child: Text(_uploading ? '上传中...' : '上传笔记(${_images.length}/$_maxCount)'),
)
onPressed 传 null 就是禁用,这是 Flutter 的通用约定(比 Vue 的 :disabled 更内建)。
四、关键代码剖析
Future<void> _onAddTap() async {
final picked = await PickerService.instance.showPickSheet(
context,
maxCount: _maxCount - _images.length, // 剩余可选张数(限总数 9)
);
if (picked == null || picked.isEmpty) return; // 用户取消
setState(() => _images.addAll(picked)); // 不可变对象直接合并
}
提交时预留了上传接口位(等接口文档到了替换 TODO):
// TODO: 上传接口待用户提供(UploadApi.uploadImages 已预留位置)
// final urls = await UploadApi.instance.uploadImages(_images);
// 成功后把 urls 提交给"创建笔记"接口,再 Navigator.pop 返回。
await Future<void>.delayed(const Duration(milliseconds: 500)); // 模拟请求
五、踩坑记录
- 千万别在 GridView 里用
Image.file判断平台——dart:io在 Web 编译不过,必须用AssetEntityImage(原生)/Image.memory(Web); PickedImage是不可变对象:直接addAll进列表再setState即可,不需要深拷贝;- 上传前校验:没选图时点击要提示,别发空请求;
setState里_images.removeAt(index)即可删图,网格随itemCount变化自动收缩;- 满 9 张时加号格用
SizedBox.shrink()占位而不是直接不渲染,保持网格布局稳定。
六、扩展思考
- 选图弹层本身是另一个封装(
showPickSheet内部是showModalBottomSheet),见 smooth_sheet_demo.md 的"模态 vs 非模态"对比; - 上传动作的通用包装(loading 防重入、finally 复位)见 share_demo.md 的
_sharing模式; - 真实项目:把 TODO 处换成
UploadApi.uploadImages,再把返回的 url 列表交给"创建笔记"接口——本页结构不用动。