库存数据看板
This commit is contained in:
@@ -2,32 +2,82 @@
|
||||
<div style="display: flex; height: calc(100vh - 100px);">
|
||||
<!-- 左侧导航树 -->
|
||||
<div style="width: 280px; min-width: 200px; border-right: 1px solid #eee; padding: 10px 0;">
|
||||
<el-tree
|
||||
:data="warehouseTreeData"
|
||||
node-key="warehouseId"
|
||||
:props="treeProps"
|
||||
highlight-current
|
||||
@node-click="handleTreeNodeClick"
|
||||
:default-expand-all="true"
|
||||
style="height: 100%; overflow-y: auto;"
|
||||
/>
|
||||
<el-tree :data="warehouseTreeData" node-key="warehouseId" :props="treeProps" highlight-current
|
||||
@node-click="handleTreeNodeClick" :default-expand-all="true" style="height: 100%; overflow-y: auto;" />
|
||||
</div>
|
||||
<!-- 右侧图表 -->
|
||||
<div ref="chartContainer" style="flex: 1; height: 100%;"></div>
|
||||
<div style="flex: 1; height: calc(100vh - 100px); overflow-y: scroll; overflow-x: hidden;">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
仓库库存
|
||||
</template>
|
||||
<div style="height: 600px;">
|
||||
<ChartWrapper>
|
||||
<rea-tree ref="reaTree" :stock-data="stockData" :warehouse-tree-data="warehouseTreeData" height="600px" />
|
||||
</ChartWrapper>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
物料统计TOP10
|
||||
</template>
|
||||
<div style="height: 300px;">
|
||||
<ChartWrapper>
|
||||
<material-bar ref="materialBar" :stock-data="stockData"
|
||||
:selected-warehouse-id="currentTreeNode ? currentTreeNode.warehouseId : null"
|
||||
:warehouse-tree-data="warehouseTreeData" />
|
||||
</ChartWrapper>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
变动趋势
|
||||
<el-date-picker style="float: right;" v-model="defaultTimeRange" type="daterange" range-separator="至"
|
||||
start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" />
|
||||
</template>
|
||||
<div style="height: 300px;">
|
||||
<ChartWrapper>
|
||||
<TrendChart ref="trendChart" :warehouseId="currentTreeNode ? currentTreeNode.warehouseId : null"
|
||||
:time-range="defaultTimeRange"></TrendChart>
|
||||
</ChartWrapper>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
import { listStock } from "@/api/wms/stock";
|
||||
import { listWarehouse } from "@/api/wms/warehouse";
|
||||
import 'element-ui/lib/theme-chalk/index.css';
|
||||
import ReaTree from './panels/reattree.vue';
|
||||
import MaterialBar from './panels/bar.vue';
|
||||
import TrendChart from './panels/trendChart.vue';
|
||||
import ChartWrapper from '@/components/ChartWrapper/index.vue';
|
||||
|
||||
export default {
|
||||
name: "StockBox",
|
||||
components: {
|
||||
ReaTree,
|
||||
MaterialBar,
|
||||
TrendChart,
|
||||
ChartWrapper
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
stockData: [],
|
||||
warehouseData: [],
|
||||
warehouseTreeData: [],
|
||||
@@ -37,49 +87,29 @@ export default {
|
||||
isLeaf: 'isLeaf',
|
||||
id: 'id'
|
||||
},
|
||||
currentTreeNode: null
|
||||
currentTreeNode: null,
|
||||
defaultTimeRange: [
|
||||
new Date(new Date().setDate(new Date().getDate() - 30)).toISOString().split('T')[0],
|
||||
new Date().toISOString().split('T')[0]
|
||||
]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose();
|
||||
}
|
||||
this.chart = echarts.init(this.$refs.chartContainer);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize();
|
||||
},
|
||||
async loadData() {
|
||||
try {
|
||||
// 显示加载动画
|
||||
this.chart.showLoading();
|
||||
|
||||
const [warehouseRes, stockRes] = await Promise.all([
|
||||
listWarehouse(),
|
||||
listStock({ pageNum: 1, pageSize: 9999 })
|
||||
]);
|
||||
|
||||
// 隐藏加载动画
|
||||
this.chart.hideLoading();
|
||||
|
||||
|
||||
// 处理树结构
|
||||
this.warehouseTreeData = this.handleTree(warehouseRes.data, 'warehouseId', 'parentId');
|
||||
this.stockData = stockRes.rows;
|
||||
|
||||
// 创建层级数据
|
||||
const treeData = this.createTreeData();
|
||||
|
||||
// 更新图表
|
||||
this.updateChart(treeData);
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
this.chart.hideLoading();
|
||||
this.$message.error('库存数据加载失败');
|
||||
}
|
||||
},
|
||||
@@ -89,216 +119,26 @@ export default {
|
||||
console.error('handleTree: data is not array', data);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const map = {};
|
||||
const tree = [];
|
||||
|
||||
|
||||
// 创建节点映射
|
||||
data.forEach(item => {
|
||||
map[item[id]] = { ...item, children: [] };
|
||||
});
|
||||
|
||||
|
||||
// 构建树结构
|
||||
data.forEach(item => {
|
||||
const node = map[item[id]];
|
||||
if (!item[parentId] || item[parentId] === 0) {
|
||||
// 顶级节点(parentId为0或null)
|
||||
tree.push(node);
|
||||
} else if (map[item[parentId]]) {
|
||||
map[item[parentId]].children.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
},
|
||||
// 创建树形数据
|
||||
createTreeData() {
|
||||
// 递归构建仓库树
|
||||
const buildWarehouseTree = (warehouseNode, parentId = '') => {
|
||||
const stocks = this.stockData.filter(stock => stock.warehouseId === warehouseNode.warehouseId);
|
||||
const children = [];
|
||||
let totalQuantity = 0;
|
||||
// 添加库存物品
|
||||
stocks.forEach(stock => {
|
||||
const quantity = Number(stock.quantity) || 0;
|
||||
totalQuantity += quantity;
|
||||
// 物料节点 id 用 仓库id+物料编码
|
||||
children.push({
|
||||
id: `${warehouseNode.warehouseId}_${stock.itemCode || stock.itemName}`,
|
||||
name: stock.itemName,
|
||||
value: quantity,
|
||||
itemInfo: {
|
||||
type: stock.itemType,
|
||||
code: stock.itemCode,
|
||||
unit: stock.unit,
|
||||
batchNo: stock.batchNo
|
||||
}
|
||||
});
|
||||
});
|
||||
// 递归处理子仓库
|
||||
if (warehouseNode.children && warehouseNode.children.length > 0) {
|
||||
warehouseNode.children.forEach(child => {
|
||||
const childNode = buildWarehouseTree(child, warehouseNode.warehouseId);
|
||||
if (childNode.value > 0) {
|
||||
children.push(childNode);
|
||||
totalQuantity += childNode.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 仓库节点 id 用 warehouseId
|
||||
return {
|
||||
id: String(warehouseNode.warehouseId),
|
||||
name: warehouseNode.warehouseName,
|
||||
value: totalQuantity,
|
||||
warehouseInfo: {
|
||||
code: warehouseNode.warehouseCode
|
||||
},
|
||||
children: children.length > 0 ? children : undefined
|
||||
};
|
||||
};
|
||||
// 直接返回顶级仓库节点
|
||||
return this.warehouseTreeData.map(warehouse => buildWarehouseTree(warehouse));
|
||||
},
|
||||
// 获取层级样式配置(参考ECharts官网示例)
|
||||
getLevelOption() {
|
||||
return [
|
||||
// 顶级仓库层级样式(parentId为0或null)
|
||||
{
|
||||
itemStyle: {
|
||||
borderColor: '#555',
|
||||
borderWidth: 4,
|
||||
gapWidth: 3
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: '#333'
|
||||
}
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
height: 35,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
// 子仓库层级样式
|
||||
{
|
||||
itemStyle: {
|
||||
borderColor: '#777',
|
||||
borderWidth: 3,
|
||||
gapWidth: 2
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: '#555'
|
||||
}
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
height: 28,
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
// 物料层级样式
|
||||
{
|
||||
itemStyle: {
|
||||
borderColor: '#999',
|
||||
borderWidth: 2,
|
||||
gapWidth: 1
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: '#777'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
// 更新图表
|
||||
updateChart(treeData) {
|
||||
const option = {
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 18
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: (info) => {
|
||||
const value = info.value || 0;
|
||||
const treePath = info.treePathInfo || [];
|
||||
let path = '';
|
||||
|
||||
// 构建完整路径(从第一个节点开始)
|
||||
for (let i = 0; i < treePath.length; i++) {
|
||||
if (treePath[i].name) {
|
||||
path += treePath[i].name;
|
||||
if (i < treePath.length - 1) path += '/';
|
||||
}
|
||||
}
|
||||
|
||||
const content = [];
|
||||
content.push(`<div class="tooltip-title">${echarts.format.encodeHTML(path)}</div>`);
|
||||
content.push(`库存数量: ${echarts.format.addCommas(value)} ${this.getItemUnit(info.data)}`);
|
||||
|
||||
// 添加物品详细信息
|
||||
if (info.data.itemInfo) {
|
||||
const item = info.data.itemInfo;
|
||||
content.push(`物料类型: ${this.getItemTypeName(item.type)}`);
|
||||
content.push(`物料编码: ${item.code || '无'}`);
|
||||
if (item.batchNo) content.push(`批次号: ${item.batchNo}`);
|
||||
}
|
||||
|
||||
return content.join('<br>');
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '库存',
|
||||
type: 'treemap',
|
||||
visibleMin: 300, // 只有当区块面积大于300时才会显示标签
|
||||
leafDepth: 2, // 只在叶子节点显示标签
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
formatter: (params) => {
|
||||
// 对于物料只显示名称和数量
|
||||
if (params.data.itemInfo) {
|
||||
const unit = params.data.itemInfo.unit || '';
|
||||
return `${params.name}\n${params.value}${unit}`;
|
||||
}
|
||||
// 对于仓库只显示名称
|
||||
return params.name;
|
||||
},
|
||||
ellipsis: true // 超出时显示省略号
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
levels: this.getLevelOption(),
|
||||
data: treeData // 直接使用顶级仓库节点数组
|
||||
}]
|
||||
};
|
||||
|
||||
this.chart.setOption(option, true);
|
||||
},
|
||||
// 获取物料单位
|
||||
getItemUnit(data) {
|
||||
return data.itemInfo?.unit || '';
|
||||
},
|
||||
// 获取物料类型名称
|
||||
getItemTypeName(type) {
|
||||
const typeMap = {
|
||||
raw_material: '原材料',
|
||||
product: '产品',
|
||||
semi_product: '半成品'
|
||||
};
|
||||
return typeMap[type] || type || '未分类';
|
||||
return tree;
|
||||
},
|
||||
// 刷新数据
|
||||
refresh() {
|
||||
@@ -307,32 +147,9 @@ export default {
|
||||
// 导航树点击事件
|
||||
handleTreeNodeClick(node) {
|
||||
this.currentTreeNode = node;
|
||||
// 图表高亮并聚焦对应节点
|
||||
if (node && node.warehouseId) {
|
||||
this.highlightChartNode(node.warehouseId);
|
||||
this.$refs.reaTree.highlightNode(node.warehouseId);
|
||||
}
|
||||
},
|
||||
// 高亮并聚焦图表节点
|
||||
highlightChartNode(id) {
|
||||
if (!this.chart) return;
|
||||
// 高亮节点
|
||||
this.chart.dispatchAction({
|
||||
type: 'highlight',
|
||||
seriesIndex: 0,
|
||||
targetNodeId: id
|
||||
});
|
||||
// 聚焦节点(自动缩放到该节点)
|
||||
this.chart.dispatchAction({
|
||||
type: 'treemapRootToNode',
|
||||
seriesIndex: 0,
|
||||
targetNodeId: id
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
if (this.chart) {
|
||||
this.chart.dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -345,4 +162,8 @@ export default {
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.el-row {
|
||||
margin: 10px !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user