Sfoglia il codice sorgente

新增展示型推广SD-广告活动;修改表格样式以及边框弧度

WanGxC 1 anno fa
parent
commit
217dca0dad
74 ha cambiato i file con 4460 aggiunte e 114 eliminazioni
  1. 5 1
      src/components/TextSelector/index.vue
  2. 1 1
      src/theme/app.scss
  3. 2 1
      src/views/adManage/ad-overview/daily/crud.tsx
  4. 2 1
      src/views/adManage/ad-overview/hourly/crud.tsx
  5. 5 1
      src/views/adManage/ad-overview/index.vue
  6. 2 1
      src/views/adManage/ad-overview/monthly/crud.tsx
  7. 2 1
      src/views/adManage/ad-overview/weekly/crud.tsx
  8. 5 5
      src/views/adManage/sb/campaigns/chartComponents/adStruct.vue
  9. 2 1
      src/views/adManage/sb/campaigns/crud.tsx
  10. 1 0
      src/views/adManage/sb/campaigns/index.vue
  11. 2 2
      src/views/adManage/sb/chartComponents/dataTendency.vue
  12. 12 9
      src/views/adManage/sb/index.vue
  13. 2 1
      src/views/adManage/sb/keywords/crud.ts
  14. 2 1
      src/views/adManage/sb/placement/crud.tsx
  15. 2 1
      src/views/adManage/sb/searchTerm/crud.tsx
  16. 2 1
      src/views/adManage/sb/targets/crud.tsx
  17. 9 0
      src/views/adManage/sd/campaigns/CreateCampaigns/index.vue
  18. 93 0
      src/views/adManage/sd/campaigns/api.ts
  19. 59 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/ads/api.ts
  20. 130 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/ads/crud.tsx
  21. 73 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/ads/index.vue
  22. 12 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/api.ts
  23. 11 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/api.ts
  24. 95 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/crud.tsx
  25. 67 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/index.vue
  26. 84 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/index.vue
  27. 28 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/api.ts
  28. 122 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/crud.tsx
  29. 71 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/index.vue
  30. 11 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/manualTarget/api.ts
  31. 115 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/manualTarget/crud.tsx
  32. 105 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/manualTarget/index.vue
  33. 11 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negKeyword/api.ts
  34. 87 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negKeyword/crud.tsx
  35. 35 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negKeyword/index.vue
  36. 11 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negProduct/api.ts
  37. 66 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negProduct/crud.tsx
  38. 35 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negProduct/index.vue
  39. 11 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negTarget/api.ts
  40. 66 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negTarget/crud.tsx
  41. 26 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negTarget/index.vue
  42. 11 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/searchTerm/api.ts
  43. 83 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/searchTerm/crud.tsx
  44. 90 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/searchTerm/index.vue
  45. 57 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/api.ts
  46. 125 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/crud.tsx
  47. 82 0
      src/views/adManage/sd/campaigns/campaignDetail/adGroups/index.vue
  48. 14 0
      src/views/adManage/sd/campaigns/campaignDetail/api.ts
  49. 20 0
      src/views/adManage/sd/campaigns/campaignDetail/budget/api.ts
  50. 56 0
      src/views/adManage/sd/campaigns/campaignDetail/budget/crud.tsx
  51. 46 0
      src/views/adManage/sd/campaigns/campaignDetail/budget/index.vue
  52. 116 0
      src/views/adManage/sd/campaigns/campaignDetail/budget/lineChart.vue
  53. 72 0
      src/views/adManage/sd/campaigns/campaignDetail/index.vue
  54. 12 0
      src/views/adManage/sd/campaigns/campaignDetail/placement/api.ts
  55. 89 0
      src/views/adManage/sd/campaigns/campaignDetail/placement/crud.tsx
  56. 74 0
      src/views/adManage/sd/campaigns/campaignDetail/placement/index.vue
  57. 346 0
      src/views/adManage/sd/campaigns/chartComponents/adStruct.vue
  58. 261 0
      src/views/adManage/sd/campaigns/chartComponents/dataTendency.vue
  59. 173 0
      src/views/adManage/sd/campaigns/crud.tsx
  60. 132 0
      src/views/adManage/sd/campaigns/index.vue
  61. 321 0
      src/views/adManage/sd/chartComponents/dataTendency.vue
  62. 80 0
      src/views/adManage/sd/index.vue
  63. 3 2
      src/views/adManage/sp/advertisedProducts/crud.tsx
  64. 2 1
      src/views/adManage/sp/campaigns/crud.tsx
  65. 3 3
      src/views/adManage/sp/campaigns/index.vue
  66. 4 4
      src/views/adManage/sp/chartComponents/dataTendency.vue
  67. 5 1
      src/views/adManage/sp/index.vue
  68. 11 1
      src/views/adManage/sp/keywords/crud.tsx
  69. 2 1
      src/views/adManage/sp/placement/crud.tsx
  70. 2 1
      src/views/adManage/sp/purchasedOtherProducts/crud.tsx
  71. 71 60
      src/views/adManage/sp/searchTerm/crud.tsx
  72. 2 1
      src/views/adManage/sp/targets/crud.tsx
  73. 344 0
      src/views/adManage/utils/commonTabColumn.tsx
  74. 269 11
      src/views/adManage/utils/enum.ts

+ 5 - 1
src/components/TextSelector/index.vue

@@ -7,7 +7,7 @@
       </el-icon>
     </span>
     <template #dropdown>
-      <el-dropdown-menu>
+      <el-dropdown-menu class="dropdown-menu-scroll">
         <el-dropdown-item v-for="info in options" :command="info.value" :disabled="info.disabled"> {{ info.label }}</el-dropdown-item>
       </el-dropdown-menu>
     </template>
@@ -46,4 +46,8 @@ const handleCommand = (command: string) => {
   /* display: flex;
   align-items: center; */
 }
+.dropdown-menu-scroll {
+  max-height: 300px; /* 根据需要调整最大高度 */
+  overflow-y: auto;
+}
 </style>

+ 1 - 1
src/theme/app.scss

@@ -390,7 +390,7 @@ body,
   display: flex;
   height: 40px;
   gap: 24px;
-  border-radius: 10px;
+  border-radius: 8px;
   overflow: hidden;
 }
 

+ 2 - 1
src/views/adManage/ad-overview/daily/crud.tsx

@@ -30,9 +30,10 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
       table: {
         height: 800,
         showSummary: true,
+        stripe: false,
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         // rowClassName() {
         //   return '.el-table__footer td.el-table__cell'

+ 2 - 1
src/views/adManage/ad-overview/hourly/crud.tsx

@@ -30,9 +30,10 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
       table: {
         height: 800,
         showSummary: true,
+        stripe: false,
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle:  {
           border: 'none',

+ 5 - 1
src/views/adManage/ad-overview/index.vue

@@ -50,4 +50,8 @@ onBeforeMount(async () => {
 
 </script>
 
-<style scoped></style>
+<style scoped>
+::v-deep(.el-table .el-table__header-wrapper .cell) {
+  border-right: 1px solid rgb(218, 221, 223);
+}
+</style>

+ 2 - 1
src/views/adManage/ad-overview/monthly/crud.tsx

@@ -30,9 +30,10 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
       table: {
         height: 800,
         showSummary: true,
+        stripe: false,
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle:  {
           border: 'none',

+ 2 - 1
src/views/adManage/ad-overview/weekly/crud.tsx

@@ -30,9 +30,10 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
       table: {
         height: 800,
         showSummary: true,
+        stripe: false,
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle:  {
           border: 'none',

+ 5 - 5
src/views/adManage/sb/campaigns/chartComponents/adStruct.vue

@@ -34,7 +34,7 @@ import * as echarts from "echarts"
 import TextSelector from '/@/components/TextSelector/index.vue'
 import { getAdStructureData } from "/@/views/adManage/sb/campaigns/api"
 import { createDisabledOptions } from '../../../utils/dropdowndisable'
-import { barOptions1, barOptions2, barOptionsMap, metricMap, pieOptions } from '/@/views/adManage/utils/enum'
+import { sbBarOptions1, sbBarOptions2, barOptionsMap, metricMap, pieOptions } from '/@/views/adManage/utils/enum'
 import { useShopInfo } from '/@/stores/shopInfo'
 
 
@@ -49,8 +49,8 @@ const dateRange = inject('dateRange')
 
 // 下拉框相关
 let modelValue = ref(pieOptions[0].value)
-let barModelValue1 = ref(barOptions1[0].value)
-let barModelValue2 = ref(barOptions2[2].value)
+let barModelValue1 = ref(sbBarOptions1[0].value)
+let barModelValue2 = ref(sbBarOptions2[2].value)
 
 onMounted(async () => {
   barChart = echarts.init(bar.value)
@@ -178,8 +178,8 @@ function updateBarChartData(resp) {
   barChart.setOption(option)
 }
 
-const computedBarOptions1 = computed(() => createDisabledOptions(barOptions1, barModelValue2.value, barModelValue1.value))
-const computedBarOptions2 = computed(() => createDisabledOptions(barOptions2, barModelValue1.value, barModelValue2.value))
+const computedBarOptions1 = computed(() => createDisabledOptions(sbBarOptions1, barModelValue2.value, barModelValue1.value))
+const computedBarOptions2 = computed(() => createDisabledOptions(sbBarOptions2, barModelValue1.value, barModelValue2.value))
 const computedPieOptions = computed(() => createDisabledOptions(pieOptions, modelValue.value))
 
 // 初始化图表

+ 2 - 1
src/views/adManage/sb/campaigns/crud.tsx

@@ -32,12 +32,13 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none'
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
+        stripe: false,
         showSummary: true,
       },
       container: {

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

@@ -127,4 +127,5 @@ watch(queryParams, async () => {
 ::v-deep(.el-table .el-table__footer-wrapper .cell) {
     font-weight: 600;
 }
+
 </style>

+ 2 - 2
src/views/adManage/sb/chartComponents/dataTendency.vue

@@ -13,7 +13,7 @@
 <script lang="ts" setup>
 import { ref,onMounted, onBeforeUnmount, Ref, unref, watch, computed } from 'vue'
 import * as echarts from 'echarts'
-import { spCampaignMetricsEnum } from '/@/views/adManage/utils/enum.js'
+import {sbCampaignMetricsEnum, spCampaignMetricsEnum} from '/@/views/adManage/utils/enum.js'
 import MetricsCards from '/@/components/MetricsCards/index.vue'
 import XEUtils from 'xe-utils'
 import { buildChartOpt, parseQueryParams } from '/@/views/adManage/utils/tools.js'
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
     {metric: 'Click', color: '#3fd4cf', 'label': '点击量'},
     {metric: 'Spend', color: '#ff9500', 'label': '花费'},
   ],
-  metricEnum: () => spCampaignMetricsEnum
+  metricEnum: () => sbCampaignMetricsEnum
 })
 
 const metrics = ref(props.initMetric)

+ 12 - 9
src/views/adManage/sb/index.vue

@@ -45,15 +45,6 @@ const selectedPortfolios: Ref<string[]> = ref([])
 const portfolios: Ref<Portfolio[]> = ref([])
 const { dateRange } = storeToRefs(publicData)
 const tabActiveName = ref('Campaigns')
-const tabs = [
-  { label: '广告活动', name: 'Campaigns' },
-  { label: '关键词', name: 'Keywords' },
-  { label: '商品投放', name: 'Targets' },
-  // { label: '推广商品', name: 'AdvertisedProducts' },
-  // { label: '购买的其他商品', name: 'PurchasedOtherProducts' },
-  { label: '搜索词', name: 'SearchTerm' },
-  { label: '广告位', name: 'Placement' }
-]
 const tabsComponents: any = {
   Campaigns,
   Keywords,
@@ -63,6 +54,15 @@ const tabsComponents: any = {
   SearchTerm,
   Placement
 }
+const tabs = [
+  { label: '广告活动', name: 'Campaigns' },
+  { label: '关键词', name: 'Keywords' },
+  { label: '商品投放', name: 'Targets' },
+  // { label: '推广商品', name: 'AdvertisedProducts' },
+  // { label: '购买的其他商品', name: 'PurchasedOtherProducts' },
+  { label: '搜索词', name: 'SearchTerm' },
+  { label: '广告位', name: 'Placement' }
+]
 
 provide('dateRange', dateRange)
 
@@ -74,4 +74,7 @@ onBeforeMount(async () => {
 </script>
 
 <style scoped>
+::v-deep(.el-table .el-table__header-wrapper .cell) {
+  border-right: 1px solid rgb(218, 221, 223);
+}
 </style>

+ 2 - 1
src/views/adManage/sb/keywords/crud.ts

@@ -32,13 +32,14 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
+        stripe: false
       },
       container: {
         fixedHeight: false

+ 2 - 1
src/views/adManage/sb/placement/crud.tsx

@@ -32,13 +32,14 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
+        stripe: false
       },
       container: {
         fixedHeight: false

+ 2 - 1
src/views/adManage/sb/searchTerm/crud.tsx

@@ -23,13 +23,14 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 				headerCellStyle: {
 					backgroundColor: '#f6f7fa', // 直接设置背景颜色
 					height: '20px',
-					// border: '0.5px solid #ddd',
+					borderRight: 'none',
 				},
 				cellStyle: {
 					border: 'none',
 					borderBottom: '0.5px solid #ddd',
 				},
 				showSummary: true,
+				stripe: false
 			},
 			container: {
 				fixedHeight: false,

+ 2 - 1
src/views/adManage/sb/targets/crud.tsx

@@ -32,13 +32,14 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
+        stripe: false
       },
       container: {
         fixedHeight: false

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

@@ -0,0 +1,9 @@
+<template>
+  <p>新建广告页面</p>
+</template>
+
+<script lang="ts" setup>
+</script>
+
+<style scoped>
+</style>

+ 93 - 0
src/views/adManage/sd/campaigns/api.ts

@@ -0,0 +1,93 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sdcampaigns/report/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: "/api/ad_manage/sdcampaigns/",
+        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 getLineHourData(query: UserPageQuery) {
+    query["dateRangeType"] = "D"
+    return request({
+        url: "/api/ad_manage/sdcampaigns/ams/hourly_trend",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    query["dateRangeType"] = "D"
+    return request({
+        url: apiPrefix + "trend/daily",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineWeekData(query: UserPageQuery) {
+    query["dateRangeType"] = "W"
+    return request({
+        url: apiPrefix + "trend/weekly",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineMonthData(query: UserPageQuery) {
+    query["dateRangeType"] = "M"
+    return request({
+        url: apiPrefix + "trend/monthly",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getAdStructureData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "structure",
+        method: 'GET',
+        params: query
+    })
+}

+ 59 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/ads/api.ts

@@ -0,0 +1,59 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/ads/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}
+export function GetObj(id: InfoReq) {
+    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) {
+  query['dateRangeType'] = 'D'
+  return request({
+    url: apiPrefix + 'daily/',
+    method: 'GET',
+    params: query
+  })
+}

+ 130 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/ads/crud.tsx

@@ -0,0 +1,130 @@
+import * as api from './api'
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+import { parseQueryParams } from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+
+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: 750,
+				headerCellStyle: {
+					backgroundColor: '#f6f7fa', // 直接设置背景颜色
+					height: '20px',
+					// border: '0.5px solid #ddd',
+				},
+				cellStyle: {
+					border: 'none',
+					borderBottom: '0.5px solid #ddd',
+				},
+			},
+			container: {
+        fixedHeight: false
+      },
+			actionbar: {
+				show: true,
+				color: "#626aef",
+				buttons: {
+					add: {
+						show: false
+					},
+					create: {
+						text: '添加广告',
+						// type: 'primary',
+						color: "#626aef",
+						plain: true,
+						show: true,
+						click() {
+
+						}
+					},
+				}
+			},
+			search: {
+				show: true,
+				buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						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: {
+        adGroupName: {
+					title: '广告组名称',
+					column: {
+						width: 230,
+						fixed: 'left',
+					}
+				},
+				state: {
+					title: '状态',
+					column: {
+						width: "100px"
+					}
+				},
+				...BaseColumn
+			}
+		}
+	}
+}

+ 73 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/ads/index.vue

@@ -0,0 +1,73 @@
+<template>
+	<fs-page class="fs-page-custom">
+		<fs-crud ref="crudRef" v-bind="crudBinding">
+			<template #search-left>
+				<DateRangePicker v-model="dateRange"></DateRangePicker>
+			</template>
+			<template #header-middle>
+				<DataTendencyChart :query="queryParams" :fetch-card="getCardData" :fetch-line="getLineData"> </DataTendencyChart>
+			</template>
+			<template v-for="field of Object.keys(BaseColumn)" #[`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>
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small" />
+        </div>
+      </template>
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+import { LocationQueryValue } from 'vue-router'
+import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { getCardData, getLineData } from './api'
+import { storeToRefs } from 'pinia'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+
+
+interface Props {
+	adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+const showCompare = ref(false)
+const queryParams = ref({
+	adGroupId: props.adGroupId,
+	dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+onMounted(() => {
+	crudExpose.doRefresh()
+})
+watch(queryParams, async () => {
+	crudExpose.doRefresh()
+}, { deep: true })
+</script>
+
+<style lang="scss">
+.chart-tabs {
+	margin: 5px 0;
+
+	.el-tabs__nav {
+		padding-left: 0 !important;
+	}
+}
+</style>

+ 12 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/api.ts

@@ -0,0 +1,12 @@
+import { request } from '/@/utils/service'
+import { LocationQueryValue } from 'vue-router'
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/'
+
+export function GetObj(adGroupId: LocationQueryValue | LocationQueryValue[]) {
+	return request({
+		url: apiPrefix + 'head/',
+		method: 'get',
+		params: { adGroupId },
+	})
+}

+ 11 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/targets/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 95 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/crud.tsx

@@ -0,0 +1,95 @@
+import * as api from './api'
+import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+import { BaseColumn } 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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {},
+      container: {
+        fixedHeight: true,
+      },
+      actionbar: {
+        show: false,
+      },
+      search: {
+        show: true,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        keyword: {
+          title: '目标群体',
+          column: {
+            width: '200px',
+            fixed: 'left',
+            align: 'center',
+          },
+        },
+        state: {
+          title: '状态',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        suggestedBid: {
+          title: '建议竞价',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        bid: {
+          title: '出价',
+          column: {
+            width: '60px',
+            align: 'center',
+          },
+        },
+        // currentBid: {
+        //   title: '当前竞价',
+        //   column: {
+        //     width: '100px',
+        //     align: 'center',
+        //   },
+        // },
+        ...BaseColumn,
+      },
+    },
+  }
+}

+ 67 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/autoTarget/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+
+      <template v-for="field of Object.keys(BaseColumn)" #[`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 { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { LocationQueryValue } from 'vue-router'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+// const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+  dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const showCompare = ref(false)
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped></style>

+ 84 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="asj-container">
+    <div class="asj-detail-header">
+      <span style="font-size: x-large; font-weight: bold; color: #0f1111;margin: 5px;">
+        <span> {{ adGroupInfo.adGroupName }} </span>
+      </span>
+      <div class="asj-detail-info">
+        <span style="color: rgb(177, 177, 177);">状态: </span>
+        <span>
+          <el-button class="no-hover-effect" type="success" size="small" round plain>{{ dynStatusEnum[adGroupInfo.state] }}</el-button>
+        </span>
+        <span class="head-span">投放类型: </span> <span>{{ targetTypeEnum[adGroupInfo.creative_type] }}</span>
+        <!-- <span>默认竞价:{{ profile.currency_symbol + adGroupInfo.defaultBid }}</span> -->
+        <span class="head-span">投放日期: </span> <span>{{ adGroupInfo.startDate }} ~ {{ adGroupInfo.endDate ?? '无结束日期' }}</span>
+      </div>
+    </div>
+    <el-tabs type="border-card" class="asj-detail-tabs" v-model="tabActiveName">
+      <el-tab-pane label="广告" name="ads">
+        <Ads v-if="tabActiveName==='ads'" :adGroupId="route.query.adGroupId"></Ads>
+      </el-tab-pane>
+      <template v-if="route.query.targetingType ==='target'">
+        <el-tab-pane label="商品投放" name="tab2">
+          <ManualTarget v-if="tabActiveName === 'tab2'" :adGroupId="route.query.adGroupId">商品投放</ManualTarget>
+        </el-tab-pane>
+        <!--<el-tab-pane label="否定商品" name="tab3">-->
+        <!--  <NegProduct v-if="tabActiveName === 'tab3'" :adGroupId="route.query.adGroupId">否定商品</NegProduct>-->
+        <!--</el-tab-pane>-->
+      </template>
+      <template v-else>
+        <el-tab-pane label="关键词" name="tab2">
+          <Keyword v-if="tabActiveName === 'tab2'" :ad-group-id="route.query.adGroupId">关键词</Keyword>
+        </el-tab-pane>
+        <el-tab-pane label="否定词" name="tab3">
+          <NegKeyword v-if="tabActiveName === 'tab3'" :ad-group-id="route.query.adGroupId">否定词</NegKeyword>
+        </el-tab-pane>
+        <el-tab-pane label="搜索关键词" name="searchTerm">
+          <SearchTerm v-if="tabActiveName === 'searchTerm'" :adGroupId="route.query.adGroupId" />
+        </el-tab-pane>
+      </template>
+    </el-tabs>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, Ref} from 'vue'
+import {useRoute} from 'vue-router'
+import {GetObj} from './api'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {storeToRefs} from 'pinia'
+import Ads from './ads/index.vue'
+import SearchTerm from './searchTerm/index.vue'
+import Keyword from './keyword/index.vue'
+import AutoTarget from './autoTarget/index.vue'
+import ManualTarget from './manualTarget/index.vue'
+import NegProduct from './negProduct/index.vue'
+import NegKeyword from './negKeyword/index.vue'
+import NegTarget from './negTarget/index.vue'
+import {dynStatusEnum, targetTypeEnum} from '/@/views/adManage/utils/enum'
+// import { usePublicData } from '/@/stores/publicData'
+
+
+const tabActiveName = ref('ads')
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const route = useRoute()
+const adGroupInfo: Ref<SpAdGroup> = ref({})
+
+onMounted(async () => {
+  const resp = await GetObj(route.query.adGroupId)
+  adGroupInfo.value = resp.data
+  console.log(111, route.query)
+})
+
+</script>
+
+<style scoped>
+.head-span {
+  color: rgb(177, 177, 177);
+  margin-left: 40px;
+}
+:deep(.el-tabs--border-card) {
+  border: none;
+}
+</style>

+ 28 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/api.ts

@@ -0,0 +1,28 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/keywords/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}
+
+export function getCardData(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + "total/",
+        method: 'GET',
+        params: query
+    })
+}
+
+export function getLineData(query: UserPageQuery) {
+    query["dateRangeType"] = "D"
+    return request({
+        url: apiPrefix + "daily/",
+        method: 'GET',
+        params: query
+    })
+}

+ 122 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/crud.tsx

@@ -0,0 +1,122 @@
+import * as api from './api'
+import {CreateCrudOptionsProps, CreateCrudOptionsRet, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} 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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 750,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          // border: '0.5px solid #ddd',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+      },
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: true,
+        color: "#626aef",
+        buttons: {
+          add: {
+            show: false
+          },
+          create: {
+            text: '添加关键词',
+            // type: 'primary',
+            color: "#626aef",
+            plain: true,
+            show: true,
+            click() {
+
+            }
+          },
+        }
+      },
+      search: {
+        show: true,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        keywordText: {
+          title: '关键词',
+          column: {
+            width: '200px',
+            fixed: 'left',
+            align: 'center',
+          },
+        },
+        state: {
+          title: '状态',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        suggestedBid: {
+          title: '建议竞价',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        bid: {
+          title: '出价',
+          column: {
+            width: '60px',
+            align: 'center',
+          },
+        },
+        // currentBid: {
+        //   title: '当前竞价',
+        //   column: {
+        //     width: '100px',
+        //     align: 'center',
+        //   },
+        // },
+        ...BaseColumn,
+      },
+    },
+  }
+}

+ 71 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+      <template #header-middle>
+        <DataTendencyChart :query="queryParams" :fetch-card="getCardData" :fetch-line="getLineData"> </DataTendencyChart>
+      </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`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 { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { LocationQueryValue } from 'vue-router'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import {getCardData, getLineData} from '/@/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/keyword/api'
+import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+// const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+  dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const showCompare = ref(false)
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped></style>

+ 11 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/manualTarget/api.ts

@@ -0,0 +1,11 @@
+import {request} from '/@/utils/service'
+import {UserPageQuery} from '@fast-crud/fast-crud'
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/targets/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 115 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/manualTarget/crud.tsx

@@ -0,0 +1,115 @@
+import * as api from './api'
+import {CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} 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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 750,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          // border: '0.5px solid #ddd',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+      },
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: false,
+      },
+      search: {
+        show: true,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        resolvedExpression_value: {
+          title: '商品和分类',
+          column: {
+            width: '200px',
+            fixed: 'left',
+            align: 'left',
+          },
+        },
+        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'},
+            ]
+          })
+        },
+        ASIN: {
+          title: 'ASIN',
+          column: {
+            width: '130px',
+            align: 'center',
+          },
+        },
+        suggestedBid: {
+          title: '建议竞价',
+          column: {
+            width: '100px',
+            align: 'center',
+          },
+        },
+        bid: {
+          title: '出价',
+          column: {
+            width: '60px',
+            align: 'center',
+          },
+        },
+        // '分时': {
+        //   title: '分时调价',
+        //   column: {
+        //     width: '100px',
+        //     align: 'right',
+        //   }
+        // },
+
+        ...BaseColumn,
+      },
+    },
+  }
+}

+ 105 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/manualTarget/index.vue

@@ -0,0 +1,105 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+
+      <template #header-middle>
+        <DataTendencyChart
+            :query="queryParams"
+            :fetchCard="getCardData"
+            :fetchLine="getLineData">
+        </DataTendencyChart>
+      </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`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>
+
+      <template #cell_resolvedExpression_value="scope">
+        <el-tooltip effect="dark" :content="scope.row.resolvedExpression_value" placement="top">
+          <div>
+            <el-link :underline="false" style="color: rgb(30, 33, 40)">
+              {{ scope.row.resolvedExpression_value }}
+            </el-link>
+            <br>
+            <span style="color: rgb(109, 119, 132)">ASIN: </span> <span style="color: rgb(30, 33, 40)">{{ scope.row.ASIN }}</span>
+          </div>
+        </el-tooltip>
+      </template>
+
+
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn'
+import {LocationQueryValue} from 'vue-router'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import {getCardData, getLineData} from '/@/views/adManage/sb/campaigns/campaignDetail/adGroups/adGroupDetail/ads/api'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DataTendencyChart from '/@/views/adManage/sb/chartComponents/dataTendency.vue'
+// import { useShopInfo } from '/@/stores/shopInfo'
+
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+
+const props = defineProps<Props>()
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const {dateRange} = storeToRefs(publicData)
+// const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+  dateRange,
+})
+const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, context: queryParams})
+const showCompare = ref(false)
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+    queryParams,
+    async () => {
+      crudExpose.doRefresh()
+    },
+    {deep: true}
+)
+</script>
+
+<style scoped>
+/* .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;
+} */
+</style>

+ 11 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negKeyword/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/negativekeywords/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 87 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negKeyword/crud.tsx

@@ -0,0 +1,87 @@
+import * as api from './api'
+import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 750,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          // border: '0.5px solid #ddd',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+      },
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: true,
+        color: "#626aef",
+        buttons: {
+          add: {
+            show: false
+          },
+          create: {
+            text: '新建广告活动',
+            // type: 'primary',
+            color: "#626aef",
+            plain: true,
+            show: true,
+            click() {
+
+            }
+          },
+        }
+      },
+      search: {
+        show: false,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true,
+          },
+          compact: {
+            show: false,
+          },
+        },
+      },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: true,
+      },
+      columns: {
+        keywordText: {
+          title: '否定词',
+        },
+      },
+    },
+  }
+}

+ 35 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negKeyword/index.vue

@@ -0,0 +1,35 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+import { LocationQueryValue } from 'vue-router'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped></style>

+ 11 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negProduct/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/negativetarget/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 66 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negProduct/crud.tsx

@@ -0,0 +1,66 @@
+import * as api from './api'
+import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {},
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: false,
+      },
+      search: {
+        show: false,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        resolvedExpression_type: {
+          title: '否定类型',
+        },
+        product: {
+          title: '商品和品牌',
+        },
+        resolvedExpression_value: {
+          title: 'SKU/ASIN',
+        },
+      },
+    },
+  }
+}

+ 35 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negProduct/index.vue

@@ -0,0 +1,35 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+import { LocationQueryValue } from 'vue-router'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped></style>

+ 11 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negTarget/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/negativetarget/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 66 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negTarget/crud.tsx

@@ -0,0 +1,66 @@
+import * as api from './api'
+import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {},
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: false,
+      },
+      search: {
+        show: false,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        resolvedExpression_type: {
+          title: '否定类型',
+        },
+        product: {
+          title: '商品和品牌',
+        },
+        resolvedExpression_value: {
+          title: 'SKU/ASIN',
+        },
+      },
+    },
+  }
+}

+ 26 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/negTarget/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <el-tabs v-model="tabActiveName">
+    <el-tab-pane label="否定商品" name="negProduct">
+      <NegProduct v-if="tabActiveName === 'negProduct'" :ad-group-id="props.adGroupId"/>
+    </el-tab-pane>
+    <el-tab-pane label="否定词" name="negKeyword">
+      <NegKeyword v-if="tabActiveName === 'negKeyword'" :ad-group-id="props.adGroupId"/>
+    </el-tab-pane>
+  </el-tabs>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { LocationQueryValue } from 'vue-router'
+import NegProduct from '../negProduct/index.vue'
+import NegKeyword from '../negKeyword/index.vue'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const tabActiveName = ref('negProduct')
+
+</script>
+
+<style scoped></style>

+ 11 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/searchTerm/api.ts

@@ -0,0 +1,11 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+
+export const apiPrefix = '/api/ad_manage/sbgroupdetail/searchwords/';
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix + 'list/',
+        method: 'get',
+        params: query,
+    })
+}

+ 83 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/searchTerm/crud.tsx

@@ -0,0 +1,83 @@
+import * as api from './api'
+import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
+import { inject } from 'vue'
+import { BaseColumn } 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 hasPermissions = inject('$hasPermissions')
+
+  return {
+    crudOptions: {
+      table: {
+        height: 750,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          // border: '0.5px solid #ddd',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+      },
+      container: {
+        fixedHeight: false,
+      },
+      actionbar: {
+        show: false,
+      },
+      search: {
+        show: true,
+        buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+      },
+      // toolbar: {
+      //   buttons: {
+      //     search: {
+      //       show: true,
+      //     },
+      //     compact: {
+      //       show: false,
+      //     },
+      //   },
+      // },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        query: {
+          title: '搜索词',
+          column: {
+            width: '200px',
+            fixed: 'left',
+          },
+        },
+        keywordText: {
+          title: '关键词',
+          column: {
+            width: '200px',
+          },
+        },
+        ...BaseColumn,
+      },
+    },
+  }
+}

+ 90 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/adGroupDetail/searchTerm/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+
+      <template #cell_query="scope">
+        <el-tooltip effect="dark" :content="scope.row.query" placement="top">
+          <div class="en-text">{{ scope.row.query }}</div>
+        </el-tooltip>
+      </template>
+      <template #cell_keywordText="scope">
+        <el-tooltip effect="dark" :content="scope.row.keywordText" placement="top">
+            <div class="en-text">{{ scope.row.keywordText }}</div>
+        </el-tooltip>
+      </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`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 { ref, onMounted, computed, watch, onBeforeMount } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud'
+import { createCrudOptions } from './crud'
+// import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { LocationQueryValue } from 'vue-router'
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
+import DataCompare from '/@/components/dataCompare/index.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+interface Props {
+  adGroupId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+// const { profile } = storeToRefs(shopInfo)
+const queryParams = ref({
+  adGroupId: props.adGroupId,
+  dateRange,
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+const showCompare = ref(false)
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped>
+.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;
+}
+</style>

+ 57 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/api.ts

@@ -0,0 +1,57 @@
+import { request } from '/@/utils/service'
+import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud'
+
+export const apiPrefix = '/api/ad_manage/sbcampaigndetail/sbgroups/'
+export function GetList(query: UserPageQuery) {
+	return request({
+		url: apiPrefix + 'list/',
+		method: 'get',
+		params: query,
+	})
+}
+export function GetObj(id: number) {
+	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,
+	})
+}

+ 125 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/crud.tsx

@@ -0,0 +1,125 @@
+import * as api from './api';
+import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
+import { inject, nextTick, ref } from 'vue';
+import { BaseColumn } 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: 750,
+				headerCellStyle: {
+					backgroundColor: '#f6f7fa', // 直接设置背景颜色
+					height: '20px',
+					// border: '0.5px solid #ddd',
+				},
+				cellStyle: {
+					border: 'none',
+					borderBottom: '0.5px solid #ddd',
+				},
+			},
+			container: {
+        fixedHeight: false
+      },
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			toolbar: {
+        buttons: {
+					search: {
+						show: true
+					},
+					compact: {
+						show: false
+					}
+				}
+			},
+			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: {
+        adGroupName: {
+          title: '广告组名称',
+          column: {
+            width: '150px'
+          },
+					search: {
+						show: true,
+						component: {
+							props: {
+								clearable: true
+							}
+						}
+					},
+					form: {
+						rules: [{required: true, message:'必填项'}]
+					}
+        },
+				creative_type: {
+					title: '投放类型',
+					type: 'dict-select',
+					column: {
+						align: 'center',
+						width: '130px'
+					},
+					dict: dict({
+						data: [
+							{ label: '商品集', value: 'PRODUCT_COLLECTION' },
+							{ label: '品牌视频', value: 'BRAND_VIDEO' },
+							{ label: '视频', value: 'VIDEO' }
+						]
+					})
+				},
+				state: {
+					title: '状态',
+					column: {
+						width: 100
+					}
+				},
+				...BaseColumn
+			}
+		}
+	}
+}

+ 82 - 0
src/views/adManage/sd/campaigns/campaignDetail/adGroups/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+      <template #header-middle>
+        <DataTendencyChart
+          :query="queryParams"
+          :fetchCard="getCardData"
+          :fetchLine="getLineData">
+        </DataTendencyChart>
+      </template>
+      <template #cell_adGroupName="scope">
+        <el-tooltip effect="dark" :content="scope.row.campaignName" placement="top">
+          <el-link type="primary" :underline="false" @click="jumpAds(scope.row)">
+            <div class="en-text">{{ scope.row.adGroupName }}</div>
+          </el-link>
+        </el-tooltip>
+      </template>
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {LocationQueryValue, useRouter} from 'vue-router'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
+import {getCardData, getLineData} from './api'
+
+const publicData = usePublicData()
+const router = useRouter()
+interface Props {
+  campaignId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const { dateRange } = storeToRefs(publicData)
+const queryParams = ref({
+  campaignId: props.campaignId,
+  dateRange
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+onMounted(() => {
+	crudExpose.doRefresh()
+})
+const jumpAds = (row: any) => {
+  router.push({
+    name: 'SbAdGroupDetail',
+    query: { adGroupId: row.adGroupId, targetingType: row.targetingType, tagsViewName: row.adGroupName }
+  })
+}
+watch(
+  queryParams,
+  async () => {
+    crudExpose.doRefresh()
+  },
+  { deep: true }
+)
+
+</script>
+
+<style scoped>
+.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;
+}
+
+</style>

+ 14 - 0
src/views/adManage/sd/campaigns/campaignDetail/api.ts

@@ -0,0 +1,14 @@
+import { request } from '/@/utils/service'
+import { LocationQueryValue } from 'vue-router'
+// import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+// import XEUtils from 'xe-utils';
+
+export const apiPrefix = '/api/ad_manage/sbcampaigndetail/'
+
+export function GetObj(campaignId: LocationQueryValue | LocationQueryValue[]) {
+	return request({
+		url: apiPrefix + 'head/',
+		method: 'get',
+		params: { campaignId },
+	})
+}

+ 20 - 0
src/views/adManage/sd/campaigns/campaignDetail/budget/api.ts

@@ -0,0 +1,20 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery } from '@fast-crud/fast-crud';
+
+
+export const apiPrefix = '/api/ad_manage/sbcampaigndetail/budget/'
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+        params: query,
+    })
+}
+
+export function GetLineList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+        params: query,
+    })
+}

+ 56 - 0
src/views/adManage/sd/campaigns/campaignDetail/budget/crud.tsx

@@ -0,0 +1,56 @@
+import * as api from './api'
+import {CreateCrudOptionsProps, CreateCrudOptionsRet, UserPageQuery} from '@fast-crud/fast-crud'
+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)
+	}
+
+	return {
+		crudOptions: {
+			table: {
+				// height: 800,
+			},
+			container: {
+				fixedHeight: false,
+			},
+			actionbar: {
+				show: false,
+			},
+			search: {
+				show: true,
+				buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+			},
+			toolbar: {
+				buttons: {
+					search: {
+						show: true,
+					},
+					compact: {
+						show: false,
+					},
+				},
+			},
+			request: {
+				pageRequest,
+			},
+			rowHandle: {
+				show: false,
+			},
+			columns: {
+
+			},
+		},
+	}
+}

+ 46 - 0
src/views/adManage/sd/campaigns/campaignDetail/budget/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+      <template #header-middle>
+        <LineChart :query="queryParams"/>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {ref} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {LocationQueryValue} from 'vue-router'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {createCrudOptions} from './crud'
+import LineChart from './lineChart.vue'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+interface Props {
+  campaignId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+const queryParams = ref({
+  campaignId: props.campaignId,
+  dateRange
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+// onMounted(async () => {
+// 	crudExpose.doRefresh()
+// })
+
+</script>
+
+<style scoped>
+:deep(.el-card) {
+  border: none;
+}
+</style>

+ 116 - 0
src/views/adManage/sd/campaigns/campaignDetail/budget/lineChart.vue

@@ -0,0 +1,116 @@
+<template>
+  <el-card v-loading="loading" shadow="never" style="margin-bottom: 5px; border-radius:0;">
+    <div style="height: 350px;" ref="chartRef"></div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import { ref,onMounted, onBeforeUnmount, watch, computed } from 'vue'
+import * as echarts from 'echarts'
+import { GetLineList } from './api'
+import { parseQueryParams } from '/@/views/adManage/utils/tools.js'
+
+interface Props {
+  query: any
+}
+const props = defineProps<Props>()
+const chartRef = ref()
+let chartObj:any
+const loading = ref(true)
+const queryParams = computed(() => parseQueryParams(props.query))
+
+const resizeChart = () => { chartObj.resize() }
+const addResize = () => { window.addEventListener('resize', resizeChart) }
+const removeResize = () => { window.removeEventListener('resize', resizeChart) }
+onMounted(() => {
+  addResize()
+  setTimeout(() => { initLine() }, 0)
+})
+onBeforeUnmount(() => {
+	if(chartObj) {
+		chartObj.dispose()
+    chartObj = null
+	}
+  removeResize()
+})
+
+const initLine = async () => {
+  chartObj = echarts.init(chartRef.value)
+  const option = {
+    dataset: {
+      source: []
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        label: {
+          backgroundColor: '#6a7985'
+        }
+      }
+    },
+    grid: { top: 30, bottom: 30, left: 30, right: 30 },
+    xAxis: {
+      type: 'category'
+    },
+    yAxis: {
+      type: 'value',
+      name: '预算',
+      axisLine: {
+        show: true,
+				lineStyle: { color: '#0085ff' }
+      },
+    },
+    series: [
+      {
+        type: 'line',
+        encode: {
+          x: 'date',
+          y: 'campaignBudgetAmount'
+        },
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 4,
+        lineStyle: { width: 2 },
+        itemStyle: {
+          color: '#0085ff',
+          borderColor: '#0085ff'
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#0085ff93' },
+            { offset: 1, color: '#0085ff03' },
+          ]),
+        },
+      }
+    ]
+  }
+  const items = await getDataset()
+  option.dataset.source = items
+  chartObj.setOption(option)
+  loading.value = false
+}
+const getDataset = async () => {
+  const resp = await GetLineList(queryParams.value)
+  return resp.data
+}
+watch(
+  props.query,
+  async () => {
+    loading.value = true
+
+    const dataset = await getDataset()
+    chartObj.setOption({
+      dataset: {
+        source: dataset
+      }
+    })
+
+    loading.value = false
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped>
+
+</style>

+ 72 - 0
src/views/adManage/sd/campaigns/campaignDetail/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="asj-container">
+    <div class="asj-detail-header">
+      <span style="font-size: x-large; font-weight: bold; color: #0f1111;margin: 5px;">
+        <span> {{ campaignInfo.campaignName }}</span>
+      </span>
+      <div class="asj-detail-info">
+        <span style="color: rgb(177, 177, 177)">状态: </span>
+        <span>
+          <el-button class="no-hover-effect" type="success" round plain size="small" style="margin-top: -3px">{{ dynStatusEnum[campaignInfo.state] }}</el-button>
+        </span><!--({{ campaignInfo.servingStatus }})-->
+        <span class="head-span">预算: </span> <span>{{ profile.currency_symbol + campaignInfo.budget }} | {{ campaignInfo.budgetType }}</span>
+        <!-- <span>投放类型:{{ campaignInfo.targetingType }}</span> -->
+        <span class="head-span">投放日期: </span> <span>{{ campaignInfo.startDate }} ~ {{ campaignInfo.endDate ?? '无结束日期' }}</span>
+        <span class="head-span">广告组合: </span> <span>{{ campaignInfo?.portfolioName }}</span>
+        <span class="head-span">竞价: </span> <span>{{ getEnumLabel(dynBidOptimizationEnum, campaignInfo.bidOptimization) }}</span>
+      </div>
+    </div>
+    <el-tabs type="border-card" class="asj-detail-tabs" v-model="tabActiveName">
+      <el-tab-pane label="广告组" name="adGroup">
+        <AdGroups :campaignId="route.query.campaignId" v-if="tabActiveName==='adGroup'"></AdGroups>
+      </el-tab-pane>
+      <el-tab-pane label="预算" name="budget">
+        <Budget :campaignId="route.query.campaignId" v-if="tabActiveName==='budget'"></Budget>
+      </el-tab-pane>
+      <el-tab-pane label="自动化" name="automation">
+        自动化
+      </el-tab-pane>
+      <el-tab-pane label="广告位" name="placement">
+        <Placement :campaignId="route.query.campaignId" v-if="tabActiveName==='placement'"/>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, Ref} from 'vue'
+import {useRoute} from 'vue-router'
+import AdGroups from './adGroups/index.vue'
+import Placement from './placement/index.vue'
+import Budget from './budget/index.vue'
+import {getEnumLabel} from '/@/views/adManage/utils/tools.js'
+import {dynBidOptimizationEnum, dynStatusEnum} from '/@/views/adManage/utils/enum.js'
+import {useShopInfo} from '/@/stores/shopInfo'
+// import { usePublicData } from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+
+import {GetObj} from './api'
+
+const shopInfo = useShopInfo()
+const { profile } = storeToRefs(shopInfo)
+const route = useRoute()
+const campaignInfo: Ref<SpCampaign> = ref({})
+const tabActiveName = ref('adGroup')
+
+onMounted(async () => {
+  const resp = await GetObj(route.query.campaignId)
+  campaignInfo.value = resp.data
+})
+
+</script>
+
+<style lang="scss" scoped>
+
+.head-span {
+  color: rgb(177, 177, 177);
+  margin-left: 40px;
+}
+:deep(.el-tabs--border-card) {
+  border: none;
+}
+</style>

+ 12 - 0
src/views/adManage/sd/campaigns/campaignDetail/placement/api.ts

@@ -0,0 +1,12 @@
+import { request } from '/@/utils/service';
+import { UserPageQuery } from '@fast-crud/fast-crud';
+
+
+export const apiPrefix = '/api/ad_manage/sbcampaigndetail/placement/'
+export function GetList(query: UserPageQuery) {
+    return request({
+        url: apiPrefix,
+        method: 'get',
+        params: query,
+    })
+}

+ 89 - 0
src/views/adManage/sd/campaigns/campaignDetail/placement/crud.tsx

@@ -0,0 +1,89 @@
+import * as api from './api'
+import {CreateCrudOptionsProps, CreateCrudOptionsRet, UserPageQuery} from '@fast-crud/fast-crud'
+import {parseQueryParams} from '/@/views/adManage/utils/tools.js'
+import XEUtils from 'xe-utils'
+import {sbCampaignPlacementEnum, spCampaignPlacementEnum} from '/@/views/adManage/utils/enum'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn'
+
+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)
+	}
+
+
+	return {
+		crudOptions: {
+				table: {
+					height: 750,
+					headerCellStyle: {
+						backgroundColor: '#f6f7fa', // 直接设置背景颜色
+						height: '20px',
+						// border: '0.5px solid #ddd',
+					},
+					cellStyle: {
+						border: 'none',
+						borderBottom: '0.5px solid #ddd',
+					},
+				},
+			container: {
+        fixedHeight: false
+      },
+			actionbar: {
+				show: false,
+				buttons: {
+					add: {
+						show: false
+					}
+				}
+			},
+			search: {
+				show: true,
+				buttons: {
+					search: {
+						show: false
+					},
+					reset: {
+						show: false
+					}
+				}
+			},
+			toolbar: {
+        buttons: {
+					search: {
+						show: true
+					},
+					compact: {
+						show: false
+					},
+				}
+			},
+			request: {
+				pageRequest,
+			},
+			pagination: {
+				show: false
+			},
+			rowHandle: {
+        show: false
+      },
+			columns: {
+        placement: {
+          title: '广告位',
+          column: {
+            width: '200px',
+						fixed: 'left',
+						formatter({ value, row, index }){
+								return XEUtils.find(sbCampaignPlacementEnum, (item) => item.value === value).label
+						}
+          }
+        },
+				bidAdjustment: {
+					title: '竞价调整'
+				},
+        ...BaseColumn
+			}
+		}
+	}
+}

+ 74 - 0
src/views/adManage/sd/campaigns/campaignDetail/placement/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <fs-page class="fs-page-custom" v-loading='loading'>
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange"></DateRangePicker>
+      </template>
+      <template v-for="field of Object.keys(BaseColumn)" #[`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>
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small" />
+        </div>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import {BaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+import {LocationQueryValue} from 'vue-router'
+import DataCompare from '/@/components/dataCompare/index.vue'
+
+defineOptions({
+  name: "Placement"
+})
+const publicData = usePublicData()
+const { dateRange } = storeToRefs(publicData)
+interface Props {
+  campaignId: LocationQueryValue | LocationQueryValue[]
+}
+const props = defineProps<Props>()
+// const loading = ref(true)
+
+const showCompare = ref(false)
+
+const queryParams = ref({
+  campaignId: props.campaignId,
+  dateRange
+})
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: queryParams })
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+
+watch(
+  dateRange,
+  async () => crudExpose.doRefresh()
+)
+
+</script>
+
+<style scoped>
+.red {
+  color: red;
+}
+.green {
+  color: #1cbc0e;
+}
+</style>

+ 346 - 0
src/views/adManage/sd/campaigns/chartComponents/adStruct.vue

@@ -0,0 +1,346 @@
+<template>
+  <div v-loading="loading">
+    <el-row :gutter="5">
+      <el-col :span="7">
+        <div>
+          <!--<TextSelector v-model="modelValue" :options="computedPieOptions" @change="changePie" style="margin-top: 5px"/>-->
+          <el-select v-model="modelValue" class="m-2" size="small" @change="changePie" style="width: 120px">
+            <el-option
+                v-for="item in computedPieOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div ref="pie" style="height: 400px;"></div>
+      </el-col>
+      <el-col :span="17">
+        <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="computedsdBarOptions1" @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="computedsdBarOptions2" @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 { computed, inject, onMounted, ref, watch } from "vue"
+import * as echarts from "echarts"
+import TextSelector from '/@/components/TextSelector/index.vue'
+import { getAdStructureData } from "/@/views/adManage/sd/campaigns/api"
+import { createDisabledOptions } from '../../../utils/dropdowndisable'
+import { sdBarOptions1, sdBarOptions2, sdBarOptionsMap, metricMap, pieOptions } from '/@/views/adManage/utils/enum'
+import { useShopInfo } from '/@/stores/shopInfo'
+
+
+const shopInfo = useShopInfo()
+let pieChart = ref()
+let barChart = ref()
+const pie = ref()
+const bar = ref()
+const loading = ref(true)
+
+const dateRange = inject('dateRange')
+
+// 下拉框相关
+let modelValue = ref(pieOptions[0].value)
+let barModelValue1 = ref(sdBarOptions1[0].value)
+let barModelValue2 = ref(sdBarOptions2[2].value)
+
+onMounted(async () => {
+  barChart = echarts.init(bar.value)
+  pieChart = echarts.init(pie.value)
+
+  window.addEventListener('resize', resizeChart)  // 监听窗口大小变化,调整图表大小
+  setTimeout(() => {
+    resizeChart()
+  }, 0)
+  await initPieBarData()
+  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 pieData
+let barData
+let pieBarData
+// 柱状图初始数据
+let ACOSList
+let SpendList
+let xAxisList
+let xAxisMapList
+
+async function initPieBarData() {
+  pieBarData = await setAdStructureData()
+  pieData = [
+    { value: pieBarData.tactic_data[0].Spend, name: '商品' },
+    { value: pieBarData.tactic_data[1].Spend, name: '受众' },
+  ]
+  barData = pieBarData.target_data
+  // 柱状图初始化数据
+  ACOSList = barData.map(item => item.ACOS)
+  SpendList = barData.map(item => item.Spend)
+  // 将x轴映射为中文
+  xAxisList = barData.map(item => item.Name)
+  const classificationMap = {
+    'views': '浏览再营销',
+    'audience': '亚马逊人群',
+    'category': '品类',
+    'purchases': '购买再营销',
+    'asin': '商品'
+  }
+  xAxisMapList = xAxisList.map(item => classificationMap[item])
+  loading.value = false
+}
+
+// 重置图像
+let flag = ref()
+let option
+let option2
+
+// 点击下拉框后重新渲染饼图
+function changePie(newValue) {
+  modelValue.value = newValue
+  flag.value = modelValue.value
+  option2.series[0].data = [
+    { value: pieBarData.tactic_data[0][flag.value], name: '商品' },
+    { value: pieBarData.tactic_data[1][flag.value], name: '受众' },
+  ]
+  pieChart.setOption(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()
+    updatePieChartData(resp)
+    updateBarChartData(resp)
+    loading.value = false
+  }
+})
+
+// 根据新数据和当前下拉框选择更新 饼图数据
+function updatePieChartData(resp) {
+  option2.series[0].data = [
+    { value: resp.tactic_data[0][modelValue.value], name: '商品' },
+    { value: resp.tactic_data[1][modelValue.value], name: '受众' },
+  ]
+  pieChart.setOption(option2)
+}
+
+// 根据新数据和当前下拉框选择更新 柱状图数据
+function updateBarChartData(resp) {
+  const barValues1 = resp.target_data.map(item => item[barModelValue1.value])
+  const barValues2 = resp.target_data.map(item => item[barModelValue2.value])
+
+  option.series[0].data = barValues1
+  option.series[1].data = barValues2
+  barChart.setOption(option)
+}
+
+const computedsdBarOptions1 = computed(() => createDisabledOptions(sdBarOptions1, barModelValue2.value, barModelValue1.value))
+const computedsdBarOptions2 = computed(() => createDisabledOptions(sdBarOptions2, barModelValue1.value, barModelValue2.value))
+const computedPieOptions = computed(() => createDisabledOptions(pieOptions, modelValue.value))
+
+// 初始化图表
+function initChart() {
+  // 柱状图配置
+  option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+      rich: {
+        b: {
+          color: '#4C5058',
+          fontSize: 15,
+          fontWeight: 'bold',
+          lineHeight: 33
+        },
+      }
+    },
+    toolbox: {
+      feature: {
+        saveAsImage: { yAxisIndex: 'none' }
+      }
+    },
+    grid: { top: 55, right: 60, bottom: 55, left: 55, },
+    xAxis: [
+      {
+        type: 'category',
+        boundaryGap: true,
+        data: xAxisMapList,
+        // axisLabel: {
+        //   rotate: -30, // 将标签旋转
+        //   fontSize: 13
+        // }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        // name: '数据1',
+        // axisLabel: {
+        //     formatter: '{value} %'
+        // },
+        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: sdBarOptionsMap[barModelValue1.value],
+        type: 'bar',
+        barWidth: 15,
+        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: [4, 4, 4, 4],
+        },
+      },
+      {
+        name: sdBarOptionsMap[barModelValue2.value],
+        type: 'bar',
+        barWidth: 15,
+        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: [4, 4, 4, 4],
+        },
+      },
+    ],
+  }
+  barChart.setOption(option)
+  // 饼图配置
+  option2 = {
+    tooltip: {
+      show: false,
+      trigger: 'item',
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['20%', '45%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          // borderRadius: 10,
+          borderWidth: 1, // 设置边框的宽度
+          borderColor: '#fff', // 将边框颜色设置为白色或图表背景颜色
+        },
+        emphasis: {
+          label: {
+            show: true,
+            // fontSize: 40,
+            fontWeight: 'bold',
+          }
+        },
+        label: {
+          show: true,
+          position: 'outside', // 标签显示在外侧
+          // formatter: `{b}\n{b|${metricMap[modelValue.value]}:{c}}\n{d}%`, // 标签文本格式器
+          formatter: (params) => {
+            return params.name + '\n' + '{b|' + metricMap[modelValue.value] + ': }' + '{b|' + params.data.value + '}' + '\n' + params.percent + '%'
+          },
+          rich: {
+            b: {
+              color: '#4C5058',
+              fontSize: 15,
+              fontWeight: 'bold',
+              lineHeight: 33
+            },
+          }
+        },
+        labelLine: {
+          normal: {
+            show: true
+          }
+        },
+        data: pieData
+      }
+    ]
+  }
+  pieChart.setOption(option2)
+  resizeChart()
+}
+
+function resizeChart() {
+  barChart.resize()
+  pieChart.resize()
+}
+
+defineExpose({ resizeChart })
+
+</script>
+
+<style scoped>
+.dropdown-scroll {
+  max-height: 200px; /* 设置最大高度,您可以根据需要调整 */
+  overflow-y: auto; /* 垂直方向上溢出时显示滚动条 */
+}
+</style>

+ 261 - 0
src/views/adManage/sd/campaigns/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: 4,
+            }
+        },
+        {
+            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>

+ 173 - 0
src/views/adManage/sd/campaigns/crud.tsx

@@ -0,0 +1,173 @@
+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: true,
+        color: "#626aef",
+        buttons: {
+          add: {
+            show: false
+          },
+          create: {
+            text: '新建广告活动',
+            // type: 'primary',
+            color: "#626aef",
+            plain: true,
+            show: true,
+            click() {
+
+            }
+          },
+        }
+      },
+      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
+          }
+        },
+        campaignName: {
+          title: '广告活动名称',
+          column: {
+            fixed: 'left',
+            width: 180,
+            sortable: true,
+          },
+        },
+        tactic: {
+          title: '投放类型',
+          column: {
+            width: 120,
+            align: 'center',
+            formatter: (row) => {
+              switch (row.value) {
+                case 'T00030':
+                  return '受众'
+                case 'T00020':
+                  return '商品'
+                default:
+                  return '--'
+              }
+            }
+          }
+        },
+        state: {
+          title: '状态',
+          sortable: true,
+          column: {
+            width: 90,
+            align: 'center'
+          },
+          type: 'dict-select',
+          search: {
+            show: true
+          },
+          dict: dict({
+            data: [
+              {value: 'paused', label: '已暂停', color: 'warning'},
+              {value: 'enabled', label: '投放中', color: 'success'},
+            ]
+          })
+        },
+        budget: {
+          title: '每日预算',
+          column: {
+            sortable: true,
+            width: 130,
+            align: 'right',
+          }
+        },
+        ...SdBaseColumn
+      }
+    }
+  }
+}

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

@@ -0,0 +1,132 @@
+<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'"
+                :query="queryParams"
+                :fetchCard="getCardData"
+                :fetchLine="getLineData"
+                :fetch-line-month="getLineMonthData"
+                :fetch-line-week="getLineWeekData">
+            </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 === 'scatterView'">散点视图</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-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_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="compare-switch">
+          <span>数据对比 </span>
+          <el-switch v-model="showCompare" size="small"/>
+        </div>
+      </template>
+    </fs-crud>
+  </fs-page>
+</template>
+
+<script lang="ts" setup>
+import {onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
+import {createCrudOptions} from './crud'
+import {useShopInfo} from '/@/stores/shopInfo'
+import {usePublicData} from '/@/stores/publicData'
+import {storeToRefs} from 'pinia'
+import {useRouter} from 'vue-router'
+import AdStructChart from './chartComponents/adStruct.vue'
+import DataTendencyChart from '/@/views/adManage/sd/chartComponents/dataTendency.vue'
+import {getCardData, getLineData, getLineMonthData, getLineWeekData} from './api'
+import {SdBaseColumn} from '/@/views/adManage/utils/commonTabColumn.js'
+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 router = useRouter()
+const showCompare = ref(false)
+
+const jumpGroup = (row: any) => {
+  router.push({
+    name: 'SbCampaignDetail',
+    query: {campaignId: row.campaignId, tagsViewName: row.campaignName},
+  })
+}
+
+onMounted(async () => {
+  crudExpose.doRefresh()
+})
+
+watch(queryParams, async () => {
+  crudExpose.doRefresh()
+}, {deep: true})
+
+</script>
+
+<style lang="scss" scoped>
+.compare-switch {
+  flex: none;
+}
+
+::v-deep(.el-table__footer-wrapper td.el-table__cell:nth-child(n+2):nth-child(-n+4) .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--border .el-table__footer-wrapper) {
+  border: none;
+}
+
+::v-deep(.el-table .el-table__footer-wrapper .cell) {
+  font-weight: 600;
+}
+</style>

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

@@ -0,0 +1,321 @@
+<template>
+  <div v-loading="loading">
+    <MetricsCards v-model="metrics" :metric-items="metricsItems" @change="changeMetric"></MetricsCards>
+    <el-radio-group v-model="statDim" class="chart-button-group" @change="changeStatDim">
+      <el-radio-button label="day">日</el-radio-button>
+      <el-radio-button label="week" :disabled="!props.fetchLineWeek">周</el-radio-button>
+      <el-radio-button label="month" :disabled="!props.fetchLineWeek">月</el-radio-button>
+    </el-radio-group>
+    <div style="height: 350px;" ref="chartRef"></div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import {computed, onBeforeUnmount, onMounted, Ref, ref, watch} from 'vue'
+import * as echarts from 'echarts'
+import {sdCampaignMetricsEnum} from '/@/views/adManage/utils/enum.js'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+import XEUtils from 'xe-utils'
+import {buildChartOpt, parseQueryParams} from '/@/views/adManage/utils/tools.js'
+
+// import { useShopInfo } from '/@/stores/shopInfo'
+// import { usePublicData } from '/@/stores/publicData'
+// import { storeToRefs } from 'pinia'
+
+defineOptions({
+  name: "DataTendencyChart"
+})
+
+interface Props {
+  fetchCard: Function,
+  fetchLine: Function,
+  fetchLineMonth?: Function,
+  fetchLineWeek?: Function,
+  query: {[key: string]: any},
+  initMetric?: ShowMetric[],
+  metricEnum?: {[key: string]: string}[]
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  initMetric: () => [
+    {metric: 'Impression', color: '#0085ff', 'label': '曝光量'},
+    {metric: 'Click', color: '#3fd4cf', 'label': '点击量'},
+    {metric: 'Spend', color: '#ff9500', 'label': '花费'},
+  ],
+  metricEnum: () => sdCampaignMetricsEnum
+})
+
+const metrics = ref(props.initMetric)
+// const shopInfo = useShopInfo()
+// const publicData = usePublicData()
+// const { dateRange } = storeToRefs(publicData)
+const metricsItems: Ref<MetricData[]> = ref([])
+let chartObj:any
+const chartRef = ref()
+const statDim = ref('day')
+const option: any = {
+  dataset: {
+    source: []
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      label: {
+        backgroundColor: '#6a7985'
+      }
+    }
+  },
+  legend: {
+    selected: {},  // 控制显隐
+    show: false
+  },
+  grid: {
+    top: 50, right: 150, bottom: 30, left: 65,
+  },
+  xAxis: {
+    type: 'category'
+  },
+  yAxis: [
+    {
+      id: 0,
+      type: 'value',
+      name: '',
+      splitLine: {
+        show: true // 设置显示分割线
+      },
+      axisLine: {
+        show: true,
+				lineStyle: { color: '' }
+      },
+      show: true
+    },
+    {
+      id: 1,
+      type: 'value',
+      name: '',
+      position: 'right',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+				lineStyle: {
+					color: ''
+				}
+      },
+      show: true
+    },
+    {
+      id: 2,
+      type: 'value',
+      position: 'right',
+      offset: 90,
+      name: '',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+				lineStyle: {
+					color: ''
+				}
+      },
+      show: true
+    }
+  ],
+  series: [
+    {
+      id: 0,
+      name: '',
+      type: 'bar',
+      encode: {
+        x: 'Name',
+        y: ''
+      },
+      barWidth: '18px',
+      yAxisIndex: 0,
+      itemStyle: {
+        color: '',
+        borderRadius: 4,
+      }
+    },
+    {
+      id: 1,
+      name: '',
+      type: 'line',
+      encode: {
+        x: 'Name',
+        y: ''
+      },
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      yAxisIndex: 1,
+      itemStyle: {
+        // color: '#ff9500',
+        // borderColor: '#ff9500'
+      },
+      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: 'Name',
+        y: ''
+      },
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      yAxisIndex: 2,
+      itemStyle: {},
+      areaStyle: {},
+      emphasis: {
+        focus:'series'
+      }
+    }
+  ]
+}
+const loading = ref(true)
+const queryParams = computed(() => parseQueryParams(props.query))
+
+onMounted(() => {
+  getMetricsItems()
+	addResize()
+  // initLine()
+  setTimeout(() => { initLine() }, 0)
+})
+onBeforeUnmount(() => {
+	if(chartObj) {
+		chartObj.dispose()
+    chartObj = null
+	}
+  removeResize()
+})
+
+const initLine = async () => {
+	chartObj = echarts.init(chartRef.value)
+	const items = await getDataset()
+	option.dataset.source = items
+
+  XEUtils.arrayEach(option.series, (info:any, index) => {
+    const color = metrics.value[index].color
+    info.name = metrics.value[index].label
+    info.encode.y = metrics.value[index].metric
+    if (info.type === 'bar') {
+      info.itemStyle.color = color
+    } else {
+      info.itemStyle = { color: color, borderColor: color }
+      info.areaStyle = {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: color + '53' },
+          { offset: 1, color: color + '03' },
+        ])
+      }
+    }
+  })
+  XEUtils.arrayEach(option.yAxis, (info:any, index) => {
+    info.name = metrics.value[index].label
+    info.axisLine.lineStyle.color = metrics.value[index].color
+  })
+
+  XEUtils.arrayEach(props.metricEnum, info => {
+    option.legend.selected[info.label] = false
+  })
+  for(const info of metrics.value) {
+    option.legend.selected[info.label] = true
+  }
+  // console.log(option)
+	chartObj.setOption(option)
+  loading.value = false
+}
+const getDataset = async () => {
+	if (statDim.value === 'week') {
+    if (props.fetchLineWeek) {
+      const resp = await props.fetchLineWeek(queryParams.value)
+      return resp.data
+    }
+  } else if (statDim.value === 'month') {
+    if (props.fetchLineMonth) {
+      const resp = await props.fetchLineMonth(queryParams.value)
+      return resp.data
+    }
+  } else {
+    const resp = await props.fetchLine(queryParams.value)
+    return resp.data
+  }
+}
+const getMetricsItems = async () => {
+	const resp = await props.fetchCard(queryParams.value)
+	const data = resp.data
+  metricsItems.value.length = 0
+	XEUtils.arrayEach(props.metricEnum, 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)
+}
+
+const changeStatDim = async () => {
+  loading.value = true
+  let source = await getDataset()
+  if (source.length > 0) {
+    chartObj.setOption({dataset: {source: source}})
+  }
+  loading.value = false
+}
+
+watch(
+  props.query,
+  async () => {
+    // console.log("------watch-----queryParams", props.query)
+    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%;
+}
+
+.chart-button-group {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 5px;
+}
+</style>

+ 80 - 0
src/views/adManage/sd/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="asj-container">
+    <div class="public-search">
+      <DateRangePicker v-model="dateRange"></DateRangePicker>
+      <el-select
+          v-model="selectedPortfolios"
+          placeholder="广告组合"
+          clearable
+          multiple
+          style="width: 400px"
+          collapse-tags
+          collapse-tags-tooltip
+          :max-collapse-tags="3"
+      >
+        <el-option v-for="info of portfolios" :label="info.name" :value="info.portfolioId"></el-option>
+      </el-select>
+    </div>
+    <div class="asj-tabs">
+      <div v-for="tab of tabs" :key="tab.name" :class="['asj-tab', { active: tabActiveName === tab.name }]" @click="tabActiveName = tab.name">
+        {{ tab.label }}
+      </div>
+    </div>
+    <component :is="tabsComponents[tabActiveName]"></component>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onBeforeMount, Ref, watch, provide } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { usePublicData } from '/@/stores/publicData'
+import { storeToRefs } from 'pinia'
+import { GetAllPortfolios } from '/@/views/adManage/portfolios/api'
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import Campaigns from '/@/views/adManage/sd/campaigns/index.vue'
+import Keywords from '/@/views/adManage/sb/keywords/index.vue'
+import Targets from '/@/views/adManage/sb/targets/index.vue'
+import SearchTerm from '/@/views/adManage/sb/searchTerm/index.vue'
+import AdvertisedProducts from './advertisedProducts/index.vue'
+import PurchasedOtherProducts from './purchasedOtherProducts/index.vue'
+import Placement from '/@/views/adManage/sb/placement/index.vue'
+
+// const shopInfo = useShopInfo()
+const publicData = usePublicData()
+const selectedPortfolios: Ref<string[]> = ref([])
+const portfolios: Ref<Portfolio[]> = ref([])
+const { dateRange } = storeToRefs(publicData)
+const tabActiveName = ref('Campaigns')
+const tabsComponents: any = {
+  Campaigns,
+  // Keywords,
+  // Targets,
+  // AdvertisedProducts,
+  // PurchasedOtherProducts,
+  // SearchTerm,
+  // Placement
+}
+const tabs = [
+  { label: '广告活动', name: 'Campaigns' },
+  // { label: '关键词', name: 'Keywords' },
+  // { label: '商品投放', name: 'Targets' },
+  // { label: '推广商品', name: 'AdvertisedProducts' },
+  // { label: '购买的其他商品', name: 'PurchasedOtherProducts' },
+  // { label: '搜索词', name: 'SearchTerm' },
+  // { label: '广告位', name: 'Placement' }
+]
+
+provide('dateRange', dateRange)
+
+onBeforeMount(async () => {
+  const resp: APIResponseData = await GetAllPortfolios()
+  portfolios.value = resp.data
+})
+
+</script>
+
+<style scoped>
+::v-deep(.el-table .el-table__header-wrapper .cell) {
+  border-right: 1px solid rgb(218, 221, 223);
+}
+</style>

+ 3 - 2
src/views/adManage/sp/advertisedProducts/crud.tsx

@@ -33,14 +33,15 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
-        },
+        stripe: false
+      },
       container: {
         fixedHeight: false
       },

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

@@ -33,13 +33,14 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
+        stripe: false
       },
       container: {
         fixedHeight: false

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

@@ -57,13 +57,13 @@
 </template>
 
 <script lang="ts" setup>
-import {ref, onMounted, computed, watch, onBeforeMount} from 'vue'
-import {useFs, FsPage} from '@fast-crud/fast-crud'
+import {onMounted, ref, watch} from 'vue'
+import {FsPage, useFs} from '@fast-crud/fast-crud'
 import {createCrudOptions} from './crud'
 import {useShopInfo} from '/@/stores/shopInfo'
 import {usePublicData} from '/@/stores/publicData'
 import {storeToRefs} from 'pinia'
-import {useRoute, useRouter} from 'vue-router'
+import {useRouter} from 'vue-router'
 import AdStructChart from './chartComponents/adStruct.vue'
 import DataTendencyChart from '/@/views/adManage/sp/chartComponents/dataTendency.vue'
 import {getCardData, getLineData, getLineMonthData, getLineWeekData} from './api'

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

@@ -11,12 +11,12 @@
 </template>
 
 <script lang="ts" setup>
-import { ref,onMounted, onBeforeUnmount, Ref, unref, watch, computed } from 'vue'
+import {computed, onBeforeUnmount, onMounted, Ref, ref, watch} from 'vue'
 import * as echarts from 'echarts'
-import {sbCampaignMetricsEnum, spCampaignMetricsEnum} from '/@/views/adManage/utils/enum.js'
+import {spCampaignMetricsEnum} from '/@/views/adManage/utils/enum.js'
 import MetricsCards from '/@/components/MetricsCards/index.vue'
 import XEUtils from 'xe-utils'
-import { buildChartOpt, parseQueryParams } from '/@/views/adManage/utils/tools.js'
+import {buildChartOpt, parseQueryParams} from '/@/views/adManage/utils/tools.js'
 
 // import { useShopInfo } from '/@/stores/shopInfo'
 // import { usePublicData } from '/@/stores/publicData'
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
     {metric: 'Click', color: '#3fd4cf', 'label': '点击量'},
     {metric: 'Spend', color: '#ff9500', 'label': '花费'},
   ],
-  metricEnum: () => sbCampaignMetricsEnum
+  metricEnum: () => spCampaignMetricsEnum
 })
 
 const metrics = ref(props.initMetric)

+ 5 - 1
src/views/adManage/sp/index.vue

@@ -74,4 +74,8 @@ onBeforeMount(async () => {
 
 </script>
 
-<style scoped></style>
+<style scoped>
+::v-deep(.el-table .el-table__header-wrapper .cell) {
+  border-right: 1px solid rgb(218, 221, 223);
+}
+</style>

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

@@ -30,7 +30,17 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
   return {
     crudOptions: {
       table: {
-        height: 800
+        height: 800,
+        headerCellStyle: {
+          backgroundColor: '#f6f7fa', // 直接设置背景颜色
+          height: '20px',
+          borderRight: 'none',
+        },
+        cellStyle: {
+          border: 'none',
+          borderBottom: '0.5px solid #ddd',
+        },
+        stripe: false
       },
       container: {
         fixedHeight: false

+ 2 - 1
src/views/adManage/sp/placement/crud.tsx

@@ -32,13 +32,14 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
+        stripe: false
       },
       container: {
         fixedHeight: false

+ 2 - 1
src/views/adManage/sp/purchasedOtherProducts/crud.tsx

@@ -22,13 +22,14 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 				headerCellStyle: {
 					backgroundColor: '#f6f7fa', // 直接设置背景颜色
 					height: '20px',
-					// border: '0.5px solid #ddd',
+					borderRight: 'none',
 				},
 				cellStyle: {
 					border: 'none',
 					borderBottom: '0.5px solid #ddd',
 				},
 				showSummary: true,
+				stripe: false
 			},
 			container: {
 				fixedHeight: false,

+ 71 - 60
src/views/adManage/sp/searchTerm/crud.tsx

@@ -1,66 +1,77 @@
 import * as api from './api'
-import { dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud'
-import { inject } from 'vue'
-import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js'
-import { parseQueryParams } from '/@/views/adManage/utils/tools.js'
+import {dict, UserPageQuery, compute, CreateCrudOptionsProps, CreateCrudOptionsRet} from '@fast-crud/fast-crud'
+import {inject} from 'vue'
+import {BaseColumn} 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)
-	}
+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 hasPermissions = inject('$hasPermissions')
+  //权限判定
+  const hasPermissions = inject('$hasPermissions')
 
-	return {
-		crudOptions: {
-			table: {
-				height: 800,
-				rowKey: (row: any) => row.keywordId + row.searchTerm,
-			},
-			container: {
-				fixedHeight: false,
-			},
-			actionbar: {
-				show: false,
-			},
-			search: {
-				show: false,
-			},
-			toolbar: {
-				buttons: {
-					search: {
-						show: true,
-					},
-					compact: {
-						show: false,
-					},
-				},
-			},
-			request: {
-				pageRequest,
-			},
-			rowHandle: {
-				show: false,
-			},
-			columns: {
-				searchTerm: {
-					title: '搜索词',
-				},
-				matchType: {
-					title: '定向',
-				},
-				campaignName: {
-					title: '广告活动',
-				},
-				adGroupName: {
-					title: '广告组',
-				},
-				...BaseColumn,
-			},
-		},
-	}
+  return {
+    crudOptions: {
+      table: {
+        height: 800,
+        rowKey: (row: any) => row.keywordId + row.searchTerm,
+        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,
+      },
+      search: {
+        show: false,
+      },
+      toolbar: {
+        buttons: {
+          search: {
+            show: true,
+          },
+          compact: {
+            show: false,
+          },
+        },
+      },
+      request: {
+        pageRequest,
+      },
+      rowHandle: {
+        show: false,
+      },
+      columns: {
+        searchTerm: {
+          title: '搜索词',
+        },
+        matchType: {
+          title: '定向',
+        },
+        campaignName: {
+          title: '广告活动',
+        },
+        adGroupName: {
+          title: '广告组',
+        },
+        ...BaseColumn,
+      },
+    },
+  }
 }

+ 2 - 1
src/views/adManage/sp/targets/crud.tsx

@@ -33,13 +33,14 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
         headerCellStyle: {
           backgroundColor: '#f6f7fa', // 直接设置背景颜色
           height: '20px',
-          // border: '0.5px solid #ddd',
+          borderRight: 'none',
         },
         cellStyle: {
           border: 'none',
           borderBottom: '0.5px solid #ddd',
         },
         showSummary: true,
+        stripe: false
       },
       container: {
         fixedHeight: false

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

@@ -705,6 +705,350 @@ export const SbBaseColumn = {
   }
 }
 
+export const SdBaseColumn = {
+  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) => {
+        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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  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>
+        )
+      }
+    }
+  },
+  ViewImpression: {
+    title: '可见曝光量',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '140px',
+      border: '0.5px solid #ddd',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top"
+                            content="来自亚马逊广告API,符合媒体评级委员会(Media Ratings Council)可见标准的曝光量">
+            <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) => {
+        if (row.value != null) {
+          return row.value
+        } else {
+          return '--'
+        }
+      }
+    }
+  },
+  VCPM: {
+    title: '千次可见曝光成本',
+    column: {
+      align: 'right',
+      sortable: true,
+      width: '180px',
+      border: '0.5px solid #ddd',
+      renderHeader() {
+        return (
+            <span>
+                <el-tooltip placement="top"
+                            content="千次可见曝光成本,系统计算,花费/可见曝光量*1000">
+            <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) => {
+        if (row.value != null) {
+          return row.value
+        } else {
+          return '--'
+        }
+      }
+    }
+  },
+  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) {
   return function(number) {

+ 269 - 11
src/views/adManage/utils/enum.ts

@@ -58,6 +58,130 @@ export const sbCampaignMetricsEnum = [
   {label: '视频取消静音数', value: 'VideoUnmutes'},
   {label: '可见曝光量', value: 'ViewableImpression'},
 ]
+export const sbBarOptions1= [
+  {label: '曝光量', value: 'Impression'},
+  {label: '点击量', value: 'Click'},
+  {label: '花费', value: 'Spend'},
+  {label: '订单量', value: 'TotalPurchases'},
+  {label: '销售额', value: 'TotalSales'},
+  {label: '销量', value: 'TotalUnitOrdered'},
+  {label: '点击率', value: 'CTR'},
+  {label: '点击成本', value: 'CPC'},
+  {label: '转化率', value: 'PurchasesRate'},
+  {label: 'ACOS', value: 'ACOS'},
+  {label: 'ROAS', value: 'ROAS'},
+  {label: '订单成本', value: 'CPA'},
+  {label: '推广商品销售额', value: 'TotalSalesSameSKU'},
+  {label: '其它商品销售额', value: 'TotalSalesOtherSKU'},
+  {label: '推广商品订单量', value: 'TotalPurchasesSameSKU'},
+  {label: '其它商品订单量', value: 'TotalPurchasesOtherSKU'},
+  {label: '推广商品销量', value: 'TotalUnitOrderedSameSKU'},
+  {label: '其它商品销量', value: 'TotalUnitOrderedOtherSKU'},
+  {label: '详情页浏览次数', value: 'DPV'},
+  {label: '详情页浏览率', value: 'DPVR'},
+  {label: '新客订单数', value: 'NTBOrder'},
+  {label: '新客订单占比', value: 'NTBOrderRate'},
+  {label: '新客销售额', value: 'NTBSales'},
+  {label: '新客销售占比', value: 'NTBSalesRate'},
+  {label: '新客销量', value: 'NTBUnitOrdered'},
+  {label: '新客销量占比', value: 'NTBUnitOrderedRate'},
+  {label: '浏览点击率', value: 'VCTR'},
+  {label: '可见曝光率', value: 'VIR'},
+  {label: '浏览率', value: 'VTR'},
+  {label: '播放5秒或完成数占比', value: 'Video5SecondViewRate'},
+  {label: '播放5秒或完成数', value: 'Video5SecondViews'},
+  {label: '视频播放25%', value: 'VideoFirstQuartileViews'},
+  {label: '视频播放50%', value: 'VideoMidpointViews'},
+  {label: '视频播放75%', value: 'VideoThirdQuartileViews'},
+  {label: '视频播放100%', value: 'VideoCompleteViews'},
+  {label: '视频取消静音数', value: 'VideoUnmutes'},
+  {label: '可见曝光量', value: 'ViewableImpression'},
+]
+export const sbBarOptions2= [
+  {label: '曝光量', value: 'Impression'},
+  {label: '点击量', value: 'Click'},
+  {label: '花费', value: 'Spend'},
+  {label: '订单量', value: 'TotalPurchases'},
+  {label: '销售额', value: 'TotalSales'},
+  {label: '销量', value: 'TotalUnitOrdered'},
+  {label: '点击率', value: 'CTR'},
+  {label: '点击成本', value: 'CPC'},
+  {label: '转化率', value: 'PurchasesRate'},
+  {label: 'ACOS', value: 'ACOS'},
+  {label: 'ROAS', value: 'ROAS'},
+  {label: '订单成本', value: 'CPA'},
+  {label: '推广商品销售额', value: 'TotalSalesSameSKU'},
+  {label: '其它商品销售额', value: 'TotalSalesOtherSKU'},
+  {label: '推广商品订单量', value: 'TotalPurchasesSameSKU'},
+  {label: '其它商品订单量', value: 'TotalPurchasesOtherSKU'},
+  {label: '推广商品销量', value: 'TotalUnitOrderedSameSKU'},
+  {label: '其它商品销量', value: 'TotalUnitOrderedOtherSKU'},
+  {label: '详情页浏览次数', value: 'DPV'},
+  {label: '详情页浏览率', value: 'DPVR'},
+  {label: '新客订单数', value: 'NTBOrder'},
+  {label: '新客订单占比', value: 'NTBOrderRate'},
+  {label: '新客销售额', value: 'NTBSales'},
+  {label: '新客销售占比', value: 'NTBSalesRate'},
+  {label: '新客销量', value: 'NTBUnitOrdered'},
+  {label: '新客销量占比', value: 'NTBUnitOrderedRate'},
+  {label: '浏览点击率', value: 'VCTR'},
+  {label: '可见曝光率', value: 'VIR'},
+  {label: '浏览率', value: 'VTR'},
+  {label: '播放5秒或完成数占比', value: 'Video5SecondViewRate'},
+  {label: '播放5秒或完成数', value: 'Video5SecondViews'},
+  {label: '视频播放25%', value: 'VideoFirstQuartileViews'},
+  {label: '视频播放50%', value: 'VideoMidpointViews'},
+  {label: '视频播放75%', value: 'VideoThirdQuartileViews'},
+  {label: '视频播放100%', value: 'VideoCompleteViews'},
+  {label: '视频取消静音数', value: 'VideoUnmutes'},
+  {label: '可见曝光量', value: 'ViewableImpression'},
+]
+
+export const sdCampaignMetricsEnum = [
+  {label: '花费', value: 'Spend'},
+  {label: '销售额', value: 'TotalSales'},
+  {label: '销售额(vCPM)', value: 'TotalUnitOrderedVCPM'},
+  {label: '销售额(Click)', value: 'TotalSalesClick'},
+  {label: '订单数', value: 'TotalPurchases'},
+  {label: '订单数(vCPM)', value: 'TotalPurchasesVCPM'},
+  {label: '订单数(Click)', value: 'TotalPurchasesClick'},
+  {label: 'ACOS', value: 'ACOS'},
+  {label: 'ROAS', value: 'ROAS'},
+  {label: '点击成本', value: 'CPC'},
+  {label: '千次可见曝光成本', value: 'VCPM' },
+  {label: '转化率', value: 'PurchasesRate'},
+  {label: '订单成本', value: 'CPA'},
+  {label: '销量', value: 'TotalUnitOrdered'},
+  {label: '销量(vCPM)', value: 'TotalUnitOrdered'},
+  {label: '销量(Click)', value: 'TotalPurchasesClick'},
+  {label: '曝光量', value: 'Impression'},
+  {label: '点击量', value: 'Click'},
+  {label: '点击率', value: 'CTR'},
+  {label: '可见曝光量', value: 'ViewableImpression'},
+  {label: '可见曝光率', value: 'VIR'},
+  {label: '详情页浏览次数', value: 'DPV'},
+  {label: '详情页浏览率', value: 'DPVR'},
+  {label: '商品详情页浏览次数(vCPM)', value: 'DPVVCPM'},
+  {label: '商品详情页浏览次数(Click)', value: 'DPVClick'},
+  // {label: '浏览订单', value: 'DPVR'},
+  {label: '推广商品销售额', value: 'TotalSalesSameSKU'},
+  {label: '其它商品销售额', value: 'TotalSalesOtherSKU'},
+  {label: '推广商品订单数', value: 'TotalPurchasesSameSKU'},
+  {label: '其它商品订单数', value: 'TotalPurchasesOtherSKU'},
+  {label: '新客订单数', value: 'NTBOrder'},
+  {label: '新客订单占比', value: 'NTBOrderRate'},
+  {label: '新客销售额', value: 'NTBSales'},
+  {label: '新客销售占比', value: 'NTBSalesRate'},
+  {label: '新客销量', value: 'NTBUnitOrdered'},
+  {label: '新客销量占比', value: 'NTBUnitOrderedRate'},
+  {label: '视频播放25%', value: 'VideoFirstQuartileViews'},
+  {label: '视频播放50%', value: 'VideoMidpointViews'},
+  {label: '视频播放75%', value: 'VideoThirdQuartileViews'},
+  {label: '视频播放100%', value: 'VideoCompleteViews'},
+  {label: '视频取消静音数', value: 'VideoUnmutes'},
+  {label: '浏览率(VTR)', value: 'VTR'},
+  {label: '浏览点击率(vCTR)', value: 'VCTR'},
+]
 
 export const spCampaignPuchasedOtherProductsMetricsEnum = [
   {label: '其它商品订单数', value: 'TotalPurchasesOtherSKU'},
@@ -138,27 +262,141 @@ export const metricMap = {
   'Click': '点击量',
 }
 
-export const barOptionsMap = {
-  'ACOS': 'ACOS',
-  'ROAS': 'ROAS',
+
+export const sdBarOptionsMap = {
   'Spend': '花费',
   'TotalSales': '销售额',
+  'TotalSalesVCPM': '销售额(vCPM)',
+  'TotalSalesClick': '销售额(Click)',
   'TotalPurchases': '订单数',
-  'TotalUnitOrdered': '销量',
+  'TotalPurchasesVCPM': '订单数(vCPM)',
+  'TotalPurchasesClick': '订单数(Click)',
+  'ACOS': 'ACOS',
+  'ROAS': 'ROAS',
   'CPC': '点击成本',
+  'VCPM': '千次可见曝光成本',
+  'PurchasesRate': '转化率',
   'CPA': '订单成本',
+  'TotalUnitOrdered': '销量',
+  'TotalUnitOrderedVCPM': '销量(vCPM)',
+  'TotalUnitOrderedClick': '销量(Click)',
   'Impression': '曝光量',
   'Click': '点击量',
   'CTR': '点击率',
-  'PurchasesRate': '转化率',
+  'ViewableImpression': '可见曝光量',
+  'VIR': '可见曝光率',
+  'DPV': '详情页浏览次数',
+  'DPVR': '详情页浏览率',
+  'DPVVCPM': '商品详情页浏览次数(vCPM)',
+  'DPVClick': '商品详情页浏览次数(Click)',
+  // '浏览订单': 'DPVR',
   'TotalSalesSameSKU': '推广商品销售额',
-  'TotalSalesOtherSKU': '其他商品销售额',
+  'TotalSalesOtherSKU': '其商品销售额',
   'TotalPurchasesSameSKU': '推广商品订单数',
-  'TotalPurchasesOtherSKU': '其他商品订单数',
-  'TotalUnitOrderedSameSKU': '推广商品销量',
-  'TotalUnitOrderedOtherSKU': '其他商品销量',
-  'TopOfSearchImpressionShare': '搜索结果顶部展示份额'
+  'TotalPurchasesOtherSKU': '其它商品订单数',
+  'NTBOrder': '新客订单数',
+  'NTBOrderRate': '新客订单占比',
+  'NTBSales': '新客销售额',
+  'NTBSalesRate': '新客销售占比',
+  'NTBUnitOrdered': '新客销量',
+  'NTBUnitOrderedRate': '新客销量占比',
+  'VideoFirstQuartileViews': '视频播放25%',
+  'VideoMidpointViews': '视频播放50%',
+  'VideoThirdQuartileViews': '视频播放75%',
+  'VideoCompleteViews': '视频播放100%',
+  'VideoUnmutes': '视频取消静音数',
+  'VTR': '浏览率(VTR)',
+  'VCTR': '浏览点击率(vCTR)'
 }
+export const sdBarOptions1 = [
+  { value: 'Spend', label: '花费' },
+  { value: 'TotalSales', label: '销售额' },
+  { value: 'TotalSalesVCPM', label: '销售额(vCPM)' },
+  { value: 'TotalSalesClick', label: '销售额(Click)' },
+  { value: 'TotalPurchases', label: '订单数' },
+  { value: 'TotalPurchasesVCPM', label: '订单数(vCPM)' },
+  { value: 'TotalPurchasesClick', label: '订单数(Click)' },
+  { value: 'ACOS', label: 'ACOS' },
+  { value: 'ROAS', label: 'ROAS' },
+  { value: 'CPC', label: '点击成本' },
+  { value: 'VCPM', label: '千次可见曝光成本' },
+  { value: 'PurchasesRate', label: '转化率' },
+  { value: 'CPA', label: '订单成本' },
+  { value: 'TotalUnitOrdered', label: '销量' },
+  { value: 'TotalUnitOrderedVCPM', label: '销量(vCPM)' },
+  { value: 'TotalUnitOrderedClick', label: '销量(Click)' },
+  { value: 'Impression', label: '曝光量' },
+  { value: 'Click', label: '点击量' },
+  { value: 'CTR', label: '点击率' },
+  { value: 'ViewableImpression', label: '可见曝光量' },
+  { value: 'VIR', label: '可见曝光率' },
+  { value: 'DPV', label: '详情页浏览次数' },
+  { value: 'DPVR', label: '详情页浏览率' },
+  { value: 'DPVVCPM', label: '商品详情页浏览次数(vCPM)' },
+  { value: 'DPVClick', label: '商品详情页浏览次数(Click)' },
+  { value: 'TotalSalesSameSKU', label: '推广商品销售额' },
+  { value: 'TotalSalesOtherSKU', label: '其它商品销售额' },
+  { value: 'TotalPurchasesSameSKU', label: '推广商品订单数' },
+  { value: 'TotalPurchasesOtherSKU', label: '其它商品订单数' },
+  { value: 'NTBOrder', label: '新客订单数' },
+  { value: 'NTBOrderRate', label: '新客订单占比' },
+  { value: 'NTBSales', label: '新客销售额' },
+  { value: 'NTBSalesRate', label: '新客销售占比' },
+  { value: 'NTBUnitOrdered', label: '新客销量' },
+  { value: 'NTBUnitOrderedRate', label: '新客销量占比' },
+  { value: 'VideoFirstQuartileViews', label: '视频播放25%' },
+  { value: 'VideoMidpointViews', label: '视频播放50%' },
+  { value: 'VideoThirdQuartileViews', label: '视频播放75%' },
+  { value: 'VideoCompleteViews', label: '视频播放100%' },
+  { value: 'VideoUnmutes', label: '视频取消静音数' },
+  { value: 'VTR', label: '浏览率(VTR)' },
+  { value: 'VCTR', label: '浏览点击率(vCTR)' }
+]
+export const sdBarOptions2 = [
+  { value: 'Spend', label: '花费' },
+  { value: 'TotalSales', label: '销售额' },
+  { value: 'TotalSalesVCPM', label: '销售额(vCPM)' },
+  { value: 'TotalSalesClick', label: '销售额(Click)' },
+  { value: 'TotalPurchases', label: '订单数' },
+  { value: 'TotalPurchasesVCPM', label: '订单数(vCPM)' },
+  { value: 'TotalPurchasesClick', label: '订单数(Click)' },
+  { value: 'ACOS', label: 'ACOS' },
+  { value: 'ROAS', label: 'ROAS' },
+  { value: 'CPC', label: '点击成本' },
+  { value: 'VCPM', label: '千次可见曝光成本' },
+  { value: 'PurchasesRate', label: '转化率' },
+  { value: 'CPA', label: '订单成本' },
+  { value: 'TotalUnitOrdered', label: '销量' },
+  { value: 'TotalUnitOrderedVCPM', label: '销量(vCPM)' },
+  { value: 'TotalUnitOrderedClick', label: '销量(Click)' },
+  { value: 'Impression', label: '曝光量' },
+  { value: 'Click', label: '点击量' },
+  { value: 'CTR', label: '点击率' },
+  { value: 'ViewableImpression', label: '可见曝光量' },
+  { value: 'VIR', label: '可见曝光率' },
+  { value: 'DPV', label: '详情页浏览次数' },
+  { value: 'DPVR', label: '详情页浏览率' },
+  { value: 'DPVVCPM', label: '商品详情页浏览次数(vCPM)' },
+  { value: 'DPVClick', label: '商品详情页浏览次数(Click)' },
+  { value: 'TotalSalesSameSKU', label: '推广商品销售额' },
+  { value: 'TotalSalesOtherSKU', label: '其它商品销售额' },
+  { value: 'TotalPurchasesSameSKU', label: '推广商品订单数' },
+  { value: 'TotalPurchasesOtherSKU', label: '其它商品订单数' },
+  { value: 'NTBOrder', label: '新客订单数' },
+  { value: 'NTBOrderRate', label: '新客订单占比' },
+  { value: 'NTBSales', label: '新客销售额' },
+  { value: 'NTBSalesRate', label: '新客销售占比' },
+  { value: 'NTBUnitOrdered', label: '新客销量' },
+  { value: 'NTBUnitOrderedRate', label: '新客销量占比' },
+  { value: 'VideoFirstQuartileViews', label: '视频播放25%' },
+  { value: 'VideoMidpointViews', label: '视频播放50%' },
+  { value: 'VideoThirdQuartileViews', label: '视频播放75%' },
+  { value: 'VideoCompleteViews', label: '视频播放100%' },
+  { value: 'VideoUnmutes', label: '视频取消静音数' },
+  { value: 'VTR', label: '浏览率(VTR)' },
+  { value: 'VCTR', label: '浏览点击率(vCTR)' }
+]
+
 
 export const TargetExpressionEnum = [
   { label: '类目', value: 'ASIN_CATEGORY_SAME_AS' },
@@ -177,6 +415,27 @@ export const TargetExpressionEnum = [
   { label: '其它', value: 'OTHER' },
 ]
 
+export const barOptionsMap = {
+  'ACOS': 'ACOS',
+  'ROAS': 'ROAS',
+  'Spend': '花费',
+  'TotalSales': '销售额',
+  'TotalPurchases': '订单数',
+  'TotalUnitOrdered': '销量',
+  'CPC': '点击成本',
+  'CPA': '订单成本',
+  'Impression': '曝光量',
+  'Click': '点击量',
+  'CTR': '点击率',
+  'PurchasesRate': '转化率',
+  'TotalSalesSameSKU': '推广商品销售额',
+  'TotalSalesOtherSKU': '其他商品销售额',
+  'TotalPurchasesSameSKU': '推广商品订单数',
+  'TotalPurchasesOtherSKU': '其他商品订单数',
+  'TotalUnitOrderedSameSKU': '推广商品销量',
+  'TotalUnitOrderedOtherSKU': '其他商品销量',
+  'TopOfSearchImpressionShare': '搜索结果顶部展示份额'
+}
 export const barOptions1 = [
   {
     value: 'ACOS',
@@ -256,7 +515,6 @@ export const barOptions1 = [
     label: '搜索结果顶部展示份额'
   },
 ]
-
 export const barOptions2 = [
   {
     value: 'ACOS',