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
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,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,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>
|
||||
Loading…
Reference in New Issue