193 lines
6.4 KiB
Dart
193 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
import 'package:new_learn/core/services/picker_service.dart';
|
||
|
||
/// ============================================================
|
||
/// 【NotePickerPage】笔记图片选择页(学生上传笔记:多张图)
|
||
///
|
||
/// 一、本页解决什么问题:
|
||
/// 笔记上传前要先"选图 + 预览 + 确认",这就是一个真实的
|
||
/// 「多图选择页」:网格缩略图 + 右下角加号 + 底部提交按钮。
|
||
///
|
||
/// 二、和普通图片组件的区别:
|
||
/// 1. 选图走 PickerService(Web 自动降级 image_picker,
|
||
/// 原生走 wechat_assets_picker 微信风格相册,可多选);
|
||
/// 2. 缩略图双端统一:
|
||
/// - 原生 → AssetEntityImage(实体缩略图,秒开不卡顿)
|
||
/// - Web → Image.memory(字节)
|
||
/// 3. 提交按钮已预留 UploadApi.uploadImages 占位,
|
||
/// 等用户给上传接口文档后,替换 TODO 处即可打通。
|
||
///
|
||
/// 三、高频坑:
|
||
/// 1. 千万别在 GridView 里用 Image.file 判断平台——
|
||
/// dart:io 在 Web 编译不过,必须用 AssetEntityImage / Image.memory;
|
||
/// 2. 选择后要 setState 刷新网格,PickedImage 是不可变对象,
|
||
/// 直接 add 进列表再 setState 即可,不需要深拷贝;
|
||
/// 3. 上传前记得校验:没选图时按钮要置灰/提示,别发空请求。
|
||
/// ============================================================
|
||
class NotePickerPage extends StatefulWidget {
|
||
const NotePickerPage({super.key});
|
||
|
||
@override
|
||
State<NotePickerPage> createState() => _NotePickerPageState();
|
||
}
|
||
|
||
class _NotePickerPageState extends State<NotePickerPage> {
|
||
/// 已选中的图片(最多 9 张)
|
||
final List<PickedImage> _images = [];
|
||
|
||
/// 是否正在上传
|
||
bool _uploading = false;
|
||
|
||
/// 最大可选数
|
||
static const int _maxCount = 9;
|
||
|
||
/// 点击加号:弹「拍照/相册」底部弹层
|
||
Future<void> _onAddTap() async {
|
||
final picked = await PickerService.instance.showPickSheet(
|
||
context,
|
||
maxCount: _maxCount - _images.length, // 剩余可选张数
|
||
);
|
||
if (picked == null || picked.isEmpty) return;
|
||
setState(() => _images.addAll(picked));
|
||
}
|
||
|
||
/// 删除某张图
|
||
void _onRemoveTap(int index) {
|
||
setState(() => _images.removeAt(index));
|
||
}
|
||
|
||
/// 提交:预留上传接口位置(TODO: 用户提供接口文档后替换)
|
||
Future<void> _onSubmit() async {
|
||
if (_images.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('请先选择笔记图片')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
setState(() => _uploading = true);
|
||
|
||
// TODO: 上传接口待用户提供(UploadApi.uploadImages 已预留位置)
|
||
// 接口给到后:
|
||
// final urls = await UploadApi.instance.uploadImages(_images);
|
||
// 成功后把 urls 提交给"创建笔记"接口,再 Navigator.pop 返回。
|
||
await Future<void>.delayed(const Duration(milliseconds: 500)); // 模拟请求
|
||
if (!mounted) return;
|
||
setState(() => _uploading = false);
|
||
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('已选 ${_images.length} 张图(上传接口待接入)'),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 渲染单张缩略图(双端统一,走 PickerService)
|
||
Widget _buildThumb(PickedImage image) {
|
||
return PickerService.buildThumb(image);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text('笔记图片选择'),
|
||
centerTitle: true,
|
||
),
|
||
body: Column(
|
||
children: [
|
||
Expanded(
|
||
child: GridView.builder(
|
||
padding: EdgeInsets.all(16.w),
|
||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 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);
|
||
},
|
||
),
|
||
),
|
||
// 底部提交按钮
|
||
Padding(
|
||
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 20.h),
|
||
child: SizedBox(
|
||
width: double.infinity,
|
||
height: 46.h,
|
||
child: FilledButton(
|
||
onPressed: _uploading ? null : _onSubmit,
|
||
style: FilledButton.styleFrom(
|
||
backgroundColor: const Color(0xFF3B82F6),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(23.r),
|
||
),
|
||
),
|
||
child: Text(
|
||
_uploading ? '上传中...' : '上传笔记(${_images.length}/$_maxCount)',
|
||
style: TextStyle(fontSize: 15.sp, color: Colors.white),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 加号格子
|
||
Widget _buildAddCell() {
|
||
return GestureDetector(
|
||
onTap: _onAddTap,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF5F7FA),
|
||
borderRadius: BorderRadius.circular(10.r),
|
||
border: Border.all(color: const Color(0xFFE3E8EF)),
|
||
),
|
||
child: const Icon(Icons.add, size: 34, color: Color(0xFFB0B8C4)),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 单张已选图片格子(右上角删除角标)
|
||
Widget _buildImageCell(int index) {
|
||
final image = _images[index];
|
||
return Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(10.r),
|
||
child: _buildThumb(image),
|
||
),
|
||
// 右上角删除按钮
|
||
Positioned(
|
||
top: 4.h,
|
||
right: 4.w,
|
||
child: GestureDetector(
|
||
onTap: () => _onRemoveTap(index),
|
||
child: Container(
|
||
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),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|