feat(RealTimeInventory): 添加库存历史趋势图表功能

在库存详情对话框中新增库存历史趋势图表,支持选择时间范围和显示单位。图表展示库存变化趋势,帮助用户更好地分析库存动态。

主要变更:
- 增加echarts图表库依赖
- 添加时间范围选择器和单位切换控件
- 实现历史数据API调用和数据处理
- 添加图表初始化、渲染和响应式调整逻辑
- 优化对话框关闭时的资源清理
master
huangjinysf 2 months ago
parent 0bcb34eef4
commit f6550cd819

@ -87,8 +87,9 @@
<el-dialog
v-model="wmsDialogVisible"
title="库存详情"
width="80%"
width="90%"
destroy-on-close
@close="handleDialogClose"
>
<WmsTable
v-if="wmsDialogVisible"
@ -97,14 +98,46 @@
:specification="currentWmsSpecification"
:loading-height="200"
:show-action="true"
@data-loaded="onWmsDataLoaded"
/>
<!-- 库存历史图表 -->
<div v-if="wmsDialogVisible" class="inventory-chart-section">
<div class="chart-header">
<h4>库存历史趋势 - {{ currentWmsModel }} - {{ currentWmsSpecification }}</h4>
</div>
<div class="chart-controls">
<span class="filter-label">时间范围:</span>
<el-select v-model="selectedTimeRange" @change="onChartTimeRangeChange" style="width: 120px">
<el-option label="近1天" value="近1天" />
<el-option label="近3天" value="近3天" />
<el-option label="近1周" value="近1周" />
<el-option label="近1月" value="近1月" />
</el-select>
<span class="filter-label" style="margin-left: 20px;">显示单位:</span>
<el-radio-group v-model="chartUnit" @change="onChartUnitChange" size="small">
<el-radio-button value="box">箱数</el-radio-button>
<el-radio-button value="weight">重量</el-radio-button>
</el-radio-group>
</div>
<div v-if="historyChartLoading" v-loading="true" class="chart-loading" style="height: 400px;"></div>
<div v-else-if="!hasHistoryData" class="no-data-message" style="height: 400px;">
暂无历史数据
</div>
<div v-show="!historyChartLoading && hasHistoryData" ref="inventoryChartRef" class="chart" style="height: 450px;"></div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, nextTick } from 'vue'
import { API_CONFIG } from "@/config/api"
import { getDateRangeByTimeRange } from '@/utils/dateFormat';
import * as echarts from 'echarts';
import { ElMessage } from 'element-plus'
import WmsTable from '@/components/WmsTable/index.vue'
@ -119,6 +152,15 @@ const currentWmsModel = ref('')
const currentWmsSpecification = ref('')
const wmsTableRef = ref(null)
//
const inventoryChartRef = ref(null)
const historyChartLoading = ref(false)
const historyData = ref([])
const hasHistoryData = ref(false)
const selectedTimeRange = ref('近1月')
const chartUnit = ref('box')
let inventoryChart = null
//
const showDifference = ref(true)
@ -337,12 +379,411 @@ const handleCellDblClick = (row, column, cell, event) => {
wire_disc = null;
}
console.log('双击事件参数:', {
columnKey,
model,
wire_disc,
specification: row.specification
});
// WMS
currentWmsModel.value = model;
currentWmsSpecification.value = row.specification;
//
historyData.value = [];
hasHistoryData.value = false;
// WMS
wmsDialogVisible.value = true;
//
fetchInventoryHistoryData(model, row.specification, wire_disc);
};
//
const fetchInventoryHistoryData = (model, specification, wireDisc) => {
historyChartLoading.value = true;
const dateRange = getDateRangeByTimeRange(selectedTimeRange.value);
// API URL
const params = new URLSearchParams({
model: model || '',
specification: specification || '',
wire_disc: wireDisc || '',
start_date: dateRange.start_date,
end_date: dateRange.end_date
});
let apiUrl = `${API_CONFIG.BASE_URL}/api/plan/wms/history?${params.toString()}`;
console.log('API调用参数:', {
model,
specification,
wireDisc,
dateRange,
apiUrl
});
fetch(apiUrl)
.then(response => response.json())
.then(data => {
console.log('API原始返回数据:', data);
if (data.code === 200 && data.data && data.data.records) {
let records = data.data.records;
console.log('原始records数据:', records);
console.log('records数量:', records.length);
//
if (records.length > 0) {
console.log('第一条记录结构:', records[0]);
console.log('第一条记录的所有键:', Object.keys(records[0]));
}
// 使
if (isMoreThanOneWeek(dateRange.start_date, dateRange.end_date)) {
records = downsampleHistoryToKeyTimes(records);
console.log('降采样后records数量:', records.length);
}
historyData.value = records;
hasHistoryData.value = historyData.value.length > 0;
console.log('设置后的historyData:', historyData.value);
console.log('hasHistoryData:', hasHistoryData.value);
nextTick(() => {
setTimeout(() => {
console.log('开始初始化图表');
initInventoryChart();
}, 100);
});
} else {
console.error('API返回数据格式错误:', data);
console.error('data.code:', data.code);
console.error('data.data:', data.data);
historyData.value = [];
hasHistoryData.value = false;
}
})
.catch(error => {
console.error('库存历史数据API调用失败:', error);
historyData.value = [];
hasHistoryData.value = false;
})
.finally(() => {
historyChartLoading.value = false;
});
};
//
const downsampleHistoryToKeyTimes = (records, targetHours = [8,9,10,11,12,13,14,15,16,17, 18]) => {
if (!records?.length) return [];
const groups = new Map();
records.forEach(item => {
const dt = new Date(item.create_time);
const dateKey = dt.toISOString().split('T')[0];
if (!groups.has(dateKey)) groups.set(dateKey, []);
groups.get(dateKey).push({ dt, item });
});
const result = [];
groups.forEach((items, dateKey) => {
items.sort((a, b) => a.dt - b.dt);
const selected = new Map();
targetHours.forEach(targetH => {
let closest = null;
let minDiff = Infinity;
items.forEach(({ dt, item }) => {
const diff = Math.abs((dt.getHours() - targetH) * 60 + dt.getMinutes());
if (diff < minDiff) {
minDiff = diff;
closest = item;
}
});
if (closest && minDiff <= 90) {
selected.set(targetH, closest);
}
});
targetHours.forEach(h => {
if (selected.has(h)) {
result.push(selected.get(h));
}
});
});
result.sort((a, b) => new Date(a.create_time) - new Date(b.create_time));
return result;
};
//
const isMoreThanOneWeek = (startDate, endDate) => {
if (!startDate || !endDate) return false;
const start = new Date(startDate);
const end = new Date(endDate);
const diffDays = (end - start) / (1000 * 60 * 60 * 24);
return diffDays > 7;
};
//
const initInventoryChart = () => {
console.log('initInventoryChart 被调用');
console.log('inventoryChartRef.value:', inventoryChartRef.value);
console.log('hasHistoryData:', hasHistoryData.value);
console.log('historyData.value:', historyData.value);
if (!inventoryChartRef.value) {
console.warn('图表容器不存在,跳过初始化');
return;
}
if (!hasHistoryData.value || !historyData.value.length) {
console.warn('没有历史数据,跳过图表初始化');
return;
}
//
if (inventoryChart) {
console.log('销毁现有图表');
inventoryChart.dispose();
}
//
console.log('创建新的ECharts实例');
inventoryChart = echarts.init(inventoryChartRef.value);
//
const chartData = prepareInventoryChartData();
console.log('准备的图表数据:', chartData);
if (!chartData.length) {
console.warn('图表数据为空,不渲染图表');
return;
}
//
const option = {
title: {
text: '库存历史趋势',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: function(params) {
let result = `<div style="margin-bottom: 5px;"><strong>${echarts.format.formatTime('yyyy-MM-dd hh:mm', params[0].value[0])}</strong></div>`;
params.forEach(param => {
const seriesName = param.seriesName;
const value = param.value[1];
const unit = chartUnit.value === 'box' ? '箱' : 'kg';
result += `<div style="margin: 3px 0;">
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${param.color};"></span>
${seriesName}: ${value}${unit}
</div>`;
});
return result;
}
},
legend: {
data: ['库存'],
top: 30
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'time',
splitLine: {
show: false
}
}
],
yAxis: [
{
type: 'value',
name: chartUnit.value === 'box' ? '箱数' : '重量(kg)',
axisLabel: {
formatter: '{value}'
}
}
],
series: chartData,
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 100
}
]
};
console.log('图表配置选项:', option);
//
inventoryChart.setOption(option);
console.log('图表渲染完成');
//
window.addEventListener('resize', resizeInventoryChart);
};
//
const prepareInventoryChartData = () => {
console.log('prepareInventoryChartData 被调用');
console.log('hasHistoryData.value:', hasHistoryData.value);
console.log('historyData.value.length:', historyData.value ? historyData.value.length : 0);
const series = [];
if (hasHistoryData.value && historyData.value.length > 0) {
console.log('开始处理历史数据');
console.log('第一条历史数据示例:', historyData.value[0]);
const historySeriesData = historyData.value.map((item, index) => {
console.log(`处理第${index}条数据:`, item);
//
const timeField = item.create_time || item.createTime || item.time || item.timestamp;
const boxCountField = item.box_count || item.boxCount || item.box || item.count || item.total_number;
const weightField = item.total_weight || item.totalWeight || item.weight || item.total_net_weight || item.totalGrossWeight;
// console.log(':', {
// timeField,
// boxCountField,
// weightField
// });
if (!timeField) {
// console.warn(`${index}`);
return null;
}
const time = new Date(timeField).getTime();
let value;
if (chartUnit.value === 'box') {
value = boxCountField || 0;
} else {
value = weightField || 0;
}
console.log(`时间戳: ${time}, 值: ${value}`);
if (isNaN(time) || time <= 0) {
console.warn(`${index}条数据时间格式无效:`, timeField);
return null;
}
return [time, value];
}).filter(item => item !== null); //
// console.log(':', historySeriesData);
// console.log(':', historySeriesData.length);
if (historySeriesData.length > 0) {
series.push({
name: '库存',
type: 'line',
data: historySeriesData,
smooth: true,
lineStyle: {
color: '#E6A23C',
width: 2
},
itemStyle: {
color: '#E6A23C'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(230, 162, 60, 0.3)' },
{ offset: 1, color: 'rgba(230, 162, 60, 0.1)' }
])
}
});
console.log('成功创建图表序列');
} else {
console.warn('没有有效的图表数据');
}
} else {
console.warn('没有历史数据或历史数据为空');
}
console.log('最终返回的series:', series);
return series;
};
//
const resizeInventoryChart = () => {
if (inventoryChart) {
inventoryChart.resize();
}
};
// WmsTable
const onWmsDataLoaded = (loadedData) => {
console.log('WmsTable数据加载完成:', loadedData);
};
//
const onChartTimeRangeChange = () => {
if (currentWmsModel.value && currentWmsSpecification.value) {
fetchInventoryHistoryData(currentWmsModel.value, currentWmsSpecification.value, null);
}
};
//
const onChartUnitChange = () => {
if (hasHistoryData.value) {
nextTick(() => {
setTimeout(() => {
initInventoryChart();
}, 100);
});
}
};
//
const handleDialogClose = () => {
//
if (inventoryChart) {
inventoryChart.dispose();
inventoryChart = null;
}
//
historyData.value = [];
hasHistoryData.value = false;
//
window.removeEventListener('resize', resizeInventoryChart);
};
//
@ -450,4 +891,58 @@ onMounted(() => {
.table-wrapper::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 库存历史图表样式 */
.inventory-chart-section {
margin-top: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.chart-header h4 {
margin: 0;
font-size: 16px;
color: #303133;
}
.chart-controls {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.filter-label {
margin-right: 10px;
font-weight: bold;
white-space: nowrap;
}
.chart-loading {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.no-data-message {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
.chart {
width: 100%;
min-height: 350px;
}
</style>
Loading…
Cancel
Save