Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

This commit is contained in:
2026-05-07 11:20:02 +08:00
3 changed files with 523 additions and 309 deletions

View File

@@ -85,26 +85,90 @@
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-if="!salesRestricted" v-loading="loading" :data="coilList" @row-click="handleRowClick" height="400px"
style="width: 100%" :row-class-name="getRowClassName">
<!-- 自定义列 -->
<el-table-column v-for="column in renderColumns" :label="column.label" :align="column.align" :prop="column.prop"
:width="column.width" :show-overflow-tooltip="column.showOverflowTooltip" />
<el-table-column v-if="orderBy" label="表面处理" prop="surfaceTreatmentDesc"></el-table-column>
<el-table-column v-if="orderBy" label="品质" prop="qualityStatus"></el-table-column>
<el-table-column v-if="orderBy" label="切边" prop="trimmingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="镀层质量" prop="zincLayer"></el-table-column>
</el-table>
<div v-if="multiple" class="coil-selector-drag-panel">
<DragResizePanel direction="vertical" :initialSize="450" :minSize="200">
<template slot="panelA">
<div style="height: 100%; overflow-y: scroll; overflow-x: hidden;">
<!-- 数据表格 -->
<el-table v-loading="loading" :data="coilList" @row-click="handleRowClick"
style="width: 100%" :row-class-name="getRowClassName">
<!-- 自定义列 -->
<el-table-column v-for="column in renderColumns" :label="column.label" :align="column.align"
:prop="column.prop" :width="column.width" :show-overflow-tooltip="column.showOverflowTooltip" />
<el-table-column v-if="orderBy" label="表面处理" prop="surfaceTreatmentDesc"></el-table-column>
<el-table-column v-if="orderBy" label="品质" prop="qualityStatus"></el-table-column>
<el-table-column v-if="orderBy" label="切边" prop="trimmingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="镀层质量" prop="zincLayer"></el-table-column>
</el-table>
<!-- 分页 -->
<div
style="display: flex; justify-content: flex-end; align-items: flex-end; gap: 10px; padding-top: 10px;">
<span>
总净重{{ coilTrimStatistics.total_net_weight || 0 }}t
</span>
<pagination v-if="!rangeMode" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</div>
</template>
<template slot="panelB">
<div v-if="selectedCoils.length > 0" class="panel-b-container">
<div class="selected-stats">
<div class="stats-content">
<div class="stat-item">
<span class="stat-label">总卷数</span>
<span class="stat-value">{{ totalCoils }}</span>
</div>
<div class="stat-item">
<span class="stat-label">总净重</span>
<span class="stat-value">{{ totalNetWeight }}t</span>
</div>
</div>
</div>
<div class="selected-table-wrapper">
<el-table :data="selectedCoils" height="100%">
<el-table-column v-for="column in renderColumns" :label="column.label" :align="column.align"
:prop="column.prop" :width="column.width" :show-overflow-tooltip="column.showOverflowTooltip" />
<el-table-column v-if="orderBy" label="表面处理" prop="surfaceTreatmentDesc"></el-table-column>
<el-table-column v-if="orderBy" label="品质" prop="qualityStatus"></el-table-column>
<el-table-column v-if="orderBy" label="切边" prop="trimmingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="镀层质量" prop="zincLayer"></el-table-column>
<el-table-column label="操作" width="50">
<template slot-scope="scope">
<el-button size="mini" @click="handleRemove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-else class="empty-panel">
<el-descriptions title="提示" :column="1" border>
<el-descriptions-item label="说明">
请从上方表格中选择钢卷
</el-descriptions-item>
<el-descriptions-item label="操作方式">
点击上方表格行进行选择再次点击可取消选择
</el-descriptions-item>
<el-descriptions-item label="已选数量">
{{ totalCoils }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
</DragResizePanel>
</div>
<!-- 非多选模式的原始布局 -->
<div v-else>
<el-tabs type="card" v-model="currentTab" @tab-click="getList">
<el-tab-pane label="全部钢卷" name="all"></el-tab-pane>
<el-tab-pane label="我的钢卷" name="my"></el-tab-pane>
</el-tabs>
<!-- 增加 row-class-name 绑定动态设置行样式 -->
<!-- 数据表格 -->
<el-table v-loading="loading" :data="coilList" @row-click="handleRowClick" height="400px" style="width: 100%"
:row-class-name="getRowClassName">
<!-- 自定义列 -->
@@ -116,48 +180,18 @@
<el-table-column v-if="orderBy" label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="镀层质量" prop="zincLayer"></el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div style="display: flex; justify-content: flex-end; align-items: flex-end; gap: 10px;">
<span>
总净重{{ coilTrimStatistics.total_net_weight || 0 }}t
</span>
<!-- 分页 -->
<div style="display: flex; justify-content: flex-end; align-items: flex-end; gap: 10px;">
<span>
总净重{{ coilTrimStatistics.total_net_weight || 0 }}t
</span>
<pagination v-if="!rangeMode" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
<div v-if="multiple && selectedCoils.length > 0" class="selected-stats">
<div class="stats-content">
<div class="stat-item">
<span class="stat-label">总卷数</span>
<span class="stat-value">{{ totalCoils }}</span>
</div>
<div class="stat-item">
<span class="stat-label">总净重</span>
<span class="stat-value">{{ totalNetWeight }}t</span>
</div>
<pagination v-if="!rangeMode" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</div>
<el-table v-if="multiple && selectedCoils.length > 0" :data="selectedCoils">
<el-table-column v-for="column in renderColumns" :label="column.label" :align="column.align" :prop="column.prop"
:width="column.width" :show-overflow-tooltip="column.showOverflowTooltip" />
<el-table-column v-if="orderBy" label="表面处理" prop="surfaceTreatmentDesc"></el-table-column>
<el-table-column v-if="orderBy" label="品质" prop="qualityStatus"></el-table-column>
<el-table-column v-if="orderBy" label="切边" prop="trimmingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="包装" prop="packagingRequirement"></el-table-column>
<el-table-column v-if="orderBy" label="镀层质量" prop="zincLayer"></el-table-column>
<el-table-column label="操作" width="50">
<template slot-scope="scope">
<el-button size="mini" @click="handleRemove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleConfirm">确认选择</el-button>
<el-button @click="handleClose">取消</el-button>
@@ -195,6 +229,7 @@ import { defaultColumns } from './data';
import ActualWarehouseSelect from '@/components/KLPService/ActualWarehouseSelect/index.vue';
import WarehouseBirdMini from '@/views/wms/warehouse/components/WarehouseBirdMini.vue';
import DragResizeBox from '@/components/DragResizeBox/index.vue';
import DragResizePanel from '@/components/DragResizePanel/index.vue';
import OrderDetail from '@/views/crm/components/OrderDetail.vue';
export default {
@@ -205,6 +240,7 @@ export default {
ActualWarehouseSelect,
WarehouseBirdMini,
DragResizeBox,
DragResizePanel,
OrderDetail
},
dicts: ['coil_itemname', 'coil_material', 'coil_manufacturer', 'coil_quality_status'],
@@ -255,11 +291,6 @@ export default {
type: Array,
default: () => []
},
// 销售视角:钢卷受限访问
salesRestricted: {
type: Boolean,
default: false
},
// 是否根据实际库区查询钢卷
orderBy: {
type: Boolean,
@@ -308,7 +339,6 @@ export default {
dataType: 1 // 只查询当前数据,不查询历史数据
},
columns: defaultColumns,
currentTab: 'my',
selectedCoils: [],
warehouseList: [],
selectedNodeId: null,
@@ -341,9 +371,6 @@ export default {
}
}
},
currentUserId() {
return this.$store.getters.id;
},
renderColumns() {
// 如果有自定义列配置,使用它;否则使用默认列
return this.coilColumn.length > 0 ? this.coilColumn : this.columns;
@@ -469,11 +496,6 @@ export default {
classNames.push('selected-coil-row');
}
// 销售受限模式下,判断当前行是否有权限
if (this.salesRestricted && row.saleId !== this.currentUserId) {
classNames.push('disabled-coil-row'); // 禁用行类名
}
return classNames.join(' ');
},
@@ -503,11 +525,6 @@ export default {
...this.filters,
};
queryPayload.selectType = queryPayload.itemType;
// 处于销售视角且my视图时只查询当前用户的钢卷
console.log('this.salesRestricted', this.salesRestricted, this.currentTab, this.currentUserId);
if (this.salesRestricted && this.currentTab === 'my') {
queryPayload.saleId = this.currentUserId;
}
const response = await listMaterialCoil(queryPayload);
const { pageNum, pageSize, excludeBound, orderBy, ...noPager } = queryPayload;
getCoilStatisticsList(noPager).then((res) => {
@@ -555,7 +572,7 @@ export default {
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
pageSize: 50,
currentCoilNo: null,
grade: null,
dataType: 1,
@@ -567,11 +584,6 @@ export default {
// 点击表格行选择 - 新增禁用行拦截
handleRowClick(row) {
// 销售受限模式下,拦截无权限行的点击
if (this.salesRestricted && row.saleId !== this.currentUserId) {
this.$message.warning('您没有权限选择此钢卷');
return; // 终止后续逻辑
}
if (this.disableO) {
// 并且钢卷的warehouseId不是1988150210872930306
// 1988150380649967617
@@ -599,10 +611,6 @@ export default {
this.$message.warning('请选择有效的钢卷数据');
return;
}
if (this.salesRestricted && row.saleId != this.currentUserId) {
this.$message.warning('您没有权限选择此钢卷');
return;
}
if (this.multiple) {
// 检查是否已经选择
if (this.selectedCoils.some(item => item.coilId === row.coilId)) {
@@ -724,6 +732,59 @@ export default {
overflow-y: auto;
}
// 多选模式拖拽面板容器样式
.coil-selector-drag-panel {
height: 70vh;
max-height: 700px;
}
// panelA容器样式
.panel-a-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
// 表格容器样式
.table-wrapper {
flex: 1;
overflow: hidden;
}
// 分页容器样式
.pagination-wrapper {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
padding-top: 10px;
}
// panelB容器样式
.panel-b-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
// 已选表格容器样式
.selected-table-wrapper {
flex: 1;
overflow: hidden;
}
// 空面板样式
.empty-panel {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 20px;
}
::v-deep .el-table {
--el-table-row-hover-bg-color: var(--el-color-primary-light-9);
}
@@ -748,38 +809,6 @@ export default {
}
}
// 核心:禁用行样式(销售权限受限)
::v-deep .el-table .disabled-coil-row {
background-color: #f8f8f8 !important;
color: #c0c4cc !important;
cursor: not-allowed !important;
// 覆盖hover高亮
&:hover>td {
background-color: #f8f8f8 !important;
}
// 覆盖当前行高亮
&.current-row {
background-color: #f8f8f8 !important;
}
// 禁止单元格交互
td {
pointer-events: none;
user-select: none; // 禁止文本选中
}
// 当同时是已选行时(优先级:禁用 > 已选)
&.selected-coil-row {
background-color: #f5f5f5 !important;
&:hover>td {
background-color: #f5f5f5 !important;
}
}
}
.dialog-footer {
text-align: right;
}

View File

@@ -4,14 +4,8 @@
<div class="filter-panel">
<el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="时间范围">
<time-range-picker
v-model="timeRangeParams"
start-key="recordStartDate"
end-key="recordEndDate"
:default-start-time="defaultStartTime"
:default-end-time="defaultEndTime"
@quick-select="loadData"
/>
<time-range-picker v-model="timeRangeParams" start-key="recordStartDate" end-key="recordEndDate"
:default-start-time="defaultStartTime" :default-end-time="defaultEndTime" @quick-select="loadData" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-refresh" @click="loadData">刷新</el-button>
@@ -19,26 +13,58 @@
</el-form>
</div>
<!-- 统计卡片 -->
<div class="stat-cards">
<!-- 图表区域 -->
<div class="charts-section">
<!-- 产线能源类型二维表 -->
<el-row :gutter="20">
<el-col :span="6" v-for="(stat, index) in statistics" :key="index">
<div class="stat-card" :class="'stat-' + index">
<div class="stat-icon">
<i :class="stat.icon"></i>
<el-col :span="24">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">产线能源类型二维表</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
<div class="table-container">
<el-table :data="tableData" border style="width: 100%" :summary-method="getSummaries" show-summary>
<el-table-column prop="lineName" label="产线"></el-table-column>
<el-table-column v-for="type in energyTypeList" :key="type.energyTypeId"
:prop="'type_' + type.energyTypeId" :label="type.name">
<template slot-scope="scope">
{{ scope.row['type_' + type.energyTypeId] ? Number(scope.row['type_' +
type.energyTypeId]).toFixed(2)
: 0 }}
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<el-row :gutter="20">
<el-row :gutter="20" style="margin-top: 20px">
<!-- 产线能源时序分析 -->
<el-col :span="24">
<div class="chart-card">
<div class="chart-header" style="flex-wrap: wrap; gap: 12px;">
<span class="chart-title">产线能源时序分析</span>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<el-select v-model="selectedLineAnalysisEnergyTypeId" placeholder="请选择能源类型"
@change="updateLineEnergyTrendChart" style="width: 150px;">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name"
:value="item.energyTypeId">
</el-option>
</el-select>
<el-select v-model="selectedLines" multiple placeholder="请选择产线" @change="updateLineEnergyTrendChart"
style="width: 200px;">
<el-option v-for="line in productionLines" :key="line.value" :label="line.label" :value="line.value">
</el-option>
</el-select>
</div>
</div>
<div ref="lineEnergyTrendChart" class="chart-container"></div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px">
<!-- 能源消耗趋势图 -->
<el-col :span="16">
<div class="chart-card">
@@ -49,62 +75,39 @@
</div>
</el-col>
<!-- 能源类型占比 -->
<!-- 能源产线占比 -->
<el-col :span="8">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">能源类型占比</span>
<span class="chart-title">能源产线占比</span>
<el-select v-model="selectedEnergyTypeId" placeholder="请选择能源" @change="updateEnergyTypeChart"
style="width: 150px;">
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name"
:value="item.energyTypeId">
</el-option>
</el-select>
</div>
<div ref="energyTypeChart" class="chart-container"></div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px">
<!-- 产线能耗对比 -->
<el-col :span="12">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">产线能耗对比</span>
</div>
<div ref="productionLineChart" class="chart-container"></div>
</div>
</el-col>
<!-- 设备能耗排行 -->
<el-col :span="12">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">设备能耗排行 TOP 10</span>
</div>
<div ref="deviceRankingChart" class="chart-container"></div>
</div>
</el-col>
</el-row>
<!-- 产线能源类型二维表 -->
<el-row :gutter="20" style="margin-top: 20px">
<!-- 能源流向桑基图 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">产线能源类型二维表</span>
</div>
<div class="table-container">
<el-table :data="tableData" border style="width: 100%" :summary-method="getSummaries" show-summary>
<el-table-column prop="lineName" label="产线"></el-table-column>
<el-table-column v-for="type in energyTypeList" :key="type.energyTypeId"
:prop="'type_' + type.energyTypeId" :label="type.name">
<template slot-scope="scope">
{{ scope.row['type_' + type.energyTypeId] ? Number(scope.row['type_' + type.energyTypeId]).toFixed(2) : 0 }}
</template>
</el-table-column>
<el-table-column prop="total" label="合计" fixed="right" width="150">
<template slot-scope="scope">
{{ scope.row.total ? Number(scope.row.total).toFixed(2) : 0 }}
</template>
</el-table-column>
</el-table>
<div class="chart-header" style="flex-wrap: wrap; gap: 12px;">
<span class="chart-title">能源流向</span>
<el-select v-model="selectedSankeyEnergyTypeId" placeholder="请选择能源类型" @change="updateSankeyChart"
style="width: 200px;">
<el-option v-for="type in getValidSankeyEnergyTypes()" :key="type.energyTypeId" :label="type.name"
:value="type.energyTypeId">
</el-option>
</el-select>
<el-alert v-if="getValidSankeyEnergyTypes().length === 0" title="暂无可用能源类型(需要能源类型有且仅有一个主表)" type="warning"
:closable="false" size="small" style="flex: 1; max-width: 500px; margin-left: auto;"></el-alert>
</div>
<div ref="sankeyChart" class="chart-container"></div>
</div>
</el-col>
</el-row>
@@ -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}<br/>能耗: ${params.data.value.toFixed(2)}`
} else {
return `${params.name}<br/>能耗: ${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;

View File

@@ -8,7 +8,7 @@
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<coil-selector dialogWidth="1200px" :use-trigger="true" multiple @confirm="handleBatchAdd" :orderId="orderId"
<coil-selector :use-trigger="true" multiple @confirm="handleBatchAdd" :orderId="orderId"
:filters="{ selectType: 'product', status: 0, excludeBound: true, orderBy: true }" :orderBy="true" :disableO="true">
<el-button type="primary" plain icon="el-icon-plus" size="mini">批量新增</el-button>
</coil-selector>