diff --git a/video-gen-app/src/components/UploadSelector.tsx b/video-gen-app/src/components/UploadSelector.tsx index 40a91a66..98960dcb 100644 --- a/video-gen-app/src/components/UploadSelector.tsx +++ b/video-gen-app/src/components/UploadSelector.tsx @@ -1,6 +1,10 @@ -import React, { useRef, useState } from 'react'; -import { Modal, Tooltip } from 'antd'; -import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, LoadingOutlined, CheckOutlined, DownOutlined } from '@ant-design/icons'; +import React, { useRef, useState, useEffect } from 'react'; +import { Modal, Tooltip, Empty, Input, List, Spin, Tag, Typography, message } from 'antd'; +import { HistoryOutlined, UserOutlined, FolderOpenOutlined, PlusOutlined, CheckOutlined, ReloadOutlined, SearchOutlined, PictureOutlined } from '@ant-design/icons'; +import { getPrivatePortraitProjects, getPrivatePortraitSelectableAssets } from '../api'; +import type { PrivatePortraitProject, PrivatePortraitSelectableAsset } from '../types'; + +const { Text } = Typography; interface UploadSelectorProps { children: React.ReactNode; @@ -48,21 +52,76 @@ const UploadSelector: React.FC = ({ }; 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 [selectedPortraitItems, setSelectedPortraitItems] = useState>(new Map()); const [historyActiveTab, setHistoryActiveTab] = useState<'asset' | 'history'>('asset'); - const [portraitExpandedGroups, setPortraitExpandedGroups] = useState([1]); + + const [portraitProjects, setPortraitProjects] = useState([]); + const [portraitProjectId, setPortraitProjectId] = useState(); + const [portraitKeyword, setPortraitKeyword] = useState(''); + const [portraitAssets, setPortraitAssets] = useState([]); + const [loadingPortraitProjects, setLoadingPortraitProjects] = useState(false); + const [loadingPortraitAssets, setLoadingPortraitAssets] = useState(false); + + const getPreviewUrl = (url?: string | null) => { + if (!url) return ''; + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:') || url.startsWith('blob:')) { + return url; + } + const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, ''); + return `${base}${url.startsWith('/') ? '' : '/'}${url}`; + }; + + const loadPortraitProjects = async () => { + setLoadingPortraitProjects(true); + try { + const res = await getPrivatePortraitProjects({ page: 1, pageSize: 100, status: 'active' }); + setPortraitProjects(res.items || []); + if (!portraitProjectId && res.items?.length) { + setPortraitProjectId(res.items[0].id); + } + } catch (err: any) { + message.error(err?.message || '加载真人素材项目失败'); + } finally { + setLoadingPortraitProjects(false); + } + }; + + const loadPortraitAssets = async () => { + setLoadingPortraitAssets(true); + try { + const res = await getPrivatePortraitSelectableAssets({ projectId: portraitProjectId, keyword: portraitKeyword.trim() || undefined, page: 1, pageSize: 100 }); + setPortraitAssets(res.items || []); + } catch (err: any) { + message.error(err?.message || '加载真人素材失败'); + } finally { + setLoadingPortraitAssets(false); + } + }; + + useEffect(() => { + if (!portraitModalVisible) return; + setSelectedPortraitItems(new Map()); + loadPortraitProjects(); + }, [portraitModalVisible]); + + useEffect(() => { + if (!portraitModalVisible) return; + loadPortraitAssets(); + }, [portraitModalVisible, portraitProjectId]); + + const togglePortraitAsset = (asset: PrivatePortraitSelectableAsset) => { + setSelectedPortraitItems((prev) => { + const next = new Map(prev); + if (next.has(asset.id)) { + next.delete(asset.id); + } else { + next.set(asset.id, asset); + } + return next; + }); + }; const toggleHistoryItem = (id: number) => { setSelectedHistoryItems(prev => @@ -70,18 +129,6 @@ const UploadSelector: React.FC = ({ ); }; - 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); @@ -91,14 +138,18 @@ const UploadSelector: React.FC = ({ }; const confirmPortraitSelection = () => { - const items = mockPortraitData.filter(item => selectedPortraitItems.includes(item.id)); - const transformedItems = items.map(item => ({ + const selected = Array.from(selectedPortraitItems.values()); + if (!selected.length) { + message.warning('请选择至少一个真人素材'); + return; + } + const transformedItems = selected.map(item => ({ ...item, - avatar: item.url, + avatar: getPreviewUrl(item.previewUrl), })); onPortraitSelect?.(transformedItems); setPortraitModalVisible(false); - setSelectedPortraitItems([]); + setSelectedPortraitItems(new Map()); setModalVisible(false); }; @@ -135,12 +186,6 @@ const UploadSelector: React.FC = ({ }, ]; - 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 ( <> = ({ { setPortraitModalVisible(false); - setSelectedPortraitItems([]); + setSelectedPortraitItems(new Map()); }} - footer={null} - width={"80%"} + width={920} centered - styles={{ - body: { padding: 0, position: "relative", overflow: 'auto' }, - }} + footer={[ + , + , + ]} > -
- - {portraitGroups.map((group) => ( -
-
togglePortraitGroup(group.id)} +
+
+
+ 项目组 + +
+ + }} + renderItem={(item) => ( + setPortraitProjectId(item.id)} style={{ - fontSize: 12, - transform: portraitExpandedGroups.includes(group.id) ? 'rotate(180deg)' : 'rotate(0deg)', - transition: 'transform 0.2s ease', + cursor: 'pointer', + padding: '10px 12px', + borderRadius: 10, + marginBottom: 6, + border: portraitProjectId === item.id ? '1px solid #8b5cf6' : '1px solid transparent', + background: portraitProjectId === item.id ? '#f5f3ff' : '#fff', }} - /> -
-
- {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} 个素材 -
-
+ > +
+ {item.name} + Active {item.activeAssetCount || 0} +
+ + )} + /> + +
+ +
+
+ } + placeholder="搜索素材名称" + value={portraitKeyword} + onChange={(e) => setPortraitKeyword(e.target.value)} + onPressEnter={loadPortraitAssets} + style={{ flex: 1 }} + /> -
+ + {portraitAssets.length === 0 ? ( + + ) : ( +
+ {portraitAssets.map((asset) => { + const active = selectedPortraitItems.has(asset.id); + return ( +
togglePortraitAsset(asset)} + style={{ + cursor: 'pointer', + border: active ? '2px solid #8b5cf6' : '1px solid #edf0f5', + borderRadius: 12, + overflow: 'hidden', + background: '#fff', + boxShadow: active ? '0 8px 20px rgba(139,92,246,0.18)' : '0 4px 12px rgba(15,23,42,0.04)', + position: 'relative', + }} + > +
+ {asset.previewUrl ? ( + {asset.name + ) : ( + + )} +
+ {active && ( +
+ +
+ )} +
+ {asset.name || '未命名素材'} +
+ Active + {asset.projectName} +
+
+
+ ); + })} +
+ )} +
- - - ); diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 90e7542b..8dc9b98e 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -2520,21 +2520,8 @@ const AIChatPage: React.FC = () => { 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); - } + onPortraitSelect={(items) => { + handlePrivatePortraitAssetsSelected(items as any); }} uploading={uploading} tooltipTitle={mediaType === 'image' @@ -2588,7 +2575,7 @@ const AIChatPage: React.FC = () => {
)} - {mediaType === 'video' && currentMedia.length === 0 && ( + {/* {mediaType === 'video' && currentMedia.length === 0 && ( - )} + )} */} {/* 层叠附件展示 - 鼠标移入向右排列展开 */} {currentMedia.length > 0 && ( @@ -2713,21 +2700,8 @@ const AIChatPage: React.FC = () => { 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); - } + onPortraitSelect={(items) => { + handlePrivatePortraitAssetsSelected(items as any); }} uploading={uploading} tooltipTitle={mediaType === 'image' @@ -2768,7 +2742,7 @@ const AIChatPage: React.FC = () => {
)} - {mediaType === 'video' && currentMedia.length > 0 && ( + {/* {mediaType === 'video' && currentMedia.length > 0 && (
{ e.stopPropagation(); setPrivateAssetPickerOpen(true); }} @@ -2795,7 +2769,7 @@ const AIChatPage: React.FC = () => { 真
- )} + )} */} )} diff --git a/video-gen-app/src/pages/GeneratePage.tsx b/video-gen-app/src/pages/GeneratePage.tsx index eaae235f..30aac7ed 100644 --- a/video-gen-app/src/pages/GeneratePage.tsx +++ b/video-gen-app/src/pages/GeneratePage.tsx @@ -1838,17 +1838,18 @@ const GeneratePage: React.FC = () => { }); 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}下载失败`); - } - } + onPortraitSelect={(items) => { + items.forEach((item: any) => { + setReferences(prev => [...prev, { + url: item.previewUrl || '', + type: 'image', + name: item.name || '真人素材', + source: 'private_portrait_asset', + private_asset_id: item.id, + label: '', + }]); + }); + message.success(`已添加 ${items.length} 个真人素材参考`); }} uploading={uploading} tooltipTitle={`参考内容(${references.length}/10)`} diff --git a/video-gen-app/src/pages/LoginPage.css b/video-gen-app/src/pages/LoginPage.css index 7a94967a..e06cbddc 100644 --- a/video-gen-app/src/pages/LoginPage.css +++ b/video-gen-app/src/pages/LoginPage.css @@ -295,9 +295,8 @@ .login-code-btn { height: 48px !important; - border-radius: 0 10px 10px 0 !important; + border-radius: 10 !important; border: 1.5px solid #e2e8f0 !important; - border-left: none !important; font-weight: 600 !important; min-width: 100px !important; }