前测功能完善详情编辑删除

This commit is contained in:
Lrd
2026-06-30 14:47:51 +08:00
parent 72a1b6e2b0
commit 5cd5d9f24f
5 changed files with 1168 additions and 188 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-1NS8asx8.js"></script>
<script type="module" crossorigin src="/assets/index-BVNhLxFb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D3IaMBsq.css">
</head>
<body>
+9 -2
View File
@@ -250,8 +250,15 @@ const MaterialListPage: React.FC = () => {
dataIndex: 'note',
key: 'note',
width: 150,
ellipsis: true,
render: (text: string) => <span style={{ color: '#94a3b8' }}>{text || '-'}</span>,
render: (text: string) => (
<Input.TextArea
value={text || ''}
readOnly
autoSize={{ minRows: 1, maxRows: 4 }}
style={{ color: '#94a3b8', resize: 'none', border: 'none', background: 'transparent', padding: 0 }}
placeholder="-"
/>
),
},
{
title: '用户ID',
+274 -185
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Table, Tag, Button, Typography, Space, Modal, Form, Input, Select, Cascader, App, Pagination } from 'antd';
import { PlusOutlined, EditOutlined, FileTextOutlined } from '@ant-design/icons';
import { getPreTestList, createPreTest, getArea } from '../api';
import { PlusOutlined, EditOutlined, FileTextOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import { getPreTestList, createPreTest, getArea, getPreTestDetail, updatePreTest, deletePreTest } from '../api';
interface PreTestRecord {
id: string;
@@ -134,178 +134,14 @@ const formatDateTime = (dateStr: string) => {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const columns = [
{
title: '前测模板编号',
dataIndex: 'id',
key: 'id',
width: 120,
render: (text: string) => <Typography.Text strong style={{ color: '#1e293b', fontSize: 13 }}>{text}</Typography.Text>,
},
{
title: '模板名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
render: (text: string) => (
<span style={{ color: '#1e293b', fontWeight: 500, fontSize: 13 }}>{text}</span>
),
},
{
title: '平台',
dataIndex: 'platform',
key: 'platform',
render: (text: string) => (
<Tag color={text === 'AD' ? 'blue' : text === 'QIANCHUAN' ? 'green' : 'orange'} style={{ borderRadius: 6, fontSize: 12 }}>
{text === 'AD' ? 'AD' : text === 'QIANCHUAN' ? '千川' : '本地推'}
</Tag>
),
},
{
title: '转化目标',
dataIndex: 'externalAction',
key: 'externalAction',
ellipsis: true,
width: 200,
render: (text: string) => {
if (!text) return <span style={{ color: '#94a3b8', fontSize: 13 }}>-</span>;
const actionArray = text.split(',').filter(item => item.trim());
const labels = actionArray.map(action => {
const field = PRETEST_FIELDS.find(f => f.name === action.trim());
return field ? field.label : action;
});
return (
<span style={{ color: '#1e293b', fontSize: 13 }}>{labels.join(', ')}</span>
);
},
},
{
title: '目标转化成本',
dataIndex: 'cpaBid',
key: 'cpaBid',
width: 120,
render: (text: number) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
),
},
{
title: '受众性别',
dataIndex: 'audienceGender',
key: 'audienceGender',
width: 90,
render: (text: string) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text === 'ALL' ? '不限' : text === 'MALE' ? '男' : text === 'FEMALE' ? '女' : text || '-'}</span>
),
},
{
title: '受众年龄',
dataIndex: 'audienceAge',
key: 'audienceAge',
width: 90,
render: (text: string[]) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
),
},
{
title: '受众地区',
dataIndex: 'audience_region',
key: 'audience_region',
ellipsis: true,
width: 150,
render: (text: string[]) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
),
},
{
title: '网络类型',
dataIndex: 'audienceNetwork',
key: 'audienceNetwork',
width: 90,
render: (text: string[]) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
),
},
{
title: '客户主体名称',
dataIndex: 'cusName',
key: 'cusName',
ellipsis: true,
width: 120,
render: (text: string) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
),
},
{
title: '出价类型',
dataIndex: 'pricingType',
key: 'pricingType',
width: 90,
render: (text: string) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
),
},
{
title: '目标点击成本',
dataIndex: 'cpcBid',
key: 'cpcBid',
width: 120,
render: (text: number) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
),
},
{
title: '预算金额',
dataIndex: 'budget',
key: 'budget',
width: 100,
render: (text: number) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
),
},
{
title: '默认模板',
dataIndex: 'isDefault',
key: 'isDefault',
width: 90,
render: (text: boolean) => (
<Tag color={text ? 'green' : 'default'} style={{ borderRadius: 6, fontSize: 12 }}>
{text ? '是' : '否'}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{formatDateTime(text)}</Typography.Text>,
},
{
title: '操作',
dataIndex: 'operation',
fixed: 'right' as const,
key: 'operation',
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 { message } = App.useApp();
const [records, setRecords] = useState<PreTestRecord[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit' | 'detail'>('create');
const [currentRecord, setCurrentRecord] = useState<PreTestRecord | null>(null);
const [form] = Form.useForm();
const [submitLoading, setSubmitLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
@@ -353,7 +189,6 @@ const PreTest: React.FC = () => {
const loadCityOptions = async (selectedOptions: any) => {
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
try {
const res = await getArea({ level: 'TWO_LEVEL', parent_code: targetOption.value });
targetOption.children = (res.data || []).map((item: any) => ({
@@ -376,12 +211,263 @@ const PreTest: React.FC = () => {
key: item.id,
}));
const columns = [
{
title: '前测模板编号',
dataIndex: 'id',
key: 'id',
width: 120,
render: (text: string) => <Typography.Text strong style={{ color: '#1e293b', fontSize: 13 }}>{text}</Typography.Text>,
},
{
title: '模板名称',
dataIndex: 'name',
key: 'name',
ellipsis: true,
render: (text: string) => (
<span style={{ color: '#1e293b', fontWeight: 500, fontSize: 13 }}>{text}</span>
),
},
{
title: '平台',
dataIndex: 'platform',
key: 'platform',
render: (text: string) => (
<Tag color={text === 'AD' ? 'blue' : text === 'QIANCHUAN' ? 'green' : 'orange'} style={{ borderRadius: 6, fontSize: 12 }}>
{text === 'AD' ? 'AD' : text === 'QIANCHUAN' ? '千川' : '本地推'}
</Tag>
),
},
{
title: '转化目标',
dataIndex: 'externalAction',
key: 'externalAction',
ellipsis: true,
width: 200,
render: (text: string) => {
if (!text) return <span style={{ color: '#94a3b8', fontSize: 13 }}>-</span>;
const field = PRETEST_FIELDS.find(f => f.name === text.trim());
return (
<span style={{ color: '#1e293b', fontSize: 13 }}>{field ? field.label : text}</span>
);
},
},
{
title: '目标转化成本',
dataIndex: 'cpaBid',
key: 'cpaBid',
width: 120,
render: (text: number) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
),
},
{
title: '受众性别',
dataIndex: 'audienceGender',
key: 'audienceGender',
width: 90,
render: (text: string) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text === 'ALL' ? '不限' : text === 'MALE' ? '男' : text === 'FEMALE' ? '女' : text || '-'}</span>
),
},
{
title: '受众年龄',
dataIndex: 'audienceAge',
key: 'audienceAge',
width: 90,
render: (text: string[]) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
),
},
{
title: '受众地区',
dataIndex: 'audience_region',
key: 'audience_region',
ellipsis: true,
width: 150,
render: (text: string[]) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
),
},
{
title: '网络类型',
dataIndex: 'audienceNetwork',
key: 'audienceNetwork',
width: 90,
render: (text: string[]) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{Array.isArray(text) ? text.join(', ') : text || '-'}</span>
),
},
{
title: '客户主体名称',
dataIndex: 'cusName',
key: 'cusName',
ellipsis: true,
width: 120,
render: (text: string) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
),
},
{
title: '出价类型',
dataIndex: 'pricingType',
key: 'pricingType',
width: 90,
render: (text: string) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text || '-'}</span>
),
},
{
title: '目标点击成本',
dataIndex: 'cpcBid',
key: 'cpcBid',
width: 120,
render: (text: number) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
),
},
{
title: '预算金额',
dataIndex: 'budget',
key: 'budget',
width: 100,
render: (text: number) => (
<span style={{ color: '#1e293b', fontSize: 13 }}>{text !== undefined ? text : '-'}</span>
),
},
{
title: '默认模板',
dataIndex: 'isDefault',
key: 'isDefault',
width: 90,
render: (text: boolean) => (
<Tag color={text ? 'green' : 'default'} style={{ borderRadius: 6, fontSize: 12 }}>
{text ? '是' : '否'}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>{formatDateTime(text)}</Typography.Text>,
},
{
title: '操作',
dataIndex: 'operation',
fixed: 'right' as const,
key: 'operation',
render: (_: any, record: PreTestRecord) => (
<Space size={4}>
<Button
type="text"
size="small"
icon={<EyeOutlined />}
style={{ color: '#6366f1', fontSize: 12 }}
onClick={() => handleDetail(record)}
>
</Button>
<Button
type="text"
size="small"
icon={<EditOutlined />}
style={{ color: '#6366f1', fontSize: 12 }}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
style={{ color: '#ef4444', fontSize: 12 }}
onClick={() => handleDelete(record)}
>
</Button>
</Space>
),
},
];
const handleOpenModal = () => {
form.resetFields();
setPlatform('AD');
setModalMode('create');
setCurrentRecord(null);
setModalOpen(true);
};
const fillFormWithDetail = (data: any) => {
setCurrentRecord(data);
setPlatform(data.platform || 'AD');
const regionCodes = data.audience_region || [];
const regionPaths = regionCodes.map((code: any) => [String(code)]);
form.setFieldsValue({
name: data.name,
note: data.note,
platform: data.platform,
external_action: data.externalAction,
cpa_bid: data.cpaBid,
audience_gender: data.audienceGender,
audience_age: data.audienceAge || [],
audience_region: regionPaths,
audience_network: data.audienceNetwork || [],
cus_name: data.cusName,
pricing_type: data.pricingType,
cost_cap: data.costCap !== undefined ? data.costCap : false,
target_cost: data.targetCost !== undefined ? data.targetCost : false,
nobid: data.nobid !== undefined ? data.nobid : false,
cpc_bid: data.cpcBid,
budget: data.budget,
is_default: data.isDefault !== undefined ? data.isDefault : false,
});
};
const handleDetail = async (record: PreTestRecord) => {
try {
const res = await getPreTestDetail(record.id);
const data = res.data;
setModalMode('detail');
fillFormWithDetail(data);
setModalOpen(true);
} catch (error) {
message.error('获取详情失败');
}
};
const handleEdit = async (record: PreTestRecord) => {
try {
const res = await getPreTestDetail(record.id);
const data = res.data;
setModalMode('edit');
fillFormWithDetail(data);
setModalOpen(true);
} catch (error) {
message.error('获取详情失败');
}
};
const handleDelete = (record: PreTestRecord) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该前测模板吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await deletePreTest(record.id);
message.success('删除成功');
loadRecords();
} catch (error) {
message.error('删除失败');
}
},
});
};
const handleCloseModal = () => {
setModalOpen(false);
form.resetFields();
@@ -393,12 +479,11 @@ const PreTest: React.FC = () => {
const values = await form.validateFields();
setSubmitLoading(true);
const regionCodes = (values.audience_region || []).map((path: string[]) => path[path.length - 1]);
const externalActionStr = (values.external_action || []).join(',');
const result = await createPreTest({
const params = {
name: values.name,
note: values.note,
platform: values.platform,
external_action: externalActionStr,
external_action: values.external_action,
cpa_bid: values.cpa_bid || 0,
audience_gender: values.audience_gender,
audience_age: values.audience_age || [],
@@ -412,9 +497,17 @@ const PreTest: React.FC = () => {
cpc_bid: values.cpc_bid || 0,
budget: values.budget || 0,
is_default: values.is_default || false,
});
};
let result;
if (modalMode === 'edit' && currentRecord) {
result = await updatePreTest(currentRecord.id, params);
message.success('前测模板更新成功');
} else {
result = await createPreTest(params);
message.success('前测模板创建成功');
}
message.success('前测模板创建成功');
setModalOpen(false);
form.resetFields();
loadRecords();
@@ -424,7 +517,7 @@ const PreTest: React.FC = () => {
} else if (error?.response?.data?.message) {
message.error(error.response.data.message);
} else {
message.error('创建失败,请重试');
message.error(modalMode === 'edit' ? '更新失败,请重试' : '创建失败,请重试');
}
} finally {
setSubmitLoading(false);
@@ -502,21 +595,23 @@ const PreTest: React.FC = () => {
</div>
<Modal
title={<Space></Space>}
title={<Space>{modalMode === 'create' ? '新增前测模板' : modalMode === 'edit' ? '编辑前测模板' : '前测模板详情'}</Space>}
open={modalOpen}
onCancel={handleCloseModal}
onOk={handleSubmit}
okText="提交"
okText={modalMode === 'detail' ? '' : '提交'}
cancelText="取消"
width={720}
height={600}
confirmLoading={submitLoading}
style={{ borderRadius: 16 }}
footer={modalMode === 'detail' ? null : undefined}
>
<Form
form={form}
layout="vertical"
style={{ marginTop: 16 }}
disabled={modalMode === 'detail'}
>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
@@ -557,10 +652,8 @@ const PreTest: React.FC = () => {
label="转化目标"
>
<Select
mode="multiple"
placeholder="请选择转化目标(可多选)"
style={{ borderRadius: 8, minHeight: 40 }}
maxTagCount="responsive"
placeholder="请选择转化目标"
style={{ borderRadius: 8, height: 40 }}
options={PRETEST_FIELDS.filter(field => {
if (platform === 'AD') {
return field.name.startsWith('AD_');
@@ -713,7 +806,6 @@ const PreTest: React.FC = () => {
style={{ flex: 1 }}
name="cost_cap"
label="是否最优成本出价"
valuePropName="checked"
>
<Select
placeholder="是否最优成本出价"
@@ -729,7 +821,6 @@ const PreTest: React.FC = () => {
style={{ flex: 1 }}
name="target_cost"
label="是否稳定成本出价"
valuePropName="checked"
>
<Select
placeholder="是否稳定成本出价"
@@ -745,7 +836,6 @@ const PreTest: React.FC = () => {
style={{ flex: 1 }}
name="nobid"
label="是否最大转化出价"
valuePropName="checked"
>
<Select
placeholder="是否最大转化出价"
@@ -799,7 +889,6 @@ const PreTest: React.FC = () => {
name="is_default"
label="默认模板"
initialValue={false}
valuePropName="checked"
>
<Select
placeholder="请选择默认模板"