feat(wms): 新增6个报表页面以及报表总汇

1.退库原因分析报表returnReasonAnalysis
统计各类退库(生产退库、销售退库等)的数量、占比及主要退库原因(如质量问题、订单变更),为改进生产、销售和质量管理提供数据支持。
2.库存变动趋势分析表inventoryTrendAnalysis
内容:以折线图/柱状图展示某物料或类别在一段时间内的库存数量变化(如近3个月每周结存),标注关键节点(如大额入库/出库)。
作用:识别库存波动规律(如季节性增减),预测未来库存需求,避免积压或短缺。
3.安全库存预警表safetyStockAlert
内容:对比物料当前库存与设定的“安全库存值”,列出“低于安全库存”(短缺预警)或“高于最高库存”(积压预警)的物料及差异量。
4.呆滞料库存报表stagnantInventory
内容:定义“呆滞标准”(如6个月未出库),统计符合标准的物料及数量、金额,标注呆滞原因(如订单取消/设计变更)。
作用:推动呆滞料处理(如折价处理、返工利用),减少资金占用和仓储成本。
5.库存差异报表inventoryDifference
内容:记录盘点后实际数量与系统账面数量的差异(差异量、差异率),标注差异物料及可能原因(如漏记、丢失、计数错误)。
作用:跟踪差异处理进度(如调账、追责),改进仓库操作规范(如加强入库扫码校验)。 
6定期生成库存周转报表inventoryTurnover
库存周转率=(销售数量/库存数量)x100%例如6月销售31台,期末库存65台库存周转率=(31/65)*100%=47.69%
库存周转率=(该期间的出库总金额/该期间的平均库存金额)x100%=该期间出库总金额x2/(期初库存金额+期末库存金额)x100%库存周转率=312*2/(490+589)=56.9%
master
zangch@mesnac.com 4 months ago
parent 0285999cf3
commit 6a109dc6b6

@ -0,0 +1,97 @@
export interface ReportQuery {
tenantId?: string;
materialCategoryId?: number;
}
export interface ReturnReasonAnalysisVO {
tenantId?: string;
returnReasonCategory?: string;
returnOrderCount?: number;
totalReturnAmount?: number;
orderCountRatio?: number;
amountRatio?: number;
materialName?: string;
materialCategoryName?: string;
materialCode?: string;
}
export interface InventoryTrendAnalysisVO {
tenantId?: string;
materialId?: number;
materialCode?: string;
materialName?: string;
materialCategoryName?: string;
statisticsMonth?: string;
statisticsWeek?: number;
currentInventoryQty?: number;
weekInstockQty?: number;
weekOutstockQty?: number;
lastWeekInventoryQty?: number;
keyNodeMark?: string;
}
export interface SafetyStockAlertVO {
tenantId?: string;
materialId?: number;
materialCode?: string;
materialName?: string;
materialCategoryName?: string;
currentInventoryQty?: number;
safeStockAmount?: number;
minStockAmount?: number;
maxStockAmount?: number;
alertStatus?: string;
differenceAmount?: number;
lastUpdateTime?: string;
}
export interface StagnantInventoryVO {
tenantId?: string;
materialId?: number;
materialCode?: string;
materialName?: string;
materialCategoryName?: string;
stagnantInventoryQty?: number;
materialUnit?: string;
lastOutstockTime?: string;
stagnantDays?: number;
stagnantReason?: string;
materialSpec?: string;
warehouseName?: string;
lastActivityTime?: string;
}
export interface InventoryDifferenceVO {
tenantId?: string;
checkCode?: string;
checkType?: string;
warehouseId?: number;
warehouseName?: string;
materialId?: number;
materialCode?: string;
materialName?: string;
materialCategoryName?: string;
bookInventoryQty?: number;
actualInventoryQty?: number;
differenceQty?: number;
differenceType?: string;
differenceRate?: number;
differenceLevel?: string;
checkTime?: string;
checkBy?: string;
}
export interface InventoryTurnoverVO {
tenantId?: string;
materialId?: number;
materialCode?: string;
materialName?: string;
materialCategoryName?: string;
statisticsMonth?: string;
beginInventoryQty?: number;
endInventoryQty?: number;
monthOutstockQty?: number;
inventoryTurnoverRate?: number;
simpleTurnoverRate?: number;
turnoverEvaluation?: string;
}

@ -0,0 +1,387 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物料大类" prop="materialCategoryId">
<el-select v-model="queryParams.materialCategoryId" placeholder="请选择物料大类" clearable>
<el-option v-for="item in materialCategoryOptions"
:key="item.materialCategoryId"
:label="item.materialCategoryName"
:value="item.materialCategoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-green-500">{{ differenceStats.noDifference }}</div>
<div class="stat-label">无差异</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-blue-500">{{ differenceStats.profit }}</div>
<div class="stat-label">盘盈</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-red-500">{{ differenceStats.loss }}</div>
<div class="stat-label">盘亏</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-orange-500">{{ differenceStats.avgRate.toFixed(2) }}%</div>
<div class="stat-label">平均差异率</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表展示区域 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>差异类型分布</span>
</template>
<div ref="pieChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>差异等级分布</span>
</template>
<div ref="barChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="盘点单号" align="center" prop="checkCode" />
<el-table-column label="盘点类型" align="center" prop="checkType" />
<el-table-column label="仓库名称" align="center" prop="warehouseName" />
<el-table-column label="物料编码" align="center" prop="materialCode" />
<el-table-column label="物料名称" align="center" prop="materialName" show-overflow-tooltip />
<el-table-column label="物料大类" align="center" prop="materialCategoryName" />
<el-table-column label="账面数量" align="center" prop="bookInventoryQty" />
<el-table-column label="实际盘点数量" align="center" prop="actualInventoryQty" />
<el-table-column label="差异量" align="center" prop="differenceQty" />
<el-table-column label="差异类型" align="center" prop="differenceType">
<template #default="scope">
<el-tag :type="getDifferenceTypeTag(scope.row.differenceType)">
{{ scope.row.differenceType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="差异率(%)" align="center" prop="differenceRate" />
<el-table-column label="差异等级" align="center" prop="differenceLevel">
<template #default="scope">
<el-tag :type="getDifferenceLevelTag(scope.row.differenceLevel)">
{{ scope.row.differenceLevel }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="盘点时间" align="center" prop="checkTime" width="180" />
<el-table-column label="盘点人员" align="center" prop="checkBy" />
</el-table>
</el-card>
</div>
</template>
<script setup name="InventoryDifference" lang="ts">
import { getInventoryDifference, exportInventoryDifference } from '@/api/wms/report';
import { listBaseMaterialCategoryInWMS } from '@/api/wms/baseMaterialCategory';
import { InventoryDifferenceVO, ReportQuery } from '@/api/wms/report/types';
import { BaseMaterialCategoryVO } from '@/api/wms/baseMaterialCategory/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<InventoryDifferenceVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const materialCategoryOptions = ref<BaseMaterialCategoryVO[]>([]);
//
const differenceStats = ref({
noDifference: 0,
profit: 0,
loss: 0,
avgRate: 0
});
//
const pieChartRef = ref<HTMLDivElement>();
const barChartRef = ref<HTMLDivElement>();
let pieChart: echarts.ECharts;
let barChart: echarts.ECharts;
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<ReportQuery>({
tenantId: undefined,
materialCategoryId: undefined
});
/** 查询库存差异报表列表 */
const getList = async () => {
loading.value = true;
const res = await getInventoryDifference(queryParams.value);
reportList.value = res.data;
loading.value = false;
//
calculateStats();
//
nextTick(() => {
updateCharts();
});
};
/** 计算统计数据 */
const calculateStats = () => {
differenceStats.value = {
noDifference: reportList.value.filter(item => item.differenceType === '无差异').length,
profit: reportList.value.filter(item => item.differenceType === '盘盈').length,
loss: reportList.value.filter(item => item.differenceType === '盘亏').length,
avgRate: reportList.value.length > 0
? reportList.value.reduce((sum, item) => sum + (item.differenceRate || 0), 0) / reportList.value.length
: 0
};
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: InventoryDifferenceVO[]) => {
ids.value = selection.map(item => item.materialId!);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/wms/report/inventoryDifference/export', {
...queryParams.value
}, `库存差异报表_${new Date().getTime()}.xlsx`);
};
/** 获取物料大类选项 */
const getMaterialCategoryOptions = async () => {
const res = await listBaseMaterialCategoryInWMS({});
materialCategoryOptions.value = res.data.rows || [];
};
/** 获取差异类型标签 */
const getDifferenceTypeTag = (type: string) => {
switch (type) {
case '盘盈': return 'success';
case '盘亏': return 'danger';
case '无差异': return 'info';
default: return '';
}
};
/** 获取差异等级标签 */
const getDifferenceLevelTag = (level: string) => {
switch (level) {
case '重大差异': return 'danger';
case '一般差异': return 'warning';
case '轻微差异': return 'info';
case '无差异': return 'success';
default: return '';
}
};
/** 初始化图表 */
const initCharts = () => {
if (pieChartRef.value) {
pieChart = echarts.init(pieChartRef.value);
}
if (barChartRef.value) {
barChart = echarts.init(barChartRef.value);
}
};
/** 更新图表数据 */
const updateCharts = () => {
if (!reportList.value.length) return;
//
const typeCount = reportList.value.reduce((acc, item) => {
const type = item.differenceType || '未知';
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const pieData = Object.keys(typeCount).map(type => ({
name: type,
value: typeCount[type]
}));
const pieOption = {
title: {
text: '差异类型分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '差异类型',
type: 'pie',
radius: '50%',
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
//
const levelCount = reportList.value.reduce((acc, item) => {
const level = item.differenceLevel || '未知';
acc[level] = (acc[level] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const barCategories = Object.keys(levelCount);
const barData = Object.values(levelCount);
const barOption = {
title: {
text: '差异等级分布',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: barCategories
},
yAxis: {
type: 'value',
name: '物料数量'
},
series: [
{
name: '物料数量',
type: 'bar',
data: barData,
itemStyle: {
color: function(params: any) {
const colors = ['#67C23A', '#E6A23C', '#F56C6C', '#909399'];
return colors[params.dataIndex % colors.length];
}
}
}
]
};
pieChart?.setOption(pieOption);
barChart?.setOption(barOption);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
pieChart?.resize();
barChart?.resize();
};
onMounted(() => {
getMaterialCategoryOptions();
getList();
initCharts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
pieChart?.dispose();
barChart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-item {
padding: 20px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
</style>

@ -0,0 +1,246 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物料大类" prop="materialCategoryId">
<el-select v-model="queryParams.materialCategoryId" placeholder="请选择物料大类" clearable>
<el-option v-for="item in materialCategoryOptions"
:key="item.materialCategoryId"
:label="item.materialCategoryName"
:value="item.materialCategoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 图表展示区域 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span>库存变动趋势图</span>
</template>
<div ref="lineChartRef" style="height: 400px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="物料编码" align="center" prop="materialCode" />
<el-table-column label="物料名称" align="center" prop="materialName" show-overflow-tooltip />
<el-table-column label="物料大类" align="center" prop="materialCategoryName" />
<el-table-column label="统计月份" align="center" prop="statisticsMonth" />
<el-table-column label="统计周" align="center" prop="statisticsWeek" />
<el-table-column label="当前库存数量" align="center" prop="currentInventoryQty" />
<el-table-column label="本周入库数量" align="center" prop="weekInstockQty" />
<el-table-column label="本周出库数量" align="center" prop="weekOutstockQty" />
<el-table-column label="上周结存数量" align="center" prop="lastWeekInventoryQty" />
<el-table-column label="关键节点标注" align="center" prop="keyNodeMark">
<template #default="scope">
<el-tag :type="getNodeMarkType(scope.row.keyNodeMark)">
{{ scope.row.keyNodeMark }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup name="InventoryTrendAnalysis" lang="ts">
import { getInventoryTrendAnalysis, exportInventoryTrendAnalysis } from '@/api/wms/report';
import { listBaseMaterialCategoryInWMS } from '@/api/wms/baseMaterialCategory';
import { InventoryTrendAnalysisVO, ReportQuery } from '@/api/wms/report/types';
import { BaseMaterialCategoryVO } from '@/api/wms/baseMaterialCategory/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<InventoryTrendAnalysisVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const materialCategoryOptions = ref<BaseMaterialCategoryVO[]>([]);
//
const lineChartRef = ref<HTMLDivElement>();
let lineChart: echarts.ECharts;
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<ReportQuery>({
tenantId: undefined,
materialCategoryId: undefined
});
/** 查询库存变动趋势分析报表列表 */
const getList = async () => {
loading.value = true;
const res = await getInventoryTrendAnalysis(queryParams.value);
reportList.value = res.data;
loading.value = false;
//
nextTick(() => {
updateChart();
});
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: InventoryTrendAnalysisVO[]) => {
ids.value = selection.map(item => item.materialId!);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/wms/report/inventoryTrendAnalysis/export', {
...queryParams.value
}, `库存变动趋势分析报表_${new Date().getTime()}.xlsx`);
};
/** 获取物料大类选项 */
const getMaterialCategoryOptions = async () => {
const res = await listBaseMaterialCategoryInWMS({});
materialCategoryOptions.value = res.data.rows || [];
};
/** 获取关键节点标注类型 */
const getNodeMarkType = (mark: string) => {
switch (mark) {
case '大额入库': return 'success';
case '大额出库': return 'danger';
default: return 'info';
}
};
/** 初始化图表 */
const initChart = () => {
if (lineChartRef.value) {
lineChart = echarts.init(lineChartRef.value);
}
};
/** 更新图表数据 */
const updateChart = () => {
if (!reportList.value.length || !lineChart) return;
//
const materialGroups = reportList.value.reduce((groups, item) => {
const key = item.materialName || '';
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {} as Record<string, InventoryTrendAnalysisVO[]>);
const series: any[] = [];
const xAxisData = [...new Set(reportList.value.map(item => `${item.statisticsWeek}`))];
Object.keys(materialGroups).slice(0, 5).forEach((materialName, index) => {
const data = xAxisData.map(week => {
const weekNum = parseInt(week.replace('第', '').replace('周', ''));
const item = materialGroups[materialName].find(d => d.statisticsWeek === weekNum);
return item?.currentInventoryQty || 0;
});
series.push({
name: materialName,
type: 'line',
data: data,
smooth: true
});
});
const option = {
title: {
text: '库存变动趋势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: Object.keys(materialGroups).slice(0, 5),
top: 30
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData
},
yAxis: {
type: 'value',
name: '库存数量'
},
series: series
};
lineChart.setOption(option);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
lineChart?.resize();
};
onMounted(() => {
getMaterialCategoryOptions();
getList();
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
lineChart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 20px;
}
</style>

@ -0,0 +1,368 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物料大类" prop="materialCategoryId">
<el-select v-model="queryParams.materialCategoryId" placeholder="请选择物料大类" clearable>
<el-option v-for="item in materialCategoryOptions"
:key="item.materialCategoryId"
:label="item.materialCategoryName"
:value="item.materialCategoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-green-500">{{ turnoverStats.fast }}</div>
<div class="stat-label">快速周转</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-blue-500">{{ turnoverStats.normal }}</div>
<div class="stat-label">正常周转</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-orange-500">{{ turnoverStats.slow }}</div>
<div class="stat-label">缓慢周转</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-gray-500">{{ turnoverStats.noFlow }}</div>
<div class="stat-label">无流动</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表展示区域 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>周转评价分布</span>
</template>
<div ref="pieChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>库存周转率排行</span>
</template>
<div ref="barChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="物料编码" align="center" prop="materialCode" />
<el-table-column label="物料名称" align="center" prop="materialName" show-overflow-tooltip />
<el-table-column label="物料大类" align="center" prop="materialCategoryName" />
<el-table-column label="统计月份" align="center" prop="statisticsMonth" />
<el-table-column label="月初库存数量" align="center" prop="beginInventoryQty" />
<el-table-column label="月末库存数量" align="center" prop="endInventoryQty" />
<el-table-column label="月出库数量" align="center" prop="monthOutstockQty" />
<el-table-column label="库存周转率(%)" align="center" prop="inventoryTurnoverRate" />
<el-table-column label="简化周转率(%)" align="center" prop="simpleTurnoverRate" />
<el-table-column label="周转评价" align="center" prop="turnoverEvaluation">
<template #default="scope">
<el-tag :type="getTurnoverEvaluationType(scope.row.turnoverEvaluation)">
{{ scope.row.turnoverEvaluation }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup name="InventoryTurnover" lang="ts">
import { getInventoryTurnover, exportInventoryTurnover } from '@/api/wms/report';
import { listBaseMaterialCategoryInWMS } from '@/api/wms/baseMaterialCategory';
import { InventoryTurnoverVO, ReportQuery } from '@/api/wms/report/types';
import { BaseMaterialCategoryVO } from '@/api/wms/baseMaterialCategory/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<InventoryTurnoverVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const materialCategoryOptions = ref<BaseMaterialCategoryVO[]>([]);
//
const turnoverStats = ref({
fast: 0,
normal: 0,
slow: 0,
noFlow: 0
});
//
const pieChartRef = ref<HTMLDivElement>();
const barChartRef = ref<HTMLDivElement>();
let pieChart: echarts.ECharts;
let barChart: echarts.ECharts;
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<ReportQuery>({
tenantId: undefined,
materialCategoryId: undefined
});
/** 查询库存周转报表列表 */
const getList = async () => {
loading.value = true;
const res = await getInventoryTurnover(queryParams.value);
reportList.value = res.data;
loading.value = false;
//
calculateStats();
//
nextTick(() => {
updateCharts();
});
};
/** 计算统计数据 */
const calculateStats = () => {
turnoverStats.value = {
fast: reportList.value.filter(item => item.turnoverEvaluation === '快速周转').length,
normal: reportList.value.filter(item => item.turnoverEvaluation === '正常周转').length,
slow: reportList.value.filter(item => item.turnoverEvaluation === '缓慢周转').length,
noFlow: reportList.value.filter(item => item.turnoverEvaluation === '无流动').length
};
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: InventoryTurnoverVO[]) => {
ids.value = selection.map(item => item.materialId!);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/wms/report/inventoryTurnover/export', {
...queryParams.value
}, `库存周转报表_${new Date().getTime()}.xlsx`);
};
/** 获取物料大类选项 */
const getMaterialCategoryOptions = async () => {
const res = await listBaseMaterialCategoryInWMS({});
materialCategoryOptions.value = res.data.rows || [];
};
/** 获取周转评价类型 */
const getTurnoverEvaluationType = (evaluation: string) => {
switch (evaluation) {
case '快速周转': return 'success';
case '正常周转': return 'primary';
case '缓慢周转': return 'warning';
case '无流动': return 'danger';
default: return 'info';
}
};
/** 初始化图表 */
const initCharts = () => {
if (pieChartRef.value) {
pieChart = echarts.init(pieChartRef.value);
}
if (barChartRef.value) {
barChart = echarts.init(barChartRef.value);
}
};
/** 更新图表数据 */
const updateCharts = () => {
if (!reportList.value.length) return;
//
const evaluationCount = reportList.value.reduce((acc, item) => {
const evaluation = item.turnoverEvaluation || '未知';
acc[evaluation] = (acc[evaluation] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const pieData = Object.keys(evaluationCount).map(evaluation => ({
name: evaluation,
value: evaluationCount[evaluation]
}));
const pieOption = {
title: {
text: '周转评价分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '周转评价',
type: 'pie',
radius: '50%',
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
// 10
const sortedData = [...reportList.value]
.sort((a, b) => (b.inventoryTurnoverRate || 0) - (a.inventoryTurnoverRate || 0))
.slice(0, 10);
const barCategories = sortedData.map(item => item.materialCode);
const barData = sortedData.map(item => item.inventoryTurnoverRate);
const barOption = {
title: {
text: '库存周转率排行前10名',
left: 'center'
},
tooltip: {
trigger: 'axis',
formatter: '{b}<br/>周转率: {c}%'
},
xAxis: {
type: 'category',
data: barCategories,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '周转率(%)'
},
series: [
{
name: '周转率',
type: 'bar',
data: barData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
}
]
};
pieChart?.setOption(pieOption);
barChart?.setOption(barOption);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
pieChart?.resize();
barChart?.resize();
};
onMounted(() => {
getMaterialCategoryOptions();
getList();
initCharts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
pieChart?.dispose();
barChart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-item {
padding: 20px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
</style>

@ -0,0 +1,462 @@
<template>
<div class="p-2">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="text-xl font-bold">WMS报表中心</span>
<el-button type="primary" @click="refreshAllData"></el-button>
</div>
</template>
<!-- 报表导航卡片 -->
<el-row :gutter="20" class="mb-6">
<el-col :span="8" v-for="report in reportCards" :key="report.key">
<el-card shadow="hover" class="report-card" @click="navigateToReport(report.route)">
<div class="report-item">
<div class="report-icon">
<el-icon :size="40" :color="report.color">
<component :is="report.icon" />
</el-icon>
</div>
<div class="report-content">
<div class="report-title">{{ report.title }}</div>
<div class="report-desc">{{ report.description }}</div>
<div class="report-count">{{ report.count }} 条记录</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 快速统计概览 -->
<el-row :gutter="20" class="mb-6">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span>快速统计概览</span>
</template>
<el-row :gutter="20">
<el-col :span="4">
<div class="overview-item">
<div class="overview-value text-red-500">{{ overviewStats.alertCount }}</div>
<div class="overview-label">预警物料</div>
</div>
</el-col>
<el-col :span="4">
<div class="overview-item">
<div class="overview-value text-orange-500">{{ overviewStats.stagnantCount }}</div>
<div class="overview-label">呆滞物料</div>
</div>
</el-col>
<el-col :span="4">
<div class="overview-item">
<div class="overview-value text-yellow-500">{{ overviewStats.returnCount }}</div>
<div class="overview-label">退库单数</div>
</div>
</el-col>
<el-col :span="4">
<div class="overview-item">
<div class="overview-value text-blue-500">{{ overviewStats.differenceCount }}</div>
<div class="overview-label">盘点差异</div>
</div>
</el-col>
<el-col :span="4">
<div class="overview-item">
<div class="overview-value text-green-500">{{ overviewStats.avgTurnoverRate.toFixed(1) }}%</div>
<div class="overview-label">平均周转率</div>
</div>
</el-col>
<el-col :span="4">
<div class="overview-item">
<div class="overview-value text-purple-500">{{ overviewStats.totalInventory }}</div>
<div class="overview-label">总库存量</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<!-- 图表展示区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>报表数据分布</span>
</template>
<div ref="pieChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>关键指标趋势</span>
</template>
<div ref="lineChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup name="ReportDashboard" lang="ts">
import {
getReturnReasonAnalysis,
getInventoryTrendAnalysis,
getSafetyStockAlert,
getStagnantInventory,
getInventoryDifference,
getInventoryTurnover
} from '@/api/wms/report';
import * as echarts from 'echarts';
import { useRouter } from 'vue-router';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
//
const reportCards = ref([
{
key: 'returnReason',
title: '退库原因分析',
description: '分析各类退库原因及占比',
icon: 'PieChart',
color: '#409EFF',
route: '/wms/returnReasonAnalysis',
count: 0
},
{
key: 'inventoryTrend',
title: '库存变动趋势',
description: '展示库存数量变化趋势',
icon: 'TrendCharts',
color: '#67C23A',
route: '/wms/inventoryTrendAnalysis',
count: 0
},
{
key: 'safetyStock',
title: '安全库存预警',
description: '监控库存安全水位',
icon: 'Warning',
color: '#E6A23C',
route: '/wms/safetyStockAlert',
count: 0
},
{
key: 'stagnantInventory',
title: '呆滞料库存',
description: '识别长期未流动物料',
icon: 'Clock',
color: '#F56C6C',
route: '/wms/stagnantInventory',
count: 0
},
{
key: 'inventoryDifference',
title: '库存差异报表',
description: '盘点差异分析',
icon: 'DataAnalysis',
color: '#909399',
route: '/wms/inventoryDifference',
count: 0
},
{
key: 'inventoryTurnover',
title: '库存周转报表',
description: '分析库存周转效率',
icon: 'Refresh',
color: '#9C27B0',
route: '/wms/inventoryTurnover',
count: 0
}
]);
//
const overviewStats = ref({
alertCount: 0,
stagnantCount: 0,
returnCount: 0,
differenceCount: 0,
avgTurnoverRate: 0,
totalInventory: 0
});
//
const pieChartRef = ref<HTMLDivElement>();
const lineChartRef = ref<HTMLDivElement>();
let pieChart: echarts.ECharts;
let lineChart: echarts.ECharts;
/** 导航到具体报表页面 */
const navigateToReport = (route: string) => {
router.push(route);
};
/** 刷新所有数据 */
const refreshAllData = async () => {
await Promise.all([
loadReturnReasonData(),
loadInventoryTrendData(),
loadSafetyStockData(),
loadStagnantInventoryData(),
loadInventoryDifferenceData(),
loadInventoryTurnoverData()
]);
nextTick(() => {
updateCharts();
});
};
/** 加载各报表数据 */
const loadReturnReasonData = async () => {
try {
const res = await getReturnReasonAnalysis({});
reportCards.value[0].count = res.data.length;
overviewStats.value.returnCount = res.data.reduce((sum, item) => sum + (item.returnOrderCount || 0), 0);
} catch (error) {
console.error('加载退库原因数据失败:', error);
}
};
const loadInventoryTrendData = async () => {
try {
const res = await getInventoryTrendAnalysis({});
reportCards.value[1].count = res.data.length;
} catch (error) {
console.error('加载库存趋势数据失败:', error);
}
};
const loadSafetyStockData = async () => {
try {
const res = await getSafetyStockAlert({});
reportCards.value[2].count = res.data.length;
overviewStats.value.alertCount = res.data.filter(item => item.alertStatus !== '正常').length;
} catch (error) {
console.error('加载安全库存数据失败:', error);
}
};
const loadStagnantInventoryData = async () => {
try {
const res = await getStagnantInventory({});
reportCards.value[3].count = res.data.length;
overviewStats.value.stagnantCount = res.data.length;
overviewStats.value.totalInventory = res.data.reduce((sum, item) => sum + (item.stagnantInventoryQty || 0), 0);
} catch (error) {
console.error('加载呆滞料数据失败:', error);
}
};
const loadInventoryDifferenceData = async () => {
try {
const res = await getInventoryDifference({});
reportCards.value[4].count = res.data.length;
overviewStats.value.differenceCount = res.data.filter(item => item.differenceType !== '无差异').length;
} catch (error) {
console.error('加载库存差异数据失败:', error);
}
};
const loadInventoryTurnoverData = async () => {
try {
const res = await getInventoryTurnover({});
reportCards.value[5].count = res.data.length;
overviewStats.value.avgTurnoverRate = res.data.length > 0
? res.data.reduce((sum, item) => sum + (item.inventoryTurnoverRate || 0), 0) / res.data.length
: 0;
} catch (error) {
console.error('加载库存周转数据失败:', error);
}
};
/** 初始化图表 */
const initCharts = () => {
if (pieChartRef.value) {
pieChart = echarts.init(pieChartRef.value);
}
if (lineChartRef.value) {
lineChart = echarts.init(lineChartRef.value);
}
};
/** 更新图表数据 */
const updateCharts = () => {
//
const pieData = reportCards.value.map(card => ({
name: card.title,
value: card.count
}));
const pieOption = {
title: {
text: '报表数据分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '数据量',
type: 'pie',
radius: '50%',
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
// 线
const lineOption = {
title: {
text: '关键指标趋势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['预警物料', '呆滞物料', '差异物料'],
top: 30
},
xAxis: {
type: 'category',
data: ['本周', '上周', '上上周', '三周前', '四周前']
},
yAxis: {
type: 'value',
name: '数量'
},
series: [
{
name: '预警物料',
type: 'line',
data: [overviewStats.value.alertCount, overviewStats.value.alertCount * 0.9, overviewStats.value.alertCount * 1.1, overviewStats.value.alertCount * 0.8, overviewStats.value.alertCount * 1.2],
smooth: true,
itemStyle: { color: '#E6A23C' }
},
{
name: '呆滞物料',
type: 'line',
data: [overviewStats.value.stagnantCount, overviewStats.value.stagnantCount * 1.1, overviewStats.value.stagnantCount * 0.9, overviewStats.value.stagnantCount * 1.3, overviewStats.value.stagnantCount * 0.7],
smooth: true,
itemStyle: { color: '#F56C6C' }
},
{
name: '差异物料',
type: 'line',
data: [overviewStats.value.differenceCount, overviewStats.value.differenceCount * 0.8, overviewStats.value.differenceCount * 1.2, overviewStats.value.differenceCount * 0.6, overviewStats.value.differenceCount * 1.4],
smooth: true,
itemStyle: { color: '#909399' }
}
]
};
pieChart?.setOption(pieOption);
lineChart?.setOption(lineOption);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
pieChart?.resize();
lineChart?.resize();
};
onMounted(() => {
refreshAllData();
initCharts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
pieChart?.dispose();
lineChart?.dispose();
});
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.report-card {
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.report-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.report-item {
display: flex;
align-items: center;
padding: 20px;
}
.report-icon {
margin-right: 20px;
}
.report-content {
flex: 1;
}
.report-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 8px;
color: #303133;
}
.report-desc {
font-size: 0.9rem;
color: #606266;
margin-bottom: 8px;
}
.report-count {
font-size: 0.8rem;
color: #909399;
}
.overview-item {
text-align: center;
padding: 20px;
}
.overview-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 8px;
}
.overview-label {
font-size: 0.9rem;
color: #666;
}
.el-card {
margin-bottom: 20px;
}
</style>

@ -0,0 +1,258 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物料大类" prop="materialCategoryId">
<el-select v-model="queryParams.materialCategoryId" placeholder="请选择物料大类" clearable>
<el-option v-for="item in materialCategoryOptions"
:key="item.materialCategoryId"
:label="item.materialCategoryName"
:value="item.materialCategoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 图表展示区域 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>退库原因分布</span>
</template>
<div ref="pieChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>退库数量统计</span>
</template>
<div ref="barChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="退库原因分类" align="center" prop="returnReasonCategory" />
<el-table-column label="退库单数" align="center" prop="returnOrderCount" />
<el-table-column label="退库总数量" align="center" prop="totalReturnAmount" />
<el-table-column label="退库单数占比(%)" align="center" prop="orderCountRatio" />
<el-table-column label="退库数量占比(%)" align="center" prop="amountRatio" />
<el-table-column label="主要物料名称" align="center" prop="materialName" show-overflow-tooltip />
<el-table-column label="物料大类" align="center" prop="materialCategoryName" />
<el-table-column label="物料编码" align="center" prop="materialCode" />
</el-table>
</el-card>
</div>
</template>
<script setup name="ReturnReasonAnalysis" lang="ts">
import { getReturnReasonAnalysis, exportReturnReasonAnalysis } from '@/api/wms/report';
import { listBaseMaterialCategoryInWMS } from '@/api/wms/baseMaterialCategory';
import { ReturnReasonAnalysisVO, ReportQuery } from '@/api/wms/report/types';
import { BaseMaterialCategoryVO } from '@/api/wms/baseMaterialCategory/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<ReturnReasonAnalysisVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const materialCategoryOptions = ref<BaseMaterialCategoryVO[]>([]);
//
const pieChartRef = ref<HTMLDivElement>();
const barChartRef = ref<HTMLDivElement>();
let pieChart: echarts.ECharts;
let barChart: echarts.ECharts;
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<ReportQuery>({
tenantId: undefined,
materialCategoryId: undefined
});
/** 查询退库原因分析报表列表 */
const getList = async () => {
loading.value = true;
const res = await getReturnReasonAnalysis(queryParams.value);
reportList.value = res.data;
loading.value = false;
//
nextTick(() => {
updateCharts();
});
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: ReturnReasonAnalysisVO[]) => {
ids.value = selection.map(item => item.tenantId!);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/wms/report/returnReasonAnalysis/export', {
...queryParams.value
}, `退库原因分析报表_${new Date().getTime()}.xlsx`);
};
/** 获取物料大类选项 */
const getMaterialCategoryOptions = async () => {
const res = await listBaseMaterialCategoryInWMS({});
materialCategoryOptions.value = res.data.rows || [];
};
/** 初始化图表 */
const initCharts = () => {
if (pieChartRef.value) {
pieChart = echarts.init(pieChartRef.value);
}
if (barChartRef.value) {
barChart = echarts.init(barChartRef.value);
}
};
/** 更新图表数据 */
const updateCharts = () => {
if (!reportList.value.length) return;
//
const pieData = reportList.value.map(item => ({
name: item.returnReasonCategory,
value: item.returnOrderCount
}));
const pieOption = {
title: {
text: '退库原因分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '退库原因',
type: 'pie',
radius: '50%',
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
//
const barData = reportList.value.map(item => item.totalReturnAmount);
const barCategories = reportList.value.map(item => item.returnReasonCategory);
const barOption = {
title: {
text: '退库数量统计',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: barCategories,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value'
},
series: [
{
name: '退库数量',
type: 'bar',
data: barData,
itemStyle: {
color: '#409EFF'
}
}
]
};
pieChart?.setOption(pieOption);
barChart?.setOption(barOption);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
pieChart?.resize();
barChart?.resize();
};
onMounted(() => {
getMaterialCategoryOptions();
getList();
initCharts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
pieChart?.dispose();
barChart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 20px;
}
</style>

@ -0,0 +1,321 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物料大类" prop="materialCategoryId">
<el-select v-model="queryParams.materialCategoryId" placeholder="请选择物料大类" clearable>
<el-option v-for="item in materialCategoryOptions"
:key="item.materialCategoryId"
:label="item.materialCategoryName"
:value="item.materialCategoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-red-500">{{ alertStats.shortage }}</div>
<div class="stat-label">短缺预警</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-orange-500">{{ alertStats.belowSafety }}</div>
<div class="stat-label">低于安全库存</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-yellow-500">{{ alertStats.overstock }}</div>
<div class="stat-label">积压预警</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-green-500">{{ alertStats.normal }}</div>
<div class="stat-label">正常</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表展示区域 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<span>安全库存预警分布</span>
</template>
<div ref="chartRef" style="height: 400px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="物料编码" align="center" prop="materialCode" />
<el-table-column label="物料名称" align="center" prop="materialName" show-overflow-tooltip />
<el-table-column label="物料大类" align="center" prop="materialCategoryName" />
<el-table-column label="当前库存数量" align="center" prop="currentInventoryQty" />
<el-table-column label="安全库存值" align="center" prop="safeStockAmount" />
<el-table-column label="最小库存值" align="center" prop="minStockAmount" />
<el-table-column label="最大库存值" align="center" prop="maxStockAmount" />
<el-table-column label="预警状态" align="center" prop="alertStatus">
<template #default="scope">
<el-tag :type="getAlertType(scope.row.alertStatus)">
{{ scope.row.alertStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="差异量" align="center" prop="differenceAmount" />
<el-table-column label="最后更新时间" align="center" prop="lastUpdateTime" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup name="SafetyStockAlert" lang="ts">
import { getSafetyStockAlert, exportSafetyStockAlert } from '@/api/wms/report';
import { listBaseMaterialCategoryInWMS } from '@/api/wms/baseMaterialCategory';
import { SafetyStockAlertVO, ReportQuery } from '@/api/wms/report/types';
import { BaseMaterialCategoryVO } from '@/api/wms/baseMaterialCategory/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<SafetyStockAlertVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const materialCategoryOptions = ref<BaseMaterialCategoryVO[]>([]);
//
const alertStats = ref({
shortage: 0,
belowSafety: 0,
overstock: 0,
normal: 0
});
//
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts;
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<ReportQuery>({
tenantId: undefined,
materialCategoryId: undefined
});
/** 查询安全库存预警报表列表 */
const getList = async () => {
loading.value = true;
const res = await getSafetyStockAlert(queryParams.value);
reportList.value = res.data;
loading.value = false;
//
calculateStats();
//
nextTick(() => {
updateChart();
});
};
/** 计算统计数据 */
const calculateStats = () => {
alertStats.value = {
shortage: reportList.value.filter(item => item.alertStatus === '短缺预警').length,
belowSafety: reportList.value.filter(item => item.alertStatus === '低于安全库存').length,
overstock: reportList.value.filter(item => item.alertStatus === '积压预警').length,
normal: reportList.value.filter(item => item.alertStatus === '正常').length
};
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: SafetyStockAlertVO[]) => {
ids.value = selection.map(item => item.materialId!);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/wms/report/safetyStockAlert/export', {
...queryParams.value
}, `安全库存预警报表_${new Date().getTime()}.xlsx`);
};
/** 获取物料大类选项 */
const getMaterialCategoryOptions = async () => {
const res = await listBaseMaterialCategoryInWMS({});
materialCategoryOptions.value = res.data.rows || [];
};
/** 获取预警状态类型 */
const getAlertType = (status: string) => {
switch (status) {
case '短缺预警': return 'danger';
case '低于安全库存': return 'warning';
case '积压预警': return 'info';
case '正常': return 'success';
default: return '';
}
};
/** 初始化图表 */
const initChart = () => {
if (chartRef.value) {
chart = echarts.init(chartRef.value);
}
};
/** 更新图表数据 */
const updateChart = () => {
if (!reportList.value.length || !chart) return;
//
const statusCount = reportList.value.reduce((acc, item) => {
const status = item.alertStatus || '未知';
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const pieData = Object.keys(statusCount).map(status => ({
name: status,
value: statusCount[status]
}));
const option = {
title: {
text: '安全库存预警分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '预警状态',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: pieData
}
]
};
chart.setOption(option);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
chart?.resize();
};
onMounted(() => {
getMaterialCategoryOptions();
getList();
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-item {
padding: 20px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
</style>

@ -0,0 +1,359 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="物料大类" prop="materialCategoryId">
<el-select v-model="queryParams.materialCategoryId" placeholder="请选择物料大类" clearable>
<el-option v-for="item in materialCategoryOptions"
:key="item.materialCategoryId"
:label="item.materialCategoryName"
:value="item.materialCategoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-red-500">{{ stagnantStats.neverOut }}</div>
<div class="stat-label">从未出库</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-orange-500">{{ stagnantStats.overSixMonths }}</div>
<div class="stat-label">超过6个月未出库</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-item">
<div class="stat-value text-blue-500">{{ stagnantStats.totalQty }}</div>
<div class="stat-label">呆滞库存总量</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表展示区域 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>呆滞原因分布</span>
</template>
<div ref="pieChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<span>呆滞天数分布</span>
</template>
<div ref="barChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="物料编码" align="center" prop="materialCode" />
<el-table-column label="物料名称" align="center" prop="materialName" show-overflow-tooltip />
<el-table-column label="物料大类" align="center" prop="materialCategoryName" />
<el-table-column label="呆滞库存数量" align="center" prop="stagnantInventoryQty" />
<el-table-column label="计量单位" align="center" prop="materialUnit" />
<el-table-column label="最后出库时间" align="center" prop="lastOutstockTime" width="180" />
<el-table-column label="呆滞天数" align="center" prop="stagnantDays">
<template #default="scope">
<el-tag :type="getStagnantDaysType(scope.row.stagnantDays)">
{{ scope.row.stagnantDays }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="呆滞原因" align="center" prop="stagnantReason" />
<el-table-column label="物料规格" align="center" prop="materialSpec" show-overflow-tooltip />
<el-table-column label="所在仓库" align="center" prop="warehouseName" />
</el-table>
</el-card>
</div>
</template>
<script setup name="StagnantInventory" lang="ts">
import { getStagnantInventory, exportStagnantInventory } from '@/api/wms/report';
import { listBaseMaterialCategoryInWMS } from '@/api/wms/baseMaterialCategory';
import { StagnantInventoryVO, ReportQuery } from '@/api/wms/report/types';
import { BaseMaterialCategoryVO } from '@/api/wms/baseMaterialCategory/types';
import * as echarts from 'echarts';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const reportList = ref<StagnantInventoryVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const materialCategoryOptions = ref<BaseMaterialCategoryVO[]>([]);
//
const stagnantStats = ref({
neverOut: 0,
overSixMonths: 0,
totalQty: 0
});
//
const pieChartRef = ref<HTMLDivElement>();
const barChartRef = ref<HTMLDivElement>();
let pieChart: echarts.ECharts;
let barChart: echarts.ECharts;
const queryFormRef = ref<ElFormInstance>();
const queryParams = ref<ReportQuery>({
tenantId: undefined,
materialCategoryId: undefined
});
/** 查询呆滞料库存报表列表 */
const getList = async () => {
loading.value = true;
const res = await getStagnantInventory(queryParams.value);
reportList.value = res.data;
loading.value = false;
//
calculateStats();
//
nextTick(() => {
updateCharts();
});
};
/** 计算统计数据 */
const calculateStats = () => {
stagnantStats.value = {
neverOut: reportList.value.filter(item => item.stagnantReason === '从未出库').length,
overSixMonths: reportList.value.filter(item => item.stagnantReason === '超过6个月未出库').length,
totalQty: reportList.value.reduce((sum, item) => sum + (item.stagnantInventoryQty || 0), 0)
};
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StagnantInventoryVO[]) => {
ids.value = selection.map(item => item.materialId!);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/wms/report/stagnantInventory/export', {
...queryParams.value
}, `呆滞料库存报表_${new Date().getTime()}.xlsx`);
};
/** 获取物料大类选项 */
const getMaterialCategoryOptions = async () => {
const res = await listBaseMaterialCategoryInWMS({});
materialCategoryOptions.value = res.data.rows || [];
};
/** 获取呆滞天数类型 */
const getStagnantDaysType = (days: number) => {
if (days >= 365) return 'danger';
if (days >= 180) return 'warning';
return 'info';
};
/** 初始化图表 */
const initCharts = () => {
if (pieChartRef.value) {
pieChart = echarts.init(pieChartRef.value);
}
if (barChartRef.value) {
barChart = echarts.init(barChartRef.value);
}
};
/** 更新图表数据 */
const updateCharts = () => {
if (!reportList.value.length) return;
//
const reasonCount = reportList.value.reduce((acc, item) => {
const reason = item.stagnantReason || '未知';
acc[reason] = (acc[reason] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const pieData = Object.keys(reasonCount).map(reason => ({
name: reason,
value: reasonCount[reason]
}));
const pieOption = {
title: {
text: '呆滞原因分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '呆滞原因',
type: 'pie',
radius: '50%',
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
//
const daysRanges = ['0-90天', '91-180天', '181-365天', '365天以上'];
const daysData = daysRanges.map(range => {
switch (range) {
case '0-90天':
return reportList.value.filter(item => (item.stagnantDays || 0) <= 90).length;
case '91-180天':
return reportList.value.filter(item => (item.stagnantDays || 0) > 90 && (item.stagnantDays || 0) <= 180).length;
case '181-365天':
return reportList.value.filter(item => (item.stagnantDays || 0) > 180 && (item.stagnantDays || 0) <= 365).length;
case '365天以上':
return reportList.value.filter(item => (item.stagnantDays || 0) > 365).length;
default:
return 0;
}
});
const barOption = {
title: {
text: '呆滞天数分布',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: daysRanges
},
yAxis: {
type: 'value',
name: '物料数量'
},
series: [
{
name: '物料数量',
type: 'bar',
data: daysData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
}
]
};
pieChart?.setOption(pieOption);
barChart?.setOption(barOption);
};
/** 窗口大小改变时重新调整图表 */
const handleResize = () => {
pieChart?.resize();
barChart?.resize();
};
onMounted(() => {
getMaterialCategoryOptions();
getList();
initCharts();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
pieChart?.dispose();
barChart?.dispose();
});
</script>
<style scoped>
.el-card {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-item {
padding: 20px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
</style>
Loading…
Cancel
Save