上传组件修改

This commit is contained in:
sjy
2026-07-07 10:26:40 +08:00
parent 3c0647a6aa
commit 5ae7f5b64b
8 changed files with 1262 additions and 607 deletions
@@ -0,0 +1,508 @@
import React, { useRef, useState } from 'react';
import { Modal, Tooltip } from 'antd';
import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, LoadingOutlined, CheckOutlined, DownOutlined } from '@ant-design/icons';
interface UploadSelectorProps {
children: React.ReactNode;
accept?: string;
onLocalSelect?: (files: File[]) => void;
onHistorySelect?: (items: any[]) => void;
onPortraitSelect?: (items: any[]) => void;
uploading?: boolean;
tooltipTitle?: string;
}
const UploadSelector: React.FC<UploadSelectorProps> = ({
children,
accept = 'image/*,video/*',
onLocalSelect,
onHistorySelect,
onPortraitSelect,
uploading,
tooltipTitle,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleLocalSelect = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && onLocalSelect) {
onLocalSelect(Array.from(files));
}
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const [modalVisible, setModalVisible] = useState(false);
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [portraitModalVisible, setPortraitModalVisible] = useState(false);
const handleClick = () => {
if (!uploading) {
setModalVisible(true);
}
};
const mockHistoryData = [];
const mockPortraitData = [
{ id: 1, url: '/src/assets/homebtn1.png', name: '人像1' },
{ id: 2, url: '/src/assets/homebtn1.png', name: '人像2' },
{ id: 3, url: '/src/assets/homebtn1.png', name: '人像3' },
{ id: 4, url: '/src/assets/homebtn1.png', name: '人像4' },
{ id: 5, url: '/src/assets/homebtn1.png', name: '人像5' },
{ id: 6, url: '/src/assets/homebtn1.png', name: '人像6' },
{ id: 7, url: '/src/assets/homebtn1.png', name: '人像7' },
{ id: 8, url: '/src/assets/homebtn1.png', name: '人像8' },
];
const [selectedHistoryItems, setSelectedHistoryItems] = useState<number[]>([]);
const [selectedPortraitItems, setSelectedPortraitItems] = useState<number[]>([]);
const [historyActiveTab, setHistoryActiveTab] = useState<'asset' | 'history'>('asset');
const [portraitExpandedGroups, setPortraitExpandedGroups] = useState<number[]>([1]);
const toggleHistoryItem = (id: number) => {
setSelectedHistoryItems(prev =>
prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
);
};
const togglePortraitItem = (id: number) => {
setSelectedPortraitItems(prev =>
prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
);
};
const togglePortraitGroup = (groupId: number) => {
setPortraitExpandedGroups(prev =>
prev.includes(groupId) ? prev.filter(id => id !== groupId) : [...prev, groupId]
);
};
const confirmHistorySelection = () => {
const items = mockHistoryData.filter(item => selectedHistoryItems.includes(item.id));
onHistorySelect?.(items);
setHistoryModalVisible(false);
setSelectedHistoryItems([]);
setModalVisible(false);
};
const confirmPortraitSelection = () => {
const items = mockPortraitData.filter(item => selectedPortraitItems.includes(item.id));
const transformedItems = items.map(item => ({
...item,
avatar: item.url,
}));
onPortraitSelect?.(transformedItems);
setPortraitModalVisible(false);
setSelectedPortraitItems([]);
setModalVisible(false);
};
const options = [
{
key: 'history',
label: '历史记录',
icon: <HistoryOutlined style={{ fontSize: 20, color: '#6366f1' }} />,
description: '从历史上传记录中选择',
onClick: () => {
setModalVisible(false);
setHistoryModalVisible(true);
},
},
{
key: 'portrait',
label: '人像',
icon: <UserOutlined style={{ fontSize: 20, color: '#ec4899' }} />,
description: '从人像库中选择',
onClick: () => {
setModalVisible(false);
setPortraitModalVisible(true);
},
},
{
key: 'local',
label: '本地选取',
icon: <FolderOpenOutlined style={{ fontSize: 20, color: '#10b981' }} />,
description: '从本地电脑选择文件',
onClick: () => {
setModalVisible(false);
handleLocalSelect();
},
},
];
const portraitGroups = [
{ id: 1, name: '111', items: mockPortraitData.slice(0, 4) },
{ id: 2, name: '222', items: mockPortraitData.slice(4, 6) },
{ id: 3, name: '333', items: mockPortraitData.slice(6, 8) },
];
return (
<>
<input
ref={fileInputRef}
type="file"
accept={accept}
multiple
onChange={handleFileChange}
style={{ display: 'none' }}
/>
{tooltipTitle ? (
<Tooltip title={tooltipTitle}>
<div onClick={handleClick} style={{ cursor: 'pointer' }}>
{children}
</div>
</Tooltip>
) : (
<div onClick={handleClick} style={{ cursor: 'pointer' }}>
{children}
</div>
)}
<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}
onCancel={() => {
setHistoryModalVisible(false);
setSelectedHistoryItems([]);
}}
footer={null}
width={"80%"}
height={"50%"}
centered
>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<div
onClick={() => setHistoryActiveTab('asset')}
style={{
padding: '6px 16px',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
fontWeight: historyActiveTab === 'asset' ? 600 : 500,
color: historyActiveTab === 'asset' ? '#fff' : '#64748b',
background: historyActiveTab === 'asset' ? '#6366f1' : '#f1f5f9',
transition: 'all 0.2s ease',
}}
>
</div>
<div
onClick={() => setHistoryActiveTab('history')}
style={{
padding: '6px 16px',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
fontWeight: historyActiveTab === 'history' ? 600 : 500,
color: historyActiveTab === 'history' ? '#fff' : '#64748b',
background: historyActiveTab === 'history' ? '#6366f1' : '#f1f5f9',
transition: 'all 0.2s ease',
}}
>
</div>
</div>
<div style={{ minHeight: 200, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ textAlign: 'center', color: '#94a3b8', fontSize: 14 }}>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 16, paddingTop: 16, borderTop: '1px solid #e2e8f0' }}>
<div style={{ fontSize: 14, color: '#64748b' }}>
<span style={{ color: '#ef4444', fontWeight: 600 }}>{selectedHistoryItems.length}</span>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<button
onClick={() => {
setHistoryModalVisible(false);
setSelectedHistoryItems([]);
}}
style={{
padding: '8px 24px',
borderRadius: 8,
border: '1px solid #e2e8f0',
background: '#fff',
cursor: 'pointer',
fontSize: 14,
color: '#64748b',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#cbd5e1';
e.currentTarget.style.background = '#f8fafc';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e2e8f0';
e.currentTarget.style.background = '#fff';
}}
>
</button>
<button
onClick={confirmHistorySelection}
style={{
padding: '8px 24px',
borderRadius: 8,
border: 'none',
background: '#ef4444',
cursor: 'pointer',
fontSize: 14,
color: '#fff',
fontWeight: 600,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#dc2626';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#ef4444';
}}
>
</button>
</div>
</div>
</Modal>
<Modal
title="真人人像库"
open={portraitModalVisible}
onCancel={() => {
setPortraitModalVisible(false);
setSelectedPortraitItems([]);
}}
footer={null}
width={"80%"}
centered
styles={{
body: { padding: 0, position: "relative", overflow: 'auto' },
}}
>
<div style={{ height: '500px', overflowY: 'auto' }}>
{portraitGroups.map((group) => (
<div
key={group.id}
style={{
borderRadius: 12,
background: '#f8fafc',
overflow: 'hidden',
}}
>
<div
onClick={() => togglePortraitGroup(group.id)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f1f5f9';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<span style={{ fontSize: 14, fontWeight: 600, color: '#1e293b' }}>
{group.name}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, color: '#64748b', fontSize: 13 }}>
<span>{portraitExpandedGroups.includes(group.id) ? '收起' : '展开'}</span>
<DownOutlined
style={{
fontSize: 12,
transform: portraitExpandedGroups.includes(group.id) ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
}}
/>
</div>
</div>
{portraitExpandedGroups.includes(group.id) && (
<div style={{ padding: '16px', borderTop: '1px solid #e2e8f0' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
{group.items.map((item) => (
<div
key={item.id}
onClick={() => togglePortraitItem(item.id)}
style={{
height: 120,
position: 'relative',
borderRadius: 8,
overflow: 'hidden',
cursor: 'pointer',
aspectRatio: '1',
border: selectedPortraitItems.includes(item.id) ? '2px solid #ef4444' : '2px solid transparent',
transition: 'all 0.2s ease',
}}
>
<img
src={item.url}
alt={item.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
{selectedPortraitItems.includes(item.id) && (
<div
style={{
position: 'absolute',
top: 4,
right: 4,
width: 20,
height: 20,
borderRadius: '50%',
background: '#ef4444',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CheckOutlined style={{ fontSize: 12, color: '#fff' }} />
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 16, paddingTop: 16, borderTop: '1px solid #e2e8f0' }}>
<div style={{ fontSize: 14, color: '#64748b' }}>
<span style={{ color: '#ef4444', fontWeight: 600 }}>{selectedPortraitItems.length}</span>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<button
onClick={() => {
setPortraitModalVisible(false);
setSelectedPortraitItems([]);
}}
style={{
padding: '8px 24px',
borderRadius: 8,
border: '1px solid #e2e8f0',
background: '#fff',
cursor: 'pointer',
fontSize: 14,
color: '#64748b',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#cbd5e1';
e.currentTarget.style.background = '#f8fafc';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#e2e8f0';
e.currentTarget.style.background = '#fff';
}}
>
</button>
<button
onClick={confirmPortraitSelection}
style={{
padding: '8px 24px',
borderRadius: 8,
border: 'none',
background: '#ef4444',
cursor: 'pointer',
fontSize: 14,
color: '#fff',
fontWeight: 600,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#dc2626';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#ef4444';
}}
>
使
</button>
</div>
</div>
</div>
</Modal>
</>
);
};
export default UploadSelector;