This commit is contained in:
2026-06-18 10:22:52 +08:00
5 changed files with 812 additions and 106 deletions
+2 -10
View File
@@ -17,24 +17,15 @@ import InitialReplication from './pages/InitialReplication';
import InitialInfo from './pages/InitialInfo';
import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord';
import PreTest from './pages/PreTest';
import AuthorizationPage from './pages/AuthorizationPage';
import RemoveInfo from './pages/RemoveInfo';
import RemoveRw from './pages/RemoveRw';
// import RemoveFenbu from './pages/RemoveFenbu';
import ConsumePage from './pages/ConsumePage';
import { useAuthStore } from './store/useAuthStore';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { user, loading, checkAuth } = useAuthStore();
useEffect(() => {
const token = localStorage.getItem('auth_token');
if (!token && !loading && !user) {
@@ -117,6 +108,7 @@ const App = () => {
<Route path="generated" element={<GeneratedRecord />} />
<Route path="pretest" element={<PreTest />} />
<Route path="authorization" element={<AuthorizationPage />} />
<Route path="consume" element={<ConsumePage />} />
</Route>
+89
View File
@@ -393,6 +393,95 @@ export async function getfour(projectId: string, stepId: string ,params: any): P
return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-video`, params);
}
// 获取授权链接
export interface RequestOAuthParams {
open_type: number;
}
export async function requestOAuth(params: RequestOAuthParams): Promise<any> {
return api.post(`/user-oauth/request_oauth`, params);
}
// 巨量授权回调
export interface JuliangCallbackParams {
auth_code: string;
state: string;
}
export async function juliang_callback(params: JuliangCallbackParams): Promise<any> {
const query = new URLSearchParams();
query.set('auth_code', params.auth_code);
query.set('state', params.state);
return api.get(`/user-oauth/juliang_callback?${query.toString()}`);
}
// 授权列表
export interface OAuthListParams {
account_userid?: string;
open_type?: number;
account_id?: string;
page?: number;
page_size?: number;
}
export async function getOAuthList(params: OAuthListParams): Promise<any> {
const query = new URLSearchParams();
if (params.account_userid) query.set('account_userid', params.account_userid);
if (params.open_type !== undefined) query.set('open_type', String(params.open_type));
if (params.account_id) query.set('account_id', params.account_id);
if (params.page !== undefined) query.set('page', String(params.page));
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/user-oauth/oauth_list?${query.toString()}`);
}
// 前测模板列表
// /api/pre-test-template/list
export interface PreTestListParams {
page?: number;
pageSize?: number;
}
export async function getPreTestList(params?: PreTestListParams): Promise<any> {
if (USE_MOCK) return mock.mockGetPreTestList(params);
const page = params?.page || 1;
const pageSize = Math.min(params?.pageSize || 10, 100);
return api.get(`/pre-test-template/list?page=${page}&page_size=${pageSize}`);
}
// 获取前测字段列表
// /api/pre-test-template/fields
export async function getPreTestFields(): Promise<any> {
if (USE_MOCK) return mock.mockGetPreTestFields();
return api.get(`/pre-test-template/fields`);
}
// 创建前测模板
// /api/pre-test-template/create
export async function createPreTest(params: any): Promise<any> {
if (USE_MOCK) return mock.mockCreatePreTest(params);
return api.post(`/pre-test-template/create`, params);
}
// 前测模板详情
// /api/pre-test-template/select/{template_id}
export async function getPreTestDetail(templateId: string): Promise<any> {
return api.get(`/pre-test-template/select/${templateId}`);
}
// 更新前测模板
// /api/pre-test-template/update/{template_id}
export async function updatePreTest(templateId: string, params: any): Promise<any> {
return api.post(`/pre-test-template/update/${templateId}`, params);
}
// 删除前测模板
// /api/pre-test-template/delete/{template_id}
export async function deletePreTest(templateId: string): Promise<any> {
return api.get(`/pre-test-template/delete/${templateId}`);
}
// 获取默认前测模板
// /api/pre-test-template/default
export async function getDefaultPreTest(): Promise<any> {
return api.get(`/pre-test-template/default`);
}
// 修改第四步视频 AI 提词 JSON schema
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);
+60
View File
@@ -383,3 +383,63 @@ export async function mockCreateGenerationTask(params: any): Promise<any> {
...params,
};
}
// ── PreTest Mock ────────────────────────────────────────────
const MOCK_PRETEST_FIELDS = [
{ id: 'title', name: 'title', label: '标题', description: '视频标题字段' },
{ id: 'description', name: 'description', label: '描述', description: '视频描述字段' },
{ id: 'image', name: 'image', label: '图片', description: '视频封面或配图' },
{ id: 'content', name: 'content', label: '内容', description: '视频正文内容' },
{ id: 'date', name: 'date', label: '日期', description: '活动或发布日期' },
{ id: 'step', name: 'step', label: '步骤', description: '操作步骤说明' },
{ id: 'duration', name: 'duration', label: '时长', description: '视频时长' },
{ id: 'brand', name: 'brand', label: '品牌', description: '品牌名称' },
{ id: 'slogan', name: 'slogan', label: '标语', description: '品牌宣传语' },
{ id: 'name', name: 'name', label: '姓名', description: '人物姓名' },
{ id: 'story', name: 'story', label: '故事', description: '故事内容' },
];
const MOCK_PRETEST_RECORDS = [
{ id: 'PT001', authorizationId: '1867060028363785', templateName: '产品介绍视频', selectedFields: ['title', 'description', 'image'], status: 'completed', createdAt: '2024-01-15 10:30:00' },
{ id: 'PT002', authorizationId: '1867060028363785', templateName: '活动宣传视频', selectedFields: ['title', 'content', 'date'], status: 'pending', createdAt: '2024-01-15 11:20:00' },
{ id: 'PT003', authorizationId: '1867059808785418', templateName: '教程演示视频', selectedFields: ['step', 'duration', 'image'], status: 'completed', createdAt: '2024-01-14 14:45:00' },
{ id: 'PT004', authorizationId: '1867060028363785', templateName: '品牌推广视频', selectedFields: ['brand', 'slogan'], status: 'testing', createdAt: '2024-01-14 09:15:00' },
{ id: 'PT005', authorizationId: '1867059757929740', templateName: '用户案例视频', selectedFields: ['name', 'story', 'image'], status: 'completed', createdAt: '2024-01-13 16:00:00' },
];
export async function mockGetPreTestFields(): Promise<any[]> {
await delay(300);
return [...MOCK_PRETEST_FIELDS];
}
export async function mockGetPreTestList(params?: { page?: number; pageSize?: number }): Promise<any> {
await delay(400);
const page = params?.page || 1;
const pageSize = Math.min(params?.pageSize || 10, 100);
const start = (page - 1) * pageSize;
const end = start + pageSize;
const items = MOCK_PRETEST_RECORDS.slice(start, end);
return {
page,
pageSize,
total: MOCK_PRETEST_RECORDS.length,
items,
};
}
export async function mockCreatePreTest(params: any): Promise<any> {
await delay(500);
const newRecord = {
id: `PT-${Date.now()}`,
authorizationId: '',
templateName: params.templateName,
selectedFields: params.selectedFields,
status: params.status || 'pending',
createdAt: new Date().toLocaleString('zh-CN'),
};
MOCK_PRETEST_RECORDS.unshift(newRecord);
return newRecord;
}
+227 -95
View File
@@ -1,16 +1,30 @@
import React, { useEffect, useState } from 'react';
import { Button, Table, Tag, message } from 'antd';
import { Button, Table, Tag, Modal, Select, App, Input } from 'antd';
import { PlusOutlined, CheckCircleOutlined, ClockCircleOutlined, CiCircleOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getOAuthList, juliang_callback, requestOAuth } from '../api';
const OPEN_TYPE_MAP: Record<number, string> = {
1: '千川',
2: '广告',
3: '本地推',
4: '星图',
5: '快手代理商',
6: '巨量星图',
7: '巨量服务单',
8: '腾讯服务单',
9: '腾讯营销K2',
10: '腾讯营销K3',
};
// 授权数据类型
interface AuthorizationData {
id: string;
status: string;
description: string;
account_userid?: string;
open_type?: number;
account_id?: string;
}
// 状态配置
const statusConfig = {
active: { label: '已授权', color: 'green', icon: CheckCircleOutlined },
pending: { label: '待授权', color: 'gold', icon: ClockCircleOutlined },
@@ -18,62 +32,72 @@ const statusConfig = {
revoked: { label: '已撤销', color: 'gray', icon: CiCircleOutlined },
};
interface ApiResponse {
code: number;
message: string;
data: AuthorizationData[];
}
// 模拟授权列表接口
const mockApiResponse: ApiResponse = {
code: 200,
message: 'success',
data: [
{ id: '1867060028363785', status: 'active', description: '用户张三的API授权' },
{ id: '1867059757929740', status: 'pending', description: '用户李四的API授权' },
{ id: '1867059808785418', status: 'active', description: '用户王五的API授权' },
{ id: '1867060028363786', status: 'expired', description: '用户赵六的API授权' },
{ id: '1867060028363787', status: 'revoked', description: '用户钱七的API授权' },
],
};
// 模拟调用接口 /api/admin/user-oauth-apps/list
const fetchAuthorizationList = async (): Promise<ApiResponse> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(mockApiResponse);
}, 800);
});
};
const AuthorizationPage: React.FC = () => {
const navigate = useNavigate();
const { message } = App.useApp();
const [authorizations, setAuthorizations] = useState<AuthorizationData[]>([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [listLoading, setListLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [selectedOpenType, setSelectedOpenType] = useState<number | undefined>(undefined);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [searchParams, setSearchParams] = useState({
account_userid: '',
open_type: undefined as number | undefined,
account_id: '',
});
// 页面初始化时获取授权列表
useEffect(() => {
const loadData = async () => {
setListLoading(true);
try {
const response = await fetchAuthorizationList();
if (response.code === 200) {
setAuthorizations(response.data);
} else {
message.error(response.message);
}
} catch (error) {
message.error('获取授权列表失败');
} finally {
setListLoading(false);
}
};
loadData();
loadOAuthList();
handleCallback();
}, []);
// 状态标签渲染
const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => {
setListLoading(true);
try {
const response = await getOAuthList({
page,
page_size: pageSize,
account_userid: params.account_userid || undefined,
open_type: params.open_type,
account_id: params.account_id || undefined,
});
if (response) {
if (response.data) {
setAuthorizations(response.data.data || response.data);
}
if (response.pagination) {
setTotal(response.pagination.total || 0);
setCurrentPage(response.pagination.page || 1);
setPageSize(response.pagination.pageSize || 10);
}
} else {
setAuthorizations(response || []);
}
} catch (error) {
message.error('获取授权列表失败');
} finally {
setListLoading(false);
}
};
const handleCallback = async () => {
const params = new URLSearchParams(window.location.search);
const authCode = params.get('auth_code');
const state = params.get('state');
if (authCode && state) {
try {
await juliang_callback({ auth_code: authCode, state });
message.success('授权成功');
window.history.replaceState({}, document.title, window.location.pathname);
loadOAuthList();
} catch (error) {
message.error('授权回调失败');
}
}
};
const renderStatus = (status: string) => {
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.expired;
const Icon = config.icon;
@@ -85,32 +109,74 @@ const AuthorizationPage: React.FC = () => {
);
};
// 跳转到消耗记录页面
const handleGoToConsume = () => {
navigate('/consume');
const handleAuthorize = () => {
console.log('handleAuthorize');
setShowModal(true);
};
// 处理点击授权按钮
const handleAuthorize = () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择需要授权的记录');
const handleConfirm = async () => {
if (!selectedOpenType) {
message.warning('请选择开户方式');
return;
}
setLoading(true);
// 模拟授权操作
setTimeout(() => {
setAuthorizations(prev =>
prev.map(item =>
selectedRowKeys.includes(item.id) ? { ...item, status: 'active' } : item
)
);
setSelectedRowKeys([]);
try {
const response = await requestOAuth({ open_type: selectedOpenType });
console.log(response);
if (response.authUrl) {
window.open(response.authUrl, '_blank');
} else {
message.error('获取授权链接失败');
}
} catch (error) {
message.error('请求授权失败');
} finally {
setLoading(false);
message.success(`成功授权 ${selectedRowKeys.length} 条记录`);
}, 800);
setShowModal(false);
setSelectedOpenType(undefined);
}
};
// 表格列配置
// account_id
// :
// "1836691333531724"
// account_name
// :
// "BP-1836691333531724"
// account_role
// :
// "CUSTOMER_ADMIN"
// account_userid
// :
// null
// account_username
// :
// "a***3@minzhong.cn"
// appid
// :
// "1866261617455433"
// created_at
// :
// "2026-06-15T10:00:22.734565Z"
// id
// :
// "0019ecab9b8bc57d964"
// material_auth_status
// :
// true
// open_type
// :
// 2
// port_type
// :
// 1
// updated_at
// :
// "2026-06-17T06:41:33.012018Z"
// user_id
// :
// "0019e96d23e57a808e2"
const columns = [
{
title: '序号',
@@ -128,6 +194,13 @@ const AuthorizationPage: React.FC = () => {
<span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span>
),
},
{
title: '开户方式',
dataIndex: 'open_type',
key: 'open_type',
width: 140,
render: (text: number) => <span style={{ color: '#1e293b' }}>{OPEN_TYPE_MAP[text] || text}</span>,
},
{
title: '授权状态',
dataIndex: 'status',
@@ -136,24 +209,12 @@ const AuthorizationPage: React.FC = () => {
render: (text: string) => renderStatus(text),
},
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: 120,
render: (_: any, record: AuthorizationData) => (
<Button
type="primary"
size="small"
style={{
borderRadius: 8,
border: 'none',
}}
onClick={handleGoToConsume}
>
</Button>
),
}
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
render: (text: string) => <span style={{ color: '#64748b' }}>{text}</span>,
},
];
const tableData = authorizations.map((item, index) => ({
@@ -163,16 +224,57 @@ const AuthorizationPage: React.FC = () => {
}));
return (
<div style={{ padding: 12, minHeight: '94vh', background: '#f8fafc' }}>
{/* 页面标题 */}
<div style={{ padding: 12, minHeight: '94vh', background: '#f8fafc', borderRadius: 12 }}>
<div>
<h1 style={{lineHeight: '28px', fontSize: 24, fontWeight: 600, color: '#1e293b', marginBottom: 8, marginTop: 0 }}>
<h1 style={{ lineHeight: '28px', fontSize: 24, fontWeight: 600, color: '#1e293b', marginBottom: 8, marginTop: 0 }}>
</h1>
</div>
{/* 操作栏 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<div style={{ display: 'flex', gap: 12 }}>
<Input
placeholder="账号用户ID"
value={searchParams.account_userid}
onChange={(e) => setSearchParams(prev => ({ ...prev, account_userid: e.target.value }))}
style={{ width: 180 }}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Select
placeholder="开户方式"
value={searchParams.open_type}
onChange={(value) => setSearchParams(prev => ({ ...prev, open_type: value }))}
style={{ width: 140 }}
options={Object.entries(OPEN_TYPE_MAP).map(([key, value]) => ({
value: Number(key),
label: value,
}))}
/>
<Input
placeholder="账号ID"
value={searchParams.account_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, account_id: e.target.value }))}
style={{ width: 180 }}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Button
type="primary"
size="medium"
onClick={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
>
</Button>
<Button
size="medium"
onClick={() => {
setSearchParams({ account_userid: '', open_type: undefined, account_id: '' });
setCurrentPage(1);
loadOAuthList(1, pageSize);
}}
>
</Button>
</div>
<Button
type="primary"
size="medium"
@@ -189,16 +291,22 @@ const AuthorizationPage: React.FC = () => {
</Button>
</div>
{/* 表格 */}
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
<Table
dataSource={tableData}
columns={columns}
loading={listLoading}
pagination={{
pageSize: 10,
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`,
onChange: (page, size) => {
setCurrentPage(page);
setPageSize(size);
loadOAuthList(page, size);
},
}}
rowKey="id"
bordered={false}
@@ -206,6 +314,30 @@ const AuthorizationPage: React.FC = () => {
scroll={{ x: 'max-content' }}
/>
</div>
<Modal
title="选择开户方式"
open={showModal}
onOk={handleConfirm}
onCancel={() => {
setShowModal(false);
setSelectedOpenType(undefined);
}}
okText="确认授权"
cancelText="取消"
confirmLoading={loading}
>
<Select
placeholder="请选择开户方式"
value={selectedOpenType}
onChange={(value) => setSelectedOpenType(value)}
style={{ width: '100%' }}
options={Object.entries(OPEN_TYPE_MAP).map(([key, value]) => ({
value: Number(key),
label: value,
}))}
/>
</Modal>
</div>
);
};
+433
View File
@@ -0,0 +1,433 @@
import React, { useState, useEffect } from 'react';
import { Table, Tag, Button, Typography, Space, Modal, Form, Input, Select, message } from 'antd';
import { PlusOutlined, EditOutlined } from '@ant-design/icons';
import { getPreTestList, createPreTest } from '../api';
interface PreTestRecord {
id: string;
authorizationId: string;
templateName: string;
selectedFields: string[];
status: string;
createdAt: string;
}
interface PreTestField {
id: string;
name: string;
label: string;
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: 'AD:应用-激活', description: '应用激活行为' },
{ id: 'AD_APP_AUTH', name: 'AD_APP_AUTH', label: 'AD:应用-授信', description: '应用授信行为' },
{ id: 'AD_APP_BOOK', name: 'AD_APP_BOOK', label: 'AD:应用-预约表单', description: '应用预约表单' },
{ id: 'AD_APP_BUY', name: 'AD_APP_BUY', label: 'AD:应用-APP内付费', description: 'APP内付费行为' },
{ id: 'AD_APP_CLICKS', name: 'AD_APP_CLICKS', label: 'AD:应用-点击量', description: '应用点击量' },
{ id: 'AD_APP_DETAIL', name: 'AD_APP_DETAIL', label: 'AD:应用-APP内详情页到站UV', description: 'APP内详情页到站UV' },
{ id: 'AD_APP_DOWNLOADED', name: 'AD_APP_DOWNLOADED', label: 'AD:应用-下载完成', description: '应用下载完成' },
{ id: 'AD_APP_INSTALLED', name: 'AD_APP_INSTALLED', label: 'AD:应用-安装完成', description: '应用安装完成' },
{ id: 'AD_APP_KEY_BEHAVIOR', name: 'AD_APP_KEY_BEHAVIOR', label: 'AD:应用-关键行为', description: '应用关键行为' },
{ id: 'AD_APP_ORDER', name: 'AD_APP_ORDER', label: 'AD:应用-APP内下单', description: 'APP内下单行为' },
{ id: 'AD_APP_PAY', name: 'AD_APP_PAY', label: 'AD:应用-付费', description: '应用付费行为' },
{ id: 'AD_APP_PRE_AUTH', name: 'AD_APP_PRE_AUTH', label: 'AD:应用-预授信', description: '应用预授信行为' },
{ id: 'AD_APP_PRE_DOWNLOAD', name: 'AD_APP_PRE_DOWNLOAD', label: 'AD:应用-预约下载', description: '应用预约下载' },
{ id: 'AD_APP_PUSH_ORDER', name: 'AD_APP_PUSH_ORDER', label: 'AD:应用-首次发单(乘客)', description: '首次发单行为' },
{ id: 'AD_APP_REGISTER', name: 'AD_APP_REGISTER', label: 'AD:应用-注册', description: '应用注册行为' },
{ id: 'AD_APP_SHOW', name: 'AD_APP_SHOW', label: 'AD:应用-展示量', description: '应用展示量' },
{ id: 'AD_APP_SUBMIT', name: 'AD_APP_SUBMIT', label: 'AD:应用-提交认证', description: '应用提交认证' },
{ id: 'AD_APP_VIEW', name: 'AD_APP_VIEW', label: 'AD:应用-APP内访问', description: 'APP内访问行为' },
{ id: 'AD_CLUE_AUTH', name: 'AD_CLUE_AUTH', label: 'AD:销售线索收集-授信', description: '销售线索授信' },
{ id: 'AD_CLUE_BUTTON', name: 'AD_CLUE_BUTTON', label: 'AD:销售线索收集-按钮跳转', description: '按钮跳转' },
{ id: 'AD_CLUE_CLICKS', name: 'AD_CLUE_CLICKS', label: 'AD:销售线索收集-点击量', description: '点击量' },
{ id: 'AD_CLUE_CONFIRM', name: 'AD_CLUE_CONFIRM', label: 'AD:销售线索收集-回访-信息确认', description: '回访信息确认' },
{ id: 'AD_CLUE_CONSULT', name: 'AD_CLUE_CONSULT', label: 'AD:销售线索收集-有效咨询', description: '有效咨询' },
{ id: 'AD_CLUE_CONSULT_MSG', name: 'AD_CLUE_CONSULT_MSG', label: 'AD:销售线索收集-留资咨询', description: '留资咨询' },
{ id: 'AD_CLUE_COUPON', name: 'AD_CLUE_COUPON', label: 'AD:销售线索收集-卡券领取', description: '卡券领取' },
{ id: 'AD_CLUE_CUSTOMER', name: 'AD_CLUE_CUSTOMER', label: 'AD:销售线索收集-有效获客', description: '有效获客' },
{ id: 'AD_CLUE_CVT', name: 'AD_CLUE_CVT', label: 'AD:销售线索收集-多转化', description: '多转化' },
{ id: 'AD_CLUE_DONE', name: 'AD_CLUE_DONE', label: 'AD:销售线索收集-完件', description: '完件' },
{ id: 'AD_CLUE_FORM', name: 'AD_CLUE_FORM', label: 'AD:销售线索收集-表单提交', description: '表单提交' },
{ id: 'AD_CLUE_FRIENDS', name: 'AD_CLUE_FRIENDS', label: 'AD:销售线索收集-回访-加为好友', description: '回访加为好友' },
{ id: 'AD_CLUE_INSURANCE', name: 'AD_CLUE_INSURANCE', label: 'AD:销售线索收集-保险支付', description: '保险支付' },
{ id: 'AD_CLUE_INTENTION', name: 'AD_CLUE_INTENTION', label: 'AD:销售线索收集-存在意向', description: '存在意向' },
{ id: 'AD_CLUE_INTENTION_FORM', name: 'AD_CLUE_INTENTION_FORM', label: 'AD:销售线索收集-意向表单', description: '意向表单' },
{ id: 'AD_CLUE_INTENTION_TEL', name: 'AD_CLUE_INTENTION_TEL', label: 'AD:销售线索收集-意向话单', description: '意向话单' },
{ id: 'AD_CLUE_MESSAGE', name: 'AD_CLUE_MESSAGE', label: 'AD:销售线索收集-私信消息', description: '私信消息' },
{ id: 'AD_CLUE_MONEY', name: 'AD_CLUE_MONEY', label: 'AD:销售线索收集-放款', description: '放款' },
{ id: 'AD_CLUE_MSG', name: 'AD_CLUE_MSG', label: 'AD:销售线索收集-私信留资', description: '私信留资' },
{ id: 'AD_CLUE_PAGE', name: 'AD_CLUE_PAGE', label: 'AD:销售线索收集-访问目标页面', description: '访问目标页面' },
{ id: 'AD_CLUE_PAY', name: 'AD_CLUE_PAY', label: 'AD:销售线索收集-付费', description: '付费' },
{ id: 'AD_CLUE_PRE_AUTH', name: 'AD_CLUE_PRE_AUTH', label: 'AD:销售线索收集-预授信', description: '预授信' },
{ id: 'AD_CLUE_PROTENTIAL_DEAL', name: 'AD_CLUE_PROTENTIAL_DEAL', label: 'AD:销售线索收集-回访-高潜成交', description: '回访高潜成交' },
{ id: 'AD_CLUE_PUSH_ORDER', name: 'AD_CLUE_PUSH_ORDER', label: 'AD:销售线索收集-首次发单(乘客)', description: '首次发单' },
{ id: 'AD_CLUE_REGISTER', name: 'AD_CLUE_REGISTER', label: 'AD:销售线索收集-注册', description: '注册' },
{ id: 'AD_CLUE_SHOW', name: 'AD_CLUE_SHOW', label: 'AD:销售线索收集-展示量', description: '展示量' },
{ id: 'AD_CLUE_SUBMIT', name: 'AD_CLUE_SUBMIT', label: 'AD:销售线索收集-提交认证', description: '提交认证' },
{ id: 'AD_CLUE_TEL', name: 'AD_CLUE_TEL', label: 'AD:销售线索收集-智能电话确认接通', description: '智能电话确认接通' },
{ id: 'AD_CLUE_TEL_CALL', name: 'AD_CLUE_TEL_CALL', label: 'AD:销售线索收集-电话接通', description: '电话接通' },
{ id: 'AD_CLUE_WX_ADD', name: 'AD_CLUE_WX_ADD', label: 'AD:销售线索收集-微信-添加企业微信', description: '添加企业微信' },
{ id: 'AD_CLUE_WX_COPY', name: 'AD_CLUE_WX_COPY', label: 'AD:销售线索收集-微信复制', description: '微信复制' },
{ id: 'AD_CLUE_WX_MSG', name: 'AD_CLUE_WX_MSG', label: 'AD:销售线索收集-微信-用户首次消息', description: '用户首次消息' },
{ id: 'AD_ECP_APP_BUY', name: 'AD_ECP_APP_BUY', label: 'AD:电商-app内下单', description: '电商app内下单' },
{ id: 'AD_ECP_APP_DETAIL', name: 'AD_ECP_APP_DETAIL', label: 'AD:电商-app内详情页到站uv', description: '电商app内详情页到站uv' },
{ id: 'AD_ECP_APP_VIEW', name: 'AD_ECP_APP_VIEW', label: 'AD:电商-app内访问', description: '电商app内访问' },
{ id: 'AD_ECP_BUTTON', name: 'AD_ECP_BUTTON', label: 'AD:电商-按钮跳转', description: '电商按钮跳转' },
{ id: 'AD_ECP_INTEREST', name: 'AD_ECP_INTEREST', label: 'AD:电商-引流电商种草', description: '引流电商种草' },
{ id: 'AD_ECP_SHOP', name: 'AD_ECP_SHOP', label: 'AD:电商-调起店铺', description: '调起店铺' },
{ id: 'AD_ECP_SHOP_STAY', name: 'AD_ECP_SHOP_STAY', label: 'AD:电商-店铺停留', description: '店铺停留' },
{ id: 'AD_MINIAPP_ACTIVATE', name: 'AD_MINIAPP_ACTIVATE', label: 'AD:快应用-激活', description: '快应用激活' },
{ id: 'AD_MINIAPP_KEY_BEHAVIOR', name: 'AD_MINIAPP_KEY_BEHAVIOR', label: 'AD:快应用-关键行为', description: '快应用关键行为' },
{ id: 'AD_MINIAPP_PAY', name: 'AD_MINIAPP_PAY', label: 'AD:快应用-付费', description: '快应用付费' },
{ id: 'AD_MINIAPP_REGISTER', name: 'AD_MINIAPP_REGISTER', label: 'AD:快应用-注册', description: '快应用注册' },
{ id: 'AD_NATIVE_ACTIVATE', name: 'AD_NATIVE_ACTIVATE', label: 'AD:原声互动-激活', description: '原声互动激活' },
{ id: 'AD_NATIVE_CLICKS', name: 'AD_NATIVE_CLICKS', label: 'AD:原声互动-组件点击', description: '组件点击' },
{ id: 'AD_NATIVE_FANS_GROUP', name: 'AD_NATIVE_FANS_GROUP', label: 'AD:原声互动-粉丝入群', description: '粉丝入群' },
{ id: 'AD_NATIVE_FOLLOW', name: 'AD_NATIVE_FOLLOW', label: 'AD:原声互动-帐号关注', description: '帐号关注' },
{ id: 'AD_NATIVE_INTERACTIVE', name: 'AD_NATIVE_INTERACTIVE', label: 'AD:原声互动-互动', description: '互动' },
{ id: 'AD_NATIVE_LIVE', name: 'AD_NATIVE_LIVE', label: 'AD:原声互动-预约直播', description: '预约直播' },
{ id: 'AD_NATIVE_LIVE_DONATE', name: 'AD_NATIVE_LIVE_DONATE', label: 'AD:原声互动-直播间营销捐赠', description: '直播间营销捐赠' },
{ id: 'AD_NATIVE_LIVE_PAY', name: 'AD_NATIVE_LIVE_PAY', label: 'AD:原声互动-直播间打赏', description: '直播间打赏' },
{ id: 'AD_NATIVE_LIVE_STAY', name: 'AD_NATIVE_LIVE_STAY', label: 'AD:原声互动-直播间停留', description: '直播间停留' },
{ id: 'AD_NATIVE_LIVE_VIEW', name: 'AD_NATIVE_LIVE_VIEW', label: 'AD:原声互动-直播间观看', description: '直播间观看' },
{ id: 'AD_NATIVE_PAY', name: 'AD_NATIVE_PAY', label: 'AD:原声互动-付费', description: '原声互动付费' },
{ id: 'AD_PRODUCT_ACTIVATE', name: 'AD_PRODUCT_ACTIVATE', label: 'AD:商品-激活', description: '商品激活' },
{ id: 'AD_PRODUCT_APP_BUY', name: 'AD_PRODUCT_APP_BUY', label: 'AD:商品-app内下单', description: '商品app内下单' },
{ id: 'AD_PRODUCT_APP_DETAIL', name: 'AD_PRODUCT_APP_DETAIL', label: 'AD:商品-app内详情页到站uv', description: '商品app内详情页到站uv' },
{ id: 'AD_PRODUCT_APP_PAY', name: 'AD_PRODUCT_APP_PAY', label: 'AD:商品-app内付费', description: '商品app内付费' },
{ id: 'AD_PRODUCT_APP_VIEW', name: 'AD_PRODUCT_APP_VIEW', label: 'AD:商品-app内访问', description: '商品app内访问' },
{ id: 'AD_PRODUCT_FORM', name: 'AD_PRODUCT_FORM', label: 'AD:商品-表单提交', description: '商品表单提交' },
{ id: 'AD_PRODUCT_KEY_BEHAVIOR', name: 'AD_PRODUCT_KEY_BEHAVIOR', label: 'AD:商品-关键行为', description: '商品关键行为' },
{ id: 'AD_PRODUCT_PAY', name: 'AD_PRODUCT_PAY', label: 'AD:商品-付费', description: '商品付费' },
{ id: 'AD_TINYAPP_ACTIVATE', name: 'AD_TINYAPP_ACTIVATE', label: '千川:小程序-激活', description: '小程序激活' },
{ id: 'AD_TINYAPP_KEY_BEHAVIOR', name: 'AD_TINYAPP_KEY_BEHAVIOR', label: '千川:小程序-关键行为', description: '小程序关键行为' },
{ id: 'AD_TINYAPP_PAY', name: 'AD_TINYAPP_PAY', label: '千川:小程序-付费', description: '小程序付费' },
{ id: 'QC_LIVE_BUY', name: 'QC_LIVE_BUY', label: '千川:直播投放-直播间下单', description: '直播间下单' },
{ id: 'QC_LIVE_CHECK', name: 'QC_LIVE_CHECK', label: '千川:直播投放-直播间结算', description: '直播间结算' },
{ id: 'QC_LIVE_COMMENTS', name: 'QC_LIVE_COMMENTS', label: '千川:直播投放-直播间评论', description: '直播间评论' },
{ id: 'QC_LIVE_DEAL', name: 'QC_LIVE_DEAL', label: '千川:直播投放-直播间成交', description: '直播间成交' },
{ id: 'QC_LIVE_ENTRY', name: 'QC_LIVE_ENTRY', label: '千川:直播投放-进入直播间', description: '进入直播间' },
{ id: 'QC_LIVE_FANS', name: 'QC_LIVE_FANS', label: '千川:直播投放-直播间粉丝提升', description: '直播间粉丝提升' },
{ id: 'QC_LIVE_HIT', name: 'QC_LIVE_HIT', label: '千川:直播投放-直播加热', description: '直播加热' },
{ id: 'QC_LIVE_PRODUCT_CLICKS', name: 'QC_LIVE_PRODUCT_CLICKS', label: '千川:直播投放-直播间商品点击', description: '直播间商品点击' },
{ id: 'QC_LIVE_ROI_CHECK', name: 'QC_LIVE_ROI_CHECK', label: '千川:直播投放-结算roi', description: '结算roi' },
{ id: 'QC_LIVE_ROI_DEAL', name: 'QC_LIVE_ROI_DEAL', label: '千川:直播投放-支付roi-直播间成交', description: '支付roi直播间成交' },
{ id: 'QC_LIVE_ROI_QC', name: 'QC_LIVE_ROI_QC', label: '千川:直播投放-支付roi-千川直接+间接订单', description: '支付roi千川订单' },
{ id: 'QC_PRODUCT_BUY', name: 'QC_PRODUCT_BUY', label: '千川:商品投放-商品购买', description: '商品购买' },
{ id: 'QC_PRODUCT_COMMENTS', name: 'QC_PRODUCT_COMMENTS', label: '千川:商品投放-点赞评论', description: '点赞评论' },
{ id: 'QC_PRODUCT_FANS', name: 'QC_PRODUCT_FANS', label: '千川:商品投放-粉丝提升', description: '粉丝提升' },
{ id: 'QC_PRODUCT_INTEREST', name: 'QC_PRODUCT_INTEREST', label: '千川:商品投放-人群种草', description: '人群种草' },
{ id: 'QC_PRODUCT_QC', name: 'QC_PRODUCT_QC', label: '千川:商品投放-千川直接+间接订单', description: '千川订单' },
{ id: 'QC_PRODUCT_ROI', name: 'QC_PRODUCT_ROI', label: '千川:商品投放-商品支付roi', description: '商品支付roi' },
];
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 70,
render: (text: number) => <Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>{text}</Typography.Text>,
},
{
title: '前测模板ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
width: 100,
render: (text: string) => <Typography.Text strong style={{ color: '#1e293b', fontSize: 13 }}>{text}</Typography.Text>,
},
{
title: '模板名称',
dataIndex: 'templateName',
key: 'templateName',
ellipsis: true,
width: 150,
render: (text: string) => (
<span style={{ color: '#1e293b', fontWeight: 500, fontSize: 13 }}>{text}</span>
),
},
{
title: '选中的字段',
dataIndex: 'selectedFields',
key: 'selectedFields',
render: (fields: string[]) => (
<Space wrap size={6}>
{fields.map((field) => {
const fieldInfo = PRETEST_FIELDS.find(f => f.name === field);
return (
<Tag
key={field}
style={{
background: 'rgba(99,102,241,0.08)',
color: '#6366f1',
border: 'none',
borderRadius: 6,
fontSize: 11,
padding: '2px 8px',
}}
>
{fieldInfo?.label || field}
</Tag>
);
})}
</Space>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (text: string) => {
const config = statusConfig[text as keyof typeof statusConfig] || statusConfig.pending;
return (
<Tag color={config.color} style={{ borderRadius: 6, fontSize: 12, fontWeight: 500 }}>
{config.label}
</Tag>
);
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{text}</Typography.Text>,
},
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: 140,
render: (_: any, record: PreTestRecord) => (
<Space size={4}>
<Button
type="text"
size="small"
icon={<EditOutlined />}
style={{ color: '#6366f1', fontSize: 12 }}
>
</Button>
</Space>
),
},
];
const PreTest: React.FC = () => {
const [records, setRecords] = useState<PreTestRecord[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [submitLoading, setSubmitLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
useEffect(() => {
loadRecords();
}, [currentPage, pageSize]);
const loadRecords = async () => {
setLoading(true);
try {
const data = await getPreTestList({ page: currentPage, pageSize });
setRecords(data.items || []);
setTotal(data.total || 0);
} catch (error) {
message.error('获取前测列表失败');
} finally {
setLoading(false);
}
};
const tableData = records.map((item, index) => ({
...item,
index: (currentPage - 1) * pageSize + index + 1,
key: item.id,
}));
const handleOpenModal = () => {
form.resetFields();
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
form.resetFields();
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setSubmitLoading(true);
const result = await createPreTest({
templateName: values.templateName,
selectedFields: values.selectedFields,
status: values.status,
});
message.success('前测模板创建成功');
setModalOpen(false);
form.resetFields();
loadRecords();
} catch (error) {
message.error('创建失败,请重试');
} finally {
setSubmitLoading(false);
}
};
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
};
return (
<div style={{ minHeight: 'calc(100vh - 80px)', background: '#f1f5f9', padding: 12, borderRadius: 12 }}>
<div>
<h1 style={{ lineHeight: '32px', fontSize: 26, fontWeight: 700, color: '#1e293b', marginBottom: 4, marginTop: 0 }}>
</h1>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginBottom: 16 }}>
<Button
type="primary"
size="medium"
icon={<PlusOutlined />}
loading={loading}
onClick={handleOpenModal}
style={{
borderRadius: 12,
fontSize: 14,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none',
boxShadow: '0 4px 14px rgba(99,102,241,0.3)',
}}
>
</Button>
</div>
<div style={{
background: '#fff',
borderRadius: 16,
boxShadow: '0 4px 20px rgba(0,0,0,0.04)',
overflow: 'hidden',
}}>
<Table
dataSource={tableData}
columns={columns}
loading={loading}
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100'],
onChange: handlePageChange,
style: { padding: '16px 24px', borderTop: '1px solid #f1f5f9' },
}}
rowKey="id"
bordered={false}
style={{ padding: 0 }}
scroll={{ x: 'max-content' }}
onRow={() => ({
style: {
cursor: 'pointer',
transition: 'background-color 0.15s',
},
onMouseEnter: (e: React.MouseEvent) => {
(e.currentTarget as HTMLElement).style.backgroundColor = '#f8fafc';
},
onMouseLeave: (e: React.MouseEvent) => {
(e.currentTarget as HTMLElement).style.backgroundColor = 'transparent';
},
})}
/>
</div>
<Modal
title={<Space><PlusOutlined /></Space>}
open={modalOpen}
onCancel={handleCloseModal}
onOk={handleSubmit}
okText="提交"
cancelText="取消"
width={520}
confirmLoading={submitLoading}
style={{ borderRadius: 16 }}
>
<Form
form={form}
layout="vertical"
style={{ marginTop: 16 }}
>
<Form.Item
name="templateName"
label="模板名称"
rules={[{ required: true, message: '请输入模板名称' }]}
>
<Input
placeholder="请输入模板名称"
style={{ borderRadius: 8, height: 40 }}
/>
</Form.Item>
<Form.Item
name="selectedFields"
label="前测字段"
rules={[{ required: true, message: '请选择前测字段' }]}
>
<Select
mode="multiple"
placeholder="请选择前测字段(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
options={PRETEST_FIELDS.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="status"
label="状态"
initialValue="pending"
>
<Select
placeholder="请选择状态"
style={{ borderRadius: 8, height: 40 }}
options={[
{ value: 'pending', label: '待测试' },
{ value: 'testing', label: '测试中' },
]}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default PreTest;