真人人像修改

This commit is contained in:
sjy
2026-07-08 17:15:19 +08:00
parent 35ffa317e7
commit bb53063358
17 changed files with 1320 additions and 679 deletions
+149 -122
View File
@@ -1,60 +1,73 @@
import React, { useRef, useState } from 'react';
import { Modal, Tooltip } from 'antd';
import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, TeamOutlined } from '@ant-design/icons';
import { Popover, Tooltip, Modal } from 'antd';
import { FolderOpenOutlined, DatabaseOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons';
import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset } from '../types';
import PrivatePortraitAssetPicker from './privatePortrait/picker/AssetPicker';
interface UploadSelectorProps {
children: React.ReactNode;
accept?: string;
multiple?: boolean;
onLocalSelect?: (files: File[]) => void;
onHistorySelect?: (items: any[]) => 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;
}
const UploadSelector: React.FC<UploadSelectorProps> = ({
children,
accept = 'image/*,video/*',
multiple = true,
onLocalSelect,
onHistorySelect,
onPortraitSelect,
onPortraitLibrarySelect,
uploading,
tooltipTitle,
maxImageCount,
maxVideoCount,
usedImageCount,
usedVideoCount,
usedVideoDuration,
maxVideoDuration,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [modalVisible, setModalVisible] = useState(false);
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
const [portraitLibraryType, setPortraitLibraryType] = useState<PrivatePortraitLibraryType>('real_person');
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const handleLocalSelect = () => {
setPopoverOpen(false);
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && onLocalSelect) {
onLocalSelect(Array.from(files));
const fileList = Array.from(files);
onLocalSelect(fileList);
}
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleClick = () => {
if (!uploading) {
setModalVisible(true);
}
};
const openPortraitPicker = (libraryType: PrivatePortraitLibraryType) => {
setModalVisible(false);
setPopoverOpen(false);
if (onPortraitSelect) {
setPortraitLibraryType(libraryType);
setPortraitPickerOpen(true);
return;
}
if (onPortraitLibrarySelect) {
onPortraitLibrarySelect(libraryType);
return;
@@ -63,48 +76,102 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
setPortraitPickerOpen(true);
};
const handleHistorySelect = () => {
setPopoverOpen(false);
setHistoryModalVisible(true);
};
const confirmHistorySelection = () => {
onHistorySelect?.([]);
setHistoryModalVisible(false);
setModalVisible(false);
};
const options = [
{
key: 'history',
label: '历史记录',
icon: <HistoryOutlined style={{ fontSize: 20, color: '#6366f1' }} />,
description: '从历史上传记录中选择',
onClick: () => {
setModalVisible(false);
setHistoryModalVisible(true);
},
},
{
key: 'real_person',
label: '真人素材',
icon: <UserOutlined style={{ fontSize: 20, color: '#ec4899' }} />,
description: '从真人私域素材库中选择',
onClick: () => openPortraitPicker('real_person'),
},
{
key: 'aigc_virtual',
label: '虚拟素材',
icon: <TeamOutlined style={{ fontSize: 20, color: '#8b5cf6' }} />,
description: '从虚拟私域素材库中选择',
onClick: () => openPortraitPicker('aigc_virtual'),
},
{
key: 'local',
label: '本地选取',
icon: <FolderOpenOutlined style={{ fontSize: 20, color: '#10b981' }} />,
description: '从本地电脑选择文件',
onClick: () => {
setModalVisible(false);
handleLocalSelect();
},
},
];
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>
<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>
<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 (
<>
@@ -112,86 +179,38 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
ref={fileInputRef}
type="file"
accept={accept}
multiple
multiple={multiple}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
{tooltipTitle ? (
<Tooltip title={tooltipTitle}>
<div onClick={handleClick} style={{ cursor: uploading ? 'not-allowed' : 'pointer' }}>
{children}
</div>
<Popover
content={content}
open={popoverOpen}
onOpenChange={setPopoverOpen}
trigger="click"
placement="bottomLeft"
>
<div style={{ cursor: uploading ? 'not-allowed' : 'pointer' }}>
{children}
</div>
</Popover>
</Tooltip>
) : (
<div onClick={handleClick} style={{ cursor: uploading ? 'not-allowed' : 'pointer' }}>
{children}
</div>
<Popover
content={content}
open={popoverOpen}
onOpenChange={setPopoverOpen}
trigger="click"
placement="bottomLeft"
>
<div style={{ cursor: uploading ? 'not-allowed' : 'pointer' }}>
{children}
</div>
</Popover>
)}
<Modal
title="选择上传来源"
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
width={400}
centered
destroyOnHidden
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, paddingTop: 8 }}>
{options.map((option) => (
<div
key={option.key}
onClick={option.onClick}
style={{
display: 'flex',
alignItems: 'center',
gap: 16,
padding: '16px 20px',
borderRadius: 12,
background: '#f8fafc',
cursor: 'pointer',
transition: 'all 0.2s ease',
border: '1px solid transparent',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#fff';
e.currentTarget.style.borderColor = '#e2e8f0';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f8fafc';
e.currentTarget.style.borderColor = 'transparent';
e.currentTarget.style.boxShadow = 'none';
}}
>
<div
style={{
width: 48,
height: 48,
borderRadius: 12,
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
}}
>
{option.icon}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 15, fontWeight: 600, color: '#1e293b', marginBottom: 2 }}>
{option.label}
</div>
<div style={{ fontSize: 13, color: '#64748b' }}>
{option.description}
</div>
</div>
<PlusOutlined style={{ fontSize: 14, color: '#94a3b8' }} />
</div>
))}
</div>
</Modal>
<Modal
title="选择资产素材"
open={historyModalVisible}
@@ -234,6 +253,14 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
onPortraitSelect?.(assets);
setPortraitPickerOpen(false);
}}
accept={accept}
maxCount={accept === 'image/*' && !multiple ? 1 : undefined}
maxImageCount={maxImageCount}
maxVideoCount={maxVideoCount}
usedImageCount={usedImageCount}
usedVideoCount={usedVideoCount}
usedVideoDuration={usedVideoDuration}
maxVideoDuration={maxVideoDuration}
/>
</>
);