上传组件修改
This commit is contained in:
@@ -1,30 +0,0 @@
|
|||||||
# Debug Session: ratio-options-not-showing
|
|
||||||
|
|
||||||
## Session ID
|
|
||||||
ratio-options-not-showing
|
|
||||||
|
|
||||||
## Created
|
|
||||||
2026-07-01
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
用户反馈:GenerateConver.tsx 中比例选项(ratioOptions)不显示,控制台无报错。
|
|
||||||
|
|
||||||
## Hypotheses (待验证假设)
|
|
||||||
|
|
||||||
1. **H1**: `ratioOptions` 默认值未生效 - useState 初始化失败
|
|
||||||
2. **H2**: `getEngine()` 返回的 `data.engine.image` 不存在或为空数组,if 条件未进入
|
|
||||||
3. **H3**: 比例按钮渲染区域被父容器 CSS 隐藏(如 `display: none`, `visibility: hidden`, `overflow: hidden`)
|
|
||||||
4. **H4**: `ratioOptions` 在某处被重置为空数组
|
|
||||||
5. **H5**: 组件条件渲染导致整个比例区域未挂载
|
|
||||||
|
|
||||||
## Evidence Points
|
|
||||||
- EP1: 检查 `ratioOptions` 初始值是否为 8 个元素的数组
|
|
||||||
- EP2: 检查 `getEngine()` 返回后 `data.engine.image` 是否存在
|
|
||||||
- EP3: 检查渲染区域父容器的 CSS 是否有隐藏属性
|
|
||||||
- EP4: 搜索代码中是否有 `setRatioOptions([])` 调用
|
|
||||||
|
|
||||||
## Status
|
|
||||||
[OPEN] - 调试中
|
|
||||||
|
|
||||||
## Log File
|
|
||||||
`trae-debug-log-ratio-options-not-showing.ndjson`
|
|
||||||
|
|||||||
@@ -982,7 +982,7 @@ const AppLayout: React.FC = () => {
|
|||||||
placement="left"
|
placement="left"
|
||||||
onClose={() => setMobileMenuOpen(false)}
|
onClose={() => setMobileMenuOpen(false)}
|
||||||
open={mobileMenuOpen}
|
open={mobileMenuOpen}
|
||||||
width={280}
|
size={280}
|
||||||
closable={true}
|
closable={true}
|
||||||
className="mobile-menu-drawer"
|
className="mobile-menu-drawer"
|
||||||
styles={{
|
styles={{
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -22,6 +22,8 @@ import bg2 from '../assets/bg2.png';
|
|||||||
import bg3 from '../assets/bg3.png';
|
import bg3 from '../assets/bg3.png';
|
||||||
import text from '../assets/testb.png';
|
import text from '../assets/testb.png';
|
||||||
|
|
||||||
|
import UploadSelector from '../components/UploadSelector';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -1251,6 +1253,8 @@ const AIChatPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
try {
|
try {
|
||||||
audioDuration = await getAudioDuration(file);
|
audioDuration = await getAudioDuration(file);
|
||||||
@@ -1287,7 +1291,6 @@ const AIChatPage: React.FC = () => {
|
|||||||
}];
|
}];
|
||||||
const labels = generateMediaLabels(newList);
|
const labels = generateMediaLabels(newList);
|
||||||
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
||||||
message.success(`${isImage ? '图片' : (isAudio ? '音频' : '视频')}上传成功`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('上传失败');
|
message.error('上传失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1297,6 +1300,118 @@ const AIChatPage: React.FC = () => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const doUpload = async (file: File): Promise<false | { name: string; type: 'image' | 'video' | 'audio'; url: string; label: string; duration?: number }> => {
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
const isVideo = file.type.startsWith('video/');
|
||||||
|
const isAudio = file.type.startsWith('audio/');
|
||||||
|
|
||||||
|
if (!isImage && !isVideo && !isAudio) {
|
||||||
|
message.error('仅支持图片、视频或音频文件');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxMB = isVideo ? 100 : (isAudio ? 50 : 10);
|
||||||
|
if (file.size / 1024 / 1024 > maxMB) {
|
||||||
|
message.error(`${isVideo ? '视频' : (isAudio ? '音频' : '图片')}大小不能超过${maxMB}MB`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAudio) {
|
||||||
|
const audioExt = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (!['wav', 'mp3'].includes(audioExt || '')) {
|
||||||
|
message.error('音频仅支持wav和mp3格式');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoDuration = 0;
|
||||||
|
let audioDuration = 0;
|
||||||
|
if (isVideo) {
|
||||||
|
try {
|
||||||
|
videoDuration = await getVideoDuration(file);
|
||||||
|
if (videoDuration < 2) {
|
||||||
|
message.error('视频素材最短不能少于 2 秒');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const latestMedia = useAppStore.getState().currentMedia;
|
||||||
|
const existingVideoDuration = latestMedia
|
||||||
|
.filter((m) => m.type === 'video')
|
||||||
|
.reduce((sum, m) => sum + (m.duration || 0), 0);
|
||||||
|
if (existingVideoDuration + videoDuration > 15) {
|
||||||
|
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)} 秒`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('无法获取视频信息,请检查文件是否损坏');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAudio) {
|
||||||
|
try {
|
||||||
|
audioDuration = await getAudioDuration(file);
|
||||||
|
if (audioDuration < 2) {
|
||||||
|
message.error('音频素材最短不能少于 2 秒');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const latestMedia = useAppStore.getState().currentMedia;
|
||||||
|
const existingAudioDuration = latestMedia
|
||||||
|
.filter((m) => m.type === 'audio')
|
||||||
|
.reduce((sum, m) => sum + (m.duration || 0), 0);
|
||||||
|
if (existingAudioDuration + audioDuration > 15) {
|
||||||
|
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)} 秒`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('无法获取音频信息,请检查文件是否损坏');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadFn = isImage ? uploadImage : (isAudio ? uploadAudio : uploadVideo);
|
||||||
|
const res = await uploadFn(file);
|
||||||
|
const mediaType: 'image' | 'video' | 'audio' = isImage ? 'image' : (isAudio ? 'audio' : 'video');
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
type: mediaType,
|
||||||
|
url: res.url,
|
||||||
|
label: '',
|
||||||
|
...(isVideo && { duration: videoDuration }),
|
||||||
|
...(isAudio && { duration: audioDuration }),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
message.error('上传失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchUpload = async (files: File[]) => {
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
|
const result = await doUpload(file);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const latestMedia = useAppStore.getState().currentMedia;
|
||||||
|
const newList = [...latestMedia, result];
|
||||||
|
const labels = generateMediaLabels(newList);
|
||||||
|
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
message.success(`成功上传${successCount}个文件${failCount > 0 ? `,${failCount}个文件上传失败` : ''}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemoveMedia = (index: number) => {
|
const handleRemoveMedia = (index: number) => {
|
||||||
const newList = currentMedia.filter((_, i) => i !== index);
|
const newList = currentMedia.filter((_, i) => i !== index);
|
||||||
@@ -1305,12 +1420,12 @@ const AIChatPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
// const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
// if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
// e.preventDefault();
|
||||||
handleSend();
|
// handleSend();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 检测光标前的 @ 符号
|
// 检测光标前的 @ 符号
|
||||||
const checkMention = (textarea: HTMLTextAreaElement, value: string) => {
|
const checkMention = (textarea: HTMLTextAreaElement, value: string) => {
|
||||||
@@ -1470,7 +1585,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
{/* 隐藏的音频播放器 */}
|
{/* 隐藏的音频播放器 */}
|
||||||
<audio
|
<audio
|
||||||
id="audio-player"
|
id="audio-player"
|
||||||
src={playingAudioUrl || ''}
|
src={playingAudioUrl || null}
|
||||||
autoPlay
|
autoPlay
|
||||||
onEnded={() => setPlayingAudioUrl(null)}
|
onEnded={() => setPlayingAudioUrl(null)}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
@@ -2351,17 +2466,43 @@ const AIChatPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{/* 没有上传时的卡片样式 */}
|
{/* 没有上传时的卡片样式 */}
|
||||||
{currentMedia.length === 0 && (
|
{currentMedia.length === 0 && (
|
||||||
<Upload
|
<UploadSelector
|
||||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||||
showUploadList={false}
|
onLocalSelect={handleBatchUpload}
|
||||||
beforeUpload={handleUpload}
|
onHistorySelect={(items) => {
|
||||||
>
|
const newMedia = items.map((item: any) => ({
|
||||||
<Tooltip title={mediaType === 'image'
|
name: item.name,
|
||||||
|
type: item.type as 'image' | 'video' | 'audio',
|
||||||
|
url: '',
|
||||||
|
label: '',
|
||||||
|
}));
|
||||||
|
setCurrentMedia([...currentMedia, ...newMedia]);
|
||||||
|
message.success(`成功添加${items.length}个历史记录`);
|
||||||
|
}}
|
||||||
|
onPortraitSelect={async (items) => {
|
||||||
|
const files: File[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(item.avatar);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], item.name, { type: blob.type });
|
||||||
|
files.push(file);
|
||||||
|
} catch {
|
||||||
|
message.error(`图片${item.name}下载失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (files.length > 0) {
|
||||||
|
await handleBatchUpload(files);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
uploading={uploading}
|
||||||
|
tooltipTitle={mediaType === 'image'
|
||||||
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
|
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
|
||||||
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},
|
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},
|
||||||
视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}${maxAudio > 0 ? `,
|
视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}${maxAudio > 0 ? `,
|
||||||
音频${currentMedia.filter(m => m.type === 'audio').length}/${maxAudio}` : ''}`
|
音频${currentMedia.filter(m => m.type === 'audio').length}/${maxAudio}` : ''}`
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 54,
|
width: 54,
|
||||||
@@ -2404,8 +2545,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</UploadSelector>
|
||||||
</Upload>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
|
{/* 层叠附件展示 - 鼠标移入向右排列展开 */}
|
||||||
@@ -2510,17 +2650,43 @@ const AIChatPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* 右下角圆形+上传按钮 */}
|
{/* 右下角圆形+上传按钮 */}
|
||||||
{currentMedia.length > 0 && (
|
{currentMedia.length > 0 && (
|
||||||
<Upload
|
<UploadSelector
|
||||||
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
|
||||||
showUploadList={false}
|
onLocalSelect={handleBatchUpload}
|
||||||
beforeUpload={handleUpload}
|
onHistorySelect={(items) => {
|
||||||
>
|
const newMedia = items.map((item: any) => ({
|
||||||
<Tooltip title={mediaType === 'image'
|
name: item.name,
|
||||||
|
type: item.type as 'image' | 'video' | 'audio',
|
||||||
|
url: '',
|
||||||
|
label: '',
|
||||||
|
}));
|
||||||
|
setCurrentMedia([...currentMedia, ...newMedia]);
|
||||||
|
message.success(`成功添加${items.length}个历史记录`);
|
||||||
|
}}
|
||||||
|
onPortraitSelect={async (items) => {
|
||||||
|
const files: File[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(item.avatar);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], item.name, { type: blob.type });
|
||||||
|
files.push(file);
|
||||||
|
} catch {
|
||||||
|
message.error(`图片${item.name}下载失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (files.length > 0) {
|
||||||
|
await handleBatchUpload(files);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
uploading={uploading}
|
||||||
|
tooltipTitle={mediaType === 'image'
|
||||||
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
|
? `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount}`
|
||||||
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},
|
: `图片${currentMedia.filter(m => m.type === 'image').length}/${maxImageCount},
|
||||||
视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}${maxAudio > 0 ? `,
|
视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}${maxAudio > 0 ? `,
|
||||||
音频${currentMedia.filter(m => m.type === 'audio').length}/${maxAudio}` : ''}`
|
音频${currentMedia.filter(m => m.type === 'audio').length}/${maxAudio}` : ''}`
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -2550,8 +2716,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<PlusOutlined style={{ fontSize: 14, color: '#8b5cf6', lineHeight: 1 }} />
|
<PlusOutlined style={{ fontSize: 14, color: '#8b5cf6', lineHeight: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</UploadSelector>
|
||||||
</Upload>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2587,7 +2752,23 @@ const AIChatPage: React.FC = () => {
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
onKeyPress={handleKeyPress}
|
onPaste={(e) => {
|
||||||
|
const items = e.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
const imageFiles: File[] = [];
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.startsWith('image/')) {
|
||||||
|
const file = items[i].getAsFile();
|
||||||
|
if (file) imageFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
imageFiles.forEach(async (file) => {
|
||||||
|
await handleUpload(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={composerPlaceholder}
|
placeholder={composerPlaceholder}
|
||||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
getRecordsPage,
|
getRecordsPage,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { formatDate } from "../utils/formatDate";
|
import { formatDate } from "../utils/formatDate";
|
||||||
|
import UploadSelector from "../components/UploadSelector";
|
||||||
import { generateUUID } from "../utils/uuid";
|
import { generateUUID } from "../utils/uuid";
|
||||||
|
|
||||||
// const calcVideoCredits = (duration: number, resolution: Resolution): number => {
|
// const calcVideoCredits = (duration: number, resolution: Resolution): number => {
|
||||||
@@ -260,10 +261,58 @@ const GeneratePage: React.FC = () => {
|
|||||||
const [creditRatios, setCreditRatios] = useState<any>([]);
|
const [creditRatios, setCreditRatios] = useState<any>([]);
|
||||||
const [cimage, setCimage] = useState<any>([]);
|
const [cimage, setCimage] = useState<any>([]);
|
||||||
|
|
||||||
|
const handlePasteUpload = async (file: File) => {
|
||||||
|
const isImage = file.type.startsWith("image/");
|
||||||
|
const isVideo = file.type.startsWith("video/");
|
||||||
|
if (!isImage && !isVideo) {
|
||||||
|
message.error("仅支持图片或视频文件");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const maxMB = isVideo ? 100 : 10;
|
||||||
|
if (file.size / 1024 / 1024 > maxMB) {
|
||||||
|
message.error(
|
||||||
|
`${isVideo ? "视频" : "图片"}大小不能超过${maxMB}MB`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const imageCount = references.filter(
|
||||||
|
(r) => r.type === "image",
|
||||||
|
).length;
|
||||||
|
const videoCount = references.filter(
|
||||||
|
(r) => r.type === "video",
|
||||||
|
).length;
|
||||||
|
if (isImage && imageCount >= 10) {
|
||||||
|
message.error("最多上传10张图片");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isVideo && videoCount >= 3) {
|
||||||
|
message.error("最多上传3个视频");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setUploading(true);
|
||||||
|
const uploadFn = isImage ? uploadImage : uploadVideo;
|
||||||
|
try {
|
||||||
|
const res = await uploadFn(file);
|
||||||
|
const typeLabel = isImage ? "图片" : "视频";
|
||||||
|
const typeCount = isImage
|
||||||
|
? imageCount + 1
|
||||||
|
: videoCount + 1;
|
||||||
|
setReferences((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
url: res.url,
|
||||||
|
type: isImage ? "image" : "video",
|
||||||
|
name: `${typeLabel}${typeCount}`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
message.success(`${typeLabel}上传成功`);
|
||||||
|
} catch {
|
||||||
|
message.error("上传失败");
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
// 点击空白处关闭图片设置浮层
|
// 点击空白处关闭图片设置浮层
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1772,62 +1821,38 @@ const GeneratePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Upload
|
<UploadSelector
|
||||||
accept="image/*,video/*"
|
accept="image/*,video/*"
|
||||||
showUploadList={false}
|
onLocalSelect={(files) => {
|
||||||
multiple
|
files.forEach(async (file) => {
|
||||||
beforeUpload={(file) => {
|
await handlePasteUpload(file);
|
||||||
const isImage = file.type.startsWith("image/");
|
});
|
||||||
const isVideo = file.type.startsWith("video/");
|
|
||||||
if (!isImage && !isVideo) {
|
|
||||||
message.error("仅支持图片或视频文件");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const maxMB = isVideo ? 100 : 10;
|
|
||||||
if (file.size / 1024 / 1024 > maxMB) {
|
|
||||||
message.error(
|
|
||||||
`${isVideo ? "视频" : "图片"}大小不能超过${maxMB}MB`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const imageCount = references.filter(
|
|
||||||
(r) => r.type === "image",
|
|
||||||
).length;
|
|
||||||
const videoCount = references.filter(
|
|
||||||
(r) => r.type === "video",
|
|
||||||
).length;
|
|
||||||
if (isImage && imageCount >= 10) {
|
|
||||||
message.error("最多上传10张图片");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isVideo && videoCount >= 3) {
|
|
||||||
message.error("最多上传3个视频");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
setUploading(true);
|
|
||||||
const uploadFn = isImage ? uploadImage : uploadVideo;
|
|
||||||
uploadFn(file)
|
|
||||||
.then((res) => {
|
|
||||||
const typeLabel = isImage ? "图片" : "视频";
|
|
||||||
const typeCount = isImage
|
|
||||||
? imageCount + 1
|
|
||||||
: videoCount + 1;
|
|
||||||
setReferences((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
url: res.url,
|
|
||||||
type: isImage ? "image" : "video",
|
|
||||||
name: `${typeLabel}${typeCount}`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
message.success(`${typeLabel}上传成功`);
|
|
||||||
})
|
|
||||||
.catch(() => message.error("上传失败"))
|
|
||||||
.finally(() => setUploading(false));
|
|
||||||
return false;
|
|
||||||
}}
|
}}
|
||||||
|
onHistorySelect={(items) => {
|
||||||
|
items.forEach((item: any) => {
|
||||||
|
setReferences(prev => [...prev, {
|
||||||
|
url: '',
|
||||||
|
type: item.type,
|
||||||
|
name: item.name,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
message.success(`成功添加${items.length}个历史记录`);
|
||||||
|
}}
|
||||||
|
onPortraitSelect={async (items) => {
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(item.avatar);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], item.name, { type: blob.type });
|
||||||
|
await handlePasteUpload(file);
|
||||||
|
} catch {
|
||||||
|
message.error(`图片${item.name}下载失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
uploading={uploading}
|
||||||
|
tooltipTitle={`参考内容(${references.length}/10)`}
|
||||||
>
|
>
|
||||||
<Tooltip title={`参考内容(${references.length}/10)`}>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 48,
|
width: 48,
|
||||||
@@ -1861,8 +1886,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</UploadSelector>
|
||||||
</Upload>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Textarea */}
|
{/* Textarea */}
|
||||||
@@ -1885,6 +1909,23 @@ const GeneratePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
setShowMention(false);
|
setShowMention(false);
|
||||||
}}
|
}}
|
||||||
|
onPaste={(e) => {
|
||||||
|
const items = e.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
const imageFiles: File[] = [];
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.startsWith('image/')) {
|
||||||
|
const file = items[i].getAsFile();
|
||||||
|
if (file) imageFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
imageFiles.forEach(async (file) => {
|
||||||
|
await handlePasteUpload(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="上传参考素材、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
placeholder="上传参考素材、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
@@ -4547,7 +4588,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={480}
|
width={480}
|
||||||
centered
|
centered
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
closable={false}
|
closable={false}
|
||||||
title={null}
|
title={null}
|
||||||
styles={{
|
styles={{
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ import {
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
|
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
|
||||||
|
|
||||||
import hot from '../assets/homebtn1.png';
|
|
||||||
import mashup from '../assets/homebtn2.png';
|
|
||||||
import aicreate from '../assets/homebtn3.png';
|
|
||||||
|
|
||||||
|
|
||||||
// 把 ISO 时间格式化成 MM-DD HH:mm(与图片一致)
|
// 把 ISO 时间格式化成 MM-DD HH:mm(与图片一致)
|
||||||
@@ -45,6 +43,7 @@ const HomePage: React.FC = () => {
|
|||||||
const [caseAssets, setCaseAssets] = useState<any[]>([]);
|
const [caseAssets, setCaseAssets] = useState<any[]>([]);
|
||||||
const [previewAsset, setPreviewAsset] = useState<any>(null);
|
const [previewAsset, setPreviewAsset] = useState<any>(null);
|
||||||
const previewVideoRef = useRef<HTMLVideoElement>(null);
|
const previewVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('works');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getHomeCaseHeader().then((res: any) => {
|
getHomeCaseHeader().then((res: any) => {
|
||||||
@@ -97,21 +96,28 @@ const HomePage: React.FC = () => {
|
|||||||
|
|
||||||
const aiEntries = [
|
const aiEntries = [
|
||||||
{
|
{
|
||||||
icon: <FileTextOutlined style={{ fontSize: 24, color: '#6366f1' }} />,
|
icon: <FileTextOutlined style={{ fontSize: 24 }} />,
|
||||||
|
title: '项目创建',
|
||||||
|
description: '新建项目、设置图文视频参数、核对信息并生成素材',
|
||||||
|
action: '立即创作',
|
||||||
|
path: '/projects',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FileTextOutlined style={{ fontSize: 24 }} />,
|
||||||
title: '爆款复刻',
|
title: '爆款复刻',
|
||||||
description: '上传参考视频与产品图片,一键复刻爆款视频开头',
|
description: '上传参考视频与产品图片,一键复刻爆款视频开头',
|
||||||
action: '立即创作',
|
action: '立即创作',
|
||||||
path: '/initial',
|
path: '/initial',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ScissorOutlined style={{ fontSize: 24, color: '#f97316' }} />,
|
icon: <ScissorOutlined style={{ fontSize: 24 }} />,
|
||||||
title: '拆镜复刻',
|
title: '拆镜复刻',
|
||||||
description: '精细化镜头复刻工具,拆分参考视频单镜头独立复刻,提升素材原创度,规避素材同质化',
|
description: '精细化镜头复刻工具,拆分参考视频单镜头独立复刻,提升素材原创度,规避素材同质化',
|
||||||
action: '开始混剪',
|
action: '开始拆镜',
|
||||||
path: '/removelens',
|
path: '/removelens',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <RobotOutlined style={{ fontSize: 24, color: '#10b981' }} />,
|
icon: <RobotOutlined style={{ fontSize: 24 }} />,
|
||||||
title: 'AI成片',
|
title: 'AI成片',
|
||||||
description: '输入想法、剧本或上传参考,智能生成视频/图片',
|
description: '输入想法、剧本或上传参考,智能生成视频/图片',
|
||||||
action: '立即生成',
|
action: '立即生成',
|
||||||
@@ -132,13 +138,6 @@ const HomePage: React.FC = () => {
|
|||||||
? mockVideos
|
? mockVideos
|
||||||
: mockVideos.filter(v => v.type === activeTab);
|
: mockVideos.filter(v => v.type === activeTab);
|
||||||
|
|
||||||
const materialCases = [
|
|
||||||
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=modern%20city%20skyline%20night%20view&image_size=landscape_16_9',
|
|
||||||
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=nature%20forest%20landscape%20sunlight&image_size=landscape_16_9',
|
|
||||||
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=abstract%20technology%20background%20digital&image_size=landscape_16_9',
|
|
||||||
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=food%20cooking%20kitchen%20delicious&image_size=landscape_16_9',
|
|
||||||
'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=fashion%20clothing%20style%20elegant&image_size=landscape_16_9',
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content_box">
|
<div className="content_box">
|
||||||
@@ -172,7 +171,7 @@ const HomePage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* ========== 顶部工作台引导区域(三步流程) ========== */}
|
{/* ========== 顶部工作台引导区域(三步流程) ========== */}
|
||||||
<div className="animate-fadeInUp" style={{
|
{/* <div className="animate-fadeInUp" style={{
|
||||||
padding: '24px 28px',
|
padding: '24px 28px',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
background: 'linear-gradient(135deg, #f0f9ff 0%, #faf5ff 50%, #fef3c7 100%)',
|
background: 'linear-gradient(135deg, #f0f9ff 0%, #faf5ff 50%, #fef3c7 100%)',
|
||||||
@@ -181,7 +180,6 @@ const HomePage: React.FC = () => {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
{/* 区域标题 */}
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18 }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
@@ -194,19 +192,6 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div >
|
|
||||||
<span style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 12, color: '#1a50bbff', letterSpacing: 0.3,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
navigate('/authorization')
|
|
||||||
}}
|
|
||||||
>如需进行账户素材推送 一键推送
|
|
||||||
<ArrowRightOutlined style={{ marginLeft: 8, transform: 'rotate(0deg)' }} />
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,7 +212,6 @@ const HomePage: React.FC = () => {
|
|||||||
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
|
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
|
||||||
第1步
|
第1步
|
||||||
</div>
|
</div>
|
||||||
{/* 插图占位:项目卡(标题/描述输入框 + 行业分类 chip) */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 140,
|
minHeight: 140,
|
||||||
@@ -240,21 +224,18 @@ const HomePage: React.FC = () => {
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
}}>
|
}}>
|
||||||
{/* 项目名称占位 */}
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<div style={{ width: 8, height: 8, borderRadius: 2, background: '#6366f1' }} />
|
<div style={{ width: 8, height: 8, borderRadius: 2, background: '#6366f1' }} />
|
||||||
<div style={{ flex: 1, height: 22, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4, display: 'flex', alignItems: 'center', padding: '0 8px', fontSize: 10, color: '#94a3b8' }}>
|
<div style={{ flex: 1, height: 22, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4, display: 'flex', alignItems: 'center', padding: '0 8px', fontSize: 10, color: '#94a3b8' }}>
|
||||||
项目名称...
|
项目名称...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 行业分类 chip */}
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||||
<div style={{ padding: '2px 8px', background: '#eef2ff', color: '#6366f1', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #c7d2fe' }}>美妆</div>
|
<div style={{ padding: '2px 8px', background: '#eef2ff', color: '#6366f1', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #c7d2fe' }}>美妆</div>
|
||||||
<div style={{ padding: '2px 8px', background: '#fff7ed', color: '#f97316', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #fed7aa' }}>美食</div>
|
<div style={{ padding: '2px 8px', background: '#fff7ed', color: '#f97316', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #fed7aa' }}>美食</div>
|
||||||
<div style={{ padding: '2px 8px', background: '#ecfdf5', color: '#10b981', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #a7f3d0' }}>3C数码</div>
|
<div style={{ padding: '2px 8px', background: '#ecfdf5', color: '#10b981', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #a7f3d0' }}>3C数码</div>
|
||||||
<div style={{ padding: '2px 8px', background: '#f5f3ff', color: '#8b5cf6', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #ddd6fe' }}>服饰</div>
|
<div style={{ padding: '2px 8px', background: '#f5f3ff', color: '#8b5cf6', borderRadius: 10, fontSize: 10, fontWeight: 500, border: '1px solid #ddd6fe' }}>服饰</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 描述占位行 */}
|
|
||||||
<div style={{ height: 16, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
|
<div style={{ height: 16, background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
|
||||||
<div style={{ height: 16, width: '70%', background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
|
<div style={{ height: 16, width: '70%', background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4 }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -287,7 +268,6 @@ const HomePage: React.FC = () => {
|
|||||||
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
|
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
|
||||||
第2步
|
第2步
|
||||||
</div>
|
</div>
|
||||||
{/* 插图占位:图片/视频切换 + 尺寸/时长参数 */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 140,
|
minHeight: 140,
|
||||||
@@ -300,7 +280,6 @@ const HomePage: React.FC = () => {
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
}}>
|
}}>
|
||||||
{/* 图片 / 视频 切换 */}
|
|
||||||
<div style={{ display: 'flex', background: '#f1f5f9', borderRadius: 6, padding: 2, gap: 2 }}>
|
<div style={{ display: 'flex', background: '#f1f5f9', borderRadius: 6, padding: 2, gap: 2 }}>
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px 0', background: '#fff', borderRadius: 4, fontSize: 11, fontWeight: 600, color: '#6366f1', boxShadow: '0 1px 3px rgba(99,102,241,0.15)' }}>
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px 0', background: '#fff', borderRadius: 4, fontSize: 11, fontWeight: 600, color: '#6366f1', boxShadow: '0 1px 3px rgba(99,102,241,0.15)' }}>
|
||||||
<VideoCameraOutlined style={{ fontSize: 11 }} />视频
|
<VideoCameraOutlined style={{ fontSize: 11 }} />视频
|
||||||
@@ -309,7 +288,6 @@ const HomePage: React.FC = () => {
|
|||||||
<PictureOutlined style={{ fontSize: 11 }} /> 图片
|
<PictureOutlined style={{ fontSize: 11 }} /> 图片
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 尺寸参数 */}
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>尺寸比例</div>
|
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>尺寸比例</div>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
@@ -318,7 +296,6 @@ const HomePage: React.FC = () => {
|
|||||||
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>1:1</div>
|
<div style={{ flex: 1, textAlign: 'center', padding: '4px 0', background: '#fff', color: '#64748b', border: '1px solid #e2e8f0', borderRadius: 4, fontSize: 10 }}>1:1</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 时长参数 */}
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>时长</div>
|
<div style={{ fontSize: 9, color: '#94a3b8', marginBottom: 3 }}>时长</div>
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
@@ -357,7 +334,6 @@ const HomePage: React.FC = () => {
|
|||||||
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
|
<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 700, color: '#1e293b', marginBottom: 14, letterSpacing: 1 }}>
|
||||||
第3步
|
第3步
|
||||||
</div>
|
</div>
|
||||||
{/* 插图占位:核对清单(✓ 项)+ 一键生成按钮 */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 140,
|
minHeight: 140,
|
||||||
@@ -380,12 +356,7 @@ const HomePage: React.FC = () => {
|
|||||||
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#10b981', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>✓</div>
|
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#10b981', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>✓</div>
|
||||||
<div style={{ fontSize: 10, color: '#065f46', fontWeight: 500 }}>尺寸 9:16 · 时长 5s</div>
|
<div style={{ fontSize: 10, color: '#065f46', fontWeight: 500 }}>尺寸 9:16 · 时长 5s</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', background: '#fff7ed', border: '1px solid #fed7aa', borderRadius: 5 }}>
|
|
||||||
<div style={{ width: 14, height: 14, borderRadius: '50%', background: '#f97316', color: '#fff', fontSize: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>!</div>
|
|
||||||
<div style={{ fontSize: 10, color: '#9a3412', fontWeight: 500 }}>参考素材 0/3</div>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
{/* 一键生成按钮 */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -450,7 +421,6 @@ const HomePage: React.FC = () => {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 高光层(hover 时轻微变亮) */}
|
|
||||||
<span style={{
|
<span style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
@@ -466,11 +436,11 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* ========== AI 创作入口区域 ========== */}
|
{/* ========== AI 创作入口区域 ========== */}
|
||||||
<div className="animate-fadeInUp stagger-children" style={{
|
<div className="animate-fadeInUp" style={{
|
||||||
padding: '24px 28px',
|
padding: '20px',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
border: '1px solid #e2e8f0',
|
border: '1px solid #e2e8f0',
|
||||||
@@ -480,7 +450,7 @@ const HomePage: React.FC = () => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
marginBottom: 18,
|
marginBottom: 16,
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 4, height: 18, borderRadius: 2,
|
width: 4, height: 18, borderRadius: 2,
|
||||||
@@ -494,13 +464,13 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 16 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
|
||||||
{aiEntries.map((entry, index) => {
|
{aiEntries.map((entry, index) => {
|
||||||
// 三个入口用三种不同色调的渐变光晕作为视觉区分,但都保持白底卡片
|
|
||||||
const accentMap = [
|
const accentMap = [
|
||||||
{ color: '#6366f1', light: 'rgba(99,102,241,0.10)', tag: '复刻', bg: hot },
|
{ color: '#3b82f6', light: 'rgba(59,130,246,0.12)', tag: '项目' },
|
||||||
{ color: '#f97316', light: 'rgba(249,115,22,0.10)', tag: '混剪', bg: mashup },
|
{ color: '#6366f1', light: 'rgba(99,102,241,0.12)', tag: '复刻' },
|
||||||
{ color: '#10b981', light: 'rgba(16,185,129,0.10)', tag: '云创', bg: aicreate },
|
{ color: '#f97316', light: 'rgba(249,115,22,0.12)', tag: '拆镜' },
|
||||||
|
{ color: '#10b981', light: 'rgba(16,185,129,0.12)', tag: '云创' },
|
||||||
];
|
];
|
||||||
const accent = accentMap[index] || accentMap[0];
|
const accent = accentMap[index] || accentMap[0];
|
||||||
return (
|
return (
|
||||||
@@ -509,58 +479,71 @@ const HomePage: React.FC = () => {
|
|||||||
onClick={() => navigate(entry.path)}
|
onClick={() => navigate(entry.path)}
|
||||||
className="project-card"
|
className="project-card"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: '1',
|
||||||
padding: '20px 22px',
|
minWidth: 260,
|
||||||
borderRadius: 14,
|
height: 120,
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: 16,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
border: '1px solid #e2e8f0',
|
border: '1px solid #e2e8f0',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.3s cubic-bezier(0.4,0,0.2,1)',
|
transition: 'all 0.3s cubic-bezier(0.4,0,0.2,1)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
display: 'flex',
|
||||||
backgroundImage: `url(${accent.bg})`,
|
alignItems: 'center',
|
||||||
backgroundRepeat: 'no-repeat',
|
gap: 14,
|
||||||
backgroundSize: '100% 100%',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.borderColor = accent.color;
|
e.currentTarget.style.borderColor = accent.color;
|
||||||
e.currentTarget.style.boxShadow = `0 12px 32px ${accent.light}`;
|
e.currentTarget.style.boxShadow = `0 8px 24px ${accent.light}`;
|
||||||
|
e.currentTarget.style.background = accent.light;
|
||||||
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.borderColor = '#e2e8f0';
|
e.currentTarget.style.borderColor = '#e2e8f0';
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
e.currentTarget.style.background = '#fff';
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 顶部装饰光带 */}
|
<div
|
||||||
<div style={{
|
style={{
|
||||||
position: 'absolute',
|
|
||||||
top: 0, left: 0, right: 0, height: 3,
|
|
||||||
background: `linear-gradient(90deg, ${accent.color}, ${accent.color}88)`,
|
|
||||||
}} />
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
|
||||||
<div style={{
|
|
||||||
width: 48, height: 48,
|
width: 48, height: 48,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: accent.light,
|
background: accent.light,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}>
|
flexShrink: 0,
|
||||||
<span style={{ color: accent.color, fontSize: 22, display: 'flex' }}>{entry.icon}</span>
|
transition: 'all 0.3s ease',
|
||||||
|
color: accent.color,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = accent.color;
|
||||||
|
e.currentTarget.style.color = '#fff';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = accent.light;
|
||||||
|
e.currentTarget.style.color = accent.color;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 20, display: 'flex', transition: 'all 0.3s ease' }}>{entry.icon}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
fontSize: 11, color: accent.color,
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||||||
padding: '2px 8px', borderRadius: 6,
|
<span style={{ fontSize: 15, fontWeight: 600, color: '#1e293b', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
background: accent.light, fontWeight: 600,
|
|
||||||
}}>{accent.tag}</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 16, fontWeight: 700, color: '#1f2937', marginBottom: 4 }}>
|
|
||||||
{entry.title}
|
{entry.title}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10, color: accent.color,
|
||||||
|
padding: '2px 7px', borderRadius: 4,
|
||||||
|
background: accent.light, fontWeight: 600,
|
||||||
|
}}>{accent.tag}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 14, lineHeight: 1.5, minHeight: 36 }}>
|
<div style={{ fontSize: 12, color: '#64748b', }}>
|
||||||
{entry.description}
|
{entry.description}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -568,6 +551,7 @@ const HomePage: React.FC = () => {
|
|||||||
color: accent.color,
|
color: accent.color,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{entry.action}
|
{entry.action}
|
||||||
<ArrowRightOutlined style={{ fontSize: 12 }} />
|
<ArrowRightOutlined style={{ fontSize: 12 }} />
|
||||||
@@ -578,27 +562,57 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ========== 近期作品区域 ========== */}
|
{/* ========== 作品与案例区域 ========== */}
|
||||||
<div className="animate-fadeInUp" style={{
|
<div className="animate-fadeInUp" style={{
|
||||||
padding: '24px 28px',
|
padding: '24px 28px',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
border: '1px solid #e2e8f0',
|
border: '1px solid #e2e8f0',
|
||||||
marginBottom: 20,
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
{/* <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 4, height: 18, borderRadius: 2,
|
width: 4, height: 18, borderRadius: 2,
|
||||||
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
|
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
|
||||||
}} />
|
}} />
|
||||||
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
|
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
|
||||||
近期作品
|
{activeContentTab === 'works' ? '近期作品' : '素材案例'}
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* 外层Tab切换:近期作品 / 素材案例 */}
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
{[
|
||||||
|
{ key: 'works', label: '近期作品' },
|
||||||
|
{ key: 'cases', label: '素材案例' },
|
||||||
|
].map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.key}
|
||||||
|
onClick={() => setActiveContentTab(item.key as 'works' | 'cases')}
|
||||||
|
style={{
|
||||||
|
padding: '6px 16px',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.25s ease',
|
||||||
|
background: activeContentTab === item.key
|
||||||
|
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||||
|
: '#f1f5f9',
|
||||||
|
color: activeContentTab === item.key ? '#fff' : '#64748b',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
{/* 内容区域 */}
|
||||||
{/* Tab切换 */}
|
{activeContentTab === 'works' ? (
|
||||||
|
<>
|
||||||
|
{/* 近期作品子Tab */}
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
@@ -611,7 +625,7 @@ const HomePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 视频网格 */}
|
{/* 近期作品网格 */}
|
||||||
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}>
|
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}>
|
||||||
{filteredVideos.length === 0 ? (
|
{filteredVideos.length === 0 ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -629,10 +643,6 @@ const HomePage: React.FC = () => {
|
|||||||
key={video.id || `${video.type}-${Math.random()}`}
|
key={video.id || `${video.type}-${Math.random()}`}
|
||||||
className="project-card"
|
className="project-card"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 按模块分发跳转:
|
|
||||||
// - 爆款复刻(hot)→ 复刻详情页
|
|
||||||
// - AI 成片(ai)→ 对话/生成页
|
|
||||||
// - 项目记录(project)→ 项目详情页
|
|
||||||
const id = video.moduleProjectId;
|
const id = video.moduleProjectId;
|
||||||
if (video.type === 'hotOpeningReplicate' && id != null) {
|
if (video.type === 'hotOpeningReplicate' && id != null) {
|
||||||
navigate(`/initial/${id}/initialinfo`);
|
navigate(`/initial/${id}/initialinfo`);
|
||||||
@@ -655,28 +665,15 @@ const HomePage: React.FC = () => {
|
|||||||
<div style={{
|
<div style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
aspectRatio: '16/9',
|
aspectRatio: '16/9',
|
||||||
// background: '#1a1a2e',
|
|
||||||
}}>
|
}}>
|
||||||
{(() => {
|
{(() => {
|
||||||
// 1) 读取后端 API 基础地址;环境变量未配置时降级到本地 8000
|
|
||||||
const apiBase = (import.meta.env.VITE_API_BASE as string) || 'http://localhost:8000';
|
const apiBase = (import.meta.env.VITE_API_BASE as string) || 'http://localhost:8000';
|
||||||
|
|
||||||
// 2) 判断当前作品是否为"图片":
|
|
||||||
// - 爆款复刻(type === 'hot')始终是视频,不参与图片判断
|
|
||||||
// - 其他模块(项目记录 / AI 成片)根据 genType 判定
|
|
||||||
// - genType 可能是字符串 'image',也可能是数字 1(兼容两种后端约定)
|
|
||||||
const isImage = video.type !== 'hotOpeningReplicate'
|
const isImage = video.type !== 'hotOpeningReplicate'
|
||||||
&& (
|
&& (
|
||||||
String(video.resourceType ?? '').toLowerCase() === 'image'
|
String(video.resourceType ?? '').toLowerCase() === 'image'
|
||||||
|| video.resourceType === 1
|
|| video.resourceType === 1
|
||||||
|| String(video.resourceType ?? '') === '1'
|
|| String(video.resourceType ?? '') === '1'
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3) 根据媒体类型选择对应的资源路径:
|
|
||||||
// - 图片:后端返回的 imageUrl 已经是带签名的完整相对路径
|
|
||||||
// 形如 /static/generate/images/2026/06/26/0019f017f5924df4123.png?exp=...&sign=...&w=300&p=50
|
|
||||||
// - 视频:使用视频封面 videoCoverUrl(这是视频作品的静态缩略图)
|
|
||||||
// - 爆款复刻(type === 'hot')特殊处理:使用 finalVideoCoverUrl
|
|
||||||
let rawPath = '';
|
let rawPath = '';
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
rawPath = '/static' + video.resultUrl + '&w=300&p=50' || '';
|
rawPath = '/static' + video.resultUrl + '&w=300&p=50' || '';
|
||||||
@@ -685,15 +682,9 @@ const HomePage: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
rawPath = video.coverUrl || video.resultUrl || video.resultUrl || '';
|
rawPath = video.coverUrl || video.resultUrl || video.resultUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) 拼装最终 src:
|
|
||||||
// - rawPath 为空 → 用空串(让 <img> 走 onError 兜底)
|
|
||||||
// - 已经是 http(s) 完整 URL → 直接使用(OSS / CDN 场景)
|
|
||||||
// - 否则视为后端相对路径,前面拼 apiBase
|
|
||||||
const src = rawPath
|
const src = rawPath
|
||||||
? (rawPath.startsWith('http') ? rawPath : apiBase + rawPath)
|
? (rawPath.startsWith('http') ? rawPath : apiBase + rawPath)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
@@ -733,7 +724,6 @@ const HomePage: React.FC = () => {
|
|||||||
color: '#64748b',
|
color: '#64748b',
|
||||||
}}>
|
}}>
|
||||||
{(() => {
|
{(() => {
|
||||||
// 显示所属模块,而非媒体类型
|
|
||||||
const moduleMap: Record<string, string> = {
|
const moduleMap: Record<string, string> = {
|
||||||
project: '项目媒体',
|
project: '项目媒体',
|
||||||
chatAi: 'AI成片',
|
chatAi: 'AI成片',
|
||||||
@@ -765,47 +755,10 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 素材案例子Tab */}
|
||||||
|
|
||||||
{/* ========== 素材案例区域 ========== */}
|
|
||||||
{caseAssets.length > 0 && (
|
|
||||||
<div className="animate-fadeInUp" style={{
|
|
||||||
padding: '24px 28px',
|
|
||||||
borderRadius: 16,
|
|
||||||
background: '#fff',
|
|
||||||
border: '1px solid #e2e8f0',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: 18,
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<div style={{
|
|
||||||
width: 4, height: 18, borderRadius: 2,
|
|
||||||
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
|
|
||||||
}} />
|
|
||||||
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
|
|
||||||
素材案例
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#94a3b8', marginLeft: 4 }}>
|
|
||||||
精选优质作品参考
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div style={{
|
|
||||||
fontSize: 13, color: '#6366f1', cursor: 'pointer', fontWeight: 500,
|
|
||||||
display: 'flex', alignItems: 'center', gap: 2,
|
|
||||||
}}>
|
|
||||||
更多案例
|
|
||||||
<ArrowRightOutlined style={{ fontSize: 11 }} />
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab切换 */}
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeCaseTab}
|
activeKey={activeCaseTab}
|
||||||
@@ -827,7 +780,7 @@ const HomePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ========== 素材案例列表(与近期作品一致) ========== */}
|
{/* 素材案例网格 */}
|
||||||
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}>
|
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}>
|
||||||
{caseAssets.length === 0 ? (
|
{caseAssets.length === 0 ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -853,7 +806,6 @@ const HomePage: React.FC = () => {
|
|||||||
border: '1px solid #e2e8f0',
|
border: '1px solid #e2e8f0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 媒体区域 16:9 */}
|
|
||||||
<div style={{ position: 'relative', aspectRatio: '16/9', }}>
|
<div style={{ position: 'relative', aspectRatio: '16/9', }}>
|
||||||
{asset.mediaType === 'video' ? (
|
{asset.mediaType === 'video' ? (
|
||||||
<>
|
<>
|
||||||
@@ -882,7 +834,6 @@ const HomePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 底部信息栏 */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -901,8 +852,9 @@ const HomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ========== 预览弹窗 ========== */}
|
{/* ========== 预览弹窗 ========== */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -449,7 +449,10 @@ const GenerateConver: React.FC = () => {
|
|||||||
border: '1px solid rgba(99, 102, 241, 0.08)',
|
border: '1px solid rgba(99, 102, 241, 0.08)',
|
||||||
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
|
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 ,
|
||||||
|
paddingBottom: 12,
|
||||||
|
|
||||||
|
}}>
|
||||||
{/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} /> */}
|
{/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} /> */}
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{
|
<h2 style={{
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ function RemoveInfo() {
|
|||||||
<video
|
<video
|
||||||
controls
|
controls
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%'}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user