feat(api): 更新知识库和文档接口路径

feat(views): 新增广告监控相关页面组件
This commit is contained in:
2026-04-10 14:15:51 +08:00
parent 091a159eec
commit c80f67d2ab
7 changed files with 2475 additions and 17 deletions

View File

@@ -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 },
});

View File

@@ -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 },
});

View 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>

View 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>

View 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>

View 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>

View 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>