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