Ver código fonte

sp广告活动、广告组、广告数据表格展示

guojing_wu 1 ano atrás
pai
commit
0c22276c19

+ 0 - 10
src/components/DateRangePicker/index.vue

@@ -39,16 +39,6 @@ const changedValue = (newVal: string[]) => {
   emits('change', newVal)
 }
 
-// onMounted(() => {
-//   if(dateRange.value.length === 0 && props.defaultRecentDay > 0) {
-//     const now_tz = dayjs(new Date()).tz(props.timezone)
-//     const start = now_tz.subtract(props.defaultRecentDay, 'day')
-//     const end = now_tz.subtract(1, 'day')
-//     dateRange.value = [start.format("YYYY-MM-DD"), end.format("YYYY-MM-DD")]
-//     emits('update:modelValue', dateRange.value)
-//   }
-// })
-
 function disabledDate(datetime: Date) {
   const now = dayjs(new Date()).tz(props.timezone)
   const now_tz = now.startOf("day")

+ 1 - 1
src/layout/navBars/tagsView/tagsView.vue

@@ -594,7 +594,7 @@ watch(
 	background-color: var(--el-color-white);
 	border-bottom: 1px solid var(--next-border-color-light);
 	position: relative;
-	z-index: 4;
+	z-index: 100;
 	:deep(.el-scrollbar__wrap) {
 		overflow-x: auto !important;
 	}

+ 19 - 8
src/stores/shopInfo.ts

@@ -2,10 +2,11 @@ import { defineStore } from 'pinia'
 import { Profile } from '/@/components/shopSelector/types'
 import { request } from '../utils/service'
 import { ref, Ref } from 'vue'
+import { Session } from '/@/utils/storage';
 
 
 export const useShopInfo = defineStore('shopInfo', () => {
-  let profile: Ref<Profile> = ref({
+  const profile: Ref<Profile> = ref({
     id: 0,
     profile_id: "",
     account_name: "",
@@ -16,13 +17,6 @@ export const useShopInfo = defineStore('shopInfo', () => {
     marketplace_str_id: ""
   })
 
-  async function getShopInfo(id: number) {
-    return request({
-      url: '/api/adManage/profiles/' + id + '/',
-      method: 'GET'
-    })
-  }
-
   function updateShopInfo(obj: Profile) {
     profile.value.id = obj.id
     profile.value.profile_id = obj.profile_id
@@ -32,6 +26,23 @@ export const useShopInfo = defineStore('shopInfo', () => {
     profile.value.country_code = obj.country_code
     profile.value.currency_code = obj.currency_code
     profile.value.marketplace_str_id = obj.marketplace_str_id
+    Session.set('shopInfo', profile.value);
+  }
+
+  async function reqShopInfo(id: number) {
+    return request({
+      url: '/api/adManage/profiles/' + id + '/',
+      method: 'GET'
+    })
+  }
+
+  async function setShopInfo(id: number) {
+    if (Session.get('shopInfo')) {
+      profile.value = Session.get('shopInfo');
+    } else {
+      let resp: any = await reqShopInfo(id);
+      updateShopInfo(resp.data)
+    }
   }
 
   return { profile, updateShopInfo }

+ 25 - 2
src/theme/app.scss

@@ -324,10 +324,33 @@ body,
 }
 
 // 自定义全局样式
-.ads-container {
-  padding: 3px 5px;
+.asj-container {
+  padding: 3px 10px 3px 5px;
 }
 
 .fs-page-custom {
 	position: initial !important;
 }
+
+.asj-header {
+  position: sticky;
+  background-color: #fff;
+  box-shadow: 0px 0px 12px rgba(51,89,181,0.16);
+  z-index: 10;
+  top: 1px;
+  height: 80px;
+  margin-bottom: 3px;
+  display: flex;
+  flex-direction: column;
+}
+.asj-info {
+  margin: 5px;
+  display: flex;
+  flex-direction: row;
+  gap: 30px;
+}
+.asj-tabs > .el-tabs__header.is-top {
+  position: sticky;
+  top: 82px;
+  z-index: 10;
+}

+ 26 - 0
src/types/views.d.ts

@@ -346,3 +346,29 @@ declare interface MetricData extends MetricOptions {
   preVal?: string,
   gapVal?: string
 }
+
+declare interface Portfolio {
+	name: string,
+	portfolioId: string
+}
+
+declare interface SpCampaign {
+	id?: number,
+	campaignId?: string,
+	campaignName?: string,
+	budgetType?: string,
+	budget?: string,
+	startDate?: string,
+	endDate?: string,
+	targetingType?: string,
+	state?: string,
+	dynBidStrategy?: string,
+	portfolio?: Portfolio
+}
+
+declare type SpAdGroup = {
+	id?: number,
+	adGroupName?: string,
+	state?: string,
+	defaultBid?: string
+}

+ 24 - 0
src/views/adManage/portfolios/PortfoliosSelector.vue

@@ -0,0 +1,24 @@
+<template>
+  <el-select v-model="portfolios" placeholder="广告组合">
+    <el-options v-for="info in portfolios" :label="info.name" :value="info.portfolioId"></el-options>
+  </el-select>
+</template>
+
+<script lang="ts" setup>
+import { ref, onBeforeMount, Ref } from 'vue'
+import { GetList } from './api'
+
+defineOptions({
+  name: 'PortfoliosSelector'
+})
+const portfolios:Ref<portfolios[]> = ref([])
+onBeforeMount(async () => {
+  const resp:APIResponseData = await GetList({ limit: 999 })
+  portfolios.value = resp.data
+})
+
+</script>
+
+<style scoped>
+
+</style>

+ 2 - 3
src/views/adManage/portfolios/api.ts

@@ -1,9 +1,8 @@
 import { request } from '/@/utils/service';
-import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
-import XEUtils from 'xe-utils';
+import { AddReq, DelReq, EditReq, InfoReq, UserPageQuery } from '@fast-crud/fast-crud';
 
 export const apiPrefix = '/api/ad_manage/portfolios/';
-export function GetList(query: PageQuery) {
+export function GetList(query: UserPageQuery) {
     return request({
         url: apiPrefix,
         method: 'get',

+ 1 - 1
src/views/adManage/portfolios/index.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="ads-container">
+	<div class="asj-container">
 		<div class="public-search">
 			<DateRangePicker v-model="dateRange" timezone="America/Los_Angeles" ></DateRangePicker>
 		</div>

+ 46 - 0
src/views/adManage/sp/adGroups/adGroupDetail/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="asj-container">
+    <div class="asj-header">
+      <span style="font-size: x-large; font-weight: bold; color: #0f1111;margin: 5px;"> 
+        <span> {{ adGroupInfo.adGroupName }} </span>
+      </span>
+      <div class="asj-info">
+        <span>状态:{{ adGroupInfo.state }}</span>
+        <span>投放类型:</span>
+        <span>默认竞价:{{ adGroupInfo.defaultBid }}</span>
+        <span>投放日期:</span>
+      </div>
+    </div>
+    <el-tabs type="border-card" class="asj-tabs">
+      <el-tab-pane label="商品推广">
+        <Ads />
+      </el-tab-pane>
+      <el-tab-pane label="定向"></el-tab-pane>
+      <el-tab-pane label="否定投放"></el-tab-pane>
+      <el-tab-pane label="搜索关键词"></el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { Ref, ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { GetObj } from '../api'
+import Ads from '/@/views/adManage/sp/ads/index.vue'
+
+const router = useRouter()
+const route = useRoute()
+const adGroupInfo = ref({
+  id: 0, adGroupName: '', state: '', defaultBid: ''
+})
+
+onMounted(async () => {
+  const resp = await GetObj(route.query.id)
+  adGroupInfo.value = resp.data
+})
+
+</script>
+
+<style scoped>
+
+</style>

+ 42 - 0
src/views/adManage/sp/adGroups/api.ts

@@ -0,0 +1,42 @@
+import { request } from '/@/utils/service';
+import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import XEUtils from 'xe-utils';
+
+export const apiPrefix = '/api/ad_manage/spgroups/';
+export function GetList(query: PageQuery) {
+    return request({
+        url: apiPrefix,
+        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 },
+    });
+}

+ 97 - 0
src/views/adManage/sp/adGroups/crud.tsx

@@ -0,0 +1,97 @@
+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 { successMessage } from '/@/utils/message';
+import { BaseColumn } from '/@/views/adManage/utils/commonTabColumn.js';
+
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: UserPageQuery) => {
+		query.campaign = context.campaignId
+		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: {},
+			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:'必填项'}]
+					}
+        },
+				state: {
+					title: '状态'
+				},
+				defaultBid: {
+					title: '默认竞价'
+				},
+				...BaseColumn
+			}
+		}
+	}
+}

+ 61 - 0
src/views/adManage/sp/adGroups/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+      <template #search-left>
+        <DateRangePicker v-model="dateRange" timezone="America/Los_Angeles" style="margin-bottom: 5px;"></DateRangePicker>
+      </template>
+      <template #header-middle>
+        <MetricsCards v-model="metrics" :metric-items="options" @change="changeMetric"></MetricsCards>
+      </template>
+      <template #cell_adGroupName="scope">
+        <el-link type="primary" :underline="false" @click="jumpAds(scope.row)">{{ scope.row.adGroupName }}</el-link>
+      </template>
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup>
+import { Ref, ref, onMounted } from 'vue'
+import { useFs, FsPage } from '@fast-crud/fast-crud';
+import DateRangePicker from '/@/components/DateRangePicker/index.vue'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+import { createCrudOptions } from './crud';
+import { useRoute, useRouter } from 'vue-router'
+
+const router = useRouter()
+const props = defineProps({
+  campaignId: { type: String, required: true }
+})
+const dateRange: Ref<string[]> = ref([])
+const metrics = ref([{metric: 'ACOS', color: 'blue'}])
+const options = ref([
+  {label: 'ACOS', value: 'ACOS', metricVal: "18.00%", preVal: '20.15%', gapVal: '-2.00%', disabled:true},
+  {label: '点击量', value: 'clicks', metricVal: "19.00%", preVal: '20.15%', gapVal: '-1.00%', disabled:true},
+  {label: '曝光量', value: 'impression', metricVal: "20.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率1', value: 'rate1', metricVal: "1.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率2', value: 'rate2', metricVal: "2.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率3', value: 'rate3', metricVal: "3.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率4', value: 'rate4', metricVal: "4.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率5', value: 'rate5', metricVal: "5.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率6', value: 'rate6', metricVal: "6.00%", preVal: '15.00%', gapVal: '5.00%'},
+])
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { campaignId: props.campaignId } });
+
+onMounted(() => {
+	crudExpose.doRefresh();
+})
+const changeMetric = () => {
+	console.log(metrics.value)
+}
+const jumpAds = (row: any) => {
+  router.push({
+    name: 'AdGroupDetail',
+    query: { id: row.id, adGroupId: row.adGroupId, tagsViewName: row.adGroupName }
+  })
+}
+
+</script>
+
+<style scoped>
+
+</style>

+ 42 - 0
src/views/adManage/sp/ads/api.ts

@@ -0,0 +1,42 @@
+import { request } from '/@/utils/service';
+import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
+import XEUtils from 'xe-utils';
+
+export const apiPrefix = '/api/ad_manage/spads/';
+export function GetList(query: PageQuery) {
+    return request({
+        url: apiPrefix,
+        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 },
+    });
+}

+ 114 - 0
src/views/adManage/sp/ads/crud.tsx

@@ -0,0 +1,114 @@
+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'
+
+export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
+	const pageRequest = async (query: UserPageQuery) => {
+		query["adGroup"] = context["adGroupId"]
+		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: {},
+			container: {
+        fixedHeight: false
+      },
+			actionbar: {
+				show: true,
+				buttons: {
+					add: {
+						show: false
+					},
+					create: {
+						text: '添加推广商品',
+						type: "primary",
+						show: true,
+						click() {
+
+						}
+					}
+				}
+			},
+			search: {
+				show: false
+			},
+			toolbar: {
+        buttons: {
+					search: {
+						show: true
+					},
+					compact: {
+						show: false
+					}
+				}
+			},
+			request: {
+				pageRequest,
+				addRequest,
+				editRequest,
+				delRequest,
+			},
+			rowHandle: {
+				fixed: 'right',
+				width: 80,
+				buttons: {
+					view: {
+						show: false,
+					},
+					edit: {
+						iconRight: 'Edit',
+						type: 'text',
+            text: null
+						// show: hasPermissions('dictionary:Update'),
+					},
+					remove: {
+						iconRight: 'Delete',
+						type: 'text',
+            text: null
+						// show: hasPermissions('dictionary:Delete'),
+					},
+				},
+			},
+			columns: {
+        asin: {
+          title: 'ASIN',
+          column: {
+            width: '200px'
+          },
+					search: {
+						show: true,
+						component: {
+							props: {
+								clearable: true
+							}
+						}
+					},
+					form: {
+						rules: [{required: true, message:'必填项'}]
+					}
+        },
+				sku: {
+					title: 'SKU'
+				},
+				state: {
+					title: '状态'
+				}
+			}
+		}
+	}
+}

+ 44 - 0
src/views/adManage/sp/ads/index.vue

@@ -0,0 +1,44 @@
+<template>
+	<fs-page class="fs-page-custom">
+    <fs-crud ref="crudRef" v-bind="crudBinding">
+			<template #header-middle>
+        <MetricsCards v-model="metrics" :metric-items="options" @change="changeMetric"></MetricsCards>
+				<div style="height: 500px; border: 1px solid red;">图形区域</div>
+      </template>
+		</fs-crud>
+	</fs-page>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, onBeforeUnmount, watch, nextTick, onActivated } from 'vue';
+import { useFs, FsPage } from '@fast-crud/fast-crud';
+import { createCrudOptions } from './crud';
+import { useRoute, useRouter } from 'vue-router'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+
+const route = useRoute()
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {adGroupId: route.query.adGroupId} });
+const metrics = ref([{metric: 'ACOS', color: 'blue'}])
+const options = ref([
+  {label: 'ACOS', value: 'ACOS', metricVal: "18.00%", preVal: '20.15%', gapVal: '-2.00%', disabled:true},
+  {label: '点击量', value: 'clicks', metricVal: "19.00%", preVal: '20.15%', gapVal: '-1.00%', disabled:true},
+  {label: '曝光量', value: 'impression', metricVal: "20.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率1', value: 'rate1', metricVal: "1.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率2', value: 'rate2', metricVal: "2.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率3', value: 'rate3', metricVal: "3.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率4', value: 'rate4', metricVal: "4.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率5', value: 'rate5', metricVal: "5.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率6', value: 'rate6', metricVal: "6.00%", preVal: '15.00%', gapVal: '5.00%'},
+])
+
+onMounted(() => {
+	crudExpose.doRefresh();
+})
+const changeMetric = () => {
+
+}
+</script>
+
+<style scoped>
+
+</style>

+ 2 - 2
src/views/adManage/sp/campaigns/api.ts

@@ -2,7 +2,7 @@ import { request } from '/@/utils/service';
 import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
 import XEUtils from 'xe-utils';
 
-export const apiPrefix = '/api/ad_manage/spCampaigns/';
+export const apiPrefix = '/api/ad_manage/spcampaigns/';
 export function GetList(query: PageQuery) {
     return request({
         url: apiPrefix,
@@ -12,7 +12,7 @@ export function GetList(query: PageQuery) {
 }
 export function GetObj(id: InfoReq) {
     return request({
-        url: apiPrefix + id,
+        url: apiPrefix + id + "/",
         method: 'get',
     });
 }

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

@@ -0,0 +1,49 @@
+<template>
+  <div class="asj-container">
+    <div class="asj-header">
+      <span style="font-size: x-large; font-weight: bold; color: #0f1111;margin: 5px;"> 
+        <span> {{ campaignInfo.campaignName }}</span>
+      </span>
+      <div class="asj-info">
+        <span>状态:{{ campaignInfo.state }}</span>
+        <span>预算:${{ campaignInfo.budget }}</span>
+        <span>投放类型:{{ campaignInfo.targetingType }}</span>
+        <span>投放日期:{{ campaignInfo.startDate }} ~ {{ campaignInfo.endDate ?? '无结束日期' }}</span>
+        <span>广告组合:{{ campaignInfo?.portfolio?.name }}</span>
+        <span>竞价策略:{{ campaignInfo.dynBidStrategy }}</span>
+      </div>
+    </div>
+    <el-tabs type="border-card" class="asj-tabs">
+      <el-tab-pane label="广告组">
+        <AdGroups :campaignId="route.query.campaignId"></AdGroups>
+      </el-tab-pane>
+      <el-tab-pane label="预算"></el-tab-pane>
+      <el-tab-pane label="自动化"></el-tab-pane>
+      <el-tab-pane label="广告位"></el-tab-pane>
+      <el-tab-pane label="否定投放"></el-tab-pane>
+      <el-tab-pane label="操作日志"></el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, onBeforeMount, Ref } from 'vue'
+import AdGroups from '/@/views/adManage/sp/adGroups/index.vue'
+import { useRoute, useRouter } from 'vue-router'
+
+import { GetObj } from '../api'
+
+const router = useRouter()
+const route = useRoute()
+const campaignInfo: Ref<SpCampaign> = ref({})
+
+onMounted(async () => {
+  const resp = await GetObj(route.query.id)
+  campaignInfo.value = resp.data
+})
+
+</script>
+
+<style lang="scss">
+
+</style>

+ 52 - 0
src/views/adManage/sp/campaigns/chartComponents/adStruct.vue

@@ -0,0 +1,52 @@
+<template>
+  <div style="height: 500px;" ref="lineRef"></div>
+</template>
+
+<script lang="ts" setup>
+import { ref,onMounted, onBeforeUnmount } from 'vue'
+import * as echarts from 'echarts'
+
+defineOptions({
+  name: "AdStructChart"
+})
+
+let chartObj:any
+const lineRef = ref()
+const resizeChart = () => { chartObj.resize() }
+const addResize = () => { window.addEventListener('resize', resizeChart) }
+const removeResize = () => { window.removeEventListener('resize', resizeChart) }
+
+onMounted(() => {
+	initLine()
+	addResize()
+});
+onBeforeUnmount(() => {
+	if(chartObj) {
+		chartObj.dispose()
+    chartObj = null
+	}
+	removeResize()
+})
+const initLine = () => {
+	chartObj = echarts.init(lineRef.value)
+	const option = {
+		xAxis: {
+			data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+		},
+		yAxis: {},
+		series: [
+			{
+				type: 'bar',
+				data: [23, 24, 18, 25, 27, 28, 25]
+			}
+		]
+	}
+	chartObj.setOption(option)
+}
+defineExpose({resizeChart})
+
+</script>
+
+<style scoped>
+
+</style>

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

@@ -0,0 +1,52 @@
+<template>
+  <div style="height: 500px;" ref="lineRef"></div>
+</template>
+
+<script lang="ts" setup>
+import { ref,onMounted, onBeforeUnmount } from 'vue'
+import * as echarts from 'echarts'
+
+defineOptions({
+  name: "DataTendencyChart"
+})
+
+let chartObj:any
+const lineRef = ref()
+const resizeChart = () => { chartObj.resize() }
+const addResize = () => { window.addEventListener('resize', resizeChart) }
+const removeResize = () => { window.removeEventListener('resize', resizeChart) }
+
+onMounted(() => {
+	initLine()
+	addResize()
+});
+onBeforeUnmount(() => {
+	if(chartObj) {
+		chartObj.dispose()
+    chartObj = null
+	}
+  removeResize()
+})
+const initLine = () => {
+	chartObj = echarts.init(lineRef.value)
+	const option = {
+		xAxis: {
+			data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+		},
+		yAxis: {},
+		series: [
+			{
+				type: 'bar',
+				data: [23, 24, 18, 25, 27, 28, 25]
+			}
+		]
+	}
+	chartObj.setOption(option)
+}
+defineExpose({resizeChart})
+
+</script>
+
+<style scoped>
+
+</style>

+ 14 - 5
src/views/adManage/sp/campaigns/crud.tsx

@@ -2,9 +2,12 @@ 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 { useShopInfo } from '/@/stores/shopInfo'
+
 
 export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
 	const pageRequest = async (query: UserPageQuery) => {
+		query["profile_id"] = context['query']["profile_id"]
 		return await api.GetList(query);
 	};
 	const editRequest = async ({ form, row }: EditReq) => {
@@ -86,10 +89,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 				},
 			},
 			columns: {
-        name: {
+        campaignName: {
           title: '广告活动',
           column: {
-            width: '150px'
+            width: '200px'
           },
 					search: {
 						show: true,
@@ -120,15 +123,21 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
 					title: '状态'
 				},
 				startDate: {
-					title: '开始日期'
+					title: '开始日期',
+					column: {
+            width: '100px'
+          },
 				},
 				endDate: {
-					title: '结束日期'
+					title: '结束日期',
+					column: {
+            width: '100px'
+          },
 				},
 				budget: {
 					title: '预算'
 				},
-				portfolio: {
+				"portfolio.name": {
 					title: '广告组合'
 				},
         ...BaseColumn

+ 60 - 34
src/views/adManage/sp/campaigns/index.vue

@@ -2,55 +2,81 @@
 	<fs-page class="fs-page-custom">
     <fs-crud ref="crudRef" v-bind="crudBinding">
 			<template #header-middle>
-				<el-card style="margin-bottom: 5px;" shadow="hover">
-					<div style="height: 500px;" ref="lineRef" id="line-changes"></div>
-				</el-card>
+				<el-tabs v-model="tabActiveName" class="chart-tabs" type="border-card" @tab-change="changeTab">
+					<el-tab-pane label="数据趋势" name="dataTendency">
+						<MetricsCards v-model="metrics" :metric-items="options" @change="changeMetric"></MetricsCards>
+						<DataTendencyChart ref="dataTendencyRef"></DataTendencyChart>
+					</el-tab-pane>
+					<el-tab-pane label="广告结构" name="adStruct">
+						<AdStructChart ref="adStructChartRef"/>
+					</el-tab-pane>
+					<el-tab-pane label="散点视图" name="s"></el-tab-pane>
+				</el-tabs>
 			</template>
+			<template #cell_campaignName="scope">
+        <el-link type="primary" :underline="false" @click="jumpGroup(scope.row)">{{ scope.row.campaignName }}</el-link>
+      </template>
 		</fs-crud>
 	</fs-page>
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted, onBeforeMount } from 'vue';
+import { ref, onMounted, onBeforeUnmount, watch,nextTick, onActivated } from 'vue';
 import { useFs, FsPage } from '@fast-crud/fast-crud';
 import { createCrudOptions } from './crud';
-import * as echarts from 'echarts';
+import { useShopInfo } from '/@/stores/shopInfo'
+import { useRoute, useRouter } from 'vue-router'
+import MetricsCards from '/@/components/MetricsCards/index.vue'
+import AdStructChart from './chartComponents/adStruct.vue'
+import DataTendencyChart from './chartComponents/dataTendency.vue'
 
-const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
-const lineRef = ref()
-let chartObj:any = null
+const tabActiveName = ref("dataTendency")
+const shopInfo = useShopInfo()
+const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {query: { profile_id: shopInfo.profile.profile_id }} });
 
+const metrics = ref([{metric: 'ACOS', color: 'blue'}])
+const options = ref([
+  {label: 'ACOS', value: 'ACOS', metricVal: "18.00%", preVal: '20.15%', gapVal: '-2.00%', disabled:true},
+  {label: '点击量', value: 'clicks', metricVal: "19.00%", preVal: '20.15%', gapVal: '-1.00%', disabled:true},
+  {label: '曝光量', value: 'impression', metricVal: "20.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率1', value: 'rate1', metricVal: "1.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率2', value: 'rate2', metricVal: "2.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率3', value: 'rate3', metricVal: "3.00%", preVal: '15.00%', gapVal: '5.00%', disabled:true},
+  {label: '转化率4', value: 'rate4', metricVal: "4.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率5', value: 'rate5', metricVal: "5.00%", preVal: '15.00%', gapVal: '5.00%'},
+  {label: '转化率6', value: 'rate6', metricVal: "6.00%", preVal: '15.00%', gapVal: '5.00%'},
+])
+const adStructChartRef = ref()
+const dataTendencyRef = ref()
+const route = useRoute()
+const router = useRouter()
 onMounted(() => {
 	crudExpose.doRefresh();
-	// initLine()
-	setTimeout(() => { initLine() }, 0);
-	window.addEventListener('resize', () => chartObj.resize())
-});
-
-onBeforeMount(() => {
-	if(chartObj) {
-		chartObj.dispose()
-    chartObj = null
-	}
 })
 
-const initLine = () => {
-	chartObj = echarts.init(lineRef.value)
-	const option = {
-		xAxis: {
-			data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-		},
-		yAxis: {},
-		series: [
-			{
-				type: 'bar',
-				data: [23, 24, 18, 25, 27, 28, 25]
-			}
-		]
-	}
-	chartObj.setOption(option)
+const changeTab = () => {
+	nextTick(() => {
+		adStructChartRef.value.resizeChart()
+		dataTendencyRef.value.resizeChart()
+	})
+}
+const changeMetric = () => {
+	console.log(metrics.value)
+}
+const jumpGroup = (row: any) => {
+	router.push({
+		name: 'CampaignDetail',
+		query: { id: row.id, campaignId: row.campaignId, tagsViewName: row.campaignName },
+	})
 }
 </script>
 
-<style scoped>
+<style lang="scss">
+.chart-tabs {
+	margin: 5px 0;
+	.el-tabs__nav {
+		padding-left: 0 !important;
+	}
+}
+
 </style>

+ 38 - 14
src/views/adManage/sp/index.vue

@@ -1,11 +1,13 @@
 <template>
-  <div class="ads-container">
+  <div class="asj-container">
     <div class="public-search">
-      <DateRangePicker v-model="dateRange" timezone="America/Los_Angeles" @change="changeDateRange"></DateRangePicker>
-      <el-select v-model="portfolios" placeholder="广告组合"></el-select>
+      <DateRangePicker v-model="dateRange" timezone="America/Los_Angeles"></DateRangePicker>
+      <el-select v-model="selectedPortfolios" placeholder="广告组合" clearable multiple>
+        <el-option v-for="info of portfolios" :label="info.name" :value="info.portfolioId"></el-option>
+      </el-select>
     </div>
-    <el-tabs>
-      <el-tab-pane label="广告活动">
+    <el-tabs v-model="tabActiveName" class="ad-tabs">
+      <el-tab-pane label="广告活动" name="campaigns">
         <campaigns></campaigns>
       </el-tab-pane>
       <el-tab-pane label="关键词投放"></el-tab-pane>
@@ -19,18 +21,28 @@
 <script lang="ts" setup>
 import DateRangePicker from '/@/components/DateRangePicker/index.vue'
 import campaigns from './campaigns/index.vue'
-import { ref } from 'vue'
+import { recentDaysRange } from '/@/views/adManage/utils/tools'
+import { ref, onBeforeMount, Ref } from 'vue'
+import { useShopInfo } from '/@/stores/shopInfo'
+import { GetList } from '/@/views/adManage/portfolios/api'
 
-const portfolios = ref([])
-const dateRange = ref([])
 
-function changeDateRange(val: []) {
-  
-}
+const shopInfo = useShopInfo()
+const selectedPortfolios: Ref<string[]> = ref([])
+const portfolios: Ref<Portfolio[]> = ref([])
+const dateRange: Ref<string[]> = ref([])
+const tabActiveName = ref("campaigns")
+
+onBeforeMount(async () => {
+  dateRange.value = recentDaysRange(shopInfo.profile.time_zone, 7)
+
+  const resp:APIResponseData = await GetList({ limit: 999 })
+  portfolios.value = resp.data
+})
 
 </script>
 
-<style scoped>
+<style>
 .public-search {
   display: flex;
   gap: 3px;
@@ -42,14 +54,26 @@ function changeDateRange(val: []) {
   background-color: #f8f8f8;
   box-shadow: 0px 0px 0px rgba(51,89,181,0.16);
 }
-:deep(.el-tabs__header.is-top) {
+
+.ad-tabs > .el-tabs__header.is-top {
   background-color: #fff;
   position: sticky;
   top: 32px;
   z-index: 9;
   box-shadow: 0px 0px 12px rgba(51,89,181,0.16);
 }
-:deep(.el-tabs__nav) {
+.ad-tabs .el-tabs__nav {
   padding-left: 10px;
 }
+/* :deep(.el-tabs__header.is-top ) {
+  background-color: #fff;
+  position: sticky;
+  top: 32px;
+  z-index: 9;
+  box-shadow: 0px 0px 12px rgba(51,89,181,0.16);
+} */
+
+/* :deep(.el-tabs__nav) {
+  padding-left: 10px;
+} */
 </style>

+ 8 - 0
src/views/adManage/utils/tools.ts

@@ -0,0 +1,8 @@
+import dayjs, { Dayjs } from 'dayjs'
+
+export function recentDaysRange(timezone: string, days: number):string[] {
+    const now_tz = dayjs(new Date()).tz(timezone)
+    const start = now_tz.subtract(days, 'day')
+    const end = now_tz.subtract(1, 'day')
+    return [start.format("YYYY-MM-DD"), end.format("YYYY-MM-DD")]
+}