refactor(crm/report): 重构销售报表页面为组件化结构
将原销售报表页面拆分为多个独立组件,包括销售汇总卡片、销售员图表、行业图表、客户等级图表和订单明细组件 优化代码结构,提升可维护性和复用性
This commit is contained in:
148
klp-ui/src/views/crm/report/CustomerLevelChart.vue
Normal file
148
klp-ui/src/views/crm/report/CustomerLevelChart.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<el-card class="echarts-card" shadow="hover">
|
||||
<div slot="header" class="card-header">
|
||||
<span>客户等级统计</span>
|
||||
</div>
|
||||
<div ref="customerLevelChartRef" class="chart-item"></div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export default {
|
||||
name: "CustomerLevelChart",
|
||||
props: {
|
||||
// 客户等级统计数据
|
||||
customerLevelStatList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 客户等级字典(格式化显示用)
|
||||
customerLevelDict: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 图表实例
|
||||
customerLevelChart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.initChart();
|
||||
this.updateChart();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
// 监听数据变化,自动更新图表
|
||||
customerLevelStatList: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.updateChart();
|
||||
}
|
||||
},
|
||||
customerLevelDict: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
if (this.customerLevelChart) {
|
||||
this.customerLevelChart.dispose();
|
||||
this.customerLevelChart = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 初始化图表
|
||||
initChart() {
|
||||
const dom = this.$refs.customerLevelChartRef;
|
||||
if (dom && !this.customerLevelChart) {
|
||||
this.customerLevelChart = echarts.init(dom);
|
||||
}
|
||||
},
|
||||
// 更新图表
|
||||
updateChart() {
|
||||
if (!this.customerLevelChart) return;
|
||||
|
||||
// 格式化饼图数据
|
||||
const pieData = this.customerLevelStatList.map(item => ({
|
||||
name: this.customerLevelDict.find(d => d.value === item.customerLevel)?.label || item.customerLevel,
|
||||
value: Number(item.salesAmount || 0)
|
||||
}));
|
||||
|
||||
// 空数据配置
|
||||
const emptyOption = {
|
||||
tooltip: { trigger: "item" },
|
||||
legend: { orient: "vertical", left: "left", textStyle: { fontSize: 12 } },
|
||||
series: [{ name: "销售金额", type: "pie", radius: ["40%", "70%"], data: [] }],
|
||||
graphic: {
|
||||
type: "text",
|
||||
left: "center",
|
||||
top: "center",
|
||||
style: { text: "暂无数据", fontSize: 14, color: "#999" }
|
||||
}
|
||||
};
|
||||
|
||||
// 有数据配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: "{b}<br/>销售金额:{c} 元<br/>占比:{d}%"
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
left: "left",
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
series: [{
|
||||
name: "销售金额",
|
||||
type: "pie",
|
||||
radius: ["40%", "70%"],
|
||||
center: ["50%", "50%"],
|
||||
avoidLabelOverlap: false,
|
||||
label: { show: false },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: "bold" } },
|
||||
labelLine: { show: false },
|
||||
data: pieData
|
||||
}],
|
||||
graphic: null
|
||||
};
|
||||
|
||||
this.customerLevelChart.setOption(this.customerLevelStatList.length > 0 ? option : emptyOption, true);
|
||||
},
|
||||
// 自适应窗口
|
||||
handleResize() {
|
||||
if (this.customerLevelChart) {
|
||||
this.customerLevelChart.resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.echarts-card {
|
||||
height: 400px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
139
klp-ui/src/views/crm/report/IndustryChart.vue
Normal file
139
klp-ui/src/views/crm/report/IndustryChart.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<el-card class="echarts-card" shadow="hover">
|
||||
<div slot="header" class="card-header">
|
||||
<span>行业统计</span>
|
||||
</div>
|
||||
<div ref="industryChartRef" class="chart-item"></div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export default {
|
||||
name: "IndustryChart",
|
||||
props: {
|
||||
// 行业统计数据
|
||||
industryStatList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 图表实例
|
||||
industryChart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.initChart();
|
||||
this.updateChart();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
// 监听数据变化,自动更新图表
|
||||
industryStatList: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
if (this.industryChart) {
|
||||
this.industryChart.dispose();
|
||||
this.industryChart = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 初始化图表
|
||||
initChart() {
|
||||
const dom = this.$refs.industryChartRef;
|
||||
if (dom && !this.industryChart) {
|
||||
this.industryChart = echarts.init(dom);
|
||||
}
|
||||
},
|
||||
// 更新图表
|
||||
updateChart() {
|
||||
if (!this.industryChart) return;
|
||||
|
||||
const industryNames = this.industryStatList.map(item => item.industry);
|
||||
const salesAmounts = this.industryStatList.map(item => Number(item.salesAmount || 0));
|
||||
const customerCounts = this.industryStatList.map(item => item.customerCount || 0);
|
||||
|
||||
// 空数据配置
|
||||
const emptyOption = {
|
||||
tooltip: { trigger: "axis" },
|
||||
legend: { show: false },
|
||||
xAxis: { type: "category", data: [] },
|
||||
yAxis: { type: "value" },
|
||||
series: [{ data: [], type: "bar" }],
|
||||
graphic: {
|
||||
type: "text",
|
||||
left: "center",
|
||||
top: "center",
|
||||
style: { text: "暂无数据", fontSize: 14, color: "#999" }
|
||||
}
|
||||
};
|
||||
|
||||
// 有数据配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: { type: "shadow" },
|
||||
formatter: (params) => {
|
||||
const index = params[0].dataIndex;
|
||||
return `${industryNames[index]}<br/>销售金额:${salesAmounts[index].toFixed(2)} 元<br/>客户数量:${customerCounts[index]} 家`;
|
||||
}
|
||||
},
|
||||
legend: { show: false },
|
||||
grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: industryNames,
|
||||
axisLabel: { rotate: 30, fontSize: 12 }
|
||||
},
|
||||
yAxis: { type: "value", name: "销售金额(元)" },
|
||||
series: [{
|
||||
name: "销售金额",
|
||||
type: "bar",
|
||||
data: salesAmounts,
|
||||
itemStyle: { color: "#f72585" }
|
||||
}],
|
||||
graphic: null
|
||||
};
|
||||
|
||||
this.industryChart.setOption(this.industryStatList.length > 0 ? option : emptyOption, true);
|
||||
},
|
||||
// 自适应窗口
|
||||
handleResize() {
|
||||
if (this.industryChart) {
|
||||
this.industryChart.resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.echarts-card {
|
||||
height: 400px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
399
klp-ui/src/views/crm/report/SalesReportOrderDetail.vue
Normal file
399
klp-ui/src/views/crm/report/SalesReportOrderDetail.vue
Normal file
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<div class="order-detail-container" v-loading="loading">
|
||||
<div class="table-toolbar">
|
||||
<el-button
|
||||
type="success"
|
||||
icon="el-icon-download"
|
||||
@click="handleExport"
|
||||
>
|
||||
导出订单明细
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 三个Tab页切换 -->
|
||||
<el-tabs v-model="activeTab" class="order-tabs" tab-position="top">
|
||||
<!-- Tab1: 订单维度(订单主表信息) -->
|
||||
<el-tab-pane label="订单维度" name="mainOrder">
|
||||
<el-table
|
||||
:data="orderDetailList"
|
||||
border
|
||||
style="width: 100%;"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column prop="orderCode" label="订单编号" min-width="120"></el-table-column>
|
||||
<el-table-column prop="customerCode" label="客户编码" min-width="100"></el-table-column>
|
||||
<el-table-column prop="companyName" label="公司名称" min-width="150"></el-table-column>
|
||||
<el-table-column prop="contactPerson" label="联系人" min-width="80"></el-table-column>
|
||||
<el-table-column prop="customerLevel" label="客户等级" min-width="100"></el-table-column>
|
||||
<el-table-column prop="industry" label="所属行业" min-width="120"></el-table-column>
|
||||
<el-table-column prop="orderAmount" label="订单金额">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.orderAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="salesman" label="销售员" min-width="80"></el-table-column>
|
||||
<el-table-column prop="deliveryDate" label="交货日期" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.deliveryDate ? formatDate(scope.row.deliveryDate) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderStatus" label="订单状态" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.orderStatus === 1" type="success">已完成</el-tag>
|
||||
<el-tag v-else-if="scope.row.orderStatus === 0" type="warning">待处理</el-tag>
|
||||
<el-tag v-else type="danger">已取消</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="financeStatus" label="财务状态" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.financeStatus === 1" type="success">已结款</el-tag>
|
||||
<el-tag v-else type="danger">未结款</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unpaidAmount" label="未结金额" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.unpaidAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemCount" label="明细数量" min-width="80"></el-table-column>
|
||||
<el-table-column prop="objectionCount" label="异议数量" min-width="80"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab2: 明细维度(订单产品明细,扁平展示) -->
|
||||
<el-tab-pane label="明细维度" name="itemDetail">
|
||||
<el-table
|
||||
:data="flatOrderItemList"
|
||||
border
|
||||
style="width: 100%;"
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="orderCode" label="关联订单编号" min-width="120"></el-table-column>
|
||||
<el-table-column prop="companyName" label="客户公司" min-width="150"></el-table-column>
|
||||
<el-table-column prop="productCode" label="产品编码" min-width="120"></el-table-column>
|
||||
<el-table-column prop="productName" label="产品名称" min-width="150"></el-table-column>
|
||||
<el-table-column prop="specification" label="产品规格" min-width="120"></el-table-column>
|
||||
<el-table-column prop="productCount" label="产品数量" min-width="80"></el-table-column>
|
||||
<el-table-column prop="unitPrice" label="单价(元)" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.unitPrice) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemAmount" label="明细金额(元)" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.itemAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="明细备注" min-width="150"></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab3: 客户分组(折叠面板 + 展开后独立订单表格,替代树表) -->
|
||||
<el-tab-pane label="客户分组" name="customerGroup">
|
||||
<!-- 无客户数据提示 -->
|
||||
<div v-if="customerGroupedList.length === 0" class="empty-tip">
|
||||
暂无客户分组数据
|
||||
</div>
|
||||
|
||||
<!-- 折叠面板:每个面板对应一个客户,展开显示该客户的订单表格 -->
|
||||
<el-collapse v-else class="customer-collapse" accordion>
|
||||
<!-- 循环渲染客户折叠项 -->
|
||||
<el-collapse-item
|
||||
v-for="customer in customerGroupedList"
|
||||
:key="customer.customerKey"
|
||||
:name="customer.customerKey"
|
||||
>
|
||||
<!-- 折叠面板标题:展示客户核心信息 + 汇总数据 -->
|
||||
<template #title>
|
||||
<div class="collapse-title">
|
||||
<span class="company-name">{{ customer.companyName }}</span>
|
||||
<span class="customer-info">客户编码:{{ customer.customerCode }}</span>
|
||||
<span class="customer-info">客户等级:{{ customer.customerLevel }}</span>
|
||||
<span class="customer-info">所属行业:{{ customer.industry }}</span>
|
||||
<span class="summary-info">总订单金额:{{ formatAmount(customer.totalOrderAmount) }} 元</span>
|
||||
<span class="summary-info">订单数量:{{ customer.orderCount }} 单</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 展开后:该客户的独立订单表格 -->
|
||||
<div class="customer-order-table">
|
||||
<el-table
|
||||
:data="customer.orderList"
|
||||
border
|
||||
style="width: 100%;"
|
||||
stripe
|
||||
empty-text="该客户暂无订单数据"
|
||||
>
|
||||
<el-table-column prop="orderCode" label="订单编号" min-width="120"></el-table-column>
|
||||
<el-table-column prop="orderAmount" label="订单金额(元)" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.orderAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="salesman" label="销售员" min-width="80"></el-table-column>
|
||||
<el-table-column prop="deliveryDate" label="交货日期" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.deliveryDate ? formatDate(scope.row.deliveryDate) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderStatus" label="订单状态" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.orderStatus === 1" type="success">已完成</el-tag>
|
||||
<el-tag v-else-if="scope.row.orderStatus === 0" type="warning">待处理</el-tag>
|
||||
<el-tag v-else type="danger">已取消</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="financeStatus" label="财务状态" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.financeStatus === 1" type="success">已结款</el-tag>
|
||||
<el-tag v-else type="danger">未结款</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unpaidAmount" label="未结金额(元)" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.unpaidAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 分页组件(针对主订单列表,全局生效) -->
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pageParams.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageParams.pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 20px; text-align: right;"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SalesReportOrderDetail",
|
||||
props: {
|
||||
// 订单明细列表(主表)
|
||||
orderDetailList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 分页参数
|
||||
pageParams: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ pageNum: 1, pageSize: 10 })
|
||||
},
|
||||
// 总条数
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 金额格式化方法
|
||||
formatAmount: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
// 日期格式化方法(年月日)
|
||||
formatDate: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 激活的Tab页
|
||||
activeTab: "mainOrder"
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 扁平后的订单产品明细(Tab2使用)
|
||||
flatOrderItemList() {
|
||||
const flatList = [];
|
||||
if (!this.orderDetailList || this.orderDetailList.length === 0) {
|
||||
return flatList;
|
||||
}
|
||||
this.orderDetailList.forEach(order => {
|
||||
// 过滤空明细
|
||||
if (!order.orderItemList || order.orderItemList.length === 0) return;
|
||||
// 给每个明细添加主表关联信息
|
||||
order.orderItemList.forEach(item => {
|
||||
flatList.push({
|
||||
...item,
|
||||
orderCode: order.orderCode || '',
|
||||
companyName: order.companyName || '',
|
||||
customerCode: order.customerCode || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
return flatList;
|
||||
},
|
||||
|
||||
// 按客户分组的数据(复用原有逻辑,仅修改渲染方式)
|
||||
customerGroupedList() {
|
||||
if (!this.orderDetailList || this.orderDetailList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const customerMap = new Map();
|
||||
// 遍历订单,按客户分组
|
||||
this.orderDetailList.forEach(order => {
|
||||
// 跳过无效订单
|
||||
if (!order.customerCode && !order.companyName) {
|
||||
return;
|
||||
}
|
||||
// 生成唯一客户Key
|
||||
const customerKey = `${order.customerCode || 'unknown_code'}_${order.companyName || 'unknown_company'}`;
|
||||
// 转换订单金额为数字
|
||||
const orderAmountNum = Number(order.orderAmount) || 0;
|
||||
|
||||
// 初始化客户信息
|
||||
if (!customerMap.has(customerKey)) {
|
||||
customerMap.set(customerKey, {
|
||||
customerKey,
|
||||
companyName: order.companyName || '未知公司',
|
||||
customerCode: order.customerCode || '未知编码',
|
||||
customerLevel: order.customerLevel || '未知等级',
|
||||
industry: order.industry || '未知行业',
|
||||
totalOrderAmount: 0,
|
||||
orderCount: 0,
|
||||
orderList: []
|
||||
});
|
||||
}
|
||||
|
||||
// 更新客户统计数据和订单列表
|
||||
const customerItem = customerMap.get(customerKey);
|
||||
customerItem.orderCount += 1;
|
||||
customerItem.totalOrderAmount += orderAmountNum;
|
||||
if (order.orderCode) {
|
||||
customerItem.orderList.push({ ...order });
|
||||
}
|
||||
});
|
||||
|
||||
// 转换为数组返回
|
||||
return Array.from(customerMap.values());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 分页大小改变,通知父组件
|
||||
handleSizeChange(val) {
|
||||
this.$emit("size-change", val);
|
||||
},
|
||||
// 当前页改变,通知父组件
|
||||
handleCurrentChange(val) {
|
||||
this.$emit("current-change", val);
|
||||
},
|
||||
// 表格勾选,通知父组件
|
||||
handleSelectionChange(val) {
|
||||
this.$emit("selection-change", val);
|
||||
},
|
||||
// 导出订单,通知父组件
|
||||
handleExport() {
|
||||
this.$emit("export-order");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-detail-container {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
padding: 15px 20px;
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.order-tabs {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
/* 无数据提示样式 */
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 客户分组折叠面板样式 */
|
||||
.customer-collapse {
|
||||
--el-collapse-item-content-padding: 0;
|
||||
--el-collapse-item-border-width: 1px 0 0 0;
|
||||
--el-collapse-border-color: #ebeef5;
|
||||
}
|
||||
|
||||
/* 折叠面板标题样式 */
|
||||
.collapse-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.collapse-title .company-name {
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.collapse-title .customer-info {
|
||||
color: #606266;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.collapse-title .summary-info {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
/* 客户订单表格样式 */
|
||||
.customer-order-table {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
/* 优化折叠面板和表格样式 */
|
||||
:deep(.el-collapse-item__header) {
|
||||
padding: 12px 20px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #ebeef5 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__empty-block) {
|
||||
min-height: 80px;
|
||||
}
|
||||
</style>
|
||||
95
klp-ui/src/views/crm/report/SalesReportSummaryCard.vue
Normal file
95
klp-ui/src/views/crm/report/SalesReportSummaryCard.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="summary-stat-container" v-loading="loading">
|
||||
<el-card class="summary-card" shadow="hover">
|
||||
<div class="summary-list">
|
||||
<div class="summary-item">
|
||||
<span class="item-label">总订单数</span>
|
||||
<span class="item-value">{{ summaryData.totalOrderCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">总销售金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.totalSalesAmount) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">已完成订单数</span>
|
||||
<span class="item-value">{{ summaryData.completedOrderCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">已完成销售金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.completedSalesAmount) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">未结款总金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.totalUnpaidAmount) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">平均订单金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.avgOrderAmount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SalesReportSummaryCard",
|
||||
props: {
|
||||
// 汇总数据
|
||||
summaryData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 金额格式化方法(从父组件传入,保证一致性)
|
||||
formatAmount: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary-stat-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
width: calc(16.666% - 20px);
|
||||
text-align: center;
|
||||
padding: 15px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
</style>
|
||||
141
klp-ui/src/views/crm/report/SalesmanChart.vue
Normal file
141
klp-ui/src/views/crm/report/SalesmanChart.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<el-card class="echarts-card" shadow="hover">
|
||||
<div slot="header" class="card-header">
|
||||
<span>销售员统计</span>
|
||||
</div>
|
||||
<div ref="salesmanChartRef" class="chart-item"></div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export default {
|
||||
name: "SalesmanChart",
|
||||
props: {
|
||||
// 销售员统计数据
|
||||
salesmanStatList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 图表实例
|
||||
salesmanChart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表
|
||||
this.$nextTick(() => {
|
||||
this.initChart();
|
||||
this.updateChart();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
// 监听数据变化,自动更新图表
|
||||
salesmanStatList: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁图表实例,移除事件监听
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
if (this.salesmanChart) {
|
||||
this.salesmanChart.dispose();
|
||||
this.salesmanChart = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 初始化图表实例
|
||||
initChart() {
|
||||
const dom = this.$refs.salesmanChartRef;
|
||||
if (dom && !this.salesmanChart) {
|
||||
this.salesmanChart = echarts.init(dom);
|
||||
}
|
||||
},
|
||||
// 更新图表配置
|
||||
updateChart() {
|
||||
if (!this.salesmanChart) return;
|
||||
|
||||
const salesmanNames = this.salesmanStatList.map(item => item.salesman);
|
||||
const salesAmounts = this.salesmanStatList.map(item => Number(item.salesAmount || 0));
|
||||
const orderCounts = this.salesmanStatList.map(item => item.orderCount || 0);
|
||||
|
||||
// 空数据配置
|
||||
const emptyOption = {
|
||||
tooltip: { trigger: "axis" },
|
||||
legend: { show: false },
|
||||
xAxis: { type: "category", data: [] },
|
||||
yAxis: { type: "value" },
|
||||
series: [{ data: [], type: "bar" }],
|
||||
graphic: {
|
||||
type: "text",
|
||||
left: "center",
|
||||
top: "center",
|
||||
style: { text: "暂无数据", fontSize: 14, color: "#999" }
|
||||
}
|
||||
};
|
||||
|
||||
// 有数据配置
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: { type: "shadow" },
|
||||
formatter: (params) => {
|
||||
const index = params[0].dataIndex;
|
||||
return `${salesmanNames[index]}<br/>销售金额:${salesAmounts[index].toFixed(2)} 元<br/>订单数量:${orderCounts[index]} 单`;
|
||||
}
|
||||
},
|
||||
legend: { show: false },
|
||||
grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: salesmanNames,
|
||||
axisLabel: { rotate: 30, fontSize: 12 }
|
||||
},
|
||||
yAxis: { type: "value", name: "销售金额(元)" },
|
||||
series: [{
|
||||
name: "销售金额",
|
||||
type: "bar",
|
||||
data: salesAmounts,
|
||||
itemStyle: { color: "#4895ef" }
|
||||
}],
|
||||
graphic: null
|
||||
};
|
||||
|
||||
this.salesmanChart.setOption(this.salesmanStatList.length > 0 ? option : emptyOption, true);
|
||||
},
|
||||
// 窗口自适应
|
||||
handleResize() {
|
||||
if (this.salesmanChart) {
|
||||
this.salesmanChart.resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.echarts-card {
|
||||
height: 400px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="crm-sales-report-page">
|
||||
<!-- 1. 时间筛选区域(单独一行,默认当月第一天到今天) -->
|
||||
<!-- 时间筛选区域(根页面保留,统一管理查询参数) -->
|
||||
<div class="date-filter-container">
|
||||
<el-form :model="dateQuery" inline class="date-form">
|
||||
<el-form-item label="统计时间" prop="dateRange">
|
||||
@@ -22,149 +22,58 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 2. 统计区域(销售汇总关键指标) -->
|
||||
<div class="summary-stat-container" v-loading="summaryLoading">
|
||||
<el-card class="summary-card" shadow="hover">
|
||||
<div class="summary-list">
|
||||
<div class="summary-item">
|
||||
<span class="item-label">总订单数</span>
|
||||
<span class="item-value">{{ summaryData.totalOrderCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">总销售金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.totalSalesAmount) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">已完成订单数</span>
|
||||
<span class="item-value">{{ summaryData.completedOrderCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">已完成销售金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.completedSalesAmount) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">未结款总金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.totalUnpaidAmount) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="item-label">平均订单金额(元)</span>
|
||||
<span class="item-value">{{ formatAmount(summaryData.avgOrderAmount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 1. 销售汇总指标卡组件 -->
|
||||
<SalesReportSummaryCard
|
||||
:summary-data="summaryData"
|
||||
:loading="summaryLoading"
|
||||
:format-amount="formatAmount"
|
||||
/>
|
||||
|
||||
<!-- 3. ECharts图表区域(销售员+客户等级+行业统计) -->
|
||||
<div class="echarts-container" v-loading="chartLoading">
|
||||
<!-- 2. 三个ECharts图表组件(布局由根页面控制) -->
|
||||
<div class="echarts-container">
|
||||
<el-row :gutter="20">
|
||||
<!-- 销售员统计图表 -->
|
||||
<el-col :span="8">
|
||||
<el-card class="echarts-card" shadow="hover">
|
||||
<div slot="header" class="card-header">
|
||||
<span>销售员统计</span>
|
||||
</div>
|
||||
<!-- 确保DOM元素唯一且存在 -->
|
||||
<div ref="salesmanChartRef" class="chart-item"></div>
|
||||
</el-card>
|
||||
<SalesmanChart :salesman-stat-list="salesmanStatList" />
|
||||
</el-col>
|
||||
<!-- 客户等级统计图表 -->
|
||||
<el-col :span="8">
|
||||
<el-card class="echarts-card" shadow="hover">
|
||||
<div slot="header" class="card-header">
|
||||
<span>客户等级统计</span>
|
||||
</div>
|
||||
<div ref="customerLevelChartRef" class="chart-item"></div>
|
||||
</el-card>
|
||||
<CustomerLevelChart
|
||||
:customer-level-stat-list="customerLevelStatList"
|
||||
:customer-level-dict="dict.type.customer_level"
|
||||
/>
|
||||
</el-col>
|
||||
<!-- 行业统计图表 -->
|
||||
<el-col :span="8">
|
||||
<el-card class="echarts-card" shadow="hover">
|
||||
<div slot="header" class="card-header">
|
||||
<span>行业统计</span>
|
||||
</div>
|
||||
<div ref="industryChartRef" class="chart-item"></div>
|
||||
</el-card>
|
||||
<IndustryChart :industry-stat-list="industryStatList" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 4. 订单明细表格区域 -->
|
||||
<div class="order-detail-container" v-loading="orderLoading">
|
||||
<div class="table-toolbar">
|
||||
<el-button
|
||||
type="success"
|
||||
icon="el-icon-download"
|
||||
@click="exportOrderDetails"
|
||||
>
|
||||
导出订单明细
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="orderDetailList"
|
||||
border
|
||||
style="width: 100%;"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column prop="orderCode" label="订单编号" min-width="120"></el-table-column>
|
||||
<el-table-column prop="customerCode" label="客户编码" min-width="100"></el-table-column>
|
||||
<el-table-column prop="companyName" label="公司名称" min-width="150"></el-table-column>
|
||||
<el-table-column prop="contactPerson" label="联系人" min-width="80"></el-table-column>
|
||||
<!-- <el-table-column prop="customerLevel" label="客户等级" min-width="100"></el-table-column> -->
|
||||
<!-- <el-table-column prop="industry" label="所属行业"></el-table-column> -->
|
||||
<el-table-column prop="orderAmount" label="订单金额">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.orderAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="salesman" label="销售员"></el-table-column>
|
||||
<el-table-column prop="deliveryDate" label="交货日期" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.deliveryDate ? formatDate(scope.row.deliveryDate) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderStatus" label="订单状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.orderStatus === 1" type="success">已完成</el-tag>
|
||||
<el-tag v-else-if="scope.row.orderStatus === 0" type="warning">待处理</el-tag>
|
||||
<el-tag v-else type="danger">已取消</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="financeStatus" label="财务状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.financeStatus === 1" type="success">已结款</el-tag>
|
||||
<el-tag v-else type="danger">未结款</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unpaidAmount" label="未结金额">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.unpaidAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="createTime" label="创建时间">
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime ? formatDateTime(scope.row.createTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="itemCount" label="明细数量"></el-table-column>
|
||||
<el-table-column prop="objectionCount" label="异议数量"></el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pageParams.pageNum"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageParams.pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 20px; text-align: right;"
|
||||
></el-pagination>
|
||||
</div>
|
||||
<!-- 3. 订单明细组件 -->
|
||||
<SalesReportOrderDetail
|
||||
:order-detail-list="orderDetailList"
|
||||
:page-params="pageParams"
|
||||
:total="total"
|
||||
:loading="orderLoading"
|
||||
:format-amount="formatAmount"
|
||||
:format-date="formatDate"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@export-order="exportOrderDetails"
|
||||
@selection-change="handleSelectionChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 导入子组件
|
||||
import SalesReportSummaryCard from "./SalesReportSummaryCard.vue";
|
||||
import SalesmanChart from "./SalesmanChart.vue";
|
||||
import CustomerLevelChart from "./CustomerLevelChart.vue";
|
||||
import IndustryChart from "./IndustryChart.vue";
|
||||
import SalesReportOrderDetail from "./SalesReportOrderDetail.vue";
|
||||
|
||||
// 导入API函数
|
||||
import {
|
||||
getSummary,
|
||||
@@ -172,17 +81,22 @@ import {
|
||||
getSalesmanStats,
|
||||
getCustomerLevelStats,
|
||||
getIndustryStats,
|
||||
exportOrderDetails
|
||||
exportOrderDetails as apiExportOrderDetails
|
||||
} from "@/api/crm/report";
|
||||
// 导入ECharts
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export default {
|
||||
name: "CrmSalesReport",
|
||||
dicts: ['customer_level'],
|
||||
components: {
|
||||
SalesReportSummaryCard,
|
||||
SalesmanChart,
|
||||
CustomerLevelChart,
|
||||
IndustryChart,
|
||||
SalesReportOrderDetail
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 时间查询参数(单独抽离,默认当月第一天到今天)
|
||||
// 时间查询参数
|
||||
dateQuery: {
|
||||
dateRange: []
|
||||
},
|
||||
@@ -197,63 +111,30 @@ export default {
|
||||
orderDetailList: [],
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 图表数据
|
||||
// 图表数据(传递给对应图表组件)
|
||||
salesmanStatList: [],
|
||||
customerLevelStatList: [],
|
||||
industryStatList: [],
|
||||
// 加载状态
|
||||
summaryLoading: false,
|
||||
chartLoading: false,
|
||||
orderLoading: false,
|
||||
// 表格勾选数据
|
||||
multipleSelection: [],
|
||||
// ECharts实例
|
||||
salesmanChart: null,
|
||||
customerLevelChart: null,
|
||||
industryChart: null
|
||||
multipleSelection: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 初始化默认时间:当月第一天到今天
|
||||
// 初始化默认时间
|
||||
this.initDefaultDateRange();
|
||||
// 页面加载时查询所有数据
|
||||
// 页面加载查询所有数据
|
||||
this.queryAllData();
|
||||
},
|
||||
mounted() {
|
||||
// 延迟初始化ECharts(确保DOM完全渲染)
|
||||
this.$nextTick(() => {
|
||||
this.initECharts();
|
||||
// 统一绑定resize事件(避免重复绑定)
|
||||
window.addEventListener("resize", this.handleChartResize);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 移除resize事件监听
|
||||
window.removeEventListener("resize", this.handleChartResize);
|
||||
// 销毁ECharts实例,避免内存泄漏
|
||||
if (this.salesmanChart) {
|
||||
this.salesmanChart.dispose();
|
||||
this.salesmanChart = null;
|
||||
}
|
||||
if (this.customerLevelChart) {
|
||||
this.customerLevelChart.dispose();
|
||||
this.customerLevelChart = null;
|
||||
}
|
||||
if (this.industryChart) {
|
||||
this.industryChart.dispose();
|
||||
this.industryChart = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 初始化默认时间范围:当月第一天到今天
|
||||
initDefaultDateRange() {
|
||||
const now = new Date();
|
||||
// 当月第一天
|
||||
const monthFirstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// 格式化日期为yyyy-MM-dd
|
||||
const firstDayStr = this.formatToDateStr(monthFirstDay);
|
||||
const todayStr = this.formatToDateStr(now);
|
||||
// 赋值给日期选择器
|
||||
this.dateQuery.dateRange = [firstDayStr, todayStr];
|
||||
},
|
||||
// 日期对象格式化为yyyy-MM-dd字符串
|
||||
@@ -263,49 +144,43 @@ export default {
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
// 格式化金额(处理BigDecimal空值)
|
||||
// 格式化金额
|
||||
formatAmount(amount) {
|
||||
if (!amount) {
|
||||
return "0.00";
|
||||
}
|
||||
return Number(amount).toFixed(2);
|
||||
},
|
||||
// 导入时间格式化函数(简单优化,可替换为项目真实工具函数)
|
||||
// 格式化日期
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return "-";
|
||||
return timestamp;
|
||||
},
|
||||
formatDateTime(timestamp) {
|
||||
if (!timestamp) return "-";
|
||||
return timestamp;
|
||||
},
|
||||
// 重置日期范围
|
||||
resetDateRange() {
|
||||
this.initDefaultDateRange();
|
||||
this.queryAllData();
|
||||
},
|
||||
// 统一查询所有数据(汇总+图表+订单明细)
|
||||
// 统一查询所有数据
|
||||
queryAllData() {
|
||||
this.loadSummaryData();
|
||||
this.loadChartData();
|
||||
this.loadOrderDetailData();
|
||||
},
|
||||
// 组装公共查询参数(时间+分页<仅订单明细>)
|
||||
// 组装公共查询参数
|
||||
getCommonParams(needPage = false) {
|
||||
const params = {};
|
||||
// 处理时间范围参数
|
||||
if (this.dateQuery.dateRange && this.dateQuery.dateRange.length === 2) {
|
||||
params.startTime = this.dateQuery.dateRange[0];
|
||||
params.endTime = this.dateQuery.dateRange[1];
|
||||
}
|
||||
// 是否需要分页参数
|
||||
if (needPage) {
|
||||
params.pageNum = this.pageParams.pageNum;
|
||||
params.pageSize = this.pageParams.pageSize;
|
||||
}
|
||||
return params;
|
||||
},
|
||||
// 1. 加载销售汇总数据
|
||||
// 加载销售汇总数据
|
||||
loadSummaryData() {
|
||||
this.summaryLoading = true;
|
||||
const params = this.getCommonParams();
|
||||
@@ -319,9 +194,8 @@ export default {
|
||||
this.summaryLoading = false;
|
||||
});
|
||||
},
|
||||
// 2. 加载图表数据并更新ECharts
|
||||
// 加载图表数据
|
||||
loadChartData() {
|
||||
this.chartLoading = true;
|
||||
const params = this.getCommonParams();
|
||||
// 并行加载三个图表数据
|
||||
Promise.all([
|
||||
@@ -339,21 +213,12 @@ export default {
|
||||
if (industryRes.code === 200) {
|
||||
this.industryStatList = industryRes.data || [];
|
||||
}
|
||||
// 数据加载完成后更新图表(确保实例已创建)
|
||||
this.$nextTick(() => {
|
||||
this.updateSalesmanChart();
|
||||
this.updateCustomerLevelChart();
|
||||
this.updateIndustryChart();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.chartLoading = false;
|
||||
});
|
||||
},
|
||||
// 3. 加载订单明细数据
|
||||
// 加载订单明细数据
|
||||
loadOrderDetailData() {
|
||||
this.orderLoading = true;
|
||||
const params = this.getCommonParams(true); // 需要分页参数
|
||||
const params = this.getCommonParams(true);
|
||||
getOrderDetails(params)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
@@ -365,294 +230,6 @@ export default {
|
||||
this.orderLoading = false;
|
||||
});
|
||||
},
|
||||
// 初始化ECharts实例(使用ref获取DOM,更可靠)
|
||||
initECharts() {
|
||||
// 销售员统计图表(判断DOM是否存在)
|
||||
const salesmanDom = this.$refs.salesmanChartRef;
|
||||
const customerLevelDom = this.$refs.customerLevelChartRef;
|
||||
const industryDom = this.$refs.industryChartRef;
|
||||
|
||||
if (salesmanDom) {
|
||||
this.salesmanChart = echarts.init(salesmanDom);
|
||||
}
|
||||
if (customerLevelDom) {
|
||||
this.customerLevelChart = echarts.init(customerLevelDom);
|
||||
}
|
||||
if (industryDom) {
|
||||
this.industryChart = echarts.init(industryDom);
|
||||
}
|
||||
|
||||
// 初始化默认配置(空数据状态)
|
||||
this.initDefaultEChartsOption();
|
||||
},
|
||||
// 初始化ECharts默认配置
|
||||
initDefaultEChartsOption() {
|
||||
const barEmptyOption = {
|
||||
tooltip: {
|
||||
trigger: "axis"
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
type: "value"
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [],
|
||||
type: "bar"
|
||||
}
|
||||
],
|
||||
graphic: {
|
||||
type: "text",
|
||||
left: "center",
|
||||
top: "center",
|
||||
style: {
|
||||
text: "暂无数据",
|
||||
fontSize: 14,
|
||||
color: "#999"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pieEmptyOption = {
|
||||
tooltip: {
|
||||
trigger: "item"
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
left: "left",
|
||||
textStyle: {
|
||||
fontSize: 12 // 正确配置字体大小
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "销售金额",
|
||||
type: "pie",
|
||||
radius: ["40%", "70%"],
|
||||
center: ["50%", "50%"],
|
||||
data: [],
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
],
|
||||
graphic: {
|
||||
type: "text",
|
||||
left: "center",
|
||||
top: "center",
|
||||
style: {
|
||||
text: "暂无数据",
|
||||
fontSize: 14,
|
||||
color: "#999"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 给三个图表设置默认配置
|
||||
if (this.salesmanChart) {
|
||||
this.salesmanChart.setOption(barEmptyOption);
|
||||
}
|
||||
if (this.customerLevelChart) {
|
||||
this.customerLevelChart.setOption(pieEmptyOption);
|
||||
}
|
||||
if (this.industryChart) {
|
||||
this.industryChart.setOption(barEmptyOption);
|
||||
}
|
||||
},
|
||||
// 更新销售员统计图表(柱状图:修复tooltip格式化,移除无效extraData)
|
||||
updateSalesmanChart() {
|
||||
if (!this.salesmanChart || this.salesmanStatList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const salesmanNames = this.salesmanStatList.map(item => item.salesman);
|
||||
const salesAmounts = this.salesmanStatList.map(item => Number(item.salesAmount || 0));
|
||||
// 单独提取订单数量(用于tooltip格式化)
|
||||
const orderCounts = this.salesmanStatList.map(item => item.orderCount || 0);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow"
|
||||
},
|
||||
// 回调函数格式化tooltip,正确获取订单数量
|
||||
formatter: (params) => {
|
||||
const index = params[0].dataIndex;
|
||||
const salesmanName = salesmanNames[index];
|
||||
const salesAmount = salesAmounts[index].toFixed(2);
|
||||
const orderCount = orderCounts[index];
|
||||
return `${salesmanName}<br/>销售金额:${salesAmount} 元<br/>订单数量:${orderCount} 单`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: salesmanNames,
|
||||
axisLabel: {
|
||||
rotate: 30, // 文字旋转,避免重叠
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "销售金额(元)"
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "销售金额",
|
||||
type: "bar",
|
||||
data: salesAmounts,
|
||||
itemStyle: {
|
||||
color: "#4895ef"
|
||||
}
|
||||
}
|
||||
],
|
||||
graphic: null // 有数据时隐藏暂无数据提示
|
||||
};
|
||||
|
||||
this.salesmanChart.setOption(option, true);
|
||||
},
|
||||
// 更新客户等级统计图表(饼图:修复legend配置,正常渲染)
|
||||
updateCustomerLevelChart() {
|
||||
if (!this.customerLevelChart || this.customerLevelStatList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pieData = this.customerLevelStatList.map(item => ({
|
||||
name: this.dict.type.customer_level.find(d => d.value === item.customerLevel)?.label || item.customerLevel,
|
||||
value: Number(item.salesAmount || 0)
|
||||
}));
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: "{b}<br/>销售金额:{c} 元<br/>占比:{d}%"
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
left: "left",
|
||||
textStyle: {
|
||||
fontSize: 12 // 正确配置字体大小
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "销售金额",
|
||||
type: "pie",
|
||||
radius: ["40%", "70%"],
|
||||
center: ["50%", "50%"],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: "center"
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold"
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
],
|
||||
graphic: null // 有数据时隐藏暂无数据提示
|
||||
};
|
||||
|
||||
this.customerLevelChart.setOption(option, true);
|
||||
},
|
||||
// 更新行业统计图表(柱状图:修复tooltip格式化,移除无效extraData)
|
||||
updateIndustryChart() {
|
||||
if (!this.industryChart || this.industryStatList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const industryNames = this.industryStatList.map(item => item.industry);
|
||||
const salesAmounts = this.industryStatList.map(item => Number(item.salesAmount || 0));
|
||||
// 单独提取客户数量(用于tooltip格式化)
|
||||
const customerCounts = this.industryStatList.map(item => item.customerCount || 0);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow"
|
||||
},
|
||||
// 回调函数格式化tooltip,正确获取客户数量
|
||||
formatter: (params) => {
|
||||
const index = params[0].dataIndex;
|
||||
const industryName = industryNames[index];
|
||||
const salesAmount = salesAmounts[index].toFixed(2);
|
||||
const customerCount = customerCounts[index];
|
||||
return `${industryName}<br/>销售金额:${salesAmount} 元<br/>客户数量:${customerCount} 家`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: industryNames,
|
||||
axisLabel: {
|
||||
rotate: 30,
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "销售金额(元)"
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "销售金额",
|
||||
type: "bar",
|
||||
data: salesAmounts,
|
||||
itemStyle: {
|
||||
color: "#f72585"
|
||||
}
|
||||
}
|
||||
],
|
||||
graphic: null // 有数据时隐藏暂无数据提示
|
||||
};
|
||||
|
||||
this.industryChart.setOption(option, true);
|
||||
},
|
||||
// 统一处理图表自适应(避免重复绑定resize事件)
|
||||
handleChartResize() {
|
||||
if (this.salesmanChart) {
|
||||
this.salesmanChart.resize();
|
||||
}
|
||||
if (this.customerLevelChart) {
|
||||
this.customerLevelChart.resize();
|
||||
}
|
||||
if (this.industryChart) {
|
||||
this.industryChart.resize();
|
||||
}
|
||||
},
|
||||
// 分页大小改变
|
||||
handleSizeChange(val) {
|
||||
this.pageParams.pageSize = val;
|
||||
@@ -670,7 +247,7 @@ export default {
|
||||
// 导出订单明细
|
||||
exportOrderDetails() {
|
||||
const params = this.getCommonParams();
|
||||
exportOrderDetails(params).then((res) => {
|
||||
apiExportOrderDetails(params).then((res) => {
|
||||
this.handleExportBlob(res, "销售报表订单明细.xlsx");
|
||||
});
|
||||
},
|
||||
@@ -712,79 +289,8 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 统计区域样式 */
|
||||
.summary-stat-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
width: calc(16.666% - 20px);
|
||||
text-align: center;
|
||||
padding: 15px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
/* ECharts图表区域样式:确保容器高度足够且可见 */
|
||||
/* 图表容器布局样式 */
|
||||
.echarts-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.echarts-card {
|
||||
height: 400px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px); /* 减去卡片头部高度,确保高度足够 */
|
||||
min-height: 300px; /* 最小高度兜底 */
|
||||
}
|
||||
|
||||
/* 订单明细区域样式 */
|
||||
.order-detail-container {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
padding: 15px 20px;
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user