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
+257
View File
@@ -0,0 +1,257 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:new_learn/core/network/oss_api.dart';
import 'package:new_learn/core/network/oss_uploader.dart';
import 'package:new_learn/core/services/picker_service.dart';
/// ============================================================
/// 【OssUploadDemoPage】OSS 直传 Demo(客户端签名 + PUT 上传)
///
/// 一、演示链路(与 UniApp ossUpload.js 完全对照):
/// 选图 → 后端分配路径(GetXxxOSSUploadUrl) → 拿 STS(GetAliOSSSetting)
/// → V4 签名 → PUT 直传 OSS → 显示返回的 CDN URL + 上传进度。
///
/// 二、双端读取图片字节:
/// web :PickedImage.bytes(image_picker 已读好)
/// 原生 :AssetEntity.originBytes(photo_manager 纯 Dart API)
/// 这样本页不用 import dart:io,Web 也能编译。
///
/// 三、注意:
/// 本 demo 需要后端 OSS 接口可访问(本地/测试环境),
/// 403 = 签名或 STS 有问题(看 LogInterceptor 打印的响应体定位)。
/// ============================================================
class OssUploadDemoPage extends StatefulWidget {
const OssUploadDemoPage({super.key});
@override
State<OssUploadDemoPage> createState() => _OssUploadDemoPageState();
}
class _OssUploadDemoPageState extends State<OssUploadDemoPage> {
UploadScene _scene = UploadScene.avatar;
PickedImage? _image;
bool _uploading = false;
double _progress = 0;
String? _resultUrl;
String? _error;
/// 场景显示名(对应 UniApp mode)
static String _sceneName(UploadScene s) => switch (s) {
UploadScene.avatar => '头像',
UploadScene.note => '笔记',
UploadScene.aiGrading => 'AI判题',
};
/// 选图(单张)
Future<void> _pick() async {
final picked =
await PickerService.instance.showPickSheet(context, maxCount: 1);
if (picked == null || picked.isEmpty) return;
setState(() {
_image = picked.first;
_resultUrl = null;
_error = null;
_progress = 0;
});
}
/// 上传:读字节 → 4 步链路直传 OSS
Future<void> _upload() async {
final image = _image;
if (image == null) return;
// 读图片字节:web 用 bytes,原生用 AssetEntity.originBytes(无需 dart:io)
final Uint8List bytes = image.bytes ??
await image.entity?.originBytes ??
Uint8List(0);
if (bytes.isEmpty) {
setState(() => _error = '无法读取图片数据,请重新选择');
return;
}
final fileName =
image.name ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg';
setState(() {
_uploading = true;
_progress = 0;
_error = null;
_resultUrl = null;
});
try {
final url = await OssUploader.uploadImage(
bytes: bytes,
fileName: fileName,
scene: _scene,
onSendProgress: (sent, total) {
if (total > 0) {
setState(() => _progress = sent / total);
}
},
);
if (!mounted) return;
setState(() {
_uploading = false;
_progress = 1;
_resultUrl = url;
});
} catch (e) {
if (!mounted) return;
setState(() {
_uploading = false;
_error = e.toString();
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('OSS 直传'), centerTitle: true),
body: ListView(
padding: EdgeInsets.all(16.w),
children: [
_tip(
'链路:选图 → 后端分配路径(GetXxxOSSUploadUrl) → 拿STS'
'(GetAliOSSSetting) → V4签名 → PUT直传OSS。\n'
'需要后端 OSS 接口可访问;403 看控制台响应体定位签名问题。',
),
SizedBox(height: 20.h),
// ─── 场景选择 ───
Text('上传场景',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF333333))),
SizedBox(height: 8.h),
Wrap(
spacing: 10.w,
children: UploadScene.values
.map((s) => ChoiceChip(
label: Text(_sceneName(s)),
selected: _scene == s,
onSelected: _uploading
? null
: (_) => setState(() => _scene = s),
))
.toList(),
),
SizedBox(height: 20.h),
// ─── 已选图片 ───
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: const Color(0xFFEEF1F5)),
),
child: _image == null
? Text('未选择图片',
style: TextStyle(
fontSize: 13.sp, color: const Color(0xFF999999)))
: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: PickerService.buildThumb(
_image!,
width: 80.w,
height: 80.h,
),
),
SizedBox(width: 12.w),
Expanded(
child: Text(
_image!.name ?? '已选择图片',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13.sp, color: const Color(0xFF333333)),
),
),
],
),
),
SizedBox(height: 12.h),
// ─── 操作按钮 ───
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _uploading ? null : _pick,
icon: const Icon(Icons.photo_library_outlined),
label: const Text('选择图片'),
),
),
SizedBox(width: 12.w),
Expanded(
child: FilledButton.icon(
onPressed: (_image == null || _uploading) ? null : _upload,
icon: const Icon(Icons.cloud_upload_outlined),
label: Text(_uploading ? '上传中...' : '上传到 OSS'),
),
),
],
),
SizedBox(height: 20.h),
// ─── 进度条 ───
if (_uploading) ...[
LinearProgressIndicator(value: _progress, minHeight: 6),
SizedBox(height: 6.h),
Text(
'${(_progress * 100).toStringAsFixed(0)}%',
style:
TextStyle(fontSize: 12.sp, color: const Color(0xFF999999)),
),
SizedBox(height: 20.h),
],
// ─── 结果 ───
if (_resultUrl != null) ...[
_tip('上传成功!访问地址:', icon: Icons.check_circle, color: const Color(0xFF10B981)),
SizedBox(height: 8.h),
SelectableText(
_resultUrl!,
style: TextStyle(fontSize: 12.sp, color: const Color(0xFF3B82F6)),
),
],
if (_error != null) ...[
_tip('上传失败:$_error',
icon: Icons.error_outline, color: const Color(0xFFEF4444)),
],
],
),
);
}
Widget _tip(String text, {IconData? icon, Color? color}) {
final accent = color ?? const Color(0xFF3B82F6);
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: const Color(0xFFE8F0FE),
borderRadius: BorderRadius.circular(10.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null) ...[
Icon(icon, size: 18, color: accent),
SizedBox(width: 8.w),
],
Expanded(
child: Text(
text,
style: TextStyle(fontSize: 12.sp, color: const Color(0xFF666666)),
),
),
],
),
);
}
}