Files
2026-09-04 15:22:20 +08:00

193 lines
6.4 KiB
Dart
Raw Permalink 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.
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),
),
),
),
],
);
}
}