feat(api): 更新知识库和文档接口路径
feat(views): 新增广告监控相关页面组件
This commit is contained in:
@@ -34,7 +34,7 @@ export interface knowledgeInfo {
|
||||
// 获取知识库列表
|
||||
export function listknowledges(params: knowledgeQueryParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/dataset/listDataset',
|
||||
url: '/rag/dataset/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
@@ -43,7 +43,7 @@ export function listknowledges(params: knowledgeQueryParams) {
|
||||
// 创建知识库
|
||||
export function createknowledge(data: CreateknowledgeParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/dataset/createDataset',
|
||||
url: '/rag/dataset/create',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -52,7 +52,7 @@ export function createknowledge(data: CreateknowledgeParams) {
|
||||
// 更新知识库
|
||||
export function updateknowledge(data: UpdateknowledgeParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/dataset/updateDataset',
|
||||
url: '/rag/dataset/update',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
@@ -61,7 +61,7 @@ export function updateknowledge(data: UpdateknowledgeParams) {
|
||||
// 删除知识库
|
||||
export function deleteknowledge(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/dataset/deleteDataset',
|
||||
url: '/rag/dataset/delete',
|
||||
method: 'delete',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export interface DocumentInfo {
|
||||
// 获取文档列表
|
||||
export function listDocuments(params: DocumentQueryParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/listDocument',
|
||||
url: '/rag/document/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export function listDocuments(params: DocumentQueryParams) {
|
||||
// 获取文档详情
|
||||
export function getDocument(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/getDocument',
|
||||
url: '/rag/document/get',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
@@ -74,7 +74,7 @@ export function getDocument(id: string) {
|
||||
// 创建文档
|
||||
export function createDocument(data: CreateDocumentParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/createDocument',
|
||||
url: '/rag/document/create',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -83,7 +83,7 @@ export function createDocument(data: CreateDocumentParams) {
|
||||
// 更新文档
|
||||
export function updateDocument(data: UpdateDocumentParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/updateDocument',
|
||||
url: '/rag/document/update',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
@@ -104,7 +104,7 @@ export function uploadFile(file: File) {
|
||||
// 上传文档
|
||||
export function uploadDocument(data: FormData) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/createDocument',
|
||||
url: '/rag/document/create',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: {
|
||||
@@ -116,7 +116,7 @@ export function uploadDocument(data: FormData) {
|
||||
// 预览文档
|
||||
export function previewDocument(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/previewDocument',
|
||||
url: '/rag/document/preview',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
@@ -125,7 +125,7 @@ export function previewDocument(id: string) {
|
||||
// 删除文档
|
||||
export function deleteDocument(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/deleteDocument',
|
||||
url: '/rag/document/delete',
|
||||
method: 'delete',
|
||||
params: { id },
|
||||
});
|
||||
@@ -134,7 +134,7 @@ export function deleteDocument(id: string) {
|
||||
// 批量删除文档
|
||||
export function batchDeleteDocuments(ids: string[]) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/batchDeleteDocument',
|
||||
url: '/rag/document/batchDeleteDocument',
|
||||
method: 'delete',
|
||||
data: { ids },
|
||||
});
|
||||
@@ -143,7 +143,7 @@ export function batchDeleteDocuments(ids: string[]) {
|
||||
// 重新处理文档
|
||||
export function reprocessDocument(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/reprocessDocument',
|
||||
url: '/rag/document/reprocessDocument',
|
||||
method: 'post',
|
||||
data: { id },
|
||||
});
|
||||
@@ -152,7 +152,7 @@ export function reprocessDocument(id: string) {
|
||||
// 获取文档分段列表
|
||||
export function listDocumentChunks(params: DocumentChunkQueryParams) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/listDocumentChunk',
|
||||
url: '/rag/document/listDocumentChunk',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
@@ -161,7 +161,7 @@ export function listDocumentChunks(params: DocumentChunkQueryParams) {
|
||||
// 更新文档分段
|
||||
export function updateDocumentChunk(data: { id: string; content: string }) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/updateDocumentChunk',
|
||||
url: '/rag/document/updateDocumentChunk',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
@@ -170,7 +170,7 @@ export function updateDocumentChunk(data: { id: string; content: string }) {
|
||||
// 删除文档分段
|
||||
export function deleteDocumentChunk(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/deleteDocumentChunk',
|
||||
url: '/rag/document/deleteDocumentChunk',
|
||||
method: 'delete',
|
||||
params: { id },
|
||||
});
|
||||
@@ -179,7 +179,7 @@ export function deleteDocumentChunk(id: string) {
|
||||
// 获取文件向量化处理进度
|
||||
export function getDocumentProcess(id: string) {
|
||||
return request({
|
||||
url: '/rag-knowledge/document/getProcess',
|
||||
url: '/rag/document/getProcess',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
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>
|
||||
297
src/views/ads/summary/daily/index.vue
Normal file
297
src/views/ads/summary/daily/index.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="ads-summary-daily">
|
||||
<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>
|
||||
<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-card>
|
||||
<template #header><div class="card-header">每日数据趋势</div></template>
|
||||
<div ref="chartRef" class="chart"></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="date" label="日期" />
|
||||
<el-table-column prop="impressions" label="曝光" />
|
||||
<el-table-column prop="clicks" label="点击" />
|
||||
<el-table-column prop="ctr" label="点击率"><template #default="scope">{{ scope.row.ctr }}%</template></el-table-column>
|
||||
<el-table-column prop="conversions" label="转化" />
|
||||
<el-table-column prop="conversionRate" label="转化率"><template #default="scope">{{ scope.row.conversionRate }}%</template></el-table-column>
|
||||
<el-table-column prop="cost" label="花费"><template #default="scope">¥{{ scope.row.cost }}</template></el-table-column>
|
||||
<el-table-column prop="cpc" label="CPC"><template #default="scope">¥{{ scope.row.cpc }}</template></el-table-column>
|
||||
<el-table-column prop="cpa" label="CPA"><template #default="scope">¥{{ scope.row.cpa }}</template></el-table-column>
|
||||
<el-table-column prop="roi" label="ROI"><template #default="scope">{{ scope.row.roi }}</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 searchParams = reactive({
|
||||
dateRange: [],
|
||||
});
|
||||
|
||||
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 chartRef = ref();
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
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 end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
|
||||
let current = new Date(start);
|
||||
while (current <= end) {
|
||||
const date = current.toISOString().split('T')[0];
|
||||
const impressions = Math.floor(Math.random() * 10000) + 5000;
|
||||
const clicks = Math.floor(impressions * (Math.random() * 0.05 + 0.01));
|
||||
const ctr = ((clicks / impressions) * 100).toFixed(2);
|
||||
const conversions = Math.floor(clicks * (Math.random() * 0.1 + 0.02));
|
||||
const conversionRate = ((conversions / clicks) * 100).toFixed(2);
|
||||
const cost = (Math.random() * 1000 + 100).toFixed(2);
|
||||
const cpc = (parseFloat(cost) / clicks).toFixed(2);
|
||||
const cpa = (parseFloat(cost) / conversions).toFixed(2);
|
||||
const roi = (Math.random() * 5 + 1).toFixed(2);
|
||||
|
||||
mockData.push({
|
||||
date,
|
||||
impressions,
|
||||
clicks,
|
||||
ctr,
|
||||
conversions,
|
||||
conversionRate,
|
||||
cost,
|
||||
cpc,
|
||||
cpa,
|
||||
roi,
|
||||
});
|
||||
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
return mockData;
|
||||
};
|
||||
|
||||
const initChart = (data: any[]) => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (chart) chart.dispose();
|
||||
chart = echarts.init(chartRef.value);
|
||||
|
||||
const dates = data.map(item => item.date);
|
||||
const impressions = data.map(item => item.impressions);
|
||||
const clicks = data.map(item => item.clicks);
|
||||
const conversions = data.map(item => item.conversions);
|
||||
const cost = data.map(item => parseFloat(item.cost));
|
||||
|
||||
chart.setOption({
|
||||
title: {
|
||||
text: '每日数据趋势',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
legend: {
|
||||
data: ['曝光', '点击', '转化', '花费'],
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '花费',
|
||||
axisLabel: {
|
||||
formatter: '¥{value}',
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '曝光',
|
||||
type: 'bar',
|
||||
data: impressions,
|
||||
},
|
||||
{
|
||||
name: '点击',
|
||||
type: 'line',
|
||||
data: clicks,
|
||||
},
|
||||
{
|
||||
name: '转化',
|
||||
type: 'line',
|
||||
data: conversions,
|
||||
},
|
||||
{
|
||||
name: '花费',
|
||||
type: 'line',
|
||||
data: cost,
|
||||
yAxisIndex: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
data.value = getMockData();
|
||||
pagination.total = data.value.length;
|
||||
initChart(data.value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
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', () => chart?.resize());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ads-summary-daily {
|
||||
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: 400px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
461
src/views/ads/summary/industry/index.vue
Normal file
461
src/views/ads/summary/industry/index.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div class="ads-summary-industry">
|
||||
<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.industry" clearable placeholder="请选择行业">
|
||||
<el-option label="电商" value="ecommerce" />
|
||||
<el-option label="教育" value="education" />
|
||||
<el-option label="金融" value="finance" />
|
||||
<el-option label="医疗" value="medical" />
|
||||
<el-option label="游戏" value="game" />
|
||||
</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="trendChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header><div class="card-header">行业对比</div></template>
|
||||
<div ref="comparisonChartRef" 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.avgImpressions }}</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.avgClicks }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover" class="stats-card">
|
||||
<div class="stats-item">
|
||||
<div class="stats-label">行业平均CTR</div>
|
||||
<div class="stats-value">{{ totalStats.avgCtr }}%</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.avgConversions }}</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="date" label="日期" />
|
||||
<el-table-column prop="industry" label="行业"><template #default="scope">{{ industryMap[scope.row.industry] }}</template></el-table-column>
|
||||
<el-table-column prop="impressions" label="曝光" />
|
||||
<el-table-column prop="clicks" label="点击" />
|
||||
<el-table-column prop="ctr" label="点击率"><template #default="scope">{{ scope.row.ctr }}%</template></el-table-column>
|
||||
<el-table-column prop="conversions" label="转化" />
|
||||
<el-table-column prop="conversionRate" label="转化率"><template #default="scope">{{ scope.row.conversionRate }}%</template></el-table-column>
|
||||
<el-table-column prop="cost" label="花费"><template #default="scope">¥{{ scope.row.cost }}</template></el-table-column>
|
||||
<el-table-column prop="roi" label="ROI"><template #default="scope">{{ scope.row.roi }}</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 industryMap = {
|
||||
ecommerce: '电商',
|
||||
education: '教育',
|
||||
finance: '金融',
|
||||
medical: '医疗',
|
||||
game: '游戏',
|
||||
};
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
industry: '',
|
||||
});
|
||||
|
||||
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 trendChartRef = ref();
|
||||
const comparisonChartRef = ref();
|
||||
let trendChart: echarts.ECharts | null = null;
|
||||
let comparisonChart: echarts.ECharts | null = null;
|
||||
|
||||
const totalStats = reactive({
|
||||
avgImpressions: 0,
|
||||
avgClicks: 0,
|
||||
avgCtr: 0,
|
||||
avgConversions: 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 industries = ['ecommerce', 'education', 'finance', 'medical', 'game'];
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
|
||||
let current = new Date(start);
|
||||
while (current <= end) {
|
||||
const date = current.toISOString().split('T')[0];
|
||||
industries.forEach(industry => {
|
||||
const impressions = Math.floor(Math.random() * 10000) + 5000;
|
||||
const clicks = Math.floor(impressions * (Math.random() * 0.05 + 0.01));
|
||||
const ctr = ((clicks / impressions) * 100).toFixed(2);
|
||||
const conversions = Math.floor(clicks * (Math.random() * 0.1 + 0.02));
|
||||
const conversionRate = ((conversions / clicks) * 100).toFixed(2);
|
||||
const cost = (Math.random() * 1000 + 100).toFixed(2);
|
||||
const roi = (Math.random() * 5 + 1).toFixed(2);
|
||||
|
||||
mockData.push({
|
||||
date,
|
||||
industry,
|
||||
impressions,
|
||||
clicks,
|
||||
ctr,
|
||||
conversions,
|
||||
conversionRate,
|
||||
cost,
|
||||
roi,
|
||||
});
|
||||
});
|
||||
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
return mockData;
|
||||
};
|
||||
|
||||
const calculateTotalStats = (data: any[]) => {
|
||||
if (data.length === 0) return;
|
||||
|
||||
totalStats.avgImpressions = Math.round(data.reduce((sum, item) => sum + item.impressions, 0) / data.length);
|
||||
totalStats.avgClicks = Math.round(data.reduce((sum, item) => sum + item.clicks, 0) / data.length);
|
||||
totalStats.avgCtr = (data.reduce((sum, item) => sum + parseFloat(item.ctr), 0) / data.length).toFixed(2);
|
||||
totalStats.avgConversions = Math.round(data.reduce((sum, item) => sum + item.conversions, 0) / data.length);
|
||||
};
|
||||
|
||||
const initTrendChart = (data: any[]) => {
|
||||
if (!trendChartRef.value) return;
|
||||
|
||||
if (trendChart) trendChart.dispose();
|
||||
trendChart = echarts.init(trendChartRef.value);
|
||||
|
||||
// 按日期分组数据
|
||||
const dateMap: { [key: string]: { impressions: number; clicks: number } } = {};
|
||||
data.forEach(item => {
|
||||
if (!dateMap[item.date]) {
|
||||
dateMap[item.date] = { impressions: 0, clicks: 0 };
|
||||
}
|
||||
dateMap[item.date].impressions += item.impressions;
|
||||
dateMap[item.date].clicks += item.clicks;
|
||||
});
|
||||
|
||||
const dates = Object.keys(dateMap).sort();
|
||||
const impressions = dates.map(date => dateMap[date].impressions);
|
||||
const clicks = dates.map(date => dateMap[date].clicks);
|
||||
|
||||
trendChart.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: impressions,
|
||||
},
|
||||
{
|
||||
name: '点击',
|
||||
type: 'line',
|
||||
data: clicks,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const initComparisonChart = (data: any[]) => {
|
||||
if (!comparisonChartRef.value) return;
|
||||
|
||||
if (comparisonChart) comparisonChart.dispose();
|
||||
comparisonChart = echarts.init(comparisonChartRef.value);
|
||||
|
||||
// 按行业分组数据
|
||||
const industryMap: { [key: string]: { ctr: number; conversionRate: number } } = {};
|
||||
data.forEach(item => {
|
||||
if (!industryMap[item.industry]) {
|
||||
industryMap[item.industry] = { ctr: 0, conversionRate: 0 };
|
||||
}
|
||||
industryMap[item.industry].ctr += parseFloat(item.ctr);
|
||||
industryMap[item.industry].conversionRate += parseFloat(item.conversionRate);
|
||||
});
|
||||
|
||||
// 计算平均值
|
||||
const industryCount = Object.keys(industryMap).length;
|
||||
Object.keys(industryMap).forEach(industry => {
|
||||
industryMap[industry].ctr /= industryCount;
|
||||
industryMap[industry].conversionRate /= industryCount;
|
||||
});
|
||||
|
||||
comparisonChart.setOption({
|
||||
title: {
|
||||
text: '行业对比',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['点击率', '转化率'],
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Object.keys(industryMap).map(industry => industryMap[industry as keyof typeof industryMap]),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '百分比',
|
||||
axisLabel: {
|
||||
formatter: '{value}%',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '点击率',
|
||||
type: 'bar',
|
||||
data: Object.values(industryMap).map(item => item.ctr.toFixed(2)),
|
||||
},
|
||||
{
|
||||
name: '转化率',
|
||||
type: 'bar',
|
||||
data: Object.values(industryMap).map(item => item.conversionRate.toFixed(2)),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
data.value = getMockData();
|
||||
pagination.total = data.value.length;
|
||||
calculateTotalStats(data.value);
|
||||
initTrendChart(data.value);
|
||||
initComparisonChart(data.value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.industry = '';
|
||||
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', () => {
|
||||
trendChart?.resize();
|
||||
comparisonChart?.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ads-summary-industry {
|
||||
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>
|
||||
815
src/views/ads/summary/monitor/index.vue
Normal file
815
src/views/ads/summary/monitor/index.vue
Normal file
@@ -0,0 +1,815 @@
|
||||
<template>
|
||||
<div class="ads-summary-monitor">
|
||||
<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.department" placeholder="选择部门"
|
||||
><el-option v-for="option in departmentOptions" :key="option.value || 'all'" :label="option.label" :value="option.value" /></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="trendChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header><div class="card-header">部门达成率对比</div></template>
|
||||
<div ref="comparisonChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header><div class="card-header">预算执行分布</div></template>
|
||||
<div ref="distributionChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header><div class="card-header">月度预算执行</div></template>
|
||||
<div ref="monthlyChartRef" 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.totalBudget }}</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.executed }}</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.overallRate }}%</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.dailyAverage }}</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="department" label="部门" />
|
||||
<el-table-column prop="monthlyBudget" label="月度任务" />
|
||||
<el-table-column prop="executed" label="已达成" />
|
||||
<el-table-column prop="achievementRate" label="达成率"
|
||||
><template #default="scope">{{ scope.row.achievementRate }}%</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="surplusBudget" label="剩余任务" />
|
||||
<el-table-column prop="dailyAverage" label="剩余日均" />
|
||||
<el-table-column prop="yesterday" label="昨日" />
|
||||
<el-table-column prop="estimatedAchievement" label="预计达成" />
|
||||
<el-table-column prop="dailyDifference" label="日均差" />
|
||||
<el-table-column prop="previousDay" label="前日" />
|
||||
<el-table-column prop="sequentialRatio" label="环比" />
|
||||
<el-table-column prop="timeProgress" label="对比时间进度" />
|
||||
<el-table-column prop="januaryAchievement" label="1月达成" />
|
||||
<el-table-column prop="januarySequentialRatio" label="环比" />
|
||||
<el-table-column prop="differenceFromTarget" label="目标差" />
|
||||
</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 } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
interface DepartmentData {
|
||||
department: string;
|
||||
monthlyBudget: string;
|
||||
executed: string;
|
||||
achievementRate: string;
|
||||
surplusBudget: string;
|
||||
surplusDays: string;
|
||||
dailyAverage: string;
|
||||
yesterday: string;
|
||||
estimatedAchievement: string;
|
||||
dailyDifference: string;
|
||||
previousDay: string;
|
||||
sequentialRatio: string;
|
||||
timeProgress: string;
|
||||
januaryAchievement: string;
|
||||
januarySequentialRatio: string;
|
||||
differenceFromTarget: string;
|
||||
}
|
||||
|
||||
const departmentOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '一区一部', value: '一区一部' },
|
||||
{ label: '一区二部', value: '一区二部' },
|
||||
{ label: '一区三部', value: '一区三部' },
|
||||
{ label: '一区四部', value: '一区四部' },
|
||||
{ label: '代理运营总', value: '代理运营总' },
|
||||
{ label: '渠道部', value: '渠道部' },
|
||||
{ label: '电商一部', value: '电商一部' },
|
||||
{ label: '电商二部', value: '电商二部' },
|
||||
{ label: '张哥自营', value: '张哥自营' },
|
||||
{ label: '电商合计', value: '电商合计' },
|
||||
{ label: '金牛总任务', value: '金牛总任务' },
|
||||
];
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
department: '',
|
||||
});
|
||||
|
||||
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 trendChartRef = ref();
|
||||
const comparisonChartRef = ref();
|
||||
const distributionChartRef = ref();
|
||||
const monthlyChartRef = ref();
|
||||
let trendChart: echarts.ECharts | null = null;
|
||||
let comparisonChart: echarts.ECharts | null = null;
|
||||
let distributionChart: echarts.ECharts | null = null;
|
||||
let monthlyChart: echarts.ECharts | null = null;
|
||||
|
||||
const totalStats = reactive({
|
||||
totalBudget: '0',
|
||||
executed: '0',
|
||||
overallRate: '0',
|
||||
dailyAverage: '0',
|
||||
});
|
||||
|
||||
const data = ref<DepartmentData[]>([]);
|
||||
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 = (): DepartmentData[] => {
|
||||
// 根据图片中的数据生成模拟数据
|
||||
const departments = [
|
||||
{
|
||||
name: '一区一部',
|
||||
monthlyBudget: '1300000',
|
||||
executed: '937947.12',
|
||||
achievementRate: '72.15',
|
||||
surplusBudget: '362052.88',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '17240.61',
|
||||
yesterday: '36129.91',
|
||||
estimatedAchievement: '130.51%',
|
||||
dailyDifference: '18889.30',
|
||||
previousDay: '47003.57',
|
||||
sequentialRatio: '-10873.66',
|
||||
timeProgress: '42%',
|
||||
januaryAchievement: '1547156.813',
|
||||
januarySequentialRatio: '-16%',
|
||||
differenceFromTarget: '-7%',
|
||||
},
|
||||
{
|
||||
name: '一区二部',
|
||||
monthlyBudget: '3500000',
|
||||
executed: '2338291.84',
|
||||
achievementRate: '66.81',
|
||||
surplusBudget: '1161708.16',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '55319.44',
|
||||
yesterday: '106494.91',
|
||||
estimatedAchievement: '130.71%',
|
||||
dailyDifference: '51175.47',
|
||||
previousDay: '105791.93',
|
||||
sequentialRatio: '702.97',
|
||||
timeProgress: '37%',
|
||||
januaryAchievement: '3789374.035',
|
||||
januarySequentialRatio: '-8%',
|
||||
differenceFromTarget: '2%',
|
||||
},
|
||||
{
|
||||
name: '一区三部',
|
||||
monthlyBudget: '2100000',
|
||||
executed: '1896655.41',
|
||||
achievementRate: '90.32',
|
||||
surplusBudget: '203344.59',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '9683.07',
|
||||
yesterday: '55613.03',
|
||||
estimatedAchievement: '145.93%',
|
||||
dailyDifference: '45928.43',
|
||||
previousDay: '53994.08',
|
||||
sequentialRatio: '1618.95',
|
||||
timeProgress: '60%',
|
||||
januaryAchievement: '2861643.583',
|
||||
januarySequentialRatio: '-27%',
|
||||
differenceFromTarget: '-19%',
|
||||
},
|
||||
{
|
||||
name: '一区四部',
|
||||
monthlyBudget: '1600000',
|
||||
executed: '1960555.94',
|
||||
achievementRate: '122.53',
|
||||
surplusBudget: '-360555.94',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '-17169.33',
|
||||
yesterday: '66458.76',
|
||||
estimatedAchievement: '209.76%',
|
||||
dailyDifference: '83628.09',
|
||||
previousDay: '79558.31',
|
||||
sequentialRatio: '-1309.55',
|
||||
timeProgress: '93%',
|
||||
januaryAchievement: '2216583.797',
|
||||
januarySequentialRatio: '-28%',
|
||||
differenceFromTarget: '-20%',
|
||||
},
|
||||
{
|
||||
name: '代理运营总',
|
||||
monthlyBudget: '8500000',
|
||||
executed: '7134418.26',
|
||||
achievementRate: '83.93',
|
||||
surplusBudget: '1365581.74',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '65027.70',
|
||||
yesterday: '264596.76',
|
||||
estimatedAchievement: '149.32%',
|
||||
dailyDifference: '199682.28',
|
||||
previousDay: '286347.89',
|
||||
sequentialRatio: '-21651.29',
|
||||
timeProgress: '54%',
|
||||
januaryAchievement: '10414758.23',
|
||||
januarySequentialRatio: '-18%',
|
||||
differenceFromTarget: '-10%',
|
||||
},
|
||||
{
|
||||
name: '渠道部',
|
||||
monthlyBudget: '3000000',
|
||||
executed: '29134092.68',
|
||||
achievementRate: '97.11',
|
||||
surplusBudget: '865907.32',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '41233.68',
|
||||
yesterday: '806116.365',
|
||||
estimatedAchievement: '153.54%',
|
||||
dailyDifference: '764882.68',
|
||||
previousDay: '844507.14',
|
||||
sequentialRatio: '-38390.78',
|
||||
timeProgress: '67%',
|
||||
januaryAchievement: '39078360.25',
|
||||
januarySequentialRatio: '-23%',
|
||||
differenceFromTarget: '-15%',
|
||||
},
|
||||
{
|
||||
name: '电商一部',
|
||||
monthlyBudget: '4700000',
|
||||
executed: '3993326.756',
|
||||
achievementRate: '83.88',
|
||||
surplusBudget: '706673.24',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '33651.11',
|
||||
yesterday: '125785.982',
|
||||
estimatedAchievement: '140.02%',
|
||||
dailyDifference: '89563.45',
|
||||
previousDay: '132741.39',
|
||||
sequentialRatio: '-6955.41',
|
||||
timeProgress: '54%',
|
||||
januaryAchievement: '4522665.91',
|
||||
januarySequentialRatio: '4%',
|
||||
differenceFromTarget: '15%',
|
||||
},
|
||||
{
|
||||
name: '电商二部',
|
||||
monthlyBudget: '2800000',
|
||||
executed: '1302679.056',
|
||||
achievementRate: '46.52',
|
||||
surplusBudget: '1497320.94',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '7130.10',
|
||||
yesterday: '26049.373',
|
||||
estimatedAchievement: '66.06%',
|
||||
dailyDifference: '-45251.62',
|
||||
previousDay: '27892.62',
|
||||
sequentialRatio: '-1843.24',
|
||||
timeProgress: '17%',
|
||||
januaryAchievement: '2776056.70',
|
||||
januarySequentialRatio: '1%',
|
||||
differenceFromTarget: '12%',
|
||||
},
|
||||
{
|
||||
name: '张哥自营',
|
||||
monthlyBudget: '6000000',
|
||||
executed: '9770673.61',
|
||||
achievementRate: '162.84',
|
||||
surplusBudget: '-3770673.61',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '-179555.89',
|
||||
yesterday: '392448.463',
|
||||
estimatedAchievement: '300.20%',
|
||||
dailyDifference: '572004.35',
|
||||
previousDay: '331376.53',
|
||||
sequentialRatio: '61071.93',
|
||||
timeProgress: '133%',
|
||||
januaryAchievement: '8310458.83',
|
||||
januarySequentialRatio: '-28%',
|
||||
differenceFromTarget: '-20%',
|
||||
},
|
||||
{
|
||||
name: '电商合计',
|
||||
monthlyBudget: '13500000',
|
||||
executed: '15012801.94',
|
||||
achievementRate: '111.21',
|
||||
surplusBudget: '-1512801.94',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '-72038.19',
|
||||
yesterday: '544283.818',
|
||||
estimatedAchievement: '195.87%',
|
||||
dailyDifference: '616316.17',
|
||||
previousDay: '492286.58',
|
||||
sequentialRatio: '5207.28',
|
||||
timeProgress: '81%',
|
||||
januaryAchievement: '15609181.44',
|
||||
januarySequentialRatio: '-14%',
|
||||
differenceFromTarget: '-4%',
|
||||
},
|
||||
{
|
||||
name: '金牛总任务',
|
||||
monthlyBudget: '52000000',
|
||||
executed: '51280190.4',
|
||||
achievementRate: '98.62',
|
||||
surplusBudget: '719809.60',
|
||||
surplusDays: '21',
|
||||
dailyAverage: '34276.65',
|
||||
yesterday: '1615096.78',
|
||||
estimatedAchievement: '163.84%',
|
||||
dailyDifference: '1622865.58',
|
||||
previousDay: '1622865.58',
|
||||
sequentialRatio: '-7768.79',
|
||||
timeProgress: '69%',
|
||||
januaryAchievement: '65108327.29',
|
||||
januarySequentialRatio: '-20%',
|
||||
differenceFromTarget: '-12%',
|
||||
},
|
||||
];
|
||||
|
||||
const mockData: DepartmentData[] = departments.map((dept) => ({
|
||||
department: dept.name,
|
||||
monthlyBudget: dept.monthlyBudget,
|
||||
executed: dept.executed,
|
||||
achievementRate: dept.achievementRate,
|
||||
surplusBudget: dept.surplusBudget,
|
||||
surplusDays: dept.surplusDays,
|
||||
dailyAverage: dept.dailyAverage,
|
||||
yesterday: dept.yesterday,
|
||||
estimatedAchievement: dept.estimatedAchievement,
|
||||
dailyDifference: dept.dailyDifference,
|
||||
previousDay: dept.previousDay,
|
||||
sequentialRatio: dept.sequentialRatio,
|
||||
timeProgress: dept.timeProgress,
|
||||
januaryAchievement: dept.januaryAchievement,
|
||||
januarySequentialRatio: dept.januarySequentialRatio,
|
||||
differenceFromTarget: dept.differenceFromTarget,
|
||||
}));
|
||||
|
||||
return mockData;
|
||||
};
|
||||
|
||||
const calculateTotalStats = (_data: DepartmentData[]) => {
|
||||
// 使用图片中的总数据
|
||||
totalStats.totalBudget = '52000000';
|
||||
totalStats.executed = '51280190.4';
|
||||
totalStats.overallRate = '98.62';
|
||||
totalStats.dailyAverage = '1654199.70';
|
||||
};
|
||||
|
||||
const initTrendChart = (_data: DepartmentData[]) => {
|
||||
if (!trendChartRef.value) return;
|
||||
|
||||
if (trendChart) trendChart.dispose();
|
||||
trendChart = echarts.init(trendChartRef.value);
|
||||
|
||||
// 使用图片中的2026年2月数据
|
||||
const dates = [
|
||||
'2026/2/1',
|
||||
'2026/2/2',
|
||||
'2026/2/3',
|
||||
'2026/2/4',
|
||||
'2026/2/5',
|
||||
'2026/2/6',
|
||||
'2026/2/7',
|
||||
'2026/2/8',
|
||||
'2026/2/9',
|
||||
'2026/2/10',
|
||||
'2026/2/11',
|
||||
'2026/2/12',
|
||||
'2026/2/13',
|
||||
'2026/2/14',
|
||||
'2026/2/15',
|
||||
'2026/2/16',
|
||||
];
|
||||
const executedData = [
|
||||
2149518.05, 1871119.64, 2088655.19, 1702786.8, 1770269.31, 1714822.74, 1649665.0, 1622865.58, 1615096.78, 1485551.17, 1407212.21, 1364826.0,
|
||||
1233123.83, 1089680.69, 1086898.47, 816321.92,
|
||||
];
|
||||
const targetData = dates.map(() => 1733333.33); // 52000000 / 30
|
||||
|
||||
trendChart.setOption({
|
||||
title: {
|
||||
text: '预算执行趋势',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>{a}: ¥{c}',
|
||||
},
|
||||
legend: {
|
||||
data: ['实际执行', '目标'],
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
axisLabel: {
|
||||
formatter: '¥{value}',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际执行',
|
||||
type: 'line',
|
||||
data: executedData,
|
||||
areaStyle: {},
|
||||
},
|
||||
{
|
||||
name: '目标',
|
||||
type: 'line',
|
||||
data: targetData,
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const initComparisonChart = (data: DepartmentData[]) => {
|
||||
if (!comparisonChartRef.value) return;
|
||||
|
||||
if (comparisonChart) comparisonChart.dispose();
|
||||
comparisonChart = echarts.init(comparisonChartRef.value);
|
||||
|
||||
const departments = data.map((item) => item.department);
|
||||
const achievementRates = data.map((item) => parseFloat(item.achievementRate));
|
||||
|
||||
comparisonChart.setOption({
|
||||
title: {
|
||||
text: '部门达成率对比',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>{a}: {c}%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: departments,
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '达成率',
|
||||
axisLabel: {
|
||||
formatter: '{value}%',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '达成率',
|
||||
type: 'bar',
|
||||
data: achievementRates,
|
||||
itemStyle: {
|
||||
color: function (params: any) {
|
||||
const rate = params.value;
|
||||
if (rate >= 80) return '#67c23a';
|
||||
if (rate >= 60) return '#e6a23c';
|
||||
return '#f56c6c';
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const initDistributionChart = (data: DepartmentData[]) => {
|
||||
if (!distributionChartRef.value) return;
|
||||
|
||||
if (distributionChart) distributionChart.dispose();
|
||||
distributionChart = echarts.init(distributionChartRef.value);
|
||||
|
||||
distributionChart.setOption({
|
||||
title: {
|
||||
text: '预算执行分布',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: ¥{c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: data.map((item) => item.department),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '预算执行',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: data.map((item) => ({
|
||||
value: parseFloat(item.executed),
|
||||
name: item.department,
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const initMonthlyChart = (_data: DepartmentData[]) => {
|
||||
if (!monthlyChartRef.value) return;
|
||||
|
||||
if (monthlyChart) monthlyChart.dispose();
|
||||
monthlyChart = echarts.init(monthlyChartRef.value);
|
||||
|
||||
const months = ['1月', '2月', '3月', '4月', '5月', '6月'];
|
||||
const budgetData = months.map(() => (Math.random() * 10000000 + 5000000).toFixed(2));
|
||||
const executedData = months.map((_, index) => (parseFloat(budgetData[index]) * (Math.random() * 0.3 + 0.5)).toFixed(2));
|
||||
|
||||
monthlyChart.setOption({
|
||||
title: {
|
||||
text: '月度预算执行',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>{a}: ¥{c}',
|
||||
},
|
||||
legend: {
|
||||
data: ['预算', '实际执行'],
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: months,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
axisLabel: {
|
||||
formatter: '¥{value}',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '预算',
|
||||
type: 'bar',
|
||||
data: budgetData.map(parseFloat),
|
||||
itemStyle: {
|
||||
color: '#909399',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '实际执行',
|
||||
type: 'bar',
|
||||
data: executedData.map(parseFloat),
|
||||
itemStyle: {
|
||||
color: '#409eff',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
data.value = getMockData();
|
||||
pagination.total = data.value.length;
|
||||
calculateTotalStats(data.value);
|
||||
initTrendChart(data.value);
|
||||
initComparisonChart(data.value);
|
||||
initDistributionChart(data.value);
|
||||
initMonthlyChart(data.value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.department = '';
|
||||
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', () => {
|
||||
trendChart?.resize();
|
||||
comparisonChart?.resize();
|
||||
distributionChart?.resize();
|
||||
monthlyChart?.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ads-summary-monitor {
|
||||
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;
|
||||
}
|
||||
|
||||
.ads-summary-monitor :deep(.el-select) {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.ads-summary-monitor :deep(.el-select__wrapper),
|
||||
.ads-summary-monitor :deep(.el-select__selected-item),
|
||||
.ads-summary-monitor :deep(.el-select__placeholder) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.granularity-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.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>
|
||||
453
src/views/ads/summary/new-customer/index.vue
Normal file
453
src/views/ads/summary/new-customer/index.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div class="ads-summary-new-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.channel" clearable placeholder="请选择渠道">
|
||||
<el-option label="搜索广告" value="search" />
|
||||
<el-option label="社交媒体" value="social" />
|
||||
<el-option label="信息流" value="feed" />
|
||||
<el-option label="其他" value="other" />
|
||||
</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="trendChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header><div class="card-header">渠道分布</div></template>
|
||||
<div ref="channelChartRef" 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.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.acquisitionCost }}</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.newCustomerConversionRate }}%</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.newCustomerRetentionRate }}%</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="date" label="日期" />
|
||||
<el-table-column prop="channel" label="渠道"
|
||||
><template #default="scope">{{ channelMap[scope.row.channel as keyof typeof channelMap] }}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="newCustomers" label="新客户数" />
|
||||
<el-table-column prop="acquisitionCost" label="获客成本"
|
||||
><template #default="scope">¥{{ scope.row.acquisitionCost }}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="conversionRate" label="转化率"
|
||||
><template #default="scope">{{ scope.row.conversionRate }}%</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="retentionRate" label="留存率"
|
||||
><template #default="scope">{{ scope.row.retentionRate }}%</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="roi" label="ROI"
|
||||
><template #default="scope">{{ scope.row.roi }}</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 } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
interface NewCustomerData {
|
||||
date: string;
|
||||
channel: string;
|
||||
newCustomers: number;
|
||||
acquisitionCost: string;
|
||||
conversionRate: string;
|
||||
retentionRate: string;
|
||||
roi: string;
|
||||
}
|
||||
|
||||
const channelMap = {
|
||||
search: '搜索广告',
|
||||
social: '社交媒体',
|
||||
feed: '信息流',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
const searchParams = reactive({
|
||||
dateRange: [],
|
||||
channel: '',
|
||||
});
|
||||
|
||||
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 trendChartRef = ref();
|
||||
const channelChartRef = ref();
|
||||
let trendChart: echarts.ECharts | null = null;
|
||||
let channelChart: echarts.ECharts | null = null;
|
||||
|
||||
const totalStats = reactive({
|
||||
newCustomers: 0,
|
||||
acquisitionCost: '0',
|
||||
newCustomerConversionRate: 0,
|
||||
newCustomerRetentionRate: 0,
|
||||
});
|
||||
|
||||
const data = ref<NewCustomerData[]>([]);
|
||||
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 = (): NewCustomerData[] => {
|
||||
const mockData: NewCustomerData[] = [];
|
||||
const channels: (keyof typeof channelMap)[] = ['search', 'social', 'feed', 'other'];
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||
|
||||
let current = new Date(start);
|
||||
while (current <= end) {
|
||||
const date = current.toISOString().split('T')[0];
|
||||
channels.forEach((channel) => {
|
||||
const newCustomers = Math.floor(Math.random() * 100) + 10;
|
||||
const acquisitionCost = (Math.random() * 500 + 50).toFixed(2);
|
||||
const conversionRate = (Math.random() * 5 + 1).toFixed(2);
|
||||
const retentionRate = (Math.random() * 30 + 20).toFixed(2);
|
||||
const roi = (Math.random() * 3 + 0.5).toFixed(2);
|
||||
|
||||
mockData.push({
|
||||
date,
|
||||
channel,
|
||||
newCustomers,
|
||||
acquisitionCost,
|
||||
conversionRate,
|
||||
retentionRate,
|
||||
roi,
|
||||
});
|
||||
});
|
||||
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
return mockData;
|
||||
};
|
||||
|
||||
const calculateTotalStats = (data: NewCustomerData[]) => {
|
||||
totalStats.newCustomers = data.reduce((sum, item) => sum + item.newCustomers, 0);
|
||||
totalStats.acquisitionCost = data.reduce((sum, item) => sum + parseFloat(item.acquisitionCost), 0).toFixed(2);
|
||||
totalStats.newCustomerConversionRate = data.reduce((sum, item) => sum + parseFloat(item.conversionRate), 0) / data.length;
|
||||
totalStats.newCustomerRetentionRate = data.reduce((sum, item) => sum + parseFloat(item.retentionRate), 0) / data.length;
|
||||
};
|
||||
|
||||
const initTrendChart = (data: NewCustomerData[]) => {
|
||||
if (!trendChartRef.value) return;
|
||||
|
||||
if (trendChart) trendChart.dispose();
|
||||
trendChart = echarts.init(trendChartRef.value);
|
||||
|
||||
// 按日期分组数据
|
||||
const dateMap: { [key: string]: number } = {};
|
||||
data.forEach((item) => {
|
||||
if (!dateMap[item.date]) {
|
||||
dateMap[item.date] = 0;
|
||||
}
|
||||
dateMap[item.date] += item.newCustomers;
|
||||
});
|
||||
|
||||
const dates = Object.keys(dateMap).sort();
|
||||
const newCustomers = dates.map((date) => dateMap[date]);
|
||||
|
||||
trendChart.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,
|
||||
areaStyle: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const initChannelChart = (data: NewCustomerData[]) => {
|
||||
if (!channelChartRef.value) return;
|
||||
|
||||
if (channelChart) channelChart.dispose();
|
||||
channelChart = echarts.init(channelChartRef.value);
|
||||
|
||||
// 按渠道分组数据
|
||||
const channelDataMap: { [key: string]: number } = {};
|
||||
data.forEach((item) => {
|
||||
if (!channelDataMap[item.channel]) {
|
||||
channelDataMap[item.channel] = 0;
|
||||
}
|
||||
channelDataMap[item.channel] += item.newCustomers;
|
||||
});
|
||||
|
||||
channelChart.setOption({
|
||||
title: {
|
||||
text: '渠道分布',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: Object.values(channelMap),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '渠道',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: Object.entries(channelDataMap).map(([channel, value]) => ({
|
||||
value,
|
||||
name: channelMap[channel as keyof typeof channelMap],
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
data.value = getMockData();
|
||||
pagination.total = data.value.length;
|
||||
calculateTotalStats(data.value);
|
||||
initTrendChart(data.value);
|
||||
initChannelChart(data.value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchParams.dateRange = [];
|
||||
searchParams.channel = '';
|
||||
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', () => {
|
||||
trendChart?.resize();
|
||||
channelChart?.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ads-summary-new-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