上传素材管控-历史素材success

This commit is contained in:
2026-07-08 16:25:03 +08:00
parent 657505e63b
commit d085a6d2ec
47 changed files with 5424 additions and 447 deletions
+47 -21
View File
@@ -33,7 +33,7 @@ import {
import { useAppStore } from '../store/useAppStore';
import { PrivatePortraitAssetPicker } from '../components/privatePortrait';
import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset } from '../types';
import type { PrivatePortraitLibraryType, PrivatePortraitSelectableAsset, UploadResourceHistoryItem } from '../types';
import {
PlusOutlined,
@@ -78,8 +78,10 @@ interface MediaReference {
source?: string;
private_asset_id?: string;
remote_asset_id?: string;
upload_resource_id?: string;
}
interface Message {
id: string;
original_prompt: string;
@@ -1503,6 +1505,48 @@ const AIChatPage: React.FC = () => {
return true;
};
const normalizeUploadResourceHistoryItem = (item: UploadResourceHistoryItem): MediaReference | null => {
const media = item.mediaReference;
const refType = (media?.type || item.resourceType) as 'image' | 'video' | 'audio';
const url = media?.url || item.resourceUrl || item.displayUrl || item.previewUrl || '';
if (!url) {
message.error(`${item.fileName || item.id} 缺少素材地址,不能用于 AI 创作`);
return null;
}
if (refType === 'audio' && mediaType !== 'video') {
message.error('仅视频模式支持添加音频素材');
return null;
}
const duration = Number(media?.duration ?? item.durationSeconds);
return {
name: media?.name || item.fileName || item.id,
type: refType,
url,
source: 'upload_resource',
upload_resource_id: item.id,
label: '',
...((refType === 'video' || refType === 'audio') && Number.isFinite(duration) && duration > 0 ? { duration } : {}),
};
};
const handleUploadResourceHistorySelected = (items: UploadResourceHistoryItem[]) => {
const normalized = items
.map(normalizeUploadResourceHistoryItem)
.filter(Boolean) as MediaReference[];
if (!normalized.length) return;
const latestMedia = useAppStore.getState().currentMedia as MediaReference[];
if (!validateMediaReferencesBeforeAdd(latestMedia, normalized)) {
return;
}
const newList = [...latestMedia, ...normalized];
const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
message.success(`已添加 ${normalized.length} 个历史上传素材参考`);
};
const normalizePrivatePortraitAsset = (asset: PrivatePortraitSelectableAsset): MediaReference | null => {
if (asset.assetType === 'Audio') {
message.error('音频私域素材暂不支持用于 AI 创作');
@@ -2612,16 +2656,7 @@ const AIChatPage: React.FC = () => {
<UploadSelector
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
onLocalSelect={handleBatchUpload}
onHistorySelect={(items) => {
const newMedia = items.map((item: any) => ({
name: item.name,
type: item.type as 'image' | 'video' | 'audio',
url: '',
label: '',
}));
setCurrentMedia([...currentMedia, ...newMedia]);
message.success(`成功添加${items.length}个历史记录`);
}}
onHistorySelect={handleUploadResourceHistorySelected}
onPortraitLibrarySelect={(libraryType) => {
openPrivatePortraitPicker(libraryType);
}}
@@ -2792,16 +2827,7 @@ const AIChatPage: React.FC = () => {
<UploadSelector
accept={mediaType === 'image' ? 'image/*' : 'image/*,video/*,audio/*'}
onLocalSelect={handleBatchUpload}
onHistorySelect={(items) => {
const newMedia = items.map((item: any) => ({
name: item.name,
type: item.type as 'image' | 'video' | 'audio',
url: '',
label: '',
}));
setCurrentMedia([...currentMedia, ...newMedia]);
message.success(`成功添加${items.length}个历史记录`);
}}
onHistorySelect={handleUploadResourceHistorySelected}
onPortraitLibrarySelect={(libraryType) => {
openPrivatePortraitPicker(libraryType);
}}
+27 -4
View File
@@ -20,6 +20,7 @@ import {
} from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom';
import { PrivatePortraitLibraryPanel } from '../components/privatePortrait';
import { UploadResourceHistoryPanel } from '../components/uploadResource';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest, deleteHistory, deleteResourcesMaterial } from '../api';
const { Search } = Input;
@@ -52,8 +53,8 @@ const GeneratedRecord: React.FC = () => {
});
const [searchParams] = useSearchParams();
const initialFilterType = searchParams.get('filterType') === 'private_portrait' ? 'private_portrait' : 'project';
const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'private_portrait'>(initialFilterType);
const initialFilterType = searchParams.get('filterType') === 'private_portrait' ? 'private_portrait' : searchParams.get('filterType') === 'upload_resource' ? 'upload_resource' : 'project';
const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'private_portrait' | 'upload_resource'>(initialFilterType);
const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video');
const [recordlist, setRecordList] = useState<any[]>([]);
const [Pagebreak, setPagebreak] = useState<any>({
@@ -759,7 +760,7 @@ const GeneratedRecord: React.FC = () => {
});
};
const loadRecordList = () => {
if (filterType === 'private_portrait') {
if (filterType === 'private_portrait' || filterType === 'upload_resource') {
setLoading(false);
setRecordList([]);
return;
@@ -1033,9 +1034,29 @@ const GeneratedRecord: React.FC = () => {
>
</Button>
<Button
type={filterType === 'upload_resource' ? 'primary' : 'default'}
onClick={() => {
setFilterType('upload_resource');
setIsSelectionMode(false);
setSelectedItems(new Set());
}}
style={{
borderRadius: 8,
background: filterType === 'upload_resource'
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: '#f8f9fc',
border: filterType === 'upload_resource' ? 'none' : '1px solid #e2e8f0',
color: filterType === 'upload_resource' ? '#fff' : '#64748b',
fontWeight: 600,
}}
icon={<UploadOutlined />}
>
</Button>
</Space>
</div>
{filterType !== 'private_portrait' && (
{filterType !== 'private_portrait' && filterType !== 'upload_resource' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
{/* 多选模式按钮 */}
{isSelectionMode ? (
@@ -1120,6 +1141,8 @@ const GeneratedRecord: React.FC = () => {
</div>
{filterType === 'private_portrait' ? (
<PrivatePortraitLibraryPanel />
) : filterType === 'upload_resource' ? (
<UploadResourceHistoryPanel />
) : (
<>
{/* Second row filter: 视频 / 图片 */}
+28 -7
View File
@@ -18,11 +18,20 @@ import {
LoadingOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { uploadVideo, uploadImage, generateReplication, getReplicationList, getone, getReplicationDetail } from '../api';
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, getone, getReplicationDetail } from '../api';
const { Header, Content } = Layout;
const { TextArea } = Input;
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const buildAssetUrl = (url?: string): string => {
if (!url) return '';
if (/^https?:\/\//i.test(url) || url.startsWith('blob:')) return url;
return `${API_BASE}${url}`;
};
const GenerateConver: React.FC = () => {
const navigate = useNavigate();
@@ -50,8 +59,11 @@ const GenerateConver: React.FC = () => {
// 文件状态
const [videoFile, setVideoFile] = useState<File | null>(null);
const [videoUrl, setVideoUrl] = useState<string>('');
const [videoResourceId, setVideoResourceId] = useState<string>('');
const [videoDurationSeconds, setVideoDurationSeconds] = useState<number | null>(null);
const [imageFile, setImageFile] = useState<File | null>(null);
const [imageUrl, setImageUrl] = useState<string>('');
const [imageResourceId, setImageResourceId] = useState<string>('');
const [videoUploading, setVideoUploading] = useState(false);
const [imageUploading, setImageUploading] = useState(false);
@@ -222,9 +234,11 @@ const GenerateConver: React.FC = () => {
// 调用上传接口
setVideoUploading(true);
try {
const res = await uploadVideo(file);
const res = await uploadHotOpeningVideo(file, video.duration);
setVideoFile(file);
setVideoUrl(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${res.url}`);
setVideoUrl(res.url);
setVideoResourceId(res.resource_id || '');
setVideoDurationSeconds(video.duration);
message.success('视频上传成功');
resolve(false);
} catch (error) {
@@ -280,9 +294,10 @@ const GenerateConver: React.FC = () => {
// 调用上传接口
setImageUploading(true);
try {
const res = await uploadImage(file);
const res = await uploadHotOpeningImage(file);
setImageFile(file);
setImageUrl(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${res.url}`);
setImageUrl(res.url);
setImageResourceId(res.resource_id || '');
message.success('图片上传成功');
resolve(false);
} catch (error) {
@@ -363,6 +378,9 @@ const GenerateConver: React.FC = () => {
let params = {
material_video_url: videoUrl,
material_image_url: imageUrl,
material_video_resource_id: videoResourceId || undefined,
material_image_resource_id: imageResourceId || undefined,
material_video_duration_seconds: videoDurationSeconds || undefined,
source_project_name: originalProductName,
target_project_name: ownProductName,
core_content_point: productSellingPoints,
@@ -380,7 +398,10 @@ const GenerateConver: React.FC = () => {
// 清空上传的媒体和文本
setVideoUrl('');
setVideoResourceId('');
setVideoDurationSeconds(null);
setImageUrl('');
setImageResourceId('');
setOriginalProductName('');
setOwnProductName('');
setProductSellingPoints('');
@@ -818,7 +839,7 @@ const GenerateConver: React.FC = () => {
{videoUrl ? (
<div style={{ position: 'relative' }}>
<video
src={videoUrl}
src={buildAssetUrl(videoUrl)}
controls
style={{ width: '100%', borderRadius: 12, maxHeight: 200, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.08)' }}
/>
@@ -934,7 +955,7 @@ const GenerateConver: React.FC = () => {
{imageUrl ? (
<div style={{ position: 'relative' }}>
<img
src={imageUrl}
src={buildAssetUrl(imageUrl)}
alt="产品图片"
style={{ width: '100%', borderRadius: 12, maxHeight: 200, objectFit: 'contain', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.08)' }}
/>
+8 -3
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button, Drawer, Input, Modal, Popconfirm, Spin, Table, Tag, Tooltip, Upload, message } from 'antd';
import { ArrowLeftOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
import { createRemoveLens, getShotReplicationDetail, Removelist, removeCreate, splitCustom, uploadImage } from '../api';
import { createRemoveLens, getShotReplicationDetail, Removelist, removeCreate, splitCustom, uploadShotReplicateImage } from '../api';
import VideoTrimPicker from '../components/VideoTrimPicker';
const { TextArea } = Input;
@@ -30,6 +30,7 @@ function RemoveInfo() {
const [productName, setProductName] = useState('');
const [productSellingPoint, setProductSellingPoint] = useState('');
const [productImage, setProductImage] = useState('');
const [productImageResourceId, setProductImageResourceId] = useState('');
const [taskDetail, setTaskDetail] = useState<any>(null);
const [tableData, setTableData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
@@ -190,18 +191,21 @@ function RemoveInfo() {
setCurrentSegmentName('');
setProductSellingPoint('');
setProductImage('');
setProductImageResourceId('');
};
const handleProductImageChange: any = (info: any) => {
if (info.fileList.length === 0) {
setProductImage('');
setProductImageResourceId('');
}
};
const beforeUploadProductImage = async (file: File) => {
try {
const uploadResult = await uploadImage(file);
const uploadResult = await uploadShotReplicateImage(file);
setProductImage(uploadResult.url);
setProductImageResourceId(uploadResult.resource_id || '');
message.success('图片上传成功');
} catch {
message.error('图片上传失败,请重试');
@@ -231,7 +235,8 @@ function RemoveInfo() {
const params = {
target_project_name: productName.trim(),
core_content_point: productSellingPoint.trim(),
material_image_url: buildAssetUrl(productImage),
material_image_url: productImage,
material_image_resource_id: productImageResourceId || undefined,
idempotency_key: `replication_${Date.now()}`,
};
+6 -2
View File
@@ -2,13 +2,14 @@ import { useState, useRef, useCallback } from 'react';
import { Button, Modal, Input, Table, Upload, Popconfirm, message } from 'antd';
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { uploadVideo, createShotReplication, getShotReplicationList } from '../api';
import { uploadShotReplicateVideo, createShotReplication, getShotReplicationList } from '../api';
import bg1 from '../assets/bg1.png';
export default function VideoFrameExtractor() {
const navigate = useNavigate();
const [videoUrl, setVideoUrl] = useState<string>('');
const [videoResourceId, setVideoResourceId] = useState<string>('');
const [error, setError] = useState<string>('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [productName, setProductName] = useState<string>('');
@@ -28,6 +29,7 @@ export default function VideoFrameExtractor() {
URL.revokeObjectURL(videoUrl);
}
setVideoUrl('');
setVideoResourceId('');
setError('');
setVideoDuration(0);
setProductName('');
@@ -66,8 +68,9 @@ export default function VideoFrameExtractor() {
setLoading(true);
try {
const uploadResult = await uploadVideo(file);
const uploadResult = await uploadShotReplicateVideo(file, duration);
setVideoUrl(uploadResult.url);
setVideoResourceId(uploadResult.resource_id || '');
setError('');
message.success('视频上传成功');
} catch (err) {
@@ -97,6 +100,7 @@ export default function VideoFrameExtractor() {
const params = {
video_url: videoUrl,
video_duration_seconds: videoDuration,
video_resource_id: videoResourceId || undefined,
title: productName.trim(),
idempotency_key: `shot_${Date.now()}`,
};