Pārlūkot izejas kodu

Merge branch 'wang' into test

# Conflicts:
#	src/views/adManage/sp/index.vue
WanGxC 1 gadu atpakaļ
vecāks
revīzija
a2cf16b646

+ 9 - 10
src/views/adManage/sp/campaigns/chartComponents/adStruct.vue

@@ -1,20 +1,20 @@
 <template>
     <div v-loading="loading">
         <el-row :gutter="5">
-            <el-col :span="8">
+            <el-col :span="9">
                 <div>
                     <TextSelector v-model="modelValue" :options="pieOptions" @change="changePie" style="margin-top: 5px"/>
                 </div>
-                <div ref="pie" style="height: 500px;"></div>
+                <div ref="pie" style="height: 400px;"></div>
             </el-col>
-            <el-col :span="16">
+            <el-col :span="15">
                 <div style="margin-left: 40%">
                     <span style="background: #3a83f7; width: 18px; height: 10px; margin-top: 8px; display: inline-block; border-radius: 3px;"></span>
                     <TextSelector v-model="barModelValue1" :options="barOptions1" @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="barOptions2" @change="changeBarTwo" style="margin-top: 5px; margin-left: 8px;"/>
                 </div>
-                <div ref="bar" style="height: 500px;"></div>
+                <div ref="bar" style="height: 400px;"></div>
             </el-col>
         </el-row>
     </div>
@@ -322,7 +322,6 @@ function changeBarOne(newValue) {
             const value = barData[i][barFlag.value]
             barValues.push(value)
         }
-        // barChart.clear()
         option.series[0].data = barValues
         barChart.setOption(option)
     } catch (error) {
@@ -339,7 +338,6 @@ function changeBarTwo(newValue) {
             const value = barData[i][barFlag2.value]
             barValues.push(value)
         }
-        // barChart.clear()
         option.series[1].data = barValues
         barChart.setOption(option)
     } catch (error) {
@@ -354,7 +352,7 @@ function initChart() {
         tooltip: {
             trigger: 'axis',
             axisPointer: {
-                type: 'cross',
+                type: 'shadow',
                 label: {
                     backgroundColor: '#6a7985'
                 }
@@ -376,8 +374,8 @@ function initChart() {
                 boundaryGap: true,
                 data: classificationMapList,
                 axisLabel: {
-                    rotate: 30, // 将标签旋转45度
-                    fontSize: 12
+                    rotate: -30, // 将标签旋转
+                    fontSize: 13
                 },
             },
         ],
@@ -449,13 +447,14 @@ function initChart() {
     // 饼图配置
     option2 = {
         tooltip: {
+            show: false,
             trigger: 'item',
         },
         series: [
             {
                 // name: modelValue.value,
                 type: 'pie',
-                radius: ['20%', '40%'],
+                radius: ['20%', '45%'],
                 avoidLabelOverlap: false,
                 itemStyle: {
                     // borderRadius: 10,

+ 4 - 4
src/views/adManage/sp/campaigns/chartComponents/dataTendency.vue

@@ -129,7 +129,7 @@ const option: any = {
       yAxisIndex: 0,
       itemStyle: {
         color: '#0085ff',
-        borderRadius: [10, 10, 0, 0],
+        borderRadius: [6, 6, 6, 6],
       }
     },
     {
@@ -206,8 +206,8 @@ const getMetricsItems = async () => {
 	const data = resp.data
   metricsItems.value.length = 0
 	XEUtils.arrayEach(spCampaignMetricsEnum, info => {
-		const tmp:MetricData = { 
-			label: info.label, 
+		const tmp:MetricData = {
+			label: info.label,
 			value: info.value,
 			metricVal: data[info.value],
 			gapVal: data[`gap${info.value}`],
@@ -251,4 +251,4 @@ defineExpose({resizeChart})
   gap: 12px;
   width: 100%;
 }
-</style>
+</style>

+ 11 - 6
src/views/adManage/sp/index.vue

@@ -16,7 +16,9 @@
 			<el-tab-pane label="关键词投放" name="keywords" :lazy="true">
 				<Keywords ref="keywordsRef" />
 			</el-tab-pane>
-			<el-tab-pane label="商品投放" :lazy="true"></el-tab-pane>
+            <el-tab-pane label="商品投放" name="targets" :lazy="true">
+                <Targets ref="targetsRef"/>
+            </el-tab-pane>
 			<el-tab-pane label="推广商品" :lazy="true"></el-tab-pane>
 			<el-tab-pane label="购买的其它商品" :lazy="true"></el-tab-pane>
 			<el-tab-pane label="搜索词" :lazy="true">
@@ -39,6 +41,7 @@ import { usePublicData } from '/@/stores/publicData'
 import { GetList } from '/@/views/adManage/portfolios/api'
 import { nextTick } from 'process'
 import { storeToRefs } from 'pinia'
+import Targets from './targets/index.vue'
 
 const shopInfo = useShopInfo()
 const publicData = usePublicData()
@@ -46,8 +49,9 @@ const selectedPortfolios: Ref<string[]> = ref([])
 const portfolios: Ref<Portfolio[]> = ref([])
 const { dateRange } = storeToRefs(publicData)
 const tabActiveName = ref('campaigns')
-const keywordsRef = ref()
 const campaignsRef = ref()
+const keywordsRef = ref()
+const targetsRef = ref()
 
 provide('dateRange', dateRange)
 
@@ -56,10 +60,11 @@ onBeforeMount(async () => {
 	portfolios.value = resp.data
 })
 const changeTab = () => {
-	nextTick(() => {
-		campaignsRef.value.resizeTabChart()
-		keywordsRef.value.resizeTabChart()
-	})
+    nextTick(() => {
+        campaignsRef.value.resizeTabChart()
+        keywordsRef.value.resizeTabChart()
+        targetsRef.value.resizeTabChart()
+    })
 }
 publicData.$subscribe((mutation, state) => {
 	// console.log(mutation)

+ 3 - 3
src/views/adManage/sp/keywords/chartComponents/adStruct.vue

@@ -289,7 +289,7 @@ function initChart() {
         tooltip: {
             trigger: 'axis',
             axisPointer: {
-                type: 'cross',
+                type: 'shadow',
                 label: {
                     backgroundColor: '#6a7985'
                 }
@@ -351,7 +351,7 @@ function initChart() {
             {
                 // name: '数据1',
                 type: 'bar',
-                barWidth: '5%',
+                barWidth: '3%',
                 data: ACOSList,
                 yAxisIndex: 0,
                 itemStyle: {
@@ -366,7 +366,7 @@ function initChart() {
             {
                 // name: '数据2',
                 type: 'bar',
-                barWidth: '5%',
+                barWidth: '3%',
                 data: SpendList,
                 yAxisIndex: 1,
                 itemStyle: {

+ 4 - 4
src/views/adManage/sp/keywords/chartComponents/dataTendency.vue

@@ -126,7 +126,7 @@ const option: any = {
       yAxisIndex: 0,
       itemStyle: {
         color: '#0085ff',
-        borderRadius: [10, 10, 0, 0],
+        borderRadius: [6, 6, 6, 6],
       }
     },
     {
@@ -195,8 +195,8 @@ const getMetricsItems = async () => {
 	const resp = await getCardData({start: '2023-11-01', end: '2023-11-04', profile: shopInfo.profile.profile_id})
 	const data = resp.data
 	XEUtils.arrayEach(spCampaignMetricsEnum, info => {
-		const tmp:MetricData = { 
-			label: info.label, 
+		const tmp:MetricData = {
+			label: info.label,
 			value: info.value,
 			metricVal: data[info.value],
 			gapVal: data[`gap${info.value}`],
@@ -230,4 +230,4 @@ defineExpose({resizeChart})
   gap: 12px;
   width: 100%;
 }
-</style>
+</style>

+ 34 - 33
src/views/adManage/sp/keywords/index.vue

@@ -1,36 +1,36 @@
 <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 ref="dataTendencyRef"/>
-					</el-tab-pane>
-					<el-tab-pane label="广告结构" name="adStruct" :lazy="true">
-						 <AdStructChart ref="adStructChartRef" />
-					</el-tab-pane>
-					<el-tab-pane label="散点视图" name="scatterView" :lazy="true"></el-tab-pane>
-				</el-tabs>
-			</template>
-		</fs-crud>
-	</fs-page>
+    <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 ref="dataTendencyRef"/>
+                    </el-tab-pane>
+                    <el-tab-pane label="广告结构" name="adStruct" :lazy="true">
+                        <AdStructChart ref="adStructChartRef"/>
+                    </el-tab-pane>
+                    <el-tab-pane label="散点视图" name="scatterView" :lazy="true"></el-tab-pane>
+                </el-tabs>
+            </template>
+        </fs-crud>
+    </fs-page>
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted, onBeforeUnmount, watch,nextTick, onActivated, inject } from 'vue';
-import { useFs, FsPage } from '@fast-crud/fast-crud';
-import { createCrudOptions } from './crud';
-import { useRoute, useRouter } from 'vue-router'
+import {ref, onMounted, onBeforeUnmount, watch, nextTick, onActivated, inject} from 'vue'
+import {useFs, FsPage} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useRoute, useRouter} from 'vue-router'
 import DataTendencyChart from './chartComponents/dataTendency.vue'
 import {useShopInfo} from '/@/stores/shopInfo'
 import AdStructChart from './chartComponents/adStruct.vue'
 
-const tabActiveName = ref("dataTendency")
+const tabActiveName = ref('dataTendency')
 const shopInfo = useShopInfo()
 const dateRange = inject('dateRange')
 const start = dateRange.value[0]
 const end = dateRange.value[1]
-const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { profileId: shopInfo.profile.profile_id, start: start, end: end } });
+const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, context: {profileId: shopInfo.profile.profile_id, start: start, end: end}})
 
 const route = useRoute()
 const router = useRouter()
@@ -40,30 +40,31 @@ const dataTendencyRef = ref()
 console.log(111, dateRange.value)
 
 onMounted(() => {
-	crudExpose.doRefresh();
+    crudExpose.doRefresh()
 })
 
 const resizeTabChart = () => {
-	if (tabActiveName.value === "dataTendency") {
+    if (tabActiveName.value === 'dataTendency') {
         dataTendencyRef.value.resizeChart()
-	} else if (tabActiveName.value === "adStruct"){
+    } else if (tabActiveName.value === 'adStruct') {
         adStructChartRef.value.resizeChart()
-	}
+    }
 }
 const changeTab = () => {
-	nextTick(() => {
-		resizeTabChart()
-	})
+    nextTick(() => {
+        resizeTabChart()
+    })
 }
-defineExpose({ resizeTabChart })
+defineExpose({resizeTabChart})
 </script>
 
 <style lang="scss">
 .chart-tabs {
-	margin: 5px 0;
-	.el-tabs__nav {
-		padding-left: 0 !important;
-	}
+    margin: 5px 0;
+
+    .el-tabs__nav {
+        padding-left: 0 !important;
+    }
 }
 
 </style>

+ 66 - 0
src/views/adManage/sp/targets/api.ts

@@ -0,0 +1,66 @@
+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/sptargets/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix,
+        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 + "total/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "daily/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getAdStructureData(query) {
+    return request({
+        url: apiPrefix + "structure/",
+        method: 'GET',
+        params: query
+    })
+}

+ 400 - 0
src/views/adManage/sp/targets/chartComponents/adStruct.vue

@@ -0,0 +1,400 @@
+<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 } 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'
+
+let barChart = ref()
+const bar = ref()
+const loading = ref(true)
+
+let dateRange = inject('dateRange')
+
+// 下拉框相关
+const barOptions1 = [
+    {
+        value: 'ACOS',
+        label: 'ACOS',
+    },
+    {
+        value: 'ROAS',
+        label: 'ROAS',
+    },
+    {
+        value: 'Spend',
+        label: '花费',
+        units: '$',
+    },
+    {
+        value: 'TotalSales',
+        label: '销售额',
+    },
+    {
+        value: 'TotalPurchases',
+        label: '订单数',
+    },
+    {
+        value: 'TotalUnitOrdered',
+        label: '销量',
+    },
+    {
+        value: 'CPC',
+        label: '点击成本'
+    },
+    {
+        value: 'CPA',
+        label: '订单成本'
+    },
+    {
+        value: 'Impression',
+        label: '曝光量',
+    },
+    {
+        value: 'Click',
+        label: '点击量',
+    },
+    {
+        value: 'qwe',
+        label: '点击率'
+    },
+    {
+        value: '转化率',
+        label: '转化率'
+    },
+    {
+        value: 'TotalSalesSameSKU',
+        label: '推广商品销售额'
+    },
+    {
+        value: 'TotalSalesOtherSKU',
+        label: '其他商品销售额'
+    },
+    {
+        value: 'TotalPurchasesSameSKU',
+        label: '推广商品订单数'
+    },
+    {
+        value: 'TotalPurchasesOtherSKU',
+        label: '其他商品订单数'
+    },
+    {
+        value: 'TotalUnitOrderedSameSKU',
+        label: '推广商品销量'
+    },
+    {
+        value: 'TotalUnitOrderedOtherSKU',
+        label: '其他商品销量'
+    },
+    {
+        value: 'TopOfSearchImpressionShare',
+        label: '搜索结果顶部展示份额'
+    },
+]
+let barModelValue1 = ref(barOptions1[0].value)
+
+const barOptions2 = [
+    {
+        value: 'ACOS',
+        label: 'ACOS',
+    },
+    {
+        value: 'ROAS',
+        label: 'ROAS',
+    },
+    {
+        value: 'Spend',
+        label: '花费',
+        units: '$',
+    },
+    {
+        value: 'TotalSales',
+        label: '销售额',
+    },
+    {
+        value: 'TotalPurchases',
+        label: '订单数',
+    },
+    {
+        value: 'TotalUnitOrdered',
+        label: '销量',
+    },
+    {
+        value: 'CPC',
+        label: '点击成本'
+    },
+    {
+        value: 'CPA',
+        label: '订单成本'
+    },
+    {
+        value: 'Impression',
+        label: '曝光量',
+    },
+    {
+        value: 'Click',
+        label: '点击量',
+    },
+    {
+        value: 'qwe',
+        label: '点击率'
+    },
+    {
+        value: '转化率',
+        label: '转化率'
+    },
+    {
+        value: 'TotalSalesSameSKU',
+        label: '推广商品销售额'
+    },
+    {
+        value: 'TotalSalesOtherSKU',
+        label: '其他商品销售额'
+    },
+    {
+        value: 'TotalPurchasesSameSKU',
+        label: '推广商品订单数'
+    },
+    {
+        value: 'TotalPurchasesOtherSKU',
+        label: '其他商品订单数'
+    },
+    {
+        value: 'TotalUnitOrderedSameSKU',
+        label: '推广商品销量'
+    },
+    {
+        value: 'TotalUnitOrderedOtherSKU',
+        label: '其他商品销量'
+    },
+    {
+        value: 'TopOfSearchImpressionShare',
+        label: '搜索结果顶部展示份额'
+    },
+]
+let barModelValue2 = ref(barOptions2[2].value)
+
+onMounted(async () => {
+    barChart = echarts.init(bar.value)
+
+    window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+    setTimeout(() => {
+        resizeChart()
+    }, 0)
+
+    await initBarData()
+    initChart()
+})
+
+// 获取总数据
+let resp = null
+
+async function setAdStructureData() {
+    resp = await getAdStructureData({ start: dateRange.value[0], end: dateRange.value[1], profile: '3006125408623189' })
+    console.log('resp.data', resp.data)
+    return resp.data
+}
+
+// 饼图总数据和柱状图总数据
+let barData = null
+let allData = null
+// 柱状图初始数据
+let ACOSList
+let SpendList
+let classificationList
+let classificationMapList
+
+async function initBarData() {
+    allData = await setAdStructureData()
+    barData = allData
+    console.log('barData', barData)
+    // 柱状图初始化数据
+    ACOSList = barData.map(item => item.ACOS)
+    SpendList = barData.map(item => item.Spend)
+    // 将x轴映射为中文
+    classificationList = barData.map(item => item.Classification)
+    const classificationMap = {
+        'category': '品类',
+        'asin': '商品',
+    }
+    classificationMapList = classificationList.map(item => classificationMap[item])
+    loading.value = false
+}
+
+// 重置图像
+let barFlag = ref()
+let barFlag2 = ref()
+let option
+let option2
+
+function changeBarOne(newValue) {
+    barModelValue1.value = newValue
+    barFlag.value = barModelValue1.value
+    console.log(111, barModelValue1.value)
+    console.log(222, barModelValue2.value)
+    const barValues = []
+    try {
+        for (let i = 0; i < barData.length; i++) {
+            const value = barData[i][barFlag.value]
+            barValues.push(value)
+        }
+        option.series[0].data = barValues
+        barChart.setOption(option)
+    } catch (error) {
+        console.log(error)
+    }
+}
+
+function changeBarTwo(newValue) {
+    barModelValue2.value = newValue
+    barFlag2.value = barModelValue2.value
+    const barValues = []
+    try {
+        for (let i = 0; i < barData.length; i++) {
+            const value = barData[i][barFlag2.value]
+            barValues.push(value)
+        }
+        // barChart.clear()
+        option.series[1].data = barValues
+        barChart.setOption(option)
+    } catch (error) {
+        console.log(error)
+    }
+}
+
+// 判断barOptions哪个选项应该被禁用
+const computedBarOptions1 = computed(() => createDisabledOptions(barOptions1, barModelValue2.value))
+const computedBarOptions2 = computed(() => createDisabledOptions(barOptions2, barModelValue1.value))
+
+// const computedBarOptions2 = computed(() => {
+//     return barOptions2.map(option => ({
+//         ...option,
+//         disabled: barModelValue1.value === option.value
+//     }))
+// })
+
+// 初始化图表
+function initChart() {
+    // 柱状图配置
+    option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'shadow',
+                label: {
+                    backgroundColor: '#6a7985'
+                }
+            }
+        },
+        toolbox: {
+            feature: {
+                saveAsImage: { yAxisIndex: 'none' }
+            }
+        },
+        grid: {
+            top: 50, right: 60, bottom: 50, left: 60,
+
+        },
+        xAxis: [
+            {
+                type: 'category',
+                boundaryGap: true,
+                data: classificationMapList,
+                // axisLabel: {
+                //     rotate: 30, // 将标签旋转45度
+                //     fontSize: 12
+                // },
+            },
+        ],
+        yAxis: [
+            {
+                type: 'value',
+                // name: '数据1',
+                axisLine: {
+                    show: true,
+                    lineStyle: {
+                        color: '#3a83f7' // 第一个 Y 轴的颜色
+                    }
+                }
+            },
+            {
+                type: 'value',
+                // name: '数据2',
+                splitLine: {
+                    show: false
+                },
+                // axisLabel: {
+                //     formatter: '{value} 单位2'
+                // },
+                axisLine: {
+                    show: true,
+                    lineStyle: {
+                        color: '#f19a37' // 第一个 Y 轴的颜色
+                    }
+                }
+            }
+        ],
+        series: [
+            {
+                // name: '数据1',
+                type: 'bar',
+                barWidth: '3%',
+                data: ACOSList,
+                yAxisIndex: 0,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        { offset: 0, color: '#3a83f7' },
+                        { offset: 1, color: 'rgb(111, 209, 206)' },
+                    ]),
+                    // 柱状图圆角
+                    borderRadius: [6, 6, 6, 6],
+                },
+            },
+            {
+                // name: '数据2',
+                type: 'bar',
+                barWidth: '3%',
+                data: SpendList,
+                yAxisIndex: 1,
+                itemStyle: {
+                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                        { offset: 0, color: '#f19a37' },
+                        { 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>

+ 233 - 0
src/views/adManage/sp/targets/chartComponents/dataTendency.vue

@@ -0,0 +1,233 @@
+<template>
+    <MetricsCards v-model="metrics" :metric-items="metricsItems" @change="changeMetric"></MetricsCards>
+    <div style="height: 350px;" ref="chartRef"></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'
+
+defineOptions({
+    name: "DataTendencyChart"
+})
+
+onBeforeMount(async () => {
+    await getMetricsItems()
+})
+onMounted(() => {
+    initLine()
+    addResize()
+});
+onBeforeUnmount(() => {
+    if(chartObj) {
+        chartObj.dispose()
+        chartObj = null
+    }
+    removeResize()
+})
+
+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 getDataset = async () => {
+    const resp = await getLineData({profile: shopInfo.profile.profile_id, start: '2023-11-01', end: '2023-11-04'})
+    return resp.data
+}
+const initLine = async () => {
+    chartObj = echarts.init(chartRef.value)
+    const items = await getDataset()
+    option.dataset.source = items
+    for(const info of metrics.value) {
+        option.legend.selected[info.label] = true
+    }
+    chartObj.setOption(option)
+}
+
+const getMetricsItems = async () => {
+    const resp = await getCardData({start: '2023-11-01', end: '2023-11-04', profile: shopInfo.profile.profile_id})
+    const data = resp.data
+    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}`],
+            disabled: info.initShow ? true : false
+        }
+        metricsItems.value.push(tmp)
+        option.legend.selected[info.label] = false
+    })
+}
+
+const changeMetric = () => {
+    const opt = buildChartOpt(option, metrics.value)
+    chartObj.setOption(opt)
+}
+
+const resizeChart = () => { chartObj.resize() }
+const addResize = () => { window.addEventListener('resize', resizeChart) }
+const removeResize = () => { window.removeEventListener('resize', resizeChart) }
+
+
+defineExpose({resizeChart})
+
+</script>
+
+<style scoped>
+.metrics-cards {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    gap: 12px;
+    width: 100%;
+}
+</style>

+ 175 - 0
src/views/adManage/sp/targets/crud.tsx

@@ -0,0 +1,175 @@
+import * as api from './api'
+import {dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {dynBidStrategyEnum} from '/@/views/adManage/utils/enum.js'
+
+
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+    const pageRequest = async (query: UserPageQuery) => {
+        query['profile'] = context['profileId']
+        query['start'] = context['start']
+        query['end'] = context['end']
+        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
+            },
+            container: {
+                fixedHeight: false
+            },
+            actionbar: {
+                show: true,
+                buttons: {
+                    add: {
+                        show: false
+                    },
+                    create: {
+                        text: '新建广告活动',
+                        type: 'primary',
+                        show: true,
+                        click() {
+
+                        }
+                    }
+                }
+            },
+            search: {
+                show: false
+            },
+            toolbar: {
+                buttons: {
+                    search: {
+                        show: true
+                    },
+                    compact: {
+                        show: false
+                    }
+                }
+            },
+            request: {
+                pageRequest,
+                addRequest,
+                editRequest,
+                delRequest,
+            },
+            rowHandle: {
+                fixed: 'right',
+                width: 80,
+                buttons: {
+                    view: {
+                        show: false,
+                    },
+                    edit: {
+                        iconRight: 'Edit',
+                        type: 'text',
+                        text: null
+                        // show: hasPermissions('dictionary:Update'),
+                    },
+                    remove: {
+                        iconRight: 'Delete',
+                        type: 'text',
+                        text: null
+                        // show: hasPermissions('dictionary:Delete'),
+                    },
+                },
+            },
+            columns: {
+                id: {
+                    title: 'ID',
+                    column: {
+                        show: false
+                    },
+                    form: {
+                        show: false
+                    }
+                },
+                keywordText: {
+                    title: '关键词',
+                    column: {
+                        width: '200px',
+                        fixed: 'left'
+                    },
+                    search: {
+                        show: true,
+                        component: {
+                            props: {
+                                clearable: true
+                            }
+                        }
+                    },
+                },
+                state: {
+                    title: '状态'
+                },
+                campaignName: {
+                    title: '广告活动名称',
+                    column: {
+                        width: '200px',
+                        fixed: 'left'
+                    },
+                    search: {
+                        show: true,
+                        component: {
+                            props: {
+                                clearable: true
+                            }
+                        }
+                    },
+                    form: {
+                        rules: [{required: true, message: '必填项'}]
+                    }
+                },
+                adGroupName: {
+                    title: '广告组名称',
+                },
+                suggestedBudget: {
+                    title: '建议竞价',
+                    form: {
+                        show: false
+                    }
+                },
+                bid: {title: '出价'},
+                '标签': {},
+                Impression: {
+                    title: '曝光量'
+                },
+                '搜索结果顶部展示份额': {},
+                Click: {
+                    title: '点击量'
+                },
+                CTR: {
+                    title: '点击率'
+                },
+                Spend: {
+                    title: '花费'
+                },
+                CPC: {title: '点击成本'},
+                TotalPurchases: {title: '订单数'},
+                TotalSales: {title: '销售额'},
+                TotalUnitOrdered: {title: '销量'},
+                PurchasesRate: {title: '转化率'},
+                ACOS: {title: 'ACOS'},
+                ROAS: {title: 'ROAS'},
+                CPA: {title: '订单成本'},
+                ...BaseColumn
+            }
+        }
+    }
+}

+ 70 - 0
src/views/adManage/sp/targets/index.vue

@@ -0,0 +1,70 @@
+<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 ref="dataTendencyRef"/>
+                    </el-tab-pane>
+                    <el-tab-pane label="广告结构" name="adStruct" :lazy="true">
+                        <AdStructChart ref="adStructChartRef"/>
+                    </el-tab-pane>
+                    <el-tab-pane label="散点视图" name="scatterView" :lazy="true"></el-tab-pane>
+                </el-tabs>
+            </template>
+        </fs-crud>
+    </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {ref, onMounted, nextTick, inject} from 'vue'
+import {useFs, FsPage} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useRoute, useRouter} from 'vue-router'
+import DataTendencyChart from './chartComponents/dataTendency.vue'
+import {useShopInfo} from '/@/stores/shopInfo'
+import AdStructChart from './chartComponents/adStruct.vue'
+
+const tabActiveName = ref('dataTendency')
+const shopInfo = useShopInfo()
+const dateRange = inject('dateRange')
+const start = dateRange.value[0]
+const end = dateRange.value[1]
+const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, context: {profileId: shopInfo.profile.profile_id, start: start, end: end}})
+
+const route = useRoute()
+const router = useRouter()
+const adStructChartRef = ref()
+const dataTendencyRef = ref()
+
+console.log(111, dateRange.value)
+
+onMounted(() => {
+    crudExpose.doRefresh()
+})
+
+const resizeTabChart = () => {
+    if (tabActiveName.value === 'dataTendency') {
+        dataTendencyRef.value.resizeChart()
+    } else if (tabActiveName.value === 'adStruct') {
+        adStructChartRef.value.resizeChart()
+    }
+}
+const changeTab = () => {
+    nextTick(() => {
+        resizeTabChart()
+    })
+}
+defineExpose({resizeTabChart})
+</script>
+
+<style lang="scss">
+.chart-tabs {
+    margin: 5px 0;
+
+    .el-tabs__nav {
+        padding-left: 0 !important;
+    }
+}
+
+</style>

+ 6 - 0
src/views/adManage/sp/targets/utils/dropdowndisable.js

@@ -0,0 +1,6 @@
+export function createDisabledOptions(options, disabledValue) {
+    return options.map(option => ({
+        ...option,
+        disabled: option.value === disabledValue
+    }))
+}