From 29d9e7917691e987c985aefacd61a985b10da614 Mon Sep 17 00:00:00 2001 From: hitozhang Date: Mon, 20 Jan 2025 21:25:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(upload):=20upload=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AE=BD=E9=AB=98=E8=AE=BE=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E6=9B=BF=E6=8D=A2=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/lib/page/td_upload_page.dart | 102 +++++++------ .../lib/src/components/upload/td_upload.dart | 137 ++++++++++++++---- tdesign-site/src/upload/README.md | 5 +- 3 files changed, 175 insertions(+), 69 deletions(-) diff --git a/tdesign-component/example/lib/page/td_upload_page.dart b/tdesign-component/example/lib/page/td_upload_page.dart index 3c8693a73..cdda3e5c7 100644 --- a/tdesign-component/example/lib/page/td_upload_page.dart +++ b/tdesign-component/example/lib/page/td_upload_page.dart @@ -14,52 +14,39 @@ class TDUploadPage extends StatefulWidget { class TDUploadState extends State { final List files1 = []; final List files2 = [ - TDUploadFile( - key: 1, - remotePath: - 'https://tdesign.gtimg.com/demo/images/example1.png'), - TDUploadFile( - key: 2, - remotePath: - 'https://tdesign.gtimg.com/demo/images/example2.png'), - TDUploadFile( - key: 3, - remotePath: - 'https://tdesign.gtimg.com/demo/images/example3.png'), + TDUploadFile(key: 1, remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'), + TDUploadFile(key: 2, remotePath: 'https://tdesign.gtimg.com/demo/images/example2.png'), + TDUploadFile(key: 3, remotePath: 'https://tdesign.gtimg.com/demo/images/example3.png'), ]; final List files3 = [ TDUploadFile( key: 1, status: TDUploadFileStatus.loading, loadingText: '上传中...', - remotePath: - 'https://tdesign.gtimg.com/demo/images/example1.png'), + remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'), TDUploadFile( key: 2, status: TDUploadFileStatus.loading, progress: 68, - remotePath: - 'https://tdesign.gtimg.com/demo/images/example1.png'), + remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'), ]; final List files4 = [ TDUploadFile( key: 1, status: TDUploadFileStatus.retry, retryText: '重新上传', - remotePath: - 'https://tdesign.gtimg.com/demo/images/example1.png'), + remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'), ]; final List files5 = [ TDUploadFile( key: 1, status: TDUploadFileStatus.error, errorText: '上传失败', - remotePath: - 'https://tdesign.gtimg.com/demo/images/example4.png'), + remotePath: 'https://tdesign.gtimg.com/demo/images/example4.png'), ]; + final List files6 = []; - void onValueChanged(List fileList, List value, - TDUploadType event) { + void onValueChanged(List fileList, List value, TDUploadType event) { switch (event) { case TDUploadType.add: setState(() { @@ -71,6 +58,15 @@ class TDUploadState extends State { fileList.removeWhere((element) => element.key == value[0].key); }); break; + case TDUploadType.replace: + setState(() { + final firstReplaceFile = value.first; + final index = fileList.indexWhere((file) => file.key == firstReplaceFile.key); + if (index != -1) { + fileList[index] = firstReplaceFile; + } + }); + break; } } @@ -85,26 +81,31 @@ class TDUploadState extends State { @override Widget build(BuildContext context) { return ExamplePage( - title: tdTitle(), - exampleCodeGroup: 'upload', - desc: '用于相册读取或拉起拍照的图片上传功能。${PlatformUtil.isWeb ? "Web端不支持读取本地图片,请前往移动端体验。" : ""}', - children: [ - ExampleModule( - title: '组件类型', - children: [ - ExampleItem(desc: '单选上传', builder: _uploadSingle), - ExampleItem(desc: '多选上传', builder: _uploadMultiple), - ], - ), - ExampleModule( - title: '组件状态', - children: [ - ExampleItem(desc: '加载状态', builder: _uploadLoading), - ExampleItem(desc: '重新上传', builder: _uploadRetry), - ExampleItem(desc: '上传失败', builder: _uploadError), - ], - ), - ]); + title: tdTitle(), + exampleCodeGroup: 'upload', + desc: '用于相册读取或拉起拍照的图片上传功能。${PlatformUtil.isWeb ? "Web端不支持读取本地图片,请前往移动端体验。" : ""}', + children: [ + ExampleModule( + title: '组件类型', + children: [ + ExampleItem(desc: '单选上传', builder: _uploadSingle), + ExampleItem(desc: '单选上传(替换)', builder: _uploadSingleWithReplace), + ExampleItem(desc: '多选上传', builder: _uploadMultiple), + ], + ), + ExampleModule( + title: '组件状态', + children: [ + ExampleItem(desc: '加载状态', builder: _uploadLoading), + ExampleItem(desc: '重新上传', builder: _uploadRetry), + ExampleItem(desc: '上传失败', builder: _uploadError), + ], + ), + ], + test: [ + ExampleItem(ignoreCode: true, desc: '单选快速替换, 大小和图形测试', builder: _uploadSingleWithReplace), + ], + ); } Widget wrapDemoContainer(String title, {required Widget child}) { @@ -140,6 +141,23 @@ class TDUploadState extends State { )); } + @Demo(group: 'upload') + Widget _uploadSingleWithReplace(BuildContext context) { + return wrapDemoContainer('单选上传(替换)', + child: TDUpload( + files: files6, + width: 60, + height: 60, + type: TDUploadBoxType.circle, + enabledReplaceType: true, + onClick: onClick, + onCancel: onCancel, + onError: print, + onValidate: print, + onChange: ((files, type) => onValueChanged(files6, files, type)), + )); + } + @Demo(group: 'upload') Widget _uploadMultiple(BuildContext context) { return wrapDemoContainer('多选上传', diff --git a/tdesign-component/lib/src/components/upload/td_upload.dart b/tdesign-component/lib/src/components/upload/td_upload.dart index 6c5368c77..99339b5ed 100644 --- a/tdesign-component/lib/src/components/upload/td_upload.dart +++ b/tdesign-component/lib/src/components/upload/td_upload.dart @@ -26,6 +26,12 @@ enum TDUploadFileStatus { enum TDUploadType { add, // 添加 remove, // 删除 + replace, // 替换 +} + +enum TDUploadBoxType { + roundedSquare, // 圆角方形 + circle, // 圆形 } class TDUploadFile { @@ -70,7 +76,11 @@ class TDUpload extends StatefulWidget { this.onClick, required this.files, this.onChange, - this.multiple = false}) + this.multiple = false, + this.width = 80.0, + this.height = 80.0, + this.type = TDUploadBoxType.roundedSquare, + this.enabledReplaceType = false}) : super(key: key); /// 控制展示的文件列表 @@ -100,9 +110,21 @@ class TDUpload extends StatefulWidget { /// 监听点击图片位 final TDUploadClickEvent? onClick; - /// 监听添加或删除照片 + /// 监听添加, 删除和替换media事件 final TDUploadValueChangedEvent? onChange; + /// 图片宽度 + final double? width; + + /// 图片高度 + final double? height; + + /// Box类型 + final TDUploadBoxType type; + + /// 是否启用replace功能 + final bool? enabledReplaceType; + @override State createState() => _TDUploadState(); } @@ -113,6 +135,12 @@ class _TDUploadState extends State { bool get canUpload => widget.multiple ? (widget.max == 0 ? true : fileList.length < widget.max) : fileList.isEmpty; final ImagePicker _picker = ImagePicker(); + // 类型映射 + final Map _imageTypeMap = { + TDUploadBoxType.roundedSquare: TDImageType.roundedSquare, + TDUploadBoxType.circle: TDImageType.circle, + }; + @override initState() { super.initState(); @@ -120,15 +148,15 @@ class _TDUploadState extends State { } // 获取相册照片或视频 - Future> getMediaFromPicker() async { - if (!canUpload || widget.mediaType.isEmpty) { + Future> getMediaFromPicker(bool isMultiple) async { + if (widget.mediaType.isEmpty) { return []; } List medias; try { - if (widget.multiple) { + if (isMultiple) { if (widget.mediaType.length == 1 && widget.mediaType.contains(TDUploadMediaType.image)) { medias = await _picker.pickMultiImage(); } else { @@ -194,11 +222,39 @@ class _TDUploadState extends State { } } + // 替换资源 + void replaceMedia(List files, TDUploadFile oldFile) async { + if (files.isEmpty || files.length != 1) { + return; + } + + var result = await validateResources(files); + + if (result != null) { + if (widget.onValidate != null) { + widget.onValidate!(result); + } + return; + } + + var newFile = TDUploadFile(key: oldFile.key, file: File(files[0].path), assetPath: files[0].path); + + if (widget.onChange != null) { + widget.onChange!([newFile], TDUploadType.replace); + } + } + // 校验资源 - Future validateResources(List files) async { + Future validateResources(List files, [bool? multiple]) async { TDUploadValidatorError? error; - if (widget.multiple && widget.max > 0) { + // 多选逻辑,优选从参数获取 + var isMultiple = widget.multiple; + if (multiple != null) { + isMultiple = multiple; + } + + if (isMultiple && widget.max > 0) { var remain = widget.max - fileList.length; if (files.length > remain) { @@ -238,7 +294,11 @@ class _TDUploadState extends State { children: [ ...fileList.map((file) => _buildImageBox(context, file)).toList(), _buildUploadBox(context, shouldDisplay: canUpload, onTap: () async { - final files = await getMediaFromPicker(); + if (!canUpload) { + return; + } + + final files = await getMediaFromPicker(widget.multiple); extractImageList(files); }), ], @@ -252,9 +312,14 @@ class _TDUploadState extends State { child: GestureDetector( onTap: onTap, child: Container( - width: 80, - height: 80, - decoration: BoxDecoration(color: TDTheme.of(context).grayColor1, borderRadius: BorderRadius.circular(6)), + width: widget.width, + height: widget.height, + decoration: widget.type == TDUploadBoxType.circle + ? BoxDecoration( + shape: BoxShape.circle, + color: TDTheme.of(context).grayColor1, + ) + : BoxDecoration(color: TDTheme.of(context).grayColor1, borderRadius: BorderRadius.circular(6)), child: const Center( child: Icon( TDIcons.add, @@ -266,19 +331,26 @@ class _TDUploadState extends State { Widget _buildImageBox(BuildContext context, TDUploadFile file) { return GestureDetector( - onTap: () { + onTap: () async { if (widget.onClick != null) { widget.onClick!(file.key); } + // 替换资源 + if (widget.enabledReplaceType ?? false) { + final files = await getMediaFromPicker(false); + replaceMedia(files, file); + } }, child: Stack( children: [ TDImage( - width: 80, - height: 80, + key: Key(file.assetPath ?? ''), + width: widget.width, + height: widget.height, imgUrl: file.remotePath, // assetUrl: file.assetPath, imageFile: file.file, + type: _imageTypeMap[widget.type] ?? TDImageType.roundedSquare, ), Visibility(visible: file.status != TDUploadFileStatus.success, child: _buildShadowBox(file)), Visibility( @@ -293,10 +365,15 @@ class _TDUploadState extends State { child: Container( width: 20, height: 20, - decoration: const BoxDecoration( - color: Color.fromRGBO(0, 0, 0, 0.6), - borderRadius: - BorderRadius.only(bottomLeft: Radius.circular(6), topRight: Radius.circular(6))), + decoration: widget.type == TDUploadBoxType.circle + ? const BoxDecoration( + shape: BoxShape.circle, + color: Color.fromRGBO(0, 0, 0, 0.6), + ) + : const BoxDecoration( + color: Color.fromRGBO(0, 0, 0, 0.6), + borderRadius: + BorderRadius.only(bottomLeft: Radius.circular(6), topRight: Radius.circular(6))), child: const Center( child: Icon( TDIcons.close, @@ -326,14 +403,19 @@ class _TDUploadState extends State { } return Container( - width: 80, - height: 80, - decoration: BoxDecoration(color: const Color.fromRGBO(0, 0, 0, 0.4), borderRadius: BorderRadius.circular(6)), + width: widget.width, + height: widget.height, + decoration: widget.type == TDUploadBoxType.circle + ? const BoxDecoration( + shape: BoxShape.circle, + color: Color.fromRGBO(0, 0, 0, 0.4), + ) + : BoxDecoration(color: const Color.fromRGBO(0, 0, 0, 0.4), borderRadius: BorderRadius.circular(6)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, children: [ Visibility( visible: file.status == TDUploadFileStatus.loading, @@ -350,10 +432,13 @@ class _TDUploadState extends State { size: 24, color: Colors.white, )), - TDText( - displayText, - textColor: Colors.white, - style: const TextStyle(fontSize: 12, height: 1.67), + Padding( + padding: const EdgeInsets.only(top: 4), + child: TDText( + displayText, + textColor: Colors.white, + style: const TextStyle(fontSize: 12, height: 1.67), + ), ), ], ), diff --git a/tdesign-site/src/upload/README.md b/tdesign-site/src/upload/README.md index 4482770d2..ebdf720fe 100644 --- a/tdesign-site/src/upload/README.md +++ b/tdesign-site/src/upload/README.md @@ -147,6 +147,9 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; | files | List | - | 控制展示的文件列表 | | onChange | TDUploadValueChangedEvent? | - | 监听添加或删除照片 | | multiple | bool | false | 是否多选上传,默认false | - +| width | double? | - | 图片宽度 | +| height | double? | - | 图片高度 | +| type | TDUploadBoxType? | - | Box类型 | +| enabledReplaceType | bool? | false | 是否启用replace功能 | \ No newline at end of file