Files
video-gen/video-gen-app/src/components/UploadSelector.tsx
T
2026-07-24 17:56:42 +08:00

281 lines
8.5 KiB
TypeScript

import React, { useRef, useState } from 'react';
import { Popover, Tooltip } from 'antd';
import { FolderOpenOutlined, DatabaseOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons';
import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset, UploadResourceHistoryItem } from '../types';
import PrivatePortraitAssetPicker from './privatePortrait/picker/AssetPicker';
import UploadResourceHistoryPicker from './uploadResource/UploadResourceHistoryPicker';
interface UploadSelectorProps {
children: React.ReactNode;
accept?: string;
multiple?: boolean;
onLocalSelect?: (files: File[]) => void;
onHistorySelect?: (items: UploadResourceHistoryItem[]) => void;
/** 兼容旧页面:由 UploadSelector 内部打开素材选择器,确认后回传素材数组。 */
onPortraitSelect?: (items: PrivatePortraitSelectableAsset[]) => void;
onPortraitLibrarySelect?: (libraryType: PrivatePortraitLibraryType) => void;
uploading?: boolean;
tooltipTitle?: string;
maxImageCount?: number;
maxVideoCount?: number;
usedImageCount?: number;
usedVideoCount?: number;
usedVideoDuration?: number;
maxVideoDuration?: number;
maxAudioCount?: number;
usedAudioCount?: number;
maxAudioDuration?: number;
usedAudioDuration?: number;
hideLimitHint?: boolean;
/** 创作类型:'image' 时隐藏真人素材库/虚拟素材库选项 */
mediaType?: string;
}
const UploadSelector: React.FC<UploadSelectorProps> = ({
children,
accept = 'image/*,video/*',
multiple = true,
onLocalSelect,
onHistorySelect,
onPortraitSelect,
onPortraitLibrarySelect,
uploading,
tooltipTitle,
maxImageCount,
maxVideoCount,
usedImageCount,
usedVideoCount,
usedVideoDuration,
maxVideoDuration,
maxAudioCount,
usedAudioCount,
maxAudioDuration,
usedAudioDuration,
hideLimitHint,
mediaType,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
const [portraitLibraryType, setPortraitLibraryType] = useState<PrivatePortraitLibraryType>('real_person');
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const handleLocalSelect = () => {
setPopoverOpen(false);
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && onLocalSelect) {
const fileList = Array.from(files);
onLocalSelect(fileList);
}
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const openPortraitPicker = (libraryType: PrivatePortraitLibraryType) => {
setPopoverOpen(false);
if (onPortraitSelect) {
setPortraitLibraryType(libraryType);
setPortraitPickerOpen(true);
return;
}
if (onPortraitLibrarySelect) {
onPortraitLibrarySelect(libraryType);
return;
}
setPortraitLibraryType(libraryType);
setPortraitPickerOpen(true);
};
const handleHistorySelect = () => {
setPopoverOpen(false);
setHistoryModalVisible(true);
};
const content = (
<div style={{ padding: '4px 0', minWidth: 180 }}>
<div
onClick={() => {
handleLocalSelect();
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 16px',
cursor: 'pointer',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f1f5f9';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<FolderOpenOutlined style={{ fontSize: 14, color: '#64748b' }} />
<span style={{ fontSize: 14, color: '#334155' }}>
本地上传
{/* {multiple ? '本地上传(可多选拖拽)' : '本地上传'} */}
</span>
</div>
<div
onClick={handleHistorySelect}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 16px',
cursor: 'pointer',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f1f5f9';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<DatabaseOutlined style={{ fontSize: 14, color: '#64748b' }} />
<span style={{ fontSize: 14, color: '#334155' }}>从资产中选择</span>
</div>
{mediaType !== 'image' && (
<div
onClick={() => openPortraitPicker('real_person')}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 16px',
cursor: 'pointer',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f1f5f9';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<UserOutlined style={{ fontSize: 14, color: '#64748b' }} />
<span style={{ fontSize: 14, color: '#334155' }}>真人素材库</span>
</div>
)}
{mediaType !== 'image' && (
<div
onClick={() => openPortraitPicker('aigc_virtual')}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 16px',
cursor: 'pointer',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f1f5f9';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<TeamOutlined style={{ fontSize: 14, color: '#64748b' }} />
<span style={{ fontSize: 14, color: '#334155' }}>虚拟素材库</span>
</div>
)}
</div>
);
return (
<>
<input
ref={fileInputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
{tooltipTitle ? (
<Tooltip title={tooltipTitle}>
<Popover
content={content}
open={popoverOpen}
onOpenChange={setPopoverOpen}
trigger="click"
placement="bottomLeft"
>
<div style={{ cursor: uploading ? 'not-allowed' : 'pointer' }}>
{children}
</div>
</Popover>
</Tooltip>
) : (
<Popover
content={content}
open={popoverOpen}
onOpenChange={setPopoverOpen}
trigger="click"
placement="bottomLeft"
>
<div style={{ cursor: uploading ? 'not-allowed' : 'pointer' }}>
{children}
</div>
</Popover>
)}
<UploadResourceHistoryPicker
open={historyModalVisible}
onClose={() => setHistoryModalVisible(false)}
onSelect={(items) => {
onHistorySelect?.(items);
setHistoryModalVisible(false);
}}
allowedTypes={accept.includes('audio') ? ['image', 'video', 'audio'] : ['image', 'video']}
maxImageCount={maxImageCount}
maxVideoCount={maxVideoCount}
usedImageCount={usedImageCount}
usedVideoCount={usedVideoCount}
maxVideoDuration={maxVideoDuration}
usedVideoDuration={usedVideoDuration}
maxAudioCount={maxAudioCount}
usedAudioCount={usedAudioCount}
maxAudioDuration={maxAudioDuration}
usedAudioDuration={usedAudioDuration}
hideLimitHint={hideLimitHint}
defaultResourceType={mediaType === 'image' ? 'image' : ''}
/>
<PrivatePortraitAssetPicker
open={portraitPickerOpen}
libraryType={portraitLibraryType}
onClose={() => setPortraitPickerOpen(false)}
onSelect={(assets) => {
onPortraitSelect?.(assets);
setPortraitPickerOpen(false);
}}
accept={accept}
maxCount={accept === 'image/*' && !multiple ? 1 : undefined}
maxImageCount={maxImageCount}
maxVideoCount={maxVideoCount}
usedImageCount={usedImageCount}
usedVideoCount={usedVideoCount}
usedVideoDuration={usedVideoDuration}
maxVideoDuration={maxVideoDuration}
/>
</>
);
};
export default UploadSelector;