flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
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),
),
),
),
],
);
}
}