diff --git a/video-gen-app/debug-ratio-options-not-showing.md b/video-gen-app/debug-ratio-options-not-showing.md index 71af833a..e69de29b 100644 --- a/video-gen-app/debug-ratio-options-not-showing.md +++ b/video-gen-app/debug-ratio-options-not-showing.md @@ -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` diff --git a/video-gen-app/src/components/Layout/AppLayout.tsx b/video-gen-app/src/components/Layout/AppLayout.tsx index d5862b73..57a43c93 100644 --- a/video-gen-app/src/components/Layout/AppLayout.tsx +++ b/video-gen-app/src/components/Layout/AppLayout.tsx @@ -982,7 +982,7 @@ const AppLayout: React.FC = () => { placement="left" onClose={() => setMobileMenuOpen(false)} open={mobileMenuOpen} - width={280} + size={280} closable={true} className="mobile-menu-drawer" styles={{ diff --git a/video-gen-app/src/components/UploadSelector.tsx b/video-gen-app/src/components/UploadSelector.tsx new file mode 100644 index 00000000..40a91a66 --- /dev/null +++ b/video-gen-app/src/components/UploadSelector.tsx @@ -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 = ({ + children, + accept = 'image/*,video/*', + onLocalSelect, + onHistorySelect, + onPortraitSelect, + uploading, + tooltipTitle, +}) => { + const fileInputRef = useRef(null); + + const handleLocalSelect = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + 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([]); + const [selectedPortraitItems, setSelectedPortraitItems] = useState([]); + const [historyActiveTab, setHistoryActiveTab] = useState<'asset' | 'history'>('asset'); + const [portraitExpandedGroups, setPortraitExpandedGroups] = useState([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: , + description: '从历史上传记录中选择', + onClick: () => { + setModalVisible(false); + setHistoryModalVisible(true); + }, + }, + { + key: 'portrait', + label: '人像', + icon: , + description: '从人像库中选择', + onClick: () => { + setModalVisible(false); + setPortraitModalVisible(true); + }, + }, + { + key: 'local', + label: '本地选取', + icon: , + 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 ( + <> + + {tooltipTitle ? ( + +
+ {children} +
+
+ ) : ( +
+ {children} +
+ )} + + setModalVisible(false)} + footer={null} + width={400} + centered + destroyOnHidden + > +
+ {options.map((option) => ( +
{ + 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'; + }} + > +
+ {option.icon} +
+
+
+ {option.label} +
+
+ {option.description} +
+
+ +
+ ))} +
+
+ + { + setHistoryModalVisible(false); + setSelectedHistoryItems([]); + }} + footer={null} + width={"80%"} + height={"50%"} + centered + > +
+
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', + }} + > + 资产图片 +
+
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', + }} + > + 历史图片 +
+
+ +
+
+ 暂无资产图片 +
+
+ +
+
+ 已选择 {selectedHistoryItems.length} 个素材 +
+
+ + +
+
+
+ + { + setPortraitModalVisible(false); + setSelectedPortraitItems([]); + }} + footer={null} + width={"80%"} + centered + styles={{ + body: { padding: 0, position: "relative", overflow: 'auto' }, + }} + > +
+ + {portraitGroups.map((group) => ( +
+
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'; + }} + > + + {group.name} + +
+ {portraitExpandedGroups.includes(group.id) ? '收起' : '展开'} + +
+
+ {portraitExpandedGroups.includes(group.id) && ( +
+
+ {group.items.map((item) => ( +
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', + }} + > + {item.name} + {selectedPortraitItems.includes(item.id) && ( +
+ +
+ )} +
+ ))} +
+
+ )} +
+ ))} +
+
+
+
+ 已选择 {selectedPortraitItems.length} 个素材 +
+
+ + +
+
+
+ + + +
+ + ); +}; + +export default UploadSelector; \ No newline at end of file diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 1739cc5a..80fb7859 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -22,6 +22,8 @@ import bg2 from '../assets/bg2.png'; import bg3 from '../assets/bg3.png'; import text from '../assets/testb.png'; +import UploadSelector from '../components/UploadSelector'; + import { @@ -1249,6 +1251,8 @@ const AIChatPage: React.FC = () => { return false; } } + + if (isAudio) { @@ -1287,7 +1291,6 @@ const AIChatPage: React.FC = () => { }]; const labels = generateMediaLabels(newList); setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] }))); - message.success(`${isImage ? '图片' : (isAudio ? '音频' : '视频')}上传成功`); } catch (error) { message.error('上传失败'); } finally { @@ -1297,6 +1300,118 @@ const AIChatPage: React.FC = () => { return false; }; + const doUpload = async (file: File): Promise => { + 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 newList = currentMedia.filter((_, i) => i !== index); @@ -1305,12 +1420,12 @@ const AIChatPage: React.FC = () => { }; - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }; + // const handleKeyPress = (e: React.KeyboardEvent) => { + // if (e.key === 'Enter' && !e.shiftKey) { + // e.preventDefault(); + // handleSend(); + // } + // }; // 检测光标前的 @ 符号 const checkMention = (textarea: HTMLTextAreaElement, value: string) => { @@ -1470,7 +1585,7 @@ const AIChatPage: React.FC = () => { {/* 隐藏的音频播放器 */}