feat(property-marketing): 新增账号选择组件并优化图表展示逻辑

perf: 重构投放指南组件结构,移除冗余代码
perf: 优化仪表盘图表配置,使用动态渲染方式
perf: 统一组件样式引用方式,移除重复样式定义
This commit is contained in:
林志军
2025-07-09 19:18:17 +08:00
parent 24d942acfe
commit 7b21aa3060
11 changed files with 389 additions and 16934 deletions

16595
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
<template>
<a-select
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) => {
emit('update:modelValue', [value]);
};
// 初始化获取数据
onMounted(fetchAccounts);
</script>

View File

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

View File

@ -47,9 +47,7 @@ const seriesData = props.seriesData;
const initChart = () => { const initChart = () => {
if (!chart.value) return; if (!chart.value) return;
// 如果已有实例,就不重复初始化 // 如果已有实例,就不重复初始化
if (chartInstance) {
chartInstance.dispose(chart.value);
}
chartInstance = echarts.init(chart.value); chartInstance = echarts.init(chart.value);
const option = { const option = {
@ -96,17 +94,37 @@ const initChart = () => {
watch( watch(
() => [props.xAxisData, props.seriesData], () => [props.xAxisData, props.seriesData],
([newXAxis, newSeries]) => { async () => {
if (chartInstance) { await nextTick();
chartInstance.setOption({ updateChart();
xAxis: newXAxis,
series: newSeries,
});
}
}, },
{ deep: true }, { deep: true },
); );
const updateChart = () => {
if (!chartInstance) return;
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: { color: '#333' },
},
legend: {
data: props.seriesData,
},
xAxis: {
data: props.xAxisData,
},
series: props.seriesData,
};
chartInstance.setOption(option, true); // 第二个参数为 true 表示合并
};
onMounted(() => { onMounted(() => {
initChart(); initChart();
}); });

View File

@ -11,11 +11,7 @@
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<span class="label">{{ accountType == 1 ? '账号名称' : '计划名称' }}</span> <span class="label">{{ accountType == 1 ? '账号名称' : '计划名称' }}</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"> <AccountSelect v-model="query.ids"></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">
@ -65,66 +61,12 @@
</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 class="grid-demo" :gutter="24">
<a-col :span="12"> <a-col v-for="(chart, index) in chartConfigs" :key="index" :span="12">
<EchartsItem <EchartsItem
:xAxisData="xhlEcharts?.total_use_amount?.date" :key="chart.dataKey"
:seriesData="xhlEcharts?.total_use_amount?.series_data" :xAxisData="xhlEcharts?.[chart.dataKey]?.date"
:title="{ name: '消耗量', popover: '消耗量' }" :seriesData="xhlEcharts?.[chart.dataKey]?.series_data"
></EchartsItem> :title="{ name: chart.title.name, popover: chart.title.popover }"
</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> ></EchartsItem>
</a-col> </a-col>
</a-row> </a-row>
@ -141,6 +83,7 @@ import {
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';
const accountType = ref(1); const accountType = ref(1);
@ -182,6 +125,38 @@ const handleSearch = async () => {
getAccountProjectsTrend(); getAccountProjectsTrend();
} }
}; };
// 定义图表配置
const chartConfigs = [
{
dataKey: 'total_use_amount',
title: { name: '消耗量', popover: '广告投放期间已使用的预算总额,代表该账户的实际广告花费。' },
},
{
dataKey: 'avg_conversion_cost',
title: { name: '展示量', popover: '广告被用户看到的总次数,是衡量广告曝光覆盖的核心指标。' },
},
{ dataKey: 'click_number', title: { name: '点击量', popover: '用户点击广告的次数,表示广告对用户产生了实际吸引。' } },
{
dataKey: 'click_rate',
title: { name: '点击率', popover: '点击率CTR= 点击量 ÷ 展示量,衡量广告吸引力与内容质量。' },
},
{
dataKey: 'avg_click_cost',
title: { name: '平均点击成本', popover: '每次点击广告的平均花费CPC= 消耗量 ÷ 点击量。' },
},
{
dataKey: 'thousand_show_cost',
title: { name: '千次展示成本', popover: '每千次展示带来的平均成本CPM= 消耗量 ÷ 展示量 × 1000。' },
},
{
dataKey: 'conversion_number',
title: { name: '转化数', popover: '用户完成设定行为(如注册、下单)的总次数,衡量广告实际效果。' },
},
{
dataKey: 'conversion_rate',
title: { name: '转化率', popover: '转化率CVR= 转化数 ÷ 点击量,代表广告引导行为转化的能力。' },
},
];
const handleReset = async () => {}; const handleReset = async () => {};
const operators = ref([]); const operators = ref([]);

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

@ -1,7 +1,7 @@
<template> <template>
<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="">总体摘要</span>
<a-popover position="tl"> <a-popover position="tl">
<icon-question-circle /> <icon-question-circle />
<template #content> <template #content>
@ -66,9 +66,8 @@ const formattedText = computed(() => {
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';
</style> </style>

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,7 @@ const props = defineProps({
default: () => [], default: () => [],
}, },
}); });
console.log(props.optimization, 'optimization');
</script> </script>
<style scoped lang="scss"> <style lang="scss">
@import './style.scss';
</style> </style>

View File

@ -166,7 +166,9 @@ const downPage = async () => {
let fileUrl = saveForm.file_url; let fileUrl = saveForm.file_url;
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');
fileUrl =
'https://lingji-test-1334771076.cos.ap-nanjing.myqcloud.com/files/1b0d2056-75e1-4f23-995a-17c1b28b44e9.pdf';
saveForm.file_url = fileUrl; saveForm.file_url = fileUrl;
} }
console.log(fileUrl, 'fileUrl'); console.log(fileUrl, 'fileUrl');
@ -273,9 +275,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,6 +1,7 @@
.table-wrap { .table-wrap {
width: 100%; width: 100%;
.pagination-box { .pagination-box {
display: flex; display: flex;
width: 100%; width: 100%;
@ -10,161 +11,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 +18,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 +122,154 @@
} }
} }
} }
.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;
}
.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
}
.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 {
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
}
} }