Browse Source

Merge branch 'wang' into test

WanGxC 1 year ago
parent
commit
94e68a9ef1
30 changed files with 8601 additions and 105 deletions
  1. 1 1
      .env
  2. 5 0
      src/utils/emitter.ts
  3. 1 1
      src/utils/service.ts
  4. 154 0
      src/views/adManage/sb/campaigns/CreateCampaigns/api/index.ts
  5. 312 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/AdCampaign.vue
  6. 356 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/AdFormat.vue
  7. 111 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/AdGroup.vue
  8. 60 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/DeliveryType.vue
  9. 980 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/FocusCreativity.vue
  10. 181 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/KeywordTarget.vue
  11. 354 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeGood.vue
  12. 247 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/NegativeWord.vue
  13. 810 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductOrientation.vue
  14. 444 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCommodity.vue
  15. 781 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity1.vue
  16. 400 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity2.vue
  17. 464 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCommodity.vue
  18. 898 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity1.vue
  19. 513 0
      src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity2.vue
  20. 82 2
      src/views/adManage/sb/campaigns/CreateCampaigns/index.vue
  21. 96 0
      src/views/adManage/sd/campaigns/CreateCampaigns/api/index.ts
  22. 93 90
      src/views/adManage/sd/campaigns/CreateCampaigns/component/AdCampaign.vue
  23. 122 0
      src/views/adManage/sd/campaigns/CreateCampaigns/component/AdGroup.vue
  24. 269 0
      src/views/adManage/sd/campaigns/CreateCampaigns/component/CommodityOperate.vue
  25. 36 0
      src/views/adManage/sd/campaigns/CreateCampaigns/component/ContentTarget.vue
  26. 341 0
      src/views/adManage/sd/campaigns/CreateCampaigns/component/CustomTarget.vue
  27. 439 0
      src/views/adManage/sd/campaigns/CreateCampaigns/component/PromoteProduct.vue
  28. 35 1
      src/views/adManage/sd/campaigns/CreateCampaigns/index.vue
  29. 14 6
      src/views/adManage/sd/campaigns/crud.tsx
  30. 2 4
      src/views/adManage/sp/campaigns/CreateCampaigns/index.vue

+ 1 - 1
.env

@@ -3,6 +3,6 @@ VITE_PORT = 8080
 
 # open 运行 npm run dev 时自动打开浏览器
 VITE_OPEN = false
-
+BROWSER = Google Chrome.app
 # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
 VITE_PUBLIC_PATH = /web

+ 5 - 0
src/utils/emitter.ts

@@ -0,0 +1,5 @@
+import mitt from 'mitt'
+
+const emitter = mitt()
+
+export default emitter

+ 1 - 1
src/utils/service.ts

@@ -176,7 +176,7 @@ function createRequestFunction(service: any) {
 			baseURL: getBaseURL(),
 			data: {},
 		};
-
+		delete config.headers
 		// const token = userStore.getToken;
 		const token = Session.get('token');
 		if (token != null) {

+ 154 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/api/index.ts

@@ -0,0 +1,154 @@
+import { request } from '/@/utils/service'
+
+
+export function getAdMixSelect() {
+  return request({
+      url: '/api/ad_manage/portfolios/select_list',
+      method: 'GET',
+  })
+}
+
+export function postCampaignsData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sbcampaigns/create/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function postGroupData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sbgroups/create/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function postNegativeWordData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sptargets/add/negative/keywords/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function getAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getLifeStyleAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getBrands(query) {
+  return request({
+    url: '/api/ad_manage/sb/getbrands/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getStoreurl(query) {
+  return request({
+    url: '/api/ad_manage/sb/storeurl/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getPageAsins(query) {
+  return request({
+    url: '/api/ad_manage/sb/getpageasins/',
+      method: 'get',
+      params: query
+  })
+}
+export function getCommodityCard(query) {
+  return request({
+    url: '/api/sellers/listings/all/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getVideoAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function videoDetailCreate(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/video/create/',
+      method: 'post',
+      data: obj,
+  })
+}
+
+export function uploadFile(obj) {
+  return request({
+      url: '/api/ad_manage/assets/upload/',
+      method: 'post',
+      data: obj,
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+  })
+}
+
+export function checkAsset(obj) {
+  return request({
+      url: '/api/ad_manage/assets/checkasset/',
+      method: 'post',
+      data: obj,
+  })
+}
+
+export function getDefaultSpotlightAsin(query) {
+  return request({
+      url: '/api/ad_manage/sb/defaultspotlightasin/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getSellerInStock(obj) {
+  return request({
+      url: '/api/sellers/listings/sellerinstock/',
+      method: 'post',
+      data: obj
+  })
+}
+export function postStoreSpotlight(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/storespotlight/create/',
+      method: 'post',
+      data: obj
+  })
+}
+
+export function postBrandVideo(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/brandvideo/create/',
+      method: 'post',
+      data: obj
+  })
+}
+export function postVideo(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/video/create/',
+      method: 'post',
+      data: obj
+  })
+}
+

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

@@ -0,0 +1,312 @@
+<template>
+  <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>
+        <span style="font-size: 18px; padding-left: 5px">设置</span>
+      </div>
+      <el-form
+        ref="campaignRuleFormRef"
+        :model="campaignRuleForm"
+        :rules="campaignRules"
+        label-position="top"
+        label-width="120px"
+        class="demo-ruleForm"
+        :size="formSize"
+        status-icon>
+        <div class="flex-between">
+          <el-form-item label="广告活动名称" prop="campaignName" style="width: 48%">
+            <el-input v-model="campaignRuleForm.campaignName" />
+          </el-form-item>
+          <el-form-item label="广告组合" prop="adMix" style="width: 48%">
+            <el-select v-model="campaignRuleForm.adMix" placeholder="请选择" style="width: 100%">
+              <el-option v-for="item in adMixOptions" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </div>
+        <div class="flex-between">
+          <div class="flex-between" style="width: 48%">
+            <el-form-item label="开始时间" prop="startDate" style="width: 49%">
+              <el-date-picker
+                v-model="campaignRuleForm.startDate"
+                type="date"
+                label="Pick a date"
+                placeholder="开始时间"
+                format="YYYY-MM-DD"
+                value-format="YYYY-MM-DD"
+                style="width: 100%" />
+            </el-form-item>
+
+            <el-form-item label="结束时间" prop="endDate" style="width: 49%">
+              <el-date-picker
+                v-model="campaignRuleForm.endDate"
+                type="date"
+                label="Pick a date"
+                placeholder="开始时间"
+                format="YYYY-MM-DD"
+                value-format="YYYY-MM-DD"
+                style="width: 100%" />
+            </el-form-item>
+          </div>
+          <div class="flex-between" style="width: 48%">
+            <el-form-item label="预算" required prop="budget" style="width: 65%">
+              <el-input v-model="campaignRuleForm.budget" minlength="1" maxlength="7" placeholder="请输入" style="width: 100%">
+                <template #prepend>$</template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="频率" prop="frequency" style="width: 34%">
+              <el-select v-model="campaignRuleForm.frequency" placeholder="请选择" style="width: 100%">
+                <el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
+              </el-select>
+            </el-form-item>
+          </div>
+        </div>
+        <el-form-item label="品牌" prop="brand" required style="width: 48%">
+          <el-select v-model="campaignRuleForm.brand" placeholder="请选择" style="width: 100%">
+            <el-option v-for="item in brandOptions" :key="item.brandId" :label="item.brandRegistryName" :value="item.brandEntityId" />
+          </el-select>
+        </el-form-item>
+        <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>
+        <el-form-item label="自动竞价" style="margin-bottom: -13px">
+          <el-form-item>
+            <el-switch v-model="campaignRuleForm.isBid" />
+            <span style="margin-left: 10px; color: #88909b">允许亚马逊自动优化搜索结果首页以外的广告位竞价</span>
+          </el-form-item>
+        </el-form-item>
+        <div style="width: 55%" v-if="campaignRuleForm.isBid == false">
+          <el-card shadow="never" body-style="padding: 10px 10px 5px 10px;">
+            <div style="margin-bottom: 10px; font-weight: 500">展示位置出价调整</div>
+            <div style="display: flex; align-items: center">
+              <div class="left">
+                <div class="title">商品页面</div>
+                <div class="tip">产品详情页面为顾客提供在亚马逊所售卖商品的详情信息</div>
+              </div>
+              <el-form-item prop="commodityPage" style="margin-bottom: 0px !important; width: 37%">
+                <el-input v-model="campaignRuleForm.commodityPage" maxlength="3" placeholder="-99 ~ 900">
+                  <template #append>%</template>
+                </el-input>
+              </el-form-item>
+            </div>
+            <div style="display: flex; align-items: center; margin-top: 10px">
+              <div class="left">
+                <div class="title">搜索结果顶部(首页)</div>
+                <div class="tip">亚马逊首页 http://www.amazon.com</div>
+              </div>
+              <el-form-item prop="firstPage" style="margin-bottom: 0px !important; width: 37%">
+                <el-input v-model="campaignRuleForm.firstPage" maxlength="3" placeholder="-99 ~ 900" style="width: 100%">
+                  <template #append>%</template>
+                </el-input>
+              </el-form-item>
+            </div>
+            <div style="display: flex; align-items: center; margin-top: 10px">
+              <div class="left">
+                <div class="title">搜索结果的其余位置</div>
+                <div class="tip">其他位置集合, 例如搜索页</div>
+              </div>
+              <el-form-item prop="otherPlace" style="margin-bottom: 0px !important; width: 37%">
+                <el-input v-model="campaignRuleForm.otherPlace" maxlength="3" placeholder="-99 ~ 900">
+                  <template #append>%</template>
+                </el-input>
+              </el-form-item>
+            </div>
+            <div style="color: #8d9095; padding-left: 60%; margin-top: 10px">示例: $5.00 竞价降低 40% 将变为 $3.00</div>
+          </el-card>
+        </div>
+        <el-form-item style="margin: 20px 0 -10px 48%">
+          <el-button type="primary" plain @click="submitCampaignForm(campaignRuleFormRef)">保存</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, reactive, ref, watch, defineEmits } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { useRouter, useRoute } from 'vue-router'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { postCampaignsData, getAdMixSelect, getBrands } from '../api/index'
+import emitter from '/@/utils/emitter'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const formSize = ref('default')
+const campaignRuleFormRef = ref<FormInstance>()
+interface campaignRuleForm {
+  campaignName: string
+  adMix: string
+  startDate: string
+  endDate: string
+  budget: string
+  frequency: string
+  brand: string
+  isBid: boolean
+  commodityPage: string
+  otherPlace: string
+  firstPage: string
+}
+const campaignRuleForm = reactive<campaignRuleForm>({
+  campaignName: 'AiTestW10',
+  adMix: '',
+  startDate: '',
+  endDate: '',
+  budget: '',
+  frequency: 'DAILY',
+  brand: '',
+  isBid: false,
+  commodityPage: '',
+  otherPlace: '',
+  firstPage: '',
+})
+const campaignRules = reactive<FormRules<campaignRuleForm>>({
+  campaignName: [{ required: true, message: '请输入广告活动', trigger: 'blur' }],
+  startDate: [{ type: 'date', required: true, message: '请选择时间', trigger: 'blur' }],
+  budget: [
+    { required: true, message: '请输入预算', trigger: 'blur' },
+    { pattern: /^(?:[1-9]\d{0,5}|1000000)(?:\.\d{1,2})?$/, message: '预算必须是1到1000000之间的数字,小数点后最多两位', trigger: 'blur' },
+  ],
+  commodityPage: [
+    { required: true, message: '必填', trigger: 'blur' },
+    { pattern: /^-?(99|[1-8]?[0-9]?[0-9])$/, message: '请输入-99到900之间的数字', trigger: 'blur' },
+  ],
+  otherPlace: [
+    { required: true, message: '必填', trigger: 'blur' },
+    { pattern: /^-?(99|[1-8]?[0-9]?[0-9])$/, message: '请输入-99到900之间的数字', trigger: 'blur' },
+  ],
+  firstPage: [
+    { required: true, message: '必填', trigger: 'blur' },
+    { pattern: /^-?(99|[1-8]?[0-9]?[0-9])$/, message: '请输入-99到900之间的数字', trigger: 'blur' },
+  ],
+})
+const frequencyOptions = [
+  {
+    value: 'DAILY',
+    label: '每日',
+  },
+  {
+    value: 'lifeCycle',
+    label: '生命周期',
+    disabled: true,
+  },
+]
+const brandOptions = ref([])
+
+const submitCampaignForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createCampaigns()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+async function getBrandOption() {
+  const response = await getBrands({ profile_id: profile.value.profile_id })
+  brandOptions.value = response.data
+}
+
+const adMixOptions = ref([])
+async function buildAdMix() {
+  try {
+    const response = await getAdMixSelect()
+    adMixOptions.value = response.data.map((option) => {
+      return {
+        value: option.portfolioId,
+        label: option.name,
+      }
+    })
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+const campaignLoading = ref(false)
+const respCampaignId = ref('')
+const respCampaignName = ref('')
+async function createCampaigns() {
+  campaignLoading.value = true
+
+  // 构建基础请求体
+  const campaignData = {
+    profile_id: profile.value.profile_id,
+    budget: campaignRuleForm.budget,
+    budgetType: campaignRuleForm.frequency,
+    name: campaignRuleForm.campaignName,
+    brandEntityId: campaignRuleForm.brand,
+    bidOptimization: campaignRuleForm.isBid,
+    bidOptimizationStrategy: '',
+    startDate: campaignRuleForm.startDate,
+    endDate: campaignRuleForm.endDate,
+    smartDefault: 'MANUAL',
+    costType: 'CPC',
+    goal: 'PAGE_VISIT',
+    state: 'PAUSED',
+    ...(campaignRuleForm.firstPage && { h_percentage: campaignRuleForm.firstPage }),
+    ...(campaignRuleForm.commodityPage && { d_percentage: campaignRuleForm.commodityPage }),
+    ...(campaignRuleForm.otherPlace && { o_percentage: campaignRuleForm.otherPlace }),
+    ...(campaignRuleForm.adMix && { portfolioId: campaignRuleForm.adMix }),
+  }
+
+  try {
+    const response = await postCampaignsData(campaignData)
+    respCampaignId.value = response.data.campaignId
+    respCampaignName.value = response.data.campaignName
+
+    if (response.data.campaignId) {
+      ElMessage({ message: '广告活动创建成功', type: 'success' })
+    } else {
+      ElMessage.error('广告活动创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    campaignLoading.value = false
+  }
+}
+
+const emit = defineEmits(['update-campaign'])
+
+watch([respCampaignId, respCampaignName], () => {
+  if (respCampaignId.value && respCampaignName.value) {
+    emit('update-campaign', {
+      id: respCampaignId.value,
+      name: respCampaignName.value,
+    })
+  }
+})
+
+onMounted(() => {
+  buildAdMix()
+  getBrandOption()
+})
+</script>
+
+<style scoped>
+.flex-between {
+  display: flex;
+  justify-content: space-between;
+}
+.left {
+  margin-right: 12px;
+  width: 60%;
+}
+.title {
+  font-size: 14px;
+  line-height: 20px;
+  color: #1d2129;
+}
+.tip {
+  font-size: 14px;
+  line-height: 20px;
+  color: #4e5969;
+}
+</style>

+ 356 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdFormat.vue

@@ -0,0 +1,356 @@
+<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="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; color: #333333">商品集</div>
+            <div style="background-color: #1e2128; width: 200px; height: 200px; margin: 0 auto"></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; color: #333333">品牌旗舰店焦点</div>
+            <div style="background-color: #4c649d; width: 200px; height: 200px; margin: 0 auto"></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; color: #333333">视频</div>
+            <div style="background-color: #43abc3; width: 200px; height: 200px; margin: 0 auto"></div>
+            <div style="padding: 5px 0 10px 0; color: #333333; font-weight: 400">
+              使用视频宣传您的品牌或产品, 将流量吸引至您的品牌旗舰店或商品详情页
+            </div>
+          </el-radio>
+        </el-radio-group>
+      </div>
+      <div class="customize-font" v-if="adFormatRadio === 'productSet'">需要帮助创建图片或品牌旗舰店?</div>
+      <div class="customize-font" v-if="adFormatRadio === 'focus'">在创建或编辑品牌旗舰店时需要帮助?</div>
+      <div class="customize-font" v-if="adFormatRadio === 'video'">在创建或编辑视频时需要帮助?</div>
+
+      <div style="display: flex; align-items: center; margin: 20px 0 5px 0">
+        <div style="color: #616266; font-weight: 450">着陆页</div>
+        <el-tooltip content="顾客在与您的广告互动后将被引导至着陆页" placement="top">
+          <el-icon color="#616266"><InfoFilled /></el-icon>
+        </el-tooltip>
+      </div>
+
+      <div class="land-Page" v-if="adFormatRadio === 'productSet' || adFormatRadio === 'video'">
+        <el-radio-group v-model="arrivalsRadio" style="display: flex; justify-content: space-between">
+          <el-radio label="flagshipStore" border style="height: auto; width: 100%; flex: 2; align-items: flex-start; padding: 15px 10px 0 10px">
+            <div>亚马逊品牌旗舰店(包括子页面)</div>
+            <div>
+              <el-form
+                ref="ruleFormRef"
+                :model="ruleForm"
+                :rules="rules"
+                label-position="top"
+                label-width="120px"
+                class="demo-ruleForm"
+                size="default"
+                status-icon>
+                <div style="display: flex; margin-top: 10px">
+                  <el-form-item label="选择一个店铺" prop="shop" style="width: 48%; margin-right: 10px">
+                    <el-select
+                      v-model="ruleForm.shop"
+                      clearable
+                      style="width: 100%"
+                      @change="shopChanged"
+                      @blur="validateField('shop')"
+                      :disabled="arrivalsRadio == 'newArrivals' || arrivalsRadio == 'productDetailsPage'">
+                      <el-option v-for="item in shopOptions" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item label="选择一个页面" prop="page" style="width: 48%">
+                    <el-select
+                      v-model="ruleForm.page"
+                      clearable
+                      style="width: 100%"
+                      @blur="validateField('page')"
+                      :disabled="arrivalsRadio == 'newArrivals' || arrivalsRadio == 'productDetailsPage'">
+                      <el-option v-for="item in pageOptions" :key="item.storePageId" :label="item.storePageName" :value="item.storePageUrl" />
+                    </el-select>
+                  </el-form-item>
+                </div>
+              </el-form>
+            </div>
+          </el-radio>
+          <el-radio class="land-page-radio" label="newArrivals" border v-if="adFormatRadio === 'productSet'">
+            <div>新着陆页</div>
+            <div>选择要推广的商品, 我们将为您创建一个落地页</div>
+          </el-radio>
+          <el-radio class="land-page-radio" label="productDetailsPage" border v-if="adFormatRadio === 'video'">
+            <div>商品详情页</div>
+          </el-radio>
+        </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"
+              clearable
+              @blur="validateField2('focusShop')"
+              style="padding-top: 10px; margin-top: -15px; width: 500px">
+              <el-option v-for="item in focusShopOptions" :key="item.brandId" :label="item.brandRegistryName" :value="item.brandEntityId" />
+            </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')
+          ">
+          <span style="color: #306cd7; font-size: 26px">|</span>
+          <span style="font-size: 18px; padding-left: 5px">商品</span>
+        </div>
+        <ProductSetCommodity v-if="adFormatRadio === 'productSet' && arrivalsRadio === 'newArrivals'"></ProductSetCommodity>
+        <VideoCommodity
+          @update-added-data="handleUpdateAddedData"
+          v-if="adFormatRadio === 'video' && arrivalsRadio === 'productDetailsPage'"></VideoCommodity>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, defineEmits, watch, onMounted, nextTick } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import ProductSetCommodity from '../component/ProductSetCommodity.vue'
+import VideoCommodity from '../component/VideoCommodity.vue'
+import { getBrands, getStoreurl } from '../api/index'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const adFormatRadio = ref('productSet')
+const arrivalsRadio = ref('flagshipStore')
+const ruleFormRef = ref<FormInstance>()
+interface RuleForm {
+  shop: string
+  page: string
+}
+const ruleForm = reactive<RuleForm>({
+  shop: '',
+  page: '',
+})
+const rules = reactive<FormRules<RuleForm>>({
+  shop: [{ required: true, message: '请选择', trigger: 'change' }],
+  page: [{ required: true, message: '请选择', trigger: 'change' }],
+})
+const validateField = (fieldName) => {
+  ruleFormRef.value.validateField(fieldName, () => {})
+}
+
+const flagshipStoreRuleFormRef2 = ref<FormInstance>()
+interface flagshipStoreRuleForm2 {
+  focusShop: string
+}
+const flagshipStoreRuleForm2 = reactive<flagshipStoreRuleForm2>({
+  focusShop: '',
+})
+const flagshipStoreRules2 = reactive<FormRules<flagshipStoreRuleForm2>>({
+  focusShop: [{ required: true, message: '请选择', trigger: 'change' }],
+})
+const validateField2 = (fieldName) => {
+  flagshipStoreRuleFormRef2.value.validateField(fieldName, () => {})
+}
+
+const shopOptions = ref([])
+const pageOptions = ref([])
+const focusShopOptions = ref([])
+
+async function getShopOptions() {
+  try {
+    const response = await getBrands({ profile_id: profile.value.profile_id })
+    const shopOption = response.data.map((item) => {
+      return {
+        value: item.brandRegistryName,
+        label: 'ZOSI',
+      }
+    })
+    focusShopOptions.value = response.data
+    shopOptions.value = shopOption
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+watch(
+  () => ruleForm.shop,
+  () => {
+    setTimeout(() => {
+      emitter.emit('video-shop', focusShopOptions.value[0])
+    }, 2000)
+  },
+  { deep: true }
+)
+
+watch(
+  () => flagshipStoreRuleForm2.focusShop,
+  () => {
+    setTimeout(() => {
+      emitter.emit('spotlight-shop', focusShopOptions.value[0])
+    }, 2000)
+  }
+)
+
+async function getPageOptions() {
+  try {
+    const response = await getStoreurl({ profile_id: profile.value.profile_id })
+    pageOptions.value = response.data.storePageInfo
+    // console.log('pageOptions:', pageOptions.value)
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+let selectedPage: any = ''
+
+watch(
+  () => ruleForm.page,
+  (newPageValue) => {
+    // Find the selected page from pageOptions
+    selectedPage = pageOptions.value.find((page) => page.storePageUrl === newPageValue)
+    if (selectedPage) {
+      // Print the storePageUrl of the selected page
+      // console.log(`Selected page URL: ${selectedPage.storePageUrl}`)
+      setTimeout(() => {
+        emitter.emit('page', selectedPage.storePageUrl)
+      }, 2000);
+      
+    } else {
+      console.log('No page selected or matching page not found')
+    }
+  }
+)
+
+const emit = defineEmits([
+  'update:adFormatRadio',
+  'update:arrivalsRadio',
+  'update:flagshipStoreShop',
+  'update:pageOptions',
+  'update:addedTableData',
+  'update:focusShopSelect',
+])
+
+function handleUpdateAddedData(data) {
+  emit('update:addedTableData', data)
+}
+// 监听 adFormatRadio 的变化并触发事件
+watch(
+  adFormatRadio,
+  (newValue) => {
+    emit('update:adFormatRadio', newValue)
+  },
+  { immediate: true }
+)
+
+watch(
+  arrivalsRadio,
+  (newValue) => {
+    emit('update:arrivalsRadio', newValue)
+  },
+  { immediate: true }
+)
+
+watch(
+  () => ruleForm.shop,
+  (newValue) => {
+    emit('update:flagshipStoreShop', newValue)
+    if (newValue === 'ZOSI') {
+      getPageOptions()
+    }
+    if (!ruleForm.shop) {
+      ruleForm.page = ''
+      pageOptions.value = []
+    }
+  }
+)
+
+watch(
+  () => ruleForm.page,
+  (newValue) => {
+    emit('update:pageOptions', newValue)
+  }
+)
+
+watch(
+  () => flagshipStoreRuleForm2.focusShop,
+  (newValue) => {
+    emit('update:focusShopSelect', newValue)
+  },
+  { deep: true }
+)
+
+function shopChanged() {
+  setTimeout(() => {
+    emitter.emit('send-brandEntityId', { brandEntityId: focusShopOptions.value })
+  }, 2000)
+}
+
+onMounted(() => {
+  getShopOptions()
+})
+</script>
+
+<style scoped>
+.customize-font {
+  color: #1e2128;
+  font-weight: 600;
+  margin-top: 10px;
+}
+.ad-format-radio {
+  height: auto;
+  width: 100%;
+  flex: 1;
+  flex-direction: column-reverse;
+  align-items: center;
+}
+.land-page-radio {
+  height: 132px;
+  flex: 1;
+  align-items: flex-start;
+  padding: 15px 10px 0 10px;
+}
+.customize-container {
+  margin-top: 10px;
+}
+::v-deep(.ad-format-radios .el-radio-group .el-radio__inner) {
+  /* 广告格式单选按钮 */
+  margin-bottom: 3px;
+}
+
+::v-deep(.land-Page .el-radio-group .el-radio__inner) {
+  /* 着陆页单选按钮 */
+  margin-top: 3px;
+}
+::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>

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

@@ -0,0 +1,111 @@
+<template>
+  <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>
+        <span style="font-size: 18px; padding-left: 5px">广告组</span>
+      </div>
+      <el-form
+        ref="groupRuleFormRef"
+        :model="groupRuleForm"
+        :rules="groupRules"
+        label-position="left"
+        label-width="120px"
+        class="demo-ruleForm"
+        :size="formSize"
+        status-icon>
+        <el-form-item label="广告组名称" prop="groupName">
+          <el-input v-model="groupRuleForm.groupName" style="width: 600px" />
+          <el-button type="primary" plain :disabled="!respCampaignId" @click="submitGroupForm(groupRuleFormRef)" style="margin-left: 30px">保存</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, watch, Ref } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { postGroupData } from '../api/index'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const respCampaignId = inject<Ref>('respCampaignId')
+const respCampaignName = inject<Ref>('respCampaignName')
+
+const groupLoading = ref(false)
+
+const formSize = ref('default')
+const groupRuleFormRef = ref<FormInstance>()
+interface groupRuleForm {
+  groupName: string
+}
+const groupRuleForm = reactive<groupRuleForm>({
+  groupName: 'AiTestGroupW04',
+})
+const groupRules = reactive<FormRules<groupRuleForm>>({
+  groupName: [{ required: true, message: '请输入广告活动', trigger: 'blur' }],
+})
+
+const submitGroupForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createGroups()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const respAdGroupId = ref('')
+async function createGroups() {
+  groupLoading.value = true
+  const groupData = {
+    profile_id: profile.value.profile_id,
+    campaignId: respCampaignId.value,
+    name: respCampaignName.value,
+  }
+
+  const filteredRequestData = Object.fromEntries(Object.entries(groupData).filter(([_, v]) => v != null))
+  try {
+    const response = await postGroupData(filteredRequestData)
+    respAdGroupId.value = response.data.adGroupId
+    ElMessage({
+      message: '广告组创建成功',
+      type: 'success',
+    })
+    groupRuleForm.groupName = ''
+  } catch (error) {
+    ElMessage.error('广告组创建失败!')
+    console.error('请求失败:', error)
+  } finally {
+    groupLoading.value = false
+  }
+}
+
+const emit = defineEmits(['update-groupId'])
+
+watch(respAdGroupId, () => {
+  if (respAdGroupId.value) {
+    emit('update-groupId', {
+      id: respAdGroupId.value,
+    })
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.customize-container {
+  margin-top: 10px;
+}
+::v-deep(.el-form-item__label) {
+  font-weight: 500;
+  color: #505968;
+}
+</style>

+ 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>

+ 980 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/FocusCreativity.vue

@@ -0,0 +1,980 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 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>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="1" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
+                  </el-form-item>
+
+                  <el-upload
+                    v-model:file-list="fileList"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    list-type="picture-card"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture" @click="openDialog" disabled="true">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title>编辑品牌旗舰店页面</template>
+                  <div v-for="(storePage, index) in topStorePages" :key="index" style="margin-bottom: 10px; width: 85%">
+                    <el-card shadow="hover" body-style="padding: 10px;">
+                      <div style="margin-right: 8px; line-height: normal; display: flex; align-items: center">
+                        <el-image class="img-box" :src="storePage.storePageLink" />
+                        <div style="margin-left: 15px">
+                          <span><span style="color: #6d7784">当前名称:</span>{{ storePage.storePageName }}</span>
+                          <div style="margin-bottom: 5px"><span style="color: #6d7784">ASIN: </span>{{ storePage.asin }}</div>
+                          <el-input v-model="storePage.inputName" style="width: 300px" placeholder="修改品牌页面名称"></el-input>
+                        </div>
+                        <div class="card-operation">
+                          <el-button link type="primary" @click="changePicture(storePage.storePageUrl, index)" style="margin-bottom: 10px"
+                            >更换图片</el-button
+                          >
+                          <el-button link type="primary" @click="changePage(storePage.storePageUrl, index)">更换页面</el-button>
+                        </div>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-dialog v-model="commodityDialog" title="更换图片" width="50%">
+                  <el-radio-group
+                    v-loading="dialogLoading3"
+                    v-model="selectedCommodity"
+                    style="display: flex; flex-direction: column; align-content: flex-start; align-items: flex-start">
+                    <div v-for="(item, index) in stock" :key="index">
+                      <el-radio :label="item.asin" style="height: 80px; border-bottom: 1px solid #ccc">
+                        <div style="padding: 10px; display: flex; align-items: center">
+                          <div style="margin-right: 8px; line-height: normal">
+                            <el-image class="img-box" :src="item.image_link" />
+                          </div>
+                          <div style="position: relative">
+                            <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                              <div class="double-line">{{ item.title }}</div>
+                            </el-tooltip>
+                            <span>
+                              <span style="color: #6d7784">ASIN: </span>
+                              <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                            </span>
+                          </div>
+                        </div>
+                      </el-radio>
+                    </div>
+                  </el-radio-group>
+                  <div style="margin-top: 20px; display: flex; justify-content: center">
+                    <el-button type="primary" :disabled="!selectedCommodity" @click="handleSelectedStore">确定</el-button>
+                  </div>
+                </el-dialog>
+                <el-dialog v-model="pageDialog" title="更换页面" width="50%">
+                  <el-radio-group v-loading="pageDialogLoading" v-model="selectedCommodity" @change="handlePageChange" class="radio-group-item">
+                    <div v-for="(item, index) in storePageData.storePageInfo" :key="index" style="width: 100%">
+                      <el-radio
+                        :label="item.storePageId"
+                        class="radio-item"
+                        :disabled="topStorePages.some((storePage) => storePage.storePageName === item.storePageName)">
+                        <div class="radio-item-content">
+                          <div style="position: relative">
+                            <el-tooltip class="box-item" effect="dark" :content="item.storePageName" placement="top">
+                              <div class="double-line">{{ item.storePageName }}</div>
+                            </el-tooltip>
+                          </div>
+                        </div>
+                      </el-radio>
+                    </div>
+                  </el-radio-group>
+                  <!-- <div style="margin-top: 20px; display: flex; justify-content: center">
+                    <el-button type="primary" :disabled="!selectedCommodity">确定</el-button>
+                  </div> -->
+                </el-dialog>
+
+                <el-collapse-item name="4" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button type="primary" plain @click="submitForm(ruleFormRef)" :disabled="!fileList.length" style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <!-- <el-dialog v-model="lifeStyleDialog" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in lifeStyleCards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="centerDialogVisible = false">确定</el-button>
+        </span>
+      </template>
+    </el-dialog> -->
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, computed, onMounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search, Delete, Download, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import emitter from '/@/utils/emitter'
+import {
+  getAssets,
+  getLifeStyleAssets,
+  getPageAsins,
+  getCommodityCard,
+  getStoreurl,
+  getDefaultSpotlightAsin,
+  getSellerInStock,
+  postStoreSpotlight,
+  uploadFile,
+  checkAsset,
+} from '../api/index'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const createLoading = ref(false)
+const ruleFormRef = ref<FormInstance>()
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createStoreSpotlight()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+  if (val.includes('commodity')) {
+    // getCommodityCardData()
+  }
+}
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+
+// 图片上传相关
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const disabled = ref(false)
+const fileList = ref([])
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push(item)
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 清空 fileList
+    fileList.value.length = 0
+
+    // 假设每次只选择一个图片
+    selectedImageUrl.value = selectedCards.value[0].imageUrl
+
+    // 创建一个新的 UploadFile 对象
+    const newFile = {
+      name: selectedCards.value[0].title, // 或者任何你希望用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+
+    // 将新的文件对象添加到 fileList 中
+    fileList.value.push(newFile)
+  }
+
+  // 清空选中卡片
+  selectedCards.value = []
+  // 关闭对话框
+  centerDialogVisible.value = false
+}
+
+function isSelected(id) {
+  return selectedId.value === id
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'IMAGE',
+    assetSubType: 'LOGO',
+  }
+  const response = await getAssets(query)
+  console.log('🚀 ~ getAssetsData ~ response-->>', response)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.width,
+      height: asset.fileMetadata.height,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+function openDialog() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+const lifeStyleDialog = ref(false)
+const lifeStyleCards = reactive([])
+async function getLifeStyleAssetsData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      assetType: 'IMAGE',
+      assetSubType: 'LIFESTYLE_IMAGE',
+    }
+    const response = await getLifeStyleAssets(query)
+    console.log('🚀 ~ getLifeStyleAssetsData ~ response-->>', response)
+
+    lifeStyleCards.splice(0, lifeStyleCards.length)
+
+    response.data.forEach((asset) => {
+      lifeStyleCards.push({
+        id: asset.assetId,
+        title: asset.name,
+        imageUrl: asset.storageLocationUrls.defaultUrl,
+        width: asset.fileMetadata.width,
+        height: asset.fileMetadata.height,
+        size: bytesToKB(asset.fileMetadata.sizeInBytes),
+      })
+    })
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+// function openLifeStyleDialog() {
+//   lifeStyleDialog.value = true
+//   getLifeStyleAssetsData()
+// }
+
+// 获取商品数据
+const topStorePages = ref([])
+const commodityLoading = ref(false)
+const dialogLoading3 = ref(false)
+let subpageslist = []
+
+async function getDefaultCommodityData() {
+  try {
+    commodityLoading.value = true
+    const resp = await getDefaultSpotlightAsin({ profile_id: '3006125408623189' })
+    if (resp.code === 2000) {
+      topStorePages.value = resp.data.defaultlist.map((item) => ({
+        storePageName: item.storePageName,
+        storePageLink: item.image_link,
+        storePageUrl: item.storePageUrl,
+        asin: item.asin,
+        inputName: item.storePageName, // 新增字段用于存储输入值
+      }))
+      subpageslist = topStorePages.value.map((item) => ({
+      asin: item.asin,
+      pageTitle: item.inputName,
+      url: item.storePageUrl,
+  }))
+    }
+  } catch (error) {
+    console.error('Error in getDefaultCommodityData:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+const asinList = ref([])
+const stock = ref([])
+const currentEditingIndex = ref(null)
+const selectedCommodityData = ref(null)
+
+async function changePicture(pageUrl, index) {
+  commodityDialog.value = true
+  dialogLoading3.value = true
+
+  currentEditingIndex.value = index
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageUrl,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    getStock()
+    dialogLoading3.value = false
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+async function getStock() {
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      asinlist: asinList.value,
+    }
+    const response = await getSellerInStock(obj)
+    stock.value = response.data
+  } catch (error) {
+    console.error('Error in getStock:', error)
+  }
+}
+
+function handleSelectedStore() {
+  if (currentEditingIndex.value !== null) {
+    selectedCommodityData.value = stock.value.find((item) => item.asin === selectedCommodity.value)
+    if (selectedCommodityData.value) {
+      // 更新图片链接和ASIN
+      topStorePages.value[currentEditingIndex.value].storePageLink = selectedCommodityData.value.image_link
+      topStorePages.value[currentEditingIndex.value].asin = selectedCommodityData.value.asin
+      // 更新输入框的绑定值以便可以继续编辑名称
+      topStorePages.value[currentEditingIndex.value].inputName = topStorePages.value[currentEditingIndex.value].inputName
+      console.log(topStorePages.value)
+    }
+    // 重置当前编辑索引和选中的商品
+    currentEditingIndex.value = null
+    selectedCommodity.value = null
+    commodityDialog.value = false
+  }
+}
+
+// 更换页面功能
+const pageDialog = ref(false)
+const storePageData = ref({ storePageInfo: [] })
+const pageDialogLoading = ref(false)
+const selectedPageUrl = ref('')
+const firstObj: any = ref({})
+const pageAsinList = ref([])
+const selectedPageName = ref('')
+
+async function changePage(pageUrl, index) {
+  pageDialog.value = true
+  pageDialogLoading.value = true
+  currentEditingIndex.value = index
+  try {
+    const response = await getStoreurl({ profile_id: profile.value.profile_id })
+    storePageData.value = response.data
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    pageDialogLoading.value = false
+  }
+}
+
+async function getAsinList(pageurl) {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageurl,
+    }
+    const resp = await getPageAsins(query)
+    pageAsinList.value = resp.data.asinList
+
+    changeCardData()
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+async function handlePageChange(selectedPageId) {
+  const selectedPage = storePageData.value.storePageInfo.find((page) => page.storePageId === selectedPageId)
+  if (selectedPage) {
+    selectedPageUrl.value = selectedPage.storePageUrl
+    selectedPageName.value = selectedPage.storePageName
+    pageDialog.value = false
+    await getAsinList(selectedPageUrl.value)
+  } else {
+    console.error('Selected page not found in storePageData')
+  }
+}
+
+async function changeCardData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      asinlist: pageAsinList.value,
+    }
+    const response = await getSellerInStock(query)
+    if (response && response.data.length > 0) {
+      // 将response的第一个元素赋值给firstObj
+      firstObj.value = response.data[0]
+
+      // 更新图片链接和ASIN
+      topStorePages.value[currentEditingIndex.value].storePageLink = firstObj.value.image_link
+      topStorePages.value[currentEditingIndex.value].asin = firstObj.value.asin
+      topStorePages.value[currentEditingIndex.value].storePageName = selectedPageName.value
+      topStorePages.value[currentEditingIndex.value].inputName = selectedPageName.value
+      currentEditingIndex.value = null
+    } else {
+      console.error('No data')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  }
+}
+
+// 创建创意
+let brandName = ''
+let brandEntityId = ''
+const respAdGroupId = inject<Ref>('respAdGroupId')
+let brandLogoCrop = {}
+
+const upLoading = ref(false)
+let respAssetId = ''
+
+function handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+function changeFile(file: UploadFile) {
+  handleUpload(file)
+}
+
+async function handleUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+async function createStoreSpotlight() {
+  createLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      url: 'https://www.amazon.com/stores/page/1D1DD2FD-CF54-4FE5-B1A0-9E01F12F8144',
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      brandName: brandName,
+      brandLogoAssetID: respAssetId,
+      brandLogoCrop: brandLogoCrop,
+      consentToTranslate: false,
+      subpageslist: subpageslist,
+      headline: ruleForm.title,
+    }
+    const response = await postStoreSpotlight(query)
+    if (response.data.creative_state == 'success') {
+      ElMessage({ message: '创建成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+watch(topStorePages, () => {
+    subpageslist = topStorePages.value.map((item) => ({
+    asin: item.asin,
+    pageTitle: item.inputName,
+    url: item.storePageUrl,
+  }))
+  console.log('subpageslist', subpageslist)
+},{deep: true})
+
+onMounted(() => {
+  emitter.on('spotlight-shop', (newValue: any) => {
+    brandName = newValue.brandRegistryName
+    brandEntityId = newValue.brandEntityId
+  })
+})
+
+const focusShop = inject<Ref>('focusShop')
+
+async function getCommodityCollapseData() {
+  commodityLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: focusShop.value,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    console.log('asinList', asinList.value)
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+// let lastQueriedAsins = []
+const commodityCard = ref([])
+// async function getCommodityCardData() {
+//   try {
+//     commodityLoading.value = true
+//     const topAsins = asinList.value.slice(0, 3)
+
+//     const newAsins = topAsins.filter((asin) => !lastQueriedAsins.includes(asin))
+//     if (newAsins.length === 0) {
+//       commodityLoading.value = false
+//       return // 如果没有新的 ASIN,直接返回
+//     }
+
+//     lastQueriedAsins = [...topAsins]
+
+//     // 清空commodityCard,为新数据做准备
+//     commodityCard.value = []
+
+//     // 对每个新的 ASIN 发送请求
+//     for (const asin of newAsins) {
+//       const query = {
+//         profile_id: profile.value.profile_id,
+//         asin: asin,
+//       }
+
+//       try {
+//         const response = await getCommodityCard(query)
+//         commodityCard.value.push(response.data)
+//         // console.log('Response for ASIN', asin, ':', response)
+//       } catch (error) {
+//         console.log('Error for ASIN', asin, ':', error)
+//       }
+//     }
+//   } catch (error) {
+//     console.log('Outer error:', error)
+//   } finally {
+//     commodityLoading.value = false
+//   }
+// }
+
+// 更改商品功能
+const commodityDialog = ref(false)
+const selectedCommodity = ref()
+const replaceableCommodity = ref([])
+
+function openCommodityDialog(index) {
+  currentEditingIndex.value = index
+  commodityDialog.value = true
+}
+
+async function getAdditionalCommodityData() {
+  try {
+    dialogLoading3.value = true
+    // 获取除前三个之外的所有 ASIN
+    const additionalAsins = asinList.value.slice(3)
+
+    // 清空 replaceableCommodity,为新数据做准备
+    replaceableCommodity.value = []
+
+    // 对每个额外的 ASIN 发送请求
+    for (const asin of additionalAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        replaceableCommodity.value.push(response.data)
+        console.log('🚀 ~ getAdditionalCommodityData ~ replaceableCommodity-->>', replaceableCommodity.value)
+        // console.log('Response for additional ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for additional ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    dialogLoading3.value = false
+  }
+}
+
+const flattenedCommodityCard = computed(() => {
+  return commodityCard.value.flat()
+})
+
+const flattenedReplaceableCommodity = computed(() => {
+  return replaceableCommodity.value.flat()
+})
+// watch(focusShop.value,
+//   async () => {
+//     await getCommodityCollapseData()
+//     getCommodityCardData()
+//     getAdditionalCommodityData()
+//   }
+// )
+onMounted(async () => {
+  await getDefaultCommodityData()
+  // await getCommodityCollapseData()
+  // getCommodityCardData()
+  // getAdditionalCommodityData()
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.uploaded-image {
+  width: 100%; /* 或根据需要调整 */
+  height: auto; /* 保持图片的原始宽高比 */
+  display: block;
+  margin-bottom: 10px; /* 或根据需要调整 */
+}
+.upload-content {
+  text-align: center;
+  padding: 20px;
+}
+.el-carousel__item h3 {
+  color: #edf5fe;
+  opacity: 0.75;
+  line-height: 200px;
+  margin: 0;
+  text-align: center;
+}
+::v-deep(button.el-carousel__button) {
+  background-color: #3569d6;
+}
+.img-box {
+  min-width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.card-operation {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+  margin-left: 30px;
+}
+.radio-group-item {
+  display: flex;
+  flex-direction: column;
+  align-content: flex-start;
+  align-items: flex-start;
+}
+.radio-item {
+  height: 80px;
+  width: 100%;
+  border-bottom: 1px solid #ccc;
+}
+.radio-item-content {
+  padding: 10px;
+  display: flex;
+  align-items: center;
+}
+</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 :disabled="!addedKeyWordsTableData.length">保存</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>

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

@@ -0,0 +1,354 @@
+<template>
+  <div prop="matchType" 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 #e5e7ec; border-radius: 6px" v-loading="negativeGoodsLoading">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="topTabs" stretch>
+          <el-tab-pane label="排除商品" name="first">
+            <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>
+          </el-tab-pane>
+          <el-tab-pane label="排除品牌" name="second">
+
+          </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 topTabs = 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-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;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 52px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 广告组商品Tab栏 */
+::v-deep(.demo-tabs .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;
+}
+::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;
+}
+</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>

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

@@ -0,0 +1,810 @@
+<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, onMounted, 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 categoryBidInput = ref('0.75')
+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 dialogselectValue = ref('')
+let dialogOptions: any = 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() {
+  productOrientationLoading.value = true
+  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 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)',
+    }
+  }
+}
+
+onMounted(() => {
+  setProductOrientationData()
+})
+
+</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>
+

+ 444 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCommodity.vue

@@ -0,0 +1,444 @@
+<template>
+  <div prop="commodity" style="width: 100%" v-loading="productLoading">
+    <div style="width: 100%; height: 620px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="productTabs" class="demo-tabs">
+          <el-tab-pane label="搜索" name="first">
+            <div style="margin-bottom: 10px">
+              <el-input v-model="searchInp" placeholder="Please input" class="input-with-select" @change="inpChange" clearable>
+                <template #prepend>
+                  <el-select v-model="leftSelect" style="width: 100px" @change="selChange">
+                    <el-option label="名称" value="name" />
+                    <el-option label="ASIN" value="asin" />
+                    <el-option label="SKU" value="sku" />
+                  </el-select>
+                </template>
+                <template #append>
+                  <el-select v-model="rightSelect" style="width: 100px">
+                    <el-option label="最新优先" value="latest" />
+                    <el-option label="最早优先" value="earliest" />
+                    <el-option label="优选广告" value="optimal" />
+                  </el-select>
+                </template>
+              </el-input>
+            </div>
+            <el-table
+              height="490"
+              style="width: 100%"
+              v-loading="loading"
+              :data="productTableData"
+              :header-cell-style="headerCellStyle"
+              @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="50" />
+              <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>
+                      <div class="data-color">
+                        <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                        <span style="margin: 0 5px; color: #cacdd4">|</span>
+                        <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                      </div>
+                      <span>
+                        ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                      </span>
+                      <span>
+                        SKU: <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                      </span>
+                    </div>
+                  </div>
+                </template>
+              </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>
+                </template>
+                <template #default="scope">
+                  <el-button type="primary" size="small" @click="addSingleGoods(scope)" text>添加</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-pagination
+              @current-change="handleCurrentChange"
+              @size-change="handleSizeChange"
+              :current-page="currentPage"
+              :page-size="pageSize"
+              :total="totalItems"
+              layout="prev, pager, next" />
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input
+              style="padding: 10px"
+              v-model="productTextarea"
+              :rows="20"
+              type="textarea"
+              placeholder="请输入ASIN,多个ASIN使用逗号、空格或换行符分隔。(未完成)"
+              maxlength="11000" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button v-for="button in buttons" :key="button.text" :type="button.type" link @click="addGods">{{ button.text }}</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: 0">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedTableData.length }}</span>
+              <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" type="danger" text bg @click="delAllGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -12px">
+          <el-table
+            :data="addedTableData"
+            height="475"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedGoodsChange">
+            <el-table-column type="selection" width="50" />
+            <el-table-column prop="asin" label="ASIN">
+              <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>
+                    <div class="data-color">
+                      <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                      <span style="margin: 0 5px; color: #cacdd4">|</span>
+                      <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                    </div>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                    <span
+                      >SKU:
+                      <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="name" label="Name" width="120" align="right">
+              <template #header>
+                <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>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div style="display: flex; justify-content: space-around; padding-top: 5px">
+          <el-button type="primary" plain :disabled="addedTableData.length < 3"  @click="submitProductForm">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { TabsPaneContext } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import type { Ref } from 'vue'
+import { inject, onMounted, ref, watch } from 'vue'
+import emitter from '/@/utils/emitter'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { request } from '/@/utils/service'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const productTextarea = ref('')
+const productLoading = ref(false)
+let addedAdsTableItems = ref([])
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const productTableData = ref([]) // 左侧表格数据
+const loading = ref(false)
+let addedTableData = ref([])
+let selections = []
+let addedSels = []
+const searchInp = ref('')
+const leftSelect = ref('name')
+const buttons = [{ type: 'primary', text: '添加' }] as const
+const productTabs = ref('first')
+const rightSelect = ref('latest')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respCampaignName = inject<Ref>('respCampaignName')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+function setTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      productTableData.value = resp.data
+      totalItems.value = resp.total
+      currentPage.value = resp.page
+      loading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      loading.value = false
+    })
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = addedTableData.value.some((item) => item.sku === scope.row.sku)
+  if (!isAlreadyAdded) {
+    addedTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function addGods() {
+  const inputData = productTextarea.value
+  const asins = inputData.split(/[\n,]+/)
+
+  asins.forEach((asin) => {
+    if (asin.trim()) {
+      setTableData(asin.trim())
+        .then((response) => {
+          console.log(`Data for ASIN ${asin}:`, response) // 更新这里来正确地访问数据
+        })
+        .catch((error) => {
+          console.error(`Error fetching data for ASIN ${asin}:`, error)
+        })
+    }
+  })
+}
+
+function delSingleGoods(scope) {
+  const index = addedTableData.value.findIndex((item) => item.sku === scope.row.sku)
+  if (index !== -1) {
+    addedTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function delAllGoods() {
+  addedTableData.value = []
+  // addedTableData.value.splice(0, addedTableData.value.length)
+}
+
+// 删除第二个table中已经选中的项
+function delSelectedGoods() {
+  addedTableData.value = addedTableData.value.filter((item) => !addedSels.includes(item))
+  addedSels = []
+}
+
+function inpChange(e) {
+  const value = e
+  if (leftSelect.value === 'asin') {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku') {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+
+function selChange(e) {
+  console.log('e', e)
+  const value = e
+  if (leftSelect.value === 'asin' && searchInp.value) {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku' && searchInp.value) {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+// 点击表格选项触发事件
+function handleSelectionChange(selection) {
+  selections = selection
+}
+// 获取addedTable中已选中的项
+function handleAddedGoodsChange(selection) {
+  addedSels = selection
+}
+// 添加已选中的项
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !addedTableData.value.some((added) => added.sku === sel.sku) // 使用sku作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    addedTableData.value.push(...newSelections)
+  }
+}
+// 点击Tab
+const handleGoodsTabs = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event)
+}
+
+function isItemInList(item, list) {
+  return list.some((listItem) => listItem.sku === item.sku && listItem.asin === item.asin)
+}
+// 监听商品右侧表格已添加的数据并转化数据格式
+watch(
+  addedTableData,
+  (newValue, oldValue) => {
+    newValue.forEach((item) => {
+      if (!isItemInList(item, addedAdsTableItems.value)) {
+        addedAdsTableItems.value.push({ sku: item.sku, asin: item.asin })
+      }
+    })
+  },
+  { deep: true }
+)
+
+async function createAds() {
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinsku: addedAdsTableItems.value,
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/spads/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ createAds ~ resp-->>', resp)
+    productLoading.value = false
+    if (resp.data.success.length > 0) {
+      addedTableData.value = []
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function submitProductForm() {
+  productLoading.value = true
+  createAds()
+}
+
+// 处理分页器当前页变化
+function handleCurrentChange(newPage) {
+  currentPage.value = newPage
+  loading.value = true
+  setTableData()
+}
+// 处理分页器每页显示条目数变化
+function handleSizeChange(newSize) {
+  pageSize.value = newSize
+  currentPage.value = 1 // 重置到第一页
+}
+
+watch(addedTableData, () => {
+  if (addedTableData.value.length > 0) {
+    emitter.emit('addedTableData', addedTableData.value)
+  }
+},{deep:true})
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+onMounted(() => {
+  setTableData()
+})
+</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;
+}
+.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;
+}
+/* 商品定向Tab栏 */
+::v-deep(.goods-orientation-tabs #tab-1) {
+  border-right: 0;
+}
+</style>

+ 781 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity1.vue

@@ -0,0 +1,781 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 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>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="1" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
+                  </el-form-item>
+
+                  <el-upload
+                    v-model:file-list="fileList"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    list-type="picture-card"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture" @click="openDialog">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+                <el-collapse-item name="2" style="padding-right: 10px">
+                  <template #title>自定义图片(可选)</template>
+                  <el-upload v-model:file-list="fileList" action="#" accept=".png, .jpg" :limit="1" list-type="picture-card" :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">图片规格</span>
+                          <el-button type="primary" :icon="Picture" @click="openLifeStyleDialog">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 1200 x 628 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 5MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">4、内容: 图片中未添加文本、图形或徽标</div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
+                  <div v-for="(item, index) in flattenedCommodityCard" :key="index" style="margin: 0 0 5px 0">
+                    <el-card shadow="hover" body-style="padding: 10px; display: flex;">
+                      <div style="margin-right: 8px; line-height: normal">
+                        <el-image class="img-box" :src="item.image_link" />
+                      </div>
+                      <div style="position: relative">
+                        <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                          <div class="double-line">{{ item.title }}</div>
+                        </el-tooltip>
+                        <span>
+                          <span style="color: #6d7784">ASIN: </span>
+                          <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                        </span>
+                        <el-button
+                          type="primary"
+                          size="small"
+                          link
+                          @click="() => openCommodityDialog(index)"
+                          style="position: absolute; bottom: 2px; right: 0">
+                          更换商品
+                        </el-button>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item name="4" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button type="primary" plain @click="clickSave" :disabled="fileList.length == 0" style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="lifeStyleDialog" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in lifeStyleCards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="centerDialogVisible = false">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="commodityDialog" title="更换商品" width="50%">
+      <el-radio-group
+        v-loading="dialogLoading3"
+        v-model="selectedCommodity"
+        style="display: flex; flex-direction: column; align-content: flex-start; align-items: flex-start">
+        <div v-for="(item, index) in flattenedReplaceableCommodity" :key="index">
+          <el-radio :label="item.asin" style="height: 80px; border-bottom: 1px solid #ccc">
+            <div style="padding: 10px; display: flex; align-items: center">
+              <div style="margin-right: 8px; line-height: normal">
+                <el-image class="img-box" :src="item.image_link" />
+              </div>
+              <div style="position: relative">
+                <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                  <div class="double-line">{{ item.title }}</div>
+                </el-tooltip>
+                <span>
+                  <span style="color: #6d7784">ASIN: </span>
+                  <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                </span>
+              </div>
+            </div>
+          </el-radio>
+        </div>
+      </el-radio-group>
+      <div style="margin-top: 20px; display: flex; justify-content: center">
+        <el-button type="primary" :disabled="!selectedCommodity" @click="handleSelectedCommodity">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, computed, onMounted, onUnmounted, nextTick } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search, Delete, Download, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import { getAssets, getLifeStyleAssets, getPageAsins, getCommodityCard, uploadFile, checkAsset } from '../api/index'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+import axios from 'axios'
+
+// async function uploadFile(formData) {
+//   return axios.post('http://192.168.1.225/api/ad_manage/assets/upload/', formData)
+// }
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const ruleFormRef = ref<FormInstance>()
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+  if (val.includes('commodity')) {
+    getCommodityCardData()
+  }
+}
+
+// 图片上传相关
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const disabled = ref(false)
+const pictureList = ref([])
+const fileList = ref([])
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+
+// 图片上传功能
+const upLoading = ref(false)
+function handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  handleUpload(file)
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+function changeFile(file: UploadFile) {
+  handleUpload(file)
+}
+
+async function handleUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId.value)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+// 选择卡片功能
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push(item)
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 清空 fileList
+    fileList.value.length = 0
+
+    // 假设每次只选择一个图片
+    selectedImageUrl.value = selectedCards.value[0].imageUrl
+
+    // 创建一个新的 UploadFile 对象
+    const newFile = {
+      name: selectedCards.value[0].title, // 或者任何你希望用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+
+    // 将新的文件对象添加到 fileList 中
+    fileList.value.push(newFile)
+  }
+
+  // 清空选中卡片
+  selectedCards.value = []
+  // 关闭对话框
+  centerDialogVisible.value = false
+}
+
+function isSelected(id) {
+  return selectedId.value === id
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'IMAGE',
+    assetSubType: 'LOGO',
+  }
+  const response = await getAssets(query)
+  console.log('🚀 ~ getAssetsData ~ response-->>', response)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.width,
+      height: asset.fileMetadata.height,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+function openDialog() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+const lifeStyleDialog = ref(false)
+const lifeStyleCards = reactive([])
+async function getLifeStyleAssetsData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      assetType: 'IMAGE',
+      assetSubType: 'LIFESTYLE_IMAGE',
+    }
+    const response = await getLifeStyleAssets(query)
+    console.log('🚀 ~ getLifeStyleAssetsData ~ response-->>', response)
+
+    lifeStyleCards.splice(0, lifeStyleCards.length)
+
+    response.data.forEach((asset) => {
+      lifeStyleCards.push({
+        id: asset.assetId,
+        title: asset.name,
+        imageUrl: asset.storageLocationUrls.defaultUrl,
+        width: asset.fileMetadata.width,
+        height: asset.fileMetadata.height,
+        size: bytesToKB(asset.fileMetadata.sizeInBytes),
+      })
+    })
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+function openLifeStyleDialog() {
+  lifeStyleDialog.value = true
+  getLifeStyleAssetsData()
+}
+
+const pageOptionsValue = inject<Ref>('pageOptionsValue')
+const asinList = ref([])
+const commodityLoading = ref(false)
+
+async function getCommodityCollapseData() {
+  commodityLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageOptionsValue.value,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    console.log('asinList', asinList.value)
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+function clickSave() {
+  console.log(123, fileList.value)
+}
+
+let lastQueriedAsins = []
+const commodityCard = ref([])
+async function getCommodityCardData() {
+  try {
+    commodityLoading.value = true
+    const topAsins = asinList.value.slice(0, 3)
+
+    const newAsins = topAsins.filter((asin) => !lastQueriedAsins.includes(asin))
+    if (newAsins.length === 0) {
+      commodityLoading.value = false
+      return // 如果没有新的 ASIN,直接返回
+    }
+
+    lastQueriedAsins = [...topAsins]
+
+    // 清空commodityCard,为新数据做准备
+    commodityCard.value = []
+
+    // 对每个新的 ASIN 发送请求
+    for (const asin of newAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        commodityCard.value.push(response.data)
+        // console.log('Response for ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+// 更改商品功能
+const commodityDialog = ref(false)
+const selectedCommodity = ref()
+const replaceableCommodity = ref([])
+const dialogLoading3 = ref(false)
+let currentEditingIndex = ref(null)
+
+function openCommodityDialog(index) {
+  currentEditingIndex.value = index
+  commodityDialog.value = true
+}
+
+async function getAdditionalCommodityData() {
+  try {
+    dialogLoading3.value = true
+    // 获取除前三个之外的所有 ASIN
+    const additionalAsins = asinList.value.slice(3)
+
+    // 清空 replaceableCommodity,为新数据做准备
+    replaceableCommodity.value = []
+
+    // 对每个额外的 ASIN 发送请求
+    for (const asin of additionalAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        replaceableCommodity.value.push(response.data)
+        console.log('🚀 ~ getAdditionalCommodityData ~ replaceableCommodity-->>', replaceableCommodity.value)
+        // console.log('Response for additional ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for additional ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    dialogLoading3.value = false
+  }
+}
+
+function handleSelectedCommodity() {
+  if (currentEditingIndex.value !== null && selectedCommodity.value) {
+    const selectedCommodityData = flattenedReplaceableCommodity.value.find((item) => item.asin === selectedCommodity.value)
+    if (selectedCommodityData) {
+      commodityCard.value[currentEditingIndex.value] = selectedCommodityData
+    }
+    commodityDialog.value = false
+    console.log('commodityCard', commodityCard.value)
+    currentEditingIndex.value = null
+    selectedCommodity.value = null
+  }
+}
+
+watch(
+  () => pageOptionsValue.value,
+  async () => {
+    await getCommodityCollapseData()
+    getCommodityCardData()
+    getAdditionalCommodityData()
+  }
+)
+
+const flattenedCommodityCard = computed(() => {
+  return commodityCard.value.flat()
+})
+
+const flattenedReplaceableCommodity = computed(() => {
+  return replaceableCommodity.value.flat()
+})
+
+const brandEntityId = ref('')
+onMounted(() => {
+  emitter.on('send-brandEntityId', (value: any) => {
+    // console.log('接收到', value.brandEntityId[0].brandEntityId)
+    brandEntityId.value = value.brandEntityId[0].brandEntityId
+  })
+})
+
+// 接收数据端在组建卸载时解绑事件
+onUnmounted(() => {
+  emitter.off('send-brandEntityId')
+  emitter.off('test')
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.uploaded-image {
+  width: 100%; /* 或根据需要调整 */
+  height: auto; /* 保持图片的原始宽高比 */
+  display: block;
+  margin-bottom: 10px; /* 或根据需要调整 */
+}
+
+.upload-content {
+  text-align: center;
+  padding: 20px;
+}
+.el-carousel__item h3 {
+  color: #edf5fe;
+  opacity: 0.75;
+  line-height: 200px;
+  margin: 0;
+  text-align: center;
+}
+::v-deep(button.el-carousel__button) {
+  background-color: #3569d6;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

+ 400 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity2.vue

@@ -0,0 +1,400 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 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>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange">
+                <el-collapse-item name="1">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 10px 5px 0"></el-input>
+                  </el-form-item>
+                  <el-upload
+                    drag
+                    v-model:file-list="fileList"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :auto-upload="false"
+                    :on-remove="handleRemove"
+                    style="padding-right: 10px">
+                    <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+                    <div class="el-upload__text">Drop file here or <em>click to upload</em></div>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                </el-collapse-item>
+                <el-collapse-item name="2">
+                  <template #title>自定义图片(可选)</template>
+                  <el-upload
+                    class="avatar-uploader"
+                    v-model:file-list="fileList"
+                    :on-change="changeCustomFile"
+                    v-loading="upCustomLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :auto-upload="false"
+                    :on-remove="handleRemove"
+                    style="padding-right: 10px">
+                    <img v-if="imageUrl" :src="imageUrl" class="avatar" />
+                    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">图片规格</span>
+                          <el-button type="primary" :icon="Picture">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 1200 x 628 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 5MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">4、内容: 图片中未添加文本、图形或徽标</div>
+                      </div>
+                    </template>
+                  </el-upload>
+                </el-collapse-item>
+
+                <el-collapse-item name="3" style="padding: 0 10px 0 5px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
+                  <div v-for="item in commodityCard" :key="item.asin">
+                    <el-card shadow="hover" body-style="padding: 10px">
+                      <div style="padding: 10px; display: flex; align-items: center">
+                        <div style="margin-right: 8px; line-height: normal">
+                          <el-image class="img-box" :src="item.image_link" />
+                        </div>
+                        <div style="position: relative">
+                          <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                            <div class="single-line">{{ item.title }}</div>
+                          </el-tooltip>
+                          <span>
+                            <span style="color: #6d7784">ASIN: </span>
+                            <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                          </span>
+                        </div>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item name="4">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, onMounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { checkAsset, uploadFile } from '../api/index'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture } from '@element-plus/icons-vue'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const addedTableDataForVc2 = inject<Ref>('addedTableDataForVc2')
+
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+
+const ruleFormRef = ref<FormInstance>()
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  console.log(val)
+}
+
+const fileList = ref<UploadUserFile[]>([])
+
+const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
+  fileList.value = []
+}
+
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+  console.log(uploadFile)
+}
+
+const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
+  ElMessage.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + uploadFiles.length} totally`)
+}
+
+const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
+  return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
+    () => true,
+    () => false
+  )
+}
+
+function changeFile(file) {
+  handleUpload(file)
+}
+
+const upLoading = ref(false)
+let respAssetId = ''
+let brandEntityId = ''
+let brandLogoCrop = {}
+
+async function handleUpload(file) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  // formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+// 总定义图片上传
+const upCustomLoading = ref(false)
+
+function changeCustomFile(file) {
+  handleCustomUpload(file)
+}
+
+async function handleCustomUpload(file) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  // formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upCustomLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upCustomLoading.value = false
+  }
+}
+
+let asins = ref([])
+
+watch(
+  addedTableDataForVc2,
+  (newValue) => {
+    asins.value = []
+    if (Array.isArray(newValue) && newValue.length > 0) {
+      newValue.forEach((item) => {
+        if (item.asin && !asins.value.includes(item.asin)) {
+          asins.value.push(item.asin)
+        }
+      })
+    }
+    console.log('Updated ASINs:', asins.value)
+  },
+  { deep: true }
+)
+
+const commodityCard = ref([])
+onMounted(()=> {
+  emitter.on('addedTableData', (data: any) => {
+    console.log('data', data)
+    commodityCard.value = data
+    console.log(commodityCard.value)
+  })
+})
+
+function handleSelect() {
+  // console.log()
+}
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  margin-top: 5px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.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);
+}
+</style>

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

@@ -0,0 +1,464 @@
+<template>
+  <div prop="commodity" style="width: 100%" v-loading="productLoading">
+    <div style="width: 100%; height: 620px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="productTabs" class="demo-tabs">
+          <el-tab-pane label="搜索" name="first">
+            <div style="margin-bottom: 10px">
+              <el-input v-model="searchInp" placeholder="Please input" class="input-with-select" @change="inpChange" clearable>
+                <template #prepend>
+                  <el-select v-model="leftSelect" style="width: 100px" @change="selChange">
+                    <el-option label="名称" value="name" />
+                    <el-option label="ASIN" value="asin" />
+                    <el-option label="SKU" value="sku" />
+                  </el-select>
+                </template>
+                <template #append>
+                  <el-select v-model="rightSelect" style="width: 100px">
+                    <el-option label="最新优先" value="latest" />
+                    <el-option label="最早优先" value="earliest" />
+                    <el-option label="优选广告" value="optimal" />
+                  </el-select>
+                </template>
+              </el-input>
+            </div>
+            <el-table
+              height="490"
+              style="width: 100%"
+              v-loading="loading"
+              :data="productTableData"
+              :header-cell-style="headerCellStyle"
+              @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="50" />
+              <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>
+                      <div class="data-color">
+                        <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                        <span style="margin: 0 5px; color: #cacdd4">|</span>
+                        <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                      </div>
+                      <span>
+                        ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                      </span>
+                      <span>
+                        SKU: <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                      </span>
+                    </div>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" label="Name" width="120" align="right">
+                <template #header>
+                  <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" :disabled="addedTableData.length >= 1" @click="addSingleGoods(scope)" text>添加</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-pagination
+              @current-change="handleCurrentChange"
+              @size-change="handleSizeChange"
+              :current-page="currentPage"
+              :page-size="pageSize"
+              :total="totalItems"
+              layout="prev, pager, next" />
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input
+              style="padding: 10px"
+              v-model="productTextarea"
+              :rows="20"
+              type="textarea"
+              placeholder="请输入ASIN,多个ASIN使用逗号、空格或换行符分隔。(未完成)"
+              maxlength="11000" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button v-for="button in buttons" :key="button.text" :type="button.type" link @click="addGods">{{ button.text }}</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: 0">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedTableData.length }}</span>
+
+                <el-text type="warning" truncated>最多添加一个产品</el-text>
+
+              <el-button class="button" type="danger" text bg @click="delAllGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -12px">
+          <el-table
+            :data="addedTableData"
+            height="475"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedGoodsChange">
+            <el-table-column type="selection" width="50" />
+            <el-table-column prop="asin" label="ASIN">
+              <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>
+                    <div class="data-color">
+                      <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                      <span style="margin: 0 5px; color: #cacdd4">|</span>
+                      <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                    </div>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                    <span
+                      >SKU:
+                      <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="name" label="Name" width="120" align="right">
+              <template #header>
+                <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>
+              </template>
+            </el-table-column>
+          </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>
+        </div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject, onMounted, ref, watch, provide, onUnmounted } from 'vue'
+import type { Ref } from 'vue'
+import type { TabsPaneContext } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { request } from '/@/utils/service'
+import emitter from '/@/utils/emitter'
+
+// emitter.on('send-data', (newValue)=>{
+//   console.log(111, newValue)
+// })
+// // 接收数据端在组建卸载时解绑事件
+// onUnmounted(() => {
+//   emitter.off('send-data')
+// })
+
+// setTimeout(() => {
+//   emitter.emit('send-data', '123')
+// }, 2000);
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const productTextarea = ref('')
+const productLoading = ref(false)
+let addedAdsTableItems = ref([])
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const productTableData = ref([]) // 左侧表格数据
+const loading = ref(false)
+let addedTableData = ref([])
+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')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+function setTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      productTableData.value = resp.data
+      totalItems.value = resp.total
+      currentPage.value = resp.page
+      loading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      loading.value = false
+    })
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = addedTableData.value.some((item) => item.sku === scope.row.sku)
+  if (!isAlreadyAdded) {
+    addedTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function addGods() {
+  const inputData = productTextarea.value
+  const asins = inputData.split(/[\n,]+/)
+
+  asins.forEach((asin) => {
+    if (asin.trim()) {
+      setTableData(asin.trim())
+        .then((response) => {
+          console.log(`Data for ASIN ${asin}:`, response) // 更新这里来正确地访问数据
+        })
+        .catch((error) => {
+          console.error(`Error fetching data for ASIN ${asin}:`, error)
+        })
+    }
+  })
+}
+
+function delSingleGoods(scope) {
+  const index = addedTableData.value.findIndex((item) => item.sku === scope.row.sku)
+  if (index !== -1) {
+    addedTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function delAllGoods() {
+  addedTableData.value = []
+  // addedTableData.value.splice(0, addedTableData.value.length)
+}
+
+// 删除第二个table中已经选中的项
+function delSelectedGoods() {
+  addedTableData.value = addedTableData.value.filter((item) => !addedSels.includes(item))
+  addedSels = []
+}
+
+function inpChange(e) {
+  const value = e
+  if (leftSelect.value === 'asin') {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku') {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+
+function selChange(e) {
+  console.log('e', e)
+  const value = e
+  if (leftSelect.value === 'asin' && searchInp.value) {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku' && searchInp.value) {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+// 点击表格选项触发事件
+function handleSelectionChange(selection) {
+  selections = selection
+}
+// 获取addedTable中已选中的项
+function handleAddedGoodsChange(selection) {
+  addedSels = selection
+}
+// 添加已选中的项
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !addedTableData.value.some((added) => added.sku === sel.sku) // 使用sku作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    addedTableData.value.push(...newSelections)
+  }
+}
+// 点击Tab
+const handleGoodsTabs = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event)
+}
+
+function isItemInList(item, list) {
+  return list.some((listItem) => listItem.sku === item.sku && listItem.asin === item.asin)
+}
+// 监听商品右侧表格已添加的数据并转化数据格式
+watch(
+  addedTableData,
+  (newValue, oldValue) => {
+    newValue.forEach((item) => {
+      if (!isItemInList(item, addedAdsTableItems.value)) {
+        addedAdsTableItems.value.push({ sku: item.sku, asin: item.asin })
+      }
+    })
+    if (addedTableData.value.length !== 0) {
+      productSave.value = false
+    } else {
+      productSave.value = true
+    }
+  },
+  { deep: true }
+)
+
+async function createAds() {
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinsku: addedAdsTableItems.value,
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/spads/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ createAds ~ resp-->>', resp)
+    productLoading.value = false
+    if (resp.data.success.length > 0) {
+      productSave.value = false
+      addedTableData.value = []
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function submitProductForm() {
+  productLoading.value = true
+  createAds()
+}
+
+// 处理分页器当前页变化
+function handleCurrentChange(newPage) {
+  currentPage.value = newPage
+  loading.value = true
+  setTableData()
+}
+// 处理分页器每页显示条目数变化
+function handleSizeChange(newSize) {
+  pageSize.value = newSize
+  currentPage.value = 1 // 重置到第一页
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+
+const emit = defineEmits(['update-added-data']);
+
+// 当数据发生变化时,触发事件
+watch(addedTableData, (newValue) => {
+  emit('update-added-data', newValue);
+}, {deep: true});
+
+onMounted(() => {
+  setTableData()
+})
+</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;
+}
+.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;
+}
+/* 商品定向Tab栏 */
+::v-deep(.goods-orientation-tabs #tab-1) {
+  border-right: 0;
+}
+</style>

+ 898 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity1.vue

@@ -0,0 +1,898 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 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>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="1" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>品牌名称和徽标</template>
+                  <el-form-item prop="brandName">
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
+                  </el-form-item>
+
+                  <el-upload
+                    v-model:file-list="fileList"
+                    list-type="picture-card"
+                    :on-change="changeFile"
+                    v-loading="upLoading"
+                    action="#"
+                    accept=".png, .jpg"
+                    :limit="1"
+                    :auto-upload="false">
+                    <el-icon><Plus /></el-icon>
+                    <template #file="{ file }">
+                      <div>
+                        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+                        <span class="el-upload-list__item-actions">
+                          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+                            <el-icon><zoom-in /></el-icon>
+                          </span>
+                          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+                            <el-icon><Delete /></el-icon>
+                          </span>
+                        </span>
+                      </div>
+                    </template>
+                    <template #tip>
+                      <div style="margin-top: 10px">
+                        <div style="display: flex; align-items: center; justify-content: space-between">
+                          <span style="line-height: 17px; font-weight: 600; color: #1e2128">徽标规格</span>
+                          <el-button type="primary" :icon="Picture" disabled="true" @click="openDialog">从素材库中选择</el-button>
+                        </div>
+                        <div class="introduce-item">1、图片大小: 400x400 像素或更大</div>
+                        <div class="introduce-item">2、文件大小: 1MB 或更小</div>
+                        <div class="introduce-item">3、文件格式: PNG 或 JPG</div>
+                        <div class="introduce-item">
+                          4、内容: 徽标必须填满图片或置于白色或透明背景上详细了解我们的徽标要求
+                          <span style="margin-left: 25px; position: relative">
+                            <el-icon size="14" style="position: absolute; left: -14px; top: 1px"><Link /></el-icon>
+                            <el-link
+                              type="primary"
+                              :underline="false"
+                              href="https://advertising.amazon.com/resources/ad-policy/sponsored-ads-policies#brandlogo"
+                              target="_blank"
+                              >查看要求</el-link
+                            >
+                          </span>
+                        </div>
+                      </div>
+                    </template>
+                  </el-upload>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+                <el-collapse-item name="2" style="padding-right: 10px">
+                  <template #title><span style="color: #e47470; margin-right: 4px">*</span>视频</template>
+                  <div style="display: flex; justify-content: space-between">
+                    <div style="font-weight: 700; color: #1d2129; line-height: 18px; font-size: 14px">选择视频</div>
+                    <el-button type="primary" link>查看视频批准提示</el-button>
+                  </div>
+                  <div style="margin: 10px 0; font-size: 12px; font-weight: 400; color: #666; line-height: 18px">
+                    保持视频简短并紧扣主题。视频会自动播放,因此请确保前 2
+                    秒极具吸引力,并且不依靠声音来传递信息。如果您在视频中使用了文字,请确保文字清晰易辨。字幕或音频必须与将展示您广告的区域相匹配。
+                  </div>
+                  <el-upload
+                    v-model:file-list="videoList"
+                    :on-change="changeVideo"
+                    v-loading="videoLoading"
+                    accept=".mp4, .mov"
+                    action="#"
+                    :limit="1"
+                    :auto-upload="false"
+                    :on-remove="handleRemoveVideo"
+                    class="upload-demo">
+                    <el-button type="primary">上传视频</el-button>
+                    <template #tip>
+                      <el-button type="primary" disabled="true" style="margin-left: 20px">从素材库中选择</el-button>
+                      <!-- <div class="el-upload__tip">div> -->
+                      <hr style="margin: 10px 0" />
+                      <div>
+                        <div style="display: flex; justify-content: space-between">
+                          <span style="font-weight: 700; color: #1d2129; line-height: 18px; font-size: 14px">视频文件要求:</span>
+                          <el-button type="primary" link>了解更多</el-button>
+                        </div>
+                        <div class="tip-list-title">视频格式</div>
+                        <div class="tip-item">1.纵横比:16:9</div>
+                        <div class="tip-item">2.尺寸:1280 x 720 像素、1920 x 1080 像素或 3840 x 2160 像素</div>
+                        <div class="tip-item">3.文件大小:500MB 或更小</div>
+                        <div class="tip-item">4.文件格式:MP4 或 MOV</div>
+                        <div class="tip-item">5.长度:6-45 秒</div>
+                        <div class="tip-item">6.帧率:23.976、23.98、24、25、29.97 或 29.98 fps</div>
+                        <div class="tip-item">7.比特率:1 Mbps 或更高</div>
+                        <div class="tip-item">8.编解码器:H.264 或 H.2651</div>
+                        <div class="tip-item">9.配置文件:主配置文件或基线配置文件</div>
+                        <div class="tip-item">10.视频流:仅为 1</div>
+                        <div class="tip-list-title">音频规格</div>
+                        <div class="tip-item">1.语言:必须与广告投放区域匹配</div>
+                        <div class="tip-item">2.采样率:44.1 kHz 或更高</div>
+                        <div class="tip-item">3.编解码器:PCM、AAC 或 MP3</div>
+                        <div class="tip-item">4.比特率:96 kbps 或更高</div>
+                        <div class="tip-item">5.格式:立体声或单声道</div>
+                        <div class="tip-item">6.音频流:仅为 1</div>
+                      </div>
+                    </template>
+                  </el-upload>
+
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
+                </el-collapse-item>
+
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
+                  <div v-for="(item, index) in flattenedCommodityCard" :key="index" style="margin: 0 0 5px 0">
+                    <el-card shadow="hover" body-style="padding: 10px; display: flex;">
+                      <div style="margin-right: 8px; line-height: normal">
+                        <el-image class="img-box" :src="item.image_link" />
+                      </div>
+                      <div style="position: relative">
+                        <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                          <div class="double-line">{{ item.title }}</div>
+                        </el-tooltip>
+                        <span>
+                          <span style="color: #6d7784">ASIN: </span>
+                          <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                        </span>
+                        <el-button
+                          type="primary"
+                          size="small"
+                          link
+                          @click="() => openCommodityDialog(index)"
+                          style="position: absolute; bottom: 2px; right: 0">
+                          更换商品
+                        </el-button>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item name="4" style="padding-right: 10px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>标题</template>
+                  <el-form-item prop="title">
+                    <el-input v-model="ruleForm.title" maxlength="50" placeholder="请输入标题" show-word-limit style="padding: 0 10px 0 0"></el-input>
+                  </el-form-item>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button type="primary" plain @click="submitForm(ruleFormRef)" :disabled="!fileList.length" style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="lifeStyleDialog" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in lifeStyleCards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <el-image class="image" :src="item.imageUrl" fit="cover" />
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">徽标</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="centerDialogVisible = false">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="commodityDialog" title="更换商品" width="50%">
+      <el-radio-group
+        v-loading="dialogLoading3"
+        v-model="selectedCommodity"
+        style="display: flex; flex-direction: column; align-content: flex-start; align-items: flex-start">
+        <div v-for="(item, index) in flattenedReplaceableCommodity" :key="index">
+          <el-radio :label="item.asin" style="height: 80px; border-bottom: 1px solid #ccc">
+            <div style="padding: 10px; display: flex; align-items: center">
+              <div style="margin-right: 8px; line-height: normal">
+                <el-image class="img-box" :src="item.image_link" />
+              </div>
+              <div style="position: relative">
+                <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                  <div class="double-line">{{ item.title }}</div>
+                </el-tooltip>
+                <span>
+                  <span style="color: #6d7784">ASIN: </span>
+                  <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                </span>
+              </div>
+            </div>
+          </el-radio>
+        </div>
+      </el-radio-group>
+      <div style="margin-top: 20px; display: flex; justify-content: center">
+        <el-button type="primary" :disabled="!selectedCommodity" @click="handleSelectedCommodity">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, inject, Ref, watch, computed, onMounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search, Delete, Download, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import { getAssets, getLifeStyleAssets, getPageAsins, getCommodityCard, uploadFile, checkAsset, postBrandVideo } from '../api/index'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const ruleFormRef = ref<FormInstance>()
+
+interface RuleForm {
+  name: string
+  brandName: string
+  title: string
+}
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+  brandName: '',
+  title: '',
+})
+
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入广告名称', trigger: 'blur' }],
+  brandName: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }],
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+})
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createBrandVideo()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['1'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+  if (val.includes('commodity')) {
+    getCommodityCardData()
+  }
+}
+
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
+
+const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
+  if (rawFile.type !== 'image/jpeg') {
+    ElMessage.error('Avatar picture must be JPG format!')
+    return false
+  } else if (rawFile.size / 1024 / 1024 > 2) {
+    ElMessage.error('Avatar picture size can not exceed 2MB!')
+    return false
+  }
+  return true
+}
+
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+  console.log(uploadFile)
+}
+
+const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
+  ElMessage.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + uploadFiles.length} totally`)
+}
+
+const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
+  return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
+    () => true,
+    () => false
+  )
+}
+
+// 图片上传相关
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const disabled = ref(false)
+const fileList = ref([])
+const videoList = ref([])
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+const upLoading = ref(false)
+
+function handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+function changeFile(file: UploadFile) {
+  handleUpload(file)
+}
+let brandLogoCrop = {}
+let respAssetId = ''
+let brandEntityId = ''
+
+async function handleUpload(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'IMAGE')
+  formData.append('assetSubTypeList', JSON.stringify(['LOGO']))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    const { width, height } = resp.data.fileMetadata
+    brandLogoCrop = {
+      width,
+      height,
+      top: 0,
+      left: 0,
+    }
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+const videoLoading = ref(false)
+let videoAssetIds = ''
+function handleRemoveVideo(file: UploadFile) {
+  videoList.value = []
+}
+
+function changeVideo(file: UploadFile) {
+  uploadVideo(file)
+}
+
+// 上传视频
+async function uploadVideo(file: UploadFile) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'VIDEO')
+  formData.append('assetSubTypeList', JSON.stringify(['SPONSORED_BRANDS_VIDEO']))
+  videoLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    videoAssetIds = resp.data.assetId
+    
+
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    videoLoading.value = false
+  }
+}
+
+const respAdGroupId = inject<Ref>('respAdGroupId')
+let brandName = ''
+let selectedPage = ''
+async function createBrandVideo() {
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: asinListFromCommodityCard.value,
+      url: selectedPage,
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      brandName: brandName,
+      brandLogoAssetID: respAssetId,
+      brandLogoCrop: brandLogoCrop,
+      consentToTranslate: false,
+      videoAssetIds: videoAssetIds,
+      headline: ruleForm.title,
+    }
+    const response = await postBrandVideo(obj)
+    console.log('response', response)
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+onMounted(() => {
+  emitter.on('video-shop', (value: any) => {
+    brandName = value.brandRegistryName
+    brandEntityId = value.brandEntityId
+  })
+  emitter.on('page', (newPageValue: any) => {
+    selectedPage = newPageValue
+  })
+})
+
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push(item)
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 清空 fileList
+    fileList.value.length = 0
+
+    // 假设每次只选择一个图片
+    selectedImageUrl.value = selectedCards.value[0].imageUrl
+
+    // 创建一个新的 UploadFile 对象
+    const newFile = {
+      name: selectedCards.value[0].title, // 或者任何你希望用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+
+    // 将新的文件对象添加到 fileList 中
+    fileList.value.push(newFile)
+  }
+
+  // 清空选中卡片
+  selectedCards.value = []
+  // 关闭对话框
+  centerDialogVisible.value = false
+}
+
+function isSelected(id) {
+  return selectedId.value === id
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'IMAGE',
+    assetSubType: 'LOGO',
+  }
+  const response = await getAssets(query)
+  console.log('🚀 ~ getAssetsData ~ response-->>', response)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.width,
+      height: asset.fileMetadata.height,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+function openDialog() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+const lifeStyleDialog = ref(false)
+const lifeStyleCards = reactive([])
+async function getLifeStyleAssetsData() {
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      assetType: 'IMAGE',
+      assetSubType: 'LIFESTYLE_IMAGE',
+    }
+    const response = await getLifeStyleAssets(query)
+    console.log('🚀 ~ getLifeStyleAssetsData ~ response-->>', response)
+
+    lifeStyleCards.splice(0, lifeStyleCards.length)
+
+    response.data.forEach((asset) => {
+      lifeStyleCards.push({
+        id: asset.assetId,
+        title: asset.name,
+        imageUrl: asset.storageLocationUrls.defaultUrl,
+        width: asset.fileMetadata.width,
+        height: asset.fileMetadata.height,
+        size: bytesToKB(asset.fileMetadata.sizeInBytes),
+      })
+    })
+  } catch (error) {
+    console.log('error:', error)
+  }
+}
+
+function openLifeStyleDialog() {
+  lifeStyleDialog.value = true
+  getLifeStyleAssetsData()
+}
+
+const pageOptionsValue = inject<Ref>('pageOptionsValue')
+const asinList = ref([])
+const commodityLoading = ref(false)
+
+async function getCommodityCollapseData() {
+  commodityLoading.value = true
+  try {
+    const query = {
+      profile_id: profile.value.profile_id,
+      pageurl: pageOptionsValue.value,
+    }
+    const response = await getPageAsins(query)
+    asinList.value = response.data.asinList
+    console.log('asinList', asinList.value)
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+function clickSave() {
+  console.log(123, fileList.value)
+}
+
+let lastQueriedAsins = []
+const commodityCard = ref([])
+async function getCommodityCardData() {
+  try {
+    commodityLoading.value = true
+    const topAsins = asinList.value.slice(0, 3)
+
+    const newAsins = topAsins.filter((asin) => !lastQueriedAsins.includes(asin))
+    if (newAsins.length === 0) {
+      commodityLoading.value = false
+      return // 如果没有新的 ASIN,直接返回
+    }
+    lastQueriedAsins = [...topAsins]
+    // 清空commodityCard,为新数据做准备
+    commodityCard.value = []
+    // 对每个新的 ASIN 发送请求
+    for (const asin of newAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+
+      try {
+        const response = await getCommodityCard(query)
+        commodityCard.value.push(response.data)
+        // console.log('Response for ASIN', asin, ':', response)
+      } catch (error) {
+        console.log('Error for ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    commodityLoading.value = false
+  }
+}
+
+// 更改商品功能
+const commodityDialog = ref(false)
+const selectedCommodity = ref()
+const replaceableCommodity = ref([])
+const dialogLoading3 = ref(false)
+let currentEditingIndex = ref(null)
+
+function openCommodityDialog(index) {
+  currentEditingIndex.value = index
+  commodityDialog.value = true
+}
+
+async function getAdditionalCommodityData() {
+  try {
+    dialogLoading3.value = true
+    // 获取除前三个之外的所有 ASIN
+    const additionalAsins = asinList.value.slice(3)
+    // 清空 replaceableCommodity,为新数据做准备
+    replaceableCommodity.value = []
+    // 对每个额外的 ASIN 发送请求
+    for (const asin of additionalAsins) {
+      const query = {
+        profile_id: profile.value.profile_id,
+        asin: asin,
+      }
+      try {
+        const response = await getCommodityCard(query)
+        replaceableCommodity.value.push(response.data)
+      } catch (error) {
+        console.log('Error for additional ASIN', asin, ':', error)
+      }
+    }
+  } catch (error) {
+    console.log('Outer error:', error)
+  } finally {
+    dialogLoading3.value = false
+  }
+}
+
+function handleSelectedCommodity() {
+  if (currentEditingIndex.value !== null && selectedCommodity.value) {
+    const selectedCommodityData = flattenedReplaceableCommodity.value.find((item) => item.asin === selectedCommodity.value)
+    if (selectedCommodityData) {
+      commodityCard.value[currentEditingIndex.value] = selectedCommodityData
+    }
+    commodityDialog.value = false
+    currentEditingIndex.value = null
+    selectedCommodity.value = null
+  }
+}
+
+watch(
+  () => pageOptionsValue.value,
+  async () => {
+    await getCommodityCollapseData()
+    getCommodityCardData()
+    getAdditionalCommodityData()
+  }
+)
+
+const flattenedCommodityCard = computed(() => {
+  return commodityCard.value.flat()
+})
+
+const asinListFromCommodityCard = computed(() => {
+  return flattenedCommodityCard.value.map((item) => item.asin)
+})
+
+// watch(asinListFromCommodityCard, (newAsinList) => {
+//   console.log('New ASIN list:', newAsinList);
+// });
+
+const flattenedReplaceableCommodity = computed(() => {
+  return replaceableCommodity.value.flat()
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.tip-list-title {
+  line-height: 17px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #1d2129;
+  padding-top: 8px;
+  padding-bottom: 4px;
+}
+.tip-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #86909c;
+}
+.avatar-uploader .avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+::v-deep(.avatar-uploader .el-upload) {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+
+::v-deep(.avatar-uploader .el-upload:hover) {
+  border-color: var(--el-color-primary);
+}
+::v-deep(.el-icon.avatar-uploader-icon) {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  text-align: center;
+}
+::v-deep(.avatar-uploader .el-upload.el-upload--text) {
+  width: 100%;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.uploaded-image {
+  width: 100%; /* 或根据需要调整 */
+  height: auto; /* 保持图片的原始宽高比 */
+  display: block;
+  margin-bottom: 10px; /* 或根据需要调整 */
+}
+
+.upload-content {
+  text-align: center;
+  padding: 20px;
+}
+.el-carousel__item h3 {
+  color: #edf5fe;
+  opacity: 0.75;
+  line-height: 200px;
+  margin: 0;
+  text-align: center;
+}
+::v-deep(button.el-carousel__button) {
+  background-color: #3569d6;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

+ 513 - 0
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity2.vue

@@ -0,0 +1,513 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;" v-loading="loading">
+      <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>
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="120px"
+        class="demo-ruleForm"
+        size="default"
+        label-position="top"
+        status-icon>
+        <el-form-item label="广告名称" prop="name">
+          <el-input v-model="ruleForm.name" style="width: 50%" />
+        </el-form-item>
+        <div style="display: flex; border: 1px solid #dddfe6; padding: 0 0 0 5px; margin-bottom: 20px" v-loading="createLoading">
+          <div style="width: 50%; padding-left: 5px; border-right: 1px solid #dddfe6">
+            <el-scrollbar height="700px">
+              <el-collapse v-model="activeNames" @change="handleChange" style="border-top: none; border-bottom: none">
+                <el-collapse-item name="video" style="padding: 0 10px 0 5px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>视频 </template>
+                  <div>
+                    <div style="display: flex; align-items: center; justify-content: space-between">
+                      <span style="color: #1e2128; font-size: 15px; font-weight: 450">选择视频</span>
+                      <el-button type="primary" link>查看视频批准提示</el-button>
+                    </div>
+                    <div style="color: #666666; margin-bottom: 10px">
+                      保持视频简短并紧扣主题。视频会自动播放,因此请确保前 2
+                      秒极具吸引力,并且不依靠声音来传递信息。如果您在视频中使用了文字,请确保文字清晰易辨。字幕或音频必须与将展示您广告的区域相匹配。
+                    </div>
+                    <div class="upload-button-group">
+                      <el-upload
+                        v-model:file-list="fileList"
+                        :on-change="changeFile"
+                        v-loading="upLoading"
+                        accept=".mp4, .mov"
+                        action="#"
+                        :limit="1"
+                        :auto-upload="false"
+                        :on-remove="handleRemove"
+                        class="upload-demo">
+                        <el-button type="primary">上传文件</el-button>
+
+                        <!-- <template #tip>
+                          <div class="el-upload__tip"><el-button type="primary" :icon="Picture"  :disabled="true" @click="handleSelect">从素材库中选择</el-button></div>
+                        </template> -->
+                      </el-upload>
+                      <!-- <el-button type="primary" :icon="Picture" style="margin-left: -120px" :disabled="true" @click="handleSelect">从素材库中选择</el-button> -->
+                    </div>
+                  </div>
+                </el-collapse-item>
+                <div style="display: flex; align-items: center; justify-content: space-between; padding: 0 10px 0 5px">
+                  <span style="color: #1e2128; font-size: 15px; font-weight: 450">视频文件要求:</span>
+                  <el-button type="primary" link>了解更多</el-button>
+                </div>
+                <div style="padding: 0 10px 0 5px">
+                  <span style="color: #1e2128; font-size: 13px; font-weight: 450">视频规格</span>
+                  <div class="introduce-item">1.纵横比:16:9</div>
+                  <div class="introduce-item">2.尺寸:1280 x 720 像素、1920 x 1080 像素或 3840 x 2160 像素</div>
+                  <div class="introduce-item">3.文件大小:500MB 或更小</div>
+                  <div class="introduce-item">4.文件格式:MP4 或 MOV</div>
+                  <div class="introduce-item">5.长度:6-45 秒</div>
+                  <div class="introduce-item">6.帧率:23.976、23.98、24、25、29.97 或 29.98 fps</div>
+                  <div class="introduce-item">7.比特率:1 Mbps 或更高</div>
+                  <div class="introduce-item">8.编解码器:H.264 或 H.265</div>
+                  <div class="introduce-item">9.配置文件:主配置文件或基线配置文件</div>
+                  <div class="introduce-item">10.视频流:仅为 1</div>
+                </div>
+                <div style="padding: 0 10px 0 5px">
+                  <span style="color: #1e2128; font-size: 13px; font-weight: 450">音频规格</span>
+                  <div class="introduce-item">1.语言:必须与广告投放区域匹配</div>
+                  <div class="introduce-item">2.采样率:44.1 kHz 或更高</div>
+                  <div class="introduce-item">3.编解码器:PCM、AAC 或 MP3</div>
+                  <div class="introduce-item">4.比特率:96 kbps 或更高</div>
+                  <div class="introduce-item">5.格式:立体声或单声道</div>
+                  <div class="introduce-item">6.音频流:仅为 1</div>
+                </div>
+                <hr style="color: #eceef4; margin: 8px 10px 0 5px" />
+                <el-collapse-item name="commodity" style="padding: 0 10px 0 5px">
+                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品 </template>
+                  <div v-for="item in addedTableDataForVc2" :key="item.asin">
+                    <el-card shadow="hover" body-style="padding: 10px">
+                      <div style="padding: 10px; display: flex; align-items: center">
+                        <div style="margin-right: 8px; line-height: normal">
+                          <el-image class="img-box" :src="item.image_link" />
+                        </div>
+                        <div style="position: relative">
+                          <el-tooltip class="box-item" effect="dark" :content="item.title" placement="top">
+                            <div class="double-line">{{ item.title }}</div>
+                          </el-tooltip>
+                          <span>
+                            <span style="color: #6d7784">ASIN: </span>
+                            <span class="data-color" style="margin-right: 8px">{{ item.asin }}</span>
+                          </span>
+                        </div>
+                      </div>
+                    </el-card>
+                  </div>
+                </el-collapse-item>
+              </el-collapse>
+            </el-scrollbar>
+          </div>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button
+              type="primary"
+              plain
+              @click="submitForm(ruleFormRef)"
+              :disabled="!commodityCard.length && addedTableDataForVc2"
+              style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
+          </div>
+        </div>
+      </el-form>
+    </el-card>
+    <el-dialog v-model="centerDialogVisible" title="从素材库中选择" width="65%">
+      <el-input :prefix-icon="Search"></el-input>
+      <div class="grid-container">
+        <div
+          class="grid-item"
+          v-for="item in cards"
+          :key="item.id"
+          @click="selectCard(item)"
+          :class="{ selected: isSelected(item.id), hover: hoverId === item.id }"
+          @mouseover="hoverId = item.id"
+          @mouseleave="hoverId = null">
+          <el-card :body-style="{ padding: '0px' }">
+            <video class="image" :src="item.imageUrl" controls preload="none" @click.stop></video>
+            <div style="padding: 10px">
+              <span>
+                <el-tooltip placement="top" :content="item.title">
+                  {{ item.title }}
+                </el-tooltip>
+              </span>
+              <div class="bottom">
+                <div class="bottom-item">{{ item.size }}KB</div>
+                <div class="bottom-item">{{ item.width }} * {{ item.height }}</div>
+                <div class="bottom-item">背景视频</div>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="centerDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmSelection">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, watch, inject, Ref, onMounted } from 'vue'
+import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Picture, Search } from '@element-plus/icons-vue'
+import { getVideoAssets, videoDetailCreate, uploadFile, checkAsset, postVideo } from '../api/index'
+import emitter from '/@/utils/emitter'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const respAdGroupId = inject<Ref>('respAdGroupId')
+const addedTableDataForVc2 = inject<Ref>('addedTableDataForVc2')
+
+interface RuleForm {
+  name: string
+}
+const ruleFormRef = ref<FormInstance>()
+const ruleForm = reactive<RuleForm>({
+  name: '视频 广告 - 1/15/2024 17:51:10.236',
+})
+const rules = reactive<FormRules<RuleForm>>({
+  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+})
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      createVideo()
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+const activeNames = ref(['video'])
+const handleChange = (val: string[]) => {
+  // console.log(val)
+}
+
+const fileList = ref<UploadUserFile[]>([]) // 上传的文件列表
+
+const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
+  fileList.value.length = 0
+  // console.log(file, uploadFiles)
+}
+
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+  console.log(uploadFile)
+}
+
+const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
+  ElMessage.warning(`The limit is 3, you selected ${files.length} files this time, add up to ${files.length + uploadFiles.length} totally`)
+}
+
+const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
+  return ElMessageBox.confirm(`Cancel the transfer of ${uploadFile.name} ?`).then(
+    () => true,
+    () => false
+  )
+}
+
+function changeFile(file) {
+  // console.log('file', file)
+  handleUpload(file)
+}
+
+const upLoading = ref(false)
+let brandLogoCrop = {}
+let respAssetId = ''
+let brandEntityId = ''
+
+async function handleUpload(file) {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  formData.append('profile_id', profile.value.profile_id)
+  formData.append('brandEntityId', brandEntityId)
+  formData.append('assetType', 'VIDEO')
+  formData.append('assetSubTypeList', JSON.stringify([]))
+  upLoading.value = true
+  try {
+    const response = await uploadFile(formData)
+    const fileName = response.data.file_name
+    const obj = {
+      profile_id: profile.value.profile_id,
+      file_name: fileName,
+    }
+    const resp = await checkAsset(obj)
+    respAssetId = resp.data.assetId
+    console.log('🚀 ~ handleUpload ~ respAssetId-->>', respAssetId)
+    if (resp.data.checkresult == 'success') {
+      ElMessage({ message: '上传成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  } finally {
+    upLoading.value = false
+  }
+}
+
+const createLoading = ref(false)
+async function createVideo() {
+  createLoading.value = true
+  try {
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: asins.value,
+      name: ruleForm.name,
+      state: 'PAUSED',
+      adGroupId: respAdGroupId.value,
+      videoAssetIds: respAssetId,
+      consentToTranslate: false,
+    }
+    const response = await postVideo(obj)
+    if (response.data.creative_state == 'success') {
+      ElMessage({ message: '创建成功', type: 'success' })
+    } else {
+      ElMessage.error('上传失败')
+    }
+  } catch (error) {
+    console.error('error:', error)
+  } finally {
+    createLoading.value = false
+  }
+}
+
+let asins = ref([])
+watch(
+  addedTableDataForVc2,
+  (newValue) => {
+    asins.value = []
+    if (Array.isArray(newValue) && newValue.length > 0) {
+      newValue.forEach((item) => {
+        if (item.asin && !asins.value.includes(item.asin)) {
+          asins.value.push(item.asin)
+        }
+      })
+    }
+    console.log('Updated ASINs:', asins.value)
+  },
+  { deep: true }
+)
+
+// dialog以及选择card相关功能
+const selectedId = ref(null)
+const hoverId = ref(null)
+const selectedCards = ref([])
+const selectedImageUrl = ref('')
+const centerDialogVisible = ref(false)
+const cards = reactive([])
+let selectedAssetId = ''
+
+function resetSelection() {
+  selectedCards.value = []
+  selectedId.value = null
+}
+
+function bytesToKB(bytes) {
+  return (bytes / 1024).toFixed(2) // 保留两位小数
+}
+
+async function getAssetsData() {
+  const query = {
+    profile_id: profile.value.profile_id,
+    assetType: 'VIDEO',
+    specCheckApprovedPrograms: 'SPONSORED_BRANDS_VIDEO',
+  }
+  const response = await getVideoAssets(query)
+
+  cards.splice(0, cards.length)
+
+  response.data.forEach((asset) => {
+    cards.push({
+      id: asset.assetId,
+      title: asset.name,
+      imageUrl: asset.storageLocationUrls.defaultUrl,
+      width: asset.fileMetadata.resolutionWidth,
+      height: asset.fileMetadata.resolutionHeight,
+      size: bytesToKB(asset.fileMetadata.sizeInBytes),
+    })
+  })
+}
+
+function isSelected(id) {
+  return selectedCards.value.some((card) => card.id === id)
+}
+
+function selectCard(item) {
+  if (isSelected(item.id)) {
+    selectedCards.value = selectedCards.value.filter((card) => card.id !== item.id)
+  } else {
+    selectedCards.value.push({
+      id: item.id,
+      assetId: item.id,
+      title: item.title,
+      imageUrl: item.imageUrl,
+    })
+  }
+  selectedId.value = item.id
+}
+
+function handleConfirmSelection() {
+  if (selectedCards.value.length > 0) {
+    // 获取选中卡片的 assetId
+    selectedAssetId = selectedCards.value[0].assetId
+    const newFile = {
+      name: selectedCards.value[0].title, // 任何可以用作文件名的字符串
+      url: selectedImageUrl.value,
+      // 根据需要添加更多属性
+    }
+    fileList.value.push(newFile)
+
+    // 清空选中卡片
+    resetSelection()
+    centerDialogVisible.value = false
+  }
+}
+
+function handleSelect() {
+  centerDialogVisible.value = true
+  getAssetsData()
+}
+
+// 创建商品创意相关
+const loading = ref(false)
+async function createCommodity() {
+  try {
+    loading.value = true
+    const obj = {
+      profile_id: profile.value.profile_id,
+      casins: commodityCard.value[0].asin,
+      name: ruleForm.name,
+      state: 'ENABLED',
+      adGroupId: respAdGroupId.value,
+      consentToTranslate: false,
+      videoAssetIds: selectedAssetId,
+    }
+    const response = await videoDetailCreate(obj)
+    console.log('🚀 ~ response-->>', response)
+    if (response.data.creative_state == 'success') {
+      ElMessage({
+        message: '商品创意创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创意创建失败!')
+    }
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+function clickSave() {
+  console.log(123, fileList.value)
+  createCommodity()
+  commodityCard.value.length = 0
+}
+
+// 已选择的商品
+const commodityCard = ref([])
+
+watch(
+  addedTableDataForVc2,
+  (newValue) => {
+    commodityCard.value = newValue
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.upload-button-group {
+  display: flex;
+}
+.introduce-item {
+  line-height: 17px;
+  font-size: 12px;
+  color: #88909b;
+}
+.grid-container {
+  flex-wrap: wrap;
+  display: flex;
+  width: 100%;
+  justify-content: left;
+}
+.grid-item {
+  transition: outline, background-color 0.3s;
+  box-sizing: border-box;
+  border: 1px solid #ffffff00;
+  cursor: pointer;
+  width: calc(25% - 10px);
+  margin: 10px 5px;
+}
+.grid-item span {
+  display: block; /* 或者 inline-block */
+  white-space: nowrap; /* 保持文本在一行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+  max-width: 100%; /* 限制最大宽度 */
+  font-weight: 600;
+  line-height: 22px;
+}
+.grid-item.hover,
+.grid-item.selected {
+  border: 1px solid #306cd8;
+  border-radius: 4px;
+}
+.grid-item.selected > :first-child {
+  background-color: #f5f7fe;
+}
+.image {
+  width: 100%;
+  height: 146.49px;
+  padding: 10px;
+}
+.image > :first-child {
+  border-radius: 10px;
+}
+.bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 5px;
+}
+.bottom-item {
+  background-color: #f5f7fe;
+  border-radius: 4px;
+  padding: 0 3px;
+}
+.img-box {
+  width: 60px;
+  height: 60px;
+  border: 1px solid rgb(194, 199, 207);
+  border-radius: 4px;
+}
+.double-line {
+  color: #1e2128;
+  font-weight: 500;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+</style>

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

@@ -1,11 +1,87 @@
 <template>
   <div class="page-container">
-    <AdCampaigns></AdCampaigns>
+    <AdCampaign @update-campaign="handleCampaignUpdate"></AdCampaign>
+    <AdGroup @update-group-id="handleGroupIdUpdate"></AdGroup>
+    <AdFormat
+      @update:adFormatRadio="handleAdFormatRadioChange"
+      @update:arrivalsRadio="handleArrivalsRadioChange"
+      @update:flagshipStoreShop="handleFlagshipStoreShopChange"
+      @update:pageOptions="handlePageOptionsChange"
+      @update:addedTableData="handleUpdateAddedData"
+      @update:focusShopSelect="handleFocusShopSelectChange"></AdFormat>
+    
+    <ProductSetCreativity1 v-if="adFormatRadioValue === 'productSet' && arrivalsRadioValue === 'flagshipStore' && flagshipStoreShopValue === 'ZOSI'">
+    </ProductSetCreativity1>
+    <FocusCreativity v-if="adFormatRadioValue === 'focus' && focusShop === 'ENTITY2NJKG6JSUTTPB'"></FocusCreativity>
+    <VideoCreativity1 v-if="adFormatRadioValue === 'video' && arrivalsRadioValue === 'flagshipStore' && flagshipStoreShopValue === 'ZOSI'">
+    </VideoCreativity1>
+    <VideoCreativity2 v-if="adFormatRadioValue === 'video' && arrivalsRadioValue === 'productDetailsPage'"></VideoCreativity2>
+    <ProductSetCreativity2 v-if="adFormatRadioValue === 'productSet' && arrivalsRadioValue === 'newArrivals'"></ProductSetCreativity2>
+    <DeliveryType></DeliveryType>
+    <p>12323{{ pageOptionsValue }}</p>
   </div>
 </template>
 
 <script lang="ts" setup>
-import AdCampaigns from './component/AdCampaigns.vue'
+import { provide, ref, onMounted, onUnmounted} 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'
+import VideoCreativity1 from './component/VideoCreativity1.vue'
+import VideoCreativity2 from './component/VideoCreativity2.vue'
+import ProductSetCreativity1 from './component/ProductSetCreativity1.vue'
+import ProductSetCreativity2 from './component/ProductSetCreativity2.vue'
+import FocusCreativity from './component/FocusCreativity.vue'
+import emitter from '/@/utils/emitter'
+
+const respCampaignId = ref('')
+const respCampaignName = ref('')
+const respAdGroupId = ref('')
+const adFormatRadioValue = ref('')
+const arrivalsRadioValue = ref('')
+const flagshipStoreShopValue = ref('')
+const pageOptionsValue = ref('')
+const addedTableDataForVc2 = ref('')
+const focusShop = ref('')
+const focusShopLabel = ref('')
+
+provide('respCampaignId', respCampaignId)
+provide('respCampaignName', respCampaignName)
+provide('respAdGroupId', respAdGroupId)
+provide('pageOptionsValue', pageOptionsValue)
+provide('addedTableDataForVc2', addedTableDataForVc2)
+provide('focusShop', focusShop)
+provide('focusShopLabel', focusShopLabel)
+
+const handleCampaignUpdate = (data) => {
+  respCampaignId.value = data.id
+  respCampaignName.value = data.name
+}
+const handleGroupIdUpdate = (data) => {
+  respAdGroupId.value = data.id
+}
+const handleAdFormatRadioChange = (newValue) => {
+  adFormatRadioValue.value = newValue // 更新 adFormatRadioValue
+}
+const handleArrivalsRadioChange = (newValue) => {
+  arrivalsRadioValue.value = newValue
+}
+const handleFlagshipStoreShopChange = (newValue) => {
+  flagshipStoreShopValue.value = newValue
+}
+const handlePageOptionsChange = (newValue) => {
+  pageOptionsValue.value = newValue
+}
+
+function handleUpdateAddedData(data) {
+  addedTableDataForVc2.value = data
+}
+
+function handleFocusShopSelectChange(newValue) {
+  focusShop.value = newValue
+  focusShopLabel.value = 'ZOSI'
+}
 </script>
 
 <style scoped>
@@ -13,4 +89,8 @@ import AdCampaigns from './component/AdCampaigns.vue'
   padding: 12px;
   background-color: #fafafa;
 }
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+  color: #505968;
+}
 </style>

+ 96 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/api/index.ts

@@ -0,0 +1,96 @@
+import { request } from '/@/utils/service'
+
+
+export function getAdMixSelect() {
+  return request({
+      url: '/api/ad_manage/portfolios/select_list',
+      method: 'GET',
+  })
+}
+
+export function postCampaignsData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sbcampaigns/create/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function postGroupData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sbgroups/create/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function postNegativeWordData(filteredRequestData) {
+  return request({
+      url: '/api/ad_manage/sptargets/add/negative/keywords/',
+      method: 'post',
+      data: filteredRequestData,
+  })
+}
+
+export function getAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getLifeStyleAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getBrands(query) {
+  return request({
+    url: '/api/ad_manage/sb/getbrands/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getStoreurl(query) {
+  return request({
+    url: '/api/ad_manage/sb/storeurl/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getPageAsins(query) {
+  return request({
+    url: '/api/ad_manage/sb/getpageasins/',
+      method: 'get',
+      params: query
+  })
+}
+export function getCommodityCard(query) {
+  return request({
+    url: '/api/sellers/listings/all/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function getVideoAssets(query) {
+  return request({
+      url: '/api/ad_manage/sb/assets/',
+      method: 'get',
+      params: query
+  })
+}
+
+export function videoDetailCreate(obj) {
+  return request({
+      url: '/api/ad_manage/sbads/video/create/',
+      method: 'post',
+      data: obj,
+  })
+}

+ 93 - 90
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdCampaigns.vue → src/views/adManage/sd/campaigns/CreateCampaigns/component/AdCampaign.vue

@@ -1,9 +1,9 @@
 <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>
-        <span style="font-size: 18px; padding-left: 5px">设置</span>
+        <span style="font-size: 18px; padding-left: 5px">广告活动</span>
       </div>
       <el-form
         ref="campaignRuleFormRef"
@@ -12,7 +12,7 @@
         label-position="top"
         label-width="120px"
         class="demo-ruleForm"
-        :size="formSize"
+        size="default"
         status-icon>
         <div class="flex-between">
           <el-form-item label="广告活动名称" prop="campaignName" style="width: 48%">
@@ -20,8 +20,7 @@
           </el-form-item>
           <el-form-item label="广告组合" prop="adMix" style="width: 48%">
             <el-select v-model="campaignRuleForm.adMix" placeholder="请选择" style="width: 100%">
-              <el-option label="Zone one" value="shanghai" />
-              <el-option label="Zone two" value="beijing" />
+              <el-option v-for="item in adMixOptions" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </el-form-item>
         </div>
@@ -37,7 +36,6 @@
                 value-format="YYYY-MM-DD"
                 style="width: 100%" />
             </el-form-item>
-
             <el-form-item label="结束时间" prop="endDate" style="width: 49%">
               <el-date-picker
                 v-model="campaignRuleForm.endDate"
@@ -50,34 +48,20 @@
             </el-form-item>
           </div>
           <div class="flex-between" style="width: 48%">
-            <el-form-item label="预算" required prop="budget" style="width: 65%">
+            <el-form-item label="预算" required prop="budget" style="width: 100%">
               <el-input v-model="campaignRuleForm.budget" minlength="1" maxlength="7" placeholder="请输入" style="width: 100%">
                 <template #prepend>$</template>
               </el-input>
             </el-form-item>
-            <el-form-item label="频率" prop="frequency" style="width: 34%">
-              <el-select v-model="campaignRuleForm.frequency" placeholder="请选择" style="width: 100%">
-                <el-option v-for="item in frequencyOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
-              </el-select>
-            </el-form-item>
           </div>
         </div>
-        <el-form-item label="品牌" prop="brand" required style="width: 48%">
-          <el-select v-model="campaignRuleForm.brand" placeholder="请选择" style="width: 100%">
-            <el-option v-for="item in brandOptions" :key="item.value" :label="item.label" :value="item.value" />
-          </el-select>
-        </el-form-item>
-        <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>
-        <el-form-item label="自动竞价" style="margin-bottom: -13px">
-          <el-form-item>
-            <el-switch v-model="campaignRuleForm.isBid" />
-            <span style="margin-left: 10px; color: #88909b">允许亚马逊自动优化搜索结果首页以外的广告位竞价</span>
-          </el-form-item>
+        <el-form-item label="投放类型">
+          <el-radio-group v-model="targetType">
+            <el-radio label="T00030">受众</el-radio>
+            <el-radio label="T00020">内容相关投放</el-radio>
+          </el-radio-group>
         </el-form-item>
-        <el-form-item style="margin-left: 48%; margin-bottom: -10px">
+        <el-form-item style="margin: 20px 0 -10px 48%">
           <el-button type="primary" plain @click="submitCampaignForm(campaignRuleFormRef)">保存</el-button>
         </el-form-item>
       </el-form>
@@ -86,19 +70,20 @@
 </template>
 
 <script lang="ts" setup>
-import { reactive, ref } from 'vue'
+import { onMounted, reactive, ref, watch, defineEmits } from 'vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import { ElMessage } from 'element-plus'
 import { storeToRefs } from 'pinia'
-import { useRouter, useRoute } from 'vue-router'
-import { request } from '/@/utils/service'
 import { useShopInfo } from '/@/stores/shopInfo'
+import { postCampaignsData, getAdMixSelect } from '../api/index'
+import emitter from '/@/utils/emitter'
 
 const shopInfo = useShopInfo()
 const { profile } = storeToRefs(shopInfo)
-console.log('🚀 ~ profile-->>', profile)
 
-const formSize = ref('default')
+// 投放类型
+const targetType = ref('T00030')
+
 const campaignRuleFormRef = ref<FormInstance>()
 interface campaignRuleForm {
   campaignName: string
@@ -106,19 +91,13 @@ interface campaignRuleForm {
   startDate: string
   endDate: string
   budget: string
-  frequency: string
-  brand: string
-  isBid: boolean
 }
 const campaignRuleForm = reactive<campaignRuleForm>({
   campaignName: 'AiTestW01',
   adMix: '',
   startDate: '',
   endDate: '',
-  budget: '',
-  frequency: 'daily',
-  brand: 'zosi',
-  isBid: false,
+  budget: '1',
 })
 const campaignRules = reactive<FormRules<campaignRuleForm>>({
   campaignName: [{ required: true, message: '请输入广告活动', trigger: 'blur' }],
@@ -128,26 +107,6 @@ const campaignRules = reactive<FormRules<campaignRuleForm>>({
     { pattern: /^(?:[1-9]\d{0,5}|1000000)(?:\.\d{1,2})?$/, message: '预算必须是1到1000000之间的数字,小数点后最多两位', trigger: 'blur' },
   ],
 })
-
-const frequencyOptions = [
-  {
-    value: 'daily',
-    label: '每日',
-  },
-  {
-    value: 'lifeCycle',
-    label: '生命周期',
-    disabled: true,
-  },
-]
-
-const brandOptions = [
-  {
-    value: 'zosi',
-    label: 'ZOSI',
-  },
-]
-
 const submitCampaignForm = async (formEl: FormInstance | undefined) => {
   if (!formEl) return
   await formEl.validate((valid, fields) => {
@@ -160,54 +119,98 @@ const submitCampaignForm = async (formEl: FormInstance | undefined) => {
   })
 }
 
+// 广告组合相关
+const adMixOptions = ref([])
+async function buildAdMix() {
+  try {
+    const response = await getAdMixSelect()
+    adMixOptions.value = response.data.map((option) => {
+      return {
+        value: option.portfolioId,
+        label: option.name,
+      }
+    })
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+// 创建广告活动相关功能
 const campaignLoading = ref(false)
+const respCampaignId = ref('')
+const respCampaignName = ref('')
 async function createCampaigns() {
   campaignLoading.value = true
-  try {
-    const requestData = {
-      profile_id: profile.value.profile_id,
-      budget: campaignRuleForm.budget,
-      budgetType: campaignRuleForm.frequency,
-      name: campaignRuleForm.campaignName,
-      bidOptimization: campaignRuleForm.isBid,
-      bidOptimizationStrategy: '',
-      startDate: campaignRuleForm.startDate,
-      endDate: campaignRuleForm.endDate,
-      goal: 'PAGE_VISIT',
-      state: 'PAUSED',
-    }
 
-    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+  // 构建基础请求体
+  const campaignData = {
+    profile_id: profile.value.profile_id,
+    budget: campaignRuleForm.budget,
+    name: campaignRuleForm.campaignName,
+    bidOptimizationStrategy: '',
+    startDate: campaignRuleForm.startDate,
+    endDate: campaignRuleForm.endDate,
+    smartDefault: 'MANUAL',
+    costType: 'CPC',
+    goal: 'PAGE_VISIT',
+    state: 'ENABLED',
+  }
 
-    const response = await request({
-      url: '/api/ad_manage/spcampaigns/create/',
-      method: 'POST',
-      data: filteredRequestData,
-    })
-    console.log('🚀 ~ createCampaigns ~ response-->>', response)
-    campaignLoading.value = false
-    if (response.code === 2000) {
-      // adGroupSave.value = false
-      ElMessage({
-        message: '广告活动创建成功',
-        type: 'success',
-      })
+  try {
+    const response = await postCampaignsData(campaignData)
+    respCampaignId.value = response.data.campaignId
+    respCampaignName.value = response.data.campaignName
+
+    if (response.data.campaignId) {
+      ElMessage({ message: '广告活动创建成功', type: 'success' })
     } else {
       ElMessage.error('广告活动创建失败!')
     }
   } catch (error) {
     console.error('请求失败:', error)
+  } finally {
+    campaignLoading.value = false
   }
 }
+
+// 提供数据给父组件
+const emit = defineEmits(['update-campaign'])
+
+watch([respCampaignId, respCampaignName], () => {
+  if (respCampaignId.value && respCampaignName.value) {
+    emit('update-campaign', {
+      id: respCampaignId.value,
+      name: respCampaignName.value,
+    })
+  }
+})
+
+watch(targetType, () => {
+  emitter.emit('send-targetType', targetType)
+},{immediate: true})
+
+onMounted(() => {
+  buildAdMix()
+})
 </script>
 
 <style scoped>
-::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
-  font-weight: 500;
-  color: #505968;
-}
 .flex-between {
   display: flex;
   justify-content: space-between;
 }
+.left {
+  margin-right: 12px;
+  width: 60%;
+}
+.title {
+  font-size: 14px;
+  line-height: 20px;
+  color: #1d2129;
+}
+.tip {
+  font-size: 14px;
+  line-height: 20px;
+  color: #4e5969;
+}
 </style>

+ 122 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/component/AdGroup.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div class="custom-card-title">
+        <span class="custom-card-icon">|</span>
+        <span class="custom-card-Text">广告组</span>
+      </div>
+      <el-form
+        ref="campaignRuleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-position="top"
+        label-width="120px"
+        size="default"
+        status-icon>
+        <el-form-item label="广告组名称" prop="groupName">
+          <el-input v-model="ruleForm.groupName" style="width: 310px"></el-input>
+        </el-form-item>
+        <el-form-item label="竞价优化" prop="groupName">
+          <div>
+            <el-radio-group v-model="bidOptimization" class="custom-radio-group">
+              <el-radio label="1" size="large" border class="custom-radio-group-item" style="margin: 0;">
+                <div>
+                <div>针对页面访问量</div>
+                 <div style="color: #8c8c8c; margin-top: -5px;"> 我们将优化您的竞价以获得更高的点击率。通过向更有可能点击广告的顾客展示您的广告来提高商品购买意向</div>
+                </div>
+              </el-radio>
+              <el-radio label="2" size="large" border class="custom-radio-group-item" style="margin: 10px 0;">
+                针对转化率
+                <div style="color: #8c8c8c; margin-top: -5px;"> 我们将优化您的竞价以获得更高的转化率。通过向更有可能购买商品的顾客展示您的广告来提高销量</div>
+              </el-radio>
+              <el-radio label="3" size="large" border class="custom-radio-group-item">
+                针对可见展示量进行优化
+                <div style="color: #8c8c8c; margin-top: -5px;"> 我们将优化您的竞价以获得更高的可见展示次数。通过在亚马逊上向尽可能多的顾客展示您的广告来提高商品知名度</div>
+              </el-radio>
+            </el-radio-group>
+          </div>
+        </el-form-item>
+        <PromoteProduct></PromoteProduct>
+        <el-form-item style="margin: 20px 0 -10px 48%">
+          <el-button type="primary" plain @click="submitForm(ruleFormRef)">保存</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, reactive, ref, watch, defineEmits } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import PromoteProduct from './PromoteProduct.vue'
+import { storeToRefs } from 'pinia'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const ruleFormRef = ref<FormInstance>()
+interface ruleForm {
+  groupName: string
+  adMix: string
+  startDate: string
+  endDate: string
+  budget: string
+}
+const ruleForm = reactive<ruleForm>({
+  groupName: 'AiTestW01',
+  adMix: '',
+  startDate: '',
+  endDate: '',
+  budget: '1',
+})
+const rules = reactive<FormRules<ruleForm>>({
+  groupName: [{ required: true, message: '请输入广告组名称', trigger: 'blur' }],
+  startDate: [{ type: 'date', required: true, message: '请选择时间', trigger: 'blur' }],
+  budget: [
+    { required: true, message: '请输入预算', trigger: 'blur' },
+    { pattern: /^(?:[1-9]\d{0,5}|1000000)(?:\.\d{1,2})?$/, message: '预算必须是1到1000000之间的数字,小数点后最多两位', trigger: 'blur' },
+  ],
+})
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+// 竞价优化按钮组
+const bidOptimization = ref('1')
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.custom-card-title {
+  font-weight: 700;
+  padding-bottom: 18px;
+}
+.custom-card-Text {
+  font-size: 18px;
+  padding-left: 5px;
+}
+.custom-card-icon {
+  color: #306cd7;
+  font-size: 26px;
+}
+.custom-radio-group {
+  flex-direction: column;
+  align-items: normal;
+}
+.custom-radio-group-item {
+  height: auto;
+  flex-direction: row;
+  align-items: baseline;
+}
+</style>

+ 269 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/component/CommodityOperate.vue

@@ -0,0 +1,269 @@
+<template>
+  <div v-loading="containerLoading">
+  <el-scrollbar height="450px">
+    <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>
+</div>
+  <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>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, watch, reactive, CSSProperties, defineEmits } from 'vue'
+import { request } from '/@/utils/service'
+import type { FormInstance, FormRules, TabsPaneContext } from 'element-plus'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+
+
+const emit = defineEmits(['add-to-table']);
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+const containerLoading = ref(false)
+const categoryBiddingType = ref('customBid')
+const countLoadig = ref(false)
+const dialogSelectLoading = ref(false)
+const searchClassifyTableData = ref([])
+let productOrientationTableData = ref([])
+const defaultProps = {
+  children: 'ch',
+  label: 'cna',
+}
+const visible = ref(false)
+let dialogTitle = ref('')
+let categoryId = ref('')
+let commodityCount = ref([])
+let currentDialogIndex = ref(0)
+let selectedLabels = ref([]) // 选中的label数组
+
+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',
+})
+
+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' }],
+})
+
+async function validatePrices(rule, value) {
+  if (value.highest !== '' && value.lowest !== '' && value.highest <= value.lowest) {
+    return Promise.reject('最高价格必须大于最低价格')
+  }
+  return Promise.resolve()
+}
+
+async function setProductOrientationData() {
+  containerLoading.value = true
+  try {
+    const resp = await request({
+      url: '/api/ad_manage/targetable/categories/',
+      method: 'GET',
+      params: {
+        profile_id: profile.value.profile_id,
+      },
+    })
+    searchClassifyTableData.value = resp.data
+  } catch (error) {
+    console.error('请求失败:', error)
+  } finally {
+    containerLoading.value = false
+  }
+}
+
+function dialogSelectChange(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)
+}
+
+function resetDialogForm() {
+  dialogForm.prices.lowest = undefined
+  dialogForm.prices.highest = undefined
+  dialogForm.starRating = [0, 5]
+  dialogForm.dialogselectValue = []
+  dialogForm.delivery = 'all'
+  dialogForm.isCount = false
+}
+
+function dialogClose() {
+  currentDialogIndex.value++
+  resetDialogForm()
+  dialogForm.isCount = false
+  commodityCount.value = []
+  countLoadig.value = false
+}
+
+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 isCountChanged() {
+  if (dialogForm.isCount) {
+    const instanceId = currentDialogIndex.value
+    countLoadig.value = true
+    getCount(instanceId)
+  } else {
+    countLoadig.value = false
+    commodityCount.value = []
+  }
+}
+
+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)
+  }
+}
+
+// 细化按钮功能
+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 orientate(node, data) {
+  console.log('🚀 ~ orientate ~ data-->>', data)
+  const exists = productOrientationTableData.value.some((item) => item.cid === data.cid)
+  emit('add-to-table', data);
+}
+
+function dialogFormSubmit() {}
+
+onMounted(() => {
+  setProductOrientationData()
+})
+
+</script>
+
+<style scoped>
+.custom-tree-node {
+  /* el-tree自定义样式 */
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+</style>

+ 36 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/component/ContentTarget.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div class="custom-card-title">
+        <span class="custom-card-icon">|</span>
+        <span class="custom-card-Text">内容相关投放</span>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted } from 'vue'
+
+onMounted(() => {
+
+})
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.custom-card-title {
+  font-weight: 700;
+  padding-bottom: 18px;
+}
+.custom-card-Text {
+  font-size: 18px;
+  padding-left: 5px;
+}
+.custom-card-icon {
+  color: #306cd7;
+  font-size: 26px;
+}
+</style>

+ 341 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/component/CustomTarget.vue

@@ -0,0 +1,341 @@
+<template>
+  <div class="customize-container">
+    <el-card body-style="padding: 20px 80px 0 80px;">
+      <div class="custom-card-title">
+        <span class="custom-card-icon">|</span>
+        <span class="custom-card-Text">自定义定向</span>
+      </div>
+      <div class="main-container">
+        <div class="left-container">
+          <el-tabs v-model="topTabName" class="demo-tabs" @tab-click="handleClick">
+            <div class="tab-container-fixed-top">
+              <span class="tab-top-label">添加定向时的竞价设置: </span>
+              <el-select v-model="bidType">
+                <el-option v-for="item in bidTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+              <el-input v-model="bid" :disabled="bidType == '1'" style="width: 200px">
+                <template #prepend>$</template>
+              </el-input>
+            </div>
+            <el-tab-pane label="亚马逊受众" name="audience">
+              <div class="el-row align-center-bottom" style="flex-wrap: nowrap">
+                <el-select v-model="audienceType" style="width: 140px; margin-right: 10px">
+                  <el-option v-for="item in audienceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+                </el-select>
+                <el-input v-model="keywordInput" placeholder="请输入关键词进行过滤"></el-input>
+              </div>
+            </el-tab-pane>
+            <el-tab-pane label="浏览再营销" name="views">
+              <div class="tab-title">触达浏览过您推广的商品或其他类似商品、商品品类、品牌以及其他商品功能的顾客</div>
+              <el-tabs v-model="viewsTabName" type="border-card">
+                <div class="el-row align-center-bottom">
+                  <span class="select-label">回溯期: </span>
+                  <el-select v-model="viewsLookBack">
+                    <el-option v-for="item in viewsLookBackOptions" :key="item.value" :label="item.label" :value="item.value" />
+                  </el-select>
+                </div>
+                <el-tab-pane label="建议" name="advice">
+                </el-tab-pane>
+                <el-tab-pane label="搜索" name="search">
+                  <CommodityOperate @add-to-table="handleAddToTable"></CommodityOperate>
+                </el-tab-pane>
+              </el-tabs>
+            </el-tab-pane>
+            <el-tab-pane label="购买再营销" name="purchases">
+              <div class="tab-title">向购买过广告商品或其他相关商品、商品类别、品牌及其他商品功能的购买者传递信息</div>
+              <el-tabs v-model="purchasesTabName" type="border-card">
+                <div class="el-row align-center-bottom">
+                  <span class="select-label">回溯期: </span>
+                  <el-select v-model="purchasesLookBack">
+                    <el-option v-for="item in purchasesLookBackOptions" :key="item.value" :label="item.label" :value="item.value" />
+                  </el-select>
+                </div>
+                <el-tab-pane label="建议" name="advice"> </el-tab-pane>
+                <el-tab-pane label="搜索" name="search"> </el-tab-pane>
+              </el-tabs>
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+        <!-- 右侧内容区 -->
+        <div class="right-container">
+          <div class="right-container-top">
+            <div style="padding-left: 15px; font-size: 15px">
+              <span style="color: #8e9095">已添加: </span> <span style="font-weight: 500">{{ addedTableData.length }}</span>
+            </div>
+            <el-button link type="danger" @click="handleDeleteAll" style="margin-right: 15px">删除所有</el-button>
+          </div>
+          <el-table :data="addedTableData" :header-cell-style="changeTableHeader" height="649" style="width: 100%">
+            <el-table-column prop="date" label="商品">
+              <template #default="{ row }">
+                <div>浏览再营销</div>
+                <div>
+                  分类: <span style="color: black">{{ row.cna ? row.cna : '--' }}</span>
+                </div>
+                <div>
+                  回溯期: <span style="color: black">{{ row.lookBack ? row.lookBack : '--' }}</span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="name" label="竞价" width="160">
+              <template #default="{ row }">
+                <el-input v-model.number="bid">
+                  <template #prepend>$</template>
+                </el-input>
+              </template>
+            </el-table-column>
+            <el-table-column prop="address" label="当前建议竞价" width="160">
+              <template #default="{ row }">
+                <div>$</div>
+                <div>$</div>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="60">
+              <template #default="{ row }">
+                <el-button link type="danger" size="small" @click="handleButtonClick(row)">删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, watch, reactive, CSSProperties } from 'vue'
+import { request } from '/@/utils/service'
+import CommodityOperate from './CommodityOperate.vue'
+import type { FormInstance, FormRules, TabsPaneContext } from 'element-plus'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { storeToRefs } from 'pinia'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+
+// tab栏
+const topTabName = ref('audience')
+
+function handleClick(tab: TabsPaneContext, event: Event) {
+  // console.log(tab, event)
+}
+
+// tab栏顶部固定部分功能
+const bidType = ref('2')
+const bidTypeOptions = [
+  {
+    value: '1',
+    label: '建议竞价',
+  },
+  {
+    value: '2',
+    label: '自定义竞价',
+  },
+]
+const bid = ref('0.75')
+
+watch(
+  bidType,
+  () => {
+    if (bidType.value == '1') {
+      bid.value = ''
+    } else {
+      bid.value = '0.75'
+    }
+  },
+  { immediate: true }
+)
+
+// 亚马逊受众tab的 下拉框和输入框
+const audienceType = ref('1')
+const audienceTypeOptions = [
+  {
+    value: '1',
+    label: '所有受众',
+  },
+  {
+    value: '2',
+    label: '生活方式',
+  },
+  {
+    value: '3',
+    label: '兴趣',
+  },
+  {
+    value: '4',
+    label: '生活事件',
+  },
+  {
+    value: '5',
+    label: '场内客群',
+  },
+]
+
+const keywordInput = ref('') // 关键词过滤输入框
+
+// 浏览再营销下的tab栏
+const viewsTabName = ref('advice')
+const viewsLookBack = ref('7')
+const viewsLookBackOptions = [
+  {
+    value: '7',
+    label: '7天',
+  },
+  {
+    value: '14',
+    label: '14天',
+  },
+  {
+    value: '30',
+    label: '30天',
+  },
+  {
+    value: '60',
+    label: '60天',
+  },
+  {
+    value: '90',
+    label: '90天',
+  },
+]
+
+// 购买再营销下的tab栏
+const purchasesTabName = ref('advice')
+const purchasesLookBack = ref('7')
+const purchasesLookBackOptions = [
+  {
+    value: '7',
+    label: '7天',
+  },
+  {
+    value: '14',
+    label: '14天',
+  },
+  {
+    value: '30',
+    label: '30天',
+  },
+  {
+    value: '60',
+    label: '60天',
+  },
+  {
+    value: '90',
+    label: '90天',
+  },
+]
+
+// 已添加的表格数据
+const addedTableData = ref([])
+
+function handleAddToTable(data) {
+  // 检查该数据是否已经存在于 addedTableData 中
+  const exists = addedTableData.value.some((item) => item.cid === data.cid)
+  if (!exists) {
+    addedTableData.value.push(data)
+  }
+}
+
+// 删除所有Table数据
+function handleDeleteAll() {
+  addedTableData.value = []
+}
+// 操作列按钮功能
+function handleButtonClick(row) {
+  addedTableData.value = addedTableData.value.filter(item => item.cid !== row.cid)
+}
+
+function changeTableHeader(row) {
+  if (row) {
+    return {
+      background: '#f5f7fa',
+    }
+  }
+}
+</script>
+
+<style scoped>
+.customize-container {
+  margin-top: 10px;
+}
+.custom-card-title {
+  font-weight: 700;
+  padding-bottom: 18px;
+}
+.custom-card-Text {
+  font-size: 18px;
+  padding-left: 5px;
+}
+.custom-card-icon {
+  color: #306cd7;
+  font-size: 26px;
+}
+.main-container {
+  display: flex;
+  border: 1px solid #e5e7eb;
+  height: 700px;
+  margin-bottom: 20px;
+}
+.left-container {
+  width: 50%;
+  height: 699px;
+  border-right: 1px solid #e5e7eb;
+}
+.right-container {
+  width: 50%;
+  height: 699px;
+  border-bottom: 0;
+}
+.right-container-top {
+  height: 40px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.demo-tabs {
+  padding: 0 10px;
+}
+.demo-tabs > .el-tabs__content {
+  padding: 32px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+/* 选中时的字体颜色 */
+::v-deep(.demo-tabs .el-tabs__item.is-top.is-active) {
+  color: #3569d6;
+}
+/* 选中时的下划线颜色 */
+::v-deep(.demo-tabs .el-tabs__active-bar.is-top) {
+  background-color: #3569d6;
+}
+.tab-container-fixed-top {
+  display: flex;
+  align-items: center;
+  margin-bottom: 4px;
+  gap: 8px;
+  font-size: 13px;
+  margin: 10px 0;
+}
+.tab-top-label {
+  color: #4e5969;
+  font-weight: 700;
+  margin-right: 10px;
+}
+.align-center-bottom {
+  align-items: center;
+  margin-bottom: 12px;
+}
+.tab-title {
+  line-height: 20px;
+  padding-bottom: 10px;
+  color: #8e9095;
+}
+.select-label {
+  padding-right: 5px;
+  color: #606266;
+}
+/* 修改隐藏table底部border */
+::v-deep(.el-table__inner-wrapper::before) {
+  background-color: #ffffff;
+}
+</style>

+ 439 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/component/PromoteProduct.vue

@@ -0,0 +1,439 @@
+<template>
+  <div style="padding: 1px 0 2px 0;">
+    <el-divider content-position="left">
+      <span style="font-size: 18px; font-weight: 700">推广商品</span>
+    </el-divider>
+  </div>
+  <div prop="commodity" style="width: 100%" v-loading="productLoading">
+    <div style="width: 100%; height: 620px; display: flex; border: 1px solid #e5e7ec; border-radius: 6px">
+      <div style="width: 50%; border-right: 1px solid #e5e7ec">
+        <el-tabs v-model="productTabs" class="demo-tabs">
+          <el-tab-pane label="搜索" name="first">
+            <div style="margin-bottom: 10px">
+              <el-input v-model="searchInp" placeholder="Please input" class="input-with-select" @change="inpChange" clearable>
+                <template #prepend>
+                  <el-select v-model="leftSelect" style="width: 100px" @change="selChange">
+                    <el-option label="名称" value="name" />
+                    <el-option label="ASIN" value="asin" />
+                    <el-option label="SKU" value="sku" />
+                  </el-select>
+                </template>
+                <template #append>
+                  <el-select v-model="rightSelect" style="width: 100px">
+                    <el-option label="最新优先" value="latest" />
+                    <el-option label="最早优先" value="earliest" />
+                    <el-option label="优选广告" value="optimal" />
+                  </el-select>
+                </template>
+              </el-input>
+            </div>
+            <el-table
+              height="490"
+              style="width: 100%"
+              v-loading="loading"
+              :data="productTableData"
+              :header-cell-style="headerCellStyle"
+              @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="50" />
+              <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>
+                      <div class="data-color">
+                        <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                        <span style="margin: 0 5px; color: #cacdd4">|</span>
+                        <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                      </div>
+                      <span>
+                        ASIN: <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                      </span>
+                      <span>
+                        SKU: <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                      </span>
+                    </div>
+                  </div>
+                </template>
+              </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>
+                </template>
+                <template #default="scope">
+                  <el-button type="primary" size="small" @click="addSingleGoods(scope)" text>添加</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-pagination
+              @current-change="handleCurrentChange"
+              @size-change="handleSizeChange"
+              :current-page="currentPage"
+              :page-size="pageSize"
+              :total="totalItems"
+              layout="prev, pager, next" />
+          </el-tab-pane>
+          <el-tab-pane label="输入" name="second">
+            <el-input
+              style="padding: 10px"
+              v-model="productTextarea"
+              :rows="20"
+              type="textarea"
+              placeholder="请输入ASIN,多个ASIN使用逗号、空格或换行符分隔。(未完成)"
+              maxlength="11000" />
+            <div style="display: flex; flex-direction: row-reverse; margin-top: 10px">
+              <el-button v-for="button in buttons" :key="button.text" :type="button.type" link @click="addGods">{{ button.text }}</el-button>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div style="width: 50%">
+        <el-card class="box-card" shadow="never" style="border: 0">
+          <template #header>
+            <div class="card-header">
+              <span style="font-weight: 550; font-size: 15px; color: #1f2128">已添加: {{ addedTableData.length }}</span>
+              <el-button class="button" type="danger" text bg @click="delAllGoods">全部删除</el-button>
+            </div>
+          </template>
+          <div class="card-body"></div>
+        </el-card>
+        <div style="padding: 0 10px 0 10px; margin-top: -12px">
+          <el-table
+            :data="addedTableData"
+            height="475"
+            style="width: 100%"
+            :header-cell-style="headerCellStyle"
+            @selection-change="handleAddedGoodsChange">
+            <el-table-column type="selection" width="50" />
+            <el-table-column prop="asin" label="ASIN">
+              <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>
+                    <div class="data-color">
+                      <span style="font-weight: 500; color: rgb(30, 33, 41)">${{ scope.row.price ? scope.row.price : '--' }}</span>
+                      <span style="margin: 0 5px; color: #cacdd4">|</span>
+                      <span style="color: #6d7784">{{ scope.row.quantity }}</span>
+                    </div>
+                    <span
+                      >ASIN:
+                      <span class="data-color" style="margin-right: 8px">{{ scope.row.asin ? scope.row.asin : '--' }}</span>
+                    </span>
+                    <span
+                      >SKU:
+                      <span class="data-color">{{ scope.row.sku ? scope.row.sku : '--' }}</span>
+                    </span>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="name" label="Name" width="120" align="right">
+              <template #header>
+                <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>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div style="display: flex; justify-content: space-around; padding-top: 5px">
+          <el-button type="primary" plain :disabled="addedTableData.length == 0" @click="submitProductForm">保存</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { TabsPaneContext } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { storeToRefs } from 'pinia'
+import type { Ref } from 'vue'
+import { inject, onMounted, ref, watch } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { request } from '/@/utils/service'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const productTextarea = ref('')
+const productLoading = ref(false)
+let addedAdsTableItems = ref([])
+const currentPage = ref() // 当前页
+const pageSize = ref(20) // 每页显示条目数
+const totalItems = ref() // 数据总量
+const productTableData = ref([]) // 左侧表格数据
+const loading = ref(false)
+let addedTableData = ref([])
+let selections = []
+let addedSels = []
+const searchInp = ref('')
+const leftSelect = ref('name')
+const buttons = [{ type: 'primary', text: '添加' }] as const
+const productTabs = ref('first')
+const rightSelect = ref('latest')
+const respCampaignId = inject<Ref>('respCampaignId')
+const respCampaignName = inject<Ref>('respCampaignName')
+const respAdGroupId = inject<Ref>('respAdGroupId')
+
+function setTableData(asin = '', sku = '') {
+  return request({
+    url: '/api/sellers/listings/our/',
+    method: 'GET',
+    params: {
+      page: currentPage.value,
+      limit: pageSize.value,
+      profile_id: profile.value.profile_id,
+      asin,
+      sku,
+    },
+  })
+    .then((resp) => {
+      productTableData.value = resp.data
+      totalItems.value = resp.total
+      currentPage.value = resp.page
+      loading.value = false
+    })
+    .catch((error) => {
+      console.error('Error fetching data:', error)
+      loading.value = false
+    })
+}
+
+function addSingleGoods(scope) {
+  // console.log('scope', scope.row)
+  const isAlreadyAdded = addedTableData.value.some((item) => item.sku === scope.row.sku)
+  if (!isAlreadyAdded) {
+    addedTableData.value.push(scope.row)
+  } else {
+    console.log('Item is already added.')
+  }
+}
+
+function addGods() {
+  const inputData = productTextarea.value
+  const asins = inputData.split(/[\n,]+/)
+
+  asins.forEach((asin) => {
+    if (asin.trim()) {
+      setTableData(asin.trim())
+        .then((response) => {
+          console.log(`Data for ASIN ${asin}:`, response) // 更新这里来正确地访问数据
+        })
+        .catch((error) => {
+          console.error(`Error fetching data for ASIN ${asin}:`, error)
+        })
+    }
+  })
+}
+
+function delSingleGoods(scope) {
+  const index = addedTableData.value.findIndex((item) => item.sku === scope.row.sku)
+  if (index !== -1) {
+    addedTableData.value.splice(index, 1)
+    console.log('Item removed successfully.')
+  } else {
+    console.log('Item not found.')
+  }
+}
+
+function delAllGoods() {
+  addedTableData.value = []
+  // addedTableData.value.splice(0, addedTableData.value.length)
+}
+
+// 删除第二个table中已经选中的项
+function delSelectedGoods() {
+  addedTableData.value = addedTableData.value.filter((item) => !addedSels.includes(item))
+  addedSels = []
+}
+
+function inpChange(e) {
+  const value = e
+  if (leftSelect.value === 'asin') {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku') {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+
+function selChange(e) {
+  console.log('e', e)
+  const value = e
+  if (leftSelect.value === 'asin' && searchInp.value) {
+    loading.value = true
+    setTableData(value)
+  } else if (leftSelect.value === 'sku' && searchInp.value) {
+    loading.value = true
+    setTableData('', value)
+  }
+}
+// 点击表格选项触发事件
+function handleSelectionChange(selection) {
+  selections = selection
+}
+// 获取addedTable中已选中的项
+function handleAddedGoodsChange(selection) {
+  addedSels = selection
+}
+// 添加已选中的项
+function handleGoodsAdd() {
+  // 过滤掉已经存在于addedData.value中的项
+  const newSelections = selections.filter(
+    (sel) => !addedTableData.value.some((added) => added.sku === sel.sku) // 使用sku作为唯一标识
+  )
+  // 如果有新的不重复项,加入到addedData.value中
+  if (newSelections.length > 0) {
+    addedTableData.value.push(...newSelections)
+  }
+}
+// 点击Tab
+const handleGoodsTabs = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event)
+}
+
+function isItemInList(item, list) {
+  return list.some((listItem) => listItem.sku === item.sku && listItem.asin === item.asin)
+}
+// 监听商品右侧表格已添加的数据并转化数据格式
+watch(
+  addedTableData,
+  (newValue, oldValue) => {
+    newValue.forEach((item) => {
+      if (!isItemInList(item, addedAdsTableItems.value)) {
+        addedAdsTableItems.value.push({ sku: item.sku, asin: item.asin })
+      }
+    })
+  },
+  { deep: true }
+)
+
+async function createAds() {
+  try {
+    const requestData = {
+      profile_id: profile.value.profile_id,
+      campaignId: respCampaignId.value,
+      adGroupId: respAdGroupId.value,
+      asinsku: addedAdsTableItems.value,
+      state: 'PAUSED',
+    }
+    const filteredRequestData = Object.fromEntries(Object.entries(requestData).filter(([_, v]) => v != null))
+    const resp = await request({
+      url: '/api/ad_manage/spads/create/',
+      method: 'POST',
+      data: filteredRequestData,
+    })
+    console.log('🚀 ~ createAds ~ resp-->>', resp)
+    productLoading.value = false
+    if (resp.data.success.length > 0) {
+      addedTableData.value = []
+      ElMessage({
+        message: '商品创建成功',
+        type: 'success',
+      })
+    } else {
+      ElMessage.error('商品创建失败!')
+    }
+  } catch (error) {
+    console.error('请求失败:', error)
+  }
+}
+
+function submitProductForm() {
+  productLoading.value = true
+  createAds()
+}
+
+// 处理分页器当前页变化
+function handleCurrentChange(newPage) {
+  currentPage.value = newPage
+  loading.value = true
+  setTableData()
+}
+// 处理分页器每页显示条目数变化
+function handleSizeChange(newSize) {
+  pageSize.value = newSize
+  currentPage.value = 1 // 重置到第一页
+}
+
+const headerCellStyle = (args) => {
+  if (args.rowIndex === 0) {
+    return {
+      backgroundColor: 'rgba(245, 245, 245, 0.9)',
+    }
+  }
+}
+onMounted(() => {
+  setTableData()
+})
+</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;
+}
+.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;
+}
+/* 商品定向Tab栏 */
+::v-deep(.goods-orientation-tabs #tab-1) {
+  border-right: 0;
+}
+</style>

+ 35 - 1
src/views/adManage/sd/campaigns/CreateCampaigns/index.vue

@@ -1,9 +1,43 @@
 <template>
-  <p>新建广告页面</p>
+  <div class="page-container">
+    <AdCampaign></AdCampaign>
+    <AdGroup></AdGroup>
+    <component :is="currentComponent"></component>
+  </div>
 </template>
 
 <script lang="ts" setup>
+import { onMounted, ref, computed } from 'vue'
+import AdCampaign from './component/AdCampaign.vue'
+import AdGroup from './component/AdGroup.vue'
+import CustomTarget from './component/CustomTarget.vue'
+import ContentTarget from './component/ContentTarget.vue'
+import emitter from '/@/utils/emitter'
+
+// 获取targetType的值渲染自定义定向
+const targetType = ref()
+emitter.on('send-targetType', (value: any) => {
+  targetType.value = value.value
+})
+
+// 动态组件
+const currentComponent = computed(() => {
+  switch (targetType.value) {
+    case 'T00030':
+      return CustomTarget
+    case 'T00020':
+      return ContentTarget
+  }
+})
 </script>
 
 <style scoped>
+.page-container {
+  padding: 12px;
+  background-color: #fafafa;
+}
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+  color: #505968;
+}
 </style>

+ 14 - 6
src/views/adManage/sd/campaigns/crud.tsx

@@ -4,6 +4,8 @@ import {inject} from 'vue'
 import {SdBaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
 import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
 import XEUtils from 'xe-utils'
+import { useRouter } from 'vue-router'
+
 
 export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
   const pageRequest = async (query: UserPageQuery) => {
@@ -22,6 +24,8 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
     return await api.AddObj(form)
   }
 
+  const router = useRouter()
+
   //权限判定
   const hasPermissions = inject('$hasPermissions')
 
@@ -48,19 +52,23 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         show: true,
         buttons: {
           add: {
-            show: false
+            show: false,
           },
           create: {
             text: '新建广告活动',
-            // type: 'primary',
+            type: 'primary',
+            show: true,
             color: "#626aef",
             plain: true,
-            show: true,
             click() {
-
-            }
+              router.push({
+                name: 'SdCreateCampaigns',
+                query: { campaignId: 123, tagsViewName: '新建广告活动' },
+              })
+            
+            },
           },
-        }
+        },
       },
       search: {
         show: false

+ 2 - 4
src/views/adManage/sp/campaigns/CreateCampaigns/index.vue

@@ -597,7 +597,8 @@
                                 @change="dialogSelectChange"
                                 multiple
                                 placeholder="请选择"
-                                v-loading="dialogSelectLoading">
+                                :loading="dialogSelectLoading"
+                                >
                                 <el-option v-for="item in dialogForm.dialogOptions" :key="item.value" :label="item.label" :value="item.value" />
                               </el-select>
                             </el-form-item>
@@ -892,7 +893,6 @@ import { usePublicData } from '/@/stores/publicData'
 import { storeToRefs } from 'pinia'
 import { useRouter } from 'vue-router'
 import { request } from '/@/utils/service'
-import { number } from 'echarts'
 
 const negativeTableData = ref([])
 const addedNegetiveTableData = ref([])
@@ -1873,8 +1873,6 @@ function orientate(node, data) {
   }
 }
 
-
-
 let productTargetBidList = ref([])
 async function productTagetSave() {
   console.log('tableData', productOrientationTableData.value)