Преглед изворни кода

✨ feat: SB新建活动页面布局

WanGxC пре 1 година
родитељ
комит
d76074049c

+ 9 - 1
src/views/adManage/sb/campaigns/CreateCampaigns/api/index.ts

@@ -10,7 +10,7 @@ export function getAdMixSelect() {
 
 export function postCampaignsData(filteredRequestData) {
   return request({
-      url: '/api/ad_manage/spcampaigns/create/',
+      url: '/api/ad_manage/sbcampaigns/create/',
       method: 'post',
       data: filteredRequestData,
   })
@@ -23,3 +23,11 @@ export function postGroupData(filteredRequestData) {
       data: filteredRequestData,
   })
 }
+
+export function postNegativeWordData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sptargets/add/negative/keywords/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}

+ 1 - 1
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdCampaign.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="container">
+  <div class="customize-container">
     <el-card body-style="padding: 20px 80px 0 80px;" v-loading="campaignLoading">
       <div style="font-weight: 700; padding-bottom: 18px">
         <span style="color: #306cd7; font-size: 26px">|</span>

+ 66 - 16
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdFormat.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="container">
+  <div class="customize-container">
     <el-card body-style="padding: 20px 80px 20px 80px;">
       <div style="font-weight: 700; padding-bottom: 18px">
         <span style="color: #306cd7; font-size: 26px">|</span>
@@ -8,19 +8,21 @@
       <div class="ad-format-radios">
         <el-radio-group v-model="adFormatRadio" style="display: flex; justify-content: space-between">
           <el-radio class="ad-format-radio" label="productSet" border>
-            <div style="text-align: center">商品集</div>
+            <div style="text-align: center; color: #333333">商品集</div>
             <div style="background-color: #1e2128; width: 200px; height: 200px; margin: 0 auto"></div>
-            <div style="padding: 5px 0 10px 0">使用图片将流量引导至商品详情页面, 以推广多件商品</div>
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">使用图片将流量引导至商品详情页面, 以推广多件商品</div>
           </el-radio>
           <el-radio class="ad-format-radio" label="focus" border>
-            <div style="text-align: center">品牌旗舰店焦点</div>
+            <div style="text-align: center; color: #333333">品牌旗舰店焦点</div>
             <div style="background-color: #4c649d; width: 200px; height: 200px; margin: 0 auto"></div>
-            <div style="padding: 5px 0 10px 0">将流量引流到品牌旗舰店, 包括子页面</div>
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">将流量引流到品牌旗舰店, 包括子页面</div>
           </el-radio>
           <el-radio class="ad-format-radio" label="video" border>
-            <div style="text-align: center">视频</div>
+            <div style="text-align: center; color: #333333">视频</div>
             <div style="background-color: #43abc3; width: 200px; height: 200px; margin: 0 auto"></div>
-            <div style="padding: 5px 0 10px 0">使用视频宣传您的品牌或产品, 将流量吸引至您的品牌旗舰店或商品详情页</div>
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">
+              使用视频宣传您的品牌或产品, 将流量吸引至您的品牌旗舰店或商品详情页
+            </div>
           </el-radio>
         </el-radio-group>
       </div>
@@ -74,8 +76,32 @@
         </el-radio-group>
       </div>
 
+      <div v-if="adFormatRadio === 'focus'">
+        <p style="padding: 10px 0 15px 0">亚马逊上的品牌旗舰店(必须有4个或更多页面, 每个页面有1个或更多独特的商品)</p>
+        <el-form
+          ref="flagshipStoreRuleFormRef2"
+          :model="flagshipStoreRuleForm2"
+          :rules="flagshipStoreRules2"
+          label-position="top"
+          label-width="120px"
+          class="demo-ruleForm"
+          size="default"
+          status-icon>
+          <el-form-item label="选择一个店铺" prop="focusShop">
+            <el-select v-model="flagshipStoreRuleForm2.focusShop" style="padding-top: 10px; margin-top: -15px; width: 500px">
+              <el-option v-for="item in focusShopOptions" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+
       <div>
-        <div style="font-weight: 700; padding: 20px 0 10px 0" v-if="(adFormatRadio === 'productSet' && arrivalsRadio === 'newArrivals') || (adFormatRadio === 'video' && arrivalsRadio === 'productDetailsPage')">
+        <div
+          style="font-weight: 700; padding: 20px 0 10px 0"
+          v-if="
+            (adFormatRadio === 'productSet' && arrivalsRadio === 'newArrivals') ||
+            (adFormatRadio === 'video' && arrivalsRadio === 'productDetailsPage')
+          ">
           <span style="color: #306cd7; font-size: 26px">|</span>
           <span style="font-size: 18px; padding-left: 5px">商品</span>
         </div>
@@ -94,7 +120,7 @@ import VideoCommodity from '../component/VideoCommodity.vue'
 
 const adFormatRadio = ref('productSet')
 const arrivalsRadio = ref('flagshipStore')
-const flagshipStoreFormRef = ref<FormInstance>()
+const flagshipStoreRuleFormRef = ref<FormInstance>()
 interface flagshipStoreRuleForm {
   shop: string
   page: string
@@ -104,12 +130,19 @@ const flagshipStoreRuleForm = reactive<flagshipStoreRuleForm>({
   page: '',
 })
 const flagshipStoreRules = reactive<FormRules<flagshipStoreRuleForm>>({
-  shop: {
-    required: true,
-  },
-  page: {
-    required: true,
-  },
+  shop: { required: true, trigger: 'blur' },
+  page: { required: true, trigger: 'blur' },
+})
+
+const flagshipStoreRuleFormRef2 = ref<FormInstance>()
+interface flagshipStoreRuleForm2 {
+  focusShop: string
+}
+const flagshipStoreRuleForm2 = reactive<flagshipStoreRuleForm2>({
+  focusShop: '',
+})
+const flagshipStoreRules2 = reactive<FormRules<flagshipStoreRuleForm2>>({
+  focusShop: { required: true, message: 'xxx', trigger: 'blur' },
 })
 
 const shopOptions = [
@@ -132,6 +165,16 @@ const pageOptions = [
     label: '2',
   },
 ]
+const focusShopOptions = [
+  {
+    value: '1',
+    label: '1',
+  },
+  {
+    value: '2',
+    label: '2',
+  },
+]
 </script>
 
 <style scoped>
@@ -153,7 +196,7 @@ const pageOptions = [
   align-items: flex-start;
   padding: 15px 10px 0 10px;
 }
-.container {
+.customize-container {
   margin-top: 10px;
 }
 ::v-deep(.ad-format-radios .el-radio-group .el-radio__inner) {
@@ -168,4 +211,11 @@ const pageOptions = [
 ::v-deep(.land-Page .el-radio__label) {
   width: 100%;
 }
+
+::v-deep(.ad-format-radios label.el-radio.is-bordered.is-checked.el-radio--default) {
+  background: #f5f7fe;
+}
+::v-deep(.land-Page label.el-radio.is-bordered.is-checked.el-radio--default) {
+  background: #f5f7fe;
+}
 </style>

+ 5 - 5
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdGroup.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="container">
+  <div class="customize-container">
     <el-card body-style="padding: 20px 80px 0 80px;" v-loading="groupLoading">
       <div style="font-weight: 700; padding-bottom: 18px">
         <span style="color: #306cd7; font-size: 26px">|</span>
@@ -14,9 +14,9 @@
         class="demo-ruleForm"
         :size="formSize"
         status-icon>
-        <el-form-item label="广告组名称" prop="groupName" style="width: 100%">
-          <el-input v-model="groupRuleForm.groupName" style="width: 40%" />
-          <el-button type="primary" plain :disabled="!respAdGroupId" @click="submitGroupForm(groupRuleFormRef)" style="margin-left: 35px">保存</el-button>
+        <el-form-item label="广告组名称" prop="groupName">
+          <el-input v-model="groupRuleForm.groupName" style="width: 600px" />
+          <el-button type="primary" plain :disabled="!respAdGroupId" @click="submitGroupForm(groupRuleFormRef)" style="margin-left: 30px">保存</el-button>
         </el-form-item>
       </el-form>
     </el-card>
@@ -104,7 +104,7 @@ watch(respAdGroupId, () => {
 </script>
 
 <style lang="scss" scoped>
-.container {
+.customize-container {
   margin-top: 10px;
 }
 ::v-deep(.el-form-item__label) {

+ 60 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/DeliveryType.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 20px 80px;">
+      <div style="font-weight: 700; padding-bottom: 18px">
+        <span style="color: #306cd7; font-size: 26px">|</span>
+        <span style="font-size: 18px; padding-left: 5px">投放类型</span>
+      </div>
+      <div class="delivery-type-radio-group">
+        <el-radio-group v-model="deliveryTypeRadio" style="display: flex; justify-content: space-between">
+          <el-radio class="delivery-type-radio" label="keyword" border>
+            <div>关键词定向</div>
+            <div style="color: #88909b; font-weight: 400; padding-top: 5px;">选择关键词以帮助您的商品出现在购物者搜索中</div>
+          </el-radio>
+          <el-radio class="delivery-type-radio" label="commodity" border>
+            <div>商品定向</div>
+            <div style="color: #88909b; font-weight: 400; padding-top: 5px;">选择要在亚马逊上投放的目标商品</div>
+          </el-radio>
+        </el-radio-group>
+      </div>
+
+      <KeywordTarget v-if="deliveryTypeRadio === 'keyword'"></KeywordTarget>
+      <NegativeWord v-if="deliveryTypeRadio === 'keyword'"></NegativeWord>
+      <ProductOrientation v-if="deliveryTypeRadio === 'commodity'"></ProductOrientation>
+      <NegativeGood v-if="deliveryTypeRadio === 'commodity'"></NegativeGood>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import KeywordTarget from './KeywordTarget.vue'
+import NegativeWord from './NegativeWord.vue'
+import ProductOrientation from './ProductOrientation.vue'
+import NegativeGood from './NegativeGood.vue'
+
+const deliveryTypeRadio = ref('keyword')
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+::v-deep(.delivery-type-radio-group .el-radio__label) {
+  width: 100%;
+}
+.delivery-type-radio {
+  height: auto;
+  flex: 1;
+  align-items: flex-start;
+  padding: 15px 10px 15px 10px;
+}
+::v-deep(.delivery-type-radio-group .el-radio-group .el-radio__inner) {
+  /* 着陆页单选按钮 */
+  margin-top: 3px;
+}
+::v-deep(.delivery-type-radio-group .el-radio.is-bordered.is-checked.el-radio--default.delivery-type-radio) {
+  /* 选中后的背景颜色 */
+  background: #f5f7fe;
+}
+</style>

+ 181 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/KeywordTarget.vue

@@ -0,0 +1,181 @@
+<template>
+  <div style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px;font-weight: 700;">关键词定向</span>
+    </el-divider>
+    <div style="width: 100%; height: 600px; margin-top: 10px; display: flex; border: 1px solid #e5e7eb; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7eb">
+        <el-tabs v-model="keyWordsTabs" class="demo-tabs">
+          <div style="margin: 8px 10px 10px 10px">
+            <p style=" margin-bottom: -5px; font-weight: 500; color: #616266">竞价:</p>
+            <div style="display: flex; align-items: center; margin-left:-7px">
+              <el-select v-model="bidType" class="m-2" placeholder="Select" style="width: 450px">
+                <el-option v-for="item in bidTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="bidInput" placeholder="Please input">
+                <template #prepend>$</template>
+              </el-input>
+            </div>
+            <div style="display: flex; align-items: center;">
+              <span style="margin: 0 10px 0 0; font-weight: 500; color: #616266">匹配类型: </span>
+              <el-checkbox v-model="widelyType" label="广泛" />
+              <el-checkbox v-model="phraseType" label="词组" />
+              <el-checkbox v-model="exactType" label="精确" />
+            </div>
+          </div>
+          <el-tab-pane label="建议" name="first">
+            <el-table
+              height="425"
+              v-loading="loading"
+              :data="keywordsTableData"
+              :header-cell-style="headerCellStyle"
+              :header-row-style="changeKeyWordsTableHeader">
+              <el-table-column prop="asin" label="关键词"> </el-table-column>
+              <el-table-column prop="name" label="匹配类型"> </el-table-column>
+              <el-table-column prop="name" label="建议出价" width="120"> </el-table-column>
+            </el-table>
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input v-model="keyWordsTextarea" :rows="10" type="textarea" placeholder="未完成" style="padding-left: 5px" />
+            <el-button type="primary" size="small" text bg @click="addKeyWords">添加</el-button>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{  }}</span>
+              <el-button class="button" text bg @click="delAllKeyWords">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body" body-style="padding-bottom: -20px;">
+            <el-table
+              :data="addedKeyWordsTableData"
+              style="width: 100%; height: 450px"
+              :header-row-style="changeKeyWordsTableHeader"
+              :header-cell-style="headerCellStyle">
+              <el-table-column prop="keyword" label="关键词" width="auto" />
+              <el-table-column prop="bid" label="出价" />
+              <el-table-column prop="suggestBid" label="建议出价" />
+              <el-table-column prop="operate" label="操作" width="60" align="right" />
+            </el-table>
+          </div>
+        </el-card>
+        <div style="display: flex; justify-content: space-around; padding-top: 0px">
+          <el-button type="primary" plain>保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+
+
+const keyWordsTabs = ref('first')
+const bidInput = ref('0.75')
+const keywordsTableData = ref([]) // 关键词定向左侧表格数据
+const addedKeyWordsTableData = ref([]) // 关键词定向右侧表格数据
+const keyWordsTextarea = ref('')
+
+let widelyType = ref(true)
+let phraseType = ref(true)
+let exactType = ref(true)
+
+const loading = ref(false)
+const bidType = ref('customBid')
+const bidTypeOptions = [
+  {
+    value: 'suggestBid',
+    label: '建议出价',
+  },
+  {
+    value: 'customBid',
+    label: '自定义出价',
+  },
+  {
+    value: 'defaultBid',
+    label: '默认出价',
+  },
+]
+
+
+
+function delAllKeyWords() {
+  // 删除已经添加的关键词
+  addedKeyWordsTableData.value = []
+}
+
+function addKeyWords() {
+  // 添加关键词
+  if (keyWordsTextarea.value) {
+    addedKeyWordsTableData.value.push({
+      keyWords: keyWordsTextarea.value,
+    })
+    keyWordsTextarea.value = ''
+  }
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+function changeKeyWordsTableHeader(args) {
+  if (args.rowIndex === 0) {
+    return {
+      color: '#505968',
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 左侧表格Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  margin-right: 10px;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+// ::v-deep(div#pane-first) {
+//   margin-left: 15px;
+// }
+</style>

+ 398 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeGood.vue

@@ -0,0 +1,398 @@
+<template>
+  <div prop="matchType" style="width: 100%; margin-top: 20px">
+    <div style="width: 100%; height: 600px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px" v-loading="negativeGoodsLoading">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="negativeTabs" class="demo-tabs">
+          <el-tab-pane label="搜索" name="first">
+            <div style="margin-bottom: 10px">
+              <el-input placeholder="按ASIN搜索" v-model="negativeInput" @change="searchNegativeGoods" clearable />
+            </div>
+            <el-table
+              height="495"
+              style="width: 100%"
+              v-loading="loading"
+              :data="negativeTableData"
+              :header-cell-style="headerCellStyle"
+              :show-header="false">
+              <el-table-column prop="asin" label="商品">
+                <template #default="scope">
+                  <div style="display: flex; align-items: center">
+                    <div style="margin-right: 8px; line-height: normal">
+                      <el-image class="img-box" :src="scope.row.image_link" />
+                    </div>
+                    <div>
+                      <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                        <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                      </el-tooltip>
+                      <span>
+                        ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                      </span>
+                    </div>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" label="Name" width="120" align="right">
+                <template #header> </template>
+                <template #default="scope">
+                  <el-button type="primary" size="small" @click="addSingleNegativeGoods(scope)" text>添加</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input
+              v-model="negativeGoodsTextarea"
+              :rows="17"
+              type="textarea"
+              placeholder="未完成"
+              maxlength="11000"
+              style="padding: 10px 10px" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button style="margin-right: 10px" type="primary" text bg @click="addNegativeGoods">添加</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedNegetiveTableData.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllNegativeGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -30px">
+          <el-table
+            :data="addedNegetiveTableData"
+            height="473"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedNegGoods">
+            <el-table-column prop="asin" label="商品">
+              <template #default="scope">
+                <div style="display: flex; align-items: center">
+                  <div style="margin-right: 8px; line-height: normal">
+                    <el-image class="img-box" :src="scope.row.image_link" />
+                  </div>
+                  <div>
+                    <el-tooltip class="box-item" effect="dark" :content="scope.row.title" placement="top">
+                      <div class="single-line">{{ scope.row.title ? scope.row.title : '--' }}</div>
+                    </el-tooltip>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="120" align="right">
+              <template #default="scope">
+                <el-button type="primary" size="small" @click="delSingleNegativeGoods(scope)" text>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div style="display: flex; justify-content: space-around; padding-top: 10px">
+          <el-button plain type="primary" @click="negativeGoodsSave" :disabled="!addedNegetiveTableData.length">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Ref, inject, reactive, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+import { ElMessage } from 'element-plus'
+import { request } from '/@/utils/service'
+
+let selections = [] // 添加选中的项
+let addedSels = [] // 删除选中的项
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const negativeTabs = ref('first')
+const loading = ref(false)
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const negativeTableData = ref([])
+const addedNegetiveTableData = ref([])
+let negativeGoodsLoading = ref(false)
+let negativeList = reactive([])
+const tableData = negativeList
+let inputAddedNegGoods = ref([])
+const negativeInput = ref('')
+
+function setNegativeTableData(asin = '') {
+  negativeGoodsLoading.value = true
+  return request({
+    url: '/api/sellers/listings/all/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+    },
+  })
+    .then((resp) => {
+      negativeTableData.value = resp.data
+      inputAddedNegGoods.value = resp.data
+      negativeGoodsLoading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      negativeGoodsLoading.value = false
+    })
+}
+
+ const negativeGoodsTextarea = ref('')
+// 输入tab的textarea
+function addNegativeGoods() {
+  console.log('negativeGoodsTextarea', negativeGoodsTextarea.value)
+  loading.value = true
+
+  setNegativeTableData(negativeGoodsTextarea.value)
+    .then(() => {
+      addedNegetiveTableData.value = [...addedNegetiveTableData.value, ...inputAddedNegGoods.value]
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+
+function addSingleNegativeGoods(scope) {
+  const isAlreadyAdded = addedNegetiveTableData.value.some((item) => item.asin === scope.row.asin)
+  if (!isAlreadyAdded) {
+    addedNegetiveTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function delAllNegativeGoods() {
+  addedNegetiveTableData.value = []
+}
+
+function delSingleNegativeGoods(scope) {
+  const index = addedNegetiveTableData.value.findIndex((item) => item.asin === scope.row.asin)
+
+  if (index !== -1) {
+    addedNegetiveTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function searchNegativeGoods(e) {
+  console.log(e)
+  if (e === '') {
+    negativeTableData.value = []
+  } else {
+    setNegativeTableData(e)
+  }
+}
+
+function handleAddedNegGoods(selection) {
+  addedSels = selection
+}
+
+
+async function negativeGoodsSave() {
+  console.log(addedNegetiveTableData.value)
+  const asinList = addedNegetiveTableData.value.map((item) => item.asin)
+  console.log('🚀 ~ negativeGoodsSave ~ asinList-->>', asinList)
+  negativeGoodsLoading.value = true
+  console.log('addedNegetiveTableData', addedNegetiveTableData.value)
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinList: asinList,
+      matchType: 'ASIN_SAME_AS',
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/sptargets/add/negative/targets/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ negativeWordsSave ~ resp-->>', resp)
+    negativeGoodsLoading.value = false
+    if (resp.data.success.length !== 0) {
+      ElMessage({
+        message: '否定商品创建成功',
+        type: 'success',
+      })
+      delAllNegative()
+    } else {
+      ElMessage.error('否定商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function delAllNegative() {
+  negativeList.length = 0
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.column-item .el-radio-group {
+  display: inline-flex;
+  font-size: 0;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.radio-description {
+  font-size: 12px;
+  color: #666;
+  margin-top: -18px;
+  margin-left: 22px;
+}
+.radio-description-2 {
+  font-size: 12px;
+  color: #666;
+  margin-top: -10px;
+}
+.column-margin-bottom label.el-radio.is-bordered {
+  margin-bottom: 10px;
+  padding: 35px;
+}
+::v-deep(.column-margin-bottom label.el-radio.is-bordered span.el-radio__inner) {
+  margin-top: -18px;
+  margin-left: -15px;
+}
+.gap-items {
+  display: flex;
+  justify-content: flex-start;
+  width: 100%;
+  margin-bottom: 20px;
+}
+.gap-item {
+  width: 200px;
+  margin-left: 30px;
+  color: #0b0d0d;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-tabs__nav-wrap::after) {
+  height: 2px !important;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+// 输入底部样式
+::v-deep(.card-box .el-card__body) {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  // margin: 10px 0 10px 10px;
+  margin-right: 10px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.target-group-item {
+  margin-top: 15px;
+}
+.suggested-bid-item {
+  margin-left: 230px;
+  margin-right: 60px;
+}
+.bid-input {
+  width: 200px;
+  margin-left: 15px;
+}
+
+::v-deep(.goods-orientation-tabs .el-tabs__nav-scroll) {
+  margin-left: -20px !important;
+}
+::v-deep(.category-tabs .el-tabs__nav) {
+  margin-left: 20px;
+}
+::v-deep(.goods-orientation-tabs #tab-1) {
+  /* 商品定向Tab栏 */
+  border-right: 0;
+}
+.custom-tree-node {
+  /* el-tree自定义样式 */
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+.dialog-head {
+  /* 弹窗样式 */
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+</style>

+ 247 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeWord.vue

@@ -0,0 +1,247 @@
+<template>
+  <div style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px;font-weight: 700;">否定词</span>
+    </el-divider>
+    <div style="width: 100%; height: 520px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <div style="margin: 10px 0">
+          <span style="margin-left: 25px; color: #e47470">*</span>
+          <span style="color: #666666; margin-right: 10px">匹配类型: </span>
+          <el-checkbox v-model="NEGATIVE_PHRASE" label="词组否定" />
+          <el-checkbox v-model="NEGATIVE_EXACT" label="精确否定" />
+        </div>
+        <el-input
+          v-model="negativeWordsTextarea"
+          :rows="17"
+          type="textarea"
+          placeholder="请输入关键词,多个关键词使用逗号或者换行符分隔。(最多添加1000个关键词)"
+          maxlength="11000"
+          style="padding: 0 20px" />
+        <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+          <el-button style="margin-right: 18px" type="primary" text bg @click="addNegative">添加</el-button>
+        </div>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ negativeList.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllNegative">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body">
+            <el-table :data="negativeList" style="width: 100%; height: 370px; padding-bottom: 0" :header-row-style="changeNegTableHeader">
+              <el-table-column prop="negativeWords" label="否定词" width="auto" />
+              <el-table-column prop="operate" label="操作" width="60" align="right">
+                <template #default="scope">
+                  <el-button type="primary" size="small" @click="delSingleNegative(scope)" text>删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-card>
+        <div style="display: flex; justify-content: space-around">
+          <el-button type="primary" plain @click="negativeWordsSave" :disabled="!negativeList.length">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { Ref, inject, reactive, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+import { postNegativeWordData } from '../api/index'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+const tableData = reactive([])
+let negativeWordsLoading = ref(false)
+let negativeList = reactive([])
+let negativeWordsTextarea = ref('')
+let NEGATIVE_PHRASE = ref(true)
+let NEGATIVE_EXACT = ref(true)
+let exactNegativeList = reactive([]) // 用于存储精确否定的数组
+let phraseNegativeList = reactive([]) // 用于存储词组否定的数组
+
+function addNegative() {
+  const trimmedText = negativeWordsTextarea.value.trim()
+  const items = trimmedText.split(/,|\n/)
+
+  items.forEach((item) => {
+    const trimmedItem = item.trim()
+    if (trimmedItem) {
+      let phraseEntry = '词组: ' + trimmedItem
+      let exactEntry = '精确: ' + trimmedItem
+
+      if (NEGATIVE_PHRASE.value && !negativeList.some((n) => n.negativeWords === phraseEntry)) {
+        negativeList.push({ negativeWords: phraseEntry })
+        phraseNegativeList.push(trimmedItem) // 添加到词组否定数组
+      }
+      if (NEGATIVE_EXACT.value && !negativeList.some((n) => n.negativeWords === exactEntry)) {
+        negativeList.push({ negativeWords: exactEntry })
+        exactNegativeList.push(trimmedItem) // 添加到精确否定数组
+      }
+    } else {
+      console.log('有空项目,未被添加到列表中')
+    }
+  })
+
+  negativeWordsTextarea.value = ''
+  console.log('🚀 ~ exactNegativeList-->>', exactNegativeList)
+  console.log('🚀 ~ phraseNegativeList-->>', phraseNegativeList)
+}
+
+function delAllNegative() {
+  // negativeList.splice(0, negativeList.length)
+  negativeList.length = 0
+  exactNegativeList.length = 0
+  phraseNegativeList.length = 0
+}
+
+function delSingleNegative(scope) {
+  const index = negativeList.findIndex((item) => item.negativeWords === scope.row.negativeWords)
+  if (index !== -1) {
+    // 确定被删除的项是词组还是精确
+    const isPhrase = scope.row.negativeWords.startsWith('词组: ')
+    const isExact = scope.row.negativeWords.startsWith('精确: ')
+
+    // 从 negativeList 删除
+    if (negativeList.length) {
+      negativeList.splice(index, 1)
+      console.log(`已删除索引为 ${index} 的条目`)
+    } else {
+      console.log('无效的索引,无法删除条目')
+    }
+
+    // 从 exactNegativeList 或 phraseNegativeList 删除
+    const trimmedItem = scope.row.negativeWords.substring(4).trim() // 从 '词组: ' 或 '精确: ' 后开始截取
+    if (isPhrase) {
+      const phraseIndex = phraseNegativeList.findIndex((item) => item === trimmedItem)
+      if (phraseIndex !== -1) {
+        phraseNegativeList.splice(phraseIndex, 1)
+      }
+    } else if (isExact) {
+      const exactIndex = exactNegativeList.findIndex((item) => item === trimmedItem)
+      if (exactIndex !== -1) {
+        exactNegativeList.splice(exactIndex, 1)
+      }
+    }
+  } else {
+    console.log('无效的索引,无法删除条目')
+  }
+}
+
+async function negativeWordsSave() {
+  negativeWordsLoading.value = true
+  console.log('negativeList', negativeList)
+  createNegativeWords()
+  // try {
+  //   const requestData = {
+  //     profile_id: profile.value.profile_id,
+  //     campaignId: respCampaignId.value,
+  //     adGroupId: respAdGroupId.value,
+  //     state: 'PAUSED',
+  //     EkeywordList: exactNegativeList,
+  //     PkeywordList: phraseNegativeList,
+  //   }
+  //   const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+
+  //   console.log('🚀 ~ negativeWordsSave ~ resp-->>', resp)
+  //   negativeWordsLoading.value = false
+  //   if (resp.data.negativeKeyworderror.length !== 0) {
+  //     ElMessage({
+  //       message: '否定词创建成功',
+  //       type: 'success',
+  //     })
+  //     delAllNegative()
+  //   } else {
+  //     ElMessage.error('否定词创建失败!')
+  //   }
+  // } catch (error) {
+  //   console.error('请求失败:', error)
+  // }
+}
+
+async function createNegativeWords() {
+  const negativeWordsData = {
+    profile_id: profile.value.profile_id,
+    campaignId: respCampaignId.value,
+    adGroupId: respAdGroupId.value,
+    state: 'PAUSED',
+    EkeywordList: exactNegativeList,
+    PkeywordList: phraseNegativeList,
+  }
+  const filteredRequestData = Object.fromEntries(Object.entries(negativeWordsData).filter(([_, v]) => v != null))
+  try {
+    const response = await postNegativeWordData(filteredRequestData)
+    console.log('🚀 ~ createCampaigns ~ response-->>', response)
+      if (response.data.negativeKeyworderror.length !== 0) {
+      ElMessage({
+        message: '否定词创建成功',
+        type: 'success',
+      })
+      delAllNegative()
+    } else {
+      ElMessage.error('否定词创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    negativeWordsLoading.value = false
+  }
+}
+
+function changeNegTableHeader(args) {
+  if (args.rowIndex === 0) {
+    return {
+      color: '#505968',
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 左侧表格Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  margin-right: 10px;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+</style>

+ 808 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductOrientation.vue

@@ -0,0 +1,808 @@
+<template>
+  <div style="width: 100%; margin-top: 20px">
+    <el-divider content-position="left">
+      <span style="font-size: 18px; font-weight: 700">商品定向</span>
+    </el-divider>
+    <div style="width: 100%; height: 600px; display: flex; border: 1px solid #e5e7eb; border-radius: 6px" v-loading="productOrientationLoading">
+      <div style="width: 50%; border-right: 1px solid #e5e7eb">
+        <el-tabs
+          type="border-card"
+          stretch
+          class="goods-orientation-tabs"
+          style="border: 0; border-right: 0; border-bottom-left-radius: 6px; border-top-left-radius: 5px; overflow: hidden">
+          <el-tab-pane label="品类" style="border-top-left-radius: 6px">
+            <div style="display: flex; align-items: center">
+              <span style="width: 40px">竞价:</span>
+              <el-select v-model="categoryBiddingType" @change="singleGoodsBidSelectChanged" class="m-2" placeholder="Select">
+                <el-option v-for="item in categoryBiddingTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="singleGoodsBidInput" :disabled="categoryBiddingType === 'defaultBid'" style="width: 200px">
+                <template #prepend>$</template>
+              </el-input>
+            </div>
+
+            <el-tabs v-model="categoryTabs" class="category-tabs">
+              <el-tab-pane label="建议" name="first">
+                <el-table :data="proposalTableData" style="width: 100%" height="422">
+                  <el-table-column prop="proposal" label="建议" width="520">
+                    <template #header> 0建议 </template>
+                  </el-table-column>
+                  <el-table-column prop="address" label="Address">
+                    <template #header>
+                      <el-button type="primary" size="normal" link @click="handleGoodsAdd">全部添加</el-button>
+                    </template>
+                    <template #default="scope">
+                      <el-button type="primary" size="small" @click="addSingleGoods(scope)" text>添加</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </el-tab-pane>
+              <el-tab-pane label="搜索" name="second">
+                <el-input placeholder="请输入关键词过滤" />
+                <el-scrollbar height="390px">
+                  <el-tree :data="searchClassifyTableData" :props="defaultProps">
+                    <template #default="{ node, data }">
+                      <span class="custom-tree-node">
+                        <span style="width: 75%">{{ node.label }}</span>
+                        <span style="color: rgb(50, 108, 216)" v-if="data.ta == true">
+                          <a @click="refine(data)"> 细化 </a>
+                          <a style="margin-left: 8px" @click="orientate(node, data)"> 定向 </a>
+                        </span>
+                      </span>
+                    </template>
+                  </el-tree>
+                </el-scrollbar>
+                <el-dialog v-model="visible" :title="`细化分类: ${dialogTitle}`" @close="dialogClose" destroy-on-close>
+                  <div style="display: flex; justify-content: space-between">
+                    <span>根据特定品牌、价格范围、星级和Prime配送资格,细化分类</span>
+                    <span>
+                      <el-checkbox v-model="dialogForm.isCount" label="显示商品数量" @change="isCountChanged" />
+                    </span>
+                  </div>
+                  <el-form :model="dialogForm" :rules="dialogRules" ref="dialogFormRef" style="margin-top: 20px">
+                    <el-form-item style="padding-left: 140px">
+                      <span style="margin-right: 10px; color: #616266; font-weight: 500">品牌</span>
+                      <el-select
+                        v-model="dialogForm.dialogselectValue"
+                        @change="dialogSelectChange"
+                        multiple
+                        placeholder="请选择"
+                        :loading="dialogSelectLoading">
+                        <el-option v-for="item in dialogForm.dialogOptions" :key="item.value" :label="item.label" :value="item.value" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item prop="prices" style="padding-left: 112px; margin-top: 10px">
+                      <span style="margin-right: 10px; color: #616266; font-weight: 500">价格范围</span>
+                      <el-input-number v-model="dialogForm.prices.lowest" :min="1" :controls="false" placeholder="无最低商品价格" />
+                      --
+                      <el-input-number v-model="dialogForm.prices.highest" :min="1" :controls="false" placeholder="无最高商品价格" />
+                    </el-form-item>
+                    <el-form-item prop="starRating" style="padding-left: 85px; margin-top: 10px">
+                      <span style="margin-right: 15px; color: #616266; font-weight: 500">查看星级评定</span>
+                      <el-slider v-model="dialogForm.starRating" range show-stops :max="5" :marks="marks" style="width: 70%" />
+                    </el-form-item>
+                    <el-form-item prop="delivery" style="padding-left: 140px; margin-top: 30px">
+                      <span style="margin-right: 10px; color: #616266; font-weight: 500">配送</span>
+                      <el-radio-group v-model="dialogForm.delivery">
+                        <el-radio label="all" style="font-weight: 400">所有</el-radio>
+                        <el-radio label="eligible" style="font-weight: 400">具备Prime资格</el-radio>
+                        <el-radio label="diseligible" style="font-weight: 400">不具备Prime资格</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                  </el-form>
+                  <template #footer>
+                    <div style="display: flex; justify-content: space-between">
+                      <span v-loading="countLoadig"
+                        >定位到的商品数量:
+                        <span v-if="dialogForm.isCount == true">{{ commodityCount[0]?.min }} - {{ commodityCount[0]?.max }}</span></span
+                      >
+                      <span class="dialog-footer">
+                        <el-button @click="visible = false">取消</el-button>
+                        <el-button type="primary" @click="dialogFormSubmit">确定</el-button>
+                      </span>
+                    </div>
+                  </template>
+                </el-dialog>
+              </el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+          <el-tab-pane label="单个商品">
+            <div style="display: flex; align-items: center">
+              <span style="width: 40px">竞价:</span>
+              <el-select class="m-2" v-model="singleGoodsBidSelect" @change="singleGoodsBidSelectChanged">
+                <el-option v-for="item in singleGoodsBidTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="singleGoodsBidInput" :disabled="singleGoodsBidSelect == 'defaultBid'" style="width: 200px">
+                <template #prepend>$</template>
+              </el-input>
+              <!-- <div style="margin-left: 20px">
+                <span style="margin-right: 10px">类型:</span>
+                <el-checkbox v-model="expand" label="扩展" />
+                <el-checkbox v-model="accurate" label="精准" />
+              </div> -->
+            </div>
+            <el-tabs v-model="singleGoodsTabs" class="category-tabs">
+              <el-tab-pane label="建议" name="first">
+                <el-table :data="proposalTableData" style="width: 100%" height="342">
+                  <el-table-column prop="proposal" label="商品" width="520" />
+                  <el-table-column prop="address" label="类型" />
+                  <el-table-column prop="operational" label="操作" />
+                </el-table>
+              </el-tab-pane>
+              <el-tab-pane label="搜索" name="second">
+                <el-input v-model="singleGoodsSearchInp" @change="singleGoodsSearchChaneged" placeholder="按ASIN搜索"></el-input>
+                <el-table :data="searchTableData" style="width: 100%" height="309">
+                  <el-table-column prop="asin" label="商品" width="520">
+                    <template #default="{ row }">
+                      <div style="display: flex; align-items: center">
+                        <img :src="row.image_link" style="width: 40px; height: 40px; margin-right: 10px" />
+                        <span>{{ row.title }}</span>
+                      </div>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="productTypes" label="类型">
+                    <template #default="scope">
+                      <div v-if="expand">扩展</div>
+                      <div v-if="accurate">精准</div>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="operational" label="操作">
+                    <template #default="scope">
+                      <el-button class="button" text @click="addSingleSearch(scope)">添加</el-button>
+                    </template>
+                  </el-table-column>
+                </el-table>
+              </el-tab-pane>
+              <!-- TODO: 商品定向TextArea -->
+              <el-tab-pane label="输入" name="third">待完成</el-tab-pane>
+            </el-tabs>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: none">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ productOrientationTableData.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllCna">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body">
+            <el-table
+              height="460"
+              :data="productOrientationTableData"
+              style="width: 100%"
+              :header-row-style="changeKeyWordsTableHeader"
+              :header-cell-style="headerCellStyle">
+              <el-table-column prop="cna" label="分类 & 商品" width="300">
+                <template #default="scope">
+                  <div v-if="scope.row.cna || scope.row.classification">
+                    分类: <span style="color: #000000">{{ scope.row.cna ? scope.row.cna : scope.row.classification }}</span>
+                  </div>
+                  <div v-if="scope.row.asin">
+                    {{ scope.row.asin ? scope.row.asin : '--' }}
+                  </div>
+                  <div v-if="scope.row.brand">
+                    品牌: <span style="color: #000000">{{ scope.row.brand }}</span>
+                  </div>
+                  <div v-if="scope.row.low_price || scope.row.high_price">
+                    品牌价格:
+                    <span style="color: #000000">
+                      {{ scope.row.low_price ? '$' + scope.row.low_price : '--' }} -
+                      {{ scope.row.high_price ? '$' + scope.row.high_price : '--' }}
+                    </span>
+                  </div>
+                  <div v-if="scope.row.low_rating || scope.row.high_rating">
+                    评分: <span style="color: #000000">{{ scope.row.low_rating }} - {{ scope.row.high_rating }}</span>
+                  </div>
+                  <div v-if="scope.row.deliveryText">
+                    配送: <span style="color: #000000">{{ scope.row.deliveryText }}</span>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="type" label="类型">
+                <template #default="scope">
+                  {{ scope.row.productTypeText ? scope.row.productTypeText : '--' }}
+                </template>
+              </el-table-column>
+              <el-table-column prop="bid" label="竞价">
+                <template #default="scope">
+                  <el-input-number v-model="scope.row.bid" :min="0.02" :max="1000000" :controls="false" size="small" />
+                </template>
+              </el-table-column>
+              <el-table-column prop="operate" label="操作" width="60" align="right">
+                <template #default="scope">
+                  <el-button text size="small" @click="delCna(scope.$index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-card>
+        <div style="display: flex; justify-content: space-around; margin-top: -8px">
+          <el-button type="primary" plain @click="productTagetSave">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { CSSProperties, Ref, inject, reactive, ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+import { ElMessage } from 'element-plus'
+import { request } from '/@/utils/service'
+
+
+let adsTableData = ref([])
+let selections = [] // 添加选中的项
+let addedSels = [] // 删除选中的项
+
+const singleGoodsTabs = ref('first')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const categoryTabs = ref('first')
+const categoryBiddingType = ref('customBid')
+const categoryBiddingTypeOptions = [
+  {
+    value: 'defaultBid',
+    label: '默认竞价',
+  },
+  {
+    value: 'customBid',
+    label: '自定义竞价',
+  },
+]
+const singleGoodsBidSelect = ref('customBid')
+const singleGoodsBidTypeOptions = [
+  {
+    value: 'defaultBid',
+    label: '默认竞价',
+  },
+  {
+    value: 'customBid',
+    label: '自定义竞价',
+  },
+]
+const singleGoodsBidInput = ref('0.75')
+const expand = ref(true)
+const accurate = ref(false)
+const proposalTableData = ref([])
+const searchClassifyTableData = ref([])
+const productOrientationLoading = ref(false)
+const dialogSelectLoading = ref(false)
+const defaultProps = {
+  children: 'ch',
+  label: 'cna',
+}
+const countLoadig = ref(false)
+const visible = ref(false)
+let dialogTitle = ref('')
+let categoryId = ref('')
+
+const dialogForm: any = reactive({
+  prices: {
+    lowest: undefined,
+    highest: undefined,
+  },
+  starRating: [0, 5],
+  dialogselectValue: [],
+  delivery: 'all',
+  isCount: false,
+})
+const dialogFormRef = ref()
+const dialogRules = reactive({
+  prices: [{ validator: validatePrices, trigger: 'blur' }],
+})
+
+interface Mark {
+  style: CSSProperties
+  label: string
+}
+type Marks = Record<number, Mark | string>
+const marks = reactive<Marks>({
+  0: '0',
+  1: '1',
+  2: '2',
+  3: '3',
+  4: '4',
+  5: '5',
+})
+let commodityCount = ref([])
+let currentDialogIndex = ref(0)
+let productOrientationTableData = ref([])
+
+async function validatePrices(rule, value) {
+  if (value.highest !== '' && value.lowest !== '' && value.highest <= value.lowest) {
+    return Promise.reject('最高价格必须大于最低价格')
+  }
+  return Promise.resolve()
+}
+
+async function setProductOrientationData() {
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/targetable/categories/',
+      method: 'GET',
+      params: {
+        profile_id: profile.value.profile_id,
+      },
+    })
+    searchClassifyTableData.value = resp.data
+    productOrientationLoading.value = false
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+async function setDialogOption() {
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/categories/brands/',
+      method: 'GET',
+      params: {
+        profile_id: profile.value.profile_id,
+        category_id: categoryId.value,
+      },
+    })
+    const options = resp.data
+    dialogForm.dialogOptions = options.brands.map((brand) => {
+      return {
+        label: brand.name,
+        value: brand.id,
+      }
+    })
+    dialogSelectLoading.value = false
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+async function getCount(instanceId) {
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/products/count/',
+      method: 'POST',
+      data: {
+        profile_id: profile.value.profile_id,
+        category_id: categoryId.value,
+      },
+    })
+    if (instanceId === currentDialogIndex.value) {
+      commodityCount.value = resp.data.AsinCounts
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    if (instanceId === currentDialogIndex.value) {
+      countLoadig.value = false
+    }
+  }
+}
+
+function dialogClose() {
+  currentDialogIndex.value++
+  resetDialogForm()
+  dialogForm.isCount = false
+  commodityCount.value = []
+  countLoadig.value = false
+}
+
+function resetDialogForm() {
+  dialogForm.prices.lowest = undefined
+  dialogForm.prices.highest = undefined
+  dialogForm.starRating = [0, 5]
+  dialogForm.dialogselectValue = []
+  dialogForm.delivery = 'all'
+  dialogForm.isCount = false
+}
+
+function isCountChanged() {
+  if (dialogForm.isCount) {
+    const instanceId = currentDialogIndex.value
+    countLoadig.value = true
+    getCount(instanceId)
+  } else {
+    countLoadig.value = false
+    commodityCount.value = []
+  }
+}
+
+function delCna(index) {
+  productOrientationTableData.value.splice(index, 1)
+}
+
+function delAllCna() {
+  productOrientationTableData.value = []
+}
+
+function singleGoodsBidSelectChanged() {
+  if (singleGoodsBidSelect.value === 'defaultBid' || categoryBiddingType.value === 'defaultBid') {
+    singleGoodsBidInput.value = ''
+  }
+}
+
+let singleGoodsSearchInp = ref('')
+let searchTableData = ref([])
+function setSearchTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      searchTableData.value = resp.data
+      productOrientationLoading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      productOrientationLoading.value = false
+    })
+}
+function singleGoodsSearchChaneged() {
+  productOrientationLoading.value = true
+  setSearchTableData()
+}
+function addSingleSearch(scope) {
+  console.log('🚀 ~ addSingleSearch ~ scope-->>', scope)
+
+  const typesToAdd = []
+  if (expand.value) {
+    typesToAdd.push('ASIN_EXPANDED_FROM')
+  }
+  if (accurate.value) {
+    typesToAdd.push('ASIN_SAME_AS')
+  }
+  const productTypeMap = {
+    ASIN_EXPANDED_FROM: '扩展',
+    ASIN_SAME_AS: '精确',
+  }
+
+  typesToAdd.forEach((productType) => {
+    const isAlreadyAdded = productOrientationTableData.value.some((item) => item.sku === scope.row.sku && item.productType === productType)
+
+    if (!isAlreadyAdded) {
+      const newData = {
+        type: 'p',
+        asin: scope.row.asin,
+        sku: scope.row.sku,
+        productType: productType,
+        productTypeText: productTypeMap[productType],
+      }
+      productOrientationTableData.value.push(newData)
+    } else {
+      console.log(`${productType} item is already added.`)
+    }
+  })
+}
+
+let selectedLabels = ref([]) // 选中的label数组
+function dialogSelectChange(event) {
+  console.log('🚀 ~ dialogSelectChange ~ event-->>', event)
+
+  // 使用 map 来转换每个选中项的 value 为其对应的 label
+  selectedLabels.value = event.map((selectedValue) => {
+    const selectedOption = dialogForm.dialogOptions.find((option) => option.value === selectedValue)
+    return selectedOption ? selectedOption.label : ''
+  })
+
+  console.log('🚀 ~ dialogSelectChange ~ selectedLabels-->>', selectedLabels.value)
+}
+
+let refineItem = ref([])
+// 细化按钮功能
+function refine(data) {
+  console.log('🚀 ~ refine ~ data-->>', data)
+  commodityCount.value = []
+  dialogTitle.value = data.cna
+  categoryId.value = data.cid
+  refineItem.value.push(data)
+  visible.value = true
+  dialogSelectLoading.value = true
+  setDialogOption()
+}
+// 弹框提交功能
+function dialogFormSubmit() {
+  dialogFormRef.value.validate((valid) => {
+    if (valid) {
+      console.log('表单提交')
+      visible.value = false
+      const dialogClassification = dialogTitle.value
+      const dialogPrices_low = dialogForm.prices.lowest
+      const dialogPrices_high = dialogForm.prices.highest
+      const dialogStartRating = dialogForm.starRating
+      const ratingLow = dialogStartRating[0]
+      const ratingHigh = dialogStartRating[1]
+      const dialogDelivery = dialogForm.delivery
+      console.log('🚀 ~ dialogFormRef.value.validate ~ dialogDelivery-->>', dialogDelivery)
+      const deliveryMap = {
+        all: '所有',
+        eligible: '具备Prime资格',
+        diseligible: '不具备Prime资格',
+      }
+
+      selectedLabels.value.forEach((brandLabel) => {
+        // 查找与当前 brandLabel 相对应的选项
+        const selectedOption = dialogForm.dialogOptions.find((option) => option.label === brandLabel)
+        // 获取对应的 brandId,如果没有找到则默认为空
+        const brandId = selectedOption ? selectedOption.value : ''
+        const refineObj = {
+          type: 'c',
+          classification: dialogClassification,
+          classificationId: categoryId.value,
+          brand: brandLabel,
+          brandId: brandId, // 使用找到的 brandId
+          low_price: dialogPrices_low,
+          high_price: dialogPrices_high,
+          low_rating: ratingLow,
+          high_rating: ratingHigh,
+          delivery: dialogDelivery,
+          deliveryText: deliveryMap[dialogDelivery],
+        }
+        console.log('🚀 ~ dialogFormRef.value.validate ~ refineObj-->>', refineObj)
+        productOrientationTableData.value.push(refineObj)
+      })
+    } else {
+      console.log('验证失败')
+    }
+  })
+}
+
+// 定向按钮功能
+function orientate(node, data) {
+  console.log('🚀 ~ orientate ~ data-->>', data)
+  const exists = productOrientationTableData.value.some((item) => item.cid === data.cid)
+
+  if (!exists) {
+    const newData = {
+      type: 'c',
+      classification: data.cna,
+      classificationId: data.cid,
+    }
+    productOrientationTableData.value.push(newData)
+  }
+}
+
+let productTargetBidList = ref([])
+async function productTagetSave() {
+  console.log('tableData', productOrientationTableData.value)
+  // 检查是否存在 bid 为空的行
+  const hasEmptyBid = productOrientationTableData.value.some((row) => row.bid == null || row.bid === '')
+  // 直接返回,不继续执行
+  if (hasEmptyBid) {
+    console.log('存在空的 bid,不发送请求')
+    ElMessage.error('存在空的 bid,无法创建商品!')
+    return
+  }
+  productOrientationTableData.value.forEach((row) => {
+    productTargetBidList.value.push(row.bid)
+  })
+  console.log('productTargetBidList', productTargetBidList.value)
+  productOrientationLoading.value = true
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      adGroupId: respAdGroupId.value,
+      campaignId: respCampaignId.value,
+      expressionList: productOrientationTableData.value,
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/sptargets/manual/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+
+    console.log('🚀 ~ createTargetGroup ~ resp-->>', resp)
+    productOrientationLoading.value = false
+
+    if (respAdGroupId.value) {
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+
+  // 清空表格和 bid 列表
+  productOrientationTableData.value = []
+  productTargetBidList.value = []
+}
+// 点击表格选项触发事件
+function handleSelectionChange(selection) {
+  selections = selection
+}
+// 获取addedTable中已选中的项
+function handleAddedGoodsChange(selection) {
+  addedSels = selection
+}
+// 添加已选中的项
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !adsTableData.value.some((added) => added.sku === sel.sku) // 使用sku作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    adsTableData.value.push(...newSelections)
+  }
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = adsTableData.value.some((item) => item.sku === scope.row.sku)
+  if (!isAlreadyAdded) {
+    adsTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+function changeKeyWordsTableHeader(args) {
+  if (args.rowIndex === 0) {
+    return {
+      color: '#505968',
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.column-item .el-radio-group {
+  display: inline-flex;
+  font-size: 0;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.radio-description {
+  font-size: 12px;
+  color: #666;
+  margin-top: -18px;
+  margin-left: 22px;
+}
+.radio-description-2 {
+  font-size: 12px;
+  color: #666;
+  margin-top: -10px;
+}
+.column-margin-bottom label.el-radio.is-bordered {
+  margin-bottom: 10px;
+  padding: 35px;
+}
+::v-deep(.column-margin-bottom label.el-radio.is-bordered span.el-radio__inner) {
+  margin-top: -18px;
+  margin-left: -15px;
+}
+.gap-items {
+  display: flex;
+  justify-content: flex-start;
+  width: 100%;
+  margin-bottom: 20px;
+}
+.gap-item {
+  width: 200px;
+  margin-left: 30px;
+  color: #0b0d0d;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.el-tabs__nav-scroll) {
+  overflow: hidden;
+  margin-left: 20px;
+}
+::v-deep(.el-tabs__nav-wrap::after) {
+  height: 2px !important;
+}
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: white;
+}
+// 表格内容边距
+div {
+  & #pane-first,
+  & #pane-second {
+    margin: 10px;
+  }
+}
+// 输入底部样式
+::v-deep(.card-box .el-card__body) {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.box-card {
+  width: 100%;
+  // margin: 10px 0 10px 10px;
+  margin-right: 10px;
+}
+.single-line {
+  color: rgb(30, 33, 41);
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.data-color {
+  color: rgb(30, 33, 41);
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.target-group-item {
+  margin-top: 15px;
+}
+.suggested-bid-item {
+  margin-left: 230px;
+  margin-right: 60px;
+}
+.bid-input {
+  width: 200px;
+  margin-left: 15px;
+}
+
+::v-deep(.goods-orientation-tabs .el-tabs__nav-scroll) {
+  margin-left: -20px !important;
+}
+::v-deep(.category-tabs .el-tabs__nav) {
+  margin-left: 20px;
+}
+::v-deep(.goods-orientation-tabs #tab-1) {
+  /* 商品定向Tab栏 */
+  border-right: 0;
+}
+.custom-tree-node {
+  /* el-tree自定义样式 */
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+.dialog-head {
+  /* 弹窗样式 */
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+</style>
+

+ 3 - 10
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCommodity.vue

@@ -94,7 +94,7 @@
               <el-tooltip content="添加最少3件商品。我们建议添加至少5件商品,以降低在商品缺货时出现广告活动暂停的可能性。" placement="top">
                 <el-text type="warning" truncated style="width: 350px;">添加最少3件商品。我们建议添加至少5件商品,以降低在商品缺货时出现广告活动暂停的可能性</el-text>
               </el-tooltip>
-              <el-button class="button" text bg @click="delAllGoods">全部删除</el-button>
+              <el-button class="button" type="danger" text bg @click="delAllGoods">全部删除</el-button>
             </div>
           </template>
           <div class="card-body"></div>
@@ -136,7 +136,7 @@
             </el-table-column>
             <el-table-column prop="name" label="Name" width="120" align="right">
               <template #header>
-                <el-button type="primary" size="normal" link @click="delSelectedGoods">删除已选中</el-button>
+                <el-button type="danger" size="normal" link @click="delSelectedGoods">删除已选中</el-button>
               </template>
               <template #default="scope">
                 <el-button type="primary" size="small" @click="delSingleGoods(scope)" text>删除</el-button>
@@ -145,7 +145,7 @@
           </el-table>
         </div>
         <div style="display: flex; justify-content: space-around; padding-top: 5px">
-          <el-button type="primary" plain :disabled="productSave" @click="submitProductForm">保存</el-button>
+          <el-button type="primary" plain :disabled="addedTableData.length < 3"  @click="submitProductForm">保存</el-button>
         </div>
       </div>
     </div>
@@ -176,7 +176,6 @@ let selections = []
 let addedSels = []
 const searchInp = ref('')
 const leftSelect = ref('name')
-let productSave = ref(true)
 const buttons = [{ type: 'primary', text: '添加' }] as const
 const productTabs = ref('first')
 const rightSelect = ref('latest')
@@ -314,11 +313,6 @@ watch(
         addedAdsTableItems.value.push({ sku: item.sku, asin: item.asin })
       }
     })
-    if (addedTableData.value.length !== 0) {
-      productSave.value = false
-    } else {
-      productSave.value = true
-    }
   },
   { deep: true }
 )
@@ -341,7 +335,6 @@ async function createAds() {
     console.log('🚀 ~ createAds ~ resp-->>', resp)
     productLoading.value = false
     if (resp.data.success.length > 0) {
-      productSave.value = false
       addedTableData.value = []
       ElMessage({
         message: '商品创建成功',

+ 4 - 4
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCommodity.vue

@@ -57,10 +57,10 @@
               </el-table-column>
               <el-table-column prop="name" label="Name" width="120" align="right">
                 <template #header>
-                  <el-button type="primary" size="normal" link @click="handleGoodsAdd">添加已选中</el-button>
+                  <el-button type="primary" size="normal" link :disabled="addedTableData.length >= 1" @click="handleGoodsAdd">添加已选中</el-button>
                 </template>
                 <template #default="scope">
-                  <el-button type="primary" size="small" @click="addSingleGoods(scope)" text>添加</el-button>
+                  <el-button type="primary" size="small" :disabled="addedTableData.length >= 1" @click="addSingleGoods(scope)" text>添加</el-button>
                 </template>
               </el-table-column>
             </el-table>
@@ -94,7 +94,7 @@
 
                 <el-text type="warning" truncated>最多添加一个产品</el-text>
 
-              <el-button class="button" text bg @click="delAllGoods">全部删除</el-button>
+              <el-button class="button" type="danger" text bg @click="delAllGoods">全部删除</el-button>
             </div>
           </template>
           <div class="card-body"></div>
@@ -136,7 +136,7 @@
             </el-table-column>
             <el-table-column prop="name" label="Name" width="120" align="right">
               <template #header>
-                <el-button type="primary" size="normal" link @click="delSelectedGoods">删除已选中</el-button>
+                <el-button type="danger" size="normal" link @click="delSelectedGoods">删除已选中</el-button>
               </template>
               <template #default="scope">
                 <el-button type="primary" size="small" @click="delSingleGoods(scope)" text>删除</el-button>

+ 2 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/index.vue

@@ -3,6 +3,7 @@
     <AdCampaign @update-campaign="handleCampaignUpdate"></AdCampaign>
     <AdGroup @update-group-id="handleGroupIdUpdate"></AdGroup>
     <AdFormat></AdFormat>
+    <DeliveryType></DeliveryType>
   </div>
 </template>
 
@@ -11,6 +12,7 @@ import { provide, ref } from 'vue'
 import AdCampaign from './component/AdCampaign.vue'
 import AdGroup from './component/AdGroup.vue'
 import AdFormat from './component/AdFormat.vue'
+import DeliveryType from './component/DeliveryType.vue'
 
 
 const respCampaignId = ref('')