Files
admin-ui/src/views/ads/summary/customer/index.vue
2910410219 c80f67d2ab feat(api): 更新知识库和文档接口路径
feat(views): 新增广告监控相关页面组件
2026-04-10 14:15:51 +08:00

432 lines
11 KiB
Vue

<template>
<div class="ads-summary-customer">
<el-card shadow="hover">
<template #header><div class="card-header"><span>客户监控</span></div></template>
<div class="search-container">
<el-form :model="searchParams" :inline="true" class="search-form">
<el-form-item label="时间范围">
<el-date-picker
v-model="searchParams.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
/>
</el-form-item>
<el-form-item label="客户类型">
<el-select v-model="searchParams.customerType" clearable placeholder="请选择客户类型">
<el-option label="新客户" value="new" />
<el-option label="老客户" value="returning" />
<el-option label="高价值客户" value="high-value" />
<el-option label="潜在客户" value="potential" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="chart-container">
<el-row :gutter="20">
<el-col :span="12">
<el-card>
<template #header><div class="card-header">客户类型分布</div></template>
<div ref="typeChartRef" class="chart"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header><div class="card-header">客户行为趋势</div></template>
<div ref="behaviorChartRef" class="chart"></div>
</el-card>
</el-col>
</el-row>
</div>
<div class="data-container">
<el-card>
<template #header><div class="card-header">数据概览</div></template>
<div class="stats-grid">
<el-card shadow="hover" class="stats-card">
<div class="stats-item">
<div class="stats-label">总客户数</div>
<div class="stats-value">{{ totalStats.totalCustomers }}</div>
</div>
</el-card>
<el-card shadow="hover" class="stats-card">
<div class="stats-item">
<div class="stats-label">新客户数</div>
<div class="stats-value">{{ totalStats.newCustomers }}</div>
</div>
</el-card>
<el-card shadow="hover" class="stats-card">
<div class="stats-item">
<div class="stats-label">老客户数</div>
<div class="stats-value">{{ totalStats.returningCustomers }}</div>
</div>
</el-card>
<el-card shadow="hover" class="stats-card">
<div class="stats-item">
<div class="stats-label">客户留存率</div>
<div class="stats-value">{{ totalStats.retentionRate }}%</div>
</div>
</el-card>
</div>
</el-card>
</div>
<div class="table-container">
<el-card>
<template #header><div class="card-header">客户明细</div></template>
<el-table :data="pagedData" style="width: 100%">
<el-table-column prop="customerId" label="客户ID" />
<el-table-column prop="customerType" label="客户类型"><template #default="scope">{{ customerTypeMap[scope.row.customerType] }}</template></el-table-column>
<el-table-column prop="firstSeen" label="首次接触时间" />
<el-table-column prop="lastSeen" label="最近接触时间" />
<el-table-column prop="totalSpend" label="总花费"><template #default="scope">¥{{ scope.row.totalSpend }}</template></el-table-column>
<el-table-column prop="conversionCount" label="转化次数" />
<el-table-column prop="avgOrderValue" label="平均订单价值"><template #default="scope">¥{{ scope.row.avgOrderValue }}</template></el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue';
import * as echarts from 'echarts';
const customerTypeMap = {
new: '新客户',
returning: '老客户',
'high-value': '高价值客户',
potential: '潜在客户',
};
const searchParams = reactive({
dateRange: [],
customerType: '',
});
const dateShortcuts = [
{
text: '最近7天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: '最近30天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: '最近90天',
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
];
const typeChartRef = ref();
const behaviorChartRef = ref();
let typeChart: echarts.ECharts | null = null;
let behaviorChart: echarts.ECharts | null = null;
const totalStats = reactive({
totalCustomers: 0,
newCustomers: 0,
returningCustomers: 0,
retentionRate: 0,
});
const data = ref<any[]>([]);
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
const pagedData = computed(() => {
const start = (pagination.currentPage - 1) * pagination.pageSize;
const end = start + pagination.pageSize;
return data.value.slice(start, end);
});
const getMockData = () => {
const mockData = [];
const customerTypes = ['new', 'returning', 'high-value', 'potential'];
for (let i = 1; i <= 100; i++) {
const customerType = customerTypes[Math.floor(Math.random() * customerTypes.length)];
const firstSeen = new Date(Date.now() - Math.random() * 90 * 24 * 3600 * 1000).toISOString().split('T')[0];
const lastSeen = new Date(Date.now() - Math.random() * 7 * 24 * 3600 * 1000).toISOString().split('T')[0];
const totalSpend = (Math.random() * 10000 + 100).toFixed(2);
const conversionCount = Math.floor(Math.random() * 10) + 1;
const avgOrderValue = (parseFloat(totalSpend) / conversionCount).toFixed(2);
mockData.push({
customerId: `C${i.toString().padStart(6, '0')}`,
customerType,
firstSeen,
lastSeen,
totalSpend,
conversionCount,
avgOrderValue,
});
}
return mockData;
};
const calculateTotalStats = (data: any[]) => {
totalStats.totalCustomers = data.length;
totalStats.newCustomers = data.filter(item => item.customerType === 'new').length;
totalStats.returningCustomers = data.filter(item => item.customerType === 'returning').length;
totalStats.retentionRate = ((totalStats.returningCustomers / totalStats.totalCustomers) * 100).toFixed(2);
};
const initTypeChart = (data: any[]) => {
if (!typeChartRef.value) return;
if (typeChart) typeChart.dispose();
typeChart = echarts.init(typeChartRef.value);
const typeData = {
new: 0,
returning: 0,
'high-value': 0,
potential: 0,
};
data.forEach(item => {
typeData[item.customerType]++;
});
typeChart.setOption({
title: {
text: '客户类型分布',
left: 'center',
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
data: Object.values(customerTypeMap),
},
series: [
{
name: '客户类型',
type: 'pie',
radius: '50%',
data: [
{ value: typeData.new, name: customerTypeMap.new },
{ value: typeData.returning, name: customerTypeMap.returning },
{ value: typeData['high-value'], name: customerTypeMap['high-value'] },
{ value: typeData.potential, name: customerTypeMap.potential },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
});
};
const initBehaviorChart = (data: any[]) => {
if (!behaviorChartRef.value) return;
if (behaviorChart) behaviorChart.dispose();
behaviorChart = echarts.init(behaviorChartRef.value);
const dates = [];
const newCustomers = [];
const returningCustomers = [];
const end = new Date();
for (let i = 30; i >= 0; i--) {
const date = new Date(end);
date.setDate(date.getDate() - i);
dates.push(date.toISOString().split('T')[0]);
newCustomers.push(Math.floor(Math.random() * 20) + 5);
returningCustomers.push(Math.floor(Math.random() * 15) + 3);
}
behaviorChart.setOption({
title: {
text: '客户行为趋势',
left: 'center',
},
tooltip: {
trigger: 'axis',
},
legend: {
data: ['新客户', '老客户'],
bottom: 0,
},
xAxis: {
type: 'category',
data: dates,
axisLabel: {
rotate: 45,
},
},
yAxis: {
type: 'value',
name: '客户数',
},
series: [
{
name: '新客户',
type: 'line',
data: newCustomers,
},
{
name: '老客户',
type: 'line',
data: returningCustomers,
},
],
});
};
const handleSearch = () => {
data.value = getMockData();
pagination.total = data.value.length;
calculateTotalStats(data.value);
initTypeChart(data.value);
initBehaviorChart(data.value);
};
const handleReset = () => {
searchParams.dateRange = [];
searchParams.customerType = '';
pagination.currentPage = 1;
handleSearch();
};
const handlePageSizeChange = (size: number) => {
pagination.pageSize = size;
pagination.currentPage = 1;
};
const handlePageChange = (page: number) => {
pagination.currentPage = page;
};
onMounted(() => {
handleSearch();
window.addEventListener('resize', () => {
typeChart?.resize();
behaviorChart?.resize();
});
});
</script>
<style scoped>
.ads-summary-customer {
padding: 20px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 16px;
font-weight: 600;
}
.search-container {
margin-bottom: 20px;
}
.search-form {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-right: 12px;
margin-bottom: 12px;
}
.chart-container {
margin-bottom: 20px;
}
.chart {
width: 100%;
height: 300px;
}
.data-container {
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.stats-card {
text-align: center;
}
.stats-item {
padding: 10px;
}
.stats-label {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
}
.stats-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
.table-container {
margin-top: 20px;
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>