feat(api): 更新知识库和文档接口路径
feat(views): 新增广告监控相关页面组件
This commit is contained in:
432
src/views/ads/summary/customer/index.vue
Normal file
432
src/views/ads/summary/customer/index.vue
Normal file
@@ -0,0 +1,432 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user