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, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p build-only",
"build:test": "vite build --mode test && tar -czvf dist-test.tar.gz dist", "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:prod": "vite build --mode production && tar -czvf dist.tar.gz dist",
"build-only": "vite build -- mode development", "build-only": "vite build -- mode development",

View File

@ -322,6 +322,11 @@ export const getPlacementAccountsList = (params = {}) => {
return Http.get('/v1/placement-accounts/list', 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) => { export const postPlacementAccountsSync = (id: string) => {
return Http.post(`/v1/placement-accounts/${id}/sync-data`); 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,6 +1,5 @@
<template> <template>
<div> <a-card :bordered="false" class="chart-container" ref="chartContainer">
<a-card :bordered="false" class="echart-item-card">
<template #title> <template #title>
<span class="a-card-title">{{ title.name }}</span> <span class="a-card-title">{{ title.name }}</span>
<a-popover position="tl"> <a-popover position="tl">
@ -10,114 +9,148 @@
</template> </template>
</a-popover> </a-popover>
</template> </template>
<div ref="chart" style="width: 100%; height: 450px"></div>
<div class="chart" ref="chartEl" :style="{ height: height + 'px' }"></div>
</a-card> </a-card>
</div>
</template> </template>
<script setup lang="ts"> <script setup>
import { defineProps, onMounted } from 'vue'; import { ref, onMounted, watch, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
const props = defineProps({ const props = defineProps({
title: { chartData: Object,
type: Object, title: Object,
default: { height: {
name: '', type: Number,
popover: '', default: 300,
},
},
xAxisData: {
type: Array,
default: [],
},
seriesData: {
type: Array,
default: [],
}, },
}); });
const chart = ref<HTMLElement | null>(null); const chartEl = ref(null);
let chartInstance: echarts.ECharts | null = null; const chartContainer = ref(null);
let chartInstance = null;
const xAxisData = props.xAxisData; const isChartEmpty = computed(() => isEmpty(props.chartData?.series_data));
const seriesData = props.seriesData;
console.log(isChartEmpty, 'isChartEmpty');
// 初始化图表
const initChart = () => { const initChart = () => {
if (!chart.value) return; if (!chartEl.value) return;
// 如果已有实例,就不重复初始化 chartInstance = echarts.init(chartEl.value);
if (chartInstance) { updateChart();
chartInstance.dispose(chart.value); };
} // 更新图表数据
chartInstance = echarts.init(chart.value); const updateChart = () => {
if (!chartInstance) return;
const { date, series_data } = props.chartData;
const option = { const option = {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: { color: '#333' },
}, },
legend: { legend: {
type: 'scroll', data: series_data.map((item) => item.name),
orient: 'horizontal',
top: 10, // 将图例位置调整到顶部
itemWidth: 10,
itemHeight: 10,
pageButtonItemGap: 5,
pageButtonStyle: { color: '#666' },
textStyle: { color: '#666' },
data: seriesData,
}, },
grid: { grid: {
top: 80, // 调整图表内容位置,避免与图例重叠 left: '3%',
left: 40, right: '4%',
right: 40, bottom: '15%',
bottom: 40, top: '15%',
containLabel: true,
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
data: xAxisData, boundaryGap: false,
axisLabel: { color: '#666' }, data: date,
}, },
yAxis: { yAxis: {
type: 'value', 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], const resizeChart = () => {
([newXAxis, newSeries]) => {
if (chartInstance) { if (chartInstance) {
chartInstance.setOption({ chartInstance.resize();
xAxis: newXAxis,
series: newSeries,
});
} }
}, };
{ deep: true },
);
// 初始化
onMounted(() => { onMounted(() => {
initChart(); initChart();
window.addEventListener('resize', resizeChart);
}); });
// 清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (chartInstance) { if (chartInstance) {
chartInstance.dispose(); chartInstance.dispose();
chartInstance = null; chartInstance = null;
} }
window.removeEventListener('resize', resizeChart);
});
// 监听数据变化
watchEffect(() => {
JSON.stringify(props.chartData); // 强制深度监听
initChart();
}); });
</script> </script>
<style scoped lang="scss"> <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> </style>

View File

@ -8,26 +8,21 @@
<div class="container px-24px"> <div class="container px-24px">
<div class="filter-row flex mb-20px"> <div class="filter-row flex mb-20px">
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center" v-if="accountType == 2">
<span class="label">{{ accountType == 1 ? '账号名称' : '计划名称' }}</span> <span class="label">计划名称</span>
<a-space size="medium" class="w-240px"> <a-space size="medium" class="w-240px">
<a-input v-model="query.names" placeholder="请搜索..." size="medium" allow-clear @change="handleSearch"> <PlanSelect v-model="query.ids"></PlanSelect>
<template #prefix> </a-space>
<icon-search /> </div>
</template> <div class="filter-row-item flex items-center">
</a-input> <span class="label">账号名称</span>
<a-space size="medium" class="w-240px">
<AccountSelect v-model="query.placement_account_id"></AccountSelect>
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">平台</span> <span class="label">平台</span>
<a-select <a-select v-model="query.platform" class="w-150" size="medium" placeholder="全部" allow-clear>
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" <a-option v-for="(item, index) in PLATFORM_LIST" :key="index" :value="item.value" :label="item.label"
>{{ item.label }} >{{ item.label }}
</a-option> </a-option>
@ -64,68 +59,14 @@
</div> </div>
</div> </div>
<div class="table-wrap rounded-8px py-5px flex-1 flex flex-col" v-if="onLoading == false"> <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-row :gutter="[24, 24]">
<a-col :span="12"> <a-col v-for="(chart, key) in chartConfigs" :key="chart.dataKey" :span="12">
<div>
<EchartsItem <EchartsItem
:xAxisData="xhlEcharts?.total_use_amount?.date" :chartData="{ date: chart.date, series_data: chart.series_data }"
:seriesData="xhlEcharts?.total_use_amount?.series_data" :title="{ name: chart.title.name, popover: chart.title.popover }"
:title="{ name: '消耗量', popover: '消耗量' }" />
></EchartsItem> </div>
</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-col> </a-col>
</a-row> </a-row>
</div> </div>
@ -135,12 +76,13 @@
import EchartsItem from './components/echarts-item/index'; import EchartsItem from './components/echarts-item/index';
import { PLATFORM_LIST } from '../common_constants'; import { PLATFORM_LIST } from '../common_constants';
import { import {
getPlacementAccountsList,
getPlacementAccountsTrend, getPlacementAccountsTrend,
getPlacementAccountProjectsTrend, getPlacementAccountProjectsTrend,
fetchAccountOperators, fetchAccountOperators,
} from '@/api/all/propertyMarketing'; } from '@/api/all/propertyMarketing';
import OperatorSelect from '@/views/property-marketing/media-account/components/operator-select/index.vue'; 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); const accountType = ref(1);
@ -153,24 +95,35 @@ const getOperators = async () => {
} }
}; };
const query = reactive({ const query = reactive({
names: '',
platform: '', platform: '',
operator_id: '', operator_id: '',
data_time: [], data_time: [],
ids: [],
placement_account_id: [],
}); });
const xhlEcharts = reactive({});
const getAccountsTrends = async () => { const getAccountsTrends = async () => {
const { code, data } = await getPlacementAccountsTrend(query); const { code, data } = await getPlacementAccountsTrend(query);
if (code === 200) { if (code === 200) {
Object.assign(xhlEcharts, data); mergeChartData(data);
} }
onLoading.value = false; 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 getAccountProjectsTrend = async () => {
const { code, data } = await getPlacementAccountProjectsTrend(query); const { code, data } = await getPlacementAccountProjectsTrend(query);
if (code === 200) { if (code === 200) {
Object.assign(xhlEcharts, data); mergeChartData(data);
} }
onLoading.value = false; onLoading.value = false;
}; };
@ -182,25 +135,103 @@ const handleSearch = async () => {
getAccountProjectsTrend(); 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 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(() => { onMounted(() => {
handleSearch(); handleSearch();
getAccountList();
getOperators(); getOperators();
}); });
</script> </script>

View File

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

View File

@ -8,6 +8,10 @@
<p style="margin: 0">基于筛选出来的投流账户/计划的情况生成的总体描述</p> <p style="margin: 0">基于筛选出来的投流账户/计划的情况生成的总体描述</p>
</template> </template>
</a-popover> </a-popover>
<span @click="copyData" class="copybtn">
<icon-copy :style="{ fontSize: '14px' }" />
复制
</span>
</div> </div>
<div class="month-data-div"> <div class="month-data-div">
<div style="align-self: stretch"> <div style="align-self: stretch">
@ -24,6 +28,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon'; import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
import { defineProps } from 'vue'; import { defineProps } from 'vue';
import { Message } from '@arco-design/web-vue';
const props = defineProps({ const props = defineProps({
overview: { 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) => { const getCss = (highlight?: string) => {
return highlight ? classMap[highlight] : undefined; return highlight ? classMap[highlight] : undefined;
}; };
console.log(props.overview, 'overvie333');
</script> </script>
<style lang="scss"> <style lang="scss">
@import './style.scss'; @import './style.scss';

View File

@ -12,31 +12,40 @@
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 12px;
display: flex; 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 { .month-text-blue {
color: var(--Brand-Brand-6, #6D4CFE); 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 { .month-text-red {
color: var(--Functional-Danger-6, #F64B31); 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 { .month-text-black {
color: var(--Text-1, #211F24); 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; font-weight: 400;
line-height: 24px; line-height: 22px;
word-wrap: break-word word-wrap: break-word
} }

View File

@ -1,5 +1,4 @@
<template> <template>
<view>
<div class="part-div"> <div class="part-div">
<div class="part-div-header"> <div class="part-div-header">
<span class="part-div-header-title">投放建议优化</span> <span class="part-div-header-title">投放建议优化</span>
@ -90,12 +89,9 @@
</a-row> </a-row>
</div> </div>
</div> </div>
</view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// defineProps()
import { defineProps } from 'vue'; import { defineProps } from 'vue';
const props = defineProps({ const props = defineProps({
@ -104,10 +100,6 @@ const props = defineProps({
default: () => [], default: () => [],
}, },
}); });
console.log(props.optimization, 'optimization');
</script> </script>
<style scoped lang="scss"> <style lang="scss"></style>
@import './style.scss';
</style>

View File

@ -2,9 +2,9 @@
<view> <view>
<a-table class="account-table" :columns="columns" :data="listData" :pagination="false"> <a-table class="account-table" :columns="columns" :data="listData" :pagination="false">
<template #platform="{ record }"> <template #platform="{ record }">
<span class="mr-5px" v-if="record.platform.length > 0" v-for="(item, index) in record.platform"> <span class="mr-8px" v-if="record.platform.length > 0" v-for="(item, index) in record.platform">
<img :src="PLATFORM_LIST[item].icon" width="19" class="mr-4px" /> <img :src="PLATFORM_LIST?.[item]?.icon" width="15" height="15" />
<span>{{ PLATFORM_LIST[item].label }}</span> <span class="label ml-5px">{{ PLATFORM_LIST?.[item]?.label }}</span>
</span> </span>
</template> </template>
<template #operation="{ record }"> <template #operation="{ record }">
@ -33,7 +33,6 @@
import { IconDelete } from '@arco-design/web-vue/es/icon'; import { IconDelete } from '@arco-design/web-vue/es/icon';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants'; import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import html2canvas from 'html2canvas';
const columns = [ const columns = [
{ {
@ -113,30 +112,11 @@ const props = defineProps({
}); });
</script> </script>
<style scoped lang="less"> <style scoped lang="scss">
.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;
}
}
.operation-btn { .operation-btn {
// 下载
color: var(--Brand-6, #6d4cfe); color: var(--Brand-6, #6d4cfe);
font-size: 14px; font-size: 14px;
font-family: PuHuiTi-Medium; font-family: Alibaba PuHuiTi;
font-weight: 400; font-weight: 400;
line-height: 22px; line-height: 22px;
word-wrap: break-word; word-wrap: break-word;

View File

@ -5,29 +5,14 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">账户名称</span> <span class="label">账户名称</span>
<a-space size="medium" class="w-240px"> <a-space size="medium" class="w-240px">
<a-input v-model="query.account_name" placeholder="请搜索..." size="medium" allow-clear> <AccountSelect v-model="query.placement_account_id"></AccountSelect>
<template #prefix>
<icon-search />
</template>
</a-input>
</a-space> </a-space>
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">计划</span> <span class="label">计划</span>
<a-space size="medium" class="w-240px"> <a-space size="medium" class="w-240px">
<a-input <PlanSelect v-model="query.placement_account_project_id"></PlanSelect>
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-space>
</div> </div>
@ -70,6 +55,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineProps } from 'vue'; import { defineEmits, defineProps } from 'vue';
import { PLATFORM_LIST } from '@/views/property-marketing/put-account/common_constants'; 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({ const props = defineProps({
query: { query: {

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
.table-wrap { .table-wrap {
width: 100%; width: 100%;
.pagination-box { .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) { :deep(.arco-tabs) {
.arco-tabs-tab { .arco-tabs-tab {
height: 56px; height: 56px;
@ -172,14 +19,12 @@
} }
} }
.down-btn { .down-btn {
float: right; float: right;
margin-top: 10px; margin-top: 10px;
margin-bottom: 20px margin-bottom: 20px
} }
.guidelines-data-wrap { .guidelines-data-wrap {
height: 100%; height: 100%;
display: flex; 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
}
}