Browse Source

✨ feat: 新增SD新建广告活动页面

WanGxC 1 year ago
parent
commit
82971f1f9b

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

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

@@ -93,4 +93,23 @@ export function videoDetailCreate(obj) {
       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,
+  })
 }

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

@@ -131,6 +131,7 @@ 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)

+ 31 - 9
src/views/adManage/sb/campaigns/CreateCampaigns/component/AdFormat.vue

@@ -57,6 +57,7 @@
                       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" />
@@ -103,7 +104,7 @@
               clearable
               @blur="validateField2('focusShop')"
               style="padding-top: 10px; margin-top: -15px; width: 500px">
-              <el-option v-for="item in focusShopOptions" :key="item.brandEntityId" :label="item.brandRegistryName" :value="item.brandId" />
+              <el-option v-for="item in focusShopOptions" :key="item.brandId" :label="item.brandRegistryName" :value="item.brandEntityId" />
             </el-select>
           </el-form-item>
         </el-form>
@@ -129,14 +130,15 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, defineEmits, watch, onMounted } from 'vue'
+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'
-import { da } from 'element-plus/es/locale'
+
 
 const shopInfo = useShopInfo()
 const { profile } = storeToRefs(shopInfo)
@@ -203,12 +205,14 @@ async function getPageOptions() {
   }
 }
 
-onMounted(() => {
-  getShopOptions()
-  // getPageOptions()
-})
-
-const emit = defineEmits(['update:adFormatRadio', 'update:arrivalsRadio', 'update:flagshipStoreShop', 'update:pageOptions', 'update:addedTableData'])
+const emit = defineEmits([
+  'update:adFormatRadio',
+  'update:arrivalsRadio',
+  'update:flagshipStoreShop',
+  'update:pageOptions',
+  'update:addedTableData',
+  'update:focusShopSelect',
+])
 
 function handleUpdateAddedData(data) {
   emit('update:addedTableData', data)
@@ -250,6 +254,24 @@ watch(
     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>

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

@@ -0,0 +1,678 @@
+<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" 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="commodity" v-loading="commodityLoading" style="padding-right: 10px">
+                  <template #title>编辑品牌旗舰店页面</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" 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 } 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 } from '../api/index'
+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!')
+    } 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 handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+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()
+})
+</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>

+ 90 - 26
src/views/adManage/sb/campaigns/CreateCampaigns/component/ProductSetCreativity1.vue

@@ -27,7 +27,15 @@
                     <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
                   </el-form-item>
 
-                  <el-upload action="#" list-type="picture-card" :auto-upload="false" v-model:file-list="fileList" :limit="1">
+                  <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>
@@ -74,13 +82,7 @@
                 </el-collapse-item>
                 <el-collapse-item name="2" style="padding-right: 10px">
                   <template #title>自定义图片(可选)</template>
-                  <el-upload
-                    action="#"
-                    list-type="picture-card"
-                    :auto-upload="false"
-                    v-model:file-list="fileList"
-                    :limit="1"
-                    style="padding-right: 10px">
+                  <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>
@@ -151,7 +153,9 @@
             </el-scrollbar>
           </div>
           <div style="width: 50%; padding: 0 10px; position: relative">
-            <el-button type="primary" plain @click="clickSave" style="position: absolute; top: 92%; left: 46%">保存</el-button>
+            <el-button type="primary" plain @click="clickSave" :disabled="fileList.length == 0" style="position: absolute; top: 92%; left: 46%"
+              >保存</el-button
+            >
           </div>
         </div>
       </el-form>
@@ -227,10 +231,13 @@
       </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">
+      <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="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>
@@ -255,14 +262,20 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, inject, Ref, watch, computed } from 'vue'
+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 } from '../api/index'
+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)
@@ -299,12 +312,25 @@ const submitForm = async (formEl: FormInstance | undefined) => {
 
 const activeNames = ref(['1'])
 const handleChange = (val: string[]) => {
-  console.log(val)
+  // 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) => {
@@ -323,27 +349,51 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
   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([])
-
+// 图片上传功能
+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)
@@ -586,6 +636,20 @@ const flattenedCommodityCard = computed(() => {
 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>

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

@@ -153,13 +153,26 @@
 </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 type { Ref } from 'vue'
-import { inject, onMounted, ref, watch, provide } from 'vue'
 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)

+ 626 - 88
src/views/adManage/sb/campaigns/CreateCampaigns/component/VideoCreativity1.vue

@@ -17,35 +17,43 @@
         <el-form-item label="广告名称" prop="name">
           <el-input v-model="ruleForm.name" style="width: 50%" />
         </el-form-item>
-        <el-form-item style="border: 1px solid #dddfe6; padding: 0 0 0 5px">
+        <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">
-                <el-collapse-item name="1">
+              <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" style="padding: 0 10px 5px 0"></el-input>
+                    <el-input v-model="ruleForm.brandName" placeholder="请输入品牌名称" style="padding: 0 0 5px 0"></el-input>
                   </el-form-item>
-                  <el-upload
-                    class="upload-demo"
-                    drag
-                    action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
-                    multiple
-                    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>
+
+                  <el-upload action="#" list-type="picture-card" :auto-upload="false" v-model:file-list="fileList" :limit="1">
+                    <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">从素材库中选择</el-button>
+                          <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;">
+                          <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"
@@ -59,70 +67,99 @@
                       </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">
-                  <template #title> <span style="color: #e47470; margin-right: 4px">*</span>视频</template>
-                  <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" text>查看视频批准提示</el-button>
+                <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="color: #666666; margin-bottom: 10px">
+                  <div style="margin: 10px 0; font-size: 12px; font-weight: 400; color: #666; line-height: 18px">
                     保持视频简短并紧扣主题。视频会自动播放,因此请确保前 2
                     秒极具吸引力,并且不依靠声音来传递信息。如果您在视频中使用了文字,请确保文字清晰易辨。字幕或音频必须与将展示您广告的区域相匹配。
                   </div>
-                  <div class="upload-button-group">
-                    <el-upload
-                      v-model:file-list="fileList"
-                      class="upload-demo"
-                      action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
-                      multiple
-                      :on-preview="handlePreview"
-                      :on-remove="handleRemove"
-                      :before-remove="beforeRemove"
-                      :limit="3"
-                      :on-exceed="handleExceed">
-                      <el-button type="primary">上传文件</el-button>
-
-                      <template #tip>
-                        <div class="el-upload__tip">jpg/png files with a size less than 500KB.</div>
-                      </template>
-                    </el-upload>
-                    <el-button type="primary" :icon="Picture" style="margin-left: -120px" @click="handleSelect">从素材库中选择</el-button>
-                  </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" text>了解更多</el-button>
-                  </div>
+                  <el-upload
+                    v-model:file-list="fileList"
+                    class="upload-demo"
+                    action="#"
+                    accept=".mp4, .mov"
+                    multiple
+                    :on-preview="handlePreview"
+                    :on-remove="handleRemove"
+                    :before-remove="beforeRemove"
+                    :limit="1"
+                    :on-exceed="handleExceed">
+                    <el-button type="primary">上传文件</el-button>
+                    <template #tip>
+                      <el-button type="primary" 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>
 
-                  <div>
-                    <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>
-                    <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>
+                  <!-- 预览弹窗 -->
+                  <el-dialog v-model="dialogVisible">
+                    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+                  </el-dialog>
                 </el-collapse-item>
 
-                <el-collapse-item name="3">
+                <el-collapse-item name="commodity" v-loading="commodityLoading" style="padding-right: 10px">
                   <template #title> <span style="color: #e47470; margin-right: 4px">*</span>商品</template>
-                  <el-button type="primary" :icon="Plus" :disabled="true">添加商品</el-button>
+                  <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">
+                <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>
@@ -131,25 +168,133 @@
               </el-collapse>
             </el-scrollbar>
           </div>
-        </el-form-item>
+          <div style="width: 50%; padding: 0 10px; position: relative">
+            <el-button type="primary" plain @click="clickSave" 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 } from 'vue'
+import { reactive, ref, inject, Ref, watch, computed } from 'vue'
 import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus, Picture } from '@element-plus/icons-vue'
+import { Plus, Picture, Search, Delete, Download, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import { getAssets, getLifeStyleAssets, getPageAsins, getCommodityCard } from '../api/index'
+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 ruleFormRef = ref<FormInstance>()
 const ruleForm = reactive<RuleForm>({
   name: '视频 广告 - 1/15/2024 17:51:10.236',
   brandName: '',
@@ -176,21 +321,27 @@ const submitForm = async (formEl: FormInstance | undefined) => {
 const activeNames = ref(['1'])
 const handleChange = (val: string[]) => {
   console.log(val)
+  if (val.includes('commodity')) {
+    getCommodityCardData()
+  }
 }
 
-const fileList = ref<UploadUserFile[]>([
-  {
-    name: 'element-plus-logo.svg',
-    url: 'https://element-plus.org/images/element-plus-logo.svg',
-  },
-  {
-    name: 'element-plus-logo2.svg',
-    url: 'https://element-plus.org/images/element-plus-logo.svg',
-  },
-])
+const imageUrl = ref('')
+
+const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
+  imageUrl.value = URL.createObjectURL(uploadFile.raw!)
+  console.log('success!')
+}
 
-const handleRemove: UploadProps['onRemove'] = (file, uploadFiles) => {
-  console.log(file, uploadFiles)
+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) => {
@@ -208,9 +359,269 @@ const beforeRemove: UploadProps['beforeRemove'] = (uploadFile, uploadFiles) => {
   )
 }
 
-function handleSelect() {
-  // console.log()
+// 图片上传相关
+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 handleRemove(file: UploadFile) {
+  fileList.value = []
+}
+
+function handlePictureCardPreview(file: UploadFile) {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+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()
+})
 </script>
 
 <style scoped>
@@ -225,4 +636,131 @@ function handleSelect() {
   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>

+ 15 - 4
src/views/adManage/sb/campaigns/CreateCampaigns/index.vue

@@ -7,11 +7,14 @@
       @update:arrivalsRadio="handleArrivalsRadioChange"
       @update:flagshipStoreShop="handleFlagshipStoreShopChange"
       @update:pageOptions="handlePageOptionsChange"
-      @update:addedTableData="handleUpdateAddedData"></AdFormat>
-    <VideoCreativity1 v-if="adFormatRadioValue === 'video' && arrivalsRadioValue === 'flagshipStore' && flagshipStoreShopValue === 'ZOSI'">
-    </VideoCreativity1>
+      @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>
@@ -19,7 +22,7 @@
 </template>
 
 <script lang="ts" setup>
-import { provide, ref } from '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'
@@ -28,6 +31,8 @@ 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('')
@@ -37,12 +42,14 @@ const arrivalsRadioValue = ref('')
 const flagshipStoreShopValue = ref('')
 const pageOptionsValue = ref('')
 const addedTableDataForVc2 = ref('')
+const focusShop = ref('')
 
 provide('respCampaignId', respCampaignId)
 provide('respCampaignName', respCampaignName)
 provide('respAdGroupId', respAdGroupId)
 provide('pageOptionsValue', pageOptionsValue)
 provide('addedTableDataForVc2', addedTableDataForVc2)
+provide('focusShop', focusShop)
 
 const handleCampaignUpdate = (data) => {
   respCampaignId.value = data.id
@@ -67,6 +74,10 @@ const handlePageOptionsChange = (newValue) => {
 function handleUpdateAddedData(data) {
   addedTableDataForVc2.value = data
 }
+
+function handleFocusShopSelectChange(newValue) {
+  focusShop.value = newValue
+}
 </script>
 
 <style scoped>

+ 13 - 7
src/views/adManage/sd/campaigns/CreateCampaigns/component/AdCampaign.vue

@@ -55,12 +55,12 @@
             </el-form-item>
           </div>
         </div>
-        <div>
-          <el-radio-group v-model="radio">
-            <el-radio label="1">受众</el-radio>
-            <el-radio label="2">内容相关投放</el-radio>
+        <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>
-        </div>
+        </el-form-item>
         <el-form-item style="margin: 20px 0 -10px 48%">
           <el-button type="primary" plain @click="submitCampaignForm(campaignRuleFormRef)">保存</el-button>
         </el-form-item>
@@ -76,10 +76,14 @@ import { ElMessage } from 'element-plus'
 import { storeToRefs } from 'pinia'
 import { useShopInfo } from '/@/stores/shopInfo'
 import { postCampaignsData, getAdMixSelect } from '../api/index'
+import emitter from '/@/utils/emitter'
 
 const shopInfo = useShopInfo()
 const { profile } = storeToRefs(shopInfo)
 
+// 投放类型
+const targetType = ref('T00030')
+
 const campaignRuleFormRef = ref<FormInstance>()
 interface campaignRuleForm {
   campaignName: string
@@ -115,8 +119,6 @@ const submitCampaignForm = async (formEl: FormInstance | undefined) => {
   })
 }
 
-const radio = ref('1')
-
 // 广告组合相关
 const adMixOptions = ref([])
 async function buildAdMix() {
@@ -183,6 +185,10 @@ watch([respCampaignId, respCampaignName], () => {
   }
 })
 
+watch(targetType, () => {
+  emitter.emit('send-targetType', targetType)
+},{immediate: true})
+
 onMounted(() => {
   buildAdMix()
 })

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

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

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

@@ -0,0 +1,121 @@
+<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="activeName" 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">亚马逊受众</el-tab-pane>
+            <el-tab-pane label="浏览再营销" name="browse">浏览再营销</el-tab-pane>
+            <el-tab-pane label="购买再营销" name="buy">购买再营销</el-tab-pane>
+          </el-tabs>
+        </div>
+        <div class="right-container"></div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, watch } from 'vue'
+import type { TabsPaneContext } from 'element-plus'
+
+
+const activeName = ref('audience')
+
+const handleClick = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event)
+}
+
+const bidType = ref('1')
+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'
+  }
+})
+</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;
+}
+.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;
+}
+</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>

+ 27 - 0
src/views/adManage/sd/campaigns/CreateCampaigns/index.vue

@@ -1,11 +1,34 @@
 <template>
   <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>
@@ -13,4 +36,8 @@ import AdCampaign from './component/AdCampaign.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>