Przeglądaj źródła

新增广告位,优化样式

WanGxC 1 rok temu
rodzic
commit
d5776a2430

+ 17 - 9
src/views/adManage/sp/advertisedProducts/crud.tsx

@@ -40,14 +40,6 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                     add: {
                         show: false
                     },
-                    create: {
-                        text: '新建广告活动',
-                        type: 'primary',
-                        show: true,
-                        click() {
-
-                        }
-                    }
                 }
             },
             search: {
@@ -105,7 +97,23 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                         align: 'center',
                         fixed: 'left'
                     },},
-                state: {title: '状态'},
+                state: {
+                    title: '状态',
+                    column: {
+                        width: '90px',
+                        align: 'center'
+                    },
+                    type: 'dict-select',
+                    search: {
+                        show: true
+                    },
+                    dict: dict({
+                        data: [
+                            {value: 'PAUSED', label: '已暂停', color: 'warning'},
+                            {value: 'ENABLED', label: '投放中', color: 'success'},
+                        ]
+                    })
+                },
                 campaignName: {
                     title: '广告活动名称',
                     column: {

+ 223 - 205
src/views/adManage/sp/campaigns/crud.tsx

@@ -1,212 +1,230 @@
 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'
+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"]
-		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);
-	};
+export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
+    const pageRequest = async (query: UserPageQuery) => {
+        query['profile'] = context['profileId']
+        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');
+    //权限判定
+    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() {
+    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
-					}
-				},
-        campaignName: {
-          title: '广告活动',
-          column: {
-            width: '200px',
-						fixed: 'left'
-          },
-					search: {
-						show: true,
-						component: {
-							props: {
-								clearable: true
-							}
-						}
-					},
-					form: {
-						rules: [{required: true, message:'必填项'}]
-					}
-        },
-				targetingType: {
-					title: '投放类型',
-					type: 'dict-select',
-					search: {
-						show: true
-					},
-					dict: dict({
-						data: [
-							{ value: 'AUTO', label: '自动' },
-							{ value: 'MANUAL', label: '手动' },
-						]
-					})
-				},
-				state: {
-					title: '状态'
-				},
-				dynBidStrategy: {
-					title: '竞价策略',
-					form: {
-						show: false,
-					},
-					column: {
-						width: '160px'
-					},
-					type: 'dict-select',
-					dict: dict({
-						data: dynBidStrategyEnum
-					})
-				},
-				startDate: {
-					title: '开始日期',
-					column: {
-            width: '100px'
-          },
-				},
-				endDate: {
-					title: '结束日期',
-					column: {
-            width: '100px'
-          },
-				},
-				budget: {
-					title: '预算'
-				},
-				portfolioName: {
-					title: '广告组合'
-				},
-				suggestedBudget: {
-					title: '建议竞价',
-					form: {
-						show: false
-					}
-				},
-				percentTimeInBudget: {
-					title: '预算活跃均值',
-					column:{
-						minWidth: 150
-					},
-					form: {
-						show: false
-					}
-				},
-				MissedImpressions: {
-					title: '预计错过的曝光',
-					form: {
-						show: false
-					},
-					column:{
-						width: 180
-					},
-				},
-				MissedClicks: {
-					title: '预计错过的点击',
-					form: {
-						show: false
-					},
-					column:{
-						width: 180
-					},
-				},
-				MissedSales: {
-					title: '预计错过的销售',
-					form: {
-						show: false
-					},
-					column:{
-						width: 180
-					},
-				},
-        ...BaseColumn
-			}
-		}
-	}
+                        }
+                    }
+                }
+            },
+            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
+                    }
+                },
+                campaignName: {
+                    title: '广告活动',
+                    column: {
+                        width: '200px',
+                        fixed: 'left'
+                    },
+                    search: {
+                        show: true,
+                        component: {
+                            props: {
+                                clearable: true
+                            }
+                        }
+                    },
+                    form: {
+                        rules: [{required: true, message: '必填项'}]
+                    }
+                },
+                targetingType: {
+                    title: '投放类型',
+                    column: {
+                        width: '110px',
+                        align: 'center'
+                    },
+                    type: 'dict-select',
+                    search: {
+                        show: true
+                    },
+                    dict: dict({
+                        data: [
+                            {value: 'AUTO', label: '自动'},
+                            {value: 'MANUAL', label: '手动'},
+                        ]
+                    })
+                },
+                state: {
+                    title: '状态',
+                    column: {
+                        width: '90px',
+                        align: 'center'
+                    },
+                    type: 'dict-select',
+                    search: {
+                        show: true
+                    },
+                    dict: dict({
+                        data: [
+                            {value: 'PAUSED', label: '已暂停', color: 'warning'},
+                            {value: 'ENABLED', label: '投放中', color: 'success'},
+                        ]
+                    })
+                },
+                dynBidStrategy: {
+                    title: '竞价策略',
+                    form: {
+                        show: false,
+                    },
+                    column: {
+                        width: '160px'
+                    },
+                    type: 'dict-select',
+                    dict: dict({
+                        data: dynBidStrategyEnum
+                    })
+                },
+                startDate: {
+                    title: '开始日期',
+                    column: {
+                        width: '100px'
+                    },
+                },
+                endDate: {
+                    title: '结束日期',
+                    column: {
+                        width: '100px'
+                    },
+                },
+                budget: {
+                    title: '预算'
+                },
+                portfolioName: {
+                    title: '广告组合'
+                },
+                suggestedBudget: {
+                    title: '建议竞价',
+                    form: {
+                        show: false
+                    }
+                },
+                percentTimeInBudget: {
+                    title: '预算活跃均值',
+                    column: {
+                        minWidth: 150
+                    },
+                    form: {
+                        show: false
+                    }
+                },
+                MissedImpressions: {
+                    title: '预计错过的曝光',
+                    form: {
+                        show: false
+                    },
+                    column: {
+                        width: 180
+                    },
+                },
+                MissedClicks: {
+                    title: '预计错过的点击',
+                    form: {
+                        show: false
+                    },
+                    column: {
+                        width: 180
+                    },
+                },
+                MissedSales: {
+                    title: '预计错过的销售',
+                    form: {
+                        show: false
+                    },
+                    column: {
+                        width: 180
+                    },
+                },
+                ...BaseColumn
+            }
+        }
+    }
 }

+ 44 - 44
src/views/adManage/sp/campaigns/index.vue

@@ -1,61 +1,61 @@
 <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">
-					<el-tab-pane label="数据趋势" name="dataTendency">
-						<DataTendencyChart v-if="tabActiveName === 'dataTendency'"></DataTendencyChart>
-					</el-tab-pane>
-					<el-tab-pane label="广告结构" name="adStruct" >
-						<AdStructChart v-if="tabActiveName === 'adStruct'"/>
-					</el-tab-pane>
-					<el-tab-pane label="散点视图" name="scatterView">
-						<div v-if="tabActiveName === 'adStruct'">散点视图</div>
-					</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_campaignName="scope">
-        <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
-      </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>
-		</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">
+                    <el-tab-pane label="数据趋势" name="dataTendency">
+                        <DataTendencyChart v-if="tabActiveName === 'dataTendency'"></DataTendencyChart>
+                    </el-tab-pane>
+                    <el-tab-pane label="广告结构" name="adStruct">
+                        <AdStructChart v-if="tabActiveName === 'adStruct'"/>
+                    </el-tab-pane>
+                    <el-tab-pane label="散点视图" name="scatterView">
+                        <div v-if="tabActiveName === 'adStruct'">散点视图</div>
+                    </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_campaignName="scope">
+                <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+            </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>
+        </fs-crud>
+    </fs-page>
 </template>
 
 <script lang="ts" setup>
-import {ref, onMounted, Ref, nextTick, onBeforeMount} from 'vue'
-import { useFs, FsPage } from '@fast-crud/fast-crud';
-import { createCrudOptions } from './crud';
-import { useShopInfo } from '/@/stores/shopInfo'
-import { useRoute, useRouter } from 'vue-router'
+import {onMounted, ref} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {useRouter} from 'vue-router'
 import AdStructChart from './chartComponents/adStruct.vue'
 import DataTendencyChart from './chartComponents/dataTendency.vue'
 
-const tabActiveName = ref("dataTendency")
+const tabActiveName = ref('dataTendency')
 const shopInfo = useShopInfo()
-const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { profileId: shopInfo.profile.profile_id } })
+const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, context: {profileId: shopInfo.profile.profile_id}})
 const router = useRouter()
 
 onMounted(async () => {
-	crudExpose.doRefresh()
+    crudExpose.doRefresh()
 })
 
 const jumpGroup = (row: any) => {
-	router.push({
-		name: 'CampaignDetail',
-		query: { id: row.id, campaignId: row.campaignId, tagsViewName: row.campaignName },
-	})
+    router.push({
+        name: 'CampaignDetail',
+        query: {id: row.id, campaignId: row.campaignId, tagsViewName: row.campaignName},
+    })
 }
 </script>
 

+ 19 - 11
src/views/adManage/sp/keywords/crud.tsx

@@ -40,14 +40,6 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                     add: {
                         show: false
                     },
-                    create: {
-                        text: '新建广告活动',
-                        type: 'primary',
-                        show: true,
-                        click() {
-
-                        }
-                    }
                 }
             },
             search: {
@@ -104,7 +96,8 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                     title: '关键词',
                     column: {
                         width: '200px',
-                        fixed: 'left'
+                        fixed: 'left',
+                        align: 'center'
                     },
                     search: {
                         show: true,
@@ -116,13 +109,28 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                     },
                 },
                 state: {
-                    title: '状态'
+                    title: '状态',
+                    column: {
+                        width: '90px',
+                        align: 'center'
+                    },
+                    type: 'dict-select',
+                    search: {
+                        show: true
+                    },
+                    dict: dict({
+                        data: [
+                            {value: 'PAUSED', label: '已暂停', color: 'warning'},
+                            {value: 'ENABLED', label: '投放中', color: 'success'},
+                        ]
+                    })
                 },
                 campaignName: {
                     title: '广告活动名称',
                     column: {
                         width: '200px',
-                        fixed: 'left'
+                        fixed: 'left',
+                        align: 'center'
                     },
                     search: {
                         show: true,

+ 65 - 0
src/views/adManage/sp/placement/api.ts

@@ -0,0 +1,65 @@
+import {request} from '/@/utils/service'
+import {AddReq, DelReq, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+
+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
+    })
+}

+ 393 - 0
src/views/adManage/sp/placement/chartComponents/adStruct.vue

@@ -0,0 +1,393 @@
+<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 { 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')
+
+// 下拉框相关
+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 allData = null
+
+async function setAdStructureData() {
+    allData = await getAdStructureData({ start: dateRange.value[0], end: dateRange.value[1], profile: 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: 1, color: 'rgb(111, 209, 206)' },
+                    ]),
+                    // 柱状图圆角
+                    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: 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/sp/placement/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>

+ 160 - 0
src/views/adManage/sp/placement/crud.tsx

@@ -0,0 +1,160 @@
+import * as api from './api'
+import {AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.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
+                    }
+                },
+                campaignName: {
+                    title: '广告活动',
+                    column: {
+                        width: '200px',
+                        fixed: 'left',
+                        align: 'center',
+                    },
+                    search: {
+                        show: true,
+                        component: {
+                            props: {
+                                clearable: true
+                            }
+                        }
+                    },
+                    form: {
+                        rules: [{required: true, message: '必填项'}]
+                    }
+                },
+                placementClassification: {
+                    title: '广告位',
+                    column: {
+                        align: 'center',
+                    },
+                },
+                dynBidStrategy: {
+                    title: '竞价策略',
+                    column: {
+                        align: 'center',
+                    },
+                },
+                '竞价调整': {},
+                Impression: {
+                    title: '曝光量',
+                    column: {
+                        align: 'center',
+                    },
+                },
+                Click: {
+                    title: '点击量',
+                    column: {
+                        align: 'center',
+                    },
+                },
+                CTR: {title: '点击率', column: {align: 'center'}},
+                Spend: {title: '花费', column: {align: 'center'}},
+                CPC: {title: '点击成本', column: {align: 'center'}},
+                TotalPurchases: {title: '订单数', column: {align: 'center'}},
+                TotalSales: {title: '销售额', column: {align: 'center'}},
+                TotalUnitOrdered: {title: '销量', column: {align: 'center'}},
+                PurchasesRate: {title: '转化率', column: {align: 'center'}},
+                ACOS: {title: 'ACOS', column: {align: 'center'}},
+                ROAS: {title: 'ROAS', column: {align: 'center'}},
+                CPA: {title: '订单成本', column: {align: 'center'}},
+                ...BaseColumn
+            }
+        }
+    }
+}

+ 81 - 4
src/views/adManage/sp/placement/index.vue

@@ -1,12 +1,89 @@
 <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>
+            <template #cell_campaignName="scope">
+                <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+            </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>
+        </fs-crud>
+    </fs-page>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue'
+import {inject, nextTick, onMounted, ref} from 'vue'
+import {FsPage, useFs} 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()
+
+
+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})
+
+const jumpGroup = (row: any) => {
+    router.push({
+        name: 'CampaignDetail',
+        query: {id: row.id, campaignId: row.campaignId, tagsViewName: row.campaignName},
+    })
+}
 
 </script>
 
-<style scoped>
+<style lang="scss">
+.chart-tabs {
+    margin: 5px 0;
+
+    .el-tabs__nav {
+        padding-left: 0 !important;
+    }
+}
 
-</style>
+</style>

+ 17 - 9
src/views/adManage/sp/targets/crud.tsx

@@ -40,14 +40,6 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                     add: {
                         show: false
                     },
-                    create: {
-                        text: '新建广告活动',
-                        type: 'primary',
-                        show: true,
-                        click() {
-
-                        }
-                    }
                 }
             },
             search: {
@@ -101,7 +93,23 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
                     }
                 },
                 '商品和分类': {title: '商品和分类'},
-                state: {title: '状态'},
+                state: {
+                    title: '状态',
+                    column: {
+                        width: '90px',
+                        align: 'center'
+                    },
+                    type: 'dict-select',
+                    search: {
+                        show: true
+                    },
+                    dict: dict({
+                        data: [
+                            {value: 'PAUSED', label: '已暂停', color: 'warning'},
+                            {value: 'ENABLED', label: '投放中', color: 'success'},
+                        ]
+                    })
+                },
                 campaignName: {
                     title: '广告活动名称',
                     column: {