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>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<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.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>后台管理</title>
<script>
(function() {
var cached = localStorage.getItem('siteInfo');
if (cached) {
try {
var info = JSON.parse(cached);
if (info.siteName) {
document.title = info.siteName + ' - 管理后台';
}
if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]');
if (link) {
link.href = info.siteLogo;
link.type = 'image/png';
}
}
} catch (e) {}
}
})();
</script>
<script type="module" crossorigin src="/assets/index-DfKYC1kL.js"></script>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<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.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>后台管理</title>
<script>
(function() {
var cached = localStorage.getItem('siteInfo');
if (cached) {
try {
var info = JSON.parse(cached);
if (info.siteName) {
document.title = info.siteName + ' - 管理后台';
}
if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]');
if (link) {
link.href = info.siteLogo;
link.type = 'image/png';
}
}
} catch (e) {}
}
})();
</script>
<script type="module" crossorigin src="/assets/index-BE_fa7kc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
</head>
<body>
<div id="root"></div>
</body>
</html>
+3 -3
View File
@@ -200,7 +200,7 @@ const AdminPlatform: React.FC = () => {
if (!v) return '-';
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
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', alignItems: 'center', gap: 12, padding: 16, background: '#f8fafc', borderRadius: 8 }}>
<div style={{
width: 80, height: 60, borderRadius: 6, overflow: 'hidden',
width: 80, height: 48, borderRadius: 6, overflow: 'hidden',
background: currentOpenType.thumb ? 'transparent' : '#e2e8f0',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
@@ -390,7 +390,7 @@ const AdminPlatform: React.FC = () => {
<img
src={currentOpenType.thumb.startsWith('http') ? currentOpenType.thumb : `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${currentOpenType.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>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</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">
</head>
<body>
+1 -1
View File
@@ -130,5 +130,5 @@ export const api = {
post: <T>(path: string, body?: unknown, auth = true, skipAuthRedirect = false) =>
apiRequest<T>(path, { method: 'POST', body, auth, skipAuthRedirect }),
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> {
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',
}}
>
<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 ? (
<img
src={buildUrl(item.thumb)}
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' }}>
+420 -232
View File
@@ -14,9 +14,9 @@ import {
ReloadOutlined,
StarOutlined,
PlayCircleOutlined,
DeleteOutlined,
} 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 { Text } = Typography;
@@ -221,8 +221,8 @@ const GeneratedRecord: React.FC = () => {
}
// 检查下载数量限制
if (selectedItems.size > 10) {
message.warning('最多只能单次下载10个文件');
if (selectedItems.size > 30) {
message.warning('最多只能单次下载30个文件');
return;
}
@@ -266,12 +266,12 @@ const GeneratedRecord: React.FC = () => {
selectedContent.forEach((item, index) => {
setTimeout(() => {
const url = `${baseUrl}${item.videoUrl || item.imageUrl}&download=1`;
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, index * 500);
});
return;
@@ -286,6 +286,115 @@ const GeneratedRecord: React.FC = () => {
message.destroy('downloadProgress');
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 () => {
setPreTestTemplatesLoading(true);
@@ -586,7 +695,6 @@ const GeneratedRecord: React.FC = () => {
message.error(error.message || '文件名更新失败');
}
};
// 日期选择器变化处理函数
const handleDateChange = (dateString: string) => {
setSelectedDate(dateString);
};
@@ -618,7 +726,7 @@ const GeneratedRecord: React.FC = () => {
}, 5000);
});
};
useEffect(() => {
const loadRecordList = () => {
setLoading(true);
let historySource = '';
if (filterType === 'project') {
@@ -695,6 +803,9 @@ const GeneratedRecord: React.FC = () => {
setLoading(false);
});
}
};
useEffect(() => {
loadRecordList();
}, [filterType, filterMedia, Pagebreak.page, selectedDate]);
// 加载更多
const handleLoadMore = () => {
@@ -827,7 +938,7 @@ const GeneratedRecord: React.FC = () => {
}}
icon={<StarOutlined />}
>
</Button>
<Button
type={filterType === 'shot_replicate' ? 'primary' : 'default'}
@@ -882,6 +993,12 @@ const GeneratedRecord: React.FC = () => {
>
</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
onClick={() => handleDownloadSelected()}
style={{
@@ -1150,11 +1267,11 @@ const GeneratedRecord: React.FC = () => {
padding: '4px 8px',
backgroundColor: 'rgba(239, 68, 68, 0.9)',
color: '#fff',
fontSize: 10,
fontSize: 12,
borderRadius: 4,
zIndex: 10,
}}>
</div>
)}
</>
@@ -1417,7 +1534,7 @@ const GeneratedRecord: React.FC = () => {
onCancel={handleClosePreview}
width={1200}
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
type="primary"
icon={<DownloadOutlined />}
@@ -1433,251 +1550,322 @@ const GeneratedRecord: React.FC = () => {
link.click();
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)}
>
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'}
</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
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)}
>
</Button>
</div>
}
style={{ borderRadius: 20 }}
style={{ borderRadius: 16, overflow: 'hidden' }}
styles={{
body: { height: 460, 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' },
body: { height: 500, display: 'flex', flexDirection: 'column', padding: 0 },
header: { background: '#fff', borderBottom: '1px solid #f1f5f9', padding: '16px 24px' },
mask: { backgroundColor: 'rgba(0, 0, 0, 0.5)' },
}}
>
{/* 内容区域 */}
<div style={{
flex: 1,
width: '100%',
display: 'flex',
flexWrap: 'wrap',
height: '460px',
gap: 20,
padding: 20,
overflow: 'auto',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
background: '#f8fafc',
}}>
{/* 媒体预览 */}
<div style={{
flex: 1,
minWidth: '280px',
maxWidth: '800px',
display: 'flex',
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>
<div style={{
width: '70%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
{/* 参数信息 */}
<div style={{
width: '100%',
minWidth: '280px',
maxWidth: '320px',
background: '#f8fafc',
borderRadius: 12,
padding: 20,
maxHeight: '55vh',
overflowY: 'auto',
overflowX: 'hidden',
}}>
<Typography.Text strong style={{ fontSize: 14, color: '#475569', display: 'block', marginBottom: 16 }}>
padding: 20,
}}>
{isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? (
<div style={{ textAlign: 'center', padding: '40px', backgroundColor: '#fff', borderRadius: 12 }}>
<div style={{ fontSize: 48, marginBottom: 16 }}></div>
<p style={{ fontSize: 15, color: '#ef4444', fontWeight: 500, marginBottom: 8 }}></p>
<p style={{ fontSize: 13, color: '#94a3b8' }}></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: '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>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* ID */}
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}>ID</span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{previewItem.id || '-'}
</span>
</div> */}
{/* 类型 */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{filterMedia === 'video' ? '视频' : '图片'}
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<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 }}>
{filterMedia === 'video' ? '视频' : '图片'}
</span>
</div>
{filterType === 'hot_opening_replicate' && (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 4 }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, wordBreak: 'break-all', lineHeight: '1.5' }}>
{previewItem.moduleProjectTitle}
</span>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<span style={{ color: '#94a3b8', fontSize: 13, flexShrink: 0, width: 40 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, flex: 1, wordBreak: 'break-all' }}>
{previewItem.originalPrompt}
)}
{filterType === 'shot_replicate' && (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 4 }}>
<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>
</div>
)}
{/* 分辨率 */}
{filterMedia === 'image' && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
{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> */}
</>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<span style={{ color: '#94a3b8', fontSize: 13, fontWeight: 500 }}></span>
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, wordBreak: 'break-all', lineHeight: '1.5' }}>
{previewItem.originalPrompt}
</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 }}>
{filterMedia === 'image' && (
<>
<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.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}` || '-'}
</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.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>
</Modal>
@@ -1826,7 +2014,7 @@ const GeneratedRecord: React.FC = () => {
})()}
</div>
</div>
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Typography.Text style={{ fontSize: 14, color: '#475569' }}></Typography.Text>
@@ -2006,7 +2194,7 @@ const GeneratedRecord: React.FC = () => {
onClick: () => {
const id = String(record.id);
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);
const newOauthSelectOpens = [...oauthSelectOpens];
newOauthSelectOpens[index] = false;
@@ -2101,14 +2289,14 @@ const GeneratedRecord: React.FC = () => {
.filter(item => item.advertiserId)
.map(item => String(item.advertiserId))}
onChange={(targetKeys) => {
const newSelected = historyOAuthList.filter(item =>
const newSelected = historyOAuthList.filter(item =>
item.advertiserId && targetKeys.includes(String(item.advertiserId))
);
setSelectedHistoryAccounts(newSelected);
}}
titles={['可选账户列表', '已选账户列表']}
showSearch
filterOption={(inputValue, item) =>
filterOption={(inputValue, item) =>
String(item.advertiserId).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 = [
{
icon: <FileTextOutlined style={{ fontSize: 24, color: '#6366f1' }} />,
title: '爆款开头复刻',
title: '爆款复刻',
description: '上传参考视频与产品图片,一键复刻爆款视频开头',
action: '立即创作',
path: '/initial',
@@ -106,7 +106,7 @@ const HomePage: React.FC = () => {
{
icon: <ScissorOutlined style={{ fontSize: 24, color: '#f97316' }} />,
title: '拆镜复刻',
description: '一键拆解画面分镜,助力仿拍或创作',
description: '精细化镜头复刻工具,拆分参考视频单镜头独立复刻,提升素材原创度,规避素材同质化',
action: '开始混剪',
path: '/removelens',
},
@@ -143,7 +143,7 @@ const HomePage: React.FC = () => {
return (
<div className="content_box">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<h3></h3>
<h3> </h3>
<p
style={{
cursor: 'pointer',
@@ -190,7 +190,7 @@ const HomePage: React.FC = () => {
background: 'linear-gradient(180deg, #6366f1, #a855f7)',
}} />
<div style={{ fontSize: 17, fontWeight: 700, color: '#1f2937', letterSpacing: 0.3 }}>
<span style={{ color: '#a8a8a8ff', fontSize: 12 }}>()</span>
</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 }} />
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
-
-
</h3>
<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,
}}>
<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>
<h2 style={{
margin: 0,
@@ -460,15 +460,15 @@ const GenerateConver: React.FC = () => {
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
textAlign: 'center',
// textAlign: 'center',
}}>
</h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
</p>
</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>
<Button
type="primary"
@@ -796,7 +796,7 @@ const GenerateConver: React.FC = () => {
</h3>
<p style={{ margin: 0, fontSize: 13, color: '#64748b', textAlign: 'center', padding: '0 40px', lineHeight: 1.6 }}>
</p>
</div>
)}
+258 -246
View File
@@ -1,5 +1,5 @@
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 { 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 [currentRecord, setCurrentRecord] = useState<PreTestRecord | null>(null);
const [form] = Form.useForm();
const [expandOptional, setExpandOptional] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
@@ -608,7 +609,7 @@ const PreTest: React.FC = () => {
<div style={{ minHeight: 'calc(100vh - 80px)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FileTextOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<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)',
}}
>
</Button>
</div>
</div>
@@ -692,7 +693,7 @@ const PreTest: React.FC = () => {
</div>
<Modal
title={<Space>{modalMode === 'create' ? '新增前测模板' : modalMode === 'edit' ? '编辑前测模板' : '前测模板详情'}</Space>}
title={<Space>{modalMode === 'create' ? '新增前测模板(选填项如填写需准确填写)' : modalMode === 'edit' ? '编辑前测模板(选填项如填写需准确填写)' : '前测模板详情'}</Space>}
open={modalOpen}
onCancel={handleCloseModal}
onOk={handleSubmit}
@@ -743,257 +744,268 @@ const PreTest: React.FC = () => {
/>
</Form.Item>
</div>
{platform !== 'LOCAL' && (
<Form.Item
name="external_action"
label="转化目标"
>
<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="受众地区"
<Collapse
defaultActiveKey={[]}
activeKey={expandOptional ? ['optional'] : []}
onChange={() => setExpandOptional(!expandOptional)}
>
<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 }}>
<Collapse.Panel header="选填项" key="optional">
{platform !== 'LOCAL' && (
<Form.Item
name="external_action"
label="转化目标"
>
<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
style={{ flex: 1 }}
name="cost_cap"
label="是否最优成本出价"
name="audience_region"
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
placeholder="是否最优成本出价"
placeholder="请选择默认模板"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: true, label: '开启' },
{ value: false, label: '关闭' },
{ value: true, label: '' },
{ value: false, label: '' },
]}
/>
</Form.Item>
<Form.Item
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>
</Collapse.Panel>
</Collapse>
</Form>
</Modal>
</div>
+3 -3
View File
@@ -116,7 +116,7 @@ const ProjectsPage: React.FC = () => {
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}>
<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>
<h2 style={{
margin: 0,
@@ -126,7 +126,7 @@ const ProjectsPage: React.FC = () => {
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
textAlign: 'center',
// textAlign: 'center',
}}>
</h2>
@@ -134,7 +134,7 @@ const ProjectsPage: React.FC = () => {
{projects.length} ·
</p>
</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 style={{ display: 'flex', gap: 12 }}>
<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,
}}>
<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>
<h2 style={{
margin: 0,
@@ -187,15 +187,15 @@ export default function VideoFrameExtractor() {
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
textAlign: 'center',
// textAlign: 'center',
}}>
AI视频拆镜工作台
</h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
仿
</p>
</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>
<Button