@@ -5,13 +5,16 @@ import {
5
5
FormGroup ,
6
6
H4 ,
7
7
Icon ,
8
+ Spinner ,
9
+ SpinnerSize ,
8
10
Tag ,
9
11
} from '@blueprintjs/core'
10
12
import { Tooltip2 } from '@blueprintjs/popover2'
11
13
12
14
import { useLevels } from 'apis/arknights'
13
15
import { requestOperationUpload } from 'apis/copilotOperation'
14
16
import { ComponentType , useState } from 'react'
17
+ import { useList } from 'react-use'
15
18
16
19
import { withSuspensable } from 'components/Suspensable'
17
20
import { AppToaster } from 'components/Toaster'
@@ -29,18 +32,23 @@ interface FileEntry {
29
32
}
30
33
31
34
export const OperationUploader : ComponentType = withSuspensable ( ( ) => {
32
- const [ files , setFiles ] = useState ( null as FileEntry [ ] | null )
35
+ const [ files , { set : setFiles , update : updateFileWhere } ] =
36
+ useList < FileEntry > ( [ ] )
37
+
33
38
const [ globalErrors , setGlobalErrors ] = useState ( null as string [ ] | null )
39
+ const [ isProcessing , setIsProcessing ] = useState ( false )
34
40
const [ isUploading , setIsUploading ] = useState ( false )
35
41
42
+ // reasons are in the order of keys
36
43
const nonUploadableReason = Object . entries ( {
37
44
[ '正在上传,请等待' ] : isUploading ,
38
- [ '存在错误,请排查问题 ' ] : globalErrors ?. length ,
39
- [ '请选择文件' ] : ! files ? .length ,
40
- [ '文件列表中包含已上传的文件,请重新选择' ] : files ? .some (
45
+ [ '正在解析文件,请等待 ' ] : isProcessing ,
46
+ [ '请选择文件' ] : ! files . length ,
47
+ [ '文件列表中包含已上传的文件,请重新选择' ] : files . some (
41
48
( file ) => file . uploaded ,
42
49
) ,
43
- [ '文件存在错误,请修复' ] : files ?. some ( ( file ) => file . error ) ,
50
+ [ '文件存在错误,请修改内容' ] : files . some ( ( file ) => file . error ) ,
51
+ [ '存在错误,请排查问题' ] : globalErrors ?. length ,
44
52
} ) . find ( ( [ , value ] ) => value ) ?. [ 0 ]
45
53
46
54
const isUploadable = ! nonUploadableReason
@@ -55,16 +63,17 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
55
63
setGlobalErrors ( [ levelError . message ] )
56
64
}
57
65
58
- const handleFileUpload = async ( event : React . FormEvent < HTMLInputElement > ) => {
66
+ const handleFileSelect = async ( event : React . FormEvent < HTMLInputElement > ) => {
59
67
setGlobalErrors ( null )
60
68
61
69
if ( event . currentTarget . files ?. length ) {
70
+ setIsProcessing ( true )
71
+
62
72
const toFileEntry = async ( file : File ) : Promise < FileEntry > => {
63
73
const entry : FileEntry = { file }
64
- let content : object
65
74
66
75
try {
67
- content = await parseOperationFile ( file )
76
+ let content = await parseOperationFile ( file )
68
77
content = patchOperation ( content , levels )
69
78
70
79
validateOperation ( content )
@@ -79,34 +88,44 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
79
88
}
80
89
81
90
setFiles (
82
- await Promise . all (
83
- Array . from ( event . currentTarget . files ) . map ( toFileEntry ) ,
84
- ) ,
91
+ await Promise . all ( Array . from ( event . currentTarget . files , toFileEntry ) ) ,
85
92
)
93
+
94
+ setIsProcessing ( false )
86
95
} else {
87
- setFiles ( null )
96
+ setFiles ( [ ] )
88
97
}
89
98
}
90
99
91
100
const handleOperationSubmit = async ( ) => {
92
- if ( ! isUploadable || ! files ? .length ) {
101
+ if ( ! isUploadable || ! files . length ) {
93
102
return
94
103
}
95
104
96
105
setIsUploading ( true )
97
106
try {
107
+ let successCount = 0
108
+
98
109
await Promise . allSettled (
99
110
files . map ( ( file ) =>
100
111
requestOperationUpload ( JSON . stringify ( file . operation ) )
101
- . then ( ( ) => ( file . uploaded = true ) )
112
+ . then ( ( ) => {
113
+ successCount ++
114
+ updateFileWhere ( ( candidate ) => candidate === file , {
115
+ ...file ,
116
+ uploaded : true ,
117
+ } )
118
+ } )
102
119
. catch ( ( e ) => {
103
120
console . warn ( e )
104
- file . error = `上传失败:${ formatError ( e ) } `
121
+ updateFileWhere ( ( candidate ) => candidate === file , {
122
+ ...file ,
123
+ error : `上传失败:${ formatError ( e ) } ` ,
124
+ } )
105
125
} ) ,
106
126
) ,
107
127
)
108
128
109
- const successCount = files . filter ( ( file ) => ! file . error ) . length
110
129
const errorCount = files . length - successCount
111
130
112
131
AppToaster . show ( {
@@ -147,33 +166,51 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
147
166
< FileInput
148
167
large
149
168
fill
169
+ disabled = { isUploading || isProcessing }
150
170
buttonText = "浏览"
151
- text = { files ? .length ? `${ files . length } 个文件` : '选择文件...' }
171
+ text = { files . length ? `${ files . length } 个文件` : '选择文件...' }
152
172
inputProps = { {
153
173
accept : '.json' ,
154
174
multiple : true ,
155
175
} }
156
- onInputChange = { handleFileUpload }
176
+ onInputChange = { handleFileSelect }
157
177
/>
158
178
</ FormGroup >
159
179
160
180
< Tooltip2
161
181
fill
162
182
className = "mt-4"
163
183
placement = "top"
184
+ disabled = { ! nonUploadableReason }
164
185
content = { nonUploadableReason }
165
186
>
166
- { /* do not use <Button> because its disabled state does not work well with Tooltip */ }
167
- < AnchorButton
168
- large
169
- fill
170
- disabled = { ! isUploadable }
171
- loading = { isUploading }
172
- icon = "cloud-upload"
173
- onClick = { handleOperationSubmit }
174
- >
175
- 上传
176
- </ AnchorButton >
187
+ { ( ( ) => {
188
+ const settledCount = files . filter (
189
+ ( file ) => file . uploaded || file . error ,
190
+ ) . length
191
+
192
+ return (
193
+ // do not use <Button> because its disabled state does not work well with Tooltip
194
+ < AnchorButton
195
+ large
196
+ fill
197
+ disabled = { ! isUploadable }
198
+ icon = {
199
+ isUploading ? (
200
+ < Spinner
201
+ size = { SpinnerSize . SMALL }
202
+ value = { settledCount / files . length }
203
+ />
204
+ ) : (
205
+ 'cloud-upload'
206
+ )
207
+ }
208
+ onClick = { handleOperationSubmit }
209
+ >
210
+ { isUploading ? `${ settledCount } /${ files . length } ` : '上传' }
211
+ </ AnchorButton >
212
+ )
213
+ } ) ( ) }
177
214
</ Tooltip2 >
178
215
179
216
{ globalErrors && (
@@ -184,8 +221,8 @@ export const OperationUploader: ComponentType = withSuspensable(() => {
184
221
</ Callout >
185
222
) }
186
223
187
- { ! ! files ? .length && < div className = "mt-4 font-bold" > 文件详情</ div > }
188
- { files ? .map ( ( { file, uploaded, error, operation } , index ) => (
224
+ { ! ! files . length && < div className = "mt-4 font-bold" > 文件详情</ div > }
225
+ { files . map ( ( { file, uploaded, error, operation } , index ) => (
189
226
< Callout
190
227
className = "mt-2"
191
228
title = { file . name }
0 commit comments