@zjlab-fe/data-hub-ui 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/components/file-uploader/components/icons.d.ts +1 -0
- package/dist/types/components/file-uploader/components/uploader-file-item.d.ts +4 -0
- package/dist/types/components/file-uploader/components/uploader-progress-bar.d.ts +9 -0
- package/dist/types/components/file-uploader/components/uploader.d.ts +2 -0
- package/dist/types/components/file-uploader/context/use-upload-engine.d.ts +5 -0
- package/dist/types/components/file-uploader/demo/index.d.ts +1 -0
- package/dist/types/components/file-uploader/demo/mockApi.d.ts +37 -0
- package/dist/types/components/file-uploader/hooks/use-upload.d.ts +4 -0
- package/dist/types/components/file-uploader/index.d.ts +1 -0
- package/dist/types/components/file-uploader/types.d.ts +5 -0
- package/es/components/file-uploader/components/icons.js +7 -4
- package/es/components/file-uploader/components/uploader-file-item.css.js +22 -0
- package/es/components/file-uploader/components/uploader-file-item.js +4 -3
- package/es/components/file-uploader/components/uploader-progress-bar.css.js +43 -0
- package/es/components/file-uploader/components/uploader-progress-bar.js +19 -0
- package/es/components/file-uploader/components/uploader.js +4 -3
- package/es/components/file-uploader/context/use-upload-engine.js +108 -131
- package/es/components/file-uploader/hooks/use-upload.js +15 -1
- package/es/components/file-uploader/utils/upload-controllers.js +43 -1
- package/es/index.js +1 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -3,3 +3,4 @@ export declare function FileIcon(): import("react/jsx-runtime").JSX.Element;
|
|
|
3
3
|
export declare function CloseIcon(): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
export declare function PauseIcon(): import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
export declare function ResumeIcon(): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function RetryIcon(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -22,6 +22,8 @@ export interface UploaderFileItemClassNames {
|
|
|
22
22
|
pauseBtn?: string;
|
|
23
23
|
/** Resume button */
|
|
24
24
|
resumeBtn?: string;
|
|
25
|
+
/** Retry button */
|
|
26
|
+
retryBtn?: string;
|
|
25
27
|
/** Close/Cancel button */
|
|
26
28
|
closeBtn?: string;
|
|
27
29
|
}
|
|
@@ -39,6 +41,8 @@ interface FileItemProps {
|
|
|
39
41
|
onPause?: (id: string) => void;
|
|
40
42
|
/** Resume function passed from parent that has upload handlers */
|
|
41
43
|
onResume?: (id: string) => void;
|
|
44
|
+
/** Retry function passed from parent that has upload handlers */
|
|
45
|
+
onRetry?: (id: string) => void;
|
|
42
46
|
}
|
|
43
47
|
export declare function UploaderFileItem(props: FileItemProps): import("react/jsx-runtime").JSX.Element;
|
|
44
48
|
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import './uploader-progress-bar.css';
|
|
2
|
+
interface UploaderProgressBarProps {
|
|
3
|
+
/** Hide when only 0 or 1 files */
|
|
4
|
+
hideWhenSingle?: boolean;
|
|
5
|
+
/** Custom class name */
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function UploaderProgressBar({ hideWhenSingle, className }: UploaderProgressBarProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
export {};
|
|
@@ -32,6 +32,8 @@ interface UploaderProps {
|
|
|
32
32
|
onDropZoneError?: (error: DropZoneError) => void;
|
|
33
33
|
/** custom file validation callback */
|
|
34
34
|
fileValidation?: (file: File) => boolean | Promise<boolean>;
|
|
35
|
+
/** Whether to show the multi-file progress bar at the top. Default: true */
|
|
36
|
+
showProgressBar?: boolean;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Styled File Uploader - Matches FileUploader capabilities with custom styling
|
|
@@ -19,4 +19,9 @@ export declare function useUploadEngine(): {
|
|
|
19
19
|
resumeUpload: (fileId: string) => Promise<void>;
|
|
20
20
|
/** Cancel a file upload */
|
|
21
21
|
cancelUpload: (fileId: string) => Promise<void>;
|
|
22
|
+
/** Retry a failed upload (smart retry with full retry fallback) */
|
|
23
|
+
retryUpload: (fileId: string, forceFullRetry?: boolean) => Promise<{
|
|
24
|
+
success: boolean;
|
|
25
|
+
retryType: "smart" | "full";
|
|
26
|
+
}>;
|
|
22
27
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo mock for file-uploader retry scenarios.
|
|
3
|
+
* Keep fail strategies out of component business handlers.
|
|
4
|
+
*
|
|
5
|
+
* Chunk PUTs are intercepted locally (no external httpbin dependency).
|
|
6
|
+
* httpbin.org currently returns 503 in many networks, which made every chunk fail.
|
|
7
|
+
*/
|
|
8
|
+
export type FailMode = 'none' | 'urlFail' | 'partialChunkFail';
|
|
9
|
+
export type DemoEventLog = {
|
|
10
|
+
time: string;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
/** Local mock URLs intercepted by installDemoUploadMock() */
|
|
14
|
+
declare const MOCK_UPLOAD_URL = "https://upload.mock.local/ok";
|
|
15
|
+
declare const MOCK_BAD_UPLOAD_URL = "https://upload.mock.local/fail";
|
|
16
|
+
/**
|
|
17
|
+
* Intercept XMLHttpRequest for demo mock upload URLs.
|
|
18
|
+
* The real uploader uses axios PUT; this keeps the demo offline-capable and stable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installDemoUploadMock(): () => void;
|
|
21
|
+
export declare function bumpAttemptCount(fileName: string): number;
|
|
22
|
+
export declare function resetAttemptCounts(): void;
|
|
23
|
+
export declare function createDemoUploadUrls(fileName: string, partCount: number, failMode: FailMode): {
|
|
24
|
+
success: false;
|
|
25
|
+
reason: string;
|
|
26
|
+
} | {
|
|
27
|
+
success: true;
|
|
28
|
+
uploadInfo: {
|
|
29
|
+
uploadId: string;
|
|
30
|
+
uploadUrls: {
|
|
31
|
+
partNum: number;
|
|
32
|
+
uploadUrl: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare function formatDemoEvent(message: string): DemoEventLog;
|
|
37
|
+
export { MOCK_UPLOAD_URL, MOCK_BAD_UPLOAD_URL };
|
|
@@ -58,6 +58,10 @@ export declare function useUpload(): {
|
|
|
58
58
|
pause: (id: string) => void;
|
|
59
59
|
resume: (id: string) => Promise<void>;
|
|
60
60
|
cancel: (id: string) => Promise<void>;
|
|
61
|
+
retry: (id: string) => Promise<{
|
|
62
|
+
success: boolean;
|
|
63
|
+
retryType: "smart" | "full";
|
|
64
|
+
}>;
|
|
61
65
|
/** Get a specific file by ID */
|
|
62
66
|
getFileById: (id: string) => UploadFile | undefined;
|
|
63
67
|
/** Files waiting to be uploaded */
|
|
@@ -3,6 +3,7 @@ export { Uploader as FileUploader } from './components/uploader';
|
|
|
3
3
|
export { UploaderDropZone } from './components/uploader-drop-zone';
|
|
4
4
|
export { UploaderFileListLayout } from './components/uploader-file-list';
|
|
5
5
|
export { UploaderFileItem } from './components/uploader-file-item';
|
|
6
|
+
export { UploaderProgressBar } from './components/uploader-progress-bar';
|
|
6
7
|
export { useUpload } from './hooks/use-upload';
|
|
7
8
|
export type { UploadFile, ChunkState, UploadHandlers, HandlerContext, CallbackResult, GetUploadUrlsResult, UploadUrl, UploadConfig, AddFilesOptions, } from './types';
|
|
8
9
|
export type { DropZoneError, UploaderDropZoneClassNames, UploaderDropZoneProps, } from './components/uploader-drop-zone';
|
|
@@ -75,6 +75,11 @@ export interface HandlerContext {
|
|
|
75
75
|
resume: (id: string) => Promise<void>;
|
|
76
76
|
/** Cancel a file upload (always fresh) */
|
|
77
77
|
cancel: (id: string) => void;
|
|
78
|
+
/** Retry a failed upload (always fresh) */
|
|
79
|
+
retry: (id: string) => Promise<{
|
|
80
|
+
success: boolean;
|
|
81
|
+
retryType: 'smart' | 'full';
|
|
82
|
+
}>;
|
|
78
83
|
/** Get a specific file by ID (always fresh) */
|
|
79
84
|
getFileById: (id: string) => UploadFile | undefined;
|
|
80
85
|
}
|
|
@@ -7,13 +7,16 @@ function FileIcon() {
|
|
|
7
7
|
return (jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" }) }));
|
|
8
8
|
}
|
|
9
9
|
function CloseIcon() {
|
|
10
|
-
return (jsx("svg", { viewBox: "
|
|
10
|
+
return (jsx("svg", { width: "20", height: "20", viewBox: "5 5 14 14", fill: "currentColor", children: jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }));
|
|
11
11
|
}
|
|
12
12
|
function PauseIcon() {
|
|
13
|
-
return (jsx("svg", { viewBox: "
|
|
13
|
+
return (jsx("svg", { width: "20", height: "20", viewBox: "6 5 12 14", fill: "currentColor", children: jsx("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }) }));
|
|
14
14
|
}
|
|
15
15
|
function ResumeIcon() {
|
|
16
|
-
return (jsx("svg", { viewBox: "
|
|
16
|
+
return (jsx("svg", { width: "20", height: "20", viewBox: "8 5 11 14", fill: "currentColor", children: jsx("path", { d: "M8 5v14l11-7z" }) }));
|
|
17
|
+
}
|
|
18
|
+
function RetryIcon() {
|
|
19
|
+
return (jsx("svg", { width: "20", height: "20", viewBox: "4 4 16 16", fill: "currentColor", children: jsx("path", { d: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" }) }));
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
export { CloseIcon, FileIcon, PauseIcon, ResumeIcon, UploadIcon as default };
|
|
22
|
+
export { CloseIcon, FileIcon, PauseIcon, ResumeIcon, RetryIcon, UploadIcon as default };
|
|
@@ -158,6 +158,15 @@ function useStyle() {
|
|
|
158
158
|
border-radius: 0.375rem; /* 6px */
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/* Keep icon box same size as CircleProgress (20x20), not stretching to button */
|
|
162
|
+
|
|
163
|
+
.upload-action-btn svg {
|
|
164
|
+
width: 16px;
|
|
165
|
+
height: 16px;
|
|
166
|
+
display: block;
|
|
167
|
+
flex-shrink: 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
.upload-action-btn:hover {
|
|
162
171
|
background: var(--uploader-action-hover-bg);
|
|
163
172
|
color: var(--uploader-action-hover-text);
|
|
@@ -183,6 +192,19 @@ function useStyle() {
|
|
|
183
192
|
color: var(--uploader-action-close-hover-text);
|
|
184
193
|
background: var(--uploader-action-close-hover-bg);
|
|
185
194
|
}
|
|
195
|
+
|
|
196
|
+
.upload-retry-btn {
|
|
197
|
+
opacity: 0.5;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.upload-file-item:hover .upload-retry-btn {
|
|
201
|
+
opacity: 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.upload-retry-btn:hover {
|
|
205
|
+
color: var(--uploader-action-close-hover-text);
|
|
206
|
+
background: var(--uploader-action-close-hover-bg);
|
|
207
|
+
}
|
|
186
208
|
`;
|
|
187
209
|
document.head.appendChild(style);
|
|
188
210
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { UploadStatus } from '../constants.js';
|
|
3
3
|
import { CircleProgress } from './circle-progress.js';
|
|
4
|
-
import { FileIcon, PauseIcon, ResumeIcon, CloseIcon } from './icons.js';
|
|
4
|
+
import { FileIcon, PauseIcon, ResumeIcon, RetryIcon, CloseIcon } from './icons.js';
|
|
5
5
|
import { UploadingStatus } from './uploading-status.js';
|
|
6
6
|
import useStyle from './theme.css.js';
|
|
7
7
|
import useStyle$1 from './uploader-file-item.css.js';
|
|
@@ -9,7 +9,7 @@ import useStyle$1 from './uploader-file-item.css.js';
|
|
|
9
9
|
useStyle();
|
|
10
10
|
useStyle$1();
|
|
11
11
|
function UploaderFileItem(props) {
|
|
12
|
-
const { file, classNames, onCancel, onRemove, onPause, onResume } = props;
|
|
12
|
+
const { file, classNames, onCancel, onRemove, onPause, onResume, onRetry } = props;
|
|
13
13
|
const semanticClassNames = typeof classNames === 'function' ? classNames(props) : classNames;
|
|
14
14
|
const isFailed = file.status === UploadStatus.FAILED;
|
|
15
15
|
const isSuccess = file.status === UploadStatus.SUCCESS;
|
|
@@ -17,6 +17,7 @@ function UploaderFileItem(props) {
|
|
|
17
17
|
const isPaused = file.status === UploadStatus.PAUSED;
|
|
18
18
|
const showPause = !isSuccess && !isFailed && !isCancelled && !isPaused && onPause;
|
|
19
19
|
const showResume = isPaused && onResume;
|
|
20
|
+
const showRetry = isFailed && !!file.file && onRetry;
|
|
20
21
|
const handleClose = () => {
|
|
21
22
|
if (isFailed || isSuccess || isCancelled) {
|
|
22
23
|
onRemove === null || onRemove === void 0 ? void 0 : onRemove(file.id);
|
|
@@ -59,7 +60,7 @@ function UploaderFileItem(props) {
|
|
|
59
60
|
return 'cancelled';
|
|
60
61
|
return 'uploading';
|
|
61
62
|
};
|
|
62
|
-
return (jsxs("div", { className: `upload-file-item ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.container) || ''}`, "data-status": getStatusAttribute(), children: [jsxs("div", { className: `upload-file-info ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.info) || ''}`, children: [jsx("div", { className: `upload-file-icon ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.icon) || ''}`, children: jsx(FileIcon, {}) }), jsxs("span", { className: `upload-file-name ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.name) || ''}`, children: [file.fileName, file.relativePath && (jsx("span", { className: `upload-file-path ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.relativePath) || ''}`, children: file.relativePath }))] })] }), jsxs("div", { className: `upload-file-status ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.status) || ''}`, children: [jsx("span", { className: `upload-status-text ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.statusText) || ''}`, children: getStatusText() }), jsx("div", { className: semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.progress, children: jsx(CircleProgress, { percent: file.progress, isFailed: isFailed, isSuccess: isSuccess }) }), showPause && (jsx("button", { className: `upload-action-btn upload-pause-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.pauseBtn) || ''}`, onClick: () => onPause === null || onPause === void 0 ? void 0 : onPause(file.id), title: "\u6682\u505C\u4E0A\u4F20", children: jsx(PauseIcon, {}) })), showResume && (jsx("button", { className: `upload-action-btn upload-resume-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.resumeBtn) || ''}`, onClick: () => onResume === null || onResume === void 0 ? void 0 : onResume(file.id), title: "\u7EE7\u7EED\u4E0A\u4F20", children: jsx(ResumeIcon, {}) }))] }), jsx("button", { className: `upload-action-btn upload-close-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.closeBtn) || ''}`, onClick: handleClose, title: isFailed || isSuccess ? '移除' : '取消上传', children: jsx(CloseIcon, {}) })] }));
|
|
63
|
+
return (jsxs("div", { className: `upload-file-item ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.container) || ''}`, "data-status": getStatusAttribute(), children: [jsxs("div", { className: `upload-file-info ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.info) || ''}`, children: [jsx("div", { className: `upload-file-icon ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.icon) || ''}`, children: jsx(FileIcon, {}) }), jsxs("span", { className: `upload-file-name ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.name) || ''}`, children: [file.fileName, file.relativePath && (jsx("span", { className: `upload-file-path ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.relativePath) || ''}`, children: file.relativePath }))] })] }), jsxs("div", { className: `upload-file-status ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.status) || ''}`, children: [jsx("span", { className: `upload-status-text ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.statusText) || ''}`, children: getStatusText() }), jsx("div", { className: semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.progress, children: jsx(CircleProgress, { percent: file.progress, isFailed: isFailed, isSuccess: isSuccess }) }), showPause && (jsx("button", { className: `upload-action-btn upload-pause-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.pauseBtn) || ''}`, onClick: () => onPause === null || onPause === void 0 ? void 0 : onPause(file.id), title: "\u6682\u505C\u4E0A\u4F20", children: jsx(PauseIcon, {}) })), showResume && (jsx("button", { className: `upload-action-btn upload-resume-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.resumeBtn) || ''}`, onClick: () => onResume === null || onResume === void 0 ? void 0 : onResume(file.id), title: "\u7EE7\u7EED\u4E0A\u4F20", children: jsx(ResumeIcon, {}) })), showRetry && (jsx("button", { className: `upload-action-btn upload-retry-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.retryBtn) || ''}`, onClick: () => onRetry === null || onRetry === void 0 ? void 0 : onRetry(file.id), title: "\u91CD\u8BD5\u4E0A\u4F20", children: jsx(RetryIcon, {}) }))] }), jsx("button", { className: `upload-action-btn upload-close-btn ${(semanticClassNames === null || semanticClassNames === void 0 ? void 0 : semanticClassNames.closeBtn) || ''}`, onClick: handleClose, title: isFailed || isSuccess ? '移除' : '取消上传', children: jsx(CloseIcon, {}) })] }));
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export { UploaderFileItem };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function useStyle() {
|
|
2
|
+
var style = document.createElement('style');
|
|
3
|
+
style.textContent = `.uploader-progress-bar {
|
|
4
|
+
padding: 0.75rem 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.uploader-progress-bar-info {
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
margin-bottom: 0.5rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.uploader-progress-bar-text {
|
|
15
|
+
font-size: 0.875rem;
|
|
16
|
+
color: var(--uploader-text-secondary, #64748b);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.uploader-progress-bar-percent {
|
|
20
|
+
font-size: 0.875rem;
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
color: var(--uploader-text, #1e293b);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.uploader-progress-bar-track {
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 0.5rem;
|
|
28
|
+
background-color: var(--uploader-border, #e2e8f0);
|
|
29
|
+
border-radius: 9999px;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.uploader-progress-bar-fill {
|
|
34
|
+
height: 100%;
|
|
35
|
+
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
|
36
|
+
border-radius: 9999px;
|
|
37
|
+
transition: width 0.3s ease;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
document.head.appendChild(style);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { useStyle as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useUpload } from '../hooks/use-upload.js';
|
|
3
|
+
import { UploadStatus } from '../constants.js';
|
|
4
|
+
import useStyle from './uploader-progress-bar.css.js';
|
|
5
|
+
|
|
6
|
+
useStyle();
|
|
7
|
+
function UploaderProgressBar({ hideWhenSingle = true, className = '' }) {
|
|
8
|
+
const { files } = useUpload();
|
|
9
|
+
if (files.length === 0)
|
|
10
|
+
return null;
|
|
11
|
+
if (hideWhenSingle && files.length <= 1)
|
|
12
|
+
return null;
|
|
13
|
+
const completedCount = files.filter((f) => f.status === UploadStatus.SUCCESS).length;
|
|
14
|
+
const totalCount = files.length;
|
|
15
|
+
const percent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
|
16
|
+
return (jsxs("div", { className: `uploader-progress-bar ${className}`, children: [jsxs("div", { className: "uploader-progress-bar-info", children: [jsxs("span", { className: "uploader-progress-bar-text", children: ["\u5DF2\u5B8C\u6210: ", completedCount, " / ", totalCount, " \u4E2A\u6587\u4EF6"] }), jsxs("span", { className: "uploader-progress-bar-percent", children: [percent, "%"] })] }), jsx("div", { className: "uploader-progress-bar-track", children: jsx("div", { className: "uploader-progress-bar-fill", style: { width: `${percent}%` } }) })] }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { UploaderProgressBar };
|
|
@@ -6,6 +6,7 @@ import { DEFAULT_MAX_FILE_SIZE } from '../constants.js';
|
|
|
6
6
|
import { UploaderDropZone } from './uploader-drop-zone.js';
|
|
7
7
|
import { UploaderFileListLayout } from './uploader-file-list.js';
|
|
8
8
|
import { UploaderFileItem } from './uploader-file-item.js';
|
|
9
|
+
import { UploaderProgressBar } from './uploader-progress-bar.js';
|
|
9
10
|
import { useUpload } from '../hooks/use-upload.js';
|
|
10
11
|
|
|
11
12
|
useStyle();
|
|
@@ -39,15 +40,15 @@ useStyle$1();
|
|
|
39
40
|
*/
|
|
40
41
|
function Uploader(props) {
|
|
41
42
|
const { handlers, runtime, dragAreaDescription = '点击或拖拽上传文件', accept = '', directory = false, maxFiles, maxSize = DEFAULT_MAX_FILE_SIZE, // 10GB default
|
|
42
|
-
classNames, onDropZoneError, fileValidation, } = props;
|
|
43
|
+
classNames, onDropZoneError, fileValidation, showProgressBar = true, } = props;
|
|
43
44
|
const SemanticClassNames = typeof classNames === 'function' ? classNames(props) : classNames;
|
|
44
|
-
const { files, addFiles, startAll, cancel, removeFile } = useUpload();
|
|
45
|
+
const { files, addFiles, startAll, cancel, removeFile, retry } = useUpload();
|
|
45
46
|
// Auto-start newly added pending files
|
|
46
47
|
useEffect(() => {
|
|
47
48
|
startAll();
|
|
48
49
|
}, [startAll]);
|
|
49
50
|
const handleAddFiles = useCallback((dropped) => addFiles(dropped, { handlers }, runtime), [addFiles, handlers, runtime]);
|
|
50
|
-
return (jsxs("div", { className: `styled-uploader-container ${(SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.container) || ''}`, children: [jsx(UploaderDropZone, { accept: accept, directory: directory, maxFiles: maxFiles, maxSize: maxSize, dragAreaDescription: dragAreaDescription, onDrop: handleAddFiles, onError: onDropZoneError, fileValidation: fileValidation, classNames: SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.dragZone }), jsx(UploaderFileListLayout, { classNames: SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.fileList, children: files.map((file) => (jsx(UploaderFileItem, { file: file, onCancel: cancel, onRemove: removeFile, classNames: SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.fileItem }, file.id))) })] }));
|
|
51
|
+
return (jsxs("div", { className: `styled-uploader-container ${(SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.container) || ''}`, children: [jsx(UploaderDropZone, { accept: accept, directory: directory, maxFiles: maxFiles, maxSize: maxSize, dragAreaDescription: dragAreaDescription, onDrop: handleAddFiles, onError: onDropZoneError, fileValidation: fileValidation, classNames: SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.dragZone }), showProgressBar && jsx(UploaderProgressBar, {}), jsx(UploaderFileListLayout, { classNames: SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.fileList, children: files.map((file) => (jsx(UploaderFileItem, { file: file, onCancel: cancel, onRemove: removeFile, onRetry: retry, classNames: SemanticClassNames === null || SemanticClassNames === void 0 ? void 0 : SemanticClassNames.fileItem }, file.id))) })] }));
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export { Uploader };
|
|
@@ -5,7 +5,7 @@ import { UploadStatus } from '../constants.js';
|
|
|
5
5
|
import { createUploadError, UPLOAD_ERROR_CODE, parseError } from '../utils/error-handler.js';
|
|
6
6
|
import { handlerRegistry } from './handler-registry.js';
|
|
7
7
|
import { actionRegistry } from './action-registry.js';
|
|
8
|
-
import { resetInterruptedChunks, getChunksForResume } from '../utils/upload-controllers.js';
|
|
8
|
+
import { resetInterruptedChunks, getChunksForResume, getFailedChunks, resetFailedChunks, resetAllChunks } from '../utils/upload-controllers.js';
|
|
9
9
|
import { fileStateStore } from './file-state-store.js';
|
|
10
10
|
import { ensureHandlers } from '../utils/ensure-handlers.js';
|
|
11
11
|
|
|
@@ -25,6 +25,7 @@ function getHandlerContext() {
|
|
|
25
25
|
pause: () => { },
|
|
26
26
|
resume: () => __awaiter(this, void 0, void 0, function* () { }),
|
|
27
27
|
cancel: () => { },
|
|
28
|
+
retry: () => __awaiter(this, void 0, void 0, function* () { return ({ success: false, retryType: 'smart' }); }),
|
|
28
29
|
getFileById: () => undefined,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
@@ -291,135 +292,111 @@ function useUploadEngine() {
|
|
|
291
292
|
* @param forceFullRetry - Skip smart retry and do full retry immediately
|
|
292
293
|
* @returns Object with success status and retry type used
|
|
293
294
|
*/
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// };
|
|
400
|
-
// await startUpload(resetFile);
|
|
401
|
-
// handlers.onRetry?.(uploadFile, 'full');
|
|
402
|
-
// return { success: true, retryType: 'full' };
|
|
403
|
-
// } catch (error) {
|
|
404
|
-
// const uploadError = parseError(error, 'Retry failed');
|
|
405
|
-
// console.error('Full retry failed:', uploadError.message);
|
|
406
|
-
// dispatch({
|
|
407
|
-
// type: 'UPDATE_FILE',
|
|
408
|
-
// id: fileId,
|
|
409
|
-
// updates: {
|
|
410
|
-
// status: UploadStatus.FAILED,
|
|
411
|
-
// error: uploadError.message,
|
|
412
|
-
// errorCode: uploadError.code,
|
|
413
|
-
// retryCount: retryAttempt,
|
|
414
|
-
// },
|
|
415
|
-
// });
|
|
416
|
-
// handlers.onError?.(uploadFile, uploadError.originalError ?? new Error(uploadError.message));
|
|
417
|
-
// return { success: false, retryType: 'full' };
|
|
418
|
-
// }
|
|
419
|
-
// }
|
|
420
|
-
// },
|
|
421
|
-
// [dispatch, getQueueManager, startUpload, ensureHandlers],
|
|
422
|
-
// );
|
|
295
|
+
const retryUpload = useCallback((fileId_1, ...args_1) => __awaiter(this, [fileId_1, ...args_1], void 0, function* (fileId, forceFullRetry = false) {
|
|
296
|
+
var _a, _b;
|
|
297
|
+
const file = fileStateStore.getFile(fileId);
|
|
298
|
+
if (!file || !file.file) {
|
|
299
|
+
const error = createUploadError(UPLOAD_ERROR_CODE.MISSING_FILE, 'File is missing, cannot retry');
|
|
300
|
+
console.error(error.message);
|
|
301
|
+
return { success: false, retryType: 'smart' };
|
|
302
|
+
}
|
|
303
|
+
if (file.status !== UploadStatus.FAILED) {
|
|
304
|
+
return { success: false, retryType: 'smart' };
|
|
305
|
+
}
|
|
306
|
+
const h = ensureHandlers(file, dispatch);
|
|
307
|
+
if (!h) {
|
|
308
|
+
return { success: false, retryType: 'smart' };
|
|
309
|
+
}
|
|
310
|
+
const retryCount = ((_a = file.retryCount) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
311
|
+
const failedChunksResult = getFailedChunks(file);
|
|
312
|
+
if (forceFullRetry || !failedChunksResult.hasFailed) {
|
|
313
|
+
return performFullRetry(file, h, retryCount);
|
|
314
|
+
}
|
|
315
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
316
|
+
// SMART RETRY: Retry only failed chunks
|
|
317
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
318
|
+
try {
|
|
319
|
+
const updatedChunks = resetFailedChunks(file.chunks);
|
|
320
|
+
const urlResult = yield h.onGetUploadUrls(file.fileName, file.chunks.length, file);
|
|
321
|
+
if (!urlResult.success) {
|
|
322
|
+
console.warn('Smart retry URL fetch failed, falling back to full retry');
|
|
323
|
+
return performFullRetry(file, h, retryCount);
|
|
324
|
+
}
|
|
325
|
+
const { uploadId, uploadUrls } = urlResult.uploadInfo;
|
|
326
|
+
const chunksWithUrls = applyUploadUrlsToChunks(updatedChunks, uploadUrls);
|
|
327
|
+
dispatch({
|
|
328
|
+
type: 'UPDATE_FILE',
|
|
329
|
+
id: fileId,
|
|
330
|
+
updates: {
|
|
331
|
+
status: UploadStatus.UPLOADING,
|
|
332
|
+
uploadId,
|
|
333
|
+
chunks: chunksWithUrls,
|
|
334
|
+
error: undefined,
|
|
335
|
+
errorCode: undefined,
|
|
336
|
+
retryCount,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
handlerRegistry.register(fileId, h, 'retryUpload:smart');
|
|
340
|
+
const qm = getQueueManager();
|
|
341
|
+
qm.clearFileFromQueue(fileId);
|
|
342
|
+
const fileWithUrls = Object.assign(Object.assign({}, file), { uploadId, chunks: chunksWithUrls, status: UploadStatus.UPLOADING, retryCount, error: undefined, errorCode: undefined });
|
|
343
|
+
qm.addFile(fileWithUrls);
|
|
344
|
+
const ctx = getHandlerContext();
|
|
345
|
+
(_b = h.onRetry) === null || _b === void 0 ? void 0 : _b.call(h, file, 'smart', ctx);
|
|
346
|
+
return { success: true, retryType: 'smart' };
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const uploadError = parseError(error, 'Smart retry failed');
|
|
350
|
+
console.warn('Smart retry failed, falling back to full retry:', uploadError.message);
|
|
351
|
+
return performFullRetry(file, h, retryCount);
|
|
352
|
+
}
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
354
|
+
// FULL RETRY: Reset all chunks and start from beginning
|
|
355
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
356
|
+
function performFullRetry(uploadFile, handlers, retryAttempt) {
|
|
357
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
358
|
+
var _a, _b, _c;
|
|
359
|
+
try {
|
|
360
|
+
const resetChunks = resetAllChunks(uploadFile.chunks);
|
|
361
|
+
dispatch({
|
|
362
|
+
type: 'UPDATE_FILE',
|
|
363
|
+
id: fileId,
|
|
364
|
+
updates: {
|
|
365
|
+
status: UploadStatus.PENDING,
|
|
366
|
+
progress: 0,
|
|
367
|
+
error: undefined,
|
|
368
|
+
errorCode: undefined,
|
|
369
|
+
uploadId: '',
|
|
370
|
+
chunks: resetChunks,
|
|
371
|
+
retryCount: retryAttempt,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
const resetFile = Object.assign(Object.assign({}, uploadFile), { status: UploadStatus.PENDING, progress: 0, error: undefined, errorCode: undefined, uploadId: '', chunks: resetChunks, retryCount: retryAttempt });
|
|
375
|
+
const ctx = getHandlerContext();
|
|
376
|
+
(_a = handlers.onRetry) === null || _a === void 0 ? void 0 : _a.call(handlers, uploadFile, 'full', ctx);
|
|
377
|
+
yield startUpload(resetFile);
|
|
378
|
+
return { success: true, retryType: 'full' };
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
const uploadError = parseError(error, 'Retry failed');
|
|
382
|
+
console.error('Full retry failed:', uploadError.message);
|
|
383
|
+
dispatch({
|
|
384
|
+
type: 'UPDATE_FILE',
|
|
385
|
+
id: fileId,
|
|
386
|
+
updates: {
|
|
387
|
+
status: UploadStatus.FAILED,
|
|
388
|
+
error: uploadError.message,
|
|
389
|
+
errorCode: uploadError.code,
|
|
390
|
+
retryCount: retryAttempt,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
const ctx = getHandlerContext();
|
|
394
|
+
(_b = handlers.onError) === null || _b === void 0 ? void 0 : _b.call(handlers, uploadFile, (_c = uploadError.originalError) !== null && _c !== void 0 ? _c : new Error(uploadError.message), ctx);
|
|
395
|
+
return { success: false, retryType: 'full' };
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}), [dispatch, getQueueManager, startUpload]);
|
|
423
400
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
424
401
|
// PUBLIC API
|
|
425
402
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -433,7 +410,7 @@ function useUploadEngine() {
|
|
|
433
410
|
/** Cancel a file upload */
|
|
434
411
|
cancelUpload,
|
|
435
412
|
/** Retry a failed upload (smart retry with full retry fallback) */
|
|
436
|
-
|
|
413
|
+
retryUpload,
|
|
437
414
|
};
|
|
438
415
|
}
|
|
439
416
|
|