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 createState() => _NotePickerPageState(); } class _NotePickerPageState extends State { /// 已选中的图片(最多 9 张) final List _images = []; /// 是否正在上传 bool _uploading = false; /// 最大可选数 static const int _maxCount = 9; /// 点击加号:弹「拍照/相册」底部弹层 Future _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 _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.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), ), ), ), ], ); } }