This commit is contained in:
sjy
2026-06-30 14:00:42 +08:00
37 changed files with 1709 additions and 2685 deletions
+3
View File
@@ -2,6 +2,9 @@ import React, { useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ConfigProvider, App as AntApp, Spin } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import AppLayout from './components/Layout/AppLayout';
import LoginPage from './pages/LoginPage';
import ProjectsPage from './pages/ProjectsPage';
+3 -31
View File
@@ -503,7 +503,6 @@ export async function getDefaultPreTest(): Promise<any> {
export async function updateHotOpeningVideoPromptSchema(projectId: string, stepId: string, params: { prompt_schema: Record<string, any> }): Promise<any> {
return api.put(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/video-prompt-schema`, params);
}
// 镜头复刻
export async function createShotReplication(params: any): Promise<any> {
return api.post('/shot-replications/task-sets', params);
@@ -519,68 +518,50 @@ export async function getShotReplicationList(page: number, page_size: number, ke
export async function getShotReplicationDetail(taskSetId: string): Promise<any> {
return api.get(`/shot-replications/task-sets/${taskSetId}`);
}
// ai拆镜
export async function createRemoveLens(taskSetId: string, params: any): Promise<any> {
return api.post(`/shot-replications/task-sets/${taskSetId}/split-by-ai`, params);
}
// 手动分割
export async function splitCustom(taskSetId: string, params: any): Promise<any> {
return api.post(`/shot-replications/task-sets/${taskSetId}/split-custom`, params);
}
// 拆镜列表
export async function Removelist(taskSetId: string): Promise<any> {
return api.get(`/shot-replications/task-sets/${taskSetId}/segments`);
}
// 生成视频
export async function removeCreate(recordId: string, params: any): Promise<any> {
return api.post(`/shot-replications/segments/${recordId}/replication-projects`, params);
}
// 获取爆款开头复刻任务详情
export async function removeDetail(id: string): Promise<any> {
return api.get(`/shot-replications/projects/${id}`);
}
// 第一步,生成提示词
export async function removeone(projectId: string, stepId: string): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image-prompt`);
}
// 修改图片提示词
export async function updateShotImagePrompt(projectId: string, stepId: string, params: any): Promise<any> {
return api.put(`/shot-replications/projects/${projectId}/steps/${stepId}/image-prompt`, params);
}
// 修改视频提示词
export async function updateShotVideoPromptSchema(projectId: string, stepId: string, params: any): Promise<any> {
return api.put(`/shot-replications/projects/${projectId}/steps/${stepId}/video-prompt-schema`, params);
}
// 第二步,生成图片
export async function removetwo(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image`, params);
}
// 第三步,生成视频提示词
export async function removethree(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video-prompt`, params);
}
// 第四步,生成视频
export async function removefour(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video`, params);
}
// 获取地区信息
export interface GetAreaParams {
level?: string;
@@ -592,23 +573,21 @@ export async function getArea(params?: GetAreaParams): Promise<any> {
if (params?.parent_code) query.set('parent_code', params.parent_code);
return api.get(`/pre-test-template/getArea?${query.toString()}`);
}
// ── Upload Material ───────────────────────────────────────
export interface AsyncBatchUploadTask {
advertiser_ids: string[];
resource_ids: string[];
oauth_id: string;
is_pre_test: string;
pre_test_template: string;
source_model: string;
}
export interface AsyncBatchUploadParams {
tasks: AsyncBatchUploadTask[];
}
export async function asyncBatchUploadMaterial(params: AsyncBatchUploadParams): Promise<any> {
return api.post('/upload-material/async-batch-upload', params);
}
// 获取上传历史
// /api/upload-material/upload-history
export interface UploadHistoryParams {
@@ -624,7 +603,6 @@ export async function getUploadHistory(params: UploadHistoryParams): Promise<any
const query = searchParams.toString();
return api.get(`/upload-material/upload-history${query ? `?${query}` : ''}`);
}
export interface UploadFilenames {
source_id: string;
file_name: string;
@@ -636,7 +614,6 @@ export interface UpdateFilenameParams {
export async function updateFilename(params: UpdateFilenameParams): Promise<any> {
return api.post('/upload-material/batch-update-filename', params);
}
// 查询素材消耗列表
export interface MaterialConsumpListParams {
advertiser_id?: string;
@@ -652,7 +629,6 @@ export async function getMaterialConsumpList(params: MaterialConsumpListParams):
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/material-consumption/list?${query.toString()}`);
}
// 拉取消耗
export async function syncMaterialConsumption(params: { date: string; advertiser_id?: string }): Promise<any> {
const query = new URLSearchParams();
@@ -660,12 +636,10 @@ export async function syncMaterialConsumption(params: { date: string; advertiser
if (params.date) query.set('date', params.date);
return api.get(`/material-consumption/sync?${query.toString()}`);
}
// 获取消耗字段列表
export async function getMaterialConsumptionFields(): Promise<any> {
return api.get('/material-consumption/fields');
}
// ── Contact ────────────────────────────────────────────────
export interface ContactRequestParams {
phone: string;
@@ -674,11 +648,9 @@ export interface ContactRequestParams {
name: string;
message?: string;
}
export async function createContactRequest(params: ContactRequestParams): Promise<any> {
return api.post('/contact/request', params);
}
// 查询上传素材列表
export interface ResourcesMaterialListParams {
advertiser_id?: string;
+12 -2
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Button, Table, Modal, App, Input, Pagination, Typography, Space } from 'antd';
import { LockOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { getOAuthAccountList, deleteOAuthAccount, getOpenTypeAll } from '../api';
const formatDateTime = (dateStr: string) => {
@@ -27,6 +27,7 @@ interface AuthorizationData {
const AuthAccountPage: React.FC = () => {
const navigate = useNavigate();
const [searchParamsUrl] = useSearchParams();
const { message } = App.useApp();
const [authorizations, setAuthorizations] = useState<AuthorizationData[]>([]);
const [listLoading, setListLoading] = useState(false);
@@ -41,7 +42,13 @@ const AuthAccountPage: React.FC = () => {
const [openTypeMap, setOpenTypeMap] = useState<Record<number, string>>({});
useEffect(() => {
loadOAuthList();
const oauthId = searchParamsUrl.get('oauth_id');
if (oauthId) {
setSearchParams(prev => ({ ...prev, oauth_id: oauthId }));
loadOAuthList(1, 10, { ...searchParams, oauth_id: oauthId });
} else {
loadOAuthList();
}
loadOpenTypeList();
}, []);
@@ -245,6 +252,7 @@ const AuthAccountPage: React.FC = () => {
value={searchParams.advertiser_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
style={{ width: 180 }}
allowClear={true}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Input
@@ -252,6 +260,7 @@ const AuthAccountPage: React.FC = () => {
value={searchParams.oauth_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, oauth_id: e.target.value }))}
style={{ width: 180 }}
allowClear={true}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Input
@@ -259,6 +268,7 @@ const AuthAccountPage: React.FC = () => {
value={searchParams.advertiser_name}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_name: e.target.value }))}
style={{ width: 180 }}
allowClear={true}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Button
+14 -15
View File
@@ -2,15 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Button, Table, Tag, Modal, Select, App, Input, Pagination, Typography, Space } from 'antd';
import { useNavigate } from 'react-router-dom';
import { PlusOutlined, LockOutlined } from '@ant-design/icons';
import { getOAuthList, requestOAuth, getOpenTypeAll } from '../api';
const PORT_TYPE_MAP: Record<number, string> = {
1: '巨量',
2: '磁力',
3: '巨量星图',
4: '服务单',
5: '腾讯',
};
import { getOAuthList, requestOAuth, getOpenTypeAll, getDefaultPreTest } from '../api';
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
@@ -23,7 +15,6 @@ const formatDateTime = (dateStr: string) => {
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 安全拼接URL,避免双斜杠
const buildUrl = (path: string): string => {
const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000";
@@ -37,7 +28,6 @@ const buildUrl = (path: string): string => {
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
return `${cleanBase}/${cleanPath}`;
};
interface AuthorizationData {
id: string;
status: string;
@@ -79,8 +69,17 @@ const AuthorizationPage: React.FC = () => {
useEffect(() => {
loadOAuthList();
loadOpenTypeList();
list();
}, []);
const list = async () => {
try {
const res = await getDefaultPreTest();
console.log(res);
} catch (error) {
}
};
const loadOpenTypeList = async () => {
try {
const res = await getOpenTypeAll();
@@ -240,9 +239,9 @@ const AuthorizationPage: React.FC = () => {
),
},
{
title: '授权过期时间',
dataIndex: 'accessTokenExpired',
key: 'accessTokenExpired',
title: '授权有效时间',
dataIndex: 'refreshTokenExpired',
key: 'refreshTokenExpired',
width: 160,
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
},
@@ -263,7 +262,7 @@ const AuthorizationPage: React.FC = () => {
<>
<Button
type="link"
onClick={() => navigate(`/authacc?advertiserId=${record.advertiserId}`)}
onClick={() => navigate(`/authacc?oauth_id=${record.id}`)}
style={{ color: '#6366f1', padding: 0 }}
>
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Table, Tag, Button, Pagination, Typography, Modal, Checkbox, Input, DatePicker, App } from 'antd';
import { Table, Button, Pagination, Typography, Modal, Checkbox, Input, DatePicker, App } from 'antd';
import { ArrowLeftOutlined, DollarOutlined, SettingOutlined, SyncOutlined } from '@ant-design/icons';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { getMaterialConsumpList, getMaterialConsumptionFields, syncMaterialConsumption } from '../api';
@@ -1287,6 +1287,7 @@ const AIChatPage: React.FC = () => {
borderRadius: 16, color: '#64748b' }}>{msg.genType === 'image' ? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}` : `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`}</span>
<span style={{
// background: 'rgba(239, 68, 68, 0.08)',
fontSize: 14,
borderRadius: 16, color: '#ef4444', fontWeight: 500 }}>{msg.creditsCost}</span>
</div>
File diff suppressed because it is too large Load Diff
+67 -5
View File
@@ -14,7 +14,7 @@ import {
UploadOutlined,
} from '@ant-design/icons';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll } from '../api';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList } from '../api';
const { Search } = Input;
const { Text } = Typography;
@@ -68,6 +68,11 @@ const GeneratedRecord: React.FC = () => {
const [selectedHistoryAccounts, setSelectedHistoryAccounts] = useState<any[]>([]);
const [openTypeMap, setOpenTypeMap] = useState<Record<number, string>>({});
const [isPreTest, setIsPreTest] = useState<string>('2');
const [preTestTemplate, setPreTestTemplate] = useState<string>('');
const [preTestTemplates, setPreTestTemplates] = useState<any[]>([]);
const [preTestTemplatesLoading, setPreTestTemplatesLoading] = useState(false);
// 推送任务历史弹窗相关状态
const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false);
const [uploadHistoryList, setUploadHistoryList] = useState<any[]>([]);
@@ -669,6 +674,18 @@ const GeneratedRecord: React.FC = () => {
message.destroy('downloadProgress');
message.success(`下载完成,共 ${successCount} 个文件`);
};
const loadPreTestTemplates = async () => {
setPreTestTemplatesLoading(true);
try {
const res = await getPreTestList({ page: 1, pageSize: 100 });
setPreTestTemplates(res.data?.data || res.data || []);
} catch (error) {
console.error('加载前测模板失败:', error);
} finally {
setPreTestTemplatesLoading(false);
}
};
const handleBatchUploadSelected = () => {
if (selectedItems.size === 0) {
message.warning('请先选择要推送的媒体');
@@ -677,6 +694,9 @@ const GeneratedRecord: React.FC = () => {
setAccountIdLists([[]]);
setAccountIdInputs(['']);
setSelectedOauthItems([undefined]);
setIsPreTest('2');
setPreTestTemplate('');
loadPreTestTemplates();
setUploadConfigModalVisible(true);
};
const handleSinglePushToMedia = () => {
@@ -688,6 +708,9 @@ const GeneratedRecord: React.FC = () => {
setSelectedOauthItems([undefined]);
setMaterialFileNames(new Map());
setUnifiedFileName('');
setIsPreTest('2');
setPreTestTemplate('');
loadPreTestTemplates();
setUploadConfigModalVisible(true);
};
const loadOAuthList = async (page: number, pageSize: number) => {
@@ -779,6 +802,8 @@ const GeneratedRecord: React.FC = () => {
let tasks: {
advertiser_ids: string[];
resource_ids: string[];
is_pre_test: string;
pre_test_template: string;
oauth_id: string;
source_model: string;
}[] = [];
@@ -815,6 +840,8 @@ const GeneratedRecord: React.FC = () => {
resource_ids: resourceIds,
oauth_id: oauthItem.value,
source_model: sourceModel,
is_pre_test: isPreTest,
pre_test_template: preTestTemplate,
});
});
}
@@ -844,6 +871,8 @@ const GeneratedRecord: React.FC = () => {
resource_ids: resourceIds,
oauth_id: String(account.oauthId),
source_model: sourceModel,
is_pre_test: isPreTest,
pre_test_template: preTestTemplate,
});
});
}
@@ -868,6 +897,8 @@ const GeneratedRecord: React.FC = () => {
setSelectedHistoryAccounts([]);
setMaterialFileNames(new Map());
setUnifiedFileName('');
setIsPreTest('2');
setPreTestTemplate('');
} catch (error: any) {
console.error('批量推送失败:', error);
message.error(error.message || '批量推送失败');
@@ -1202,7 +1233,6 @@ const GeneratedRecord: React.FC = () => {
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}></Typography.Text>
<Space>
<Button
type={filterMedia === 'video' ? 'primary' : 'default'}
@@ -1512,7 +1542,41 @@ 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>
<Select
value={isPreTest}
onChange={(value) => {
setIsPreTest(value);
if (value !== '1') {
setPreTestTemplate('');
}
}}
style={{ width: 120 }}
options={[
{ value: '1', label: '是' },
{ value: '2', label: '否' },
]}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Typography.Text style={{ fontSize: 14, color: '#475569' }}></Typography.Text>
<Select
value={preTestTemplate}
onChange={(value) => setPreTestTemplate(value)}
style={{ width: 200 }}
disabled={isPreTest !== '1'}
loading={preTestTemplatesLoading}
placeholder={isPreTest !== '1' ? '请先开启前测' : '选择前测模板'}
options={preTestTemplates.map((item) => ({
value: item.id,
label: item.name,
}))}
/>
</div>
</div>
<Tabs
size="small"
activeKey={accountTab}
@@ -1533,7 +1597,6 @@ const GeneratedRecord: React.FC = () => {
},
]}
/>
{accountTab === 'new' && (
<div>
{selectedOauthItems.map((oauthItem, index) => (
@@ -1728,7 +1791,6 @@ const GeneratedRecord: React.FC = () => {
</Button>
</div>
)}
{accountTab === 'history' && (
<div style={{ padding: 8 }}>
{historyOAuthLoading ? (
+221 -195
View File
@@ -19,13 +19,6 @@ interface PreTestField {
description?: string;
}
const statusConfig = {
completed: { label: '已完成', color: 'success' },
pending: { label: '待测试', color: 'default' },
testing: { label: '测试中', color: 'processing' },
failed: { label: '测试失败', color: 'error' },
};
const PRETEST_FIELDS: PreTestField[] = [
{ id: 'AD_APP_ACTIVATE', name: 'AD_APP_ACTIVATE', label: '应用-激活', description: '应用激活行为' },
{ id: 'AD_APP_AUTH', name: 'AD_APP_AUTH', label: '应用-授信', description: '应用授信行为' },
@@ -129,12 +122,24 @@ const PRETEST_FIELDS: PreTestField[] = [
{ id: 'QC_PRODUCT_ROI', name: 'QC_PRODUCT_ROI', label: '商品投放-商品支付roi', description: '商品支付roi' },
];
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const columns = [
{
title: '前测模板ID',
title: '前测模板编号',
dataIndex: 'id',
key: 'id',
width: 110,
width: 120,
render: (text: string) => <Typography.Text strong style={{ color: '#1e293b', fontSize: 13 }}>{text}</Typography.Text>,
},
{
@@ -273,13 +278,13 @@ const columns = [
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{text}</Typography.Text>,
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{formatDateTime(text)}</Typography.Text>,
},
{
title: '操作',
dataIndex: 'operation',
fixed: 'right' as const,
key: 'operation',
width: 140,
render: (_: any, record: PreTestRecord) => (
<Space size={4}>
<Button
@@ -320,7 +325,6 @@ const PreTest: React.FC = () => {
setLoading(true);
try {
const res = await getPreTestList({ page: currentPage, pageSize });
console.log(res);
setRecords(res.data || []);
setTotal(res.pagination?.total || 0);
setCurrentPage(res.pagination?.page || 1);
@@ -514,122 +518,154 @@ const PreTest: React.FC = () => {
layout="vertical"
style={{ marginTop: 16 }}
>
<Form.Item
name="name"
label="模板名称"
rules={[{ required: true, message: '请输入模板名称' }]}
>
<Input
placeholder="请输入模板名称"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
name="platform"
label="平台"
initialValue="AD"
rules={[{ required: true, message: '请选择平台' }]}
>
<Select
placeholder="请选择平台"
style={{ borderRadius: 8, height: 40 }}
onChange={(value) => {
setPlatform(value);
}}
options={[
{ value: 'AD', label: 'AD' },
{ value: 'QIANCHUAN', label: '千川' },
{ value: 'LOCAL', label: '本地推' },
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
style={{ flex: 1 }}
name="name"
label="模板名称"
rules={[{ required: true, message: '请输入模板名称' }]}
>
<Input
placeholder="请输入模板名称"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="platform"
label="平台"
initialValue="AD"
rules={[{ required: true, message: '请选择平台' }]}
>
<Select
placeholder="请选择平台"
style={{ borderRadius: 8, height: 40 }}
onChange={(value) => {
setPlatform(value);
}}
options={[
{ value: 'AD', label: 'AD' },
{ value: 'QIANCHUAN', label: '千川' },
{ value: 'LOCAL', label: '本地推' },
]}
/>
</Form.Item>
</div>
{platform !== 'LOCAL' && (
<Form.Item
name="external_action"
label="转化目标"
>
<Select
mode="multiple"
placeholder="请选择转化目标(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
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' },
]}
/>
</Form.Item>
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
name="external_action"
label="转化目标"
>
<Select
mode="multiple"
placeholder="请选择转化目标(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
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>
<Form.Item
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
name="audience_gender"
label="受众性别"
>
<Select
placeholder="请选择受众性别"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: 'ALL', label: '不限' },
{ value: 'MALE', label: '男' },
{ value: 'FEMALE', label: '女' },
]}
/>
</Form.Item>
<Form.Item
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_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="受众地区"
@@ -644,53 +680,37 @@ const PreTest: React.FC = () => {
maxTagCount="responsive"
/>
</Form.Item>
<Form.Item
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>
<Form.Item
name="cus_name"
label="客户主体名称"
>
<Input
placeholder="请输入客户主体名称"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
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 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="是否最优成本出价"
valuePropName="checked"
@@ -706,6 +726,7 @@ const PreTest: React.FC = () => {
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="target_cost"
label="是否稳定成本出价"
valuePropName="checked"
@@ -721,6 +742,7 @@ const PreTest: React.FC = () => {
</Form.Item>
<Form.Item
style={{ flex: 1 }}
name="nobid"
label="是否最大转化出价"
valuePropName="checked"
@@ -734,38 +756,42 @@ const PreTest: React.FC = () => {
]}
/>
</Form.Item>
</>
</div>
)}
<Form.Item
name="cpc_bid"
label="目标点击成本"
getValueFromEvent={(e) => parseFloat(e.target.value) || undefined}
>
<Input
type="number"
placeholder="请输入目标点击成本"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<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
name="budget"
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={3}
rows={2}
style={{ borderRadius: 8 }}
/>
</Form.Item>
+29 -18
View File
@@ -471,30 +471,42 @@ export default function VideoFrameExtractor() {
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 4, height: 20, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{ fontSize: 16, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}></span>
<div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{
fontSize: 16,
fontWeight: 700,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}>
</span>
</div>
}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
width={800}
width={850}
footer={null}
style={{ borderRadius: 20 }}
styles={{
body: { background: 'linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%)' },
header: { background: '#fff', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', padding: '20px 24px' },
body: { height: 580, 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' },
}}
>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16, gap: 8 }}>
{/* 搜索区域 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', margin: '16px 24px', flexShrink: 0 }}>
<Input
placeholder="搜索产品名称"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleSearch}
style={{
width: 200,
width: 220,
borderRadius: 10,
border: '1px solid rgba(99, 102, 241, 0.2)',
height: 40,
marginRight: 10,
border: '1px solid rgba(99, 102, 241, 0.15)',
background: 'rgba(255,255,255,0.8)',
}}
/>
<Button
@@ -502,7 +514,6 @@ export default function VideoFrameExtractor() {
onClick={handleSearch}
style={{
borderRadius: 10,
height: 40,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none',
}}
@@ -511,16 +522,15 @@ export default function VideoFrameExtractor() {
</Button>
</div>
<div
style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%)',
backdropFilter: 'blur(20px)',
<div style={{ flex: 1, overflow: 'auto', padding: '0 24px 24px' }}>
<div style={{
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(10px)',
borderRadius: 16,
overflow: 'hidden',
border: '1px solid rgba(99, 102, 241, 0.1)',
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
}}
>
border: '1px solid rgba(99, 102, 241, 0.08)',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.06)',
}}>
<Table
columns={[
{
@@ -634,6 +644,7 @@ export default function VideoFrameExtractor() {
},
}}
/>
</div>
</div>
</Modal>
</div>