新增app和跟踪页面,现已经调通

This commit is contained in:
2026-05-13 16:43:38 +08:00
parent 5fdaa89afd
commit ba7593e825
10 changed files with 1838 additions and 2 deletions

View File

@@ -474,6 +474,30 @@ public class SqlServerApiClient {
);
}
public ExecuteSqlResponse queryMatMap() {
return executeSql(
"oracle",
"select AREAID, GROUPID, POS, FULLPLACENAME, CREMATIDTYPE, MATIDTYPE, MATID, SUBPART, UPDMASTER, DISPLAYNAME, L1MAPIDX, UPDTIME from JXPLTCM.ROMTB_MATMAP order by L1MAPIDX",
emptyParams()
);
}
public ExecuteSqlResponse queryEntryTrace() {
return executeSql(
"oracle",
"select * from JXPLTCM.V_PLTCM_ENTRY_TRACE order by L1MAPIDX",
emptyParams()
);
}
public ExecuteSqlResponse queryExitTrace() {
return executeSql(
"oracle",
"select * from JXPLTCM.V_PLTCM_EXIT_TRACE order by L1MAPIDX",
emptyParams()
);
}
private org.springframework.util.MultiValueMap<String, String> convertToQueryParams(Map<String, ?> queryParams) {
org.springframework.util.LinkedMultiValueMap<String, String> multiValueMap = new org.springframework.util.LinkedMultiValueMap<>();
if (queryParams == null || queryParams.isEmpty()) {

View File

@@ -126,6 +126,34 @@ public class SqlServerApiBusinessService {
return client.queryShapeByMatId(matId);
}
/**
* 跟踪数据ROMTB_MATMAP + V_PLTCM_ENTRY_TRACE + V_PLTCM_EXIT_TRACE 一次返回。
*/
public TrackDataView getTrackData() {
List<Map<String, Object>> matMapRows = asRowList(client.queryMatMap());
List<Map<String, Object>> entryRows = asRowList(client.queryEntryTrace());
List<Map<String, Object>> exitRows = asRowList(client.queryExitTrace());
return new TrackDataView(matMapRows, entryRows, exitRows);
}
public static class TrackDataView {
private final List<Map<String, Object>> matMap;
private final List<Map<String, Object>> entryTrace;
private final List<Map<String, Object>> exitTrace;
public TrackDataView(List<Map<String, Object>> matMap,
List<Map<String, Object>> entryTrace,
List<Map<String, Object>> exitTrace) {
this.matMap = matMap;
this.entryTrace = entryTrace;
this.exitTrace = exitTrace;
}
public List<Map<String, Object>> getMatMap() { return matMap; }
public List<Map<String, Object>> getEntryTrace() { return entryTrace; }
public List<Map<String, Object>> getExitTrace() { return exitTrace; }
}
/**
* 出口卷实绩列表(分页),来自 PLTCM_PDO_EXCOIL。
*/

View File

@@ -183,6 +183,14 @@ public class SqlServerApiController {
return R.ok(businessService.getPresetSetupByCoilId(coilId));
}
/**
* 跟踪数据matMap + entryTrace + exitTrace 一次返回。
*/
@GetMapping("/track")
public R<SqlServerApiBusinessService.TrackDataView> trackData() {
return R.ok(businessService.getTrackData());
}
/**
* 换辊历史总条数。
*/

View File

@@ -143,6 +143,14 @@ export function getExcoilCount() {
})
}
// 跟踪数据matMap + entryTrace + exitTrace
export function getTrackData() {
return request({
url: '/sql-server-api/track',
method: 'get'
})
}
// 工艺预设参数按计划钢卷号查询PLTCM_PRESET_SETUP
export function getPresetSetupByCoilId(coilId) {
return request({

View File

@@ -1,4 +1,5 @@
<template>
<div>
<div class="actual-container">
<!-- 顶部实绩列表 (PLTCM_PDO_EXCOIL) -->
<div class="top-section">
@@ -74,6 +75,11 @@
<el-tag type="primary" size="mini" effect="plain">{{ row.STATUS || row.status || '产出' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="" width="64" align="center" fixed="right">
<template slot-scope="{ row }">
<el-button type="text" size="mini" style="color:#2c5282;font-weight:600" @click.stop="openQualityReport(row)">质保书</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
@@ -162,7 +168,7 @@
</el-tabs>
</div>
<!-- 右侧查找面板 -->
<!-- 右侧查找面板 + 质保书入口 -->
<div class="search-panel">
<div class="panel-title">查找</div>
<div class="search-type-group">
@@ -194,11 +200,15 @@
</div>
</div>
</div>
<quality-report-dialog ref="qualityReport" />
</div>
</template>
<script>
import * as echarts from 'echarts'
import 'echarts-gl'
import QualityReportDialog from './QualityReportDialog.vue'
import {
getExcoilList,
getExcoilCount,
@@ -360,6 +370,7 @@ function xLocData(rows) {
export default {
name: 'ActualPerformance',
components: { QualityReportDialog },
data() {
return {
excoilLoading: false,
@@ -1012,6 +1023,17 @@ export default {
return items
},
openQualityReport(row) {
// If the clicked row is already the selected row, use in-memory data
const isSame = this.selectedRow &&
(row.EXCOILID || row.excoilid) === (this.selectedRow.EXCOILID || this.selectedRow.excoilid)
const segData = isSame ? this.segData : null
const gaugeRows = isSame ? this.gaugeRows : null
const shapeRows = isSame ? this.shapeRows : null
const presetData = isSame ? this.presetData : null
this.$refs.qualityReport.open(row, segData, gaugeRows, shapeRows, presetData)
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)

View File

@@ -0,0 +1,631 @@
<template>
<el-dialog
title="产品质量报表"
:visible.sync="visible"
width="900px"
append-to-body
:close-on-click-modal="false"
@opened="onOpened"
@close="onClose"
>
<div class="rpt-toolbar">
<span class="rpt-coil-hint">{{ coilLabel }}</span>
<el-button size="small" type="primary" icon="el-icon-download" :loading="exporting" @click="exportPdf">导出 PDF</el-button>
</div>
<div ref="reportContent" class="report-body">
<!-- 页眉 -->
<div class="rpt-header">
<div class="rpt-header-left">
<img src="~@/assets/logo/logo.png" class="rpt-logo" alt="KLP" />
</div>
<div class="rpt-title">产品质量报表</div>
<div class="rpt-header-right">
<div class="brand-name">科伦普</div>
<div class="brand-sub">KE LUN PU</div>
</div>
</div>
<!-- 钢卷规格 -->
<div class="section-bar">钢卷规格</div>
<table class="rpt-table">
<tbody>
<tr>
<td class="lbl">钢卷号</td>
<td class="val bold" colspan="3">{{ rv('EXCOILID') || rv('HOT_COILID') }}</td>
<td class="lbl">来料厚度[mm]</td><td class="val">{{ rv('ENTRY_THICK') }}</td>
<td class="lbl">来料宽度[mm]</td><td class="val">{{ rv('ENTRY_WIDTH') }}</td>
<td class="lbl">来料重量[t]</td><td class="val">{{ rv('USED_ENTRY_WEIGHT') || rv('ENTRY_WEIGHT') }}</td>
</tr>
<tr>
<td class="lbl">钢种</td>
<td class="val" colspan="3">{{ rv('ORDER_QUALITY') || rv('GRADE') }}</td>
<td class="lbl">成品厚度[mm]</td><td class="val">{{ rv('EXIT_THICK') }}</td>
<td class="lbl">成品宽度[mm]</td><td class="val">{{ rv('EXIT_WIDTH') }}</td>
<td class="lbl">成品重量[t]</td><td class="val">{{ rv('MEAS_EXIT_WEIGHT') }}</td>
</tr>
<tr>
<td class="lbl">班组</td>
<td class="val" colspan="3">{{ shiftLabel }}</td>
<td class="lbl">偏差上限[mm]</td><td class="val">{{ rv('EXIT_POS_DEV') }}</td>
<td class="lbl">压下率[%]</td><td class="val">{{ reductionRate }}</td>
<td class="lbl">成品长度[m]</td><td class="val">{{ rv('EXIT_LENGTH') }}</td>
</tr>
<tr>
<td class="lbl">下工序</td>
<td class="val" colspan="3">{{ rv('PARK_TYPE') || rv('SIDE_TRIM') }}</td>
<td class="lbl">偏差下限[mm]</td><td class="val">{{ rv('EXIT_NEG_DEV') }}</td>
<td class="lbl">热卷号</td><td class="val" colspan="3">{{ rv('HOT_COILID') }}</td>
</tr>
<tr>
<td class="lbl">生产时间</td>
<td class="val" colspan="4">{{ fmtDate(row && (row.END_DATE || row.end_date)) }}</td>
<td class="lbl">生产时长</td>
<td class="val" colspan="4"></td>
</tr>
</tbody>
</table>
<!-- 质量判定 -->
<div class="section-bar">质量判定</div>
<table class="rpt-table quality-table">
<thead>
<tr>
<th>轧制总长[m]</th>
<th>头尾超差[m]</th>
<th>厚度合格率[%]</th>
<th>目标板形</th>
<th>板形合格率[%]</th>
<th>板形质量</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ rolledLength }}</td>
<td></td>
<td>{{ thickPassRate }}</td>
<td>平直</td>
<td>{{ shapePassRate }}</td>
<td :class="qualityClass">{{ qualityLabel }}</td>
</tr>
</tbody>
</table>
<!-- 成品厚度偏差 -->
<template v-if="hasGauge">
<div class="chart-title">
成品厚度偏差
<span class="chart-subtitle">产出厚度 - {{ rv('EXIT_THICK') }} [mm]</span>
</div>
<div ref="chartThick1" class="rpt-chart" />
<div class="chart-title">末架出口厚度 [mm]</div>
<div ref="chartThick4" class="rpt-chart" />
</template>
<!-- 速度趋势 -->
<template v-if="hasSeg">
<div class="chart-title">速度趋势 [m/min]</div>
<div ref="chartSpeed" class="rpt-chart" />
<div class="chart-title">张力趋势 [N/mm²]</div>
<div ref="chartTension" class="rpt-chart" />
<template v-if="hasSegField('TLMESH1') || hasSegField('TLMESH2')">
<div class="chart-title">拉矫机插入量 [mm]</div>
<div ref="chartMesh" class="rpt-chart rpt-chart-sm" />
</template>
<template v-if="hasSegField('TK1TEMP') || hasSegField('TK2TEMP')">
<div class="chart-title">酸洗槽温度 []</div>
<div ref="chartTemp" class="rpt-chart rpt-chart-sm" />
</template>
</template>
<!-- 板形曲线 -->
<template v-if="hasShape">
<div class="chart-title">总板形偏差 [IU]</div>
<div ref="chartFlatDev" class="rpt-chart" />
<div class="chart-title">弯辊力 [kN]</div>
<div ref="chartBend" class="rpt-chart rpt-chart-sm" />
<div class="chart-title">板形热力图</div>
<div ref="chartHeatmap" class="rpt-chart rpt-chart-heat" />
</template>
<!-- 页脚 -->
<div class="rpt-footer">生成时间{{ nowStr }}</div>
</div>
</el-dialog>
</template>
<script>
import * as echarts from 'echarts'
function getV(row, col) {
if (!row) return null
return row[col] !== undefined ? row[col] : row[col.toLowerCase()]
}
function getRowVal(row, col) {
const v = getV(row, col)
return v == null ? null : Number(v)
}
function xLoc(rows) {
return rows.map(r => {
const v = getRowVal(r, 'XLOCATION')
return v == null ? '' : v.toFixed(1)
})
}
function colData(rows, col) {
return rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
}
function segArr(seg, col) {
if (!seg) return []
const arr = seg[col] !== undefined ? seg[col] : (seg[col.toLowerCase()] || [])
return arr.map(v => v == null ? null : parseFloat(Number(v).toFixed(3)))
}
function segX(seg) {
const arr = seg['STARTPOS'] !== undefined ? seg['STARTPOS'] : (seg['startpos'] || seg['XLOCATION'] || seg['xlocation'] || [])
return arr.map(v => v == null ? '' : Number(v).toFixed(1))
}
function calcYRange(vals) {
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
if (!nums.length) return {}
const min = Math.min(...nums), max = Math.max(...nums)
if (min === max) {
const base = Math.abs(min) || 1
return { min: parseFloat((min - base * 0.2).toFixed(4)), max: parseFloat((max + base * 0.2).toFixed(4)) }
}
const pad = (max - min) * 0.15
return { min: parseFloat((min - pad).toFixed(4)), max: parseFloat((max + pad).toFixed(4)) }
}
function lineOpt(title, xData, series, yName) {
const allVals = series.flatMap(s => s.data)
const range = calcYRange(allVals)
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal', color: '#1a365d' }, top: 4, left: 8 },
legend: { top: 4, right: 6, textStyle: { fontSize: 9 }, itemWidth: 12, itemHeight: 8 },
tooltip: { trigger: 'axis', confine: true },
grid: { top: 38, bottom: 28, left: 10, right: 10, containLabel: true },
xAxis: { type: 'category', data: xData, name: '带钢长度[m]', nameTextStyle: { fontSize: 9 }, axisLabel: { fontSize: 9, interval: 'auto' } },
yAxis: { type: 'value', name: yName, nameTextStyle: { fontSize: 9 }, axisLabel: { fontSize: 9 }, min: range.min, max: range.max },
series: series.map((s, i) => ({
name: s.name, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: s.dash ? 1 : 1.5, type: s.dash ? 'dashed' : 'solid', color: s.color },
data: s.data, z: s.dash ? 2 : 3
}))
}
}
const PALETTE = ['#409EFF', '#E6A23C', '#67C23A', '#F56C6C', '#9B59B6', '#1ABC9C']
export default {
name: 'QualityReportDialog',
data() {
return {
visible: false,
exporting: false,
row: null,
segData: null,
gaugeRows: null,
shapeRows: null,
presetData: null,
nowStr: '',
chartInsts: []
}
},
computed: {
coilLabel() {
const id = this.row ? (getV(this.row, 'EXCOILID') || getV(this.row, 'HOT_COILID') || '') : ''
return id ? `钢卷号:${id}` : ''
},
shiftLabel() {
const s = this.row ? (getV(this.row, 'SHIFT') || '') : ''
const c = this.row ? (getV(this.row, 'CREW') || '') : ''
return [s, c].filter(Boolean).join('') || '—'
},
reductionRate() {
const e = parseFloat(this.row?.ENTRY_THICK || this.row?.entry_thick)
const x = parseFloat(this.row?.EXIT_THICK || this.row?.exit_thick)
if (!e || !x || e <= 0) return '—'
return ((e - x) / e * 100).toFixed(2)
},
rolledLength() {
const el = getV(this.row, 'EXIT_LENGTH')
if (el != null) return el
if (this.gaugeRows?.length) {
const max = Math.max(...this.gaugeRows.map(r => getRowVal(r, 'XLOCATION') || 0))
if (max > 0) return max.toFixed(1)
}
return '—'
},
thickPassRate() {
if (!this.gaugeRows?.length) return '—'
const posLim = parseFloat(getV(this.row, 'EXIT_POS_DEV')) || 0.03
const negLim = Math.abs(parseFloat(getV(this.row, 'EXIT_NEG_DEV')) || 0.03)
const target = parseFloat(getV(this.row, 'EXIT_THICK'))
if (!target) return '—'
const total = this.gaugeRows.length
const pass = this.gaugeRows.filter(r => {
const v = getRowVal(r, 'THICK4') || getRowVal(r, 'THICK1')
if (v == null) return true
const dev = v - target
return dev <= posLim && dev >= -negLim
}).length
return ((pass / total) * 100).toFixed(2)
},
shapePassRate() {
if (!this.shapeRows?.length) return '—'
const total = this.shapeRows.length
const pass = this.shapeRows.filter(r => {
const v = getRowVal(r, 'ABSDEVIATION')
return v == null || Math.abs(v) <= 10
}).length
return ((pass / total) * 100).toFixed(2)
},
qualityLabel() {
const q = getV(this.row, 'QUALITY')
if (q) return q
const tp = parseFloat(this.thickPassRate)
if (!isNaN(tp)) return tp >= 99 ? '合格' : '不合格'
return '—'
},
qualityClass() {
return this.qualityLabel === '合格' ? 'cell-ok' : this.qualityLabel === '不合格' ? 'cell-ng' : ''
},
hasSeg() { return !!this.segData && Object.keys(this.segData).length > 0 },
hasGauge() { return !!this.gaugeRows?.length },
hasShape() { return !!this.shapeRows?.length }
},
methods: {
rv(col) { const v = getV(this.row, col); return v != null && v !== '' ? v : '—' },
hasSegField(col) { return !!(this.segData && (this.segData[col]?.length || this.segData[col.toLowerCase()]?.length)) },
fmtDate(v) { if (!v) return '—'; return String(v).replace('T', ' ').substring(0, 19) },
open(row, segData, gaugeRows, shapeRows, presetData) {
this.row = row
this.segData = segData || null
this.gaugeRows = gaugeRows || null
this.shapeRows = shapeRows || null
this.presetData = presetData || null
this.visible = true
},
// @opened fires AFTER the dialog's CSS transition completes — DOM is fully painted
onOpened() {
this.nowStr = new Date().toLocaleString('zh-CN').replace(/\//g, '-')
this.$nextTick(() => this.renderAll())
},
onClose() {
this.disposeCharts()
},
disposeCharts() {
this.chartInsts.forEach(c => { try { c.dispose() } catch (_) {} })
this.chartInsts = []
},
// Pass explicit pixel size so echarts doesn't measure a zero-height el
initChart(refName, height) {
const el = this.$refs[refName]
if (!el) return null
const w = el.offsetWidth || 840
const h = height || el.offsetHeight || 180
const c = echarts.init(el, null, { renderer: 'canvas', width: w, height: h })
this.chartInsts.push(c)
return c
},
renderAll() {
this.disposeCharts()
if (this.hasGauge) this.renderGaugeCharts()
if (this.hasSeg) this.renderSegCharts()
if (this.hasShape) this.renderShapeCharts()
},
renderGaugeCharts() {
const rows = this.gaugeRows
const x = xLoc(rows)
const target = parseFloat(getV(this.row, 'EXIT_THICK'))
const posLim = parseFloat(getV(this.row, 'EXIT_POS_DEV')) || 0.03
const negLim = Math.abs(parseFloat(getV(this.row, 'EXIT_NEG_DEV'))) || 0.03
// THICK1 偏差图 (1架出口 vs 目标)
const t1 = colData(rows, 'THICK1')
const t1Ref = colData(rows, 'THICK1REF')
if (t1.some(v => v != null)) {
const c = this.initChart('chartThick1', 180)
if (c) {
const series = [{ name: '1架出口厚度', data: t1, color: PALETTE[0] }]
if (t1Ref.some(v => v != null)) series.push({ name: '目标', data: t1Ref, color: '#909399', dash: true })
if (target) {
const up = rows.map(() => parseFloat((target + posLim).toFixed(4)))
const lo = rows.map(() => parseFloat((target - negLim).toFixed(4)))
series.push({ name: '上限', data: up, color: '#F56C6C', dash: true })
series.push({ name: '下限', data: lo, color: '#67C23A', dash: true })
}
c.setOption(lineOpt('1架出口厚度 [mm]', x, series, 'mm'))
}
}
// THICK4 末架出口
const t4 = colData(rows, 'THICK4')
const t4Ref = colData(rows, 'THICK4REF')
if (t4.some(v => v != null)) {
const c = this.initChart('chartThick4', 180)
if (c) {
const series = [{ name: '末架出口厚度', data: t4, color: PALETTE[0] }]
if (t4Ref.some(v => v != null)) series.push({ name: '目标', data: t4Ref, color: '#909399', dash: true })
if (target) {
const up = rows.map(() => parseFloat((target + posLim).toFixed(4)))
const lo = rows.map(() => parseFloat((target - negLim).toFixed(4)))
series.push({ name: '上限', data: up, color: '#F56C6C', dash: true })
series.push({ name: '下限', data: lo, color: '#67C23A', dash: true })
}
c.setOption(lineOpt('末架出口厚度 [mm]', x, series, 'mm'))
}
}
},
renderSegCharts() {
const seg = this.segData
const x = segX(seg)
// 速度
const speedSeries = [
this.hasSegField('PLSPEED') && { name: '酸洗速度', data: segArr(seg,'PLSPEED'), color: PALETTE[0] },
this.hasSegField('TRIMSPEED') && { name: '圆盘剪速度', data: segArr(seg,'TRIMSPEED'), color: PALETTE[1] },
this.hasSegField('MILLEXITSPEED') && { name: '轧机出口速度', data: segArr(seg,'MILLEXITSPEED'), color: PALETTE[2] },
this.hasSegField('MILLENTRYSPEED')&& { name: '轧机入口速度', data: segArr(seg,'MILLENTRYSPEED'),color: PALETTE[3] },
this.hasSegField('PORSPEED') && { name: '开卷速度', data: segArr(seg,'PORSPEED'), color: PALETTE[4] }
].filter(Boolean)
if (speedSeries.length) {
const c = this.initChart('chartSpeed', 180)
if (c) c.setOption(lineOpt('速度趋势', x, speedSeries, 'm/min'))
}
// 张力
const tensSeries = [
this.hasSegField('PLTENS') && { name: '酸洗张力', data: segArr(seg,'PLTENS'), color: PALETTE[0] },
this.hasSegField('ENLTENS') && { name: '入口活套张力', data: segArr(seg,'ENLTENS'), color: PALETTE[1] },
this.hasSegField('CXLTENS') && { name: '出口活套张力', data: segArr(seg,'CXLTENS'), color: PALETTE[2] },
this.hasSegField('PORTENS') && { name: '开卷张力', data: segArr(seg,'PORTENS'), color: PALETTE[3] },
this.hasSegField('TRIMTENS') && { name: '圆盘剪张力', data: segArr(seg,'TRIMTENS'), color: PALETTE[4] }
].filter(Boolean)
if (tensSeries.length) {
const c = this.initChart('chartTension', 180)
if (c) c.setOption(lineOpt('张力趋势', x, tensSeries, 'N/mm²'))
}
// 拉矫插入量
if (this.hasSegField('TLMESH1') || this.hasSegField('TLMESH2')) {
const meshSeries = [
this.hasSegField('TLMESH1') && { name: '1#插入量', data: segArr(seg,'TLMESH1'), color: PALETTE[0] },
this.hasSegField('TLMESH2') && { name: '2#插入量', data: segArr(seg,'TLMESH2'), color: PALETTE[1] },
this.hasSegField('TLMESH3') && { name: '3#插入量', data: segArr(seg,'TLMESH3'), color: PALETTE[2] },
this.hasSegField('TLELONG') && { name: '延伸率', data: segArr(seg,'TLELONG'), color: PALETTE[3] }
].filter(Boolean)
const c = this.initChart('chartMesh', 140)
if (c) c.setOption(lineOpt('拉矫机', x, meshSeries, 'mm/%'))
}
// 酸洗温度
if (this.hasSegField('TK1TEMP') || this.hasSegField('TK2TEMP')) {
const tempSeries = [
this.hasSegField('TK1TEMP') && { name: '1#酸槽', data: segArr(seg,'TK1TEMP'), color: PALETTE[0] },
this.hasSegField('TK2TEMP') && { name: '2#酸槽', data: segArr(seg,'TK2TEMP'), color: PALETTE[1] },
this.hasSegField('TK3TEMP') && { name: '3#酸槽', data: segArr(seg,'TK3TEMP'), color: PALETTE[2] },
this.hasSegField('RINSETEMP') && { name: '漂洗', data: segArr(seg,'RINSETEMP'), color: PALETTE[3] }
].filter(Boolean)
const c = this.initChart('chartTemp', 140)
if (c) c.setOption(lineOpt('酸洗槽温度', x, tempSeries, '℃'))
}
},
renderShapeCharts() {
const rows = this.shapeRows
const x = xLoc(rows)
// 总板形偏差
const devData = colData(rows, 'ABSDEVIATION')
if (devData.some(v => v != null)) {
const c = this.initChart('chartFlatDev', 180)
if (c) c.setOption(lineOpt('总板形偏差', x, [{ name: '板形偏差', data: devData, color: PALETTE[0] }], 'IU'))
}
// 弯辊力
const bendSeries = [
colData(rows, 'WRBEND').some(v => v != null) && { name: '工作辊弯辊力', data: colData(rows, 'WRBEND'), color: PALETTE[0] },
colData(rows, 'IRBEND').some(v => v != null) && { name: '中间辊弯辊力', data: colData(rows, 'IRBEND'), color: PALETTE[1] }
].filter(Boolean)
if (bendSeries.length) {
const c = this.initChart('chartBend', 140)
if (c) c.setOption(lineOpt('弯辊力', x, bendSeries, 'kN'))
}
// 板形热力图 (2D heatmap)
this.renderHeatmap(rows, x)
},
renderHeatmap(rows, xLabels) {
const el = this.$refs.chartHeatmap
if (!el) return
const firstRow = rows[0]
const highId = parseInt(getRowVal(firstRow, 'HIGHZONEID')) || 26
const lowId = parseInt(getRowVal(firstRow, 'LOWZONEID')) || 1
const numZ = Math.min(Math.max(highId - lowId + 1, 1), 26)
const zoneCols = Array.from({ length: numZ }, (_, i) => `VALUES${String(lowId + i).padStart(2, '0')}`)
// 降采样 X: 最多 300 点
const step = Math.max(1, Math.floor(rows.length / 300))
const sampled = rows.filter((_, i) => i % step === 0)
let minV = Infinity, maxV = -Infinity
const data = []
sampled.forEach((row, xi) => {
zoneCols.forEach((col, yi) => {
const v = getRowVal(row, col)
if (v != null) {
data.push([xi, yi, parseFloat(v.toFixed(2))])
if (v < minV) minV = v
if (v > maxV) maxV = v
}
})
})
if (!isFinite(minV)) { minV = -20; maxV = 20 }
const absMax = Math.max(Math.abs(minV), Math.abs(maxV))
const xSampleLabels = sampled.map(r => {
const v = getRowVal(r, 'XLOCATION')
return v == null ? '' : v.toFixed(0)
})
// Show ~8 labels
const labelInterval = Math.max(1, Math.floor(sampled.length / 8))
const w = el.offsetWidth || 840
const c = echarts.init(el, null, { renderer: 'canvas', width: w, height: 200 })
this.chartInsts.push(c)
c.setOption({
title: { text: '板形热力图', textStyle: { fontSize: 12, fontWeight: 'normal', color: '#1a365d' }, top: 4, left: 8 },
tooltip: {
formatter: p => `位置: ${xSampleLabels[p.data[0]]} m<br/>通道: ${lowId + p.data[1]}<br/>值: ${p.data[2]} IU`
},
grid: { top: 36, bottom: 40, left: 40, right: 80, containLabel: false },
xAxis: {
type: 'category',
data: xSampleLabels,
name: '带钢长度[m]',
nameLocation: 'middle',
nameGap: 22,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 9, interval: labelInterval }
},
yAxis: {
type: 'category',
data: Array.from({ length: numZ }, (_, i) => String(lowId + i)),
name: '通道',
nameTextStyle: { fontSize: 9 },
axisLabel: { fontSize: 8, interval: Math.max(0, Math.floor(numZ / 8)) }
},
visualMap: {
min: -absMax, max: absMax,
calculable: false,
orient: 'vertical',
right: 4, top: 36, bottom: 40,
textStyle: { fontSize: 9 },
inRange: {
color: ['#8B0000','#CC2200','#F46D43','#FDAE61','#FEE08B',
'#FFFFBF','#D9EF8B','#A6D96A','#66BD63','#1A9850','#006837']
}
},
series: [{
type: 'heatmap',
data,
emphasis: { itemStyle: { borderColor: '#333', borderWidth: 1 } }
}]
})
this.chartInsts.push(c)
},
async exportPdf() {
this.exporting = true
try {
const [html2canvas, { jsPDF }] = await Promise.all([
import('html2canvas').then(m => m.default),
import('jspdf')
])
const el = this.$refs.reportContent
const canvas = await html2canvas(el, { scale: 2, useCORS: true, allowTaint: true, backgroundColor: '#fff', logging: false })
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' })
const pW = pdf.internal.pageSize.getWidth()
const pH = pdf.internal.pageSize.getHeight()
const mg = 8
const printW = pW - mg * 2
const ratio = canvas.width / printW
const imgH = canvas.height / ratio
let rem = imgH, srcY = 0
while (rem > 0) {
const sliceH = Math.min(pH - mg * 2, rem)
const slicePx = sliceH * ratio
const sc = document.createElement('canvas')
sc.width = canvas.width; sc.height = slicePx
sc.getContext('2d').drawImage(canvas, 0, srcY, canvas.width, slicePx, 0, 0, canvas.width, slicePx)
pdf.addImage(sc.toDataURL('image/jpeg', 0.95), 'JPEG', mg, mg, printW, sliceH)
srcY += slicePx; rem -= sliceH
if (rem > 0) pdf.addPage()
}
const id = getV(this.row, 'EXCOILID') || getV(this.row, 'HOT_COILID') || 'report'
pdf.save(`质量报表_${id}.pdf`)
} catch (e) {
this.$message.error('PDF导出失败' + e.message)
console.error(e)
} finally {
this.exporting = false
}
}
}
}
</script>
<style scoped>
.rpt-toolbar {
display: flex; align-items: center; justify-content: space-between;
padding-bottom: 10px; border-bottom: 1px solid #f0f2f5; margin-bottom: 8px;
}
.rpt-coil-hint { font-size: 13px; color: #606266; font-weight: 600; }
.report-body {
background: #fff;
padding: 18px 22px;
font-size: 12px;
color: #303133;
max-height: 75vh;
overflow-y: auto;
}
/* 页眉 */
.rpt-header {
display: flex; align-items: center; justify-content: space-between;
border-bottom: 2px solid #2c5282; padding-bottom: 8px; margin-bottom: 12px;
}
.rpt-header-left { display: flex; align-items: center; }
.rpt-logo { height: 44px; width: auto; object-fit: contain; }
.company-sub { font-size: 10px; color: #888; }
.rpt-title { font-size: 22px; font-weight: 700; color: #1a365d; letter-spacing: 3px; }
.rpt-header-right { text-align: right; }
.brand-name { font-size: 16px; font-weight: 700; color: #2c5282; }
.brand-sub { font-size: 9px; color: #aaa; letter-spacing: 2px; }
/* 节标题 */
.section-bar {
background: #2c5282; color: #fff; font-size: 12px; font-weight: 600;
padding: 3px 10px; border-radius: 2px; margin: 10px 0 5px; letter-spacing: 1px;
}
/* 表格 */
.rpt-table { width: 100%; border-collapse: collapse; font-size: 11px; margin-bottom: 4px; }
.rpt-table td, .rpt-table th {
border: 1px solid #c9d4e0; padding: 3px 7px; text-align: left; white-space: nowrap;
}
.rpt-table th { background: #dce8f4; color: #1a365d; font-weight: 600; text-align: center; }
.lbl { background: #eef3f9; color: #4a6080; font-weight: 500; width: 88px; }
.val { color: #1a1a2e; font-weight: 600; }
.val.bold { font-size: 13px; color: #1a365d; letter-spacing: 0.5px; }
.quality-table td { text-align: center; }
.cell-ok { color: #2e7d32; font-weight: 700; }
.cell-ng { color: #c62828; font-weight: 700; }
/* 图表 */
.chart-title {
font-size: 12px; font-weight: 600; color: #2c5282;
margin: 12px 0 4px; padding-left: 6px;
border-left: 3px solid #2c5282;
}
.chart-subtitle { font-size: 10px; font-weight: normal; color: #888; margin-left: 8px; }
.rpt-chart { width: 100%; height: 180px; border: 1px solid #e8edf2; border-radius: 3px; background: #fafbfc; }
.rpt-chart-sm { height: 140px; }
.rpt-chart-heat { height: 200px; }
.rpt-footer { margin-top: 16px; padding-top: 6px; border-top: 1px solid #e2e8f0; text-align: center; font-size: 10px; color: #888; }
::v-deep .el-dialog__body { padding: 12px 20px 20px; }
::v-deep .el-button--primary { background: #2c5282 !important; border-color: #2c5282 !important; }
::v-deep .el-button--primary:hover { background: #1a365d !important; border-color: #1a365d !important; }
</style>

View File

@@ -0,0 +1,436 @@
<template>
<div class="tracking-wrap">
<!-- 工具栏 -->
<div class="track-toolbar">
<span class="track-title">跟踪管理</span>
<span class="track-update-time" v-if="lastUpdated">更新时间{{ lastUpdated }}</span>
<el-button size="mini" icon="el-icon-refresh" :loading="loading" circle @click="loadData" />
</div>
<div class="tracking-body">
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-row">
<div v-for="eq in flowRow1" :key="eq.name" class="flow-eq" :class="eq.cls">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
<div class="flow-row flow-row-reverse">
<div v-for="eq in flowRow2" :key="eq.name" class="flow-eq" :class="eq.cls">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
<div class="flow-row flow-row-sparse">
<div v-for="eq in flowRow3" :key="eq.name" class="flow-eq" :class="eq.cls" :style="eq.style">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
<div class="flow-row">
<div v-for="eq in flowRow4" :key="eq.name" class="flow-eq" :class="eq.cls">
<div class="eq-name">{{ eq.name }}<span class="eq-sub"> / {{ eq.label }}</span></div>
<div v-for="(slot, si) in slotsFor(eq.displayNames)" :key="si"
class="eq-slot" :class="slot.matId ? 'slot-active' : 'slot-empty'">
{{ slot.matId || '' }}
</div>
</div>
</div>
</div>
<!-- 入口跟踪 -->
<div class="section-label">入口跟踪</div>
<div class="entry-wrap">
<div v-for="(lane, li) in entryLanes" :key="li" class="entry-lane">
<div v-for="pos in lane" :key="pos.displayName"
class="entry-card" :class="{ 'has-coil': !!coilAt(pos.displayName) }">
<div class="entry-card-header">{{ pos.label }}</div>
<template v-if="coilAt(pos.displayName)">
<div class="entry-kv"><span class="ek">冷卷号</span><span class="ev">{{ coilAt(pos.displayName).coilid }}</span></div>
<div class="entry-kv"><span class="ek">热卷号</span><span class="ev">{{ coilAt(pos.displayName).hot_coilid }}</span></div>
<div class="entry-kv"><span class="ek">钢种</span><span class="ev">{{ coilAt(pos.displayName).grade }}</span></div>
<div class="entry-kv"><span class="ek">来料厚[mm]</span><span class="ev">{{ coilAt(pos.displayName).entry_thick }}</span></div>
<div class="entry-kv"><span class="ek">成品厚[mm]</span><span class="ev">{{ coilAt(pos.displayName).exit_thick }}</span></div>
<div class="entry-kv"><span class="ek">厚差[mm]</span><span class="ev">{{ coilAt(pos.displayName).min_exit_thick }} / {{ coilAt(pos.displayName).max_exit_thick }}</span></div>
<div class="entry-kv"><span class="ek">来料宽[mm]</span><span class="ev">{{ coilAt(pos.displayName).entry_width }}</span></div>
<div class="entry-kv"><span class="ek">成品宽[mm]</span><span class="ev">{{ coilAt(pos.displayName).exit_width }}</span></div>
<div class="entry-kv"><span class="ek">重量[t]</span><span class="ev">{{ coilAt(pos.displayName).entry_weight }}</span></div>
<div class="entry-kv"><span class="ek">轧制模式</span><span class="ev">{{ coilAt(pos.displayName).roll_mode }}</span></div>
</template>
<div v-else class="entry-empty"></div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="section-label">详细信息</div>
<el-table :data="detailTableRows" size="mini" border stripe style="width:100%">
<el-table-column prop="coilid" label="冷卷号" width="130" show-overflow-tooltip />
<el-table-column prop="hot_coilid" label="热卷号" width="130" show-overflow-tooltip />
<el-table-column prop="grade" label="钢种" width="100" />
<el-table-column prop="entry_thick" label="来料厚度" width="80" />
<el-table-column prop="exit_thick" label="产品厚度" width="80" />
<el-table-column prop="max_exit_thick" label="偏差上限" width="80" />
<el-table-column prop="min_exit_thick" label="偏差下限" width="80" />
<el-table-column prop="entry_width" label="来料宽度" width="80" />
<el-table-column prop="exit_width" label="产品宽度" width="80" />
<el-table-column prop="roll_mode" label="轧制模式" width="100" />
<el-table-column prop="park_type" label="包装要求" width="80" />
<el-table-column prop="side_trim" label="切边要求" width="80" />
<el-table-column prop="entry_weight" label="来料重量" width="80" />
<el-table-column prop="split_num" label="分卷数" width="70" />
<el-table-column prop="position" label="位置" width="120" show-overflow-tooltip />
<el-table-column prop="online_date" label="上线时间" width="160" show-overflow-tooltip />
</el-table>
</div>
</div>
</template>
<script>
import { getTrackData } from '@/api/l2/timing'
const DISPLAY_LABEL = {
UC1: '1#上卷小车', UC2: '2#上卷小车',
WEIT1: '1#称重位', WEIT2: '2#称重位',
CR1: '1#地辊', CR2: '2#地辊',
ENC1: '1#倒卷小车', ENC2: '2#倒卷小车',
ENC3: '3#倒卷小车', ENC4: '4#倒卷小车',
CS1: '1#上卷鞍座', CS2: '2#上卷鞍座',
CS3: '3#上卷鞍座', CS4: '4#上卷鞍座',
CS5: '5#上卷鞍座', CS6: '6#上卷鞍座',
CS7: '7#上卷鞍座', CS8: '8#上卷鞍座',
CS9: '9#上卷鞍座', CS10: '10#上卷鞍座',
CS11: '11#上卷鞍座', CS12: '12#上卷鞍座',
CS13: '13#上卷鞍座', CS14: '14#上卷鞍座',
CXL: '出口活套', CXL1: '出口活套1', CXL2: '出口活套2',
CPR: '酸洗段', CPR1: '酸洗段1', CPR2: '酸洗段2',
CEL: '入口活套', CEL1: '入口活套1', CEL2: '入口活套2',
TRIMMER: '圆盘剪',
RINSE: '清洗段',
TLV: '拉矫机', TLV1: '拉矫机1',
TEL: '联机活套', TEL1: '联机活套1', TEL2: '联机活套2',
EXAM: '检查站', EXAMC: '检查站',
MILL: '轧机',
COILER: '卷取机',
EXC: '卸卷小车',
DC0: '交接鞍座',
EXC1: '步进梁1', EXC2: '步进梁2'
}
const ENTRY_LANES = [
[
{ displayName: 'UC1', label: '1#上卷小车' },
{ displayName: 'WEIT1', label: '1#称重位' },
{ displayName: 'CR1', label: '1#地辊' },
{ displayName: 'CS13', label: '13#上卷鞍座' },
{ displayName: 'CS11', label: '11#上卷鞍座' },
{ displayName: 'CS9', label: '9#上卷鞍座' },
{ displayName: 'CS7', label: '7#上卷鞍座' },
{ displayName: 'ENC1', label: '1#倒卷小车' },
{ displayName: 'CS5', label: '5#上卷鞍座' },
{ displayName: 'CS3', label: '3#上卷鞍座' },
{ displayName: 'CS1', label: '1#上卷鞍座' }
],
[
{ displayName: 'UC2', label: '2#上卷小车' },
{ displayName: 'WEIT2', label: '2#称重位' },
{ displayName: 'CR2', label: '2#地辊' },
{ displayName: 'CS14', label: '14#上卷鞍座' },
{ displayName: 'CS12', label: '12#上卷鞍座' },
{ displayName: 'CS10', label: '10#上卷鞍座' },
{ displayName: 'CS8', label: '8#上卷鞍座' },
{ displayName: 'ENC2', label: '2#倒卷小车' },
{ displayName: 'CS6', label: '6#上卷鞍座' },
{ displayName: 'CS4', label: '4#上卷鞍座' },
{ displayName: 'CS2', label: '2#上卷鞍座' }
]
]
const FLOW_ROWS = {
row1: [
{ name: 'CXL', label: '出口活套', displayNames: ['CXL'], cls: 'eq-process' },
{ name: 'CPR', label: '酸洗段', displayNames: ['CPR'], cls: 'eq-process' },
{ name: 'CEL', label: '入口活套', displayNames: ['CEL','CEL1'], cls: 'eq-process' },
{ name: 'UC1', label: '开卷机', displayNames: ['UC1'], cls: 'eq-entry' },
{ name: 'WEIT1', label: '称重位', displayNames: ['WEIT1'], cls: 'eq-entry' },
{ name: 'CR1', label: '地辊', displayNames: ['CR1'], cls: 'eq-entry' }
],
row2: [
{ name: 'TRIMMER', label: '圆盘剪', displayNames: ['TRIMMER'], cls: 'eq-process' },
{ name: 'RINSE', label: '清洗段', displayNames: ['RINSE'], cls: 'eq-process' },
{ name: 'TLV', label: '拉矫机', displayNames: ['TLV'], cls: 'eq-process' },
{ name: 'UC2', label: '开卷机', displayNames: ['UC2'], cls: 'eq-entry' },
{ name: 'WEIT2', label: '称重位', displayNames: ['WEIT2'], cls: 'eq-entry' },
{ name: 'CR2', label: '地辊', displayNames: ['CR2'], cls: 'eq-entry' }
],
row3: [
{ name: 'TEL', label: '联机活套', displayNames: ['TEL'], cls: 'eq-mill', style: 'flex:1' },
{ name: 'EXAM', label: '检查站', displayNames: ['EXAM','EXAMC'], cls: 'eq-exit', style: 'flex:1;margin-left:auto' }
],
row4: [
{ name: 'MILL', label: '轧机', displayNames: ['MILL'], cls: 'eq-mill' },
{ name: 'COILER', label: '卷取机', displayNames: ['COILER'],cls: 'eq-mill' },
{ name: 'EXC', label: '卸卷小车',displayNames: ['EXC'], cls: 'eq-exit' },
{ name: 'DC0', label: '交接鞍座',displayNames: ['DC0'], cls: 'eq-exit' },
{ name: 'EXC1', label: '步进梁1', displayNames: ['EXC1'], cls: 'eq-exit' },
{ name: 'EXC2', label: '步进梁2', displayNames: ['EXC2'], cls: 'eq-exit' }
]
}
export default {
name: 'TrackingView',
data() {
return {
loading: false,
lastUpdated: '',
matMapRows: [],
entryTraceRows: [],
exitTraceRows: [],
pollTimer: null,
entryLanes: ENTRY_LANES,
flowRow1: FLOW_ROWS.row1,
flowRow2: FLOW_ROWS.row2,
flowRow3: FLOW_ROWS.row3,
flowRow4: FLOW_ROWS.row4
}
},
computed: {
matMapByDisplayName() {
const map = {}
for (const row of this.matMapRows) {
const dn = (row.displayname || row.DISPLAYNAME || '').toUpperCase()
if (!dn) continue
if (!map[dn]) map[dn] = []
map[dn].push({
matId: row.matid || row.MATID || null,
l1MapIdx: row.l1mapidx != null ? row.l1mapidx : row.L1MAPIDX
})
}
return map
},
entryByL1MapIdx() {
const map = {}
for (const row of this.entryTraceRows) {
const idx = row.l1mapidx != null ? row.l1mapidx : row.L1MAPIDX
if (idx != null) map[idx] = row
}
return map
},
detailTableRows() {
return this.entryTraceRows.map(row => {
const idx = row.l1mapidx != null ? row.l1mapidx : row.L1MAPIDX
const posRow = this.matMapRows.find(m => (m.l1mapidx != null ? m.l1mapidx : m.L1MAPIDX) === idx)
const dn = posRow ? (posRow.displayname || posRow.DISPLAYNAME || '').toUpperCase() : ''
return {
...row,
position: DISPLAY_LABEL[dn] || dn || ''
}
})
}
},
created() {
this.loadData()
this.pollTimer = setInterval(this.loadData, 5000)
},
beforeDestroy() {
if (this.pollTimer) clearInterval(this.pollTimer)
},
methods: {
async loadData() {
this.loading = true
try {
const res = await getTrackData()
const data = res?.data || {}
this.matMapRows = data.matMap || []
this.entryTraceRows = data.entryTrace || []
this.exitTraceRows = data.exitTrace || []
this.lastUpdated = new Date().toLocaleTimeString()
} catch (_) {}
finally { this.loading = false }
},
coilAt(displayName) {
const slots = this.matMapByDisplayName[displayName]
if (!slots || !slots.length) return null
const slot = slots[0]
if (!slot.matId) return null
return this.entryByL1MapIdx[slot.l1MapIdx] || null
},
slotsFor(displayNames) {
const result = []
for (const dn of displayNames) {
for (const s of (this.matMapByDisplayName[dn] || [])) {
result.push({ matId: s.matId })
}
}
while (result.length < 2) result.push({ matId: null })
return result.slice(0, 2)
}
}
}
</script>
<style scoped>
.tracking-wrap {
display: flex;
flex-direction: column;
height: calc(100vh - 130px);
background: #f5f7fa;
padding: 8px 12px;
box-sizing: border-box;
overflow: hidden;
}
.track-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
flex-shrink: 0;
}
.track-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.track-update-time {
font-size: 12px;
color: #909399;
margin-left: auto;
}
.tracking-body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.section-label {
font-size: 13px;
font-weight: 600;
color: #fff;
background: #2c5282;
padding: 4px 12px;
border-radius: 3px;
flex-shrink: 0;
}
/* ─── 流程图 ─── */
.flow-diagram {
background: #b8cde0;
border-radius: 6px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.flow-row {
display: flex;
gap: 6px;
align-items: stretch;
}
.flow-row-sparse {
justify-content: space-between;
}
.flow-eq {
min-width: 110px;
flex: 1;
border-radius: 4px;
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 3px;
}
.eq-name {
font-size: 11px;
font-weight: 600;
color: #1a2a4a;
white-space: nowrap;
}
.eq-sub { font-size: 10px; font-weight: normal; color: #2c4a7a; }
.eq-slot {
font-size: 11px;
font-weight: 600;
border-radius: 3px;
padding: 2px 6px;
text-align: center;
min-height: 20px;
line-height: 20px;
}
.slot-empty { background: #cdd8e8; }
.slot-active { color: #fff; background: #1a4a8a; }
.eq-process { background: #22d3ee44; border: 1px solid #06b6d4; }
.eq-process .slot-active { background: #0891b2; }
.eq-entry { background: #fbbf2444; border: 1px solid #f59e0b; }
.eq-entry .slot-active { background: #b45309; }
.eq-mill { background: #f8717144; border: 1px solid #ef4444; }
.eq-mill .slot-active { background: #dc2626; }
.eq-exit { background: #a3e63544; border: 1px solid #84cc16; }
.eq-exit .slot-active { background: #4d7c0f; }
/* ─── 入口跟踪 ─── */
.entry-wrap {
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.entry-lane {
display: flex;
gap: 5px;
overflow-x: auto;
}
.entry-card {
min-width: 108px;
flex: 1;
background: #d9e6f7;
border: 1px solid #b0c8e8;
border-radius: 4px;
padding: 5px 7px;
font-size: 11px;
}
.entry-card.has-coil { background: #c5d8f0; }
.entry-card-header {
font-size: 11px;
font-weight: 600;
color: #2c3e6b;
text-align: center;
margin-bottom: 3px;
padding-bottom: 3px;
border-bottom: 1px solid #b0c8e8;
}
.entry-kv {
display: flex;
justify-content: space-between;
padding: 1px 0;
gap: 2px;
}
.ek { color: #5a6e99; flex-shrink: 0; }
.ev { color: #1a4a8a; font-weight: 500; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.entry-empty { text-align: center; color: #b0bec5; padding: 16px 0; }
</style>

View File

@@ -46,6 +46,14 @@
<i class="el-icon-warning"></i>
<span slot="title">停机</span>
</el-menu-item>
<el-menu-item index="acidTiming">
<i class="el-icon-alarm-clock"></i>
<span slot="title">app</span>
</el-menu-item>
<el-menu-item index="tracking">
<i class="el-icon-location-outline"></i>
<span slot="title">跟踪</span>
</el-menu-item>
</el-menu>
</div>
<div style="flex: 1; overflow: hidden;">
@@ -65,6 +73,8 @@ import ActualPerformance from './components/ActualPerformance.vue';
import RollConfig from '@/views/timing/roll/index.vue';
import RollHistory from '@/views/timing/roll/history.vue';
import Stoppage from '@/views/timing/stoppage/index.vue';
import AcidTiming from '@/views/lines/acid/index.vue';
import TrackingView from './components/TrackingView.vue';
export default {
name: 'AcidSystem',
@@ -78,7 +88,9 @@ export default {
ActualPerformance,
RollConfig,
RollHistory,
Stoppage
Stoppage,
AcidTiming,
TrackingView
},
data() {
return {
@@ -98,6 +110,8 @@ export default {
rollConfig: 'RollConfig',
rollHistory: 'RollHistory',
stoppage: 'Stoppage',
acidTiming: 'AcidTiming',
tracking: 'TrackingView',
};
return componentMap[this.activeMenu];
},

View File

@@ -0,0 +1,623 @@
<template>
<el-dialog
title="产品质量报表预览"
:visible.sync="visible"
width="880px"
append-to-body
:close-on-click-modal="false"
@open="onOpen"
@close="onClose"
>
<!-- 工具栏 -->
<div class="rpt-toolbar">
<span class="rpt-coil-hint">{{ coilLabel }}</span>
<div class="rpt-toolbar-right">
<el-button
size="small"
type="primary"
icon="el-icon-download"
:loading="exporting"
@click="exportPdf"
>导出 PDF</el-button>
<el-button size="small" @click="visible = false">关闭</el-button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="dataLoading" class="rpt-loading">
<i class="el-icon-loading" /> 正在加载实绩数据
</div>
<!-- 报告主体 -->
<div v-show="!dataLoading" ref="reportContent" class="report-body">
<!-- 页眉 -->
<div class="rpt-header">
<div class="rpt-header-left">
<div class="company-line">中国五矿</div>
<div class="mcc-line">MCC 中冶赛迪信息</div>
</div>
<div class="rpt-header-title">产品质量报表</div>
<div class="rpt-header-right">
<div class="brand-name">科伦普</div>
<div class="brand-sub">KE LUN PU</div>
</div>
</div>
<!-- 钢卷规格 -->
<div class="rpt-section-bar">钢卷规格</div>
<table class="rpt-table spec-table">
<tbody>
<tr>
<td class="cell-label">钢卷号</td>
<td class="cell-val cell-coilid">{{ v('hot_coilid') || v('coilid') }}</td>
<td class="cell-label">来料厚度[mm]</td>
<td class="cell-val">{{ v('entry_thick') }}</td>
<td class="cell-label">来料宽度[mm]</td>
<td class="cell-val">{{ v('entry_width') }}</td>
<td class="cell-label">来料重量[t]</td>
<td class="cell-val">{{ v('entry_weight') }}</td>
</tr>
<tr>
<td class="cell-label">钢种</td>
<td class="cell-val">{{ v('steel_grade') || v('steelgrade') || v('process_code') }}</td>
<td class="cell-label">成品厚度[mm]</td>
<td class="cell-val">{{ v('exit_thick') }}</td>
<td class="cell-label">成品宽度[mm]</td>
<td class="cell-val">{{ v('exit_width') }}</td>
<td class="cell-label">成品重量[t]</td>
<td class="cell-val">{{ v('exit_weight') || v('entry_weight') }}</td>
</tr>
<tr>
<td class="cell-label">班组</td>
<td class="cell-val">{{ v('shift') || v('class_no') || '—' }}</td>
<td class="cell-label">偏差上限[mm]</td>
<td class="cell-val">{{ v('thick_upper') || v('up_tol') || '—' }}</td>
<td class="cell-label">压下率[%]</td>
<td class="cell-val">{{ reductionRate }}</td>
<td class="cell-label">成品长度[m]</td>
<td class="cell-val">{{ v('exit_length') }}</td>
</tr>
<tr>
<td class="cell-label">下工序</td>
<td class="cell-val">{{ v('next_process') || v('next_proc') || '—' }}</td>
<td class="cell-label">偏差下限[mm]</td>
<td class="cell-val">{{ v('thick_lower') || v('dn_tol') || '—' }}</td>
<td class="cell-label">原料卷号</td>
<td class="cell-val" colspan="3">{{ v('coilid') }}</td>
</tr>
<tr>
<td class="cell-label">生产时间</td>
<td class="cell-val" colspan="3">{{ v('prod_time') || v('start_time') || v('createtime') || '—' }}</td>
<td class="cell-label">生产时长</td>
<td class="cell-val" colspan="3">{{ productionDuration }}</td>
</tr>
</tbody>
</table>
<!-- 质量判定 -->
<div class="rpt-section-bar">质量判定</div>
<table class="rpt-table quality-table">
<thead>
<tr>
<th>轧制总长[m]</th>
<th>头尾超差[m]</th>
<th>厚度合格率[%]</th>
<th>目标板形</th>
<th>板形合格率[%]</th>
<th>板形质量</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ rolledLength }}</td>
<td>{{ v('head_tail_dev') || '—' }}</td>
<td>{{ thickPassRate }}</td>
<td>{{ v('target_shape') || '平直' }}</td>
<td>{{ v('shape_pass_rate') || '—' }}</td>
<td :class="qualityClass">{{ qualityLabel }}</td>
</tr>
</tbody>
</table>
<!-- 图表区 -->
<template v-if="hasSeries">
<!-- 厚度偏差 -->
<template v-if="hasField('thick_dev') || hasField('act_thick')">
<div class="rpt-chart-title">
成品厚度偏差
<span class="rpt-chart-subtitle">产出厚度 - {{ v('exit_thick') }} [mm]</span>
</div>
<div ref="chartThick" class="rpt-chart" />
</template>
<!-- 速度趋势 -->
<template v-if="hasField('plspeed') || hasField('trimspeed') || hasField('millexitspeed')">
<div class="rpt-chart-title">速度趋势 [m/min]</div>
<div ref="chartSpeed" class="rpt-chart" />
</template>
<!-- 轧机出口速度 -->
<template v-if="hasField('millexitspeed') && (hasField('plspeed') || hasField('trimspeed'))">
<div class="rpt-chart-title">轧机出口速度 [m/min]</div>
<div ref="chartMillSpeed" class="rpt-chart" />
</template>
<!-- 张力趋势 -->
<template v-if="hasField('pltens') || hasField('enltens') || hasField('cxltens')">
<div class="rpt-chart-title">张力趋势 [N]</div>
<div ref="chartTension" class="rpt-chart" />
</template>
<!-- 额外字段图表 -->
<template v-for="grp in extraChartGroups">
<div :key="grp.title + '_t'" class="rpt-chart-title">{{ grp.title }}</div>
<div :key="grp.title + '_c'" :ref="'chartExtra_' + grp.refKey" class="rpt-chart" />
</template>
</template>
<div v-else class="rpt-no-data">暂无实绩曲线数据</div>
<!-- 页脚 -->
<div class="rpt-footer">生成时间&nbsp;{{ nowStr }}</div>
</div>
</el-dialog>
</template>
<script>
import * as echarts from 'echarts'
import { getPlanWithSeg } from '@/api/l2/timing'
// Series fields consumed by named charts — remaining fields get auto-grouped
const NAMED_KEYS = new Set(['startpos', 'thick_dev', 'act_thick', 'plspeed', 'trimspeed', 'millexitspeed', 'pltens', 'enltens', 'cxltens'])
// Groups of extra series fields to chart together
const EXTRA_GROUPS = [
{ keys: ['bendf1', 'bendf2', 'bendf3', 'bendf4', 'bendf5'], title: '弯辊力 [kN]', unit: 'kN' },
{ keys: ['rollf1', 'rollf2', 'rollf3', 'rollf4', 'rollf5'], title: '轧制力 [kN]', unit: 'kN' },
{ keys: ['fwd_slip1', 'fwd_slip2', 'fwd_slip3', 'fwd_slip4', 'fwd_slip5'], title: '前滑值', unit: '' },
{ keys: ['temp_in', 'temp_out', 'cool_temp'], title: '温度 [°C]', unit: '°C' },
]
function lineOption(title, xData, series, yName) {
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal', color: '#303133' }, top: 4, left: 8 },
tooltip: { trigger: 'axis', confine: true },
legend: { top: 4, right: 8, textStyle: { fontSize: 10 } },
grid: { top: 40, bottom: 32, left: 10, right: 12, containLabel: true },
xAxis: {
type: 'category', data: xData, name: '带钢长度[m]',
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 9, interval: 'auto' }
},
yAxis: { type: 'value', name: yName, nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 9 } },
series: series.map((s, i) => ({
name: s.name, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1.5 },
data: s.data
}))
}
}
function dualAxisOption(title, xData, devSeries, thickSeries, exitThick) {
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal', color: '#303133' }, top: 4, left: 8 },
tooltip: { trigger: 'axis', confine: true },
legend: { top: 4, right: 8, textStyle: { fontSize: 10 } },
grid: { top: 40, bottom: 32, left: 10, right: 12, containLabel: true },
xAxis: {
type: 'category', data: xData, name: '带钢长度[m]',
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 9, interval: 'auto' }
},
yAxis: [
{ type: 'value', name: 'μm', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 9 } },
{ type: 'value', name: 'mm', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 9 }, splitLine: { show: false } }
],
series: [
{ name: '厚度偏差', type: 'line', yAxisIndex: 0, smooth: false, symbol: 'none', lineStyle: { width: 1, color: '#5F7BA0' }, data: devSeries },
thickSeries && { name: '实际厚度', type: 'line', yAxisIndex: 1, smooth: false, symbol: 'none', lineStyle: { width: 1.5, color: '#e6a23c' }, data: thickSeries }
].filter(Boolean)
}
}
export default {
name: 'QualityReportDialog',
data() {
return {
visible: false,
dataLoading: false,
exporting: false,
plan: null,
series: null,
nowStr: '',
chartInsts: []
}
},
computed: {
coilLabel() {
const id = this.plan?.hot_coilid || this.plan?.coilid || ''
return id ? `钢卷号:${id}` : ''
},
hasSeries() {
return !!this.series && Object.keys(this.series).length > 0
},
reductionRate() {
const e = parseFloat(this.plan?.entry_thick)
const x = parseFloat(this.plan?.exit_thick)
if (!e || !x || e <= 0) return '—'
return ((e - x) / e * 100).toFixed(2)
},
rolledLength() {
if (this.plan?.exit_length) return this.plan.exit_length
if (this.series?.startpos?.length) {
const max = Math.max(...this.series.startpos.filter(v => v != null))
return max > 0 ? max.toFixed(1) : '—'
}
return '—'
},
thickPassRate() {
const r = this.plan?.thick_pass_rate || this.plan?.thick_passrate
return r != null ? r : '—'
},
qualityLabel() {
const r = this.plan?.quality || this.plan?.quality_result
if (r) return r
const tp = parseFloat(this.thickPassRate)
if (!isNaN(tp)) return tp >= 99 ? '合格' : '不合格'
return '—'
},
qualityClass() {
return this.qualityLabel === '合格' ? 'cell-ok' : this.qualityLabel === '不合格' ? 'cell-ng' : ''
},
productionDuration() {
const d = this.plan?.prod_duration || this.plan?.duration
if (!d) return '—'
const sec = parseInt(d)
if (isNaN(sec)) return d
const h = Math.floor(sec / 3600)
const m = Math.floor((sec % 3600) / 60)
const s = sec % 60
return `${String(h).padStart(2, '0')}${String(m).padStart(2, '0')}${String(s).padStart(2, '0')}`
},
extraChartGroups() {
if (!this.series) return []
const result = []
// first try the predefined groups
for (const grp of EXTRA_GROUPS) {
const presentKeys = grp.keys.filter(k => this.hasField(k))
if (presentKeys.length) {
result.push({ title: grp.title, unit: grp.unit, keys: presentKeys, refKey: grp.title.replace(/\s/g, '_') })
}
}
// then collect truly unknown keys
const knownInExtra = new Set(EXTRA_GROUPS.flatMap(g => g.keys))
const unknown = Object.keys(this.series).filter(k => !NAMED_KEYS.has(k) && !knownInExtra.has(k) && Array.isArray(this.series[k]) && this.series[k].length)
if (unknown.length) {
result.push({ title: '其他工艺参数', unit: '', keys: unknown, refKey: 'other' })
}
return result
}
},
methods: {
v(key) {
const val = this.plan?.[key]
return val != null && val !== '' ? val : '—'
},
hasField(key) {
return !!(this.series?.[key]?.length)
},
// Public: open with a row object
open(row) {
this.plan = row
this.series = null
this.visible = true
},
onOpen() {
this.nowStr = new Date().toLocaleString('zh-CN').replace(/\//g, '-')
this.$nextTick(() => {
if (this.series) {
this.renderAllCharts()
} else if (this.plan) {
this.fetchSeries()
}
})
},
onClose() {
this.disposeCharts()
},
async fetchSeries() {
const id = this.plan?.encoilid || this.plan?.coilid
if (!id) return
this.dataLoading = true
try {
const res = await getPlanWithSeg(id)
const plan = res?.data?.plan || res?.data?.firstRow || this.plan
const series = res?.data?.series || null
if (plan) this.plan = plan
this.series = series
this.$nextTick(() => this.renderAllCharts())
} finally {
this.dataLoading = false
}
},
disposeCharts() {
this.chartInsts.forEach(c => { try { c.dispose() } catch (_) {} })
this.chartInsts = []
},
pick(key) {
return (this.series?.[key] || []).map(v => v == null ? null : +parseFloat(v).toFixed(3))
},
xData() {
return (this.series?.startpos || []).map(v => v == null ? '' : (+parseFloat(v).toFixed(1)))
},
renderAllCharts() {
this.disposeCharts()
if (!this.series) return
const x = this.xData()
// 厚度偏差
if (this.$refs.chartThick) {
const devKey = this.hasField('thick_dev') ? 'thick_dev' : null
const actKey = this.hasField('act_thick') ? 'act_thick' : null
if (devKey || actKey) {
const devData = devKey ? this.pick(devKey).map(v => v == null ? null : v * 1000) : []
const actData = actKey ? this.pick(actKey) : null
const c = echarts.init(this.$refs.chartThick, null, { renderer: 'canvas' })
if (devKey) {
c.setOption(dualAxisOption('成品厚度偏差', x, devData, actData, this.plan?.exit_thick))
} else {
c.setOption(lineOption('实际厚度 [mm]', x, [{ name: '实际厚度', data: actData }], 'mm'))
}
this.chartInsts.push(c)
}
}
// 速度趋势
if (this.$refs.chartSpeed) {
const series = []
if (this.hasField('plspeed')) series.push({ name: '轧制速度 plspeed', data: this.pick('plspeed') })
if (this.hasField('trimspeed')) series.push({ name: '剪切速度 trimspeed', data: this.pick('trimspeed') })
if (!this.hasField('plspeed') && !this.hasField('trimspeed') && this.hasField('millexitspeed')) {
series.push({ name: '轧机出口速度', data: this.pick('millexitspeed') })
}
if (series.length) {
const c = echarts.init(this.$refs.chartSpeed, null, { renderer: 'canvas' })
c.setOption(lineOption('速度趋势', x, series, 'm/min'))
this.chartInsts.push(c)
}
}
// 轧机出口速度(单独显示)
if (this.$refs.chartMillSpeed && this.hasField('millexitspeed')) {
const c = echarts.init(this.$refs.chartMillSpeed, null, { renderer: 'canvas' })
c.setOption(lineOption('轧机出口速度', x, [{ name: 'millexitspeed', data: this.pick('millexitspeed') }], 'm/min'))
this.chartInsts.push(c)
}
// 张力趋势
if (this.$refs.chartTension) {
const series = []
if (this.hasField('pltens')) series.push({ name: '出口张力 pltens', data: this.pick('pltens') })
if (this.hasField('enltens')) series.push({ name: '入口张力 enltens', data: this.pick('enltens') })
if (this.hasField('cxltens')) series.push({ name: 'cxltens', data: this.pick('cxltens') })
if (series.length) {
const c = echarts.init(this.$refs.chartTension, null, { renderer: 'canvas' })
c.setOption(lineOption('张力趋势', x, series, 'N'))
this.chartInsts.push(c)
}
}
// 额外图表组
for (const grp of this.extraChartGroups) {
const refKey = 'chartExtra_' + grp.refKey
const refEl = this.$refs[refKey]
const el = Array.isArray(refEl) ? refEl[0] : refEl
if (!el) continue
const series = grp.keys.filter(k => this.hasField(k)).map(k => ({ name: k, data: this.pick(k) }))
if (series.length) {
const c = echarts.init(el, null, { renderer: 'canvas' })
c.setOption(lineOption(grp.title, x, series, grp.unit))
this.chartInsts.push(c)
}
}
},
async exportPdf() {
this.exporting = true
try {
const [html2canvas, { jsPDF }] = await Promise.all([
import('html2canvas').then(m => m.default),
import('jspdf')
])
const el = this.$refs.reportContent
const canvas = await html2canvas(el, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false
})
const imgData = canvas.toDataURL('image/jpeg', 0.95)
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' })
const pageW = pdf.internal.pageSize.getWidth() // 210
const pageH = pdf.internal.pageSize.getHeight() // 297
const margin = 8
const printW = pageW - margin * 2
const ratio = canvas.width / printW
const imgH = canvas.height / ratio
let remaining = imgH
let srcY = 0
while (remaining > 0) {
const sliceH = Math.min(pageH - margin * 2, remaining)
const slicePx = sliceH * ratio
const sliceCanvas = document.createElement('canvas')
sliceCanvas.width = canvas.width
sliceCanvas.height = slicePx
const ctx = sliceCanvas.getContext('2d')
ctx.drawImage(canvas, 0, srcY, canvas.width, slicePx, 0, 0, canvas.width, slicePx)
pdf.addImage(sliceCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', margin, margin, printW, sliceH)
srcY += slicePx
remaining -= sliceH
if (remaining > 0) pdf.addPage()
}
const coilId = this.plan?.hot_coilid || this.plan?.coilid || 'report'
pdf.save(`质量报表_${coilId}.pdf`)
} catch (e) {
this.$message.error('PDF导出失败' + e.message)
console.error(e)
} finally {
this.exporting = false
}
}
}
}
</script>
<style scoped>
/* ── 工具栏 ── */
.rpt-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 10px;
border-bottom: 1px solid #f0f2f5;
margin-bottom: 10px;
}
.rpt-coil-hint { font-size: 13px; color: #606266; font-weight: 500; }
.rpt-toolbar-right { display: flex; gap: 8px; }
/* ── 加载 ── */
.rpt-loading {
padding: 40px 0;
text-align: center;
font-size: 14px;
color: #909399;
}
/* ── 报告主体 ── */
.report-body {
background: #fff;
padding: 20px 24px;
font-size: 12px;
color: #303133;
min-height: 300px;
}
/* 页眉 */
.rpt-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid #2c5282;
padding-bottom: 10px;
margin-bottom: 14px;
}
.rpt-header-left { font-size: 11px; color: #555; line-height: 1.6; }
.company-line { font-weight: 600; font-size: 13px; }
.mcc-line { font-size: 10px; color: #888; }
.rpt-header-title { font-size: 22px; font-weight: 700; color: #1a365d; letter-spacing: 2px; }
.rpt-header-right { text-align: right; }
.brand-name { font-size: 16px; font-weight: 700; color: #2c5282; letter-spacing: 1px; }
.brand-sub { font-size: 9px; color: #888; letter-spacing: 2px; }
/* 节标题 */
.rpt-section-bar {
background: #2c5282;
color: #fff;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 2px;
margin: 10px 0 6px;
letter-spacing: 1px;
}
/* 规格表 & 质量判定表 */
.rpt-table {
width: 100%;
border-collapse: collapse;
font-size: 11.5px;
margin-bottom: 4px;
}
.rpt-table td, .rpt-table th {
border: 1px solid #c9d4e0;
padding: 4px 8px;
text-align: left;
white-space: nowrap;
}
.rpt-table th {
background: #dce8f4;
color: #1a365d;
font-weight: 600;
text-align: center;
font-size: 11px;
}
.cell-label {
background: #eef3f9;
color: #4a6080;
font-weight: 500;
width: 110px;
}
.cell-val { color: #1a1a2e; font-weight: 600; }
.cell-coilid { font-size: 13px; color: #1a365d; letter-spacing: 0.5px; }
.cell-ok { color: #2e7d32; font-weight: 700; text-align: center; }
.cell-ng { color: #c62828; font-weight: 700; text-align: center; }
.quality-table td { text-align: center; }
/* 图表 */
.rpt-chart-title {
font-size: 12px;
font-weight: 600;
color: #2c5282;
margin: 14px 0 4px;
padding-left: 6px;
border-left: 3px solid #2c5282;
}
.rpt-chart-subtitle {
font-size: 11px;
font-weight: normal;
color: #888;
margin-left: 8px;
}
.rpt-chart {
width: 100%;
height: 180px;
border: 1px solid #e8edf2;
border-radius: 3px;
background: #fafbfc;
}
/* 无数据 */
.rpt-no-data {
padding: 28px 0;
text-align: center;
font-size: 13px;
color: #c0c4cc;
}
/* 页脚 */
.rpt-footer {
margin-top: 20px;
padding-top: 8px;
border-top: 1px solid #e2e8f0;
text-align: center;
font-size: 10px;
color: #888;
}
/* 覆盖 dialog padding */
::v-deep .el-dialog__body { padding: 12px 20px 20px; }
::v-deep .el-button--primary {
background: #2c5282 !important; border-color: #2c5282 !important;
}
::v-deep .el-button--primary:hover { background: #1a365d !important; border-color: #1a365d !important; }
</style>

View File

@@ -1,4 +1,5 @@
<template>
<div>
<div class="acid-view">
<div class="filter-bar">
<el-input
@@ -38,6 +39,16 @@
</el-table-column>
<el-table-column prop="exit_thick" label="出口厚" />
<el-table-column prop="entry_weight" label="重量(t)" />
<el-table-column label="" width="70" align="center">
<template slot-scope="{ row }">
<el-button
type="text"
size="mini"
style="color:#2c5282;font-weight:600"
@click.stop="openQualityReport(row)"
>质保书</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-bar">
@@ -60,6 +71,16 @@
</div>
<template v-else>
<!-- 质保书按钮右侧详情区顶部 -->
<div style="display:flex;justify-content:flex-end;margin-bottom:8px">
<el-button
size="mini"
icon="el-icon-document"
style="color:#2c5282;border-color:#2c5282"
@click="openQualityReport(selectedPlan)"
>导出质保书</el-button>
</div>
<div class="detail-grid">
<div v-for="f in planFields" :key="f.key" class="detail-cell">
<span class="cell-label">{{ f.label }}</span>
@@ -84,6 +105,10 @@
</el-col>
</el-row>
</div>
<!-- 质保书 -->
<quality-report-dialog ref="qualityReport" />
</div>
</template>
<script>
@@ -93,6 +118,7 @@ import {
getTimingPlanCount,
getPlanWithSeg
} from '@/api/l2/timing'
import QualityReportDialog from './QualityReportDialog.vue'
const PLAN_FIELDS = [
{ key: 'status', label: '状态' },
@@ -136,6 +162,7 @@ function baseOption(title, xData, series, yName) {
export default {
name: 'TimingAcidPage',
components: { QualityReportDialog },
data() {
return {
loading: false,
@@ -271,6 +298,21 @@ export default {
this.resizeHandler = () => this.chartInstances.forEach(c => { if (c) c.resize() })
window.addEventListener('resize', this.resizeHandler)
},
openQualityReport(row) {
const dialog = this.$refs.qualityReport
// Pass currently loaded series if the row matches the selectedPlan
const sameCoil = this.selectedPlan &&
(row.encoilid || row.coilid) === (this.selectedPlan.encoilid || this.selectedPlan.coilid)
if (sameCoil && this.perfSeries) {
dialog.plan = { ...this.selectedPlan }
dialog.series = this.perfSeries
} else {
dialog.plan = row
dialog.series = null
}
dialog.visible = true
},
handleReset() {
this.queryForm.coilId = ''
this.selectedPlan = null