432 lines
11 KiB
Vue
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> |