Skip to content

Commit 1688e97

Browse files
authored
Merge pull request Tencent#462 from HubuHito/hito/feat/upload
feat(upload): upload组件支持宽高设置和快速替换配置
2 parents a117710 + 93fc4a6 commit 1688e97

3 files changed

Lines changed: 175 additions & 69 deletions

File tree

tdesign-component/example/lib/page/td_upload_page.dart

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,39 @@ class TDUploadPage extends StatefulWidget {
1414
class TDUploadState extends State<TDUploadPage> {
1515
final List<TDUploadFile> files1 = [];
1616
final List<TDUploadFile> files2 = [
17-
TDUploadFile(
18-
key: 1,
19-
remotePath:
20-
'https://tdesign.gtimg.com/demo/images/example1.png'),
21-
TDUploadFile(
22-
key: 2,
23-
remotePath:
24-
'https://tdesign.gtimg.com/demo/images/example2.png'),
25-
TDUploadFile(
26-
key: 3,
27-
remotePath:
28-
'https://tdesign.gtimg.com/demo/images/example3.png'),
17+
TDUploadFile(key: 1, remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'),
18+
TDUploadFile(key: 2, remotePath: 'https://tdesign.gtimg.com/demo/images/example2.png'),
19+
TDUploadFile(key: 3, remotePath: 'https://tdesign.gtimg.com/demo/images/example3.png'),
2920
];
3021
final List<TDUploadFile> files3 = [
3122
TDUploadFile(
3223
key: 1,
3324
status: TDUploadFileStatus.loading,
3425
loadingText: '上传中...',
35-
remotePath:
36-
'https://tdesign.gtimg.com/demo/images/example1.png'),
26+
remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'),
3727
TDUploadFile(
3828
key: 2,
3929
status: TDUploadFileStatus.loading,
4030
progress: 68,
41-
remotePath:
42-
'https://tdesign.gtimg.com/demo/images/example1.png'),
31+
remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'),
4332
];
4433
final List<TDUploadFile> files4 = [
4534
TDUploadFile(
4635
key: 1,
4736
status: TDUploadFileStatus.retry,
4837
retryText: '重新上传',
49-
remotePath:
50-
'https://tdesign.gtimg.com/demo/images/example1.png'),
38+
remotePath: 'https://tdesign.gtimg.com/demo/images/example1.png'),
5139
];
5240
final List<TDUploadFile> files5 = [
5341
TDUploadFile(
5442
key: 1,
5543
status: TDUploadFileStatus.error,
5644
errorText: '上传失败',
57-
remotePath:
58-
'https://tdesign.gtimg.com/demo/images/example4.png'),
45+
remotePath: 'https://tdesign.gtimg.com/demo/images/example4.png'),
5946
];
47+
final List<TDUploadFile> files6 = [];
6048

61-
void onValueChanged(List<TDUploadFile> fileList, List<TDUploadFile> value,
62-
TDUploadType event) {
49+
void onValueChanged(List<TDUploadFile> fileList, List<TDUploadFile> value, TDUploadType event) {
6350
switch (event) {
6451
case TDUploadType.add:
6552
setState(() {
@@ -71,6 +58,15 @@ class TDUploadState extends State<TDUploadPage> {
7158
fileList.removeWhere((element) => element.key == value[0].key);
7259
});
7360
break;
61+
case TDUploadType.replace:
62+
setState(() {
63+
final firstReplaceFile = value.first;
64+
final index = fileList.indexWhere((file) => file.key == firstReplaceFile.key);
65+
if (index != -1) {
66+
fileList[index] = firstReplaceFile;
67+
}
68+
});
69+
break;
7470
}
7571
}
7672

@@ -85,26 +81,31 @@ class TDUploadState extends State<TDUploadPage> {
8581
@override
8682
Widget build(BuildContext context) {
8783
return ExamplePage(
88-
title: tdTitle(),
89-
exampleCodeGroup: 'upload',
90-
desc: '用于相册读取或拉起拍照的图片上传功能。${PlatformUtil.isWeb ? "Web端不支持读取本地图片,请前往移动端体验。" : ""}',
91-
children: [
92-
ExampleModule(
93-
title: '组件类型',
94-
children: [
95-
ExampleItem(desc: '单选上传', builder: _uploadSingle),
96-
ExampleItem(desc: '多选上传', builder: _uploadMultiple),
97-
],
98-
),
99-
ExampleModule(
100-
title: '组件状态',
101-
children: [
102-
ExampleItem(desc: '加载状态', builder: _uploadLoading),
103-
ExampleItem(desc: '重新上传', builder: _uploadRetry),
104-
ExampleItem(desc: '上传失败', builder: _uploadError),
105-
],
106-
),
107-
]);
84+
title: tdTitle(),
85+
exampleCodeGroup: 'upload',
86+
desc: '用于相册读取或拉起拍照的图片上传功能。${PlatformUtil.isWeb ? "Web端不支持读取本地图片,请前往移动端体验。" : ""}',
87+
children: [
88+
ExampleModule(
89+
title: '组件类型',
90+
children: [
91+
ExampleItem(desc: '单选上传', builder: _uploadSingle),
92+
ExampleItem(desc: '单选上传(替换)', builder: _uploadSingleWithReplace),
93+
ExampleItem(desc: '多选上传', builder: _uploadMultiple),
94+
],
95+
),
96+
ExampleModule(
97+
title: '组件状态',
98+
children: [
99+
ExampleItem(desc: '加载状态', builder: _uploadLoading),
100+
ExampleItem(desc: '重新上传', builder: _uploadRetry),
101+
ExampleItem(desc: '上传失败', builder: _uploadError),
102+
],
103+
),
104+
],
105+
test: [
106+
ExampleItem(ignoreCode: true, desc: '单选快速替换, 大小和图形测试', builder: _uploadSingleWithReplace),
107+
],
108+
);
108109
}
109110

110111
Widget wrapDemoContainer(String title, {required Widget child}) {
@@ -140,6 +141,23 @@ class TDUploadState extends State<TDUploadPage> {
140141
));
141142
}
142143

144+
@Demo(group: 'upload')
145+
Widget _uploadSingleWithReplace(BuildContext context) {
146+
return wrapDemoContainer('单选上传(替换)',
147+
child: TDUpload(
148+
files: files6,
149+
width: 60,
150+
height: 60,
151+
type: TDUploadBoxType.circle,
152+
enabledReplaceType: true,
153+
onClick: onClick,
154+
onCancel: onCancel,
155+
onError: print,
156+
onValidate: print,
157+
onChange: ((files, type) => onValueChanged(files6, files, type)),
158+
));
159+
}
160+
143161
@Demo(group: 'upload')
144162
Widget _uploadMultiple(BuildContext context) {
145163
return wrapDemoContainer('多选上传',

tdesign-component/lib/src/components/upload/td_upload.dart

Lines changed: 111 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ enum TDUploadFileStatus {
2626
enum TDUploadType {
2727
add, // 添加
2828
remove, // 删除
29+
replace, // 替换
30+
}
31+
32+
enum TDUploadBoxType {
33+
roundedSquare, // 圆角方形
34+
circle, // 圆形
2935
}
3036

3137
class TDUploadFile {
@@ -70,7 +76,11 @@ class TDUpload extends StatefulWidget {
7076
this.onClick,
7177
required this.files,
7278
this.onChange,
73-
this.multiple = false})
79+
this.multiple = false,
80+
this.width = 80.0,
81+
this.height = 80.0,
82+
this.type = TDUploadBoxType.roundedSquare,
83+
this.enabledReplaceType = false})
7484
: super(key: key);
7585

7686
/// 控制展示的文件列表
@@ -100,9 +110,21 @@ class TDUpload extends StatefulWidget {
100110
/// 监听点击图片位
101111
final TDUploadClickEvent? onClick;
102112

103-
/// 监听添加或删除照片
113+
/// 监听添加, 删除和替换media事件
104114
final TDUploadValueChangedEvent? onChange;
105115

116+
/// 图片宽度
117+
final double? width;
118+
119+
/// 图片高度
120+
final double? height;
121+
122+
/// Box类型
123+
final TDUploadBoxType type;
124+
125+
/// 是否启用replace功能
126+
final bool? enabledReplaceType;
127+
106128
@override
107129
State<TDUpload> createState() => _TDUploadState();
108130
}
@@ -113,22 +135,28 @@ class _TDUploadState extends State<TDUpload> {
113135
bool get canUpload => widget.multiple ? (widget.max == 0 ? true : fileList.length < widget.max) : fileList.isEmpty;
114136
final ImagePicker _picker = ImagePicker();
115137

138+
// 类型映射
139+
final Map<TDUploadBoxType, TDImageType> _imageTypeMap = {
140+
TDUploadBoxType.roundedSquare: TDImageType.roundedSquare,
141+
TDUploadBoxType.circle: TDImageType.circle,
142+
};
143+
116144
@override
117145
initState() {
118146
super.initState();
119147
fileList = widget.files;
120148
}
121149

122150
// 获取相册照片或视频
123-
Future<List<XFile>> getMediaFromPicker() async {
124-
if (!canUpload || widget.mediaType.isEmpty) {
151+
Future<List<XFile>> getMediaFromPicker(bool isMultiple) async {
152+
if (widget.mediaType.isEmpty) {
125153
return [];
126154
}
127155

128156
List<XFile> medias;
129157

130158
try {
131-
if (widget.multiple) {
159+
if (isMultiple) {
132160
if (widget.mediaType.length == 1 && widget.mediaType.contains(TDUploadMediaType.image)) {
133161
medias = await _picker.pickMultiImage();
134162
} else {
@@ -194,11 +222,39 @@ class _TDUploadState extends State<TDUpload> {
194222
}
195223
}
196224

225+
// 替换资源
226+
void replaceMedia(List<XFile> files, TDUploadFile oldFile) async {
227+
if (files.isEmpty || files.length != 1) {
228+
return;
229+
}
230+
231+
var result = await validateResources(files);
232+
233+
if (result != null) {
234+
if (widget.onValidate != null) {
235+
widget.onValidate!(result);
236+
}
237+
return;
238+
}
239+
240+
var newFile = TDUploadFile(key: oldFile.key, file: File(files[0].path), assetPath: files[0].path);
241+
242+
if (widget.onChange != null) {
243+
widget.onChange!([newFile], TDUploadType.replace);
244+
}
245+
}
246+
197247
// 校验资源
198-
Future<TDUploadValidatorError?> validateResources(List<XFile> files) async {
248+
Future<TDUploadValidatorError?> validateResources(List<XFile> files, [bool? multiple]) async {
199249
TDUploadValidatorError? error;
200250

201-
if (widget.multiple && widget.max > 0) {
251+
// 多选逻辑,优选从参数获取
252+
var isMultiple = widget.multiple;
253+
if (multiple != null) {
254+
isMultiple = multiple;
255+
}
256+
257+
if (isMultiple && widget.max > 0) {
202258
var remain = widget.max - fileList.length;
203259

204260
if (files.length > remain) {
@@ -238,7 +294,11 @@ class _TDUploadState extends State<TDUpload> {
238294
children: [
239295
...fileList.map((file) => _buildImageBox(context, file)).toList(),
240296
_buildUploadBox(context, shouldDisplay: canUpload, onTap: () async {
241-
final files = await getMediaFromPicker();
297+
if (!canUpload) {
298+
return;
299+
}
300+
301+
final files = await getMediaFromPicker(widget.multiple);
242302
extractImageList(files);
243303
}),
244304
],
@@ -252,9 +312,14 @@ class _TDUploadState extends State<TDUpload> {
252312
child: GestureDetector(
253313
onTap: onTap,
254314
child: Container(
255-
width: 80,
256-
height: 80,
257-
decoration: BoxDecoration(color: TDTheme.of(context).grayColor1, borderRadius: BorderRadius.circular(6)),
315+
width: widget.width,
316+
height: widget.height,
317+
decoration: widget.type == TDUploadBoxType.circle
318+
? BoxDecoration(
319+
shape: BoxShape.circle,
320+
color: TDTheme.of(context).grayColor1,
321+
)
322+
: BoxDecoration(color: TDTheme.of(context).grayColor1, borderRadius: BorderRadius.circular(6)),
258323
child: const Center(
259324
child: Icon(
260325
TDIcons.add,
@@ -266,19 +331,26 @@ class _TDUploadState extends State<TDUpload> {
266331

267332
Widget _buildImageBox(BuildContext context, TDUploadFile file) {
268333
return GestureDetector(
269-
onTap: () {
334+
onTap: () async {
270335
if (widget.onClick != null) {
271336
widget.onClick!(file.key);
272337
}
338+
// 替换资源
339+
if (widget.enabledReplaceType ?? false) {
340+
final files = await getMediaFromPicker(false);
341+
replaceMedia(files, file);
342+
}
273343
},
274344
child: Stack(
275345
children: [
276346
TDImage(
277-
width: 80,
278-
height: 80,
347+
key: Key(file.assetPath ?? ''),
348+
width: widget.width,
349+
height: widget.height,
279350
imgUrl: file.remotePath,
280351
// assetUrl: file.assetPath,
281352
imageFile: file.file,
353+
type: _imageTypeMap[widget.type] ?? TDImageType.roundedSquare,
282354
),
283355
Visibility(visible: file.status != TDUploadFileStatus.success, child: _buildShadowBox(file)),
284356
Visibility(
@@ -293,10 +365,15 @@ class _TDUploadState extends State<TDUpload> {
293365
child: Container(
294366
width: 20,
295367
height: 20,
296-
decoration: const BoxDecoration(
297-
color: Color.fromRGBO(0, 0, 0, 0.6),
298-
borderRadius:
299-
BorderRadius.only(bottomLeft: Radius.circular(6), topRight: Radius.circular(6))),
368+
decoration: widget.type == TDUploadBoxType.circle
369+
? const BoxDecoration(
370+
shape: BoxShape.circle,
371+
color: Color.fromRGBO(0, 0, 0, 0.6),
372+
)
373+
: const BoxDecoration(
374+
color: Color.fromRGBO(0, 0, 0, 0.6),
375+
borderRadius:
376+
BorderRadius.only(bottomLeft: Radius.circular(6), topRight: Radius.circular(6))),
300377
child: const Center(
301378
child: Icon(
302379
TDIcons.close,
@@ -326,14 +403,19 @@ class _TDUploadState extends State<TDUpload> {
326403
}
327404

328405
return Container(
329-
width: 80,
330-
height: 80,
331-
decoration: BoxDecoration(color: const Color.fromRGBO(0, 0, 0, 0.4), borderRadius: BorderRadius.circular(6)),
406+
width: widget.width,
407+
height: widget.height,
408+
decoration: widget.type == TDUploadBoxType.circle
409+
? const BoxDecoration(
410+
shape: BoxShape.circle,
411+
color: Color.fromRGBO(0, 0, 0, 0.4),
412+
)
413+
: BoxDecoration(color: const Color.fromRGBO(0, 0, 0, 0.4), borderRadius: BorderRadius.circular(6)),
332414
child: Padding(
333415
padding: const EdgeInsets.symmetric(vertical: 16),
334416
child: Center(
335417
child: Column(
336-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
418+
mainAxisAlignment: MainAxisAlignment.center,
337419
children: [
338420
Visibility(
339421
visible: file.status == TDUploadFileStatus.loading,
@@ -350,10 +432,13 @@ class _TDUploadState extends State<TDUpload> {
350432
size: 24,
351433
color: Colors.white,
352434
)),
353-
TDText(
354-
displayText,
355-
textColor: Colors.white,
356-
style: const TextStyle(fontSize: 12, height: 1.67),
435+
Padding(
436+
padding: const EdgeInsets.only(top: 4),
437+
child: TDText(
438+
displayText,
439+
textColor: Colors.white,
440+
style: const TextStyle(fontSize: 12, height: 1.67),
441+
),
357442
),
358443
],
359444
),

0 commit comments

Comments
 (0)