You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
381 lines
11 KiB
Vue
381 lines
11 KiB
Vue
<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>
|
|
<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-green-500">{{ differenceStats.noDifference }}</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-blue-500">{{ differenceStats.profit }}</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-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>
|