Merge remote-tracking branch 'origin/feature/v1.3_营销资产中台' into feature/0707_产品权限

This commit is contained in:
rd
2025-07-08 17:28:32 +08:00
18 changed files with 835 additions and 430 deletions

View File

@ -294,12 +294,29 @@ export const getPlacementAccountProjectsTrend = (params = {}) => {
export const getPlacementGuide = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getGuideList`, params);
};
//查询投放指南历史
export const getPlacementGuideHistory = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getGuideListHistory`, params);
};
// 前端定时轮询获取ai检测结果
export const getAiResult = (params: {}) => {
return Http.get(`/v1/placement-account-projects/getAiResult`, params);
};
export const savePlacementGuide = (params: {}) => {
return Http.post(`/v1/placement-account-projects/saveGuideResult`, params);
};
export const getPlacementGuideDetail = (id: string) => {
return Http.get(`/v1/placement-account-projects/historylog/${id}`);
};
//删除记录
export const deleteHistorylog = (id: string) => {
return Http.delete(`/v1/placement-account-projects/historylog/${id}`);
};
// 投放账号-列表
export const getPlacementAccountsList = (params = {}) => {
return Http.get('/v1/placement-accounts/list', params);
@ -309,3 +326,5 @@ export const getPlacementAccountsList = (params = {}) => {
export const postPlacementAccountsSync = (id: string) => {
return Http.post(`/v1/placement-accounts/${id}/sync-data`);
};

View File

@ -137,13 +137,25 @@ const COMPONENTS: AppRouteRecordRaw[] = [
path: 'investmentGuidelines',
name: 'PutAccountInvestmentGuidelines',
meta: {
locale: '平台投放指南',
locale: '投放指南',
requiresAuth: true,
requireLogin: true,
roles: ['*'],
},
component: () => import('@/views/property-marketing/put-account/investment-guidelines'),
},
{
path: 'detail/:id',
name: 'guideDetail',
meta: {
locale: '投放指南详情',
requiresAuth: true,
hideInMenu: true,
roles: ['*'],
activeMenu: 'PutAccountInvestmentGuidelines',
},
component: () => import('@/views/property-marketing/put-account/investment-guidelines/detail'),
},
],
},
{

View File

@ -3,7 +3,6 @@
* @Date: 2025-06-27 17:36:31
*/
import dayjs from 'dayjs';
export function toFixed(num: number | string, n: number): number {
return parseFloat(parseFloat(num.toString()).toFixed(n));
}

View File

@ -43,7 +43,6 @@ let chartInstance: echarts.ECharts | null = null;
const xAxisData = props.xAxisData;
const seriesData = props.seriesData;
console.log(seriesData, 'seriesData');
const initChart = () => {
if (!chart.value) return;
@ -53,8 +52,6 @@ const initChart = () => {
}
chartInstance = echarts.init(chart.value);
console.log('init');
const option = {
tooltip: {
trigger: 'axis',
@ -98,9 +95,14 @@ const initChart = () => {
};
watch(
() => props.seriesData,
() => {
initChart(); //重新渲染的方法
() => [props.xAxisData, props.seriesData],
([newXAxis, newSeries]) => {
if (chartInstance) {
chartInstance.setOption({
xAxis: newXAxis,
series: newSeries,
});
}
},
{ deep: true },
);

View File

@ -45,7 +45,7 @@
<div class="filter-row-item flex items-center">
<span class="label">时间筛选</span>
<a-space class="w-240px">
<a-range-picker size="medium" allow-clear format="YYYY-MM-DD HH:mm" class="w-100%" />
<a-range-picker v-model="query.data_time" size="medium" allow-clear format="YYYY-MM-DD" class="w-100%" />
</a-space>
</div>
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch">
@ -156,6 +156,7 @@ const query = reactive({
names: '',
platform: '',
operator_id: '',
data_time: [],
});
const xhlEcharts = reactive({});
const getAccountsTrends = async () => {

View File

@ -25,23 +25,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">人群分析</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
18-24岁女性兴趣为美妆/穿搭一线城市抖音平台 ROI 3.2</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(2)" style="width: 25px; height: 17px" />
25-34岁男性兴趣为数码产品二线城市巨量引擎 ROI 2.8</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(3)" style="width: 25px; height: 17px" />
18-24岁男性兴趣为运动/健身三线城市抖音 ROI 2.3</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '人群分析').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '人群分析')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -50,23 +39,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">投放素材</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
图文风格 + 明确福利点CTR 3.2%CVR 8.5%</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
场景短视频 + 明确人设定位CTR 2.7%CVR 7.1%</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
口播讲解类 + 产品对比CTR 2.1%CVR 6.0%</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '投放素材').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '投放素材')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -77,23 +55,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">投放时段</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
晚高峰时段19:0021:00ROI 3.1</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
中午时段11:3013:00ROI 2.5</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
下午茶时段15:0017:00ROI 2.3</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '投放时段').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '投放时段')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -103,23 +70,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">平台表现</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
抖音 - ROI 3.2CVR 8.5%</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(2)" style="width: 25px; height: 17px" />
聚光平台 - ROI 2.7CVR 7.3%</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(3)" style="width: 25px; height: 17px" />
B站 - ROI 2.4CVR 6.8%</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '平台表现').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PERFORMANCE_ANALYSIS, '平台表现')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -140,23 +96,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">人群建议</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
集中在 1824 岁女性 + 精准兴趣标签护肤口红</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(2)" style="width: 25px; height: 17px" />
2430 岁男性 + 实用类内容受众工具控搞机党</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(3)" style="width: 25px; height: 17px" />
泛娱乐向受众 + 较大地域分布兴趣短剧直播带货</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PLACEMENT_SUGGESTION, '人群建议').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PLACEMENT_SUGGESTION, '人群建议')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -167,23 +112,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">素材建议</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
福利明确+钩子强的图文短视频建议加限时优惠提示</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(2)" style="width: 25px; height: 17px" />
场景代入型视频突出客户痛点与产品关联</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(3)" style="width: 25px; height: 17px" />
达人口播/测评搭配标题党封面吸引点击</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PLACEMENT_SUGGESTION, '素材建议').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PLACEMENT_SUGGESTION, '素材建议')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -195,23 +129,12 @@
<div class="overall-strategy">
<span class="placement-optimization-title">投放策略建议</span>
<a-space direction="vertical">
<a-space class="placement-optimization-str">
<span>
<img :src="getStarIcon(1)" style="width: 25px; height: 17px" />
预算前置在 ROI 最佳时段和平台优先抢头部流量</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(2)" style="width: 25px; height: 17px" />
中等预算组合投放 + 高点击素材A/B测试</span
>
</a-space>
<a-space>
<span>
<img :src="getStarIcon(3)" style="width: 25px; height: 17px" />
低预算长周期测试重点看 CVR优胜劣汰</span
>
<a-space
v-if="getSubmoduleContent(MODEL_PLACEMENT_SUGGESTION, '投放策略建议').length > 0"
v-for="(item, index) in getSubmoduleContent(MODEL_PLACEMENT_SUGGESTION, '投放策略建议')"
:key="index"
>
<span><img :src="getStarIcon(index)" style="width: 25px; height: 17px" />{{ item }}</span>
</a-space>
</a-space>
</div>
@ -229,7 +152,28 @@ const props = defineProps({
type: Array,
default: () => [],
},
tmp: {
type: Number,
default: 0,
},
});
const MODEL_PERFORMANCE_ANALYSIS = '表现分析';
const MODEL_PLACEMENT_SUGGESTION = '新投放建议生成';
// 封装通用方法来获取 submodules 的内容
const getSubmoduleContent = (moduleName: string, submoduleName: string) => {
const module = props.action_guide
.find((item) => item.title === moduleName)
?.submodules.find((mod) => mod.subtitle === submoduleName);
const content = [];
if (module) {
const content = Object.values(module.content);
return content;
}
return [];
};
import { getStarIcon } from '../../constants';
import { defineProps } from 'vue';
</script>

View File

@ -1,36 +1,55 @@
<template>
<view>
<div class="part-div">
<div class="part-div-header">
<span class="part-div-header-title">本月摘要</span>
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">本月摘要</p>
</template>
</a-popover>
</div>
<div class="month-data-div">
<div style="align-self: stretch">
<span class="month-text-black">总消耗</span>
<span class="month-text-blue">52,382.16</span>
<span class="month-normal-span">较上周期</span><span class="month-text-red">12.6%</span>
<span class="month-normal-span"><br />整体ROI</span><span class="month-text-blue">2.84</span>
<span class="month-normal-span">属于</span><span class="month-text-red">中等偏高水平</span>
<span class="month-text-black">较上周期 </span><span class="month-text-red">+0.45</span>
<span class="month-normal-span"><br />主要转化来源</span><span class="month-text-blue">抖音 46.3%</span>
<span class="month-normal-span">CTR 2.91%<br />优质素材表现</span>
<span class="month-text-blue">美团酒店爆款横版1号</span>
<span class="month-normal-spanw">CTR 3.47%CVR 5.92%</span>
</div>
<div class="part-div">
<div class="part-div-header">
<span class="part-div-header-title">本月摘要</span>
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">本月摘要2</p>
</template>
</a-popover>
</div>
<div class="month-data-div">
<div style="align-self: stretch">
<a-space direction="vertical" v-for="(item, index) in overview">
<span :style="{ color: getColor(item?.highlight) }">{{ item.text }}</span>
<br />
</a-space>
<!-- <span class="month-text-black">总消耗</span>-->
<!-- <span class="month-text-blue">52,382.16</span>-->
<!-- <span class="month-normal-span">较上周期</span><span class="month-text-red">12.6%</span>-->
<!-- <span class="month-normal-span"><br />整体ROI</span><span class="month-text-blue">2.84</span>-->
<!-- <span class="month-normal-span">属于</span><span class="month-text-red">中等偏高水平</span>-->
<!-- <span class="month-text-black">较上周期 </span><span class="month-text-red">+0.45</span>-->
<!-- <span class="month-normal-span"><br />主要转化来源</span><span class="month-text-blue">抖音 46.3%</span>-->
<!-- <span class="month-normal-span">CTR 2.91%<br />优质素材表现</span>-->
<!-- <span class="month-text-blue">美团酒店爆款横版1号</span>-->
<!-- <span class="month-normal-spanw">CTR 3.47%CVR 5.92%</span>-->
</div>
</div>
</view>
</div>
</template>
<script setup lang="ts">
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
import { defineProps } from 'vue';
const colorMap = {
purple: 'purple',
orange: 'orange',
};
const props = defineProps({
overview: {
type: Array,
default: () => [],
},
});
const getColor = (highlight?: string) => {
return highlight ? colorMap[highlight] : undefined;
};
console.log(props.overview, 'overvie333');
</script>
<style lang="scss">
<style lang="scss">
@import './style.scss';
</style>

View File

@ -1,4 +1,3 @@
//本月摘要数据-div
.month-data-div {
align-self: stretch;
padding: 16px 30px 16px 16px;
@ -15,7 +14,6 @@
display: flex;
}
//本月摘要-蓝色字体
.month-text-blue {
color: var(--Brand-Brand-6, #6D4CFE);
font-size: 16px;
@ -25,7 +23,6 @@
word-wrap: break-word
}
//红色字体
.month-text-red {
color: var(--Functional-Danger-6, #F64B31);
font-size: 16px;
@ -35,7 +32,6 @@
word-wrap: break-word
}
//黑色字体
.month-text-black {
color: var(--Text-1, #211F24);
font-size: 16px;

View File

@ -16,7 +16,7 @@
<a-col :span="24">
<div class="overall-strategy">
<span class="placement-optimization-title">总体策略</span>
<span class="placement-optimization-str">{{props.optimization?.[0]?.['content']}}</span>
<span class="placement-optimization-str">{{ props.optimization?.[0]?.['content'] }}</span>
</div>
</a-col>
</a-row>
@ -24,13 +24,13 @@
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title">预算分配</span>
<span class="placement-optimization-str">{{props.optimization?.[1]?.['content']}}</span>
<span class="placement-optimization-str">{{ props.optimization?.[1]?.['content'] }}</span>
</div>
</a-col>
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title">时段优化</span>
<span class="placement-optimization-str">{{props.optimization?.[2]?.['content']}}</span>
<span class="placement-optimization-str">{{ props.optimization?.[2]?.['content'] }}</span>
</div>
</a-col>
</a-row>
@ -38,14 +38,13 @@
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title">人群包优化</span>
<span class="placement-optimization-str">{{props.optimization?.[3]?.['content']}}</span>
<span class="placement-optimization-str">{{ props?.optimization?.[3]?.['content'] }}</span>
</div>
</a-col>
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title">素材优化</span>
<span class="placement-optimization-str">{{props.optimization?.[4]['content']}}</span>
<span class="placement-optimization-str">{{ props?.optimization?.[4]?.['content'] }}</span>
</div>
</a-col>
</a-row>
@ -65,6 +64,8 @@ const props = defineProps({
default: () => [],
},
});
console.log(props.optimization, 'optimization');
</script>
<style scoped lang="scss">

View File

@ -1,12 +1,12 @@
<template>
<view>
<a-table
class="account-table"
:columns="columns"
:data="listResult.data"
:filter-icon-align-left="alignLeft"
:pagination="false"
>
<a-table class="account-table" :columns="columns" :data="listData" :pagination="false">
<template #platform="{ record }">
<span class="mr-5px" v-if="record.platform.length > 0" v-for="(item, index) in record.platform">
<img :src="PLATFORM_LIST[item].icon" width="19" class="mr-4px" />
<span>{{ PLATFORM_LIST[item].label }}</span>
</span>
</template>
<template #operation="{ record }">
<a-space size="medium">
<a-space>
@ -15,14 +15,14 @@
type="warning"
ok-text="确认删除"
cancel-text="取消"
@ok="deleteBrand(record.id)"
@ok="deleteData(record.id)"
>
<icon-delete></icon-delete>
</a-popconfirm>
</a-space>
<a-space>
<a-button type="outline" class="operation-btn">下载</a-button>
<a-button type="outline" class="operation-btn">详情</a-button>
<a-button type="outline" @click="downloadDetailAsImage(record.id)" class="operation-btn">下载</a-button>
<a-button type="outline" @click="goDetail(record.id)" class="operation-btn">详情</a-button>
</a-space>
</a-space>
</template>
@ -30,23 +30,15 @@
</view>
</template>
<script setup lang="ts">
import top1 from '@/assets/img/captcha/top1.svg';
import top2 from '@/assets/img/captcha/top2.svg';
import top3 from '@/assets/img/captcha/top3.svg';
import { IconDelete } from '@arco-design/web-vue/es/icon';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants';
import { Message } from '@arco-design/web-vue';
import html2canvas from 'html2canvas';
const listQuery = reactive({
project_id: ref(''),
platform: ref(''),
page: ref(1),
name: ref(''),
page_size: ref('10'),
});
const columns = [
{
title: '生成日期',
dataIndex: 'date',
slotName: 'account_name',
dataIndex: 'created_at',
width: 60,
minWidth: 60,
},
@ -57,18 +49,16 @@ const columns = [
minWidth: 120,
},
{
titleSlotName: 'project_name',
width: 180,
minWidth: 180,
title: '项目',
dataIndex: 'project_name',
title: '计划',
dataIndex: 'plan',
},
{
title: '平台',
titleSlotName: 'week_consumption',
dataIndex: 'platform',
width: 120,
minWidth: 120,
slotName: 'platform',
},
{
title: '操作',
@ -78,40 +68,57 @@ const columns = [
minWidth: 60,
},
];
const listResult = reactive({
data: [
{
date: '日期',
account: '账号',
project_name: '项目名称',
week_consumption: '本周总消耗',
week_consumption_rate: 0.1,
roi: '2.6',
ctr: '3.1%',
},
{
account_name: '全球旅行',
platform_name: '平台',
project_name: '项目名称',
week_consumption: '本周总消耗',
week_consumption_rate: -0.1,
roi: '2.6',
ctr: '3.1%',
},
{
account_name: '全球旅行',
platform_name: '平台',
project_name: '项目名称',
week_consumption: '本周总消耗',
week_consumption_rate: -0.1,
roi: '2.6',
ctr: '3.1%',
},
],
total: ref(0),
});
// hotTranslation.vue
import { useRouter } from 'vue-router';
import { deleteHistorylog } from '@/api/all/propertyMarketing';
import { defineEmits } from 'vue'; // 引入 useRouter
const emits = defineEmits(['onSearch']);
const topImages = [top1, top2, top3];
const router = useRouter(); // 创建 router 实例
// 详情
const goDetail = async (id) => {
// 使用 router.push 进行编程式导航,并传递话题 ID
router.push(`/put-account/detail/${id}`);
};
const downloadDetailAsImage = (id) => {
const url = `/put-account/detail/${id}`;
const win = window.open(url, '_blank');
win.onload = () => {
setTimeout(() => {
html2canvas(win.document.body, {
useCORS: true,
scale: 2,
}).then((canvas) => {
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = imgData;
link.download = `详情页面_${Date.now()}.png`;
link.click();
win.close(); // 关闭新窗口
});
}, 2000); // 等待页面加载
};
};
const deleteData = async (id) => {
const { code, message } = await deleteHistorylog(id);
if (code == 200) {
Message.success(message);
emits('onSearch');
console.log('onsearch')
}
};
const props = defineProps({
listData: {
type: Array,
default: () => {
data: [];
},
},
});
</script>
<style scoped lang="less">

View File

@ -1,95 +1,77 @@
<!--表单搜索组件-->
<template>
<view>
<a-space size="large" direction="vertical" class="search-form">
<a-row class="grid-demo" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="8">
<a-space>
<span>账户</span>
<a-input
class="w-310px"
:style="{ width: '320px' }"
v-model="query.account_name"
placeholder="请搜索..."
size="medium"
allow-clear
>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</a-col>
<a-col :span="8">
<a-space>
<span>计划</span>
<a-input
class="w-310px"
:style="{ width: '320px' }"
v-model="query.plan"
placeholder="请搜索..."
size="medium"
allow-clear
>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</a-col>
<a-col :span="8">
<a-space>
<span>平台</span>
<div class="container px-24px">
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">账户名称</span>
<a-space size="medium" class="w-240px">
<a-input v-model="query.account_name" placeholder="请搜索..." size="medium" allow-clear>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<a-select
v-model="query.platform"
class="w-320"
size="medium"
placeholder="全部"
allow-clear
@change="handleSearch"
>
<a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label"
>{{ item.label }}
</a-option>
</a-select>
</a-space>
</a-col>
</a-row>
<div class="filter-row-item flex items-center">
<span class="label">计划</span>
<a-space size="medium" class="w-240px">
<a-input
class="w-310px"
:style="{ width: '320px' }"
v-model="query.plan"
placeholder="请搜索..."
size="medium"
allow-clear
>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space>
</div>
<a-row class="grid-demo" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="10">
<a-space>
<span>时间筛选</span>
<a-range-picker
showTime
:time-picker-props="{
defaultValue: ['00:00:00', '00:00:00'],
}"
@select="onSelect"
style="width: 380px"
/>
</a-space>
</a-col>
<a-col :span="8">
<a-space>
<a-button type="outline" class="search-btn" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button type="outline" class="reset-btn" @click="handleSearch">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</a-space>
</a-col>
</a-row>
</a-space>
</view>
<div class="filter-row-item flex items-center">
<span class="label">平台</span>
<a-select
v-model="query.platform"
class="w-150"
size="medium"
placeholder="全部"
allow-clear
@change="handleSearch"
>
<a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label"
>{{ item.label }}
</a-option>
</a-select>
</div>
</div>
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">时间筛选</span>
<a-space size="medium" class="w-240px">
<a-range-picker v-model="query.data_time" size="medium" allow-clear format="YYYY-MM-DD" class="w-310" />
</a-space>
</div>
<div class="filter-row-item flex items-center">
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
<template #default>搜索</template>
</a-button>
<a-button class="w-84px reset-btn" size="medium" @click="handleReset">
<template #icon>
<icon-refresh />
</template>
<template #default>重置</template>
</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
@ -103,52 +85,77 @@ const props = defineProps({
},
});
const emits = defineEmits('onSearch', 'onReset', 'update:query');
// 获取最近7天的日期
const getLast7Days = () => {
const today = new Date();
const last7Days = new Date(today);
last7Days.setDate(today.getDate() - 7);
return [last7Days.toISOString().split('T')[0], today.toISOString().split('T')[0]];
};
const handleSearch = () => {
emits('onSearch', props.query);
};
// 使用 computed 实现 v-model
const localQuery = computed({
get: () => props.query,
set: (value) => emits('update:query', value),
});
onMounted(() => {
localQuery.value.data_time = getLast7Days();
});
</script>
<style scoped lang="scss">
:deep(.arco-select-view-single),
:deep(.arco-select-view-multiple),
:deep(.arco-picker),
:deep(.arco-input-wrapper) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
width: 224px;
height: 32px;
.container {
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-select-view-multiple),
:deep(.arco-picker) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
}
.search-btn {
// 搜索
color: var(--Brand-6, #6d4cfe);
font-size: 14px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
height: 32px;
border-radius: 3px;
word-wrap: break-word;
}
.reset-btn {
// 重置
color: var(--Text-2, #3c4043);
font-size: 14px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
height: 32px;
border-radius: 3px;
word-wrap: break-word;
}
</style>

View File

@ -3,9 +3,9 @@
<a-table
class="account-table"
:columns="columns"
:data="listResult.data"
:filter-icon-align-left="alignLeft"
:data="listData"
:pagination="false"
@sorter-change="handleSorterChange"
>
<template #week_consumption>
<a-space>
@ -53,8 +53,10 @@
</template>
<template #platform="{ record }">
<img :src="PLATFORM_LIST[record.platform].icon" width="19" class="mr-4px" />
<span>{{ PLATFORM_LIST[record.platform].label }}</span>
<a-space size="medium" v-if="record.platform">
<img :src="PLATFORM_LIST[record.platform]?.icon" width="19" class="mr-4px" />
<span>{{ PLATFORM_LIST[record.platform].label }}</span>
</a-space>
</template>
<template #weekConsumptionRate="{ record }">
@ -73,22 +75,30 @@
<template #suffix>%</template>
</a-statistic>
</template>
</a-table>
<template #clickRate="{ record }">
<span>{{ `${record.click_rate}%` }}</span>
</template>
</a-table>
</view>
</template>
<script setup lang="ts">
import { PLATFORM_LIST } from '../../../common_constants';
defineProps({
listResult: {
type: Object,
const props = defineProps({
listData: {
type: Array,
default: () => {
total: 0;
data: [];
},
}
},
});
const emit = defineEmits(['updateQuery']);
const handleSorterChange = (column, order) => {
emit('updateQuery', { column, order });
};
const listQuery = reactive({
project_id: ref(''),
platform: ref(''),
@ -122,6 +132,9 @@ const columns = [
dataIndex: 'total_use_amount',
width: 120,
minWidth: 120,
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: '本周总消耗环比',
@ -130,25 +143,34 @@ const columns = [
width: 120,
minWidth: 120,
slotName: 'weekConsumptionRate',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
titleSlotName: 'roi',
dataIndex: 'roi',
width: 120,
minWidth: 120,
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
titleSlotName: 'ctr',
dataIndex: 'click_rate',
width: 120,
minWidth: 120,
slotName: 'clickRate',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
];
</script>
<style lang="scss" scoped>
.table-data {
//账户列表-分页
.account-page {
padding: 10px 24px 20px 24px;
background-color: #fff;

View File

@ -0,0 +1,51 @@
.container {
:deep(.arco-input-wrapper),
:deep(.arco-select-view-single),
:deep(.arco-select-view-multiple),
:deep(.arco-picker) {
border-radius: 4px;
border-color: #d7d7d9;
background-color: #fff;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);
border-color: rgb(var(--primary-6));
box-shadow: 0 0 0 0 var(--color-primary-light-2);
}
}
:deep(.search-btn) {
border-radius: 4px;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
color: #6d4cfe;
}
:deep(.reset-btn) {
border-radius: 4px;
border: 1px solid var(--BG-500, #b1b2b5);
background: var(--BG-white, #fff);
}
.filter-row {
.filter-row-item {
&:not(:last-child) {
margin-right: 24px;
}
.label {
margin-right: 8px;
color: #211f24;
font-family: 'PuHuiTi-Regular';
font-size: 14px;
font-style: normal;
font-weight: 400;
flex-shrink: 0;
line-height: 22px; /* 157.143% */
}
:deep(.arco-space-item) {
width: 100%;
}
}
}
}

View File

@ -9,13 +9,21 @@ import top3 from '@/assets/img/captcha/top3.svg';
*/
export function getStarIcon(score: number): string {
switch (score) {
case 1:
case 0:
return top1;
case 2:
case 1:
return top2;
case 3:
case 2:
return top3;
default:
return top1; // 默认返回最高分图标
}
}
// ai检测结果状态
export enum AiResultStatus {
WAIT = 0, // 待执行
PENDING = 1, // 处理中
FAILED = 2, // 失败
SUCCESS = 3, // 成功
}

View File

@ -0,0 +1,102 @@
<template>
<div class="guidelines-data-wrap">
<div class="filter-wrap bg-#fff rounded-8px border-1px border-#D7D7D9 border-solid">
<div class="top flex h-64px px-24px py-10px justify-between items-center">
<p class="text-18px font-400 lh-26px color-#211F24 title">投放信息</p>
</div>
<div class="container px-24px pt-12px pb-24px">
<a-row class="grid-demo" :gutter="24">
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>账户</span>
<span>{{ detailData?.account }}</span>
</a-space>
</div>
</a-col>
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>计划</span>
<span>3</span>
</a-space>
</div>
</a-col>
</a-row>
<a-row class="grid-demo" :gutter="24" style="margin-top: 30px">
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>平台</span>
<span class="mr-5px" v-if="detailData.platform.length > 0" v-for="(item, index) in detailData.platform">
<img :src="PLATFORM_LIST[item].icon" width="19" class="mr-4px" />
<span>{{ PLATFORM_LIST[item].label }}</span>
</span>
</a-space>
</div>
</a-col>
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>生成时间</span>
<span>{{ detailData.created_at }}</span>
</a-space>
</div>
</a-col>
</a-row>
</div>
</div>
<div>
<MonthData :overview="aiResult.overview"></MonthData>
<!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<!-- 投放行动指南-->
<ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import MonthData from './components/month-data/index.vue';
import PlacementSuggestions from './components/placement-suggestions/index.vue';
import ActionGuideDistribution from './components/action-guide-distribution';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants.ts';
import { getPlacementGuideDetail } from '@/api/all/propertyMarketing';
import { useRoute } from 'vue-router';
const aiResult = reactive({
optimization: [], // 投放建议优化
action_guide: [], // 新投放建议生成
overview: [], // 新投放建议生成
});
const detailData = reactive({
created_at: '',
account: '',
platform: [],
});
const route = useRoute();
const id = route.params.id;
const getDetail = async () => {
const { code, data } = await getPlacementGuideDetail(id);
if (code === 200) {
Object.assign(aiResult, data.ai_result);
Object.assign(detailData, data);
console.log(aiResult, 'aiResult');
}
};
onMounted(() => {
getDetail();
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -1,8 +1,13 @@
<template>
<view>
<div class="guidelines-data-wrap">
<div class="part-div">
<div>
<a-tabs v-model:activeKey="tabData" class="a-tab-class" default-active-key="placement_guide">
<a-tabs
v-model:activeKey="tabData"
@tab-click="onSearch"
class="a-tab-class"
default-active-key="placement_guide"
>
<a-tab-pane key="placement_guide" title="投放指南"></a-tab-pane>
<a-tab-pane key="guide_history">
<template #title>历史投放指南</template>
@ -11,33 +16,49 @@
</div>
<!--表单组件搜索-->
<listSearchForm v-model:query="query" @onSearch="onSearch"></listSearchForm>
<!-- 投放指南-->
<PlacementGuideList
v-if="tabData === 'placement_guide'"
:listResult="{ data: guideListData.data, total: pageInfo.total }"
></PlacementGuideList>
<!-- 历史指南列表-->
<GuideListHistory v-if="tabData === 'guide_history'"></GuideListHistory>
<component
:is="currentComponent"
:listData="tabData === 'placement_guide' ? placementGuideList : guideHistoryList"
@onSearch="onSearch"
@updateQuery="handleUpdateQuery"
/>
<div v-if="listData.total > 0" class="pagination-box flex justify-end">
<a-pagination
:total="listData.total"
size="mini"
show-total
show-jumper
show-page-size
:current="query.page"
:page-size="query.pageSize"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
<a-spin :loading="loading" tip="数据分析中">
<!-- 本月摘要-->
<MonthData></MonthData>
<div v-if="tabData === 'placement_guide'">
<a-spin :loading="loading" tip="AI分析中">
<!-- 本月摘要-->
<MonthData :overview="aiResult.overview"></MonthData>
<!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<!-- 投放行动指南-->
<ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution>
</a-spin>
<div>
<!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<!-- 投放行动指南-->
<ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution>
</a-spin>
</div>
<div v-if="tabData == 'placement_guide'">
<a-space class="down-btn">
<a-button type="outline" @click="handleSearch">
<a-button type="outline" @click="downPage">
<template #icon>
<icon-download />
</template>
<template #default>下载</template>
</a-button>
<a-button type="primary" @click="handleSearch">
<a-button type="primary" @click="handleSave">
<template #icon>
<icon-drive-file />
</template>
@ -45,7 +66,7 @@
</a-button>
</a-space>
</div>
</view>
</div>
</template>
<script setup lang="ts">
@ -58,58 +79,145 @@ import PlacementSuggestions from './components/placement-suggestions/index.vue';
import ActionGuideDistribution from './components/action-guide-distribution';
import {
getAiResult,
getPlacementAccountData,
getPlacementAccountDataList,
getPlacementGuide,
getPlacementGuideHistory,
savePlacementGuide,
} from '@/api/all/propertyMarketing';
import { Message } from '@arco-design/web-vue';
import html2canvas from 'html2canvas';
import { AiResultStatus } from '@/views/property-marketing/put-account/investment-guidelines/constants';
const tabData = ref('placement_guide');
const query = reactive({
platform: '',
});
const loading = ref(false);
const guideListData = reactive({
data: [],
});
const pageInfo = reactive({
total: 0,
page_size: 0,
date_time: '',
sort_column: '',
sort_order: '',
page_size: 20,
page: 1,
});
const onSearch = async () => {
const { code, data } = await getPlacementGuide(query);
if (code === 200) {
guideListData.data = data.data;
getSyncAiResult();
}
console.log(guideListData, 'guideListData');
const currentComponent = computed(() => {
return tabData.value === 'placement_guide' ? PlacementGuideList : GuideListHistory;
});
const onPageChange = (current) => {
query.page = current;
onSearch();
};
const onPageSizeChange = (pageSize) => {
query.page_size = pageSize;
onSearch();
};
const handleUpdateQuery = (payload) => {
payload.order = payload.order === 'ascend' ? 'asc' : 'desc';
query.sort_column = payload.column;
query.sort_order = payload.order;
onSearch();
};
const loading = ref(false);
const listData = reactive({
total: 0,
list: [],
});
const placementGuideList = ref([]); // 投放指南数据
const guideHistoryList = ref([]); // 历史投放指南数据
const onSearch = async () => {
let result;
if (tabData.value === 'placement_guide') {
result = await getPlacementGuide(query);
placementGuideList.value = result?.data?.data || [];
} else {
result = await getPlacementGuideHistory(query);
guideHistoryList.value = result?.data?.data || [];
}
listData.total = result.data.total;
if (placementGuideList.value.length > 0) {
loading.value = true;
startTask();
}
};
const aiResult = reactive({
optimization: [], //投放建议优化
action_guide: [], //新投放建议生成
optimization: [], // 投放建议优化
action_guide: [], // 新投放建议生成
overview: [], // 新投放建议生成
});
const getSyncAiResult = async () => {
const { code, data } = await getAiResult(query);
if (code === 200) {
// 成功或者失败清除定时任务
if ((data.status && data.status === 3) || data.status === 2) {
// clearInterval(timer);
loading.value = false;
// 下载当前页面
const downPage = async () => {
await nextTick(); // 确保 DOM 更新完成
html2canvas(document.querySelector('.guidelines-data-wrap')).then((canvas) => {
const imgData = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = imgData;
const timestamp = new Date().getTime();
link.download = `投放指南-${timestamp}.png`;
link.click();
});
};
const saveForm = reactive({
account: [],
plan: [],
platform: [],
aiResult: [],
code: '',
});
const timerRef = ref<number | null>(null);
const startTask = () => {
//todo 暂时注释
return
if (timerRef.value !== null) return;
timerRef.value = setInterval(async () => {
try {
const { code, data } = await getAiResult(query);
console.log('定时任务执行结果:', data);
if (data.ai_result_status === AiResultStatus.SUCCESS || data.ai_result_status === AiResultStatus.FAILED) {
stopTask();
console.log('任务已完成,定时器已关闭');
}
if (data.ai_result_status === AiResultStatus.SUCCESS) {
loading.value = false;
aiResult.optimization = data.result?.optimization?.modules || [];
aiResult.action_guide = data.result?.action_guide?.modules || [];
aiResult.overview = data.result?.overview || [];
}
saveForm.code = data?.code;
console.log(aiResult, 'aiResult');
} catch (error) {
console.error('定时任务执行出错:', error);
stopTask();
}
aiResult.optimization = data.result.optimization.modules;
aiResult.action_guide = data.result?.action_guide?.modules;
}, 5000);
};
const stopTask = () => {
if (timerRef.value !== null) {
clearInterval(timerRef.value); // 清除定时器
timerRef.value = null; // 重置引用
console.log('定时器已停止');
}
};
// 定时任务请求接口
// const timer = setInterval(() => {
// getSyncAiResult();
// }, 5000);
onUnmounted(() => {
stopTask();
});
const handleSave = async () => {
const updatedSaveForm = {
...saveForm,
ai_result: aiResult,
};
const { code, message, data } = await savePlacementGuide(updatedSaveForm);
if (code === 200) {
Message.success(message);
}
};
onMounted(() => {
onSearch();
});
</script>
<style lang="scss">

View File

@ -1,3 +1,15 @@
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
}
.part-div {
width: 100%;
background: var(--BG-white, white);
@ -8,6 +20,7 @@
flex-direction: column;
justify-content: flex-start;
display: inline-flex;
margin-top: 15px;
// margin: 10px;
.arco-tabs {
@ -42,7 +55,6 @@
padding: 0 8px;
}
//每块div 标题
.part-div-header-title {
justify-content: center;
display: flex;
@ -55,13 +67,12 @@
word-wrap: break-word;
}
//账户指南-搜索div
.search-form {
padding: 10px 24px 20px 24px;
}
//账户列表-表格
.account-table {
padding: 1px 24px 20px 24px;
width: 100%;
@ -85,7 +96,6 @@
margin: 10px;
}
//本周摘要div
.month-body-div {
align-self: stretch;
height: 64px;
@ -96,7 +106,7 @@
display: inline-flex
}
//本月摘要标题
.month-data-title {
justify-content: center;
display: flex;
@ -110,7 +120,6 @@
}
//投放建议-总体策略
.overall-strategy {
width: 98%;
padding: 20px 10px 20px 16px;
@ -126,9 +135,7 @@
}
//投放优化每块div小标题
.placement-optimization-title {
// 总体策略
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
@ -147,10 +154,8 @@
word-wrap: break-word
}
//表现分析标题
.player-title {
margin: 10px 0px 15px 20px;
// 表现分析
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
@ -174,3 +179,103 @@
margin-bottom: 20px
}
.guidelines-data-wrap {
height: 100%;
display: flex;
flex-direction: column;
.filter-wrap {
border-radius: 8px;
border: 1px solid #e6e6e8;
:deep(.arco-tabs) {
.arco-tabs-tab {
height: 56px;
padding: 0 8px;
}
}
:deep(.arco-btn) {
.arco-btn-icon {
line-height: 14px;
}
}
.top {
.title {
font-family: 'PuHuiTi-Medium';
font-style: normal;
}
}
.overview-row {
.overview-item {
height: 88px;
border-radius: 8px;
background: var(--BG-100, #f7f8fa);
padding: 16px;
&:not(:last-child) {
margin-right: 12px;
}
}
}
}
.table-wrap {
width: 100%;
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
}
}
.pagination-box {
display: flex;
width: 100%;
padding: 16px 24px;
justify-content: flex-end;
align-items: center;
.arco-pagination {
.arco-pagination-list {
.arco-pagination-item {
border-radius: 4px;
border: 1px solid var(--BG-300, #e6e6e8);
&.arco-pagination-item-ellipsis {
border: none;
}
&.arco-pagination-item-active {
background-color: transparent;
border: 1px solid var(--Brand-Brand-6, #6d4cfe);
}
}
}
.arco-pagination-jumper-prepend {
color: var(--Text-2, #3c4043);
text-align: right;
font-family: 'PuHuiTi-Medium';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
.arco-select-view-single,
.arco-pagination-jumper-input {
border-radius: 4px;
border: 1px solid var(--BG-300, #e6e6e8);
background-color: #fff;
}
}
}
}