Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user