feat(cost): 添加折线图统计功能

- 在综合成本页面添加折线图统计按钮
- 实现折线图弹窗组件和相关图表展示功能
- 集成 echarts 库进行数据可视化
- 添加图表渲染和销毁逻辑
- 实现按列数据生成对应的折线图选项
- 添加全屏图表样式和响应式布局支持
- 扩展表格数据显示逻辑以支持图表数据提取
This commit is contained in:
jhd
2026-07-09 17:38:03 +08:00
parent 25b34ec82e
commit 41ca57896b

View File

@@ -23,6 +23,7 @@
<el-button size="mini" style="float:right;margin-left:8px" @click="openColCfg">列配置</el-button> <el-button size="mini" style="float:right;margin-left:8px" @click="openColCfg">列配置</el-button>
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-money" @click="openPriceMgr">价格管理</el-button> <el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-money" @click="openPriceMgr">价格管理</el-button>
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-warning-outline" @click="anomalyOpen = true">异常分析</el-button> <el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-warning-outline" @click="anomalyOpen = true">异常分析</el-button>
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-s-data" @click="chartOpen = true">折线图统计</el-button>
<!-- <el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-upload2" @click="backfillCost" :loading="backfilling">反填</el-button> --> <!-- <el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-upload2" @click="backfillCost" :loading="backfilling">反填</el-button> -->
<span style="float:right;margin-right:12px;font-size:12px;color:#606266;display:flex;align-items:center"> <span style="float:right;margin-right:12px;font-size:12px;color:#606266;display:flex;align-items:center">
<span style="margin-right:4px">{{ inputMode ? '录入' : '查看' }}</span> <span style="margin-right:4px">{{ inputMode ? '录入' : '查看' }}</span>
@@ -334,6 +335,16 @@
<!-- Anomaly analysis --> <!-- Anomaly analysis -->
<AnomalyAnalysis :visible.sync="anomalyOpen" :reportId="activeReport ? activeReport.reportId : null" :allItems="allItems" :gridRows="gridRows" :allCols="allCols" @anomaly-found="onAnomalyFound" /> <AnomalyAnalysis :visible.sync="anomalyOpen" :reportId="activeReport ? activeReport.reportId : null" :allItems="allItems" :gridRows="gridRows" :allCols="allCols" @anomaly-found="onAnomalyFound" />
<!-- Line chart -->
<el-dialog title="折线图统计" :visible.sync="chartOpen" width="100%" top="0" append-to-body :close-on-click-modal="false" custom-class="chart-fullscreen" @opened="renderCharts" @closed="disposeCharts">
<div class="chart-grid" v-if="chartDataCols.length">
<div v-for="(col, idx) in chartDataCols" :key="idx" class="chart-item">
<div :ref="'chart_'+idx" style="width:100%;height:280px"></div>
</div>
</div>
<el-empty v-else description="暂无数据" />
</el-dialog>
</template> </template>
</div> </div>
</div> </div>
@@ -352,6 +363,7 @@ import { listProductionLine } from "@/api/wms/productionLine"
import { listRollGrindAll } from "@/api/mes/roll/rollGrind" import { listRollGrindAll } from "@/api/mes/roll/rollGrind"
import { listEnergyRecord, addEnergyRecord, updateEnergyRecord } from "@/api/ems/energyRecord" import { listEnergyRecord, addEnergyRecord, updateEnergyRecord } from "@/api/ems/energyRecord"
import AnomalyAnalysis from "./components/AnomalyAnalysis" import AnomalyAnalysis from "./components/AnomalyAnalysis"
import * as echarts from 'echarts'
function parseDateRange(detailDate) { function parseDateRange(detailDate) {
const d = (detailDate || '').slice(0, 10) const d = (detailDate || '').slice(0, 10)
@@ -485,7 +497,8 @@ export default {
priceOpen: false, priceList: [], priceSaving: false, priceOpen: false, priceList: [], priceSaving: false,
backfilling: false, backfilling: false,
progressOpen: false, progressTitle: '', progressTasks: [], progressOpen: false, progressTitle: '', progressTasks: [],
anomalyOpen: false, anomalyMap: {} anomalyOpen: false, anomalyMap: {},
chartOpen: false, _chartInstances: []
} }
}, },
computed: { computed: {
@@ -575,6 +588,17 @@ export default {
}, },
tableData() { tableData() {
return [...this.summaryRows, ...this.gridRows] return [...this.summaryRows, ...this.gridRows]
},
chartDataCols() {
if (!this.gridRows.length) return []
return this.displayCols.filter(col => {
if (col.$type === 'detail') {
const key = 'q' + col.itemId
return this.gridRows.some(r => r[key] != null)
}
const key = 'mv' + col.mIdx
return this.gridRows.some(r => r[key] != null)
})
} }
}, },
watch: { configOpen(v) { if (!v) this.rpOpen = false } }, watch: { configOpen(v) { if (!v) this.rpOpen = false } },
@@ -1173,6 +1197,60 @@ export default {
if (!set) return false if (!set) return false
return set.has(String(itemId)) return set.has(String(itemId))
}, },
/* charts */
buildChartOption(col) {
const rows = this.gridRows.filter(r => r.detailDate).sort((a,b) => a.detailDate.localeCompare(b.detailDate))
const dates = rows.map(r => r.detailDate.slice(0,10))
const makeOption = (title, series) => ({
title: { text: title, left: 'center', textStyle: { fontSize: 12 } },
tooltip: { trigger: 'axis' },
legend: series.length > 1 ? { top: 'bottom', textStyle: { fontSize: 11 } } : undefined,
xAxis: { type: 'category', data: dates, axisLabel: { rotate: 30, fontSize: 10 } },
yAxis: { type: 'value', axisLabel: { fontSize: 10 } },
grid: { left: 50, right: 16, top: 36, bottom: 40 },
series
})
if (col.$type === 'detail') {
const label = (col.itemName || col.itemCode) + (col.unit ? '('+col.unit+')' : '')
if (col.isShift) {
return makeOption(label, [
{ name: '甲班', type: 'line', data: rows.map(r => { const v = parseFloat(r['q'+col.itemId+'_1']); return isNaN(v) ? 0 : v }), smooth: true },
{ name: '乙班', type: 'line', data: rows.map(r => { const v = parseFloat(r['q'+col.itemId+'_2']); return isNaN(v) ? 0 : v }), smooth: true }
])
}
return makeOption(label, [{ type: 'line', data: rows.map(r => { const v = parseFloat(r['q'+col.itemId]); return isNaN(v) ? 0 : v }), smooth: true }])
}
// metric
const label = (col.metricName || '') + (col.unit ? '('+col.unit+')' : '')
if (col.isShift) {
return makeOption(label, [
{ name: '甲班', type: 'line', data: rows.map(r => { const v = parseFloat(r['mv'+col.mIdx+'_1']); return isNaN(v) ? 0 : v }), smooth: true },
{ name: '乙班', type: 'line', data: rows.map(r => { const v = parseFloat(r['mv'+col.mIdx+'_2']); return isNaN(v) ? 0 : v }), smooth: true }
])
}
return makeOption(label, [{ type: 'line', data: rows.map(r => { const v = parseFloat(r['mv'+col.mIdx]); return isNaN(v) ? 0 : v }), smooth: true }])
},
renderCharts() {
this.disposeCharts()
this.$nextTick(() => {
this._chartInstances = []
this.chartDataCols.forEach((col, idx) => {
const el = this.$refs['chart_'+idx]
if (!el || !el[0]) return
const chart = echarts.init(el[0])
chart.setOption(this.buildChartOption(col))
this._chartInstances.push(chart)
})
})
},
disposeCharts() {
if (this._chartInstances) {
this._chartInstances.forEach(c => c.dispose())
this._chartInstances = []
}
},
async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } }, async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } },
async loadAllMetrics(rid) { async loadAllMetrics(rid) {
const q = { pageNum:1, pageSize:99999 }; if (rid) q.reportId = rid const q = { pageNum:1, pageSize:99999 }; if (rid) q.reportId = rid
@@ -1231,4 +1309,9 @@ export default {
/deep/ .summary-row td .cell { color: #303133; } /deep/ .summary-row td .cell { color: #303133; }
.summary-label { font-weight: bold; color: #303133; padding: 0 5px; } .summary-label { font-weight: bold; color: #303133; padding: 0 5px; }
.summary-val { font-weight: bold; color: #303133; } .summary-val { font-weight: bold; color: #303133; }
/deep/ .chart-fullscreen { width: 100% !important; max-width: 100% !important; height: 100vh !important; margin: 0 !important; }
/deep/ .chart-fullscreen .el-dialog__body { height: calc(100vh - 90px); overflow: auto; padding: 16px 20px; }
/deep/ .chart-fullscreen .el-dialog__header { padding: 12px 20px; }
.chart-grid { display: flex; flex-wrap: wrap; }
.chart-item { width: 33.33%; padding: 6px; box-sizing: border-box; }
</style> </style>