Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(upload): upload组件支持宽高设置和快速替换配置 #462

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 60 additions & 42 deletions tdesign-component/example/lib/page/td_upload_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,52 +14,39 @@ class TDUploadPage extends StatefulWidget {
class TDUploadState extends State<TDUploadPage> {
final List<TDUploadFile> files1 = [];
final List<TDUploadFile> 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<TDUploadFile> 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<TDUploadFile> 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<TDUploadFile> 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<TDUploadFile> files6 = [];

void onValueChanged(List<TDUploadFile> fileList, List<TDUploadFile> value,
TDUploadType event) {
void onValueChanged(List<TDUploadFile> fileList, List<TDUploadFile> value, TDUploadType event) {
switch (event) {
case TDUploadType.add:
setState(() {
Expand All @@ -71,6 +58,15 @@ class TDUploadState extends State<TDUploadPage> {
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;
}
}

Expand All @@ -85,26 +81,31 @@ class TDUploadState extends State<TDUploadPage> {
@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}) {
Expand Down Expand Up @@ -140,6 +141,23 @@ class TDUploadState extends State<TDUploadPage> {
));
}

@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('多选上传',
Expand Down
137 changes: 111 additions & 26 deletions tdesign-component/lib/src/components/upload/td_upload.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ enum TDUploadFileStatus {
enum TDUploadType {
add, // 添加
remove, // 删除
replace, // 替换
}

enum TDUploadBoxType {
roundedSquare, // 圆角方形
circle, // 圆形
}

class TDUploadFile {
Expand Down Expand Up @@ -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);

/// 控制展示的文件列表
Expand Down Expand Up @@ -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<TDUpload> createState() => _TDUploadState();
}
Expand All @@ -113,22 +135,28 @@ class _TDUploadState extends State<TDUpload> {
bool get canUpload => widget.multiple ? (widget.max == 0 ? true : fileList.length < widget.max) : fileList.isEmpty;
final ImagePicker _picker = ImagePicker();

// 类型映射
final Map<TDUploadBoxType, TDImageType> _imageTypeMap = {
TDUploadBoxType.roundedSquare: TDImageType.roundedSquare,
TDUploadBoxType.circle: TDImageType.circle,
};

@override
initState() {
super.initState();
fileList = widget.files;
}

// 获取相册照片或视频
Future<List<XFile>> getMediaFromPicker() async {
if (!canUpload || widget.mediaType.isEmpty) {
Future<List<XFile>> getMediaFromPicker(bool isMultiple) async {
if (widget.mediaType.isEmpty) {
return [];
}

List<XFile> medias;

try {
if (widget.multiple) {
if (isMultiple) {
if (widget.mediaType.length == 1 && widget.mediaType.contains(TDUploadMediaType.image)) {
medias = await _picker.pickMultiImage();
} else {
Expand Down Expand Up @@ -194,11 +222,39 @@ class _TDUploadState extends State<TDUpload> {
}
}

// 替换资源
void replaceMedia(List<XFile> 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<TDUploadValidatorError?> validateResources(List<XFile> files) async {
Future<TDUploadValidatorError?> validateResources(List<XFile> 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) {
Expand Down Expand Up @@ -238,7 +294,11 @@ class _TDUploadState extends State<TDUpload> {
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);
}),
],
Expand All @@ -252,9 +312,14 @@ class _TDUploadState extends State<TDUpload> {
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,
Expand All @@ -266,19 +331,26 @@ class _TDUploadState extends State<TDUpload> {

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(
Expand All @@ -293,10 +365,15 @@ class _TDUploadState extends State<TDUpload> {
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,
Expand Down Expand Up @@ -326,14 +403,19 @@ class _TDUploadState extends State<TDUpload> {
}

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,
Expand All @@ -350,10 +432,13 @@ class _TDUploadState extends State<TDUpload> {
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),
),
),
],
),
Expand Down
Loading
Loading