Ver código fonte

新增: SP新建广告活动-广告活动; SD表格数据展示;

WanGxC 1 ano atrás
pai
commit
3778737406

+ 1 - 1
src/components/dataCompare/index.vue

@@ -11,7 +11,7 @@
             <Bottom v-if="props.gapVal < 0"/>
           </el-icon>
         </template>
-        <span>{{ props.gapVal ? props.gapVal.toFixed(2) + '%' : ''}}</span>
+        <span>{{ props.gapVal ? props.gapVal.toFixed(2) + '%' : '--'}}</span>
       </p>
     </template>
     <p>对比周期:{{ compareDate[0] }} ~ {{ compareDate[1] }}</p>

+ 8 - 8
src/router/route.ts

@@ -46,14 +46,14 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			icon: 'iconfont icon-gerenzhongxin',
 		},
 	},
-	{
-		path: '/createcampaigns',
-		name: 'createcampaigns',
-		component: () => import('/@/views/adManage/sb/campaigns/CreateCampaigns/index.vue'),
-		meta: {
-			title: '新建广告活动',
-		},
-	},
+	// {
+	// 	path: '/createcampaigns',
+	// 	name: 'createcampaigns',
+	// 	component: () => import('/@/views/adManage/sb/campaigns/CreateCampaigns/index.vue'),
+	// 	meta: {
+	// 		title: '新建广告活动',
+	// 	},
+	// },
 ];
 
 /**

+ 1 - 1
src/views/adManage/sd/audiences/crud.tsx

@@ -108,7 +108,7 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
           title: '定向投放',
           column: {
             fixed: 'left',
-            width: 300,
+            width: 320,
             sortable: true
           },
         },

+ 1 - 1
src/views/adManage/sd/audiences/index.vue

@@ -28,7 +28,7 @@
         <div>
           <!-- 单独渲染第一个元素 -->
           <div
-            v-if="scope.row.resolvedExpression[0]?.value && scope.row.resolvedExpression[0].value.length > 0" style="font-weight: 500; color: #505968">
+            v-if="scope.row.resolvedExpression[0]?.value && scope.row.resolvedExpression[0].value.length > 0" style="font-weight: 550; color: #505968">
             {{ scope.row.resolvedExpression[0].value[0].value ?? '' }}
           </div>
           <!-- 渲染其他所有元素 -->

+ 1 - 1
src/views/adManage/sd/campaigns/crud.tsx

@@ -118,7 +118,7 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
           title: '广告活动名称',
           column: {
             fixed: 'left',
-            width: 180,
+            width: 340,
             sortable: true,
           },
         },

+ 5 - 3
src/views/adManage/sd/index.vue

@@ -34,7 +34,7 @@ import DateRangePicker from '/@/components/DateRangePicker/index.vue'
 import Campaigns from '/@/views/adManage/sd/campaigns/index.vue'
 import Audiences from '/@/views/adManage/sd/audiences/index.vue'
 import Targets from '/@/views/adManage/sd/targets/index.vue'
-import SearchTerm from '/@/views/adManage/sb/searchTerm/index.vue'
+import MatchedDelivery from '/@/views/adManage/sd/matchedDelivery/index.vue'
 import AdvertisedProducts from './advertisedProducts/index.vue'
 import PurchasedOtherProducts from './purchasedOtherProducts/index.vue'
 import Placement from '/@/views/adManage/sb/placement/index.vue'
@@ -49,18 +49,20 @@ const tabsComponents: any = {
   Campaigns,
   Targets,
   Audiences,
+  MatchedDelivery,
   // AdvertisedProducts,
   // PurchasedOtherProducts,
-  // SearchTerm,
+  
   // Placement
 }
 const tabs = [
   { label: '广告活动', name: 'Campaigns' },
   { label: '商品投放', name: 'Targets' },
   { label: '受众', name: 'Audiences' },
+  { label: '匹配的投放', name: 'MatchedDelivery' },
   // { label: '推广商品', name: 'AdvertisedProducts' },
   // { label: '购买的其他商品', name: 'PurchasedOtherProducts' },
-  // { label: '搜索词', name: 'SearchTerm' },
+
   // { label: '广告位', name: 'Placement' }
 ]
 

+ 81 - 0
src/views/adManage/sd/matchedDelivery/api.ts

@@ -0,0 +1,81 @@
+import { AddReq, DelReq, EditReq, UserPageQuery } from '@fast-crud/fast-crud';
+import { request } from '/@/utils/service';
+
+export const apiPrefix = '/api/ad_manage/sd-match-target/report/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list',
+        method: 'get',
+        params: query,
+    })
+}
+export function GetObj(id: any) {
+    return request({
+        url: apiPrefix + id + "/",
+        method: 'get',
+    });
+}
+
+export function AddObj(obj: AddReq) {
+    return request({
+        url: apiPrefix,
+        method: 'post',
+        data: obj,
+    });
+}
+
+export function UpdateObj(obj: EditReq) {
+    return request({
+        url: apiPrefix + obj.id + '/',
+        method: 'put',
+        data: obj,
+    });
+}
+
+export function DelObj(id: DelReq) {
+    return request({
+        url: apiPrefix + id + '/',
+        method: 'delete',
+        data: { id },
+    });
+}
+
+export function getCardData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "amount",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "trend/daily",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "trend/weekly",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "trend/monthly",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getAdStructureData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "structure/",
+        method: 'GET',
+        params: query
+    })
+}

+ 236 - 0
src/views/adManage/sd/matchedDelivery/chartComponents/adStruct.vue

@@ -0,0 +1,236 @@
+<template>
+    <div v-loading="loading">
+        <el-row :gutter="5">
+            <el-col :span="24">
+                <div style="margin-left: 45%">
+                    <span style="background: #3a83f7; width: 18px; height: 10px; margin-top: 8px; display: inline-block; border-radius: 3px;"></span>
+                    <TextSelector v-model="barModelValue1" :options="computedBarOptions1" @change="changeBarOne" style="margin-top: 5px; margin-left: 8px;"/>
+                    <span style="background: #f19a37; width: 18px; height: 10px; margin-top: 8px; margin-left: 20px; display: inline-block; border-radius: 3px;"></span>
+                    <TextSelector v-model="barModelValue2" :options="computedBarOptions2" @change="changeBarTwo" style="margin-top: 5px; margin-left: 8px;"/>
+                </div>
+                <div ref="bar" style="height: 400px;"></div>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script setup>
+import { onMounted, ref, inject, computed, watch } from "vue"
+import * as echarts from "echarts"
+import TextSelector from '/@/components/TextSelector/index.vue'
+import { getAdStructureData } from "/@/views/adManage/sp/targets/api"
+import { createDisabledOptions } from '../../../utils/dropdowndisable'
+import { barOptions1, barOptions2, barOptionsMap } from "/@/views/adManage/utils/enum"
+import { useShopInfo } from '/@/stores/shopInfo'
+
+
+const shopInfo = useShopInfo()
+let barChart = ref()
+const pie = ref()
+const bar = ref()
+const loading = ref(true)
+
+const dateRange = inject('dateRange')
+
+// 下拉框相关
+let barModelValue1 = ref(barOptions1[0].value)
+let barModelValue2 = ref(barOptions2[2].value)
+
+onMounted(async () => {
+    barChart = echarts.init(bar.value)
+
+    window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+    setTimeout(() => {
+        resizeChart()
+    }, 0)
+
+    await initBarData()
+    initChart()
+})
+
+// 获取总数据
+let allData = null
+
+async function setAdStructureData() {
+    allData = await getAdStructureData({ startDate: dateRange.value[0], endDate: dateRange.value[1], profileId: shopInfo.profile.profile_id })
+    return allData.data
+}
+
+// 柱状图总数据
+let barData = null
+let responseData = null
+// 柱状图初始数据
+let ACOSList
+let SpendList
+let xAxisList
+let xAxisMapList
+
+async function initBarData() {
+    responseData = await setAdStructureData()
+    barData = responseData
+    // 柱状图初始化数据
+    ACOSList = barData.map(item => item.ACOS)
+    SpendList = barData.map(item => item.Spend)
+    // 将x轴映射为中文
+    xAxisList = barData.map(item => item.Classification)
+    const classificationMap = {
+        'BROAD': '关键词-广泛',
+        'category': '品类',
+        'EXACT': '关键词-精准',
+        'asin': '商品',
+        'PHRASE': '关键词-词组',
+        'close-match': '紧密匹配',
+        'loose-match': '广泛匹配',
+        'substitutes': '同类商品',
+        'complements': '关联商品'
+    }
+    xAxisMapList = xAxisList.map(item => classificationMap[item])
+    loading.value = false
+}
+
+// 重置图像
+let flag = ref()
+let option
+let option2
+
+// 点击下拉框后重新渲染柱状图
+function changeBarOne(newValue) {
+    barModelValue1.value = newValue
+    updateBarChart()
+}
+
+function changeBarTwo(newValue) {
+    barModelValue2.value = newValue
+    updateBarChart()
+}
+
+function updateBarChart() {
+    const barValues1 = barData.map(item => item[barModelValue1.value])
+    const barValues2 = barData.map(item => item[barModelValue2.value])
+
+    option.series[0].data = barValues1
+    option.series[1].data = barValues2
+    barChart.setOption(option)
+}
+
+// 监听时间变化重新渲染表格
+watch(dateRange, async () => {
+    if (dateRange.value) {
+        loading.value = true
+        const resp = await setAdStructureData()
+        updateBarChartData(resp)
+        loading.value = false
+    }
+})
+
+// 根据新数据和当前下拉框选择更新 柱状图数据
+function updateBarChartData(resp) {
+    const barValues1 = resp.map(item => item[barModelValue1.value])
+    const barValues2 = resp.map(item => item[barModelValue2.value])
+
+    option.series[0].data = barValues1
+    option.series[1].data = barValues2
+    barChart.setOption(option)
+}
+
+const computedBarOptions1 = computed(() => createDisabledOptions(barOptions1, barModelValue2.value, barModelValue1.value))
+const computedBarOptions2 = computed(() => createDisabledOptions(barOptions2, barModelValue1.value, barModelValue2.value))
+
+// 初始化图表
+function initChart() {
+    // 柱状图配置
+    option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'shadow',
+                label: {
+                    backgroundColor: '#6a7985'
+                }
+            }
+        },
+        // legend: {data: ['数据1', '数据2'],},
+        toolbox: {
+            feature: {
+                saveAsImage: { yAxisIndex: 'none' }
+            }
+        },
+        grid: { top: 50, right: 60, bottom: 50, left: 60 },
+        xAxis: [
+            {
+                type: 'category',
+                boundaryGap: true,
+                data: xAxisMapList,
+            },
+        ],
+        yAxis: [
+            {
+                type: 'value',
+                axisLine: {
+                    show: true,
+                    lineStyle: {
+                        color: '#3a83f7' // 第一个 Y 轴的颜色
+                    }
+                }
+            },
+            {
+                type: 'value',
+                splitLine: {
+                    show: false
+                },
+                axisLine: {
+                    show: true,
+                    lineStyle: {
+                        color: '#f19a37' // 第一个 Y 轴的颜色
+                    }
+                }
+            }
+        ],
+        series: [
+            {
+                name: barOptionsMap[barModelValue1.value],
+                type: 'bar',
+                barWidth: '3%',
+                data: ACOSList,
+                yAxisIndex: 0,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                      { offset: 0, color: '#3a83f7' }, // 起始的鲜亮蓝色
+                      { offset: 0.5, color: '#5a9ef4' }, // 中间色,中度蓝色
+                      { offset: 1, color: '#8ab6f1' } // 结束的浅蓝色
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [6, 6, 6, 6],
+                },
+            },
+            {
+                name: barOptionsMap[barModelValue2.value],
+                type: 'bar',
+                barWidth: '3%',
+                data: SpendList,
+                yAxisIndex: 1,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                      { offset: 0, color: '#f19a37' },
+                      { offset: 0.5, color: '#f7b96c' }, // 中间色,浅橙色
+                      { offset: 1, color: 'rgb(234, 207, 135)' } // 结束的浅黄色
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [6, 6, 6, 6],
+                },
+            },
+        ],
+    }
+    barChart.setOption(option)
+    resizeChart()
+}
+
+function resizeChart() {
+    barChart.resize()
+}
+defineExpose({ resizeChart })
+</script>
+
+<style scoped>
+
+</style>

+ 261 - 0
src/views/adManage/sd/matchedDelivery/chartComponents/dataTendency.vue

@@ -0,0 +1,261 @@
+<template>
+    <div v-loading="loading">
+        <MetricsCards v-model="metrics" :metric-items="metricsItems" @change="changeMetric"></MetricsCards>
+        <div style="height: 350px;" ref="chartRef"></div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import {ref, onMounted, onBeforeUnmount, Ref, onBeforeMount, watch, computed} from 'vue'
+import * as echarts from 'echarts'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {getLineData, getCardData} from '../api'
+import {spCampaignMetricsEnum} from '/@/views/adManage/utils/enum.js'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+import XEUtils from 'xe-utils'
+import {buildChartOpt} from '/@/views/adManage/utils/tools.js'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+
+defineOptions({
+    name: 'DataTendencyChart'
+})
+
+onMounted(() => {
+    getMetricsItems()
+    addResize()
+    // initLine()
+    setTimeout(() => {
+        initLine()
+    }, 0)
+})
+onBeforeUnmount(() => {
+    if (chartObj) {
+        chartObj.dispose()
+        chartObj = null
+    }
+    removeResize()
+})
+
+const publicData = usePublicData()
+const metrics = ref([
+    {metric: 'Impression', color: '#0085ff', 'label': '曝光量'},
+    {metric: 'Click', color: '#3fd4cf', 'label': '点击量'},
+    {metric: 'Spend', color: '#ff9500', 'label': '花费'},
+])
+const shopInfo = useShopInfo()
+const metricsItems: Ref<MetricData[]> = ref([])
+let chartObj: any
+const chartRef = ref()
+const option: any = {
+    dataset: {
+        source: []
+    },
+    tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+            label: {
+                backgroundColor: '#6a7985'
+            }
+        }
+    },
+    legend: {
+        selected: {},  // 控制显隐
+        show: false
+    },
+    grid: {
+        top: 50, right: 150, bottom: 30, left: 55,
+    },
+    xAxis: {
+        type: 'category'
+    },
+    yAxis: [
+        {
+            id: 0,
+            type: 'value',
+            name: '曝光量',
+            splitLine: {
+                show: true // 设置显示分割线
+            },
+            axisLine: {
+                show: true,
+                lineStyle: {
+                    color: '#0085ff'
+                }
+            },
+            show: true
+        },
+        {
+            id: 1,
+            type: 'value',
+            name: '点击量',
+            position: 'right',
+            splitLine: {
+                show: false
+            },
+            axisLine: {
+                show: true,
+                lineStyle: {
+                    color: '#3fd4cf'
+                }
+            },
+            show: true
+        },
+        {
+            id: 2,
+            type: 'value',
+            position: 'right',
+            offset: 90,
+            name: '花费',
+            splitLine: {
+                show: false
+            },
+            axisLine: {
+                show: true,
+                lineStyle: {
+                    color: '#ff9500'
+                }
+            },
+            show: true
+        }
+    ],
+    series: [
+        {
+            id: 0,
+            name: '曝光量',
+            type: 'bar',
+            encode: {
+                x: 'date',
+                y: 'Impression'
+            },
+            barWidth: '20px',
+            yAxisIndex: 0,
+            itemStyle: {
+                color: '#0085ff',
+                borderRadius: [6, 6, 6, 6],
+            }
+        },
+        {
+            id: 1,
+            name: '点击量',
+            type: 'line',
+            encode: {
+                x: 'date',
+                y: 'Click'
+            },
+            symbolSize: 6,
+            symbol: 'circle',
+            smooth: true,
+            yAxisIndex: 1,
+            itemStyle: {color: '#3fd4cf', borderColor: '#3fd4cf'},
+            areaStyle: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                    {offset: 0, color: '#3fd4cf53'},
+                    {offset: 1, color: '#3fd4cf03'},
+                ]),
+            },
+            emphasis: {
+                focus: 'series'
+            }
+        },
+        {
+            id: 2,
+            name: '花费',
+            type: 'line',
+            encode: {
+                x: 'date',
+                y: 'Spend'
+            },
+            symbolSize: 6,
+            symbol: 'circle',
+            smooth: true,
+            yAxisIndex: 2,
+            itemStyle: {color: '#ff9500', borderColor: '#ff9500'},
+            areaStyle: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                    {offset: 0, color: '#ff950053'},
+                    {offset: 1, color: '#ff950003'},
+                ]),
+            },
+            emphasis: {
+                focus: 'series'
+            }
+        }
+    ]
+}
+const {dateRange} = storeToRefs(publicData)
+const loading = ref(true)
+
+const getDataset = async () => {
+    const resp = await getLineData({profile: shopInfo.profile.profile_id, start: dateRange.value[0], end: dateRange.value[1]})
+    return resp.data
+}
+const initLine = async () => {
+    chartObj = echarts.init(chartRef.value)
+    const items = await getDataset()
+    option.dataset.source = items
+    XEUtils.arrayEach(metricsItems.value, info => {
+        option.legend.selected[info.label] = false
+    })
+    for (const info of metrics.value) {
+        option.legend.selected[info.label] = true
+    }
+    chartObj.setOption(option)
+    loading.value = false
+}
+
+const getMetricsItems = async () => {
+    const resp = await getCardData({start: dateRange.value[0], end: dateRange.value[1], profile: shopInfo.profile.profile_id})
+    const data = resp.data
+    metricsItems.value.length = 0
+    XEUtils.arrayEach(spCampaignMetricsEnum, info => {
+        const tmp: MetricData = {
+            label: info.label,
+            value: info.value,
+            metricVal: data[info.value],
+            gapVal: data[`gap${info.value}`],
+            preVal: data[`prev${info.value}`],
+        }
+        metricsItems.value.push(tmp)
+    })
+}
+
+const changeMetric = () => {
+    const opt = buildChartOpt(option, metrics.value)
+    chartObj.setOption(opt)
+}
+
+watch(
+        dateRange,
+        async () => {
+            loading.value = true
+            await getMetricsItems()
+            const items = await getDataset()
+            const opt = {dataset: {source: items}}
+            chartObj.setOption(opt)
+            loading.value = false
+        }
+)
+
+const resizeChart = () => {
+    chartObj.resize()
+}
+const addResize = () => {
+    window.addEventListener('resize', resizeChart)
+}
+const removeResize = () => {
+    window.removeEventListener('resize', resizeChart)
+}
+
+
+</script>
+
+<style scoped>
+.metrics-cards {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    gap: 12px;
+    width: 100%;
+}
+</style>

+ 165 - 0
src/views/adManage/sd/matchedDelivery/crud.tsx

@@ -0,0 +1,165 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {SdMatchedDeliveryColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+  const editRequest = async ({form, row}: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({row}: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({form}: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          borderRight: 'none',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+        showSummary: true,
+        stripe: false
+      },
+      container: {
+        fixedHeight: false
+      },
+      actionbar: {
+        show: false,
+        buttons: {
+          add: {
+            show: false
+          },
+        }
+      },
+      search: {
+        show: false
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true
+          },
+          compact: {
+            show: false
+          }
+        }
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        fixed: 'right',
+        width: 100,
+        align: 'center',
+        buttons: {
+          view: {
+            show: false,
+          },
+          edit: {
+            iconRight: 'Edit',
+            type: 'text',
+            text: null
+            // show: hasPermissions('dictionary:Update'),
+          },
+          remove: {
+            show: false
+            // iconRight: 'Delete',
+            // type: 'text',
+            // text: null
+            // show: hasPermissions('dictionary:Delete'),
+          },
+        },
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            show: false
+          },
+          form: {
+            show: false
+          }
+        },
+        matchedTargetAsin:{
+          title: '匹配的投放',
+          column: {
+            fixed: 'left',
+            width: 180,
+            sortable: true
+          },
+        },
+        campaign__tactic: {
+          title: '投放类型',
+          column: {
+            width: 120,
+            align: 'center',
+            formatter: (row) => {
+              switch (row.value) {
+                case 'T00030':
+                  return '受众'
+                case 'T00020':
+                  return '商品'
+                default:
+                  return '--'
+              }
+            }
+          }
+        },
+        xxxx: {
+          title: '定向投放',
+          column: {
+            width: 230,
+            align: 'left'
+          }
+        },
+        campaignName: {
+          title: '广告活动名称',
+          column: {
+            width: 340,
+            align: 'left'
+          }
+        },
+        adGroupName: {
+          title: '广告组名称',
+          column: {
+            width: 340,
+            align: 'left'
+          }
+        },
+        campaign__costTyp: {
+          title: '成本类型',
+          column: {
+            width: 90,
+            align: 'center'
+          }
+        },
+
+        ...SdMatchedDeliveryColumn
+      }
+    }
+  }
+}

+ 229 - 0
src/views/adManage/sd/matchedDelivery/index.vue

@@ -0,0 +1,229 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #header-middle>
+        <el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card" @tab-change="changeTab">
+          <el-tab-pane label="数据趋势" name="dataTendency">
+            <DataTendencyChart
+                :query="queryParams"
+                v-if="tabActiveName === 'dataTendency'"
+                :fetchCard="getCardData"
+                :fetch-line-month="getLineMonthData"
+                :fetch-line-week="getLineWeekData"
+                :fetchLine="getLineData">
+            </DataTendencyChart>
+          </el-tab-pane>
+          <el-tab-pane label="广告结构" name="adStruct">
+            <AdStructChart v-if="tabActiveName === 'adStruct'"/>
+          </el-tab-pane>
+          <el-tab-pane label="散点视图" name="scatterView"></el-tab-pane>
+        </el-tabs>
+      </template>
+      <template #cell_percentTimeInBudget="scope">
+        <el-progress :percentage="scope.row.percentTimeInBudget > 0 ? scope.row.percentTimeInBudget * 100 : 0"/>
+      </template>
+
+      <template #cell_resolvedExpression_value="scope">
+        <el-tooltip effect="dark" placement="top">
+          <template #content>
+            <p>{{ scope.row.resolvedExpression_value }}</p>
+            {{ scope.row.ASIN }}
+          </template>
+          <div>
+            <el-link :underline="false" class="ellipsis-inline" style="color: rgb(30, 33, 40); display: inline-block; max-width: 100%;">
+              {{ scope.row.resolvedExpression_value }}
+            </el-link>
+            <br>
+            <span class="ellipsis">
+              <span style="color: rgb(109, 119, 132)">ASIN: </span>
+              <span style="color: rgb(30, 33, 40)">{{ scope.row.ASIN }}</span>
+            </span>
+          </div>
+        </el-tooltip>
+      </template>
+      
+      <template #cell_matchedTargetAsin="scope">
+        <el-tooltip effect="dark" :content="scope.row.matchedTargetAsin" placement="top">
+          <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">
+            <div class="en-text">{{ scope.row.matchedTargetAsin.toUpperCase() }}</div>
+          </el-link>
+        </el-tooltip>
+      </template>
+      <template #cell_campaignName="scope">
+        <el-tooltip effect="dark" :content="scope.row.campaignName" placement="top">
+          <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">
+            <div class="en-text">{{ scope.row.campaignName }}</div>
+          </el-link>
+        </el-tooltip>
+      </template>
+      <template #cell_adGroupName="scope">
+        <el-tooltip effect="dark" :content="scope.row.adGroupName" placement="top">
+          <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">
+            <div class="en-text">{{ scope.row.adGroupName }}</div>
+          </el-link>
+        </el-tooltip>
+      </template>
+      <template #cell_suggestedBid="scope">
+        <div>{{ scope.row.suggestedBid ? `$${scope.row.suggestedBid}` : '--' }}</div>
+        <div class="text-range">
+          {{ scope.row.suggestedBid_lower ? `$${scope.row.suggestedBid_lower}` : '--' }} ~ 
+          {{ scope.row.suggestedBid_upper ? `$${scope.row.suggestedBid_upper}` : '--' }}
+        </div>
+      </template>      
+      <template #cell_MissedImpressions="scope">
+        {{ scope.row.MissedImpressionsLower ?? '0' }} ~ {{ scope.row.MissedImpressionsUpper ?? '0' }}
+      </template>
+      <template #cell_MissedClicks="scope"> {{ scope.row.MissedClicksLower ?? '0' }} ~ {{ scope.row.MissedClicksUpper ?? '0' }}</template>
+      <template #cell_MissedSales="scope"> {{ scope.row.MissedSalesLower ?? '0' }} ~ {{ scope.row.MissedSalesUpper ?? '0' }}</template>
+
+      <template v-for="field of Object.keys(SdMatchedDeliveryColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+            :field="field"
+            :value="scope.row[field]"
+            :prev-val="scope.row[`prev${field}`]"
+            :gap-val="scope.row[`gap${field}`]"
+            :date-range="dateRange"
+            :show-compare="showCompare"/>
+      </template>
+      <template #toolbar-left>
+        <div class="campare-switch">
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small"/>
+        </div>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {nextTick, onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useRoute, useRouter} from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/sd/chartComponents/dataTendency.vue'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {usePublicData} from '/@/stores/publicData'
+import AdStructChart from './chartComponents/adStruct.vue'
+import {getCardData, getLineData, getLineMonthData, getLineWeekData} from './api'
+import {storeToRefs} from 'pinia'
+import {SdMatchedDeliveryColumn} from '/@/views/adManage/utils/commonTabColumn'
+import DataCompare from '/@/components/dataCompare/index.vue'
+
+
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const {dateRange} = storeToRefs(publicData)
+const {profile} = storeToRefs(shopInfo)
+const queryParams = ref({
+  profileId: profile.value.profile_id,
+  dateRange
+})
+
+const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, context: queryParams})
+const route = useRoute()
+const router = useRouter()
+const adStructChartRef = ref()
+const dataTendencyRef = ref()
+const showCompare = ref(false)
+
+
+onMounted(() => {
+  crudExpose.doRefresh()
+})
+
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'CampaignDetail',
+    query: {campaignId: row.campaignId, tagsViewName: row.campaignName},
+  })
+}
+
+const resizeTabChart = () => {
+  if (tabActiveName.value === 'dataTendency') {
+    dataTendencyRef.value.resizeChart()
+  } else if (tabActiveName.value === 'adStruct') {
+    adStructChartRef.value.resizeChart()
+  }
+}
+const changeTab = () => {
+  nextTick(() => {
+    resizeTabChart()
+  })
+}
+defineExpose({resizeTabChart})
+
+watch(queryParams, async () => {
+  crudExpose.doRefresh()
+}, {deep: true})
+
+</script>
+
+<style lang="scss" scoped>
+.campare-switch {
+  flex: none;
+}
+
+::v-deep(.el-table__footer-wrapper td.el-table__cell:nth-child(n+3):nth-child(-n+6) .cell) {
+  display: none;
+}
+
+.en-text {
+  max-width: 100%;
+  font-size: 13px;
+  font-weight: 420;
+  color: #326cd9;
+  word-break: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  display: -webkit-box;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+::v-deep(.el-table__footer-wrapper) {
+  border: 0;
+}
+
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+
+.text-range {
+  color: #808184;
+}
+
+.ellipsis {
+  max-width: 100%;
+  font-size: 13px;
+  font-weight: 420;
+  word-break: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  display: -webkit-box;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+.ellipsis-inline {
+  max-width: 100%;
+  font-size: 13px;
+  font-weight: 420;
+  word-break: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  display: inline;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+::v-deep(span.el-link__inner) {
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  display: inline;
+}
+
+</style>

+ 87 - 0
src/views/adManage/sd/promoteProducts/api.ts

@@ -0,0 +1,87 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import XEUtils from 'xe-utils';
+
+export const apiPrefix = '/api/ad_manage/sdtarget/report/';
+export function GetList(query: UserPageQuery) {
+    query["tactic"] = "T00030"
+    return request({
+        url: '/api/ad_manage/sdtarget/',
+        method: 'get',
+        params: query,
+    })
+}
+export function GetObj(id: any) {
+    return request({
+        url: apiPrefix + id + "/",
+        method: 'get',
+    });
+}
+
+export function AddObj(obj: AddReq) {
+    return request({
+        url: apiPrefix,
+        method: 'post',
+        data: obj,
+    });
+}
+
+export function UpdateObj(obj: EditReq) {
+    return request({
+        url: apiPrefix + obj.id + '/',
+        method: 'put',
+        data: obj,
+    });
+}
+
+export function DelObj(id: DelReq) {
+    return request({
+        url: apiPrefix + id + '/',
+        method: 'delete',
+        data: { id },
+    });
+}
+
+export function getCardData(query: UserPageQuery) {
+    query["tactic"] = "T00030"
+    return request({
+        url: apiPrefix + "amount",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    query["tactic"] = "T00030"
+    return request({
+        url: apiPrefix + "trend/daily",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+    query["tactic"] = "T00030"
+    return request({
+        url: apiPrefix + "trend/weekly",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+    query["tactic"] = "T00030"
+    return request({
+        url: apiPrefix + "trend/monthly",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getAdStructureData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "structure/",
+        method: 'GET',
+        params: query
+    })
+}

+ 236 - 0
src/views/adManage/sd/promoteProducts/chartComponents/adStruct.vue

@@ -0,0 +1,236 @@
+<template>
+    <div v-loading="loading">
+        <el-row :gutter="5">
+            <el-col :span="24">
+                <div style="margin-left: 45%">
+                    <span style="background: #3a83f7; width: 18px; height: 10px; margin-top: 8px; display: inline-block; border-radius: 3px;"></span>
+                    <TextSelector v-model="barModelValue1" :options="computedBarOptions1" @change="changeBarOne" style="margin-top: 5px; margin-left: 8px;"/>
+                    <span style="background: #f19a37; width: 18px; height: 10px; margin-top: 8px; margin-left: 20px; display: inline-block; border-radius: 3px;"></span>
+                    <TextSelector v-model="barModelValue2" :options="computedBarOptions2" @change="changeBarTwo" style="margin-top: 5px; margin-left: 8px;"/>
+                </div>
+                <div ref="bar" style="height: 400px;"></div>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script setup>
+import { onMounted, ref, inject, computed, watch } from "vue"
+import * as echarts from "echarts"
+import TextSelector from '/@/components/TextSelector/index.vue'
+import { getAdStructureData } from "/@/views/adManage/sp/targets/api"
+import { createDisabledOptions } from '../../../utils/dropdowndisable'
+import { barOptions1, barOptions2, barOptionsMap } from "/@/views/adManage/utils/enum"
+import { useShopInfo } from '/@/stores/shopInfo'
+
+
+const shopInfo = useShopInfo()
+let barChart = ref()
+const pie = ref()
+const bar = ref()
+const loading = ref(true)
+
+const dateRange = inject('dateRange')
+
+// 下拉框相关
+let barModelValue1 = ref(barOptions1[0].value)
+let barModelValue2 = ref(barOptions2[2].value)
+
+onMounted(async () => {
+    barChart = echarts.init(bar.value)
+
+    window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+    setTimeout(() => {
+        resizeChart()
+    }, 0)
+
+    await initBarData()
+    initChart()
+})
+
+// 获取总数据
+let allData = null
+
+async function setAdStructureData() {
+    allData = await getAdStructureData({ startDate: dateRange.value[0], endDate: dateRange.value[1], profileId: shopInfo.profile.profile_id })
+    return allData.data
+}
+
+// 柱状图总数据
+let barData = null
+let responseData = null
+// 柱状图初始数据
+let ACOSList
+let SpendList
+let xAxisList
+let xAxisMapList
+
+async function initBarData() {
+    responseData = await setAdStructureData()
+    barData = responseData
+    // 柱状图初始化数据
+    ACOSList = barData.map(item => item.ACOS)
+    SpendList = barData.map(item => item.Spend)
+    // 将x轴映射为中文
+    xAxisList = barData.map(item => item.Classification)
+    const classificationMap = {
+        'BROAD': '关键词-广泛',
+        'category': '品类',
+        'EXACT': '关键词-精准',
+        'asin': '商品',
+        'PHRASE': '关键词-词组',
+        'close-match': '紧密匹配',
+        'loose-match': '广泛匹配',
+        'substitutes': '同类商品',
+        'complements': '关联商品'
+    }
+    xAxisMapList = xAxisList.map(item => classificationMap[item])
+    loading.value = false
+}
+
+// 重置图像
+let flag = ref()
+let option
+let option2
+
+// 点击下拉框后重新渲染柱状图
+function changeBarOne(newValue) {
+    barModelValue1.value = newValue
+    updateBarChart()
+}
+
+function changeBarTwo(newValue) {
+    barModelValue2.value = newValue
+    updateBarChart()
+}
+
+function updateBarChart() {
+    const barValues1 = barData.map(item => item[barModelValue1.value])
+    const barValues2 = barData.map(item => item[barModelValue2.value])
+
+    option.series[0].data = barValues1
+    option.series[1].data = barValues2
+    barChart.setOption(option)
+}
+
+// 监听时间变化重新渲染表格
+watch(dateRange, async () => {
+    if (dateRange.value) {
+        loading.value = true
+        const resp = await setAdStructureData()
+        updateBarChartData(resp)
+        loading.value = false
+    }
+})
+
+// 根据新数据和当前下拉框选择更新 柱状图数据
+function updateBarChartData(resp) {
+    const barValues1 = resp.map(item => item[barModelValue1.value])
+    const barValues2 = resp.map(item => item[barModelValue2.value])
+
+    option.series[0].data = barValues1
+    option.series[1].data = barValues2
+    barChart.setOption(option)
+}
+
+const computedBarOptions1 = computed(() => createDisabledOptions(barOptions1, barModelValue2.value, barModelValue1.value))
+const computedBarOptions2 = computed(() => createDisabledOptions(barOptions2, barModelValue1.value, barModelValue2.value))
+
+// 初始化图表
+function initChart() {
+    // 柱状图配置
+    option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'shadow',
+                label: {
+                    backgroundColor: '#6a7985'
+                }
+            }
+        },
+        // legend: {data: ['数据1', '数据2'],},
+        toolbox: {
+            feature: {
+                saveAsImage: { yAxisIndex: 'none' }
+            }
+        },
+        grid: { top: 50, right: 60, bottom: 50, left: 60 },
+        xAxis: [
+            {
+                type: 'category',
+                boundaryGap: true,
+                data: xAxisMapList,
+            },
+        ],
+        yAxis: [
+            {
+                type: 'value',
+                axisLine: {
+                    show: true,
+                    lineStyle: {
+                        color: '#3a83f7' // 第一个 Y 轴的颜色
+                    }
+                }
+            },
+            {
+                type: 'value',
+                splitLine: {
+                    show: false
+                },
+                axisLine: {
+                    show: true,
+                    lineStyle: {
+                        color: '#f19a37' // 第一个 Y 轴的颜色
+                    }
+                }
+            }
+        ],
+        series: [
+            {
+                name: barOptionsMap[barModelValue1.value],
+                type: 'bar',
+                barWidth: '3%',
+                data: ACOSList,
+                yAxisIndex: 0,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                      { offset: 0, color: '#3a83f7' }, // 起始的鲜亮蓝色
+                      { offset: 0.5, color: '#5a9ef4' }, // 中间色,中度蓝色
+                      { offset: 1, color: '#8ab6f1' } // 结束的浅蓝色
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [6, 6, 6, 6],
+                },
+            },
+            {
+                name: barOptionsMap[barModelValue2.value],
+                type: 'bar',
+                barWidth: '3%',
+                data: SpendList,
+                yAxisIndex: 1,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                      { offset: 0, color: '#f19a37' },
+                      { offset: 0.5, color: '#f7b96c' }, // 中间色,浅橙色
+                      { offset: 1, color: 'rgb(234, 207, 135)' } // 结束的浅黄色
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [6, 6, 6, 6],
+                },
+            },
+        ],
+    }
+    barChart.setOption(option)
+    resizeChart()
+}
+
+function resizeChart() {
+    barChart.resize()
+}
+defineExpose({ resizeChart })
+</script>
+
+<style scoped>
+
+</style>

+ 186 - 0
src/views/adManage/sd/promoteProducts/crud.tsx

@@ -0,0 +1,186 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {SdBaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+  const pageRequest = async (query: UserPageQuery) => {
+    const params = parseQueryParams(context.value)
+    XEUtils.assign(query, params)
+    return await api.GetList(query)
+  }
+  const editRequest = async ({form, row}: EditReq) => {
+    form.id = row.id
+    return await api.UpdateObj(form)
+  }
+  const delRequest = async ({row}: DelReq) => {
+    return await api.DelObj(row.id)
+  }
+  const addRequest = async ({form}: AddReq) => {
+    return await api.AddObj(form)
+  }
+
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          borderRight: 'none',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+        showSummary: true,
+        stripe: false
+      },
+      container: {
+        fixedHeight: false
+      },
+      actionbar: {
+        show: false,
+        buttons: {
+          add: {
+            show: false
+          },
+        }
+      },
+      search: {
+        show: false
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true
+          },
+          compact: {
+            show: false
+          }
+        }
+      },
+      request: {
+        pageRequest,
+        addRequest,
+        editRequest,
+        delRequest,
+      },
+      rowHandle: {
+        fixed: 'right',
+        width: 100,
+        align: 'center',
+        buttons: {
+          view: {
+            show: false,
+          },
+          edit: {
+            iconRight: 'Edit',
+            type: 'text',
+            text: null
+            // show: hasPermissions('dictionary:Update'),
+          },
+          remove: {
+            show: false
+            // iconRight: 'Delete',
+            // type: 'text',
+            // text: null
+            // show: hasPermissions('dictionary:Delete'),
+          },
+        },
+      },
+      columns: {
+        id: {
+          title: 'ID',
+          column: {
+            show: false
+          },
+          form: {
+            show: false
+          }
+        },
+        expression:{
+          title: '定向投放',
+          column: {
+            fixed: 'left',
+            width: 320,
+            sortable: true
+          },
+        },
+        resolvedExpression: {
+          title: '类型',
+          column: {
+            align: 'center',
+            width: 130,
+            sortable: true
+          },
+        },
+        state: {
+          title: '状态',
+          column: {
+            width: '90px',
+            align: 'center',
+            sortable: true,
+          },
+          type: 'dict-select',
+          search: {
+            show: true
+          },
+          dict: dict({
+            data: [
+              {value: 'paused', label: '已暂停', color: 'warning'},
+              {value: 'enabled', label: '投放中', color: 'success'},
+            ]
+          })
+        },
+        campaignName: {
+          title: '广告活动名称',
+          column: {
+            width: 180,
+          }
+        },
+        adGroupName: {
+          title: '广告组名称',
+          column: {
+            width: 180,
+          }
+        },
+        suggestedBid: {
+          title: '建议竞价',
+          column: {
+            width: 130,
+            align: 'right'
+          }
+        },
+        bid: {
+          title: '出价',
+          column: {
+            width: 80,
+            align: 'center',
+            sortable: true,
+            formatter: (row) => {
+              return '$' + row.value
+            }
+          }
+        },
+        suggestedBid_lower: {
+          column: {
+            show: false,
+          }
+        },
+        suggestedBid_upper: {
+          column: {
+            show: false,
+          }
+        },
+
+        ...SdBaseColumn
+      }
+    }
+  }
+}

+ 238 - 0
src/views/adManage/sd/promoteProducts/index.vue

@@ -0,0 +1,238 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #header-middle>
+        <el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card" @tab-change="changeTab">
+          <el-tab-pane label="数据趋势" name="dataTendency">
+            <DataTendencyChart
+              :query="queryParams"
+              v-if="tabActiveName === 'dataTendency'"
+              :fetchCard="getCardData"
+              :fetch-line-month="getLineMonthData"
+              :fetch-line-week="getLineWeekData"
+              :fetchLine="getLineData"
+            >
+            </DataTendencyChart>
+          </el-tab-pane>
+          <el-tab-pane label="广告结构" name="adStruct">
+            <AdStructChart v-if="tabActiveName === 'adStruct'" />
+          </el-tab-pane>
+          <el-tab-pane label="散点视图" name="scatterView"></el-tab-pane>
+        </el-tabs>
+      </template>
+      <template #cell_percentTimeInBudget="scope">
+        <el-progress :percentage="scope.row.percentTimeInBudget > 0 ? scope.row.percentTimeInBudget * 100 : 0" />
+      </template>
+
+      <template #cell_expression="scope">
+        <div>
+          <!-- 单独渲染第一个元素 -->
+          <div
+            v-if="scope.row.resolvedExpression[0]?.value && scope.row.resolvedExpression[0].value.length > 0" style="font-weight: 550; color: #505968">
+            {{ scope.row.resolvedExpression[0].value[0].value ?? '' }}
+          </div>
+          <!-- 渲染其他所有元素 -->
+          <div v-if="scope.row.resolvedExpression[0]?.value && scope.row.resolvedExpression[0].value.length > 1">
+            <template v-for="(item, index) in scope.row.resolvedExpression[0].value" :key="index">
+              <span v-if="index > 0">
+                <span v-if="item.type &&
+                    (item.type === 'asinPriceGreaterThan' || 
+                    item.type === 'asinPriceLESSThan' || 
+                    item.type === 'asinReviewRatingGreaterThan' || 
+                    item.type === 'asinReviewRatingLessThan')">
+                  {{ sdtargetMap[item.type] }}
+                </span>
+                <span v-else>{{ item.type ? sdtargetMap[item.type] + ': ' : '' }}</span>
+                {{ item.value ?? '' }}
+                <span v-if="index < scope.row.resolvedExpression[0].value.length - 1">&nbsp;</span>
+              </span>
+            </template>
+          </div>
+        </div>
+      </template>
+      <template #cell_campaignName="scope">
+        <el-tooltip effect="dark" :content="scope.row.campaignName" placement="top">
+          <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">
+            <div class="en-text">{{ scope.row.campaignName }}</div>
+          </el-link>
+        </el-tooltip>
+      </template>
+      <!-- 类型 -->
+      <template #cell_resolvedExpression="scope">
+        <div>
+          {{ sdTypeMap[scope.row.resolvedExpression[0].type as string] }}
+        </div>
+      </template>
+
+      <template #cell_adGroupName="scope">
+        <el-tooltip effect="dark" :content="scope.row.adGroupName" placement="top">
+          <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">
+            <div class="en-text">{{ scope.row.adGroupName }}</div>
+          </el-link>
+        </el-tooltip>
+      </template>
+      <template #cell_suggestedBid="scope">
+        <div>{{ scope.row.suggestedBid ? `$${scope.row.suggestedBid}` : '--' }}</div>
+        <div class="text-range">
+          {{ scope.row.suggestedBid_lower ? `$${scope.row.suggestedBid_lower}` : '--' }} ~
+          {{ scope.row.suggestedBid_upper ? `$${scope.row.suggestedBid_upper}` : '--' }}
+        </div>
+      </template>
+      <template #cell_MissedImpressions="scope">
+        {{ scope.row.MissedImpressionsLower ?? '0' }} ~ {{ scope.row.MissedImpressionsUpper ?? '0' }}
+      </template>
+      <template #cell_MissedClicks="scope"> {{ scope.row.MissedClicksLower ?? '0' }} ~ {{ scope.row.MissedClicksUpper ?? '0' }}</template>
+      <template #cell_MissedSales="scope"> {{ scope.row.MissedSalesLower ?? '0' }} ~ {{ scope.row.MissedSalesUpper ?? '0' }}</template>
+
+      <template v-for="field of Object.keys(SdBaseColumn)" #[`cell_${field}`]="scope">
+        <DataCompare
+          :field="field"
+          :value="scope.row[field]"
+          :prev-val="scope.row[`prev${field}`]"
+          :gap-val="scope.row[`gap${field}`]"
+          :date-range="dateRange"
+          :show-compare="showCompare"
+        />
+      </template>
+      <template #toolbar-left>
+        <div class="campare-switch">
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small" />
+        </div>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, onMounted, ref, watch } from 'vue'
+import { FsPage, useFs } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+import { useRoute, useRouter } from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/sd/chartComponents/dataTendency.vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import AdStructChart from './chartComponents/adStruct.vue'
+import { getCardData, getLineData, getLineMonthData, getLineWeekData } from './api'
+import { storeToRefs } from 'pinia'
+import { SdBaseColumn } from '/@/views/adManage/utils/commonTabColumn'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import { sdTypeMap, sdtargetMap } from '/@/views/adManage/utils/enum'
+
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  profileId: profile.value.profile_id,
+  dateRange,
+})
+
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const route = useRoute()
+const router = useRouter()
+const adStructChartRef = ref()
+const dataTendencyRef = ref()
+const showCompare = ref(false)
+
+onMounted(() => {
+  crudExpose.doRefresh()
+})
+
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'CampaignDetail',
+    query: { campaignId: row.campaignId, tagsViewName: row.campaignName },
+  })
+}
+
+const resizeTabChart = () => {
+  if (tabActiveName.value === 'dataTendency') {
+    dataTendencyRef.value.resizeChart()
+  } else if (tabActiveName.value === 'adStruct') {
+    adStructChartRef.value.resizeChart()
+  }
+}
+const changeTab = () => {
+  nextTick(() => {
+    resizeTabChart()
+  })
+}
+defineExpose({ resizeTabChart })
+
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style lang="scss" scoped>
+.campare-switch {
+  flex: none;
+}
+
+::v-deep(.el-table__footer-wrapper td.el-table__cell:nth-child(n + 3):nth-child(-n + 6) .cell) {
+  display: none;
+}
+
+.en-text {
+  max-width: 100%;
+  font-size: 13px;
+  font-weight: 420;
+  word-break: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  display: -webkit-box;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+::v-deep(.el-table__footer-wrapper) {
+  border: 0;
+}
+
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+
+.text-range {
+  color: #808184;
+}
+
+.ellipsis {
+  max-width: 100%;
+  font-size: 13px;
+  font-weight: 420;
+  word-break: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  display: -webkit-box;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+.ellipsis-inline {
+  max-width: 100%;
+  font-size: 13px;
+  font-weight: 420;
+  word-break: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: normal;
+  display: inline;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+::v-deep(span.el-link__inner) {
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  display: inline;
+}
+</style>

+ 3 - 3
src/views/adManage/sd/targets/crud.tsx

@@ -115,7 +115,7 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         state: {
           title: '状态',
           column: {
-            width: '90px',
+            width: 90,
             align: 'center',
             sortable: true,
           },
@@ -133,13 +133,13 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         campaignName: {
           title: '广告活动名称',
           column: {
-            width: 180,
+            width: 230,
           }
         },
         adGroupName: {
           title: '广告组名称',
           column: {
-            width: 180,
+            width: 230,
           }
         },
         suggestedBid: {

+ 274 - 1
src/views/adManage/sp/campaigns/CreateCampaigns/index.vue

@@ -1,9 +1,282 @@
 <template>
-  <p>新建广告页面</p>
+  <div>
+    <el-card>
+      <div style="padding-left: 5px">
+        <div style="font-size: 20px; font-weight: bold">广告活动</div>
+        <hr />
+        <br />
+        <el-form
+          :label-position="labelPosition"
+          ref="ruleFormRef"
+          :model="ruleForm"
+          :rules="rules"
+          label-width="120px"
+          class="demo-ruleForm"
+          :size="formSize"
+          status-icon
+        >
+          <el-form-item label="广告活动名称" prop="name" style="width: 350px">
+            <el-input v-model="ruleForm.name" />
+          </el-form-item>
+          <el-form-item label="广告组合" prop="adMix">
+            <el-select v-model="ruleForm.adMix" placeholder="Activity zone">
+              <el-option label="Zone one" value="shanghai" />
+              <el-option label="Zone two" value="beijing" />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item prop="date1" label="开始日期" style="width: 350px">
+            <el-date-picker v-model="ruleForm.date1" type="date" label="开始日期" placeholder="开始日期" style="width: 100%" />
+          </el-form-item>
+          <el-form-item prop="date2" label="结束日期" style="width: 350px">
+            <el-date-picker v-model="ruleForm.date2" label="结束日期" placeholder="结束日期" style="width: 100%" />
+          </el-form-item>
+          <el-form-item prop="budget" label="每日预算" style="width: 350px">
+            <el-input v-model="ruleForm.budget" />
+          </el-form-item>
+          <!-- <el-form-item label="Instant delivery" prop="delivery">
+      <el-switch v-model="ruleForm.delivery" />
+    </el-form-item> -->
+          <el-form-item label="投放类型" prop="type" class="column-item">
+            <el-radio-group v-model="ruleForm.type" @click="changeType">
+              <div>
+                <el-radio label="auto">自动</el-radio>
+                <div class="radio-description">定向与您推广商品相似的关键词和商品</div>
+              </div>
+              <div>
+                <el-radio label="manual">手动</el-radio>
+                <div class="radio-description">选择关键词或商品以定向购物者搜索并设置自定义出价</div>
+              </div>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="竞价策略" prop="bidStrategy" class="column-item column-margin-bottom">
+            <el-radio-group v-model="ruleForm.bidStrategy">
+              <div>
+                <el-radio label="dynamicBid_Low" border>动态竞价-仅降低
+                  <div class="radio-description-2">当您的广告不太可能带来销售时,我们将实时降低您的竞价</div>
+                </el-radio>
+              </div>
+              <div>
+                <el-radio label="dynamicBid_HighAndLow" border>动态竞价-提高和降低
+                  <div class="radio-description-2">
+                    当您的广告很有可能带来销售时,我们将实时提高您的竞价(最高可达 100%),并在您的广告不太可能带来销售时降低您的竞价
+                  </div>
+                </el-radio>
+              </div>
+              <el-radio label="staticBid" border>固定竞价
+                <div class="radio-description-2">我们将使用您的确切竞价和您设置的任何手动调整,而不会根据售出可能性对您的竞价进行更改</div>
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <el-form
+            :label-position="labelPosition"
+            ref="ruleFormRef2"
+            :model="ruleForm2"
+            :rules="rules2"
+            label-width="120px"
+            class="demo-ruleForm"
+            :size="formSize"
+            status-icon
+          >
+            <el-form-item label="按展示位置调整出价" prop="placeBid">
+              <p style="color: #8e9196">除了出价策略外,您还可以将出价提高多达900%</p>
+              <div class="gap-items">
+                <div class="gap-item">搜索结果顶部(首页)</div>
+                <el-input v-model="ruleForm2.placeBid" class="gap-item">
+                  <template #append>%</template>
+                </el-input>
+              </div>
+              <div class="gap-items">
+                <div class="gap-item">商品首页</div>
+                <el-input v-model="ruleForm2.firstPage" class="gap-item">
+                  <template #append>%</template>
+                </el-input>
+              </div>
+              <div class="gap-items">
+                <div class="gap-item">搜索结果的其余位置</div>
+                <el-input v-model="ruleForm2.other" class="gap-item">
+                  <template #append>%</template>
+                </el-input>
+              </div>
+            </el-form-item>
+          </el-form>
+          
+          <el-form-item>
+            <el-button type="primary" @click="submitForm">Create</el-button>
+            <el-button @click="resetForm(ruleFormRef)">Reset</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-card>
+  </div>
 </template>
 
 <script lang="ts" setup>
+import { onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import type { FormInstance, FormRules } from 'element-plus'
+
+const route = useRoute()
+
+// const size = ref('default')
+// const labelPosition = ref('top')
+interface RuleForm {
+  name: string
+  adMix: string
+  count: string
+  date1: string
+  date2: string
+  budget: string
+  delivery: boolean
+  type: string
+  bidStrategy: string
+}
+interface RuleForm2 {
+  placeBid: string
+  firstPage: string
+  other: string
+}
+
+const formSize = ref('default')
+const labelPosition = ref('top')
+const ruleFormRef = ref<FormInstance>()
+const ruleForm = reactive<RuleForm>({
+  name: 'Hello',
+  adMix: '',
+  count: '',
+  date1: '',
+  date2: '',
+  budget: '',
+  delivery: false,
+  type: 'auto',
+  bidStrategy: 'dynamicBid_Low',
+})
+const rules = reactive<FormRules<RuleForm>>({
+  name: [ 
+    {required: true, message: 'Please input Activity name', trigger: 'blur' },
+    {min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
+  ],
+  adMix: [ {required: false, message: 'Please select Activity zone', trigger: 'change'} ],
+  count: [ {required: true, message: 'Please select Activity count', trigger: 'change'} ],
+  date1: [ {required: true, type: 'date', message: 'Please pick a date', trigger: 'change'} ],
+  date2: [ {required: false, type: 'date', message: 'Please pick a time', trigger: 'change'} ],
+  budget: [ {required: true, message: '请输入预算', trigger: 'blur'} ],
+  type: [ {required: false, message: 'Please select at least one activity type', trigger: 'change'} ],
+  bidStrategy: [ {required: true, message: 'Please select activity resource', trigger: 'change'} ],
+})
+
+const ruleFormRef2 = ref<FormInstance>()
+const ruleForm2 = reactive<RuleForm2>({
+  placeBid: '',
+  firstPage: '',
+  other: '',
+})
+const rules2 = reactive<FormRules<RuleForm2>>({
+  placeBid: [{ required: false, pattern: /^[0-9]{1,3}$/, message: '必须是0~900之间的整数百分比', trigger: 'change' }],
+  firstPage: [{ required: false, pattern: /^[0-9]{1,3}$/, message: '必须是0~900之间的整数百分比', trigger: 'change' }],
+  other: [{ required: false, pattern: /^[0-9]{1,3}$/, message: '必须是0~900之间的整数百分比', trigger: 'change' }],
+})
+
+async function submitForm() {
+  let isValid = true
+
+  // 校验第一个表单
+  if (ruleFormRef.value) {
+    await ruleFormRef.value.validate((valid) => {
+      if (!valid) {
+        console.log('Error in form 1')
+        isValid = false
+      }
+    })
+  }
+  // 校验第二个表单
+  if (isValid && ruleFormRef2.value) {
+    await ruleFormRef2.value.validate((valid) => {
+      if (!valid) {
+        console.log('Error in form 2')
+        isValid = false
+      }
+    })
+  }
+  // 如果两个表单都通过校验,则继续后续操作
+  if (isValid) {
+    console.log('Both forms are valid. Proceed with the submission.')
+    // 这里可以放置提交表单的逻辑
+  }
+}
+
+function resetForm(formEl: FormInstance | undefined) {
+  if (!formEl) return
+  formEl.resetFields()
+}
+
+const options = Array.from({ length: 10000 }).map((_, idx) => ({
+  value: `${idx + 1}`,
+  label: `${idx + 1}`,
+}))
+
+function changeType() {
+  console.log(ruleForm.type)
+}
+
+
+onMounted(() => {
+  const myTest = route.query
+  console.log(222, myTest)
+})
+
+defineOptions({
+  name: 'SpCreateCampaigns',
+})
+
+// ::v-deep(.column-margin-bottom span.el-radio__input.is-checked) {
+//   margin-top: -20px;
+//   margin-left: -15px;
+// }
 </script>
 
 <style scoped>
+::v-deep(.el-form--default.el-form--label-top .el-form-item .el-form-item__label) {
+  font-weight: 500;
+}
+.column-item .el-radio-group {
+  display: inline-flex;
+  font-size: 0;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.radio-description {
+  font-size: 12px;
+  color: #666;
+  margin-top: -18px;
+  margin-left: 22px;
+}
+.radio-description-2 {
+  font-size: 12px;
+  color: #666;
+  margin-top: -10px;
+}
+
+.column-margin-bottom label.el-radio.is-bordered {
+  margin-bottom: 10px;
+  padding: 35px;
+}
+
+::v-deep(.column-margin-bottom label.el-radio.is-bordered span.el-radio__inner) {
+  margin-top: -18px;
+  margin-left: -15px;
+}
+
+.gap-items {
+  display: flex;
+  justify-content: flex-start;
+  width: 100%;
+  margin-bottom: 20px;
+}
+.gap-item {
+  width: 200px;
+  margin-left: 30px;
+  color: #0b0d0d;
+}
 </style>

+ 10 - 1
src/views/adManage/sp/campaigns/crud.tsx

@@ -6,6 +6,7 @@ import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
 import { dynBidStrategyEnum } from '/@/views/adManage/utils/enum.js'
 import { parseQueryParams } from '/@/views/adManage/utils/tools.js'
 import { InfoFilled } from '@element-plus/icons-vue'
+import { useRouter } from 'vue-router'
 
 export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
   const pageRequest = async (query: UserPageQuery) => {
@@ -24,6 +25,8 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
     return await api.AddObj(form)
   }
 
+  const router = useRouter()
+
   //权限判定
   const hasPermissions = inject('$hasPermissions')
 
@@ -56,7 +59,13 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
             text: '新建广告活动',
             type: 'primary',
             show: true,
-            click() {},
+            click() {
+              router.push({
+                name: 'SpCreateCampaigns',
+                query: { campaignId: 123, tagsViewName: '新建广告活动' },
+              })
+            
+            },
           },
         },
       },

+ 1 - 0
src/views/adManage/sp/campaigns/index.vue

@@ -85,6 +85,7 @@ const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context:
 const router = useRouter()
 const showCompare = ref(false)
 const jumpGroup = (row: any) => {
+  console.log(123, row)
   router.push({
     name: 'SpCampaignDetail',
     query: { campaignId: row.campaignId, tagsViewName: row.campaignName },

+ 287 - 0
src/views/adManage/utils/commonTabColumn.tsx

@@ -1052,6 +1052,293 @@ export const SdBaseColumn = {
   }
 }
 
+export const SdMatchedDeliveryColumn = {
+  ACOS: {
+    title: 'ACOS',
+    column: {
+      align: 'right',
+      width: '120px',
+      sortable: true,
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="广告投入产出比,系统计算,广告花费/广告带来的销售额。">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>ACOS</span>
+            </span>
+        )
+      }
+    }
+  },
+  ROAS: {
+    title: 'ROAS',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="广告支出回报,系统计算,广告带来的销售额/广告花费。">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>ROAS</span>
+            </span>
+        )
+      }
+    }
+  },
+  Spend: {
+    title: '花费',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      border: '0.5px solid #ddd',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top"
+                            content="来自亚马逊广告API,亚马逊系统会在3天内将无效点击从统计数据中删除,因此过去3天内的花费可能会有所变化">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>花费</span>
+            </span>
+        )
+      }
+    }
+  },
+  TotalSales: {
+    title: '销售额',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="
+        Seller类型店铺:<br />
+        销售额,来自亚马逊广告API。<br />
+        在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br/>出的广告商品及库存中其他商品的销售额;<br />
+        亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+        付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。<br />
+        <br />
+        Vendor类型店铺:<br />
+        销售额,来自亚马逊广告API。<br />
+        在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的销售额;<br />
+        亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+        付款失败的订单数量和72小时内取消的订单产生的销售额将从总销售额中删除。" raw-content>
+        <span>
+        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>销售额</span>
+            </span>
+        )
+      }
+    }
+  },
+  TotalPurchases: {
+    title: '订单数',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="
+        Seller类型店铺:<br />
+        订单数,来自亚马逊广告API。<br />
+        在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+        亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+        付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除。<br />
+        <br />
+        Vendor类型店铺:<br />
+        订单数,来自亚马逊广告API。<br />
+        在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的订单数量;<br />
+        亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+        付款失败的订单数量和72小时内取消的订单数量将从订单总数中删除" raw-content>
+        <span>
+        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>订单数</span>
+            </span>
+        )
+      }
+    }
+  },
+  TotalUnitOrdered: {
+    title: '销量',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="
+        Seller类型店铺:<br />
+        销售件数,来自亚马逊广告API。<br />
+        在点击广告后的7天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+        亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+        付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。<br />
+        <br />
+        Vendor类型店铺:<br />
+        销售件数,来自亚马逊广告API。<br />
+        在点击广告后的14天内(商品推广)、14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)售<br />出的广告商品及库存中其他商品的件数;<br />
+        亚马逊系统此项数据最多可能延迟12小时更新。因此,“今天”日期范围内的销售数据可能会延迟。<br />
+        付款失败的订单和72小时内取消的订单产生的销售件数将从销量总数中删除。" raw-content>
+        <span>
+        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>销量</span>
+            </span>
+        )
+      }
+    }
+  },
+  CPC: {
+    title: '点击成本',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '130px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="单次点击成本,系统计算,花费/点击量">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>点击成本</span>
+            </span>
+      )
+      }
+    }
+  },
+  CPA: {
+    title: '订单成本',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '130px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="平均每笔订单的花费,系统计算,花费/广告订单量">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>订单成本</span>
+            </span>
+        )
+      }
+    }
+  },
+  Impression: {
+    title: '曝光量',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="来自亚马逊广告API,广告被展示的次数。">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>曝光量</span>
+            </span>
+        )
+      },
+      formatter: (row: any) => {
+        if (row.value != null) {
+          return row.value
+        } else {
+          return '--'
+        }
+      }
+    }
+  },
+  Click: {
+    title: '点击量',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="来自亚马逊广告API,广告被点击的次数。亚马逊系统会在3天内将无效点击去除。">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>点击量</span>
+            </span>
+        )
+      }
+    }
+  },
+  CTR: {
+    title: '点击率',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '120px',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="点击率,系统计算,点击量/曝光量。">
+            <span>
+                <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>点击率</span>
+            </span>
+        )
+      }
+    }
+  },
+  DPV: {
+    title: '商品详情页浏览次数',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '200px',
+      border: '0.5px solid #ddd',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top" content="商品详情页浏览次数,来自亚马逊广告API。<br />
+        在点击广告后的14天内(品牌推广)、浏览或点击广告后的14天内(展示型推广)的商品详情页浏览次数" raw-content>
+        <span>
+        <el-icon size="14" style="display:inline-block; padding-top:2px; margin-right:3px;"><InfoFilled/></el-icon>
+            </span>
+            </el-tooltip>
+            <span>商品详情页浏览次数</span>
+            </span>
+      )
+      }
+    }
+  }
+}
+
 export function createMul(mul: any) {
   return function(number: any) {
     return number * mul