Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
+46
-46
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-CM_dxMTI.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-epOb7vts.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -682,6 +682,11 @@ export async function deleteSegment(segmentId: string): Promise<void> {
|
||||
export async function reanalyzeSegment(segmentId: string): Promise<any> {
|
||||
return api.post(`/shot-replications/segments/${segmentId}/reanalyze`);
|
||||
}
|
||||
|
||||
export async function retrySplit(segmentId: string): Promise<any> {
|
||||
return api.post(`/shot-replications/segments/${segmentId}/retry-split`);
|
||||
}
|
||||
|
||||
// 删除拆镜项目
|
||||
export async function deleteShotReplicationProject(taskSetId: string): Promise<void> {
|
||||
await api.delete(`/shot-replications/task-sets/${taskSetId}`);
|
||||
|
||||
@@ -26,6 +26,10 @@ interface UploadSelectorProps {
|
||||
usedVideoCount?: number;
|
||||
usedVideoDuration?: number;
|
||||
maxVideoDuration?: number;
|
||||
maxAudioCount?: number;
|
||||
usedAudioCount?: number;
|
||||
maxAudioDuration?: number;
|
||||
usedAudioDuration?: number;
|
||||
}
|
||||
|
||||
const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
@@ -44,12 +48,17 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
usedVideoCount,
|
||||
usedVideoDuration,
|
||||
maxVideoDuration,
|
||||
maxAudioCount,
|
||||
usedAudioCount,
|
||||
maxAudioDuration,
|
||||
usedAudioDuration,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
|
||||
const [portraitLibraryType, setPortraitLibraryType] = useState<PrivatePortraitLibraryType>('real_person');
|
||||
const [historyModalVisible, setHistoryModalVisible] = useState(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const handleLocalSelect = () => {
|
||||
setPopoverOpen(false);
|
||||
@@ -224,6 +233,16 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
setHistoryModalVisible(false);
|
||||
}}
|
||||
allowedTypes={accept.includes('audio') ? ['image', 'video', 'audio'] : accept.includes('video') ? ['image', 'video'] : ['image']}
|
||||
maxImageCount={maxImageCount}
|
||||
maxVideoCount={maxVideoCount}
|
||||
usedImageCount={usedImageCount}
|
||||
usedVideoCount={usedVideoCount}
|
||||
maxVideoDuration={maxVideoDuration}
|
||||
usedVideoDuration={usedVideoDuration}
|
||||
maxAudioCount={maxAudioCount}
|
||||
usedAudioCount={usedAudioCount}
|
||||
maxAudioDuration={maxAudioDuration}
|
||||
usedAudioDuration={usedAudioDuration}
|
||||
/>
|
||||
|
||||
<PrivatePortraitAssetPicker
|
||||
|
||||
@@ -84,7 +84,7 @@ const PrivatePortraitLibraryPanel: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<div>
|
||||
{/* <div style={{ marginBottom: 16 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>私域素材库</Title>
|
||||
<Text type="secondary">统一管理真人素材和虚拟素材。真人素材需先完成人脸认证后才可上传素材,虚拟素材如需使用需先上传。</Text>
|
||||
|
||||
@@ -117,15 +117,17 @@ const RealPersonLibraryPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>真人素材库</Typography.Title>
|
||||
<Typography.Text type="secondary">创建项目组时先完成真人认证,认证成功后项目组才会正式创建并可上传素材。</Typography.Text>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>创建项目组</Button>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} lg={7}>
|
||||
<Col xs={24} lg={5}>
|
||||
<Card
|
||||
// title="真人素材项目组"
|
||||
extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>创建项目组</Button>}
|
||||
style={{ borderRadius: 16, minHeight: 520 }}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
@@ -137,7 +139,7 @@ const RealPersonLibraryPanel: React.FC = () => {
|
||||
</Spin>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={17}>
|
||||
<Col xs={24} lg={19}>
|
||||
{selected ? (
|
||||
<PrivatePortraitProjectDetail project={selected} onDeleted={() => { setSelected(null); loadProjects(); }} onChanged={loadProjects} />
|
||||
) : (
|
||||
|
||||
@@ -387,15 +387,17 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div >
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>虚拟素材库</Typography.Title>
|
||||
<Typography.Text type="secondary">可提前上传虚拟人像素材,后续生成视频时可直接使用。</Typography.Text>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>创建项目组</Button>
|
||||
</div>
|
||||
<Row gutter={[10, 10]}>
|
||||
<Col xs={24} lg={5} >
|
||||
<Card
|
||||
// title="虚拟人像项目组"
|
||||
extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>创建项目组</Button>}
|
||||
style={{ borderRadius: 16, minHeight: 520, margin: '0 auto', maxWidth: 1200}}
|
||||
>
|
||||
<Spin spinning={projectLoading}>
|
||||
|
||||
@@ -44,6 +44,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
|
||||
const [totalDays, setTotalDays] = useState(0);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [previewItem, setPreviewItem] = useState<UploadResourceHistoryItem | null>(null);
|
||||
const [batchMode, setBatchMode] = useState(false);
|
||||
|
||||
const allItems = useMemo(() => groups.flatMap((group) => group.items || []), [groups]);
|
||||
|
||||
@@ -199,11 +200,18 @@ const UploadResourceHistoryPanel: React.FC = () => {
|
||||
load();
|
||||
}}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
||||
{/* <Button icon={<ReloadOutlined />} onClick={load}>刷新</Button> */}
|
||||
</Space>
|
||||
<Space>
|
||||
{batchMode ? (
|
||||
<>
|
||||
<Button onClick={selectAll}>{selectedIds.size && selectedIds.size === allItems.length ? '取消全选' : '全选'}</Button>
|
||||
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete}>批量删除 ({selectedIds.size})</Button>
|
||||
<Button onClick={() => { setBatchMode(false); setSelectedIds(new Set()); }}>取消批量操作</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={() => setBatchMode(true)}>批量操作</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -225,7 +233,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
|
||||
<div key={item.id} style={{ width: '17%', minWidth: 240, margin: 16, border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 16, overflow: 'hidden', background: '#fff', boxShadow: '0 10px 24px rgba(15,23,42,0.06)' }}>
|
||||
<div style={{ position: 'relative', background: '#f1f5f9' }}>
|
||||
{renderMedia(item)}
|
||||
<Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />
|
||||
{batchMode && <Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />}
|
||||
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ position: 'absolute', top: 10, right: 10, margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
|
||||
</div>
|
||||
<div style={{ padding: 12 ,paddingTop: 0}}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { AudioOutlined, CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd';
|
||||
import { getUploadResourceHistoryItems } from '../../api';
|
||||
import { getUploadResourceHistoryItems ,getUploadResourceHistory} from '../../api';
|
||||
import type { UploadResourceHistoryItem } from '../../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -15,10 +15,18 @@ interface UploadResourceHistoryPickerProps {
|
||||
allowedTypes?: Array<'image' | 'video' | 'audio'>;
|
||||
selectedIds?: string[];
|
||||
title?: string;
|
||||
maxImageCount?: number;
|
||||
maxVideoCount?: number;
|
||||
usedImageCount?: number;
|
||||
usedVideoCount?: number;
|
||||
maxVideoDuration?: number;
|
||||
usedVideoDuration?: number;
|
||||
maxAudioCount?: number;
|
||||
usedAudioCount?: number;
|
||||
maxAudioDuration?: number;
|
||||
usedAudioDuration?: number;
|
||||
}
|
||||
|
||||
const today = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const buildPreviewUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
if (/^(https?:|data:|blob:)/i.test(url)) return url;
|
||||
@@ -45,29 +53,93 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
allowedTypes,
|
||||
selectedIds = [],
|
||||
title = '选择历史上传素材',
|
||||
maxImageCount,
|
||||
maxVideoCount,
|
||||
usedImageCount,
|
||||
usedVideoCount,
|
||||
maxVideoDuration,
|
||||
usedVideoDuration,
|
||||
maxAudioCount,
|
||||
usedAudioCount,
|
||||
maxAudioDuration,
|
||||
usedAudioDuration,
|
||||
}) => {
|
||||
const [date, setDate] = useState(today());
|
||||
const [resourceType, setResourceType] = useState<MediaTypeFilter>('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [groupPage, setGroupPage] = useState(1);
|
||||
const [groupPageSize, setGroupPageSize] = useState(10);
|
||||
const [itemPage, setItemPage] = useState(1);
|
||||
const [itemPageSize, setItemPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
const [groups, setGroups] = useState<any[]>([]);
|
||||
const [totalDays, setTotalDays] = useState(0);
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
const [items, setItems] = useState<UploadResourceHistoryItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [checkedMap, setCheckedMap] = useState<Map<string, UploadResourceHistoryItem>>(new Map());
|
||||
|
||||
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
||||
const effectiveAllowedTypes = useMemo(() => new Set(allowedTypes || ['image', 'video', 'audio']), [allowedTypes]);
|
||||
const effectiveAllowedTypes = useMemo(() => new Set(allowedTypes || ['image', 'video', 'audio']), [JSON.stringify(allowedTypes)]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const selectedImages = useMemo(() => Array.from(checkedMap.values()).filter(item => item.resourceType === 'image').length, [checkedMap]);
|
||||
const selectedVideos = useMemo(() => Array.from(checkedMap.values()).filter(item => item.resourceType === 'video').length, [checkedMap]);
|
||||
const selectedVideoDuration = useMemo(() => Array.from(checkedMap.values())
|
||||
.filter(item => item.resourceType === 'video')
|
||||
.reduce((sum, item) => sum + (item.durationSeconds || 0), 0), [checkedMap]);
|
||||
const selectedAudios = useMemo(() => Array.from(checkedMap.values()).filter(item => item.resourceType === 'audio').length, [checkedMap]);
|
||||
const selectedAudioDuration = useMemo(() => Array.from(checkedMap.values())
|
||||
.filter(item => item.resourceType === 'audio')
|
||||
.reduce((sum, item) => sum + (item.durationSeconds || 0), 0), [checkedMap]);
|
||||
|
||||
const maxAvailableImages = useMemo(() => maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : Infinity, [maxImageCount, usedImageCount]);
|
||||
const maxAvailableVideos = useMemo(() => maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : Infinity, [maxVideoCount, usedVideoCount]);
|
||||
const maxAvailableDuration = useMemo(() => maxVideoDuration !== undefined && usedVideoDuration !== undefined ? maxVideoDuration - usedVideoDuration : Infinity, [maxVideoDuration, usedVideoDuration]);
|
||||
const maxAvailableAudios = useMemo(() => maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : Infinity, [maxAudioCount, usedAudioCount]);
|
||||
const maxAvailableAudioDuration = useMemo(() => maxAudioDuration !== undefined && usedAudioDuration !== undefined ? maxAudioDuration - usedAudioDuration : Infinity, [maxAudioDuration, usedAudioDuration]);
|
||||
|
||||
const imageExceeded = useMemo(() => selectedImages > maxAvailableImages, [selectedImages, maxAvailableImages]);
|
||||
const videoExceeded = useMemo(() => selectedVideos > maxAvailableVideos, [selectedVideos, maxAvailableVideos]);
|
||||
const durationExceeded = useMemo(() => selectedVideoDuration > maxAvailableDuration, [selectedVideoDuration, maxAvailableDuration]);
|
||||
const audioExceeded = useMemo(() => selectedAudios > maxAvailableAudios, [selectedAudios, maxAvailableAudios]);
|
||||
const audioDurationExceeded = useMemo(() => selectedAudioDuration > maxAvailableAudioDuration, [selectedAudioDuration, maxAvailableAudioDuration]);
|
||||
|
||||
const loadGroups = useCallback(async () => {
|
||||
if (!open) return;
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const res = await getUploadResourceHistory({
|
||||
resourceType: resourceType || undefined,
|
||||
keyword: keyword || undefined,
|
||||
page: groupPage,
|
||||
pageSize: groupPageSize,
|
||||
scene: 'picker',
|
||||
});
|
||||
if (groupPage === 1) {
|
||||
setGroups(res.groups || []);
|
||||
} else {
|
||||
setGroups((prev) => [...prev, ...(res.groups || [])]);
|
||||
}
|
||||
setTotalDays(res.totalDays || 0);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载历史上传素材失败');
|
||||
if (groupPage === 1) {
|
||||
setGroups([]);
|
||||
}
|
||||
setTotalDays(0);
|
||||
} finally {
|
||||
setLoadingGroups(false);
|
||||
}
|
||||
}, [effectiveAllowedTypes, keyword, open, groupPage, groupPageSize, resourceType]);
|
||||
|
||||
const loadItems = useCallback(async (date: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getUploadResourceHistoryItems(date, {
|
||||
resourceType: resourceType || undefined,
|
||||
keyword: keyword || undefined,
|
||||
page,
|
||||
pageSize,
|
||||
page: itemPage,
|
||||
pageSize: 8,
|
||||
scene: 'picker',
|
||||
});
|
||||
const filtered = (res.items || []).filter((item) => effectiveAllowedTypes.has(item.resourceType));
|
||||
@@ -80,11 +152,23 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [date, effectiveAllowedTypes, keyword, open, page, pageSize, resourceType]);
|
||||
}, [effectiveAllowedTypes, keyword, itemPage, itemPageSize, resourceType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) load();
|
||||
}, [open, load]);
|
||||
if (open) loadGroups();
|
||||
}, [open, groupPage, loadGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
setItemPage(1);
|
||||
}
|
||||
}, [selectedDate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
loadItems(selectedDate);
|
||||
}
|
||||
}, [selectedDate, itemPage, loadItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setCheckedMap(new Map());
|
||||
@@ -129,33 +213,25 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
title={title}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={920}
|
||||
width={1000}
|
||||
centered
|
||||
destroyOnHidden
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>取消</Button>,
|
||||
<Button key="ok" type="primary" onClick={confirm} style={{ background: '#8b5cf6' }}>
|
||||
<Button key="ok" type="primary" onClick={confirm} style={{ background: '#8b5cf6' }} disabled={imageExceeded || videoExceeded || durationExceeded || audioExceeded || audioDurationExceeded}>
|
||||
应用选择 ({checkedMap.size})
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Space wrap style={{ width: '100%', marginBottom: 16 }}>
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => {
|
||||
setDate(e.target.value || today());
|
||||
setPage(1);
|
||||
}}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={resourceType}
|
||||
options={typeOptions}
|
||||
style={{ width: 120 }}
|
||||
onChange={(value) => {
|
||||
setResourceType(value);
|
||||
setPage(1);
|
||||
setGroupPage(1);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
@@ -166,18 +242,70 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
style={{ width: 220 }}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
setPage(1);
|
||||
load();
|
||||
setGroupPage(1);
|
||||
loadGroups();
|
||||
}}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadGroups}>刷新</Button>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 12, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: imageExceeded ? '#ef4444' : '#64748b', fontWeight: imageExceeded ? 600 : 400 }}>
|
||||
还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: videoExceeded || durationExceeded ? '#ef4444' : '#64748b', fontWeight: videoExceeded || durationExceeded ? 600 : 400 }}>
|
||||
视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒)
|
||||
</span>
|
||||
{maxAudioCount !== undefined && (
|
||||
<span style={{ fontSize: 13, color: audioExceeded || audioDurationExceeded ? '#ef4444' : '#64748b', fontWeight: audioExceeded || audioDurationExceeded ? 600 : 400 }}>
|
||||
音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
{items.length === 0 ? (
|
||||
<Empty description="暂无可复用的历史上传素材" style={{ padding: '48px 0' }} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 16, height: 500 }}>
|
||||
<div style={{ border: '1px solid #eef0f4', borderRadius: 12, padding: 12, background: '#fafafa', overflowY: 'auto' }}>
|
||||
<Text strong style={{ fontSize: 14 }}>日期列表</Text>
|
||||
<Spin spinning={loadingGroups}>
|
||||
{groups.length === 0 ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无日期" style={{ padding: '40px 0' }} />
|
||||
) : (
|
||||
<div style={{ height: '500px', overflowY: 'auto' }}>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{groups.map((group) => (
|
||||
<div
|
||||
key={group.generatedDate}
|
||||
onClick={() => setSelectedDate(group.generatedDate)}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
marginBottom: 6,
|
||||
cursor: 'pointer',
|
||||
border: selectedDate === group.generatedDate ? '1px solid #8b5cf6' : '1px solid transparent',
|
||||
background: selectedDate === group.generatedDate ? '#f5f3ff' : '#fff',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#334155' }}>{group.generatedDate}</div>
|
||||
<div style={{ fontSize: 12, color: '#94a3b8', marginTop: 4 }}>共 {group.total} 个素材</div>
|
||||
</div>
|
||||
))}
|
||||
{totalDays > groups.length && (
|
||||
<div style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
<Button size="small" onClick={() => setGroupPage(groupPage + 1)}>加载更多</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<div style={{ border: '1px solid #eef0f4', borderRadius: 12, padding: 12, overflowY: 'auto' }}>
|
||||
<Spin spinning={loading}>
|
||||
{!selectedDate ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="请选择日期" style={{ padding: '80px 0' }} />
|
||||
) : items.length === 0 ? (
|
||||
<Empty description="该日期暂无可复用的历史上传素材" style={{ padding: '48px 0' }} />
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14 }}>
|
||||
{items.map((item) => {
|
||||
const checked = checkedMap.has(item.id);
|
||||
@@ -219,6 +347,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{item.moduleLabel || '普通上传'}</Text>
|
||||
</div>
|
||||
|
||||
{checked && (
|
||||
<div style={{ position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 12, background: '#8b5cf6', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CheckOutlined />
|
||||
@@ -231,20 +360,22 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16, paddingBottom: 8 }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
current={itemPage}
|
||||
pageSize={itemPageSize}
|
||||
total={total}
|
||||
showSizeChanger
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
onChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage);
|
||||
setPageSize(nextSize);
|
||||
setItemPage(nextPage);
|
||||
setItemPageSize(nextSize);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// AI 创作对话框样式优化版 v12:白色极简高级风格,选项按钮、标题、头像与关键字统一使用 #8b5cf6。
|
||||
|
||||
@@ -124,6 +125,7 @@ const mockUpload = (file: File): Promise<{ url: string }> => {
|
||||
const AIChatPage: React.FC = () => {
|
||||
// ==================== 状态定义 ====================
|
||||
const { message: antdMessage } = App.useApp();
|
||||
const location = useLocation();
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean>(false);
|
||||
|
||||
@@ -474,6 +476,32 @@ const AIChatPage: React.FC = () => {
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
// ==================== 副作用 ====================
|
||||
|
||||
useEffect(() => {
|
||||
const state = location.state as any;
|
||||
|
||||
if (state) {
|
||||
if (state.generationPrompt) {
|
||||
setInputValue(state.generationPrompt);
|
||||
}
|
||||
if (state.mediaReferences && Array.isArray(state.mediaReferences) && state.mediaReferences.length > 0) {
|
||||
const mediaItems: MediaReference[] = state.mediaReferences.map((ref: any) => {
|
||||
const url = ref.url || ref.resourceUrl || ref.previewUrl || '';
|
||||
const fullUrl = url.startsWith('http') ? url : `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${url}`;
|
||||
return {
|
||||
...ref,
|
||||
name: ref.name || `参考${ref.type}`,
|
||||
type: ref.type === 'video' ? 'video' : ref.type === 'audio' ? 'audio' : 'image',
|
||||
url: fullUrl,
|
||||
duration: ref.duration || 0,
|
||||
label: ref.label || '',
|
||||
};
|
||||
});
|
||||
setCurrentMedia(mediaItems);
|
||||
}
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
// 滚动到底部的辅助函数
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -1206,6 +1234,19 @@ const AIChatPage: React.FC = () => {
|
||||
antdMessage.error('图片大小不能超过10MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(file);
|
||||
const error = validateVideoDimensions(width, height);
|
||||
if (error) {
|
||||
antdMessage.error(error);
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
antdMessage.error('无法读取图片尺寸,请检查文件是否损坏');
|
||||
return false;
|
||||
}
|
||||
|
||||
const effectiveUploadTarget = frameTarget || uploadTarget;
|
||||
if (!effectiveUploadTarget) {
|
||||
antdMessage.error('请选择首帧或尾帧上传位置');
|
||||
@@ -1272,7 +1313,7 @@ const AIChatPage: React.FC = () => {
|
||||
if (isImage) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(file);
|
||||
const error = validateImageDimensions(width, height);
|
||||
const error = validateVideoDimensions(width, height);
|
||||
if (error) {
|
||||
antdMessage.error(error);
|
||||
return false;
|
||||
@@ -1356,8 +1397,14 @@ const AIChatPage: React.FC = () => {
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
const uploadFn = isImage ? uploadImage : (isAudio ? uploadAudio : uploadVideo);
|
||||
const res = await uploadFn(file);
|
||||
let res;
|
||||
if (isImage) {
|
||||
res = await uploadImage(file);
|
||||
} else if (isAudio) {
|
||||
res = await uploadAudio(file, audioDuration);
|
||||
} else {
|
||||
res = await uploadVideo(file, videoDuration);
|
||||
}
|
||||
const mediaType: 'image' | 'video' | 'audio' = isImage ? 'image' : (isAudio ? 'audio' : 'video');
|
||||
const newList = [...currentMedia, {
|
||||
name: file.name,
|
||||
@@ -1402,6 +1449,21 @@ const AIChatPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 图片尺寸校验
|
||||
if (isImage) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(file);
|
||||
const error = validateVideoDimensions(width, height);
|
||||
if (error) {
|
||||
antdMessage.error(error);
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
antdMessage.error('无法读取图片尺寸,请检查文件是否损坏');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let videoDuration = 0;
|
||||
let audioDuration = 0;
|
||||
if (isVideo) {
|
||||
@@ -1461,8 +1523,14 @@ const AIChatPage: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadFn = isImage ? uploadImage : (isAudio ? uploadAudio : uploadVideo);
|
||||
const res = await uploadFn(file);
|
||||
let res;
|
||||
if (isImage) {
|
||||
res = await uploadImage(file);
|
||||
} else if (isAudio) {
|
||||
res = await uploadAudio(file, audioDuration);
|
||||
} else {
|
||||
res = await uploadVideo(file, videoDuration);
|
||||
}
|
||||
return {
|
||||
name: pendingMedia.name,
|
||||
type: pendingMedia.type,
|
||||
@@ -2806,6 +2874,16 @@ const AIChatPage: React.FC = () => {
|
||||
视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}${maxAudio > 0 ? `,
|
||||
音频${currentMedia.filter(m => m.type === 'audio').length}/${maxAudio}` : ''}`
|
||||
}
|
||||
maxImageCount={maxImageCount}
|
||||
maxVideoCount={maxVideoCount}
|
||||
usedImageCount={currentMedia.filter(m => m.type === 'image').length}
|
||||
usedVideoCount={currentMedia.filter(m => m.type === 'video').length}
|
||||
usedVideoDuration={currentMedia.filter(m => m.type === 'video').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||
maxVideoDuration={15}
|
||||
maxAudioCount={maxAudio > 0 ? maxAudio : undefined}
|
||||
usedAudioCount={currentMedia.filter(m => m.type === 'audio').length}
|
||||
maxAudioDuration={15}
|
||||
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -2977,6 +3055,16 @@ const AIChatPage: React.FC = () => {
|
||||
视频${currentMedia.filter(m => m.type === 'video').length}/${maxVideoCount}${maxAudio > 0 ? `,
|
||||
音频${currentMedia.filter(m => m.type === 'audio').length}/${maxAudio}` : ''}`
|
||||
}
|
||||
maxImageCount={maxImageCount}
|
||||
maxVideoCount={maxVideoCount}
|
||||
usedImageCount={currentMedia.filter(m => m.type === 'image').length}
|
||||
usedVideoCount={currentMedia.filter(m => m.type === 'video').length}
|
||||
usedVideoDuration={currentMedia.filter(m => m.type === 'video').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||
maxVideoDuration={15}
|
||||
maxAudioCount={maxAudio > 0 ? maxAudio : undefined}
|
||||
usedAudioCount={currentMedia.filter(m => m.type === 'audio').length}
|
||||
maxAudioDuration={15}
|
||||
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -3440,8 +3528,6 @@ const AIChatPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
setShowEngineModal(false);
|
||||
setCurrentMedia([]);
|
||||
setInputValue('');
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -318,9 +318,13 @@ const GeneratePage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
setUploading(true);
|
||||
const uploadFn = isImage ? uploadImage : uploadVideo;
|
||||
try {
|
||||
const res = await uploadFn(file);
|
||||
let res;
|
||||
if (isImage) {
|
||||
res = await uploadImage(file);
|
||||
} else {
|
||||
res = await uploadVideo(file, fileDuration);
|
||||
}
|
||||
const typeLabel = isImage ? "图片" : "视频";
|
||||
const typeCount = isImage
|
||||
? imageCount + 1
|
||||
@@ -4016,19 +4020,13 @@ const GeneratePage: React.FC = () => {
|
||||
message.error("仅支持图片或视频文件");
|
||||
return false;
|
||||
}
|
||||
const uploadFn = isImage
|
||||
? uploadImage
|
||||
: uploadVideo;
|
||||
uploadFn(file)
|
||||
if (isImage) {
|
||||
uploadImage(file)
|
||||
.then((res) => {
|
||||
const newRef = {
|
||||
url: res.url,
|
||||
type: isImage
|
||||
? ("image" as const)
|
||||
: ("video" as const),
|
||||
name:
|
||||
file.name ||
|
||||
(isImage ? "图片" : "视频"),
|
||||
type: "image" as const,
|
||||
name: file.name || "图片",
|
||||
};
|
||||
const curRefs =
|
||||
record.references || [];
|
||||
@@ -4041,6 +4039,41 @@ const GeneratePage: React.FC = () => {
|
||||
message.success("上传成功");
|
||||
})
|
||||
.catch(() => message.error("上传失败"));
|
||||
} else {
|
||||
new Promise<number>((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.onloadedmetadata = () => {
|
||||
resolve(video.duration || 0);
|
||||
video.remove();
|
||||
};
|
||||
video.onerror = () => {
|
||||
resolve(0);
|
||||
video.remove();
|
||||
};
|
||||
video.src = URL.createObjectURL(file);
|
||||
}).then((fileDuration) => {
|
||||
uploadVideo(file, fileDuration)
|
||||
.then((res) => {
|
||||
const newRef = {
|
||||
url: res.url,
|
||||
type: "video" as const,
|
||||
name: file.name || "视频",
|
||||
duration: fileDuration || undefined,
|
||||
};
|
||||
const curRefs =
|
||||
record.references || [];
|
||||
useAppStore
|
||||
.getState()
|
||||
.updateRecordReferences(record.id, [
|
||||
...curRefs,
|
||||
newRef,
|
||||
]);
|
||||
message.success("上传成功");
|
||||
})
|
||||
.catch(() => message.error("上传失败"));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
UploadOutlined,
|
||||
VideoCameraOutlined,
|
||||
PictureOutlined,
|
||||
AudioOutlined,
|
||||
PauseOutlined,
|
||||
ThunderboltOutlined,
|
||||
PlayCircleOutlined,
|
||||
HeartOutlined,
|
||||
@@ -47,6 +49,9 @@ const HomePage: React.FC = () => {
|
||||
const [previewAsset, setPreviewAsset] = useState<any>(null);
|
||||
const previewVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('cases');
|
||||
const [playingVideoIndex, setPlayingVideoIndex] = useState<number | null>(null);
|
||||
const [playingAudioUrl, setPlayingAudioUrl] = useState<string | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getHomeCaseHeader().then((res: any) => {
|
||||
@@ -66,6 +71,36 @@ const HomePage: React.FC = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current = null;
|
||||
}
|
||||
setPlayingAudioUrl(null);
|
||||
setPlayingVideoIndex(null);
|
||||
const allVideos = document.querySelectorAll('.preview-video-item');
|
||||
allVideos.forEach((v) => {
|
||||
(v as HTMLVideoElement).pause();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!previewAsset) {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current = null;
|
||||
}
|
||||
setPlayingAudioUrl(null);
|
||||
setPlayingVideoIndex(null);
|
||||
const allVideos = document.querySelectorAll('.preview-video-item');
|
||||
allVideos.forEach((v) => {
|
||||
(v as HTMLVideoElement).pause();
|
||||
});
|
||||
}
|
||||
}, [previewAsset]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAll = async () => {
|
||||
try {
|
||||
@@ -972,15 +1007,94 @@ const HomePage: React.FC = () => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}>视频提示词</div>
|
||||
<div style={{ fontSize: 13, color: '#000000ff', lineHeight: 1.6 }}>
|
||||
{previewAsset?.prompt || '动态描述:女性抬手整理头发,随后手持口服液用手指向产品讲解;画面切换为双手将口服液中的棕黄色液体缓缓倒入透明玻璃杯;再次切换女性讲解画面,双手做出展示动作指向产品;最后双手在胸前做出托手姿势后摊开手掌微笑描述:女性讲解的...'}
|
||||
{previewAsset?.generationPrompt }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 视频参考图 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 8 }}>视频参考图</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div style={{ width: 80, height: 80, borderRadius: 8, overflow: 'hidden', cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{previewAsset?.mediaReferences && previewAsset.mediaReferences.length > 0 ? (
|
||||
previewAsset.mediaReferences.map((ref: any, index: number) => {
|
||||
const url = ref.url || ref.resourceUrl || ref.previewUrl || '';
|
||||
const fullUrl = url.startsWith('http') ? url : `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${url}`;
|
||||
|
||||
if (ref.type === 'video') {
|
||||
return (
|
||||
<div key={index} style={{ width: 80, height: 80, borderRadius: 8, overflow: 'hidden', cursor: 'pointer', position: 'relative' }}>
|
||||
<video
|
||||
src={fullUrl}
|
||||
muted
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const video = e.currentTarget as HTMLVideoElement;
|
||||
if (playingVideoIndex === index) {
|
||||
video.pause();
|
||||
setPlayingVideoIndex(null);
|
||||
} else {
|
||||
const allVideos = document.querySelectorAll('.preview-video-item');
|
||||
allVideos.forEach((v) => {
|
||||
(v as HTMLVideoElement).pause();
|
||||
});
|
||||
video.play();
|
||||
setPlayingVideoIndex(index);
|
||||
}
|
||||
}}
|
||||
className="preview-video-item"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
{playingVideoIndex !== index && (
|
||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.3)' }}>
|
||||
<VideoCameraOutlined style={{ fontSize: 18, color: '#fff' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (ref.type === 'audio') {
|
||||
return (
|
||||
<div key={index} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (playingAudioUrl === fullUrl) {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
}
|
||||
setPlayingAudioUrl(null);
|
||||
} else {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
}
|
||||
const audio = new Audio(fullUrl);
|
||||
audioRef.current = audio;
|
||||
audio.onended = () => {
|
||||
setPlayingAudioUrl(null);
|
||||
};
|
||||
audio.play();
|
||||
setPlayingAudioUrl(fullUrl);
|
||||
}
|
||||
}} style={{ width: 80, height: 80, borderRadius: 8, overflow: 'hidden', cursor: 'pointer', background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{playingAudioUrl === fullUrl ? (
|
||||
<PauseOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
) : (
|
||||
<AudioOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={index} style={{ width: 80, height: 80, borderRadius: 8, overflow: 'hidden', cursor: 'pointer' }}>
|
||||
<img
|
||||
src={fullUrl}
|
||||
alt={`参考图 ${index + 1}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
{/* <div style={{ width: 80, height: 80, borderRadius: 8, overflow: 'hidden', cursor: 'pointer' }}>
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset?.url}`}
|
||||
alt="参考图"
|
||||
@@ -999,17 +1113,19 @@ const HomePage: React.FC = () => {
|
||||
fontSize: 12,
|
||||
}}>
|
||||
展开查看
|
||||
</div>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 口播脚本台词 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{/* <div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}>口播脚本台词</div>
|
||||
<div style={{ fontSize: 13, color: '#000000ff', lineHeight: 1.6 }}>
|
||||
{previewAsset?.script || '还在为宝宝不爱喝水发愁?试试这款天然果蔬汁!零添加糖分,维生素满满,口感清甜宝宝超爱喝。现在下单还送专属吸管杯,手慢无!点下方链接把健康带回家~'}
|
||||
</div>
|
||||
{/* <div style={{
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
color: '#6366f1',
|
||||
marginTop: 8,
|
||||
@@ -1019,8 +1135,8 @@ const HomePage: React.FC = () => {
|
||||
gap: 4,
|
||||
}}>
|
||||
完整内容女声
|
||||
</div> */}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 视频标签 */}
|
||||
{/* <div style={{ marginBottom: 16 }}>
|
||||
@@ -1118,9 +1234,16 @@ const HomePage: React.FC = () => {
|
||||
</div> */}
|
||||
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{previewAsset?.mediaType !== 'video' && (
|
||||
{/* {previewAsset?.mediaType !== 'video' && ( */}
|
||||
<Button
|
||||
// onClick={() => navigate('/generate')}
|
||||
onClick={() => {
|
||||
navigate('/conversation', {
|
||||
state: {
|
||||
generationPrompt: previewAsset?.generationPrompt,
|
||||
mediaReferences: previewAsset?.mediaReferences,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 24px',
|
||||
borderRadius: 8,
|
||||
@@ -1133,7 +1256,7 @@ const HomePage: React.FC = () => {
|
||||
>
|
||||
去AI创作
|
||||
</Button>
|
||||
)}
|
||||
{/* )}
|
||||
<Button
|
||||
// onClick={() => navigate('/initial')}
|
||||
style={{
|
||||
@@ -1147,7 +1270,7 @@ const HomePage: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
去爆款复刻
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 {uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotReplicationDetail, reanalyzeSegment, Removelist, removeCreate, reanalyzeShotReplication, splitCustom, uploadImage } from '../api';
|
||||
import {uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotReplicationDetail, reanalyzeSegment, retrySplit, Removelist, removeCreate, reanalyzeShotReplication, splitCustom, uploadImage } from '../api';
|
||||
|
||||
import VideoTrimPicker from '../components/VideoTrimPicker';
|
||||
|
||||
@@ -169,6 +169,16 @@ function RemoveInfo() {
|
||||
}
|
||||
}, [fetchSegments]);
|
||||
|
||||
const handleRetrySplit = useCallback(async (segmentId: string) => {
|
||||
try {
|
||||
await retrySplit(segmentId);
|
||||
message.success('重新切割已提交');
|
||||
fetchSegments();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新切割失败');
|
||||
}
|
||||
}, [fetchSegments]);
|
||||
|
||||
const handleDeleteSegment = useCallback(async (segmentId: string) => {
|
||||
try {
|
||||
await deleteSegment(segmentId);
|
||||
@@ -589,11 +599,11 @@ function RemoveInfo() {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
align: 'center' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{record.moduleProjectId ? (
|
||||
<Button
|
||||
type="text"
|
||||
@@ -616,11 +626,20 @@ function RemoveInfo() {
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => handleSegmentReanalyze(String(record.id))}
|
||||
style={{ color: '#ef4444', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
style={{ color: '#f89411ff', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
重新分析
|
||||
</Button>
|
||||
)}
|
||||
{record.splitStatus === 'failed' && (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => handleRetrySplit(String(record.id))}
|
||||
style={{ color: '#f89411ff', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
重新切割
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定删除此片段?"
|
||||
onConfirm={() => handleDeleteSegment(String(record.id))}
|
||||
|
||||
Reference in New Issue
Block a user