Files
App.K12.Student.Miniapp_Flu…/docs/demos/note_picker_demo.md
T
2026-09-04 15:22:20 +08:00

141 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】笔记图片选择页(多图选择 + 缩略图网格 + 删除角标)
> 源文件:`lib/pages/demo/note_picker_demo.dart` | 分类:组件封装
> 一句话:学生上传笔记前的"选图 + 预览 + 确认"页 —— 网格缩略图、右下角加号、底部提交按钮。
---
## 一、这个 Demo 解决什么问题
笔记上传前要先选图、预览、确认。本页是一个真实的**多图选择页**:
```
选图(PickerService 弹层)→ 网格缩略图预览 → 删除不满意的 → 底部提交(预留上传接口)
```
它演示了三个可复用的套路:
1. **加号格子**:GridView 比图片多一格,点它弹选图层;
2. **删除角标**:每张图右上角一个小圆叉(Stack + Positioned);
3. **双端缩略图**:同一份代码在原生 / Web 都能显示缩略图(`PickerService.buildThumb`)。
> 对照 Vue:相当于"el-upload 多图模式 + 预览删除"的前端部分,上传接口那步留了 TODO。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: NotePickerPage()));
```
- 点加号 → 弹「拍照/相册」底部弹层(原生是微信风格相册,可多选;Web 降级 image_picker);
- 选完图回到网格,右上角小叉可删;
- 选满 9 张后加号自动消失;
- 提交按钮显示 `上传笔记(n/9)`,未选图时点击提示"请先选择笔记图片"。
---
## 三、核心知识点(新手向)
### 1. 网格多一格 = 加号位
```dart
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
```dart
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. 双端缩略图:永远走服务层,别自己判断平台
```dart
Widget _buildThumb(PickedImage image) {
return PickerService.buildThumb(image);
// 原生 → AssetEntityImage(实体缩略图,秒开不卡顿)
// Web → Image.memory(字节)
}
```
### 4. 提交按钮的"禁用态"套路
```dart
FilledButton(
onPressed: _uploading ? null : _onSubmit, // null = 禁用(置灰)
child: Text(_uploading ? '上传中...' : '上传笔记(${_images.length}/$_maxCount)'),
)
```
`onPressed` 传 `null` 就是禁用,这是 Flutter 的通用约定(比 Vue 的 `:disabled` 更内建)。
---
## 四、关键代码剖析
```dart
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):
```dart
// TODO: 上传接口待用户提供(UploadApi.uploadImages 已预留位置)
// final urls = await UploadApi.instance.uploadImages(_images);
// 成功后把 urls 提交给"创建笔记"接口,再 Navigator.pop 返回。
await Future<void>.delayed(const Duration(milliseconds: 500)); // 模拟请求
```
---
## 五、踩坑记录
1. **千万别在 GridView 里用 `Image.file` 判断平台**——`dart:io` 在 Web 编译不过,必须用 `AssetEntityImage`(原生)/ `Image.memory`(Web);
2. `PickedImage` 是不可变对象:直接 `addAll` 进列表再 `setState` 即可,**不需要深拷贝**;
3. 上传前校验:没选图时点击要提示,别发空请求;
4. `setState` 里 `_images.removeAt(index)` 即可删图,网格随 `itemCount` 变化自动收缩;
5. 满 9 张时加号格用 `SizedBox.shrink()` 占位而不是直接不渲染,**保持网格布局稳定**。
---
## 六、扩展思考
- 选图弹层本身是另一个封装(`showPickSheet` 内部是 `showModalBottomSheet`),见 [smooth_sheet_demo.md](smooth_sheet_demo.md) 的"模态 vs 非模态"对比;
- 上传动作的通用包装(loading 防重入、finally 复位)见 [share_demo.md](share_demo.md) 的 `_sharing` 模式;
- 真实项目:把 TODO 处换成 `UploadApi.uploadImages`,再把返回的 url 列表交给"创建笔记"接口——本页结构不用动。