Commit 4f43ce9b authored by hzl's avatar hzl

feat: 批量新建计划

parent d53d8e3b
...@@ -269,7 +269,7 @@ ...@@ -269,7 +269,7 @@
<el-form-item <el-form-item
label="ROAS目标值" label="ROAS目标值"
prop="tiktok_json.roasBid" prop="tiktok_json.roasBid"
v-if="form.tiktok_json.optimizationGoal === 'VALUE' && form.tiktok_json.deepBidType === 'VO_MIN_ROAS'"> v-if="form.tiktok_json.optimizationGoal === 'VALUE' && form.tiktok_json.deepBidType === 'VO_MIN_ROAS' && !hasMultiCampaignConfig">
<el-input-number <el-input-number
v-model="form.tiktok_json.roasBid" v-model="form.tiktok_json.roasBid"
:min="0.01" :min="0.01"
...@@ -306,10 +306,26 @@ ...@@ -306,10 +306,26 @@
<el-switch <el-switch
v-model="form.tiktok_json.isNewCampaign" v-model="form.tiktok_json.isNewCampaign"
active-text="是" active-text="是"
inactive-text="否"> inactive-text="否"
@change="handleNewCampaignChange">
</el-switch> </el-switch>
</el-form-item> </el-form-item>
<!-- 新建计划个数输入框 -->
<el-form-item
label="新建计划个数"
prop="tiktok_json.newCampaignCount"
v-if="form.tiktok_json.isNewCampaign"
class="new-campaign-count-input">
<el-input-number
v-model="form.tiktok_json.newCampaignCount"
:min="1"
:max="10"
placeholder="请输入新建计划个数"
@change="handleNewCampaignCountChange">
</el-input-number>
</el-form-item>
<template v-if="!form.tiktok_json.isNewCampaign"> <template v-if="!form.tiktok_json.isNewCampaign">
<el-form-item <el-form-item
label="选择已有计划" label="选择已有计划"
...@@ -336,12 +352,13 @@ ...@@ -336,12 +352,13 @@
</template> </template>
<!-- 多计划独立配置区域 --> <!-- 多计划独立配置区域 -->
<div v-if="!form.tiktok_json.isNewCampaign && form.tiktok_json.campaignIdList && form.tiktok_json.campaignIdList.length > 0" class="multi-campaign-config"> <div v-if="form.platform === 2 && ((!form.tiktok_json.isNewCampaign && form.tiktok_json.campaignIdList && form.tiktok_json.campaignIdList.length > 0) || (form.tiktok_json.isNewCampaign && form.tiktok_json.newCampaignCount > 0))" class="multi-campaign-config">
<el-divider content-position="left">计划独立配置</el-divider> <el-divider content-position="left">计划独立配置</el-divider>
<div class="campaign-config-list"> <div class="campaign-config-list">
<!-- 已有计划配置 -->
<div <div
v-for="(campaignId, index) in form.tiktok_json.campaignIdList" v-for="(campaignId, index) in form.tiktok_json.campaignIdList"
:key="campaignId" :key="'existing-' + campaignId"
class="campaign-config-item"> class="campaign-config-item">
<div class="campaign-config-header"> <div class="campaign-config-header">
<h4>{{ getCampaignName(campaignId) }}</h4> <h4>{{ getCampaignName(campaignId) }}</h4>
...@@ -409,10 +426,6 @@ ...@@ -409,10 +426,6 @@
<title-group-selector v-model="campaignConfigs[index].titleGroups" /> <title-group-selector v-model="campaignConfigs[index].titleGroups" />
</el-form-item> </el-form-item>
<!-- 素材组 -->
<el-form-item label="素材组">
<material-group-selector v-model="campaignConfigs[index].materialGroups" />
</el-form-item>
<!-- 资源组 --> <!-- 资源组 -->
<el-form-item label="资源组"> <el-form-item label="资源组">
...@@ -420,6 +433,87 @@ ...@@ -420,6 +433,87 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<!-- 新建计划配置 -->
<div
v-for="(newCampaign, index) in newCampaignConfigs"
:key="'new-' + index"
class="campaign-config-item">
<div class="campaign-config-header">
<h4>新建计划 {{ index + 1 }} (共 {{ newCampaignConfigs.length }} 个)</h4>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeNewCampaignConfig(index)"
v-if="newCampaignConfigs.length > 1">
移除
</el-button>
</div>
<el-form :model="newCampaign" label-width="120px" size="small">
<!-- ROAS目标值 -->
<el-form-item label="ROAS目标值" v-if="form.tiktok_json.optimizationGoal === 'VALUE' && form.tiktok_json.deepBidType === 'VO_MIN_ROAS'">
<el-input-number
v-model="newCampaign.roasBid"
:min="0.01"
:max="1000"
:precision="2"
placeholder="请输入ROAS目标值"
style="width: 200px;"
@change="markNewCampaignAsModified(index)">
</el-input-number>
</el-form-item>
<!-- 日预算 -->
<el-form-item label="日预算($)">
<el-input-number
v-model="newCampaign.dailyBudget"
:min="20"
placeholder="请输入日预算"
style="width: 200px;"
@change="markNewCampaignAsModified(index)">
</el-input-number>
</el-form-item>
<!-- 地域组 -->
<el-form-item label="地域组">
<location-group-selector
v-model="newCampaign.locationGroups"
@change="handleNewCampaignLocationGroupsChange(index, $event)" />
<!-- 展示已选地域组列表 -->
<div v-if="newCampaign.locationGroups && newCampaign.locationGroups.length > 0" class="group-detail-container">
<div class="group-detail-title">已选地域组列表</div>
<el-table :data="getNewCampaignLocationGroupData(index)" size="mini" border style="width: 100%">
<el-table-column prop="name" label="地域组名称"></el-table-column>
<el-table-column prop="id" label="地域组ID"></el-table-column>
<el-table-column label="国家数量">
<template slot-scope="scope">
{{ getLocationGroupCountries(scope.row).length }} 个国家
</template>
</el-table-column>
<el-table-column label="计划数" width="200" align="center">
<template slot-scope="scope">
<el-input-number v-model="scope.row.campaignCount" :min="1" @change="updateNewCampaignLocationGroupCampaignCount(index, scope.row, $event)" size="mini"></el-input-number>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
<!-- 标题组 -->
<el-form-item label="标题组">
<title-group-selector v-model="newCampaign.titleGroups" @change="markNewCampaignAsModified(index)" />
</el-form-item>
<!-- 资源组 -->
<el-form-item label="资源组">
<resource-group-selector v-model="newCampaign.resourceGroups" @change="markNewCampaignAsModified(index)" />
</el-form-item>
</el-form>
</div>
</div> </div>
</div> </div>
...@@ -478,7 +572,7 @@ ...@@ -478,7 +572,7 @@
</el-form-item> </el-form-item>
<!-- 日预算(当没有多计划自定义时显示) --> <!-- 日预算(当没有多计划自定义时显示) -->
<el-form-item label="日预算($)" prop="daily_budget" v-if="form.tiktok_json.isNewCampaign || !form.tiktok_json.campaignIdList || form.tiktok_json.campaignIdList.length === 0"> <el-form-item label="日预算($)" prop="daily_budget" v-if="form.platform === 2 && !hasMultiCampaignConfig">
<el-input-number v-model="form.daily_budget" :min="0"></el-input-number> <el-input-number v-model="form.daily_budget" :min="0"></el-input-number>
</el-form-item> </el-form-item>
...@@ -490,7 +584,7 @@ ...@@ -490,7 +584,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="目标考核ROAS(%)" prop="target_roas" v-if="form.platform !== 2"> <el-form-item label="目标考核ROAS(%)" prop="target_roas" v-if="form.platform !== 2 || (form.platform === 2 && !hasMultiCampaignConfig)">
<el-input-number v-model="form.target_roas" :min="0"></el-input-number> <el-input-number v-model="form.target_roas" :min="0"></el-input-number>
</el-form-item> </el-form-item>
...@@ -521,7 +615,7 @@ ...@@ -521,7 +615,7 @@
</el-form-item> </el-form-item>
<!-- 地域组(当没有多计划自定义时显示) --> <!-- 地域组(当没有多计划自定义时显示) -->
<el-form-item label="地域组" prop="location_groups" v-if="form.tiktok_json.isNewCampaign || !form.tiktok_json.campaignIdList || form.tiktok_json.campaignIdList.length === 0"> <el-form-item label="地域组" prop="location_groups" v-if="form.platform === 2 && !hasMultiCampaignConfig">
<location-group-selector v-model="form.location_groups" @change="handleLocationGroupsChange" /> <location-group-selector v-model="form.location_groups" @change="handleLocationGroupsChange" />
<!-- 展示已选地域组列表 --> <!-- 展示已选地域组列表 -->
<div v-if="form.location_groups && form.location_groups.length > 0" class="group-detail-container"> <div v-if="form.location_groups && form.location_groups.length > 0" class="group-detail-container">
...@@ -543,12 +637,13 @@ ...@@ -543,12 +637,13 @@
</div> </div>
</el-form-item> </el-form-item>
<!-- 素材组、标题组、描述组、资源组(当没有多计划自定义时显示) --> <!-- 素材组(TikTok平台全局配置) -->
<template v-if="form.tiktok_json.isNewCampaign || !form.tiktok_json.campaignIdList || form.tiktok_json.campaignIdList.length === 0"> <el-form-item label="素材组" prop="material_groups" v-if="form.platform === 2">
<el-form-item label="素材组" prop="material_groups">
<material-group-selector v-model="form.material_groups" /> <material-group-selector v-model="form.material_groups" />
</el-form-item> </el-form-item>
<!-- 标题组、描述组、资源组(当没有多计划自定义时显示) -->
<template v-if="form.platform === 2 && !hasMultiCampaignConfig">
<el-form-item label="标题组" prop="title_groups"> <el-form-item label="标题组" prop="title_groups">
<title-group-selector v-model="form.title_groups" /> <title-group-selector v-model="form.title_groups" />
</el-form-item> </el-form-item>
...@@ -918,6 +1013,7 @@ export default { ...@@ -918,6 +1013,7 @@ export default {
placements: [], placements: [],
isSmart: false, // 添加isSmart字段,默认为false isSmart: false, // 添加isSmart字段,默认为false
isNewCampaign: true, // 是否新建计划 isNewCampaign: true, // 是否新建计划
newCampaignCount: 1, // 新增:新建计划个数
advertiserId: '', // 选中的广告主ID advertiserId: '', // 选中的广告主ID
campaignIdList: [], // 计划ID列表 campaignIdList: [], // 计划ID列表
deviceModels: [], // 设备机型列表 deviceModels: [], // 设备机型列表
...@@ -1114,6 +1210,12 @@ export default { ...@@ -1114,6 +1210,12 @@ export default {
campaignConfigs: [], campaignConfigs: [],
// 每个计划的地域组数据 // 每个计划的地域组数据
campaignLocationGroups: [], campaignLocationGroups: [],
// 新建计划配置数据
newCampaignConfigs: [],
// 每个新建计划的地域组数据
newCampaignLocationGroups: [],
// 新建计划修改状态跟踪
newCampaignModifiedStatus: [],
} }
}, },
computed: { computed: {
...@@ -1124,6 +1226,24 @@ export default { ...@@ -1124,6 +1226,24 @@ export default {
// 获取当前页可执行的任务列表(只包括状态为1未执行的任务) // 获取当前页可执行的任务列表(只包括状态为1未执行的任务)
executableTasks() { executableTasks() {
return this.tableData.filter(row => row.status === 1); return this.tableData.filter(row => row.status === 1);
},
// 计算是否有独立配置(已有计划或新建计划)
hasMultiCampaignConfig() {
if (this.form.platform !== 2) return false;
// 检查是否有已有计划配置
const hasExistingCampaigns = !this.form.tiktok_json.isNewCampaign &&
this.form.tiktok_json.campaignIdList &&
this.form.tiktok_json.campaignIdList.length > 0;
// 检查是否有新建计划配置
const hasNewCampaigns = this.form.tiktok_json.isNewCampaign &&
this.form.tiktok_json.newCampaignCount > 0;
// 检查是否有新建计划配置数据
const hasNewCampaignConfigs = this.newCampaignConfigs && this.newCampaignConfigs.length > 0;
return hasExistingCampaigns || hasNewCampaigns || hasNewCampaignConfigs;
} }
}, },
created() { created() {
...@@ -1264,6 +1384,7 @@ export default { ...@@ -1264,6 +1384,7 @@ export default {
placements: [], placements: [],
isSmart: false, // 添加isSmart字段,默认为false isSmart: false, // 添加isSmart字段,默认为false
isNewCampaign: true, // 是否新建计划 isNewCampaign: true, // 是否新建计划
newCampaignCount: 1, // 新增:新建计划个数
advertiserId: '', // 选中的广告主ID advertiserId: '', // 选中的广告主ID
campaignIdList: [], // 计划ID列表 campaignIdList: [], // 计划ID列表
deviceModels: [], // 设备机型列表 deviceModels: [], // 设备机型列表
...@@ -1274,6 +1395,11 @@ export default { ...@@ -1274,6 +1395,11 @@ export default {
this.selectedTemplate = null this.selectedTemplate = null
this.dialogVisible = true this.dialogVisible = true
// 清空新建计划配置
this.newCampaignConfigs = []
this.newCampaignLocationGroups = []
this.newCampaignModifiedStatus = []
// 初始化组选项 // 初始化组选项
this.initGroupOptions() this.initGroupOptions()
...@@ -1316,6 +1442,7 @@ export default { ...@@ -1316,6 +1442,7 @@ export default {
placements: [], placements: [],
isSmart: false, // 添加isSmart字段,默认为false isSmart: false, // 添加isSmart字段,默认为false
isNewCampaign: true, // 是否新建计划 isNewCampaign: true, // 是否新建计划
newCampaignCount: 1, // 新增:新建计划个数
advertiserId: '', // 选中的广告主ID advertiserId: '', // 选中的广告主ID
campaignIdList: [], // 计划ID列表 campaignIdList: [], // 计划ID列表
deviceModels: [], // 设备机型列表 deviceModels: [], // 设备机型列表
...@@ -1558,7 +1685,7 @@ export default { ...@@ -1558,7 +1685,7 @@ export default {
// 检查是否是多计划配置 // 检查是否是多计划配置
if (this.form.platform === 2 && !this.form.tiktok_json.isNewCampaign && this.campaignConfigs.length > 0) { if (this.form.platform === 2 && !this.form.tiktok_json.isNewCampaign && this.campaignConfigs.length > 0) {
// 计划配置,为每个计划单独调用接口 // 已有计划配置,为每个计划单独调用接口
const results = []; const results = [];
for (let i = 0; i < this.campaignConfigs.length; i++) { for (let i = 0; i < this.campaignConfigs.length; i++) {
const campaignConfig = this.campaignConfigs[i]; const campaignConfig = this.campaignConfigs[i];
...@@ -1569,12 +1696,12 @@ export default { ...@@ -1569,12 +1696,12 @@ export default {
target_roas: campaignConfig.roasBid, target_roas: campaignConfig.roasBid,
location_groups: campaignConfig.locationGroups, location_groups: campaignConfig.locationGroups,
title_groups: campaignConfig.titleGroups, title_groups: campaignConfig.titleGroups,
material_groups: campaignConfig.materialGroups,
resource_groups: campaignConfig.resourceGroups, resource_groups: campaignConfig.resourceGroups,
accountName: this.form.accountName, // 添加账户名称 accountName: this.form.accountName, // 添加账户名称
// 更新tiktok_json中的campaignIdList为单个计划 // 更新tiktok_json中的campaignIdList为单个计划
tiktok_json: JSON.stringify({ tiktok_json: JSON.stringify({
...this.form.tiktok_json, ...this.form.tiktok_json,
roasBid: campaignConfig.roasBid, // 使用计划的ROAS目标值
campaignIdList: [campaignConfig.campaignId] campaignIdList: [campaignConfig.campaignId]
}), }),
// 设置单个计划名称 // 设置单个计划名称
...@@ -1622,6 +1749,74 @@ export default { ...@@ -1622,6 +1749,74 @@ export default {
this.$message.error(`失败的计划: ${failedCampaigns.join(', ')}`); this.$message.error(`失败的计划: ${failedCampaigns.join(', ')}`);
} }
this.dialogVisible = false;
this.fetchData();
} else if (this.form.platform === 2 && this.form.tiktok_json.isNewCampaign && this.newCampaignConfigs.length > 0) {
// 新建计划配置,为每个新建计划单独调用接口
const results = [];
for (let i = 0; i < this.newCampaignConfigs.length; i++) {
const newCampaignConfig = this.newCampaignConfigs[i];
const campaignTaskData = {
...taskData,
// 使用新建计划特定的配置
daily_budget: newCampaignConfig.dailyBudget,
target_roas: newCampaignConfig.roasBid,
location_groups: newCampaignConfig.locationGroups,
title_groups: newCampaignConfig.titleGroups,
resource_groups: newCampaignConfig.resourceGroups,
accountName: this.form.accountName, // 添加账户名称
// 更新tiktok_json为新建计划模式
tiktok_json: JSON.stringify({
...this.form.tiktok_json,
isNewCampaign: true,
roasBid: newCampaignConfig.roasBid, // 使用新建计划的ROAS目标值
campaignIdList: [] // 新建计划不需要campaignIdList
}),
// 设置新建计划名称(使用默认命名)
campaignNameList: `新建计划_${i + 1}`
};
// 使用新建计划特定的地域组数据
if (this.newCampaignLocationGroups[i] && this.newCampaignLocationGroups[i].length > 0) {
campaignTaskData.locationGroups = this.newCampaignLocationGroups[i].map(group => ({
locationGroupId: group.id,
campaignCount: group.campaignCount || 1
}));
}
// 使用全局应用数据(所有新建计划共用)
campaignTaskData.apps = this.appListData.map(app => ({
pkg: app.appId,
campaignCount: app.campaignCount || 1
}));
let response;
if (this.isEdit) {
response = await updateCampaignTask(campaignTaskData);
} else {
response = await createCampaignTask(campaignTaskData);
}
results.push({
campaignName: `新建计划_${i + 1}`,
success: response.status === 200,
response: response
});
}
// 检查所有结果
const successCount = results.filter(r => r.success).length;
const totalCount = results.length;
if (successCount === totalCount) {
this.$message.success(`所有新建计划创建成功 (${successCount}/${totalCount})`);
} else {
this.$message.warning(`部分新建计划创建成功 (${successCount}/${totalCount})`);
// 显示失败的计划
const failedCampaigns = results.filter(r => !r.success).map(r => r.campaignName);
this.$message.error(`失败的计划: ${failedCampaigns.join(', ')}`);
}
this.dialogVisible = false; this.dialogVisible = false;
this.fetchData(); this.fetchData();
} else { } else {
...@@ -2407,7 +2602,6 @@ export default { ...@@ -2407,7 +2602,6 @@ export default {
dailyBudget: this.form.daily_budget || 100, dailyBudget: this.form.daily_budget || 100,
locationGroups: [...this.form.location_groups], locationGroups: [...this.form.location_groups],
titleGroups: [...this.form.title_groups], titleGroups: [...this.form.title_groups],
materialGroups: [...this.form.material_groups],
resourceGroups: [...this.form.resource_groups] resourceGroups: [...this.form.resource_groups]
}; };
}); });
...@@ -2493,6 +2687,138 @@ export default { ...@@ -2493,6 +2687,138 @@ export default {
this.campaignLocationGroups[campaignIndex][index].campaignCount = value; this.campaignLocationGroups[campaignIndex][index].campaignCount = value;
} }
}, },
// 处理新建计划开关变化
handleNewCampaignChange(value) {
if (value) {
// 选择新建计划,初始化新建计划配置
this.form.tiktok_json.newCampaignCount = 1;
this.initNewCampaignConfigs();
} else {
// 选择已有计划,清空新建计划配置
this.newCampaignConfigs = [];
this.newCampaignLocationGroups = [];
this.newCampaignModifiedStatus = [];
}
},
// 处理新建计划个数变化
handleNewCampaignCountChange(value) {
if (value > 0) {
this.initNewCampaignConfigs();
} else {
this.newCampaignConfigs = [];
this.newCampaignLocationGroups = [];
this.newCampaignModifiedStatus = [];
}
},
// 初始化新建计划配置
initNewCampaignConfigs() {
const count = this.form.tiktok_json.newCampaignCount || 1;
this.newCampaignConfigs = [];
this.newCampaignLocationGroups = [];
this.newCampaignModifiedStatus = [];
for (let i = 0; i < count; i++) {
this.newCampaignConfigs.push({
roasBid: this.form.tiktok_json.roasBid || 1,
dailyBudget: this.form.daily_budget || 100,
locationGroups: [...this.form.location_groups],
titleGroups: [...this.form.title_groups],
resourceGroups: [...this.form.resource_groups]
});
// 初始化地域组数据
this.newCampaignLocationGroups.push(
this.selectedLocationGroups.map(group => ({
id: group.id,
name: group.name,
campaignCount: group.campaignCount || 1,
countryCodes: group.countryCodes || []
}))
);
// 初始化修改状态
this.newCampaignModifiedStatus.push(false);
}
},
// 移除新建计划配置
removeNewCampaignConfig(index) {
this.newCampaignConfigs.splice(index, 1);
this.newCampaignLocationGroups.splice(index, 1);
this.newCampaignModifiedStatus.splice(index, 1);
this.form.tiktok_json.newCampaignCount = this.newCampaignConfigs.length;
},
// 标记新建计划为已修改
markNewCampaignAsModified(index) {
this.$set(this.newCampaignModifiedStatus, index, true);
},
// 处理新建计划地域组变化
handleNewCampaignLocationGroupsChange(campaignIndex, locationGroupIds) {
if (locationGroupIds && locationGroupIds.length > 0) {
this.fetchNewCampaignLocationGroups(campaignIndex, locationGroupIds);
} else {
this.newCampaignLocationGroups[campaignIndex] = [];
}
},
// 获取新建计划地域组数据
async fetchNewCampaignLocationGroups(campaignIndex, locationGroupIds) {
try {
this.newCampaignLocationGroups[campaignIndex] = locationGroupIds.map(id => {
const numId = Number(id);
const group = this.locationGroupOptions.find(item => Number(item.id) === numId);
if (group) {
return {
id: group.id,
name: group.name || `地域组 ${id}`,
campaignCount: 1,
countryCodes: group.countryCodes || []
};
} else {
return {
id: id,
name: `地域组 ${id}`,
campaignCount: 1,
countryCodes: []
};
}
});
} catch (error) {
console.error('获取新建计划地域组数据失败:', error);
this.$message.error('获取新建计划地域组数据失败');
}
},
// 获取新建计划地域组数据用于显示
getNewCampaignLocationGroupData(campaignIndex) {
return this.newCampaignLocationGroups[campaignIndex] || [];
},
// 更新新建计划地域组的计划数
updateNewCampaignLocationGroupCampaignCount(campaignIndex, group, value) {
// 验证输入值
if (value < 1) {
this.$message.warning('计划数最小为1');
// 重置为1
const index = this.newCampaignLocationGroups[campaignIndex].findIndex(item => item.id === group.id);
if (index !== -1) {
this.newCampaignLocationGroups[campaignIndex][index].campaignCount = 1;
}
return;
}
// 更新计划数
const index = this.newCampaignLocationGroups[campaignIndex].findIndex(item => item.id === group.id);
if (index !== -1) {
this.newCampaignLocationGroups[campaignIndex][index].campaignCount = value;
}
},
} }
} }
</script> </script>
...@@ -2987,4 +3313,40 @@ export default { ...@@ -2987,4 +3313,40 @@ export default {
color: #0369a1; color: #0369a1;
font-size: 13px; font-size: 13px;
} }
/* 新建计划配置样式 */
.new-campaign-config {
background-color: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
}
.new-campaign-config h4 {
color: #0369a1;
font-size: 16px;
font-weight: 600;
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 1px solid #bae6fd;
}
.new-campaign-config .el-form-item {
margin-bottom: 15px;
}
.new-campaign-config .el-form-item__label {
font-weight: 500;
color: #606266;
}
/* 新建计划个数输入框样式 */
.new-campaign-count-input {
width: 200px;
}
.new-campaign-count-input .el-input-number {
width: 100%;
}
</style> </style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment