Merge branch 'feature/v1.3_营销资产中台'

This commit is contained in:
林志军
2025-07-10 17:47:50 +08:00
19 changed files with 739 additions and 17152 deletions

16595
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,6 @@
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p build-only",
"build:test": "vite build --mode test && tar -czvf dist-test.tar.gz dist",
"build:prod": "vite build --mode production && tar -czvf dist.tar.gz dist",
"build-only": "vite build -- mode development",

View File

@ -322,6 +322,11 @@ export const getPlacementAccountsList = (params = {}) => {
return Http.get('/v1/placement-accounts/list', params);
};
// 投放账号计划
export const getplacementAccountProjectsLlist = (params = {}) => {
return Http.get('/v1/placement-account-projects/list', params);
};
// 投放账号-同步数据
export const postPlacementAccountsSync = (id: string) => {
return Http.post(`/v1/placement-accounts/${id}/sync-data`);

View File

@ -0,0 +1,66 @@
<template>
<a-select allow-search v-model="selectedValue" placeholder="请选择账号" allow-clear filterable @change="handleChange">
<a-option v-for="account in filteredAccounts" :key="account.id" :value="account.id" :label="account.name">
{{ account.name }}
</a-option>
</a-select>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { getPlacementAccountsList } from '@/api/all/propertyMarketing';
// 定义账号对象类型
interface Account {
id: number;
name: string;
}
const props = defineProps<{
modelValue?: number | string;
}>();
const emit = defineEmits(['update:modelValue', 'change']);
// 响应式数据
const selectedValue = ref(props.modelValue);
const allAccounts = ref<Account[]>([]);
const filteredAccounts = ref<Account[]>([]);
const loading = ref(false);
const searchKeyword = ref('');
// 获取账号列表
const fetchAccounts = async () => {
try {
loading.value = true;
const { code, data } = await getPlacementAccountsList({
names: searchKeyword.value,
});
if (code === 200) {
allAccounts.value = data;
filteredAccounts.value = data;
}
} catch (error) {
console.error('获取账号列表失败:', error);
} finally {
loading.value = false;
}
};
// 搜索处理
const handleSearch = (value: string) => {};
// 值变化处理
const handleChange = (value: number | string) => {
let data = [];
if (value === '') {
data = [];
} else {
data = [value];
}
emit('update:modelValue', data);
};
// 初始化获取数据
onMounted(fetchAccounts);
</script>

View File

@ -0,0 +1,61 @@
<template>
<a-select allow-search v-model="selectedValue" placeholder="请选择计划" allow-clear filterable @change="handleChange">
<a-option v-for="item in listData" :key="item.id" :value="item.id" :label="item.name">
{{ item.name }}
</a-option>
</a-select>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { getplacementAccountProjectsLlist } from '@/api/all/propertyMarketing';
interface Account {
id: number;
name: string;
}
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:modelValue', 'change']);
// 响应式数据
const selectedValue = ref(props.modelValue);
const allAccounts = ref<Account[]>([]);
const listData = ref<Account[]>([]);
const loading = ref(false);
const searchKeyword = ref('');
const fetchData = async () => {
try {
loading.value = true;
const { code, data } = await getplacementAccountProjectsLlist({
names: searchKeyword.value,
});
if (code === 200) {
allAccounts.value = data;
listData.value = data;
}
} catch (error) {
console.error('获取账号列表失败:', error);
} finally {
loading.value = false;
}
};
const handleChange = (value: any) => {
let data = [];
if (value === '') {
data = [];
} else {
data = [value];
}
emit('update:modelValue', data);
};
onMounted(fetchData);
</script>

View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped lang="scss"></style>

View File

@ -1,123 +1,156 @@
<template>
<div>
<a-card :bordered="false" class="echart-item-card">
<template #title>
<span class="a-card-title">{{ title.name }}</span>
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">{{ title.popover }}</p>
</template>
</a-popover>
</template>
<div ref="chart" style="width: 100%; height: 450px"></div>
</a-card>
</div>
<a-card :bordered="false" class="chart-container" ref="chartContainer">
<template #title>
<span class="a-card-title">{{ title.name }}</span>
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">{{ title.popover }}</p>
</template>
</a-popover>
</template>
<div class="chart" ref="chartEl" :style="{ height: height + 'px' }"></div>
</a-card>
</template>
<script setup lang="ts">
import { defineProps, onMounted } from 'vue';
<script setup>
import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
const props = defineProps({
title: {
type: Object,
default: {
name: '',
popover: '',
},
},
xAxisData: {
type: Array,
default: [],
},
seriesData: {
type: Array,
default: [],
chartData: Object,
title: Object,
height: {
type: Number,
default: 300,
},
});
const chart = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const chartEl = ref(null);
const chartContainer = ref(null);
let chartInstance = null;
const xAxisData = props.xAxisData;
const seriesData = props.seriesData;
const isChartEmpty = computed(() => isEmpty(props.chartData?.series_data));
console.log(isChartEmpty, 'isChartEmpty');
// 初始化图表
const initChart = () => {
if (!chart.value) return;
// 如果已有实例,就不重复初始化
if (chartInstance) {
chartInstance.dispose(chart.value);
}
chartInstance = echarts.init(chart.value);
if (!chartEl.value) return;
chartInstance = echarts.init(chartEl.value);
updateChart();
};
// 更新图表数据
const updateChart = () => {
if (!chartInstance) return;
const { date, series_data } = props.chartData;
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: { color: '#333' },
},
legend: {
type: 'scroll',
orient: 'horizontal',
top: 10, // 将图例位置调整到顶部
itemWidth: 10,
itemHeight: 10,
pageButtonItemGap: 5,
pageButtonStyle: { color: '#666' },
textStyle: { color: '#666' },
data: seriesData,
data: series_data.map((item) => item.name),
},
grid: {
top: 80, // 调整图表内容位置,避免与图例重叠
left: 40,
right: 40,
bottom: 40,
left: '3%',
right: '4%',
bottom: '15%',
top: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
data: xAxisData,
axisLabel: { color: '#666' },
boundaryGap: false,
data: date,
},
yAxis: {
type: 'value',
axisLabel: { color: '#666' },
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } },
},
series: seriesData,
series: series_data.map((series) => ({
name: series.name,
type: 'line',
data: series.data,
itemStyle: {
color: series.color,
},
smooth: true,
symbolSize: 6,
})),
};
chartInstance.setOption(option);
chartInstance.setOption(option, { notMerge: true });
};
watch(
() => [props.xAxisData, props.seriesData],
([newXAxis, newSeries]) => {
if (chartInstance) {
chartInstance.setOption({
xAxis: newXAxis,
series: newSeries,
});
}
},
{ deep: true },
);
// 响应式调整大小
const resizeChart = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 初始化
onMounted(() => {
initChart();
window.addEventListener('resize', resizeChart);
});
// 清理
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', resizeChart);
});
// 监听数据变化
watchEffect(() => {
JSON.stringify(props.chartData); // 强制深度监听
initChart();
});
</script>
<style scoped lang="scss">
@import './style.scss';
.chart-container {
position: relative;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: #fff;
margin-bottom: 20px;
width: 100%;
height: 50%;
border-radius: 10px;
margin-bottom: 24px;
:deep(.arco-card-header) {
border-bottom: none !important;
}
.a-card-title {
color: var(--Text-1, #211f24);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 24px;
word-wrap: break-word;
}
}
.chart-title {
position: absolute;
top: 10px;
left: 20px;
font-weight: bold;
font-size: 14px;
color: #333;
}
.chart {
width: 100%;
}
</style>

View File

@ -8,26 +8,21 @@
<div class="container px-24px">
<div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center">
<span class="label">{{ accountType == 1 ? '账号名称' : '计划名称' }}</span>
<div class="filter-row-item flex items-center" v-if="accountType == 2">
<span class="label">计划名称</span>
<a-space size="medium" class="w-240px">
<a-input v-model="query.names" placeholder="请搜索..." size="medium" allow-clear @change="handleSearch">
<template #prefix>
<icon-search />
</template>
</a-input>
<PlanSelect v-model="query.ids"></PlanSelect>
</a-space>
</div>
<div class="filter-row-item flex items-center">
<span class="label">账号名称</span>
<a-space size="medium" class="w-240px">
<AccountSelect v-model="query.placement_account_id"></AccountSelect>
</a-space>
</div>
<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-select v-model="query.platform" class="w-150" size="medium" placeholder="全部" allow-clear>
<a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label"
>{{ item.label }}
</a-option>
@ -37,7 +32,7 @@
<div class="filter-row-item flex items-center">
<span class="label">运营人员</span>
<a-space class="w-160px">
<OperatorSelect v-model="query.operator_id" :options="operators" />
<OperatorSelect v-model="query.operator_id" :options="operators" />
</a-space>
</div>
</div>
@ -64,68 +59,14 @@
</div>
</div>
<div class="table-wrap rounded-8px py-5px flex-1 flex flex-col" v-if="onLoading == false">
<a-row class="grid-demo" :gutter="24">
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.total_use_amount?.date"
:seriesData="xhlEcharts?.total_use_amount?.series_data"
:title="{ name: '消耗量', popover: '消耗量' }"
></EchartsItem>
</a-col>
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.avg_conversion_cost?.date"
:seriesData="xhlEcharts?.avg_conversion_cost?.series_data"
:title="{ name: '展示量', popover: '展示量' }"
></EchartsItem>
</a-col>
</a-row>
<a-row class="grid-demo" :gutter="24">
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.click_number?.date"
:seriesData="xhlEcharts?.click_number?.series_data"
:title="{ name: '点击量', popover: '点击量' }"
></EchartsItem>
</a-col>
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.click_rate?.date"
:seriesData="xhlEcharts?.click_rate?.series_data"
:title="{ name: '点击率', popover: '点击率' }"
></EchartsItem>
</a-col>
</a-row>
<a-row class="grid-demo" :gutter="24">
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.avg_click_cost?.date"
:seriesData="xhlEcharts?.avg_click_cost?.series_data"
:title="{ name: '平均点击成本', popover: '平均点击成本' }"
></EchartsItem>
</a-col>
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.thousand_show_cost?.date"
:seriesData="xhlEcharts?.thousand_show_cost?.series_data"
:title="{ name: '千次展示成本', popover: '千次展示成本' }"
></EchartsItem>
</a-col>
</a-row>
<a-row class="grid-demo" :gutter="24">
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.conversion_number?.date"
:seriesData="xhlEcharts?.conversion_number?.series_data"
:title="{ name: '转化数', popover: '转化数' }"
></EchartsItem>
</a-col>
<a-col :span="12">
<EchartsItem
:xAxisData="xhlEcharts?.conversion_rate?.date"
:seriesData="xhlEcharts?.conversion_rate?.series_data"
:title="{ name: '转化率', popover: '转化率' }"
></EchartsItem>
<a-row :gutter="[24, 24]">
<a-col v-for="(chart, key) in chartConfigs" :key="chart.dataKey" :span="12">
<div>
<EchartsItem
:chartData="{ date: chart.date, series_data: chart.series_data }"
:title="{ name: chart.title.name, popover: chart.title.popover }"
/>
</div>
</a-col>
</a-row>
</div>
@ -135,12 +76,13 @@
import EchartsItem from './components/echarts-item/index';
import { PLATFORM_LIST } from '../common_constants';
import {
getPlacementAccountsList,
getPlacementAccountsTrend,
getPlacementAccountProjectsTrend,
fetchAccountOperators,
} from '@/api/all/propertyMarketing';
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select/index.vue';
import AccountSelect from '@/views/components/common/AccountSelect.vue';
import PlanSelect from '@/views/components/common/PlanSelect.vue';
const accountType = ref(1);
@ -153,24 +95,35 @@ const getOperators = async () => {
}
};
const query = reactive({
names: '',
platform: '',
operator_id: '',
data_time: [],
ids: [],
placement_account_id: [],
});
const xhlEcharts = reactive({});
const getAccountsTrends = async () => {
const { code, data } = await getPlacementAccountsTrend(query);
if (code === 200) {
Object.assign(xhlEcharts, data);
mergeChartData(data);
}
onLoading.value = false;
};
const mergeChartData = (apiResponse) => {
console.log(apiResponse, 'apiResponse');
chartConfigs.value = chartConfigs.value.map((config) => {
const apiItem = apiResponse[config.dataKey] || {};
return {
...config,
date: Array.isArray(apiItem.date) ? [...apiItem.date] : [],
series_data: Array.isArray(apiItem.series_data) ? [...apiItem.series_data] : [],
};
});
};
const getAccountProjectsTrend = async () => {
const { code, data } = await getPlacementAccountProjectsTrend(query);
if (code === 200) {
Object.assign(xhlEcharts, data);
mergeChartData(data);
}
onLoading.value = false;
};
@ -182,25 +135,103 @@ const handleSearch = async () => {
getAccountProjectsTrend();
}
};
const handleReset = async () => {};
const chartConfigs = ref([
{
dataKey: 'total_use_amount',
title: { name: '消耗量', popover: '广告投放期间已使用的预算总额,代表该账户的实际广告花费。' },
date: [],
series_data: [],
},
{
dataKey: 'show_number',
title: { name: '展示量', popover: '广告被用户看到的总次数,是衡量广告曝光覆盖的核心指标。' },
date: [],
series_data: [],
},
{
dataKey: 'click_number',
title: { name: '点击量', popover: '用户点击广告的次数,表示广告对用户产生了实际吸引。' },
date: [],
series_data: [],
},
{
dataKey: 'click_rate',
title: { name: '点击率', popover: '点击率CTR= 点击量 ÷ 展示量,衡量广告吸引力与内容质量。' },
date: [],
series_data: [],
},
{
dataKey: 'avg_click_cost',
title: {
name: '平均点击成本',
popover: '每次点击广告的平均花费CPC= 消耗量 ÷ 点击量。 ',
date: [],
series_data: [],
},
},
{
dataKey: 'thousand_show_cost',
title: { name: '千次展现费用', popover: '每千次展示带来的平均成本CPM= 消耗量 ÷ 展示量 × 1000。' },
date: [],
series_data: [],
},
{
dataKey: 'conversion_number',
title: { name: '转化数', popover: '用户完成设定行为(如注册、下单)的总次数,衡量广告实际效果。' },
date: [],
series_data: [],
},
{
dataKey: 'conversion_rate',
title: { name: '转化率', popover: '转化率CVR= 转化数 ÷ 点击量,代表广告引导行为转化的能力。' },
date: [],
series_data: [],
},
{
dataKey: 'avg_conversion_cost',
title: { name: '平均转化成本', popover: '每次转化所花费的平均广告费用CPA= 消耗量 ÷ 转化数。' },
date: [],
series_data: [],
},
{
dataKey: 'deep_conversion_number',
title: {
name: '深度转化数',
popover: '完成更高价值行为(如支付、留资等)的用户数量,是衡量广告质量的进阶指标。',
},
date: [],
series_data: [],
},
{
dataKey: 'deep_conversion_rate',
title: {
name: '深度转化率',
popover: '深度转化率 = 深度转化数 ÷ 点击量,用于评估优质转化的效率。',
},
date: [],
series_data: [],
},
{
dataKey: 'roi',
title: { name: '投资回报率', popover: 'ROI = 收益 ÷ 投入,衡量广告投放的整体经济回报情况。' },
date: [],
series_data: [],
},
]);
const handleReset = async () => {
query.platform = '';
query.operator_id = '';
query.ids = [];
query.placement_account_id = [];
handleSearch();
};
const operators = ref([]);
const accountList = ref([]);
const handleTabClick = (key) => {
handleSearch();
};
// 获取账户名称
const getAccountList = async () => {
const { code, data } = await getPlacementAccountsList(query);
if (code === 200) {
accountList.value = data;
}
};
onMounted(() => {
handleSearch();
getAccountList();
getOperators();
});
</script>

View File

@ -74,6 +74,7 @@
border-color: #d7d7d9;
background-color: #fff;
height: 32px;
&:focus-within,
&.arco-input-focus {
background-color: var(--color-bg-2);

View File

@ -8,11 +8,15 @@
<p style="margin: 0">基于筛选出来的投流账户/计划的情况生成的总体描述</p>
</template>
</a-popover>
<span @click="copyData" class="copybtn">
<icon-copy :style="{ fontSize: '14px' }" />
复制
</span>
</div>
<div class="month-data-div">
<div style="align-self: stretch">
<a-space direction="vertical">
<span v-for="(line, index) in formattedText" :key="index" :class="getCss(line.highlight)" >
<span v-for="(line, index) in formattedText" :key="index" :class="getCss(line.highlight)">
{{ line.text }}
</span>
</a-space>
@ -24,6 +28,7 @@
<script setup lang="ts">
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
import { defineProps } from 'vue';
import { Message } from '@arco-design/web-vue';
const props = defineProps({
overview: {
@ -63,11 +68,22 @@ const formattedText = computed(() => {
});
});
const copyData = () => {
const contentDiv = document.querySelector('.month-data-div');
const textToCopy = contentDiv.innerText || contentDiv.textContent;
navigator.clipboard
.writeText(textToCopy)
.then(() => {
Message.success('已复制');
})
.catch((err) => {
console.error('复制失败:', err);
});
};
const getCss = (highlight?: string) => {
return highlight ? classMap[highlight] : undefined;
};
console.log(props.overview, 'overvie333');
</script>
<style lang="scss">
@import './style.scss';

View File

@ -12,31 +12,40 @@
align-items: flex-start;
gap: 12px;
display: flex;
color: var(--Text-1, #211F24);
font-size: 14px;
font-family: Alibaba PuHuiTi;
font-weight: 400;
line-height: 22px;
word-wrap: break-word;
color: var(--Brand-6, #6D4CFE);
font-size: 14px;
font-family: Alibaba PuHuiTi;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
.month-text-blue {
color: var(--Brand-Brand-6, #6D4CFE);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 24px;
word-wrap: break-word
}
.month-text-red {
color: var(--Functional-Danger-6, #F64B31);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 24px;
word-wrap: break-word
}
.month-text-black {
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
}
.copybtn {
position: absolute;
right: 20px;
color: var(--Brand-6, #6D4CFE);
font-size: 14px;
font-family: Alibaba PuHuiTi;
font-weight: 400;
line-height: 24px;
line-height: 22px;
word-wrap: break-word
}

View File

@ -1,101 +1,97 @@
<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>
<a-row class="grid-demo" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="24">
<div class="overall-strategy">
<span class="placement-optimization-title"
>总体策略
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议的整体调整概述</p>
</template>
</a-popover>
</span>
<span class="placement-optimization-str">{{ props.optimization?.[0]?.['content'] }}</span>
</div>
</a-col>
</a-row>
<a-row class="grid-demo" style="margin-right: 10px" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title"
>预算分配
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在预算分配部分的详细描述</p>
</template>
</a-popover>
</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"
>时段优化
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在时段优化部分的详细描述</p>
</template>
</a-popover>
</span>
<span class="placement-optimization-str">{{ props.optimization?.[2]?.['content'] }}</span>
</div>
</a-col>
</a-row>
<a-row class="grid-demo" style="margin-right: 10px" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title"
>人群包优化
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在人群包优化部分的详细描述</p>
</template>
</a-popover>
</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"
>素材优化
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在素材优化部分的详细描述</p>
</template>
</a-popover>
</span>
<span class="placement-optimization-str">{{ props?.optimization?.[4]?.['content'] }}</span>
</div>
</a-col>
</a-row>
</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">基于筛选出来的投流账户/计划的情况生成的优化建议</p>
</template>
</a-popover>
</div>
</view>
<div>
<a-row class="grid-demo" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="24">
<div class="overall-strategy">
<span class="placement-optimization-title"
>总体策略
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议的整体调整概述</p>
</template>
</a-popover>
</span>
<span class="placement-optimization-str">{{ props.optimization?.[0]?.['content'] }}</span>
</div>
</a-col>
</a-row>
<a-row class="grid-demo" style="margin-right: 10px" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title"
>预算分配
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在预算分配部分的详细描述</p>
</template>
</a-popover>
</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"
>时段优化
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在时段优化部分的详细描述</p>
</template>
</a-popover>
</span>
<span class="placement-optimization-str">{{ props.optimization?.[2]?.['content'] }}</span>
</div>
</a-col>
</a-row>
<a-row class="grid-demo" style="margin-right: 10px" :gutter="{ md: 8, lg: 24, xl: 32 }">
<a-col :span="12">
<div class="overall-strategy">
<span class="placement-optimization-title"
>人群包优化
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在人群包优化部分的详细描述</p>
</template>
</a-popover>
</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"
>素材优化
<a-popover position="tl">
<icon-question-circle />
<template #content>
<p style="margin: 0">优化建议在素材优化部分的详细描述</p>
</template>
</a-popover>
</span>
<span class="placement-optimization-str">{{ props?.optimization?.[4]?.['content'] }}</span>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
// defineProps()
import { defineProps } from 'vue';
const props = defineProps({
@ -104,10 +100,6 @@ const props = defineProps({
default: () => [],
},
});
console.log(props.optimization, 'optimization');
</script>
<style scoped lang="scss">
@import './style.scss';
</style>
<style lang="scss"></style>

View File

@ -2,9 +2,9 @@
<view>
<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 class="mr-8px" v-if="record.platform.length > 0" v-for="(item, index) in record.platform">
<img :src="PLATFORM_LIST?.[item]?.icon" width="15" height="15" />
<span class="label ml-5px">{{ PLATFORM_LIST?.[item]?.label }}</span>
</span>
</template>
<template #operation="{ record }">
@ -33,7 +33,6 @@
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 columns = [
{
@ -113,30 +112,11 @@ const props = defineProps({
});
</script>
<style scoped lang="less">
.arco-textarea-wrapper,
.arco-input-wrapper {
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);
}
&.arco-input-wrapper {
height: 35px;
}
}
<style scoped lang="scss">
.operation-btn {
// 下载
color: var(--Brand-6, #6d4cfe);
font-size: 14px;
font-family: PuHuiTi-Medium;
font-family: Alibaba PuHuiTi;
font-weight: 400;
line-height: 22px;
word-wrap: break-word;

View File

@ -5,29 +5,14 @@
<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>
<AccountSelect v-model="query.placement_account_id"></AccountSelect>
</a-space>
</div>
<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>
<PlanSelect v-model="query.placement_account_project_id"></PlanSelect>
</a-space>
</div>
@ -50,7 +35,7 @@
</div>
<div class="filter-row-item flex items-center">
<a-button class="w-84px search-btn mr-12px" :disabled="disabled" size="medium" @click="handleSearch">
<a-button class="w-84px search-btn mr-12px" :disabled="disabled" size="medium" @click="handleSearch">
<template #icon>
<icon-search />
</template>
@ -70,6 +55,8 @@
<script setup lang="ts">
import { defineEmits, defineProps } from 'vue';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants';
import AccountSelect from '@/views/components/common/AccountSelect.vue';
import PlanSelect from '@/views/components/common/PlanSelect.vue';
const props = defineProps({
query: {

View File

@ -7,7 +7,7 @@
:pagination="false"
@sorter-change="handleSorterChange"
>
<template #week_consumption>
<template #total_use_amount_label>
<a-space>
<span>本周总消耗</span>
<a-popover position="tl">
@ -18,7 +18,7 @@
</a-popover>
</a-space>
</template>
<template #weekConsumptionRateTitle>
<template #pre_total_use_amount_chain_title>
<a-space>
<span>本周总消耗环比</span>
<a-popover position="tl">
@ -53,12 +53,22 @@
</template>
<template #platform="{ record }">
<a-space size="medium" v-if="record.platform">
</a-space>
<div class="field-row">
<img :src="PLATFORM_LIST.find((v) => v.value === record.platform)?.icon" width="15" height="15" />
<span class="label ml-5px">{{ PLATFORM_LIST.find((v) => v.value === record.platform)?.label }}</span>
</div>
</template>
<template #total_use_amount="{ record }">
<a-space>{{ record.total_use_amount }}</a-space>
</template>
<template #weekConsumptionRate="{ record }">
<template #total_use_amoun2="{ record }">
<div class="field-row">
<img :src="PLATFORM_LIST.find((v) => v.value === record.platform)?.icon" width="15" height="15" />
<span class="label ml-5px">{{ PLATFORM_LIST.find((v) => v.value === record.platform)?.label }}</span>
</div>
</template>
<template #pre_total_use_amount_chain="{ record }">
<a-statistic
:value="record.pre_total_use_amount_chain * 100"
:value-style="{
@ -90,7 +100,7 @@ const props = defineProps({
default: () => {
data: [];
},
}
},
});
const emit = defineEmits(['updateQuery']);
@ -121,8 +131,9 @@ const columns = [
},
{
title: '本周总消耗',
titleSlotName: 'week_consumption',
titleSlotName: 'total_use_amount_label',
dataIndex: 'total_use_amount',
slotName: 'total_use_amount',
width: 120,
minWidth: 120,
sortable: {
@ -131,11 +142,11 @@ const columns = [
},
{
title: '本周总消耗环比',
titleSlotName: 'weekConsumptionRateTitle',
titleSlotName: 'pre_total_use_amount_chain_title',
dataIndex: 'pre_total_use_amount_chain',
width: 120,
minWidth: 120,
slotName: 'weekConsumptionRate',
slotName: 'pre_total_use_amount_chain',
sortable: {
sortDirections: ['ascend', 'descend'],
},
@ -162,12 +173,4 @@ const columns = [
];
</script>
<style lang="scss" scoped>
.table-data {
.account-page {
padding: 10px 24px 20px 24px;
background-color: #fff;
float: right;
}
}
</style>
<style lang="scss"></style>

View File

@ -10,8 +10,8 @@
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>账户</span>
<span>{{ detailData?.account }}</span>
<span class="span-title">账户</span>
<span class="span-content">{{ detailData?.account }}</span>
</a-space>
</div>
</a-col>
@ -19,8 +19,8 @@
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>计划</span>
<span>3</span>
<span class="span-title">计划</span>
<span class="span-content">{{detailData.plan}}</span>
</a-space>
</div>
</a-col>
@ -29,11 +29,17 @@
<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>
<span class="span-title">平台</span>
<a-space>
<span
class="mr-8px"
v-if="detailData.platform.length > 0"
v-for="(item, index) in detailData.platform"
>
<img :src="PLATFORM_LIST?.[item]?.icon" width="15" height="15" />
<span class="label ml-5px">{{ PLATFORM_LIST?.[item]?.label }}</span>
</span>
</a-space>
</a-space>
</div>
</a-col>
@ -41,20 +47,18 @@
<a-col :span="12">
<div class="">
<a-space direction="vertical">
<span>生成时间</span>
<span>{{ detailData.created_at }}</span>
<span class="span-title">生成时间</span>
<span class="span-content">{{ detailData.created_at }}</span>
</a-space>
</div>
</a-col>
</a-row>
</div>
</div>
<div>
<MonthData :overview="aiResult.overview"></MonthData>
<MonthData :overview="aiResult.overview"></MonthData>
<!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
</div>
<!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<div class="ignore-export">
<a-space class="down-btn">
<a-button type="outline" :loading="exportLoading" @click="downPage">

View File

@ -44,20 +44,10 @@
/>
</div>
</div>
<a-spin
v-show="tabData === 'placement_guide'"
:loading="loading"
tip="AI分析中...."
wrapperClassName="custom-spin-wrapper"
>
<div>
<MonthData :overview="aiResult.overview"></MonthData>
<!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<!-- 投放行动指南-->
<!-- <ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution>-->
</div>
</a-spin>
<!-- 投放建议-->
<MonthData v-if="tabData == 'placement_guide'" :overview="aiResult.overview"></MonthData>
<PlacementSuggestions v-if="tabData == 'placement_guide'" :optimization="aiResult.optimization"></PlacementSuggestions>
<div v-if="tabData == 'placement_guide'" class="ignore-export">
<a-space class="down-btn">
<a-button type="outline" :loading="exportLoading" @click="downPage">
@ -91,16 +81,16 @@ import {
savePlacementGuide,
} from '@/api/all/propertyMarketing';
import { Message } from '@arco-design/web-vue';
import { AiResultStatus } from '@/views/property-marketing/put-account/investment-guidelines/constants';
import { AiResultStatus, generatePDF } from '@/views/property-marketing/put-account/investment-guidelines/constants';
import { uploadPdf } from '@/views/property-marketing/put-account/investment-guidelines/constants';
const tabData = ref('placement_guide');
const query = reactive({
platform: '',
account_name: '',
plan: '',
date_time: [],
placement_account_id: [],
placement_account_project_id: [],
sort_column: '',
sort_order: '',
page_size: 20,
@ -142,16 +132,16 @@ const onSearch = async () => {
if (tabData.value === 'placement_guide') {
result = await getPlacementGuide(query);
placementGuideList.value = result?.data?.data || [];
if (placementGuideList.value.length > 0 && isGetAi.value) {
loading.value = true;
syncGetAiResult();
startTask();
}
} else {
result = await getPlacementGuideHistory(query);
guideHistoryList.value = result?.data?.data || [];
}
listData.total = result.data.total;
if (placementGuideList.value.length > 0 && isGetAi.value) {
loading.value = true;
syncGetAiResult();
startTask();
}
isGetAi.value = true;
};
const aiResult = reactive({
@ -167,7 +157,6 @@ const downPage = async () => {
exportLoading.value = true;
if (saveForm.file_url === '') {
fileUrl = await uploadPdf('投放指南.pdf', '.guidelines-data-wrap');
saveForm.file_url = fileUrl;
}
console.log(fileUrl, 'fileUrl');
const link = document.createElement('a');
@ -206,7 +195,6 @@ const syncGetAiResult = async () => {
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;
@ -235,7 +223,6 @@ const handleReset = () => {
console.log('handleReset');
query.page = 1;
query.page_size = 20;
query.account_name = '';
query.platform = '';
query.sort_column = '';
query.sort_order = '';
@ -273,9 +260,4 @@ onMounted(() => {
<style lang="scss">
@import './style.scss';
.custom-spin-wrapper {
display: block;
width: 100%;
}
</style>

View File

@ -1,4 +1,6 @@
.table-wrap {
width: 100%;
.pagination-box {
@ -10,161 +12,6 @@
}
}
.part-div {
width: 100%;
background: var(--BG-white, white);
overflow: hidden;
border-radius: 8px;
outline: 1px var(--BG-300, #E6E6E8) solid;
outline-offset: -1px;
flex-direction: column;
justify-content: flex-start;
display: inline-flex;
margin-top: 15px;
// margin: 10px;
.arco-tabs {
margin-bottom: 10px;
height: 76px;
.arco-tabs-tab {
height: 56px;
padding: 0 8px;
}
}
}
.part-div-header {
align-self: stretch;
height: 64px;
padding: 10px 24px 10px 24px;
justify-content: flex-start;
align-items: center;
display: inline-flex
}
.a-tab-class {
color: var(--Brand-6, #6D4CFE);
font-size: 66px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 30px;
word-wrap: break-word;
height: 56px;
padding: 0 8px;
}
.part-div-header-title {
justify-content: center;
display: flex;
flex-direction: column;
color: var(--Text-1, #211F24);
font-size: 18px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 26px;
word-wrap: break-word;
}
.search-form {
padding: 10px 24px 20px 24px;
}
.account-table {
padding: 1px 24px 20px 24px;
width: 100%;
}
.month-data-body {
width: 100%;
padding-bottom: 20px;
padding-left: 24px;
padding-right: 24px;
background: var(--BG-white, white);
overflow: hidden;
border-radius: 8px;
outline: 1px var(--BG-300, #E6E6E8) solid;
outline-offset: -1px;
flex-direction: column;
justify-content: flex-start;
align-items: center;
display: inline-flex;
margin: 10px;
}
.month-body-div {
align-self: stretch;
height: 64px;
padding-top: 10px;
padding-bottom: 10px;
justify-content: space-between;
align-items: center;
display: inline-flex
}
.month-data-title {
justify-content: center;
display: flex;
flex-direction: column;
color: var(--Text-1, #211F24);
font-size: 18px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 26px;
word-wrap: break-word
}
.overall-strategy {
width: 98%;
padding: 20px 10px 20px 16px;
background: var(--BG-100, #F7F8FA);
overflow: hidden;
border-radius: 8px;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 12px;
display: inline-flex;
margin: 20px;
}
.placement-optimization-title {
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
.placement-optimization-str {
align-self: stretch;
color: var(--Text-2, #3C4043);
font-size: 14px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
.player-title {
margin: 10px 0px 15px 20px;
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
:deep(.arco-tabs) {
.arco-tabs-tab {
height: 56px;
@ -172,14 +19,12 @@
}
}
.down-btn {
float: right;
margin-top: 10px;
margin-bottom: 20px
}
.guidelines-data-wrap {
height: 100%;
display: flex;
@ -278,4 +123,167 @@
}
}
}
.part-div {
width: 100%;
background: var(--BG-white, white);
overflow: hidden;
border-radius: 8px;
outline: 1px var(--BG-300, #E6E6E8) solid;
outline-offset: -1px;
flex-direction: column;
justify-content: flex-start;
display: inline-flex;
margin-top: 15px;
// margin: 10px;
.arco-tabs {
margin-bottom: 10px;
height: 76px;
.arco-tabs-tab {
height: 56px;
padding: 0 8px;
}
}
}
.part-div-header {
align-self: stretch;
height: 64px;
padding: 10px 24px 10px 24px;
justify-content: flex-start;
align-items: center;
display: inline-flex;
position: relative;
}
.a-tab-class {
color: var(--Brand-6, #6D4CFE);
font-size: 66px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 30px;
word-wrap: break-word;
height: 56px;
padding: 0 8px;
}
.part-div-header-title {
justify-content: center;
display: flex;
flex-direction: column;
color: var(--Text-1, #211F24);
font-size: 18px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 26px;
word-wrap: break-word;
}
.month-data-title {
justify-content: center;
display: flex;
flex-direction: column;
color: var(--Text-1, #211F24);
font-size: 18px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 26px;
word-wrap: break-word
}
.overall-strategy {
width: 98%;
padding: 20px 10px 20px 16px;
background: var(--BG-100, #F7F8FA);
overflow: hidden;
border-radius: 8px;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 12px;
display: flex;
margin: 20px;
}
.placement-optimization-title {
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
.search-form {
padding: 10px 24px 20px 24px;
}
.account-table {
padding: 1px 24px 20px 24px;
width: 100%;
}
.month-data-body {
width: 100%;
padding-bottom: 20px;
padding-left: 24px;
padding-right: 24px;
background: var(--BG-white, white);
overflow: hidden;
border-radius: 8px;
outline: 1px var(--BG-300, #E6E6E8) solid;
outline-offset: -1px;
flex-direction: column;
justify-content: flex-start;
align-items: center;
display: inline-flex;
margin: 10px;
}
.month-body-div {
align-self: stretch;
height: 64px;
padding-top: 10px;
padding-bottom: 10px;
justify-content: space-between;
align-items: center;
display: inline-flex
}
.placement-optimization-str {
color: var(--Text-2, #3C4043);
font-size: 14px;
font-family: Alibaba PuHuiTi;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
.player-title {
margin: 10px 0px 15px 20px;
color: var(--Text-1, #211F24);
font-size: 16px;
font-family: PuHuiTi-Medium;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
.span-content{
font-family: Alibaba PuHuiTi;
}
.span-title{
color: var(--Text-3, #737478);
font-size: 14px;
font-family: Alibaba PuHuiTi;
font-weight: 400;
line-height: 22px;
word-wrap: break-word
}
}