Selaa lähdekoodia

✨ feat<自动化规则>:表格树形结构;定向规则弹窗添加

xinyan 9 kuukautta sitten
vanhempi
commit
2403e81ad0

+ 14 - 4
src/views/efTools/automation/api.ts

@@ -42,7 +42,7 @@ export function DelObj(id: DelReq) {
   });
 }
 
-//关联广告活动
+// 关联广告活动
 export function getRelationCampaign(query) {
   return request({
     url: '/api/ad_manage/template/relation_campaign_list/',
@@ -51,7 +51,7 @@ export function getRelationCampaign(query) {
   });
 }
 
-//广告组合下拉框
+// 广告组合下拉框
 export function getAdGroupList(query) {
   return request({
     url: '/api/ad_manage/ad_tree/portfolios',
@@ -60,11 +60,21 @@ export function getAdGroupList(query) {
   });
 }
 
-//广告关联活动保存
+// 广告关联活动保存
 export function updateAdCampaign(body) {
   return request({
     url: '/api/ad_manage/template/relation_campaign_enable/',
     method: 'POST',
     data: body,
   });
-}
+}
+
+// 获取定向规则列表
+export function getTargetingRuleList(query) {
+  return request({
+    url: '/api/ad_manage/template/target_select',
+    method: 'GET',
+    params: query,
+  });
+}
+

+ 197 - 45
src/views/efTools/automation/components/adActivityDialog.vue

@@ -9,6 +9,7 @@ import { getAdGroupList, getRelationCampaign } from '/@/views/efTools/automation
 import { storeToRefs } from 'pinia';
 import { useShopInfo } from '/@/stores/shopInfo';
 import { ElMessage } from 'element-plus';
+import TargetRuleDialog from '/@/views/efTools/automation/components/targetRuleDialog.vue';
 
 const props = defineProps({
   templateId: {
@@ -30,7 +31,14 @@ const { templateId } = toRefs(props);
 const { activeModel } = toRefs(props);
 
 const dialogVisible = ref(props.modelValue);
-//筛选条件
+const targetRuleDialogVisible = ref(false);
+
+// 定向规则
+//const adGroupId = ref('')
+//const campaignId = ref('')
+const selectedTargetedRow = ref(null);
+
+// 筛选条件
 const searchAdCampaign = ref('');
 const selectedCampaignType = ref('sp');
 const selectedAdGroup = ref('');
@@ -49,7 +57,7 @@ const campaignStatus = [
   { value: 'PAUSED', label: '已暂停' },
 ];
 
-//表格
+// 表格
 const currentPage = ref(1);
 const pageSize = ref(25);
 const total = ref(0);
@@ -89,7 +97,6 @@ async function fetchAdCampaign() {
       limit: pageSize.value,
     });
     adCampaignName.value = resp.data;
-
     total.value = resp.total;
     currentPage.value = resp.page;
     // 开始恢复勾选状态
@@ -110,45 +117,124 @@ async function fetchAdCampaign() {
 }
 
 function handleSelectionChange(selection) {
-  if (isRestoringSelection) return; // 恢复勾选时跳过该方法
-
-  selections = selection;
-  const newSelections = selections.filter(
-      (sel) => !selectedAds.value.some((added) => added.campaignId === sel.campaignId)
-  );
-  if (newSelections.length > 0) {
-    selectedAds.value.push(...newSelections);
-  }
-  // 处理取消选中的项
-  const removedSelections = selectedAds.value.filter(
-      (added) => !selections.some((sel) => sel.campaignId === added.campaignId)
-  );
-
-  if (removedSelections.length > 0) {
-    selectedAds.value = selectedAds.value.filter(
-        (added) => !removedSelections.some((removed) => removed.campaignId === added.campaignId)
-    );
-  }
+  if (isRestoringSelection) return;
+
+  const newSelectedAds = [];
+
+  selection.forEach(item => {
+    if (item.campaignGroupInfo) {
+      // 这是父节点(广告活动)
+      newSelectedAds.push({
+        campaignId: item.campaignId,
+        campaignName: item.campaignName,
+        campaignType: item.campaignType,
+        isParent: true
+      });
+    } else {
+      // 这是子节点(广告组)
+      const parent = adCampaignName.value.find(ad =>
+          ad.campaignGroupInfo && ad.campaignGroupInfo.some(group => group.adGroupId === item.adGroupId)
+      );
+      if (parent) {
+        // 添加父节点(如果还没有添加)
+        if (!newSelectedAds.some(ad => ad.campaignId === parent.campaignId)) {
+          newSelectedAds.push({
+            campaignId: parent.campaignId,
+            campaignName: parent.campaignName,
+            campaignType: parent.campaignType,
+            isParent: true
+          });
+        }
+        // 添加子节点(广告组)
+        newSelectedAds.push({
+          adGroupId: item.adGroupId,
+          adGroupName: item.adGroupName,
+          parentCampaignId: parent.campaignId,
+          parentCampaignName: parent.campaignName,
+          campaignType: parent.campaignType
+        });
+      }
+    }
+  });
+  selectedAds.value = newSelectedAds;
 }
 
+const groupedSelectedAds = computed(() => {
+  const groups = {};
+  selectedAds.value.forEach(ad => {
+    if (ad.isParent) {
+      if (!groups[ad.campaignId]) {
+        groups[ad.campaignId] = {
+          id: ad.campaignId,
+          name: ad.campaignName,
+          campaignType: ad.campaignType,
+          campaignGroupInfo: []
+        };
+      }
+    } else {
+      if (!groups[ad.parentCampaignId]) {
+        groups[ad.parentCampaignId] = {
+          id: ad.parentCampaignId,
+          name: ad.parentCampaignName,
+          campaignType: ad.campaignType,
+          campaignGroupInfo: []
+        };
+      }
+      groups[ad.parentCampaignId].campaignGroupInfo.push({
+        id: ad.adGroupId,
+        name: ad.adGroupName
+      });
+    }
+  });
+  return Object.values(groups);
+});
+
 function removeSelectedAd(index) {
   const removedAd = selectedAds.value.splice(index, 1)[0];
-  const targetIndex = adCampaignName.value.findIndex(ad => ad.campaignName === removedAd.campaignName);
-  if (targetIndex !== -1) {
-    const adTable = refTable.value;
-    adTable.toggleRowSelection(adCampaignName.value[targetIndex], false);
-  }
-}
 
-function removeAllSelectedAds() {
-  const adTable = refTable.value;
-  selectedAds.value.forEach(ad => {
-    const targetIndex = adCampaignName.value.findIndex(item => item.campaignName === ad.campaignName);
+  if (removedAd.parentCampaignId) {
+    // 查找同一父节点下的其他子节点
+    const remainingChildren = selectedAds.value.filter(ad => ad.parentCampaignId === removedAd.parentCampaignId);
+
+    // 如果父节点下还有其他子节点,父节点不删除
+    if (remainingChildren.length > 0) {
+      // 取消表格1中该子节点的勾选
+      const targetParent = adCampaignName.value.find(ad => ad.campaignId === removedAd.parentCampaignId);
+      if (targetParent) {
+        const targetChildIndex = targetParent.campaignGroupInfo.findIndex(group => group.adGroupId === removedAd.adGroupId);
+        if (targetChildIndex !== -1) {
+          const adTable = refTable.value;
+          adTable.toggleRowSelection(targetParent.campaignGroupInfo[targetChildIndex], false);
+        }
+      }
+    } else {
+      // 如果没有其他子节点了,移除父节点
+      const parentIndex = selectedAds.value.findIndex(ad => ad.campaignId === removedAd.parentCampaignId);
+      if (parentIndex !== -1) {
+        selectedAds.value.splice(parentIndex, 1);
+      }
+
+      // 取消表格1中父节点的勾选
+      const parentIndexInTable = adCampaignName.value.findIndex(ad => ad.campaignId === removedAd.parentCampaignId);
+      if (parentIndexInTable !== -1) {
+        const adTable = refTable.value;
+        adTable.toggleRowSelection(adCampaignName.value[parentIndexInTable], false);
+      }
+    }
+  } else {
+    // 如果删除的项是父节点
+    const targetIndex = adCampaignName.value.findIndex(ad => ad.campaignId === removedAd.campaignId);
     if (targetIndex !== -1) {
+      const adTable = refTable.value;
       adTable.toggleRowSelection(adCampaignName.value[targetIndex], false);
     }
-  });
+  }
+}
+
+function removeAllSelectedAds() {
   selectedAds.value = [];
+  const adTable = refTable.value;
+  adTable.clearSelection();
 }
 
 function cancel() {
@@ -197,6 +283,51 @@ async function fetchAdGroupList() {
   }
 }
 
+function handleSelectTarget(row) {
+  console.log("=>(adActivityDialog.vue:287) row", row);
+  // 获取父节点数据
+  const parent = adCampaignName.value.find(ad =>
+      ad.campaignGroupInfo && ad.campaignGroupInfo.some(group => group.adGroupId === row.adGroupId)
+  );
+  if (parent) {
+    // 构造包含父节点和子节点的数据对象
+    selectedTargetedRow.value = {
+      parentCampaignId: parent.campaignId,
+      parentCampaignName: parent.campaignName,
+      campaignType: parent.campaignType,
+      adGroupId: row.adGroupId,
+      adGroupName: row.adGroupName,
+      isSelected: row.isSelected || false, // 同步 isSelected 状态
+    };
+  }
+
+  targetRuleDialogVisible.value = true;
+}
+
+function handleConfirm(campaignKeywordInfo) {
+  selectedTargetedRow.value.keywordInfo = campaignKeywordInfo;
+  selectedTargetedRow.value.isSelected = true;
+
+  // 更新子节点状态
+  const parent = adCampaignName.value.find(ad =>
+      ad.campaignGroupInfo && ad.campaignGroupInfo.some(group => group.adGroupId === selectedTargetedRow.value.adGroupId)
+  );
+  if (parent) {
+    const group = parent.campaignGroupInfo.find(group => group.adGroupId === selectedTargetedRow.value.adGroupId);
+    if (group) {
+      group.isSelected = true; // 更新子节点的 isSelected 状态
+    }
+  }
+
+  // 勾选子节点行
+  const table = refTable.value;
+  const targetRow = parent.campaignGroupInfo.find(group => group.adGroupId === selectedTargetedRow.value.adGroupId);
+
+  if (table && targetRow) {
+    table.toggleRowSelection(targetRow, true); // 勾选目标子节点行
+  }
+}
+
 const headerCellStyle = (args) => {
   if (args.rowIndex === 0) {
     return {
@@ -244,13 +375,16 @@ watch(dialogVisible, (newValue) => {
 
 const treeProps = computed(() => {
   if (activeModel.value === 'adGroup' || activeModel.value === 'specified') {
-    return { children: 'campaignGroupInfo' };
+    return {
+      children: 'campaignGroupInfo',
+      checkStrictly: false
+    };
   }
   return {};
 });
 
 onMounted(() => {
-  //fetchAdGroupList();
+  fetchAdGroupList();
 });
 
 </script>
@@ -258,14 +392,13 @@ onMounted(() => {
 <template>
   <el-dialog
       v-model="dialogVisible"
-      class="custom-dialog"
       style="border-radius: 10px;"
       title="关联广告活动"
       width="1158px"
   >
     <div class="container">
       <div class="container-left">
-        <el-input v-model="searchAdCampaign" placeholder="请输入广告活动" style="width: 100%;"
+        <el-input v-model="searchAdCampaign" clearable placeholder="请输入广告活动" style="width: 100%;"
                   @change="fetchAdCampaign()"></el-input>
         <div class="custom-inline">
           <el-select v-model="selectedCampaignType" placeholder="选择广告类型">
@@ -299,12 +432,11 @@ onMounted(() => {
             v-loading="loading"
             :cell-style="cellStyle"
             :data="adCampaignName"
-            :row-key="'campaignId'"
             :header-cell-style="headerCellStyle"
+            :row-key="'adGroupId'"
+            :tree-props="treeProps"
             height="400"
             style="width: 100%;"
-            :tree-props="treeProps"
-            v-bind="activeModel === 'adGroup' || activeModel === 'specified' ? treeProps : {}"
             @selection-change="handleSelectionChange"
         >
           <el-table-column label="广告活动名称">
@@ -319,6 +451,14 @@ onMounted(() => {
               </el-tag>
               {{ scope.row.campaignName }}
               {{ scope.row.adGroupName }}
+              <el-button
+                  v-if="scope.row.adGroupName && activeModel==='specified'&&!scope.row.isSelected"
+                  class="btn-link"
+                  link
+                  @click="handleSelectTarget(scope.row)">
+                选择定向
+              </el-button>
+              <span v-else-if="scope.row.adGroupName">已选择</span>
             </template>
           </el-table-column>
           <el-table-column type="selection" width="55"></el-table-column>
@@ -341,14 +481,15 @@ onMounted(() => {
         <h3>已选择({{ selectedAds.length }})</h3>
         <el-table
             :cell-style="cellStyle"
-            :data="selectedAds"
+            :data="groupedSelectedAds"
             :header-cell-style="headerCellStyle"
-            height="484"
+            :row-key="'id'"
             :tree-props="treeProps"
-            v-bind="activeModel === 'adGroup' || activeModel === 'specified' ? treeProps : {}"
+            height="484"
             style="width: 100%; margin-top: 20px;"
+            v-bind="activeModel === 'adGroup' || activeModel === 'specified' ? treeProps : {}"
         >
-          <el-table-column label="广告活动" prop="campaignName">
+          <el-table-column label="广告活动" prop="name">
             <template #default="scope">
               <el-tag
                   v-if="scope.row.campaignType"
@@ -359,7 +500,7 @@ onMounted(() => {
                 {{ scope.row.campaignType }}
               </el-tag>
               {{ scope.row.campaignName }}
-              {{ scope.row.adGroupName }}
+              {{ scope.row.name }}
             </template>
           </el-table-column>
           <el-table-column
@@ -386,6 +527,11 @@ onMounted(() => {
       </div>
     </template>
   </el-dialog>
+  <TargetRuleDialog v-if="activeModel === 'specified'"
+                    v-model="targetRuleDialogVisible"
+                    :selectedTargetedRow="selectedTargetedRow"
+                    @confirm:targetRule="handleConfirm"
+  ></TargetRuleDialog>
 </template>
 
 <style scoped>
@@ -438,4 +584,10 @@ onMounted(() => {
   padding: 15px
 }
 
+.btn-link {
+  font-size: 13px;
+  color: #0085ff;
+  /* ling-heigt: 23px; */
+}
+
 </style>

+ 130 - 19
src/views/efTools/automation/components/targetRuleDialog.vue

@@ -4,40 +4,151 @@
  * @Description: 关联广告活动-选择定向弹窗
  * @Author: xinyan
  */
-import { reactive, ref } from 'vue';
+import { onMounted, reactive, ref, watch } from 'vue';
 import { MatchType } from '../../utils/enum';
+import { getTargetingRuleList } from '/@/views/efTools/automation/api';
+import { ElMessage } from 'element-plus';
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    required: true,
+  },
+  selectedTargetedRow: {
+    type: Object,
+    required: true,
+  },
+});
+const emits = defineEmits(['update:modelValue', 'confirm:targetRule']);
+const targetRuleDialogVisible = ref(props.modelValue);
+const { selectedTargetedRow } = toRefs(props);
+const adGroupId = ref(props.adGroupId);
+const campaignKeywordInfo = ref(null);
 
 // 查询、筛选条件
 const matchType = ref('');
 const keyWord = ref('');
 
 const gridOptions = reactive({
+  height: 550,
+  showOverflow: true,
+  loading: false,
+  rowConfig: {
+    isHover: true,
+    height: 34
+  },
   columns: [
-    { field: 'keyword', title: '关键词' },
-    { field: 'match', title: '匹配方式' },
-    { type: 'checkbox', title: '全选' }
+    { field: 'keywordText', title: '关键词', width: 220 },
+    {
+      field: 'matchType',
+      title: '匹配方式',
+      formatter: ({ cellValue }) => getMatchTypeLabel(cellValue).label,
+    },
+    { type: 'checkbox', align: 'right', width: 55 }
   ],
   data: []
 });
 
+async function fetchTargetRuleList() {
+  try {
+    gridOptions.loading = true;
+    const resp = await getTargetingRuleList({
+      campaignType: selectedTargetedRow.value.campaignType,
+      campaignId: selectedTargetedRow.value.parentCampaignId,
+      adGroupId: selectedTargetedRow.value.adGroupId,
+      matchType: matchType.value,
+      search: keyWord.value,
+    });
+    gridOptions.data = resp.data.targetData;
+    gridOptions.loading = false;
+  } catch (error) {
+    ElMessage.error('请求定向数据失败');
+  }
+}
+
+function handleCheckChange({ records }) {
+  campaignKeywordInfo.value = records;
+}
+
+function getMatchTypeLabel(type: string) {
+  const matchType = MatchType.find(item => item.value === type);
+  if (matchType) {
+    return { label: matchType.label, type: type };
+  }
+  return { label: type, type: '' };
+}
+
+function cancel() {
+  targetRuleDialogVisible.value = false;
+}
+
+async function confirm() {
+  targetRuleDialogVisible.value = false;
+  emits('confirm:targetRule', campaignKeywordInfo.value);
+}
+
+const headerCellStyle = () => {
+  return {
+    fontSize: '13px',
+    height: '34px',
+  };
+};
+
+const cellStyle = () => {
+  return {
+    fontSize: '13px',
+    //fontWeight: '500',
+  };
+};
+
+watch(() => props.modelValue, (newValue) => {
+  targetRuleDialogVisible.value = newValue;
+});
+
+watch(targetRuleDialogVisible, (newValue) => {
+  emits('update:modelValue', newValue);
+});
+
+watch(() => props.selectedTargetedRow, () => {
+  fetchTargetRuleList();
+});
+
+onMounted(() => {
+  //fetchTargetRuleList();
+});
+
 </script>
 
 <template>
-  <el-select
-      v-model="matchType"
-      placeholder="Select"
-      size="large"
-      style="width: 240px"
-  >
-    <el-option
-        v-for="item in MatchType"
-        :key="item.value"
-        :label="item.label"
-        :value="item.value"
-    />
-  </el-select>
-  <el-input v-model="keyWord" placeholder="快速搜索关键词" style="width: 240px" />
-  <vxe-grid v-bind="gridOptions"></vxe-grid>
+  <el-dialog v-model="targetRuleDialogVisible"
+             style="border-radius: 10px;"
+             title="关联广告活动 > 选择定向"
+             width="1158px">
+    <div>
+      <el-select
+          v-model="matchType"
+          placeholder="全部匹配方式"
+          style="width: 128px; margin-bottom: 10px"
+          @change="fetchTargetRuleList"
+      >
+        <el-option
+            v-for="item in MatchType"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+        />
+      </el-select>
+    </div>
+    <el-input v-model="keyWord" class="mb-3" clearable placeholder="快速搜索关键词" @change="fetchTargetRuleList" />
+    <vxe-grid :cell-style="cellStyle" :header-cell-style="headerCellStyle" @checkbox-change="handleCheckChange"
+              v-bind="gridOptions"></vxe-grid>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="cancel">取消</el-button>
+        <el-button type="primary" @click="confirm">确定</el-button>
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <style scoped>

+ 22 - 26
src/views/efTools/automation/index.vue

@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import { onMounted, provide, reactive, ref, Ref, watch } from 'vue';
+import { onMounted, provide, reactive, ref, Ref } from 'vue';
 import { Search } from '@element-plus/icons-vue';
 import { TemplateType } from '../utils/enum';
 import { DelObj, GetList } from '/@/views/efTools/automation/api';
@@ -128,7 +128,7 @@ const gridOptions = reactive({
     pageSizes: [10, 20, 30],
   },
   columns: [
-    { field: 'id', title: 'ID' ,width:140},
+    { field: 'id', title: 'ID', width: 140 },
     { field: 'name', title: '模板名称' },
     {
       field: 'rule.type',
@@ -161,11 +161,25 @@ const gridEvents = {
   },
 };
 
-watch(templateType, () => {
+function handleTypeChange() {
+  localStorage.setItem('templateType', JSON.stringify(templateType.value));
   getList();
-});
+}
+
+function handleTemplateListChange() {
+  localStorage.setItem('templateList', JSON.stringify(templateList.value));
+  getList();
+}
 
 async function getList() {
+  const savedTemplateType = localStorage.getItem('templateType');
+  if (savedTemplateType) {
+    templateType.value = JSON.parse(savedTemplateType);
+  }
+  const savedTemplateList = localStorage.getItem('templateList');
+  if (savedTemplateList) {
+    templateList.value = JSON.parse(savedTemplateList);
+  }
   try {
     gridOptions.loading = true;
     const response = await GetList({
@@ -197,25 +211,6 @@ function getTemplateTypeLabel(type: number) {
   return { label: '', type: '' };
 }
 
-function getTagType(type) {
-  switch (type) {
-    case 1:
-      return 'danger'; // 分时调价
-    case 2:
-      return 'success'; // 分时预算
-    case 3:
-      return 'info'; // 广告活动
-    case 4:
-      return 'warning'; // 定向规则
-    case 5:
-      return 'primary'; // 添加搜索词
-    case 6:
-      return ''; // 添加否定词
-    default:
-      return '';
-  }
-}
-
 const cellStyle = () => {
   return {
     fontSize: '13px',
@@ -244,9 +239,10 @@ onMounted(() => {
                 clearable
                 placeholder="模板名称"
                 style="width: 240px"
-                @change="getList"
+                @change="handleTemplateListChange"
       />
-      <el-select v-model="templateType" placeholder="Select" style="width: 240px">
+      <el-select v-model="templateType" placeholder="所有类型" style="width: 240px" value-key="value"
+                 @change="handleTypeChange">
         <el-option
             v-for="item in TemplateType"
             :key="item.value"
@@ -294,7 +290,7 @@ onMounted(() => {
           @refresh="refreshTable"></component>
     </div>
   </el-drawer>
-  <AdActivityDialog v-model="isDialogVisible" :templateId="templateId" :activeModel="activeModel"/>
+  <AdActivityDialog v-model="isDialogVisible" :activeModel="activeModel" :templateId="templateId" @confirm="handleConfirm"/>
   <AutomatedRuleTips v-model="autoInfo"></AutomatedRuleTips>
 </template>
 

+ 3 - 3
src/views/efTools/utils/enum.ts

@@ -10,7 +10,7 @@ export const TemplateType = [
 
 export const MatchType = [
   { label: '全部匹配方式', value: '' },
-  { label: '广泛匹配', value: 1 },
-  { label: '词组匹配', value: 2 },
-  { label: '精准匹配', value: 3 },
+  { label: '广泛匹配', value: 'BROAD' },
+  { label: '词组匹配', value: 'PHRASE' },
+  { label: '精准匹配', value: 'EXACT' },
 ];