From 89a079e49f5d4a84d9ac926ab3dfcf66c074f851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= <2178503051@qq.com> Date: Thu, 7 May 2026 11:07:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ems/dashboard):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=83=BD=E6=BA=90=E7=AE=A1=E7=90=86=E4=BB=AA=E8=A1=A8=E7=9B=98?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=B8=8E=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除统计卡片,新增能源流向桑基图 - 优化能源类型二维表展示 - 添加产线能源时序分析图表 - 调整能源消耗趋势和能源产线占比图表 - 增加能源类型和产线筛选功能 --- klp-ui/src/views/ems/dashboard/index.vue | 569 +++++++++++++++-------- 1 file changed, 377 insertions(+), 192 deletions(-) diff --git a/klp-ui/src/views/ems/dashboard/index.vue b/klp-ui/src/views/ems/dashboard/index.vue index 36e257e5..8bd35ad8 100644 --- a/klp-ui/src/views/ems/dashboard/index.vue +++ b/klp-ui/src/views/ems/dashboard/index.vue @@ -4,14 +4,8 @@
- + 刷新 @@ -19,26 +13,58 @@
- -
+ +
+ - -
-
- + +
+
+ 产线能源类型二维表
-
-
{{ stat.value }}
-
{{ stat.label }}
+
+ + + + + +
-
- -
- + + + +
+
+ 产线能源时序分析 +
+ + + + + + + + +
+
+
+
+
+
+ +
@@ -49,62 +75,39 @@
- +
- 能源类型占比 + 能源产线占比 + + + +
- - - -
-
- 产线能耗对比 -
-
-
-
- - - -
-
- 设备能耗排行 TOP 10 -
-
-
-
-
- - - + +
-
- 产线能源类型二维表 -
-
- - - - - - - - - +
+ 能源流向 + + + + +
+
@@ -140,6 +143,11 @@ export default { productionLines: [] }, energyTypeList: [], + productionLines: [], + selectedEnergyTypeId: null, + selectedLineAnalysisEnergyTypeId: null, + selectedSankeyEnergyTypeId: null, + selectedLines: [], meterList: [], energyRecords: [], statistics: [], @@ -147,8 +155,8 @@ export default { trendChartType: 'day', trendChart: null, energyTypeChart: null, - productionLineChart: null, - deviceRankingChart: null, + lineEnergyTrendChart: null, + sankeyChart: null, loading: false, } }, @@ -161,31 +169,44 @@ export default { window.removeEventListener('resize', this.handleResize) if (this.trendChart) this.trendChart.dispose() if (this.energyTypeChart) this.energyTypeChart.dispose() - if (this.productionLineChart) this.productionLineChart.dispose() - if (this.deviceRankingChart) this.deviceRankingChart.dispose() + if (this.lineEnergyTrendChart) this.lineEnergyTrendChart.dispose() + if (this.sankeyChart) this.sankeyChart.dispose() }, methods: { initCharts() { this.trendChart = echarts.init(this.$refs.trendChart) this.energyTypeChart = echarts.init(this.$refs.energyTypeChart) - this.productionLineChart = echarts.init(this.$refs.productionLineChart) - this.deviceRankingChart = echarts.init(this.$refs.deviceRankingChart) + this.lineEnergyTrendChart = echarts.init(this.$refs.lineEnergyTrendChart) + this.sankeyChart = echarts.init(this.$refs.sankeyChart) }, handleResize() { this.trendChart && this.trendChart.resize() this.energyTypeChart && this.energyTypeChart.resize() - this.productionLineChart && this.productionLineChart.resize() - this.deviceRankingChart && this.deviceRankingChart.resize() + this.lineEnergyTrendChart && this.lineEnergyTrendChart.resize() + this.sankeyChart && this.sankeyChart.resize() }, async loadBasicData() { try { this.loading = true; const [energyTypeRes, meterRes] = await Promise.all([ listEnergyType({ pageSize: 999 }), - listMeter({ pageSize: 999, isTotalMeter: 0 }) + listMeter({ pageSize: 999 }) ]) this.energyTypeList = energyTypeRes.rows || [] + if (this.energyTypeList.length > 0) { + this.selectedEnergyTypeId = this.energyTypeList[0].energyTypeId + this.selectedLineAnalysisEnergyTypeId = this.energyTypeList[0].energyTypeId + } this.meterList = meterRes.rows || [] + this.productionLines = this.dict.type.sys_lines || [] + if (this.productionLines.length > 0) { + this.selectedLines = this.productionLines.slice(0, 3).map(l => l.value) + } + // 设置默认的桑基图能源类型(筛选有且只有一个主表的能源) + const validSankeyEnergyTypes = this.getValidSankeyEnergyTypes() + if (validSankeyEnergyTypes.length > 0) { + this.selectedSankeyEnergyTypeId = validSankeyEnergyTypes[0].energyTypeId + } await this.loadData() } catch (error) { console.error('加载基础数据失败', error) @@ -216,8 +237,8 @@ export default { this.calculateStatistics() this.updateTrendChart() this.updateEnergyTypeChart() - this.updateProductionLineChart() - this.updateDeviceRankingChart() + this.updateLineEnergyTrendChart() + this.updateSankeyChart() this.updateTableData() }, calculateStatistics() { @@ -272,13 +293,13 @@ export default { }, updateTrendChart() { const dateFormat = this.trendChartType === 'day' ? 'YYYY-MM-DD' : - this.trendChartType === 'week' ? 'YYYY-WW' : 'YYYY-MM' - + this.trendChartType === 'week' ? 'YYYY-WW' : 'YYYY-MM' + const groupedData = new Map() this.energyRecords.forEach(record => { const dateKey = dayjs(record.recordDate).format(dateFormat) const energyType = this.getEnergyTypeName(record.energyId) - + if (!groupedData.has(dateKey)) { groupedData.set(dateKey, {}) } @@ -290,7 +311,7 @@ export default { const dates = Array.from(groupedData.keys()).sort() const energyTypes = Array.from(new Set(this.energyRecords.map(r => this.getEnergyTypeName(r.energyId)))) - + const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#00CED1'] const series = energyTypes.map((type, index) => ({ name: type, @@ -333,14 +354,26 @@ export default { this.trendChart.setOption(option) }, updateEnergyTypeChart() { - const typeMap = new Map() + const lineMap = new Map() + this.energyRecords.forEach(record => { - const typeName = this.getEnergyTypeName(record.energyId) - typeMap.set(typeName, (typeMap.get(typeName) || 0) + (Number(record.consumption) || 0)) + // 只统计选中的能源类型 + if (this.selectedEnergyTypeId && record.energyId !== this.selectedEnergyTypeId) { + return + } + + const meter = this.meterList.find(m => m.meterId === record.meterId) + if (meter && meter.productionLine) { + const lines = Array.isArray(meter.productionLine) ? meter.productionLine : [meter.productionLine] + lines.forEach(lineId => { + const lineName = this.getLineName(lineId) + lineMap.set(lineName, (lineMap.get(lineName) || 0) + (Number(record.consumption) || 0)) + }) + } }) - const data = Array.from(typeMap.entries()).map(([name, value]) => ({ name, value })) - + const data = Array.from(lineMap.entries()).map(([name, value]) => ({ name, value })) + const option = { tooltip: { trigger: 'item', @@ -353,7 +386,7 @@ export default { }, series: [ { - name: '能源类型', + name: '产线消耗', type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, @@ -382,112 +415,8 @@ export default { } this.energyTypeChart.setOption(option) }, - updateProductionLineChart() { - const lineMap = new Map() - - this.energyRecords.forEach(record => { - const meter = this.meterList.find(m => m.meterId === record.meterId) - if (meter && meter.productionLine) { - const lines = Array.isArray(meter.productionLine) ? meter.productionLine : [meter.productionLine] - lines.forEach(lineId => { - const lineName = this.getLineName(lineId) - lineMap.set(lineName, (lineMap.get(lineName) || 0) + (Number(record.consumption) || 0)) - }) - } - }) - const sortedLines = Array.from(lineMap.entries()) - .sort((a, b) => b[1] - a[1]) - - const option = { - tooltip: { - trigger: 'axis', - axisPointer: { - type: 'shadow' - } - }, - grid: { - left: '3%', - right: '4%', - bottom: '3%', - containLabel: true - }, - xAxis: { - type: 'value', - name: '能耗' - }, - yAxis: { - type: 'category', - data: sortedLines.map(item => item[0]) - }, - series: [ - { - name: '能耗', - type: 'bar', - data: sortedLines.map(item => item[1]), - itemStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ - { offset: 0, color: '#409EFF' }, - { offset: 1, color: '#00CED1' } - ]), - borderRadius: [0, 4, 4, 0] - } - } - ] - } - this.productionLineChart.setOption(option) - }, - updateDeviceRankingChart() { - const deviceMap = new Map() - - this.energyRecords.forEach(record => { - const meter = this.meterList.find(m => m.meterId === record.meterId) - const deviceName = meter ? meter.meterCode : `设备${record.meterId}` - deviceMap.set(deviceName, (deviceMap.get(deviceName) || 0) + (Number(record.consumption) || 0)) - }) - const sortedDevices = Array.from(deviceMap.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 10) - - const option = { - tooltip: { - trigger: 'axis', - axisPointer: { - type: 'shadow' - } - }, - grid: { - left: '3%', - right: '4%', - bottom: '3%', - containLabel: true - }, - xAxis: { - type: 'value', - name: '能耗' - }, - yAxis: { - type: 'category', - data: sortedDevices.map(item => item[0]).reverse() - }, - series: [ - { - name: '能耗', - type: 'bar', - data: sortedDevices.map(item => item[1]).reverse(), - itemStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ - { offset: 0, color: '#67C23A' }, - { offset: 1, color: '#95E67D' } - ]), - borderRadius: [0, 4, 4, 0] - } - } - ] - } - this.deviceRankingChart.setOption(option) - }, getEnergyTypeName(energyTypeId) { const type = this.energyTypeList.find(t => t.energyTypeId === energyTypeId) return type ? type.name : '未知' @@ -496,9 +425,257 @@ export default { const line = this.dict.type.sys_lines?.find(l => l.value === lineId) return line ? line.label : lineId }, + // 获取有且仅有一个主表的能源类型列表 + getValidSankeyEnergyTypes() { + const energyTypeMeterMap = {} + // 按能源类型分组所有表计 + this.meterList.forEach(meter => { + if (!energyTypeMeterMap[meter.energyTypeId]) { + energyTypeMeterMap[meter.energyTypeId] = [] + } + energyTypeMeterMap[meter.energyTypeId].push(meter) + }) + // 筛选出有且仅有一个主表的能源类型 + return this.energyTypeList.filter(type => { + const meters = energyTypeMeterMap[type.energyTypeId] || [] + const totalMeters = meters.filter(m => m.isTotalMeter === 1) + return totalMeters.length === 1 + }) + }, + // 更新桑基图 + updateSankeyChart() { + if (!this.selectedSankeyEnergyTypeId) { + // 如果没有选择能源类型,显示空图表 + this.sankeyChart.setOption({ + title: { text: '请选择能源类型' }, + series: [] + }) + return + } + + // 获取当前能源类型的所有表计 + const currentEnergyMeters = this.meterList.filter(m => m.energyTypeId === this.selectedSankeyEnergyTypeId) + const totalMeter = currentEnergyMeters.find(m => m.isTotalMeter === 1) + const subMeters = currentEnergyMeters.filter(m => m.isTotalMeter !== 1) + + if (!totalMeter) { + this.sankeyChart.setOption({ + title: { text: '当前能源类型没有主表' }, + series: [] + }) + return + } + + // 计算各节点的能耗 + const nodeMap = {} + const links = [] + const nodes = [] + + // 获取能源类型名称 + const energyType = this.energyTypeList.find(t => t.energyTypeId === this.selectedSankeyEnergyTypeId) + const energyTypeName = energyType ? energyType.name : '能源' + + // 1. 能源节点 -> 总表节点 + let totalConsumption = 0 + // 先计算总表的总消耗 + this.energyRecords.forEach(record => { + if (record.meterId === totalMeter.meterId) { + totalConsumption += Number(record.consumption) || 0 + } + }) + + // 添加能源节点 + const energyNodeName = energyTypeName + nodes.push({ name: energyNodeName }) + nodeMap[energyNodeName] = true + + // 添加总表节点 + const totalMeterName = `总表:${totalMeter.meterCode}` + nodes.push({ name: totalMeterName }) + nodeMap[totalMeterName] = true + + // 能源 -> 总表 连接 + links.push({ + source: energyNodeName, + target: totalMeterName, + value: totalConsumption + }) + + // 2. 总表 -> 分表/产线节点 + // 按产线分组分表 + const lineSubMeterMap = {} + subMeters.forEach(meter => { + const lineName = meter.productionLine ? this.getLineName(meter.productionLine) : '未分配产线' + if (!lineSubMeterMap[lineName]) { + lineSubMeterMap[lineName] = [] + } + lineSubMeterMap[lineName].push(meter) + }) + + // 为每条产线创建节点 + Object.keys(lineSubMeterMap).forEach(lineName => { + const lineMeters = lineSubMeterMap[lineName] + let lineConsumption = 0 + + // 计算产线总消耗 + lineMeters.forEach(meter => { + this.energyRecords.forEach(record => { + if (record.meterId === meter.meterId) { + lineConsumption += Number(record.consumption) || 0 + } + }) + }) + + if (lineConsumption > 0) { + // 产线节点 + nodes.push({ name: `产线:${lineName}` }) + nodeMap[`产线:${lineName}`] = true + links.push({ + source: totalMeterName, + target: `产线:${lineName}`, + value: lineConsumption + }) + + // 为产线下的每个分表创建节点 + lineMeters.forEach(meter => { + let meterConsumption = 0 + this.energyRecords.forEach(record => { + if (record.meterId === meter.meterId) { + meterConsumption += Number(record.consumption) || 0 + } + }) + if (meterConsumption > 0) { + const meterNodeName = `设备:${meter.meterCode}` + if (!nodeMap[meterNodeName]) { + nodes.push({ name: meterNodeName }) + nodeMap[meterNodeName] = true + } + links.push({ + source: `产线:${lineName}`, + target: meterNodeName, + value: meterConsumption + }) + } + }) + } + }) + + // 渲染桑基图 + const option = { + tooltip: { + trigger: 'item', + formatter: function (params) { + if (params.dataType === 'edge') { + return `${params.data.source} → ${params.data.target}
能耗: ${params.data.value.toFixed(2)}` + } else { + return `${params.name}
能耗: ${params.value ? params.value.toFixed(2) : 0}` + } + } + }, + series: [{ + type: 'sankey', + layout: 'none', + emphasis: { + focus: 'adjacency' + }, + data: nodes, + links: links, + lineStyle: { + color: 'gradient', + curveness: 0.5 + }, + label: { + color: '#333' + } + }] + } + + this.sankeyChart.setOption(option) + }, + updateLineEnergyTrendChart() { + const dateFormat = 'YYYY-MM-DD' + const groupedData = new Map() + + this.energyRecords.forEach(record => { + if (this.selectedLineAnalysisEnergyTypeId && record.energyId !== this.selectedLineAnalysisEnergyTypeId) { + return + } + + const meter = this.meterList.find(m => m.meterId === record.meterId) + if (!meter || !meter.productionLine) return + + const lines = Array.isArray(meter.productionLine) ? meter.productionLine : [meter.productionLine] + const dateKey = dayjs(record.recordDate).format(dateFormat) + + lines.forEach(lineId => { + if (this.selectedLines.length > 0 && !this.selectedLines.includes(lineId)) { + return + } + + const lineName = this.getLineName(lineId) + if (!groupedData.has(dateKey)) { + groupedData.set(dateKey, {}) + } + if (!groupedData.get(dateKey)[lineName]) { + groupedData.get(dateKey)[lineName] = 0 + } + groupedData.get(dateKey)[lineName] += Number(record.consumption) || 0 + }) + }) + + const dates = Array.from(groupedData.keys()).sort() + const lineNames = Array.from(new Set( + Array.from(groupedData.values()).flatMap(obj => Object.keys(obj)) + )).sort() + + const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#00CED1', '#00BFFF', '#32CD32'] + const series = lineNames.map((lineName, index) => ({ + name: lineName, + type: 'line', + smooth: true, + data: dates.map(date => groupedData.get(date)[lineName] || 0), + itemStyle: { color: colors[index % colors.length] }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: colors[index % colors.length] + '80' }, + { offset: 1, color: colors[index % colors.length] + '10' } + ]) + } + })) + + const selectedEnergyType = this.energyTypeList.find(t => t.energyTypeId === this.selectedLineAnalysisEnergyTypeId) + + const option = { + tooltip: { + trigger: 'axis' + }, + legend: { + data: lineNames, + top: 10 + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + top: 80, + containLabel: true + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: dates + }, + yAxis: { + type: 'value', + name: selectedEnergyType ? selectedEnergyType.name : '能耗' + }, + series + } + this.lineEnergyTrendChart.setOption(option) + }, updateTableData() { const lineEnergyMap = new Map() - + this.energyRecords.forEach(record => { const meter = this.meterList.find(m => m.meterId === record.meterId) if (meter && meter.productionLine) { @@ -506,7 +683,7 @@ export default { lines.forEach(lineId => { const lineName = this.getLineName(lineId) const energyTypeId = record.energyId - + if (!lineEnergyMap.has(lineName)) { lineEnergyMap.set(lineName, {}) } @@ -596,12 +773,15 @@ export default { &.stat-0 { border-left: 4px solid #409EFF; } + &.stat-1 { border-left: 4px solid #67C23A; } + &.stat-2 { border-left: 4px solid #E6A23C; } + &.stat-3 { border-left: 4px solid #F56C6C; } @@ -620,14 +800,17 @@ export default { background: linear-gradient(135deg, #409EFF, #00CED1); color: #ffffff; } + .stat-1 & { background: linear-gradient(135deg, #67C23A, #95E67D); color: #ffffff; } + .stat-2 & { background: linear-gradient(135deg, #E6A23C, #F7D94C); color: #ffffff; } + .stat-3 & { background: linear-gradient(135deg, #F56C6C, #FC9494); color: #ffffff; @@ -663,6 +846,8 @@ export default { } .charts-section { + margin-bottom: 20px; + .chart-card { background: #ffffff; border-radius: 8px; From b129dd8f13c5da2339b1ac8ddd0234e1c7d66801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= <2178503051@qq.com> Date: Thu, 7 May 2026 11:08:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(CoilSelector):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=A4=9A=E9=80=89=E6=A8=A1=E5=BC=8F=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E5=B9=B6=E7=A7=BB=E9=99=A4=E9=94=80=E5=94=AE=E5=8F=97=E9=99=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构多选模式界面,使用拖拽面板展示已选钢卷列表 移除销售视角受限逻辑及相关样式 调整默认分页大小为50条 --- klp-ui/src/components/CoilSelector/index.vue | 261 ++++++++++-------- .../wms/delivery/components/detailTable.vue | 2 +- 2 files changed, 146 insertions(+), 117 deletions(-) diff --git a/klp-ui/src/components/CoilSelector/index.vue b/klp-ui/src/components/CoilSelector/index.vue index fcfb86d1..c072d6b6 100644 --- a/klp-ui/src/components/CoilSelector/index.vue +++ b/klp-ui/src/components/CoilSelector/index.vue @@ -85,26 +85,90 @@ - - - - - - - - - - +
+ + + + +
+ +
- - - - - - + @@ -116,48 +180,18 @@ -
- -
- - 总净重:{{ coilTrimStatistics.total_net_weight || 0 }}t - + +
+ + 总净重:{{ coilTrimStatistics.total_net_weight || 0 }}t + - -
- - -
-
-
- 总卷数: - {{ totalCoils }} -
-
- 总净重: - {{ totalNetWeight }}t -
+
- - - - - - - - - - - - -