@lobehub/chat 1.114.4 → 1.114.6
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/.prettierignore +0 -1
- package/.vscode/settings.json +86 -80
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +14 -0
- package/package.json +2 -2
- package/src/app/[variants]/(main)/files/[id]/page.tsx +0 -2
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +194 -183
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +2 -2
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +26 -2
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +33 -19
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +17 -7
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useUploadFilesValidation.ts +77 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/imageValidation.ts +117 -0
- package/src/config/aiModels/mistral.ts +2 -0
- package/src/libs/standard-parameters/index.ts +3 -0
- package/src/locales/default/components.ts +8 -0
- package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +29 -29
- package/src/store/aiInfra/slices/aiProvider/action.ts +80 -36
- package/src/store/image/slices/generationConfig/hooks.ts +10 -0
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx
CHANGED
@@ -12,6 +12,7 @@ import { useFileStore } from '@/store/file';
|
|
12
12
|
import { FileUploadStatus } from '@/types/files/upload';
|
13
13
|
|
14
14
|
import { useDragAndDrop } from '../hooks/useDragAndDrop';
|
15
|
+
import { useUploadFilesValidation } from '../hooks/useUploadFilesValidation';
|
15
16
|
import { useConfigPanelStyles } from '../style';
|
16
17
|
|
17
18
|
// ======== Business Types ======== //
|
@@ -19,6 +20,7 @@ import { useConfigPanelStyles } from '../style';
|
|
19
20
|
export interface ImageUploadProps {
|
20
21
|
// Callback when URL changes
|
21
22
|
className?: string; // Image URL
|
23
|
+
maxFileSize?: number;
|
22
24
|
onChange?: (url?: string) => void;
|
23
25
|
style?: React.CSSProperties;
|
24
26
|
value?: string | null;
|
@@ -414,212 +416,221 @@ SuccessDisplay.displayName = 'SuccessDisplay';
|
|
414
416
|
|
415
417
|
// ======== Main Component ======== //
|
416
418
|
|
417
|
-
const ImageUpload: FC<ImageUploadProps> = memo(
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
419
|
+
const ImageUpload: FC<ImageUploadProps> = memo(
|
420
|
+
({ value, onChange, style, className, maxFileSize }) => {
|
421
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
422
|
+
const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
|
423
|
+
const [uploadState, setUploadState] = useState<UploadState | null>(null);
|
424
|
+
const { t } = useTranslation('components');
|
425
|
+
const { message } = App.useApp();
|
426
|
+
const { validateFiles } = useUploadFilesValidation(undefined, maxFileSize);
|
427
|
+
|
428
|
+
// Cleanup blob URLs to prevent memory leaks
|
429
|
+
useEffect(() => {
|
430
|
+
return () => {
|
431
|
+
if (uploadState?.previewUrl && isLocalBlobUrl(uploadState.previewUrl)) {
|
432
|
+
URL.revokeObjectURL(uploadState.previewUrl);
|
433
|
+
}
|
434
|
+
};
|
435
|
+
}, [uploadState?.previewUrl]);
|
436
|
+
|
437
|
+
const handleFileSelect = () => {
|
438
|
+
inputRef.current?.click();
|
430
439
|
};
|
431
|
-
}, [uploadState?.previewUrl]);
|
432
440
|
|
433
|
-
|
434
|
-
|
435
|
-
|
441
|
+
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
442
|
+
const file = event.target.files?.[0];
|
443
|
+
if (!file) return;
|
436
444
|
|
437
|
-
|
438
|
-
|
439
|
-
if (!file) return;
|
445
|
+
// Validate file using unified validation hook
|
446
|
+
if (!validateFiles([file])) return;
|
440
447
|
|
441
|
-
|
442
|
-
|
448
|
+
// Create preview URL
|
449
|
+
const previewUrl = URL.createObjectURL(file);
|
443
450
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
});
|
450
|
-
|
451
|
-
try {
|
452
|
-
// Start upload
|
453
|
-
const result = await uploadWithProgress({
|
454
|
-
file,
|
455
|
-
onStatusUpdate: (updateData) => {
|
456
|
-
if (updateData.type === 'updateFile') {
|
457
|
-
setUploadState((prev) => {
|
458
|
-
if (!prev) return null;
|
459
|
-
|
460
|
-
const fileStatus = updateData.value.status;
|
461
|
-
if (!fileStatus) return prev;
|
462
|
-
|
463
|
-
return {
|
464
|
-
...prev,
|
465
|
-
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
466
|
-
progress: updateData.value.uploadState?.progress || 0,
|
467
|
-
status: fileStatus,
|
468
|
-
};
|
469
|
-
});
|
470
|
-
} else if (updateData.type === 'removeFile') {
|
471
|
-
// Handle file removal
|
472
|
-
setUploadState(null);
|
473
|
-
}
|
474
|
-
},
|
475
|
-
skipCheckFileType: true,
|
451
|
+
// Set initial upload state
|
452
|
+
setUploadState({
|
453
|
+
previewUrl,
|
454
|
+
progress: 0,
|
455
|
+
status: 'pending',
|
476
456
|
});
|
477
457
|
|
478
|
-
|
479
|
-
//
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
458
|
+
try {
|
459
|
+
// Start upload
|
460
|
+
const result = await uploadWithProgress({
|
461
|
+
file,
|
462
|
+
onStatusUpdate: (updateData) => {
|
463
|
+
if (updateData.type === 'updateFile') {
|
464
|
+
setUploadState((prev) => {
|
465
|
+
if (!prev) return null;
|
466
|
+
|
467
|
+
const fileStatus = updateData.value.status;
|
468
|
+
if (!fileStatus) return prev;
|
469
|
+
|
470
|
+
return {
|
471
|
+
...prev,
|
472
|
+
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
473
|
+
progress: updateData.value.uploadState?.progress || 0,
|
474
|
+
status: fileStatus,
|
475
|
+
};
|
476
|
+
});
|
477
|
+
} else if (updateData.type === 'removeFile') {
|
478
|
+
// Handle file removal
|
479
|
+
setUploadState(null);
|
490
480
|
}
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
481
|
+
},
|
482
|
+
skipCheckFileType: true,
|
483
|
+
});
|
484
|
+
|
485
|
+
if (result?.url) {
|
486
|
+
// Upload successful
|
487
|
+
onChange?.(result.url);
|
488
|
+
}
|
489
|
+
} catch {
|
490
|
+
// Upload failed
|
491
|
+
setUploadState((prev) =>
|
492
|
+
prev
|
493
|
+
? {
|
494
|
+
...prev,
|
495
|
+
error: 'Upload failed',
|
496
|
+
status: 'error',
|
497
|
+
}
|
498
|
+
: null,
|
499
|
+
);
|
500
|
+
} finally {
|
501
|
+
// Cleanup
|
502
|
+
if (isLocalBlobUrl(previewUrl)) {
|
503
|
+
URL.revokeObjectURL(previewUrl);
|
504
|
+
}
|
505
|
+
|
506
|
+
// Clear upload state after a delay to show completion
|
507
|
+
setTimeout(() => {
|
508
|
+
setUploadState(null);
|
509
|
+
}, 1000);
|
497
510
|
}
|
511
|
+
};
|
498
512
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
}, 1000);
|
503
|
-
}
|
504
|
-
};
|
505
|
-
|
506
|
-
const handleDelete = () => {
|
507
|
-
onChange?.(undefined);
|
508
|
-
};
|
509
|
-
|
510
|
-
const handleDrop = async (files: File[]) => {
|
511
|
-
// Show warning if multiple files detected
|
512
|
-
if (files.length > 1) {
|
513
|
-
message.warning(t('ImageUpload.actions.dropMultipleFiles'));
|
514
|
-
}
|
515
|
-
|
516
|
-
// Take the first image file
|
517
|
-
const file = files[0];
|
513
|
+
const handleDelete = () => {
|
514
|
+
onChange?.(undefined);
|
515
|
+
};
|
518
516
|
|
519
|
-
|
520
|
-
|
517
|
+
const handleDrop = async (files: File[]) => {
|
518
|
+
// Show warning if multiple files detected
|
519
|
+
if (files.length > 1) {
|
520
|
+
message.warning(t('ImageUpload.actions.dropMultipleFiles'));
|
521
|
+
}
|
521
522
|
|
522
|
-
|
523
|
-
|
524
|
-
previewUrl,
|
525
|
-
progress: 0,
|
526
|
-
status: 'pending',
|
527
|
-
});
|
523
|
+
// Take the first image file
|
524
|
+
const file = files[0];
|
528
525
|
|
529
|
-
|
530
|
-
|
531
|
-
const result = await uploadWithProgress({
|
532
|
-
file,
|
533
|
-
onStatusUpdate: (updateData) => {
|
534
|
-
if (updateData.type === 'updateFile') {
|
535
|
-
setUploadState((prev) => {
|
536
|
-
if (!prev) return null;
|
526
|
+
// Validate file using unified validation hook
|
527
|
+
if (!validateFiles([file])) return;
|
537
528
|
|
538
|
-
|
539
|
-
|
529
|
+
// Create preview URL
|
530
|
+
const previewUrl = URL.createObjectURL(file);
|
540
531
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
};
|
547
|
-
});
|
548
|
-
} else if (updateData.type === 'removeFile') {
|
549
|
-
setUploadState(null);
|
550
|
-
}
|
551
|
-
},
|
552
|
-
skipCheckFileType: true,
|
532
|
+
// Set initial upload state
|
533
|
+
setUploadState({
|
534
|
+
previewUrl,
|
535
|
+
progress: 0,
|
536
|
+
status: 'pending',
|
553
537
|
});
|
554
538
|
|
555
|
-
|
556
|
-
//
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
539
|
+
try {
|
540
|
+
// Start upload using the same logic as handleFileChange
|
541
|
+
const result = await uploadWithProgress({
|
542
|
+
file,
|
543
|
+
onStatusUpdate: (updateData) => {
|
544
|
+
if (updateData.type === 'updateFile') {
|
545
|
+
setUploadState((prev) => {
|
546
|
+
if (!prev) return null;
|
547
|
+
|
548
|
+
const fileStatus = updateData.value.status;
|
549
|
+
if (!fileStatus) return prev;
|
550
|
+
|
551
|
+
return {
|
552
|
+
...prev,
|
553
|
+
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
554
|
+
progress: updateData.value.uploadState?.progress || 0,
|
555
|
+
status: fileStatus,
|
556
|
+
};
|
557
|
+
});
|
558
|
+
} else if (updateData.type === 'removeFile') {
|
559
|
+
setUploadState(null);
|
567
560
|
}
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
561
|
+
},
|
562
|
+
skipCheckFileType: true,
|
563
|
+
});
|
564
|
+
|
565
|
+
if (result?.url) {
|
566
|
+
// Upload successful
|
567
|
+
onChange?.(result.url);
|
568
|
+
}
|
569
|
+
} catch {
|
570
|
+
// Upload failed
|
571
|
+
setUploadState((prev) =>
|
572
|
+
prev
|
573
|
+
? {
|
574
|
+
...prev,
|
575
|
+
error: 'Upload failed',
|
576
|
+
status: 'error',
|
577
|
+
}
|
578
|
+
: null,
|
579
|
+
);
|
580
|
+
} finally {
|
581
|
+
// Cleanup
|
582
|
+
if (isLocalBlobUrl(previewUrl)) {
|
583
|
+
URL.revokeObjectURL(previewUrl);
|
584
|
+
}
|
585
|
+
|
586
|
+
// Clear upload state after a delay to show completion
|
587
|
+
setTimeout(() => {
|
588
|
+
setUploadState(null);
|
589
|
+
}, 1000);
|
574
590
|
}
|
591
|
+
};
|
575
592
|
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
}
|
581
|
-
};
|
582
|
-
|
583
|
-
const { isDragOver, dragHandlers } = useDragAndDrop({
|
584
|
-
accept: 'image/*',
|
585
|
-
onDrop: handleDrop,
|
586
|
-
});
|
587
|
-
|
588
|
-
// Determine which view to render
|
589
|
-
const hasImage = Boolean(value);
|
590
|
-
const isUploading = Boolean(uploadState);
|
593
|
+
const { isDragOver, dragHandlers } = useDragAndDrop({
|
594
|
+
accept: 'image/*',
|
595
|
+
onDrop: handleDrop,
|
596
|
+
});
|
591
597
|
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
<input
|
596
|
-
accept="image/*"
|
597
|
-
onChange={handleFileChange}
|
598
|
-
onClick={(e) => {
|
599
|
-
// Reset value to allow re-selecting the same file
|
600
|
-
e.currentTarget.value = '';
|
601
|
-
}}
|
602
|
-
ref={inputRef}
|
603
|
-
style={{ display: 'none' }}
|
604
|
-
type="file"
|
605
|
-
/>
|
598
|
+
// Determine which view to render
|
599
|
+
const hasImage = Boolean(value);
|
600
|
+
const isUploading = Boolean(uploadState);
|
606
601
|
|
607
|
-
|
608
|
-
{
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
602
|
+
return (
|
603
|
+
<div className={className} {...dragHandlers} style={style}>
|
604
|
+
{/* Hidden file input */}
|
605
|
+
<input
|
606
|
+
accept="image/*"
|
607
|
+
onChange={handleFileChange}
|
608
|
+
onClick={(e) => {
|
609
|
+
// Reset value to allow re-selecting the same file
|
610
|
+
e.currentTarget.value = '';
|
611
|
+
}}
|
612
|
+
ref={inputRef}
|
613
|
+
style={{ display: 'none' }}
|
614
|
+
type="file"
|
616
615
|
/>
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
616
|
+
|
617
|
+
{/* Conditional rendering based on state */}
|
618
|
+
{isUploading && uploadState ? (
|
619
|
+
<UploadingDisplay previewUrl={uploadState.previewUrl} progress={uploadState.progress} />
|
620
|
+
) : hasImage ? (
|
621
|
+
<SuccessDisplay
|
622
|
+
imageUrl={value!}
|
623
|
+
isDragOver={isDragOver}
|
624
|
+
onChangeImage={handleFileSelect}
|
625
|
+
onDelete={handleDelete}
|
626
|
+
/>
|
627
|
+
) : (
|
628
|
+
<Placeholder isDragOver={isDragOver} onClick={handleFileSelect} />
|
629
|
+
)}
|
630
|
+
</div>
|
631
|
+
);
|
632
|
+
},
|
633
|
+
);
|
623
634
|
|
624
635
|
ImageUpload.displayName = 'ImageUpload';
|
625
636
|
|
@@ -5,14 +5,14 @@ import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/
|
|
5
5
|
import ImageUpload from './ImageUpload';
|
6
6
|
|
7
7
|
const ImageUrl = memo(() => {
|
8
|
-
const { value: imageUrl, setValue } = useGenerationConfigParam('imageUrl');
|
8
|
+
const { value: imageUrl, setValue, maxFileSize } = useGenerationConfigParam('imageUrl');
|
9
9
|
|
10
10
|
// Extract the first URL from the array for single image display
|
11
11
|
const handleChange = (url?: string) => {
|
12
12
|
setValue(url ?? null);
|
13
13
|
};
|
14
14
|
|
15
|
-
return <ImageUpload onChange={handleChange} value={imageUrl} />;
|
15
|
+
return <ImageUpload maxFileSize={maxFileSize} onChange={handleChange} value={imageUrl} />;
|
16
16
|
});
|
17
17
|
|
18
18
|
export default ImageUrl;
|
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx
CHANGED
@@ -2,18 +2,42 @@ import { memo } from 'react';
|
|
2
2
|
|
3
3
|
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
4
4
|
|
5
|
+
import ImageUpload from './ImageUpload';
|
5
6
|
import MultiImagesUpload from './MultiImagesUpload';
|
6
7
|
|
7
8
|
const ImageUrlsUpload = memo(() => {
|
8
|
-
const { value, setValue } = useGenerationConfigParam('imageUrls');
|
9
|
+
const { value, setValue, maxCount, maxFileSize } = useGenerationConfigParam('imageUrls');
|
9
10
|
|
11
|
+
// When maxCount is 1, use ImageUpload for single image upload
|
12
|
+
if (maxCount === 1) {
|
13
|
+
const handleSingleChange = (url?: string) => {
|
14
|
+
setValue(url ? [url] : []);
|
15
|
+
};
|
16
|
+
|
17
|
+
return (
|
18
|
+
<ImageUpload
|
19
|
+
maxFileSize={maxFileSize}
|
20
|
+
onChange={handleSingleChange}
|
21
|
+
value={value?.[0] ?? null}
|
22
|
+
/>
|
23
|
+
);
|
24
|
+
}
|
25
|
+
|
26
|
+
// Otherwise use MultiImagesUpload for multiple images
|
10
27
|
const handleChange = (urls: string[]) => {
|
11
28
|
// Directly set the URLs to the store
|
12
29
|
// The store will handle URL to path conversion when needed
|
13
30
|
setValue(urls);
|
14
31
|
};
|
15
32
|
|
16
|
-
return
|
33
|
+
return (
|
34
|
+
<MultiImagesUpload
|
35
|
+
maxCount={maxCount}
|
36
|
+
maxFileSize={maxFileSize}
|
37
|
+
onChange={handleChange}
|
38
|
+
value={value}
|
39
|
+
/>
|
40
|
+
);
|
17
41
|
});
|
18
42
|
|
19
43
|
export default ImageUrlsUpload;
|
@@ -7,30 +7,33 @@ import Image from 'next/image';
|
|
7
7
|
import React, { type FC, memo, useEffect, useRef, useState } from 'react';
|
8
8
|
import { useTranslation } from 'react-i18next';
|
9
9
|
|
10
|
+
import { useUploadFilesValidation } from '../../hooks/useUploadFilesValidation';
|
11
|
+
|
10
12
|
// ======== Types ======== //
|
11
13
|
|
12
14
|
/**
|
13
|
-
*
|
14
|
-
* - url:
|
15
|
-
* - file:
|
16
|
-
* - previewUrl:
|
17
|
-
*
|
15
|
+
* Unified image item data structure
|
16
|
+
* - url: Remote URL of existing image
|
17
|
+
* - file: Newly selected file that needs to be uploaded
|
18
|
+
* - previewUrl: Preview URL for local file (blob URL)
|
19
|
+
* Items with url are existing images, items with file are files to be uploaded
|
18
20
|
*/
|
19
21
|
export interface ImageItem {
|
20
|
-
//
|
22
|
+
// URL of existing image
|
21
23
|
file?: File;
|
22
24
|
id: string;
|
23
|
-
//
|
25
|
+
// Newly selected file
|
24
26
|
previewUrl?: string;
|
25
|
-
url?: string; //
|
27
|
+
url?: string; // Preview URL for local file, only used when file exists
|
26
28
|
}
|
27
29
|
|
28
30
|
interface ImageManageModalProps {
|
29
31
|
images: string[];
|
30
|
-
|
32
|
+
maxCount?: number;
|
33
|
+
// Array of existing image URLs
|
31
34
|
onClose: () => void;
|
32
35
|
onComplete: (imageItems: ImageItem[]) => void;
|
33
|
-
open: boolean; //
|
36
|
+
open: boolean; // Unified completion callback
|
34
37
|
}
|
35
38
|
|
36
39
|
// ======== Utils ======== //
|
@@ -194,19 +197,20 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
194
197
|
// ======== Main Component ======== //
|
195
198
|
|
196
199
|
const ImageManageModal: FC<ImageManageModalProps> = memo(
|
197
|
-
({ open, images, onClose, onComplete }) => {
|
200
|
+
({ open, images, maxCount, onClose, onComplete }) => {
|
198
201
|
const { styles } = useStyles();
|
199
202
|
const { t } = useTranslation('components');
|
200
203
|
const inputRef = useRef<HTMLInputElement>(null);
|
204
|
+
const { validateFiles } = useUploadFilesValidation(maxCount);
|
201
205
|
|
202
|
-
//
|
206
|
+
// Use unified data structure to manage all images
|
203
207
|
const [imageItems, setImageItems] = useState<ImageItem[]>([]);
|
204
208
|
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
205
209
|
|
206
|
-
//
|
210
|
+
// Initialize state when modal opens
|
207
211
|
useEffect(() => {
|
208
212
|
if (open) {
|
209
|
-
//
|
213
|
+
// Convert existing URLs to ImageItem format
|
210
214
|
const initialItems: ImageItem[] = images.map((url) => ({
|
211
215
|
id: generateId(),
|
212
216
|
url,
|
@@ -253,7 +257,7 @@ const ImageManageModal: FC<ImageManageModalProps> = memo(
|
|
253
257
|
const newItems = imageItems.filter((_, i) => i !== index);
|
254
258
|
setImageItems(newItems);
|
255
259
|
|
256
|
-
//
|
260
|
+
// Adjust selected index
|
257
261
|
if (selectedIndex >= newItems.length) {
|
258
262
|
setSelectedIndex(Math.max(0, newItems.length - 1));
|
259
263
|
}
|
@@ -272,18 +276,23 @@ const ImageManageModal: FC<ImageManageModalProps> = memo(
|
|
272
276
|
const files = event.target.files;
|
273
277
|
if (!files || files.length === 0) return;
|
274
278
|
|
275
|
-
//
|
279
|
+
// Validate files, pass current image count
|
280
|
+
if (!validateFiles(Array.from(files), imageItems.length)) {
|
281
|
+
return;
|
282
|
+
}
|
283
|
+
|
284
|
+
// Create new ImageItem, generate one-time preview URL for each file
|
276
285
|
const newItems: ImageItem[] = Array.from(files).map((file) => ({
|
277
286
|
file,
|
278
287
|
id: generateId(),
|
279
|
-
previewUrl: URL.createObjectURL(file), //
|
288
|
+
previewUrl: URL.createObjectURL(file), // Create only once
|
280
289
|
}));
|
281
290
|
|
282
291
|
setImageItems((prev) => [...prev, ...newItems]);
|
283
292
|
};
|
284
293
|
|
285
294
|
const handleComplete = () => {
|
286
|
-
//
|
295
|
+
// Directly pass current complete state to parent component
|
287
296
|
onComplete(imageItems);
|
288
297
|
onClose();
|
289
298
|
};
|
@@ -397,7 +406,12 @@ const ImageManageModal: FC<ImageManageModalProps> = memo(
|
|
397
406
|
|
398
407
|
{/* Footer */}
|
399
408
|
<div className={styles.footer}>
|
400
|
-
<Button
|
409
|
+
<Button
|
410
|
+
disabled={maxCount ? imageItems.length >= maxCount : false}
|
411
|
+
icon={<Upload size={16} />}
|
412
|
+
onClick={handleUpload}
|
413
|
+
type="default"
|
414
|
+
>
|
401
415
|
{t('MultiImagesUpload.modal.upload')}
|
402
416
|
</Button>
|
403
417
|
|