merge main

This commit is contained in:
2026-07-02 15:20:58 +08:00
16 changed files with 864 additions and 1200 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+36 -36
View File
@@ -1,37 +1,37 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>后台管理</title> <title>后台管理</title>
<script> <script>
(function() { (function() {
var cached = localStorage.getItem('siteInfo'); var cached = localStorage.getItem('siteInfo');
if (cached) { if (cached) {
try { try {
var info = JSON.parse(cached); var info = JSON.parse(cached);
if (info.siteName) { if (info.siteName) {
document.title = info.siteName + ' - 管理后台'; document.title = info.siteName + ' - 管理后台';
} }
if (info.siteLogo) { if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]'); var link = document.querySelector('link[rel="icon"]');
if (link) { if (link) {
link.href = info.siteLogo; link.href = info.siteLogo;
link.type = 'image/png'; link.type = 'image/png';
} }
} }
} catch (e) {} } catch (e) {}
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-DfKYC1kL.js"></script> <script type="module" crossorigin src="/assets/index-BE_fa7kc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>
+3 -3
View File
@@ -200,7 +200,7 @@ const AdminPlatform: React.FC = () => {
if (!v) return '-'; if (!v) return '-';
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const fullUrl = v.startsWith('http') ? v : `${baseUrl}${v}`; const fullUrl = v.startsWith('http') ? v : `${baseUrl}${v}`;
return <img src={fullUrl} alt="thumb" style={{ width: 80, height: 60, objectFit: 'cover' }} />; return <img src={fullUrl} alt="thumb" style={{ width: '100%', height: 48, display: 'block' }} />;
}, },
}, },
{ {
@@ -382,7 +382,7 @@ const AdminPlatform: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8fafc', borderRadius: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8fafc', borderRadius: 8 }}>
<div style={{ <div style={{
width: 80, height: 60, borderRadius: 6, overflow: 'hidden', width: 80, height: 48, borderRadius: 6, overflow: 'hidden',
background: currentOpenType.thumb ? 'transparent' : '#e2e8f0', background: currentOpenType.thumb ? 'transparent' : '#e2e8f0',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}> }}>
@@ -390,7 +390,7 @@ const AdminPlatform: React.FC = () => {
<img <img
src={currentOpenType.thumb.startsWith('http') ? currentOpenType.thumb : `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${currentOpenType.thumb}`} src={currentOpenType.thumb.startsWith('http') ? currentOpenType.thumb : `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${currentOpenType.thumb}`}
alt="thumb" alt="thumb"
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', display: 'block' }}
/> />
) : ( ) : (
<Typography.Text type="secondary" style={{ fontSize: 12 }}></Typography.Text> <Typography.Text type="secondary" style={{ fontSize: 12 }}></Typography.Text>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-Cr8hE_b3.js"></script> <script type="module" crossorigin src="/assets/index-DYahV0xa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bi8mcSs8.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bi8mcSs8.css">
</head> </head>
<body> <body>
+1 -1
View File
@@ -130,5 +130,5 @@ export const api = {
post: <T>(path: string, body?: unknown, auth = true, skipAuthRedirect = false) => post: <T>(path: string, body?: unknown, auth = true, skipAuthRedirect = false) =>
apiRequest<T>(path, { method: 'POST', body, auth, skipAuthRedirect }), apiRequest<T>(path, { method: 'POST', body, auth, skipAuthRedirect }),
put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth }), put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth }),
delete: <T>(path: string, auth = true) => apiRequest<T>(path, { method: 'DELETE', auth }), delete: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'DELETE', body, auth }),
}; };
+7 -1
View File
@@ -720,4 +720,10 @@ export async function getHomeCaseHeader(): Promise<any> {
// 首页素材按钮资源 // 首页素材按钮资源
export async function getHomeCaseButton(id: string,limit:number=5): Promise<any> { export async function getHomeCaseButton(id: string,limit:number=5): Promise<any> {
return api.get(`/home-materials?category_id=${id}&limit_per_category=${limit}&include_empty_categories=false&response_mode=grouped&page=1&page_size=20`); return api.get(`/home-materials?category_id=${id}&limit_per_category=${limit}&include_empty_categories=false&response_mode=grouped&page=1&page_size=20`);
} }
export async function deleteResourcesMaterial(params:any): Promise<any> {
return api.delete(`/generation-ai/history/batch`, params);
}
@@ -396,12 +396,12 @@ const AuthorizationPage: React.FC = () => {
background: selectedOpenType === item.openType ? '#f0f1ff' : '#fff', background: selectedOpenType === item.openType ? '#f0f1ff' : '#fff',
}} }}
> >
<div style={{ width: '100%', height: 120, marginBottom: 12, borderRadius: 8, overflow: 'hidden' }}> <div style={{ width: '100%', height: 48, marginBottom: 12, borderRadius: 8, overflow: 'hidden' }}>
{item.thumb ? ( {item.thumb ? (
<img <img
src={buildUrl(item.thumb)} src={buildUrl(item.thumb)}
alt={item.typeName} alt={item.typeName}
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', display: 'block' }}
/> />
) : ( ) : (
<div style={{ width: '100%', height: '100%', background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ width: '100%', height: '100%', background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
+420 -232
View File
@@ -14,9 +14,9 @@ import {
ReloadOutlined, ReloadOutlined,
StarOutlined, StarOutlined,
PlayCircleOutlined, PlayCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest } from '../api'; import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest, deleteHistory, deleteResourcesMaterial } from '../api';
const { Search } = Input; const { Search } = Input;
const { Text } = Typography; const { Text } = Typography;
@@ -221,8 +221,8 @@ const GeneratedRecord: React.FC = () => {
} }
// 检查下载数量限制 // 检查下载数量限制
if (selectedItems.size > 10) { if (selectedItems.size > 30) {
message.warning('最多只能单次下载10个文件'); message.warning('最多只能单次下载30个文件');
return; return;
} }
@@ -266,12 +266,12 @@ const GeneratedRecord: React.FC = () => {
selectedContent.forEach((item, index) => { selectedContent.forEach((item, index) => {
setTimeout(() => { setTimeout(() => {
const url = `${baseUrl}${item.videoUrl || item.imageUrl}&download=1`; const url = `${baseUrl}${item.videoUrl || item.imageUrl}&download=1`;
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = ''; link.download = '';
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
}, index * 500); }, index * 500);
}); });
return; return;
@@ -286,6 +286,115 @@ const GeneratedRecord: React.FC = () => {
message.destroy('downloadProgress'); message.destroy('downloadProgress');
message.success(`下载完成,共 ${successCount} 个文件`); message.success(`下载完成,共 ${successCount} 个文件`);
setIsSelectionMode(false);
setSelectedItems(new Set());
};
const handleDeleteSelected = async () => {
if (selectedItems.size === 0) {
message.warning('请先选择要删除的媒体');
return;
}
// 检查下载数量限制
if (selectedItems.size > 30) {
message.warning('最多只能单次删除30个文件');
return;
}
const selectedData = recordlist.flatMap((group: any) =>
group.items.filter((item: any) => selectedItems.has(getItemResourceId(item)))
);
let confirmContent: React.ReactNode = '';
if (filterType === 'shot_replicate') {
confirmContent = (
<div>
<div style={{ marginBottom: 8 }}></div>
<ul style={{ margin: 0, paddingLeft: 20 }}>
{selectedData.map((item: any, index: number) => (
<li key={index} style={{ marginBottom: 4, color: '#334155' }}>
{item.shotReplicateProjectTitle} {item.shotSegmentLabel}
</li>
))}
</ul>
<div style={{ marginTop: 12 }}></div>
</div>
);
} else if (filterType === 'hot_opening_replicate') {
confirmContent = (
<div>
<div style={{ marginBottom: 8 }}></div>
<ul style={{ margin: 0, paddingLeft: 20 }}>
{selectedData.map((item: any, index: number) => (
<li key={index} style={{ marginBottom: 4, color: '#334155' }}>
{item.moduleProjectTitle}
</li>
))}
</ul>
<div style={{ marginTop: 12 }}></div>
</div>
);
} else {
confirmContent = `确定删除选中的 ${selectedItems.size} 条记录吗?`;
}
Modal.confirm({
title: '确认删除',
content: confirmContent,
okText: '确定删除',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: async () => {
let historySource = '';
if (filterType === 'project') {
historySource = 'generation_record';
} else if (filterType === 'creation') {
historySource = 'chat_task';
} else if (filterType === 'hot_opening_replicate') {
historySource = 'hot_opening_replicate';
} else if (filterType === 'shot_replicate') {
historySource = 'shot_replicate';
}
const selectedContent: any[] = [];
recordlist.forEach((group: any) => {
group.items.forEach((item: any) => {
if (selectedItems.has(getItemResourceId(item))) {
selectedContent.push(item);
}
});
});
let idArr: any[] = [];
if (historySource === 'generation_record') {
idArr = selectedContent.map((item: any) => item.id);
} else if (historySource === 'chat_task') {
idArr = selectedContent.map((item: any) => item.id);
} else if (historySource === 'hot_opening_replicate') {
idArr = selectedContent.map((item: any) => item.moduleProjectId);
} else if (historySource === 'shot_replicate') {
idArr = selectedContent.map((item: any) => item.shotSegmentId);
}
const params = {
history_source: historySource,
ids: idArr,
};
try {
await deleteResourcesMaterial(params);
message.success('删除成功');
setIsSelectionMode(false);
setSelectedItems(new Set());
setPagebreak(prev => ({ ...prev, page: 1 }));
loadRecordList();
} catch (error) {
message.error('删除失败,请重试');
}
},
});
}; };
const loadPreTestTemplates = async () => { const loadPreTestTemplates = async () => {
setPreTestTemplatesLoading(true); setPreTestTemplatesLoading(true);
@@ -586,7 +695,6 @@ const GeneratedRecord: React.FC = () => {
message.error(error.message || '文件名更新失败'); message.error(error.message || '文件名更新失败');
} }
}; };
// 日期选择器变化处理函数
const handleDateChange = (dateString: string) => { const handleDateChange = (dateString: string) => {
setSelectedDate(dateString); setSelectedDate(dateString);
}; };
@@ -618,7 +726,7 @@ const GeneratedRecord: React.FC = () => {
}, 5000); }, 5000);
}); });
}; };
useEffect(() => { const loadRecordList = () => {
setLoading(true); setLoading(true);
let historySource = ''; let historySource = '';
if (filterType === 'project') { if (filterType === 'project') {
@@ -695,6 +803,9 @@ const GeneratedRecord: React.FC = () => {
setLoading(false); setLoading(false);
}); });
} }
};
useEffect(() => {
loadRecordList();
}, [filterType, filterMedia, Pagebreak.page, selectedDate]); }, [filterType, filterMedia, Pagebreak.page, selectedDate]);
// 加载更多 // 加载更多
const handleLoadMore = () => { const handleLoadMore = () => {
@@ -827,7 +938,7 @@ const GeneratedRecord: React.FC = () => {
}} }}
icon={<StarOutlined />} icon={<StarOutlined />}
> >
</Button> </Button>
<Button <Button
type={filterType === 'shot_replicate' ? 'primary' : 'default'} type={filterType === 'shot_replicate' ? 'primary' : 'default'}
@@ -882,6 +993,12 @@ const GeneratedRecord: React.FC = () => {
> >
</Button> </Button>
<Button
onClick={() => handleDeleteSelected()}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #bf0b0a 0%, #ff8165 100%)', fontWeight: 600, padding: '8px 24px', color: '#fff' }}
>
({selectedItems.size})
</Button>
<Button <Button
onClick={() => handleDownloadSelected()} onClick={() => handleDownloadSelected()}
style={{ style={{
@@ -1150,11 +1267,11 @@ const GeneratedRecord: React.FC = () => {
padding: '4px 8px', padding: '4px 8px',
backgroundColor: 'rgba(239, 68, 68, 0.9)', backgroundColor: 'rgba(239, 68, 68, 0.9)',
color: '#fff', color: '#fff',
fontSize: 10, fontSize: 12,
borderRadius: 4, borderRadius: 4,
zIndex: 10, zIndex: 10,
}}> }}>
</div> </div>
)} )}
</> </>
@@ -1417,7 +1534,7 @@ const GeneratedRecord: React.FC = () => {
onCancel={handleClosePreview} onCancel={handleClosePreview}
width={1200} width={1200}
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: 'rgba(255,255,255,0.6)', borderTop: '1px solid rgba(99, 102, 241, 0.08)' }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, padding: '16px 24px', background: '#fff', borderTop: '1px solid #f1f5f9' }}>
<Button <Button
type="primary" type="primary"
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
@@ -1433,251 +1550,322 @@ const GeneratedRecord: React.FC = () => {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
}} }}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }} style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)', fontWeight: 600, padding: '8px 24px' }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)} disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
> >
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'} {isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
</Button> </Button>
<Button
type="primary"
icon={<DeleteOutlined />}
onClick={() => {
let deleteId = '';
let confirmContent: React.ReactNode = '';
if (filterType === 'shot_replicate') {
deleteId = previewItem.shotSegmentId;
confirmContent = (
<div>
<div style={{ marginBottom: 8 }}></div>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li style={{ marginBottom: 4, color: '#334155' }}>
{previewItem.shotReplicateProjectTitle} {previewItem.shotSegmentLabel}
</li>
</ul>
<div style={{ marginTop: 12 }}></div>
</div>
);
} else if (filterType === 'hot_opening_replicate') {
deleteId = previewItem.moduleProjectId;
confirmContent = (
<div>
<div style={{ marginBottom: 8 }}></div>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li style={{ marginBottom: 4, color: '#334155' }}>
{previewItem.moduleProjectTitle}
</li>
</ul>
<div style={{ marginTop: 12 }}></div>
</div>
);
} else {
deleteId = previewItem.id;
confirmContent = '确定删除该记录吗?';
}
if (!deleteId) {
message.warning('无法获取删除ID');
return;
}
Modal.confirm({
title: '确认删除',
content: confirmContent,
okText: '确定删除',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: async () => {
let historySource = '';
if (filterType === 'project') {
historySource = 'generation_record';
} else if (filterType === 'creation') {
historySource = 'chat_task';
} else if (filterType === 'hot_opening_replicate') {
historySource = 'hot_opening_replicate';
} else if (filterType === 'shot_replicate') {
historySource = 'shot_replicate';
}
let params = {
history_source: historySource,
ids: [deleteId],
}
try {
await deleteResourcesMaterial(params); // 调用删除接口
message.success('删除成功');
handleClosePreview();
setPagebreak(prev => ({ ...prev, page: 1 }));
loadRecordList();
} catch (error) {
message.error('删除失败,请重试');
}
},
});
}}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #bf0b0a 0%, #ff8165 100%)', fontWeight: 600, padding: '8px 24px' }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
>
</Button>
<Button <Button
onClick={handleSinglePushToMedia} onClick={handleSinglePushToMedia}
style={{ borderRadius: 8 }} style={{ borderRadius: 8, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#64748b', fontWeight: 600, padding: '8px 24px' }}
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)} disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
> >
</Button> </Button>
</div> </div>
} }
style={{ borderRadius: 20 }} style={{ borderRadius: 16, overflow: 'hidden' }}
styles={{ styles={{
body: { height: 460, display: 'flex', flexDirection: 'column', padding: 0 }, body: { height: 500, display: 'flex', flexDirection: 'column', padding: 0 },
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' }, header: { background: '#fff', borderBottom: '1px solid #f1f5f9', padding: '16px 24px' },
mask: { backgroundColor: 'rgba(0, 0, 0, 0.5)' },
}} }}
> >
{/* 内容区域 */}
<div style={{ <div style={{
flex: 1, width: '100%',
display: 'flex', display: 'flex',
flexWrap: 'wrap', height: '100%',
height: '460px', background: '#f8fafc',
gap: 20,
padding: 20,
overflow: 'auto',
justifyContent: 'center',
alignItems: 'center',
}}> }}>
{/* 媒体预览 */} <div style={{
<div style={{ width: '70%',
flex: 1, height: '100%',
minWidth: '280px', display: 'flex',
maxWidth: '800px', justifyContent: 'center',
display: 'flex', alignItems: 'center',
alignItems: 'center',
justifyContent: 'center',
minHeight: '200px',
}}>
{/* 检查媒体是否过期 */}
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: 48, marginBottom: 16 }}></div>
<p style={{ fontSize: 16, color: '#ff4d4f', marginBottom: 16 }}>/</p>
</div>
) : filterMedia === 'video' ? (
<video
ref={videoRef}
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl}`}
controls
autoPlay
style={{
maxWidth: '100%',
maxHeight: '55vh',
borderRadius: 8,
objectFit: 'contain',
}}
/>
) : (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${previewItem.imageUrl}&w=300&q=50`}
alt="预览"
style={{
maxWidth: '100%',
maxHeight: '55vh',
objectFit: 'contain',
borderRadius: 8
}}
/>
)}
</div>
{/* 参数信息 */} padding: 20,
<div style={{ }}>
width: '100%', {isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? (
minWidth: '280px', <div style={{ textAlign: 'center', padding: '40px', backgroundColor: '#fff', borderRadius: 12 }}>
maxWidth: '320px', <div style={{ fontSize: 48, marginBottom: 16 }}></div>
background: '#f8fafc', <p style={{ fontSize: 15, color: '#ef4444', fontWeight: 500, marginBottom: 8 }}></p>
borderRadius: 12, <p style={{ fontSize: 13, color: '#94a3b8' }}></p>
padding: 20, </div>
maxHeight: '55vh', ) : filterMedia === 'video' ? (
overflowY: 'auto', <video
overflowX: 'hidden', ref={videoRef}
}}> src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl}`}
<Typography.Text strong style={{ fontSize: 14, color: '#475569', display: 'block', marginBottom: 16 }}> controls
autoPlay
style={{
maxWidth: '100%',
maxHeight: '100%',
borderRadius: 12,
objectFit: 'contain',
}}
/>
) : (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${previewItem.imageUrl}&w=300&q=50`}
alt="预览"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
borderRadius: 12,
}}
/>
)}
</div>
<div style={{
width: '30%',
height: '100%',
overflowY: 'auto',
scrollbarWidth: 'none',
backgroundColor: '#fff',
padding: 24,
borderLeft: '1px solid #f1f5f9',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 20 }}>
<div style={{ width: 2, height: 18, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 1 }} />
<Typography.Text strong style={{ fontSize: 15, color: '#1e293b', fontWeight: 600 }}>
</Typography.Text> </Typography.Text>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* ID */} <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}> <span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#94a3b8', fontSize: 13 }}>ID</span> <span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> {filterMedia === 'video' ? '视频' : '图片'}
{previewItem.id || '-'} </span>
</span> </div>
</div> */} {filterType === 'hot_opening_replicate' && (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 4 }}>
{/* 类型 */} <span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500, wordBreak: 'break-all', lineHeight: '1.5' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span> {previewItem.moduleProjectTitle}
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{filterMedia === 'video' ? '视频' : '图片'}
</span> </span>
</div> </div>
<div style={{ display: 'flex', gap: 12 }}> )}
<span style={{ color: '#94a3b8', fontSize: 13, flexShrink: 0, width: 40 }}></span> {filterType === 'shot_replicate' && (
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, flex: 1, wordBreak: 'break-all' }}> <div style={{ display: 'flex', justifyContent: 'space-between', gap: 4 }}>
{previewItem.originalPrompt} <span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, wordBreak: 'break-all', lineHeight: '1.5' }}>
{previewItem.shotReplicateProjectTitle} {previewItem.shotSegmentLabel}
</span> </span>
</div> </div>
)}
{/* 分辨率 */} <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{filterMedia === 'image' && ( <span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<> <span style={{ color: '#334155', fontSize: 13, fontWeight: 500, wordBreak: 'break-all', lineHeight: '1.5' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> {previewItem.originalPrompt}
<span style={{ color: '#94a3b8', fontSize: 13 }}></span> </span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> </div>
{previewItem.imageProportion}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imageSize}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx}
</span>
</div>
</>
)}
{filterMedia === 'video' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.aspectRatio}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.resolution}
</span>
</div>
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}>尺寸</span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.imagePx}
</span>
</div> */}
</>
)}
{/* 时长(视频) */} {filterMedia === 'image' && (
{filterMedia === 'video' && ( <>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span> <span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}> <span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{previewItem.imageProportion}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{previewItem.imageSize}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{previewItem.imagePx}
</span>
</div>
</>
)}
{filterMedia === 'video' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{previewItem.aspectRatio}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{previewItem.resolution}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{`${previewItem.duration}` || '-'} {`${previewItem.duration}` || '-'}
</span> </span>
</div> </div>
)}
{/* 文件大小 */}
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}>文件大小</span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.size ? formatFileSize(previewItem.size) : '-'}
</span>
</div> */}
{/* 创建时间 */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{formatDateTime(previewItem.createdAt || previewItem.generatedDate)}
</span>
</div>
{/* 生成引擎 */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.engine || previewItem.engineName || '-'}
</span>
</div>
</div>
{/* 分隔线 */}
<div style={{ borderTop: '1px dashed #e2e8f0', margin: '16px 0' }} />
{previewItem.mediaReferences && previewItem.mediaReferences.length > 0 && (
<>
<Typography.Text strong style={{ fontSize: 14, color: '#475569', display: 'block', marginBottom: 12 }}>
</Typography.Text>
{previewItem.mediaReferences.map((item, index) => (
<div
key={item.url}
onClick={() => {
// 暂停视频
if (videoRef.current) {
videoRef.current.pause();
}
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.url}`;
window.open(url, '_blank');
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: 8,
borderRadius: 6,
cursor: 'pointer',
backgroundColor: '#f1f5f9',
marginBottom: 4,
transition: 'background-color 0.2s',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.backgroundColor = '#e2e8f0';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.backgroundColor = '#f1f5f9';
}}
>
{item.type === 'image' ? (
<PictureOutlined style={{ color: '#3b82f6', fontSize: 14 }} />
) : (
<VideoCameraOutlined style={{ color: '#f59e0b', fontSize: 14 }} />
)}
<span style={{ color: '#334155', fontSize: 13 }}>
{item.name || `媒体${index + 1}`}
</span>
</div>
))}
</> </>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{formatDateTime(previewItem.createdAt || previewItem.generatedDate)}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 600 }}>
{previewItem.engine || previewItem.engineName || '-'}
</span>
</div>
</div>
<div style={{ borderTop: '1px solid #f1f5f9', margin: '20px 0' }} />
{previewItem.mediaReferences && previewItem.mediaReferences.length > 0 && !['hot_opening_replicate', 'shot_replicate'].includes(filterType) && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<div style={{ width: 2, height: 18, background: 'linear-gradient(180deg, #3b82f6 0%, #60a5fa 100%)', borderRadius: 1 }} />
<Typography.Text strong style={{ fontSize: 15, color: '#1e293b', fontWeight: 600 }}>
</Typography.Text>
</div>
{previewItem.mediaReferences.map((item, index) => (
<div
key={item.url}
onClick={() => {
if (videoRef.current) {
videoRef.current.pause();
}
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.url}`;
window.open(url, '_blank');
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '10px 12px',
borderRadius: 8,
cursor: 'pointer',
backgroundColor: '#f8fafc',
marginBottom: 8,
transition: 'all 0.25s ease',
border: '1px solid transparent',
}}
onMouseEnter={(e) => {
const el = e.currentTarget as HTMLElement;
el.style.backgroundColor = '#e0f2fe';
el.style.borderColor = '#bae6fd';
}}
onMouseLeave={(e) => {
const el = e.currentTarget as HTMLElement;
el.style.backgroundColor = '#f8fafc';
el.style.borderColor = 'transparent';
}}
>
{item.type === 'image' ? (
<PictureOutlined style={{ color: '#3b82f6', fontSize: 16 }} />
) : (
<VideoCameraOutlined style={{ color: '#f59e0b', fontSize: 16 }} />
)}
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{item.name || `媒体${index + 1}`}
</span>
</div>
))}
</>
)}
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -1826,7 +2014,7 @@ const GeneratedRecord: React.FC = () => {
})()} })()}
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}> <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Typography.Text style={{ fontSize: 14, color: '#475569' }}></Typography.Text> <Typography.Text style={{ fontSize: 14, color: '#475569' }}></Typography.Text>
@@ -2006,7 +2194,7 @@ const GeneratedRecord: React.FC = () => {
onClick: () => { onClick: () => {
const id = String(record.id); const id = String(record.id);
const newOauthItems = [...selectedOauthItems]; const newOauthItems = [...selectedOauthItems];
newOauthItems[index] = { value: id, label: String(record.accountId)+'-'+(record.accountName || '-') }; newOauthItems[index] = { value: id, label: String(record.accountId) + '-' + (record.accountName || '-') };
setSelectedOauthItems(newOauthItems); setSelectedOauthItems(newOauthItems);
const newOauthSelectOpens = [...oauthSelectOpens]; const newOauthSelectOpens = [...oauthSelectOpens];
newOauthSelectOpens[index] = false; newOauthSelectOpens[index] = false;
@@ -2101,14 +2289,14 @@ const GeneratedRecord: React.FC = () => {
.filter(item => item.advertiserId) .filter(item => item.advertiserId)
.map(item => String(item.advertiserId))} .map(item => String(item.advertiserId))}
onChange={(targetKeys) => { onChange={(targetKeys) => {
const newSelected = historyOAuthList.filter(item => const newSelected = historyOAuthList.filter(item =>
item.advertiserId && targetKeys.includes(String(item.advertiserId)) item.advertiserId && targetKeys.includes(String(item.advertiserId))
); );
setSelectedHistoryAccounts(newSelected); setSelectedHistoryAccounts(newSelected);
}} }}
titles={['可选账户列表', '已选账户列表']} titles={['可选账户列表', '已选账户列表']}
showSearch showSearch
filterOption={(inputValue, item) => filterOption={(inputValue, item) =>
String(item.advertiserId).toLowerCase().includes(inputValue.toLowerCase()) || String(item.advertiserId).toLowerCase().includes(inputValue.toLowerCase()) ||
(item.advertiserName && item.advertiserName.toLowerCase().includes(inputValue.toLowerCase())) (item.advertiserName && item.advertiserName.toLowerCase().includes(inputValue.toLowerCase()))
} }
+4 -4
View File
@@ -98,7 +98,7 @@ const HomePage: React.FC = () => {
const aiEntries = [ const aiEntries = [
{ {
icon: <FileTextOutlined style={{ fontSize: 24, color: '#6366f1' }} />, icon: <FileTextOutlined style={{ fontSize: 24, color: '#6366f1' }} />,
title: '爆款开头复刻', title: '爆款复刻',
description: '上传参考视频与产品图片,一键复刻爆款视频开头', description: '上传参考视频与产品图片,一键复刻爆款视频开头',
action: '立即创作', action: '立即创作',
path: '/initial', path: '/initial',
@@ -106,7 +106,7 @@ const HomePage: React.FC = () => {
{ {
icon: <ScissorOutlined style={{ fontSize: 24, color: '#f97316' }} />, icon: <ScissorOutlined style={{ fontSize: 24, color: '#f97316' }} />,
title: '拆镜复刻', title: '拆镜复刻',
description: '一键拆解画面分镜,助力仿拍或创作', description: '精细化镜头复刻工具,拆分参考视频单镜头独立复刻,提升素材原创度,规避素材同质化',
action: '开始混剪', action: '开始混剪',
path: '/removelens', path: '/removelens',
}, },
@@ -143,7 +143,7 @@ const HomePage: React.FC = () => {
return ( return (
<div className="content_box"> <div className="content_box">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<h3></h3> <h3> </h3>
<p <p
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
@@ -190,7 +190,7 @@ const HomePage: React.FC = () => {
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 }}>
<span style={{ color: '#a8a8a8ff', fontSize: 12 }}>()</span>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -473,7 +473,7 @@ function InitialInfo() {
<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 }} />
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}> <h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
- -
</h3> </h3>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} />
@@ -450,7 +450,7 @@ const GenerateConver: React.FC = () => {
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 }}>
<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={{
margin: 0, margin: 0,
@@ -460,15 +460,15 @@ const GenerateConver: React.FC = () => {
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent', WebkitTextFillColor: 'transparent',
backgroundClip: 'text', backgroundClip: 'text',
textAlign: 'center', // textAlign: 'center',
}}> }}>
</h2> </h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}> <p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
</p> </p>
</div> </div>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> {/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> */}
</div> </div>
<Button <Button
type="primary" type="primary"
@@ -796,7 +796,7 @@ const GenerateConver: React.FC = () => {
</h3> </h3>
<p style={{ margin: 0, fontSize: 13, color: '#64748b', textAlign: 'center', padding: '0 40px', lineHeight: 1.6 }}> <p style={{ margin: 0, fontSize: 13, color: '#64748b', textAlign: 'center', padding: '0 40px', lineHeight: 1.6 }}>
</p> </p>
</div> </div>
)} )}
+258 -246
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, Tag, Button, Typography, Space, Modal, Form, Input, Select, Cascader, App, Pagination } from 'antd'; import { Table, Tag, Button, Typography, Space, Modal, Form, Input, Select, Cascader, App, Pagination, Collapse } from 'antd';
import { PlusOutlined, EditOutlined, FileTextOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, FileTextOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { getPreTestList, createPreTest, getArea, getPreTestDetail, updatePreTest, deletePreTest } from '../api'; import { getPreTestList, createPreTest, getArea, getPreTestDetail, updatePreTest, deletePreTest } from '../api';
@@ -143,6 +143,7 @@ const PreTest: React.FC = () => {
const [modalMode, setModalMode] = useState<'create' | 'edit' | 'detail'>('create'); const [modalMode, setModalMode] = useState<'create' | 'edit' | 'detail'>('create');
const [currentRecord, setCurrentRecord] = useState<PreTestRecord | null>(null); const [currentRecord, setCurrentRecord] = useState<PreTestRecord | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [expandOptional, setExpandOptional] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
@@ -608,7 +609,7 @@ const PreTest: React.FC = () => {
<div style={{ minHeight: 'calc(100vh - 80px)' }}> <div style={{ minHeight: 'calc(100vh - 80px)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FileTextOutlined style={{ color: '#6366f1', fontSize: 16 }} /> <FileTextOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text> <Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</div> </div>
<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', gap: 16, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
@@ -646,7 +647,7 @@ const PreTest: React.FC = () => {
boxShadow: '0 4px 14px rgba(99,102,241,0.3)', boxShadow: '0 4px 14px rgba(99,102,241,0.3)',
}} }}
> >
</Button> </Button>
</div> </div>
</div> </div>
@@ -692,7 +693,7 @@ const PreTest: React.FC = () => {
</div> </div>
<Modal <Modal
title={<Space>{modalMode === 'create' ? '新增前测模板' : modalMode === 'edit' ? '编辑前测模板' : '前测模板详情'}</Space>} title={<Space>{modalMode === 'create' ? '新增前测模板(选填项如填写需准确填写)' : modalMode === 'edit' ? '编辑前测模板(选填项如填写需准确填写)' : '前测模板详情'}</Space>}
open={modalOpen} open={modalOpen}
onCancel={handleCloseModal} onCancel={handleCloseModal}
onOk={handleSubmit} onOk={handleSubmit}
@@ -743,257 +744,268 @@ const PreTest: React.FC = () => {
/> />
</Form.Item> </Form.Item>
</div> </div>
{platform !== 'LOCAL' && ( <Collapse
<Form.Item defaultActiveKey={[]}
name="external_action" activeKey={expandOptional ? ['optional'] : []}
label="转化目标" onChange={() => setExpandOptional(!expandOptional)}
>
<Select
placeholder="请选择转化目标"
style={{ borderRadius: 8, height: 40 }}
options={PRETEST_FIELDS.filter(field => {
if (platform === 'AD') {
return field.name.startsWith('AD_');
} else if (platform === 'QIANCHUAN') {
return field.name.startsWith('QC_');
} else if (platform === 'LOCAL') {
return field.name.startsWith('LOCAL_');
}
return true;
}).map(field => ({
value: field.name,
label: (
<span>
{field.label}
{field.description && (
<Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 4 }}>
({field.description})
</Typography.Text>
)}
</span>
),
}))}
/>
</Form.Item>
)}
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="cpa_bid"
label="目标转化成本"
rules={[
{ type: 'number', min: 1, max: 10000, message: '取值范围: 1-10000' },
]}
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="取值范围: 1-10000,精确到小数点后2位"
style={{ borderRadius: 8, height: 40 }}
min={1}
max={10000}
step={0.01}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="audience_gender"
label="受众性别"
>
<Select
placeholder="请选择受众性别"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: 'ALL', label: '不限' },
{ value: 'MALE', label: '男' },
{ value: 'FEMALE', label: '女' },
]}
/>
</Form.Item>
</div>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="audience_age"
label="受众年龄"
>
<Select
mode="multiple"
placeholder="请选择受众年龄(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
options={[
{ value: 'ALL', label: '不限' },
{ value: '18-23', label: '18-23岁' },
{ value: '24-30', label: '24-30岁' },
{ value: '31-40', label: '31-40岁' },
{ value: '41-49', label: '41-49岁' },
{ value: '50', label: '50岁以上' },
]}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="audience_network"
label="网络类型"
>
<Select
mode="multiple"
placeholder="请选择网络类型(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
options={[
{ value: 'ALL', label: '不限' },
{ value: '5G', label: '5G' },
{ value: '4G', label: '4G' },
{ value: '3G', label: '3G' },
{ value: '2G', label: '2G' },
{ value: 'WIFI', label: 'wifi' },
]}
/>
</Form.Item>
</div>
<Form.Item
name="audience_region"
label="受众地区"
> >
<Cascader <Collapse.Panel header="选填项" key="optional">
multiple {platform !== 'LOCAL' && (
placeholder="请选择受众地区(可多选)" <Form.Item
style={{ borderRadius: 8, minHeight: 40 }} name="external_action"
options={areaOptions} label="转化目标"
maxTagCount="responsive" >
/> <Select
</Form.Item> placeholder="请选择转化目标"
<div style={{ display: 'flex', gap: 16 }}> style={{ borderRadius: 8, height: 40 }}
<Form.Item options={PRETEST_FIELDS.filter(field => {
style={{ flex: 1 }} if (platform === 'AD') {
name="cus_name" return field.name.startsWith('AD_');
label="客户主体名称" } else if (platform === 'QIANCHUAN') {
> return field.name.startsWith('QC_');
<Input } else if (platform === 'LOCAL') {
placeholder="请输入客户主体名称" return field.name.startsWith('LOCAL_');
style={{ borderRadius: 8, height: 40 }} }
/> return true;
</Form.Item> }).map(field => ({
<Form.Item value: field.name,
style={{ flex: 1 }} label: (
name="pricing_type" <span>
label="出价类型" {field.label}
> {field.description && (
<Select <Typography.Text style={{ color: '#94a3b8', fontSize: 12, marginLeft: 4 }}>
placeholder="请选择出价类型" ({field.description})
style={{ borderRadius: 8, height: 40 }} </Typography.Text>
options={[ )}
{ value: 'OCPC', label: 'OCPC' }, </span>
{ value: 'CPA', label: 'CPA' }, ),
{ value: 'OCPM', label: 'OCPM' }, }))}
]} />
/> </Form.Item>
</Form.Item> )}
</div>
{platform == 'AD' && ( <div style={{ display: 'flex', gap: 16 }}>
<div style={{ display: 'flex', gap: 16 }}> <Form.Item
style={{ flex: 1 }}
name="cpa_bid"
label="目标转化成本"
rules={[
{ type: 'number', min: 1, max: 10000, message: '取值范围: 1-10000' },
]}
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="取值范围: 1-10000,精确到小数点后2位"
style={{ borderRadius: 8, height: 40 }}
min={1}
max={10000}
step={0.01}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="audience_gender"
label="受众性别"
>
<Select
placeholder="请选择受众性别"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: 'ALL', label: '不限' },
{ value: 'MALE', label: '男' },
{ value: 'FEMALE', label: '女' },
]}
/>
</Form.Item>
</div>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="audience_age"
label="受众年龄"
>
<Select
mode="multiple"
placeholder="请选择受众年龄(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
options={[
{ value: 'ALL', label: '不限' },
{ value: '18-23', label: '18-23岁' },
{ value: '24-30', label: '24-30岁' },
{ value: '31-40', label: '31-40岁' },
{ value: '41-49', label: '41-49岁' },
{ value: '50', label: '50岁以上' },
]}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="audience_network"
label="网络类型"
>
<Select
mode="multiple"
placeholder="请选择网络类型(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
options={[
{ value: 'ALL', label: '不限' },
{ value: '5G', label: '5G' },
{ value: '4G', label: '4G' },
{ value: '3G', label: '3G' },
{ value: '2G', label: '2G' },
{ value: 'WIFI', label: 'wifi' },
]}
/>
</Form.Item>
</div>
<Form.Item <Form.Item
style={{ flex: 1 }} name="audience_region"
name="cost_cap" label="受众地区"
label="是否最优成本出价" >
<Cascader
multiple
placeholder="请选择受众地区(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
options={areaOptions}
maxTagCount="responsive"
/>
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="cus_name"
label="客户主体名称"
>
<Input
placeholder="请输入客户主体名称"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="pricing_type"
label="出价类型"
>
<Select
placeholder="请选择出价类型"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: 'OCPC', label: 'OCPC' },
{ value: 'CPA', label: 'CPA' },
{ value: 'OCPM', label: 'OCPM' },
]}
/>
</Form.Item>
</div>
{platform == 'AD' && (
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="cost_cap"
label="是否最优成本出价"
initialValue={false}
>
<Select
placeholder="是否最优成本出价"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '开启' },
{ value: false, label: '关闭' },
]}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="target_cost"
label="是否稳定成本出价"
initialValue={false}
>
<Select
placeholder="是否稳定成本出价"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '开启' },
{ value: false, label: '关闭' },
]}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="nobid"
label="是否最大转化出价"
initialValue={false}
>
<Select
placeholder="是否最大转化出价"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '开启' },
{ value: false, label: '关闭' },
]}
/>
</Form.Item>
</div>
)}
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="cpc_bid"
label="目标点击成本"
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="请输入目标点击成本"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="budget"
label="预算金额"
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="请输入预算金额"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
</div>
<Form.Item
name="note"
label="备注"
>
<Input.TextArea
placeholder="请输入备注"
rows={2}
style={{ borderRadius: 8 }}
/>
</Form.Item>
<Form.Item
name="is_default"
label="默认模板"
initialValue={false}
> >
<Select <Select
placeholder="是否最优成本出价" placeholder="请选择默认模板"
style={{ borderRadius: 8, height: 40 }} style={{ borderRadius: 8, height: 40 }}
options={[ options={[
{ value: true, label: '开启' }, { value: true, label: '' },
{ value: false, label: '关闭' }, { value: false, label: '' },
]} ]}
/> />
</Form.Item> </Form.Item>
</Collapse.Panel>
<Form.Item </Collapse>
style={{ flex: 1 }}
name="target_cost"
label="是否稳定成本出价"
>
<Select
placeholder="是否稳定成本出价"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '开启' },
{ value: false, label: '关闭' },
]}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="nobid"
label="是否最大转化出价"
>
<Select
placeholder="是否最大转化出价"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '开启' },
{ value: false, label: '关闭' },
]}
/>
</Form.Item>
</div>
)}
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="cpc_bid"
label="目标点击成本"
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="请输入目标点击成本"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="budget"
label="预算金额"
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="请输入预算金额"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
</div>
<Form.Item
name="note"
label="备注"
>
<Input.TextArea
placeholder="请输入备注"
rows={2}
style={{ borderRadius: 8 }}
/>
</Form.Item>
<Form.Item
name="is_default"
label="默认模板"
initialValue={false}
>
<Select
placeholder="请选择默认模板"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '是' },
{ value: false, label: '否' },
]}
/>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
</div> </div>
+3 -3
View File
@@ -116,7 +116,7 @@ const ProjectsPage: React.FC = () => {
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 }}>
<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={{
margin: 0, margin: 0,
@@ -126,7 +126,7 @@ const ProjectsPage: React.FC = () => {
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent', WebkitTextFillColor: 'transparent',
backgroundClip: 'text', backgroundClip: 'text',
textAlign: 'center', // textAlign: 'center',
}}> }}>
</h2> </h2>
@@ -134,7 +134,7 @@ const ProjectsPage: React.FC = () => {
{projects.length} · {projects.length} ·
</p> </p>
</div> </div>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> {/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> */}
</div> </div>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
<Button icon={<FileTextOutlined />} onClick={() => navigate('/records')} size="large" style={{ <Button icon={<FileTextOutlined />} onClick={() => navigate('/records')} size="large" style={{
+4 -4
View File
@@ -177,7 +177,7 @@ export default function VideoFrameExtractor() {
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 }}>
<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={{
margin: 0, margin: 0,
@@ -187,15 +187,15 @@ export default function VideoFrameExtractor() {
WebkitBackgroundClip: 'text', WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent', WebkitTextFillColor: 'transparent',
backgroundClip: 'text', backgroundClip: 'text',
textAlign: 'center', // textAlign: 'center',
}}> }}>
AI视频拆镜工作台 AI视频拆镜工作台
</h2> </h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}> <p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
仿
</p> </p>
</div> </div>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> {/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} /> */}
</div> </div>
<Button <Button