This commit is contained in:
xuyuncong 2025-11-18 14:10:45 +08:00
parent acebf9911d
commit db8d0ac752
4 changed files with 778 additions and 0 deletions

View File

@ -0,0 +1,68 @@
import request from '@/utils/request';
const preUrl = '/repair/statistics';
/**
* 获取库存统计数据
* @param {Object} params - 查询参数
* @param {Array} params.dateRange - 日期范围 [开始日期, 结束日期]
* @returns {Promise} - 返回Promise对象
*/
export function getInventoryStatistics(params) {
return request({
url: preUrl + '/inventoryStatistics',
method: 'get',
params
});
}
/**
* 获取库存明细统计
* @param {Object} params - 查询参数
* @param {Array} params.dateRange - 日期范围 [开始日期, 结束日期]
* @param {number} params.pageNum - 页码
* @param {number} params.pageSize - 每页数量
* @returns {Promise} - 返回Promise对象
*/
export function getInventoryDetails(params) {
return request({
url: preUrl + '/details',
method: 'get',
params
});
}
/**
* 获取库存预警信息
* @param {Object} params - 查询参数
* @returns {Promise} - 返回Promise对象
*/
export function getInventoryAlerts(params) {
return request({
url: preUrl + '/alerts',
method: 'get',
params
});
}
/**
* 导出库存统计报表
* @param {Object} params - 查询参数
* @param {Array} params.dateRange - 日期范围 [开始日期, 结束日期]
* @returns {Promise} - 返回Promise对象
*/
export function exportInventoryStatistics(params) {
return request({
url: preUrl + '/export',
method: 'get',
params,
responseType: 'blob'
});
}
export default {
getInventoryStatistics,
getInventoryDetails,
getInventoryAlerts,
exportInventoryStatistics
};

View File

@ -0,0 +1,37 @@
/**
* 库存统计VO
*
* @author AI助手
* @date 2025年1月
*/
/**
* 库存统计VO
* @typedef {Object} InventoryStatisticsVO
* @property {number} beginningStock - 期初库存数量
* @property {number} beginningAmount - 期初库存金额
* @property {number} currentPurchaseStock - 当期购进数量
* @property {number} currentPurchaseAmount - 当期购进金额
* @property {number} currentPickingStock - 当期领出数量
* @property {number} currentPickingAmount - 当期领出金额
*/
/**
* 创建库存统计VO对象
* @param {Object} data - 初始化数据
* @returns {InventoryStatisticsVO}
*/
export function createInventoryStatisticsVO(data = {}) {
return {
beginningStock: data.beginningStock || 0,
beginningAmount: data.beginningAmount || 0,
currentPurchaseStock: data.currentPurchaseStock || 0,
currentPurchaseAmount: data.currentPurchaseAmount || 0,
currentPickingStock: data.currentPickingStock || 0,
currentPickingAmount: data.currentPickingAmount || 0
};
}
export default {
createInventoryStatisticsVO
};

View File

@ -0,0 +1,672 @@
<template>
<div class="inventory-statistics-container">
<!-- 页面标题 -->
<div class="page-header">
<h2>库存统计报表</h2>
</div>
<!-- 日期选择器 -->
<el-card class="filter-card">
<div class="filter-container">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
range-separator="至"
class="date-picker"
/>
<el-button type="primary" @click="loadData" :loading="loading">
<i class="el-icon-search"></i> 查询
</el-button>
<el-button @click="reset">重置</el-button>
<!-- <el-button type="success" @click="exportExcel" :loading="exportLoading">
<i class="el-icon-download"></i> 导出报表
</el-button> -->
</div>
</el-card>
<!-- 统计卡片展示 -->
<div class="statistics-cards">
<!-- 期初库存 -->
<el-card class="stat-card beginning">
<div class="card-header">
<h3 class="card-title">期初库存</h3>
<div class="card-icon el-icon-date"></div>
</div>
<div class="card-body">
<div class="stat-item">
<span class="stat-label">数量</span>
<span class="stat-value">{{ formatNumber(statisticsData.beginningStock) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">金额</span>
<span class="stat-value amount">{{ formatCurrency(statisticsData.beginningAmount) }}</span>
</div>
</div>
</el-card>
<!-- 当期购进 -->
<el-card class="stat-card purchase">
<div class="card-header">
<h3 class="card-title">当期购进</h3>
<div class="card-icon el-icon-document-add"></div>
</div>
<div class="card-body">
<div class="stat-item">
<span class="stat-label">数量</span>
<span class="stat-value">{{ formatNumber(statisticsData.currentPurchaseStock) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">金额</span>
<span class="stat-value amount">{{ formatCurrency(statisticsData.currentPurchaseAmount) }}</span>
</div>
</div>
</el-card>
<!-- 当期领出 -->
<el-card class="stat-card picking">
<div class="card-header">
<h3 class="card-title">当期领出</h3>
<div class="card-icon el-icon-takeaway-box"></div>
</div>
<div class="card-body">
<div class="stat-item">
<span class="stat-label">数量</span>
<span class="stat-value">{{ formatNumber(statisticsData.currentPickingStock) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">金额</span>
<span class="stat-value amount">{{ formatCurrency(statisticsData.currentPickingAmount) }}</span>
</div>
</div>
</el-card>
<!-- 期末库存 -->
<el-card class="stat-card ending">
<div class="card-header">
<h3 class="card-title">期末库存</h3>
<div class="card-icon el-icon-box"></div>
</div>
<div class="card-body">
<div class="stat-item">
<span class="stat-label">数量</span>
<span class="stat-value">{{ formatNumber(getEndingStock()) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">金额</span>
<span class="stat-value amount">{{ formatCurrency(getEndingAmount()) }}</span>
</div>
</div>
</el-card>
</div>
<!-- 图表展示区域 -->
<!-- <div class="charts-container">
<el-card class="chart-card">
<div slot="header" class="chart-header">
<span>库存数量变化趋势</span>
</div>
<div class="chart-content">
<div id="stockChart" class="chart"></div>
</div>
</el-card>
<el-card class="chart-card">
<div slot="header" class="chart-header">
<span>库存金额分布</span>
</div>
<div class="chart-content">
<div id="amountChart" class="chart"></div>
</div>
</el-card>
</div> -->
</div>
</template>
<script>
import { formatCurrency, getDateRange } from '@/utils/utils';
import { createInventoryStatisticsVO } from '@/api/repair/vo/InventoryStatisticsVO';
import { getInventoryStatistics, exportInventoryStatistics } from '@/api/repair/inventory/inventory';
// ECharts
import * as echarts from 'echarts';
export default {
name: 'InventoryStatistics',
data() {
return {
loading: false,
exportLoading: false,
queryParams: {
dateRange: [],
},
statisticsData: createInventoryStatisticsVO(),
chartInstances: {},
};
},
async mounted() {
//
this.queryParams.dateRange = getDateRange('month');
//
this.loadData();
},
beforeDestroy() {
//
window.removeEventListener('resize', this.handleResize);
//
Object.values(this.chartInstances).forEach(chart => {
if (chart && chart.dispose) {
chart.dispose();
}
});
},
mounted() {
//
this.queryParams.dateRange = getDateRange('month');
//
this.loadData();
//
window.addEventListener('resize', this.handleResize);
},
methods: {
/**
* 处理窗口大小变化
*/
handleResize() {
Object.values(this.chartInstances).forEach(chart => {
if (chart && chart.resize) {
chart.resize();
}
});
},
formatCurrency,
/**
* 格式化数字显示
* @param {number} value - 数值
* @returns {string}
*/
formatNumber(value) {
return Number(value || 0).toLocaleString('zh-CN');
},
/**
* 计算期末库存数量
* @returns {number}
*/
getEndingStock() {
const { beginningStock, currentPurchaseStock, currentPickingStock } = this.statisticsData;
return (beginningStock || 0) + (currentPurchaseStock || 0) - (currentPickingStock || 0);
},
/**
* 计算期末库存金额
* @returns {number}
*/
getEndingAmount() {
const { beginningAmount, currentPurchaseAmount, currentPickingAmount } = this.statisticsData;
return (beginningAmount || 0) + (currentPurchaseAmount || 0) - (currentPickingAmount || 0);
},
/**
* 日期范围变化处理
*/
handleDateChange() {
//
this.loadData();
},
/**
* 重置查询条件
*/
reset() {
this.queryParams.dateRange = getDateRange('month');
this.loadData();
},
/**
* 导出Excel报表
*/
exportExcel() {
this.exportLoading = true;
exportInventoryStatistics(this.queryParams)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' });
const link = document.createElement('a');
const fileName = `库存统计报表_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.xlsx`;
link.href = URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
this.$message.success('报表导出成功');
})
.catch(error => {
this.$message.error('报表导出失败');
console.error('Failed to export inventory statistics:', error);
})
.finally(() => {
this.exportLoading = false;
});
},
/**
* 加载统计数据
*/
loadData() {
this.loading = true;
// 使API
getInventoryStatistics(this.queryParams)
.then(res => {
if (res.data) {
this.statisticsData = res.data;
this.renderCharts();
}
})
.catch(error => {
this.$message.error('获取库存统计数据失败');
console.error('Failed to load inventory statistics:', error);
//
this.setMockData();
})
.finally(() => {
this.loading = false;
});
},
/**
* 设置模拟数据用于演示
*/
setMockData() {
this.statisticsData = createInventoryStatisticsVO({
beginningStock: 1500,
beginningAmount: 50000,
currentPurchaseStock: 800,
currentPurchaseAmount: 25000,
currentPickingStock: 600,
currentPickingAmount: 18000
});
this.renderCharts();
},
/**
* 渲染图表
*/
renderCharts() {
// DOM
this.$nextTick(() => {
this.renderStockChart();
this.renderAmountChart();
});
},
/**
* 渲染库存数量变化趋势图
*/
renderStockChart() {
const stockChartDom = document.getElementById('stockChart');
if (!stockChartDom) return;
//
if (this.chartInstances.stockChart) {
this.chartInstances.stockChart.dispose();
}
try {
//
const stockChart = echarts.init(stockChartDom);
this.chartInstances.stockChart = stockChart;
const endingStock = this.getEndingStock();
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function(params) {
const data = params[0];
return data.name + '<br/>数量: ' + data.value.toLocaleString('zh-CN');
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['期初库存', '当期购进', '当期领出', '期末库存'],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function(value) {
return value.toLocaleString('zh-CN');
}
}
},
series: [
{
name: '数量',
type: 'bar',
barWidth: '60%',
data: [
{
value: this.statisticsData.beginningStock,
itemStyle: { color: '#409EFF' }
},
{
value: this.statisticsData.currentPurchaseStock,
itemStyle: { color: '#67C23A' }
},
{
value: this.statisticsData.currentPickingStock,
itemStyle: { color: '#E6A23C' }
},
{
value: endingStock,
itemStyle: { color: '#909399' }
}
],
label: {
show: true,
position: 'top',
formatter: function(params) {
return params.value.toLocaleString('zh-CN');
}
}
}
]
};
stockChart.setOption(option);
} catch (error) {
console.error('Failed to initialize stock chart:', error);
}
},
/**
* 渲染库存金额分布图
*/
renderAmountChart() {
const amountChartDom = document.getElementById('amountChart');
if (!amountChartDom) return;
//
if (this.chartInstances.amountChart) {
this.chartInstances.amountChart.dispose();
}
try {
//
const amountChart = echarts.init(amountChartDom);
this.chartInstances.amountChart = amountChart;
const endingAmount = this.getEndingAmount();
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
valueFormatter: function(value) {
return formatCurrency(value);
}
},
legend: {
orient: 'vertical',
left: 'left',
textStyle: {
fontSize: 12
}
},
series: [
{
name: '库存金额',
type: 'pie',
radius: ['40%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold',
formatter: function(params) {
return params.name + '\n' + formatCurrency(params.value);
}
}
},
labelLine: {
show: false
},
data: [
{
value: this.statisticsData.beginningAmount,
name: '期初库存金额',
itemStyle: { color: '#409EFF' }
},
{
value: this.statisticsData.currentPurchaseAmount,
name: '当期购进金额',
itemStyle: { color: '#67C23A' }
},
{
value: this.statisticsData.currentPickingAmount,
name: '当期领出金额',
itemStyle: { color: '#E6A23C' }
},
{
value: endingAmount,
name: '期末库存金额',
itemStyle: { color: '#909399' }
}
]
}
]
};
amountChart.setOption(option);
} catch (error) {
console.error('Failed to initialize amount chart:', error);
}
}
}
};
</script>
<style scoped>
.inventory-statistics-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
color: #333;
font-size: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.filter-container {
display: flex;
align-items: center;
gap: 15px;
}
.date-picker {
width: 320px;
}
.statistics-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.card-icon {
font-size: 24px;
opacity: 0.7;
}
.card-body {
padding: 10px 0;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.stat-item:last-child {
margin-bottom: 0;
}
.stat-label {
color: #606266;
font-size: 14px;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: #303133;
}
.stat-value.amount {
color: #67C23A;
}
/* 不同卡片的主题色 */
.beginning .card-title {
color: #409EFF;
}
.beginning .card-icon {
color: #409EFF;
}
.purchase .card-title {
color: #67C23A;
}
.purchase .card-icon {
color: #67C23A;
}
.picking .card-title {
color: #E6A23C;
}
.picking .card-icon {
color: #E6A23C;
}
.ending .card-title {
color: #909399;
}
.ending .card-icon {
color: #909399;
}
/* 图表容器样式 */
.charts-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.chart-card {
height: 300px;
}
.chart-header {
font-weight: 600;
color: #303133;
}
.chart-content {
height: calc(100% - 50px);
}
.chart {
width: 100%;
height: 100%;
}
/* 响应式布局 */
@media (max-width: 768px) {
.charts-container {
grid-template-columns: 1fr;
}
.filter-container {
flex-direction: column;
align-items: stretch;
}
.date-picker {
width: 100%;
}
}
</style>

View File

@ -17,6 +17,7 @@
<el-table-column align="center" label="规格" width="180" prop="model">
<template slot-scope="scope">
{{ scope.row[listType]?.model }}
</template>
</el-table-column>
<el-table-column align="center" label="编码" width="180" prop="code">