2026-04-25 18:19:29 +08:00
|
|
|
|
<template>
|
2026-04-27 20:37:59 +08:00
|
|
|
|
<div class="acid-view">
|
|
|
|
|
|
<div class="filter-bar">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="queryForm.coilId"
|
|
|
|
|
|
placeholder="热卷号 / 成品卷号"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style="width: 220px"
|
|
|
|
|
|
@keyup.enter.native="handleSearch"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-button size="small" type="primary" :loading="loading" @click="handleSearch">查询</el-button>
|
|
|
|
|
|
<el-button size="small" @click="handleReset">重置</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="12" class="main-row">
|
|
|
|
|
|
<!-- 左侧计划列表 -->
|
|
|
|
|
|
<el-col :span="9">
|
|
|
|
|
|
<div class="left-card">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
ref="planTable"
|
|
|
|
|
|
:data="planRows"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
highlight-current-row
|
|
|
|
|
|
:height="tableHeight"
|
|
|
|
|
|
@row-click="handlePlanRowClick"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="hot_coilid" label="热卷号" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="coilid" label="原料卷号" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column label="状态">
|
|
|
|
|
|
<template slot-scope="{ row }">
|
|
|
|
|
|
<span :class="['status-dot', statusClass(row.status)]" width="70px"/>
|
|
|
|
|
|
<span class="status-text">{{ row.status || '—' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="厚×宽">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.entry_thick }}×{{ row.entry_width }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="exit_thick" label="出口厚" />
|
|
|
|
|
|
<el-table-column prop="entry_weight" label="重量(t)" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pagination-bar">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
small
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:total="pagination.total"
|
|
|
|
|
|
:page-size="pagination.pageSize"
|
|
|
|
|
|
:current-page="pagination.page"
|
|
|
|
|
|
@current-change="handlePageChange"
|
2026-04-25 18:19:29 +08:00
|
|
|
|
/>
|
2026-04-27 20:37:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧详情 + 图表 -->
|
|
|
|
|
|
<el-col :span="15">
|
|
|
|
|
|
<div v-if="!selectedPlan" class="empty-hint">
|
|
|
|
|
|
<el-empty description="选择左侧计划" :image-size="72" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="detail-grid">
|
|
|
|
|
|
<div v-for="f in planFields" :key="f.key" class="detail-cell">
|
|
|
|
|
|
<span class="cell-label">{{ f.label }}</span>
|
|
|
|
|
|
<span class="cell-value">{{ selectedPlan[f.key] != null ? selectedPlan[f.key] : '—' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="perf-header">
|
|
|
|
|
|
实轧实绩
|
|
|
|
|
|
<span v-if="hasPerfData" class="perf-count">({{ perfSegCount }} 段)</span>
|
|
|
|
|
|
<el-tag v-if="perfLoading" size="mini" type="info" style="margin-left:8px">加载中…</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="hasPerfData" class="charts-wrap">
|
|
|
|
|
|
<div ref="chartSpeed" class="chart-box" />
|
|
|
|
|
|
<div ref="chartMillSpeed" class="chart-box" />
|
|
|
|
|
|
<div ref="chartTension" class="chart-box" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-empty v-else-if="!perfLoading" description="暂无实绩数据" :image-size="56" style="margin-top: 24px" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
2026-04-25 18:19:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-04-27 20:37:59 +08:00
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
import {
|
|
|
|
|
|
getTimingPlanList,
|
|
|
|
|
|
getTimingPlanCount,
|
|
|
|
|
|
getTimingPlanDetail,
|
|
|
|
|
|
getTimingSegByEncoilId
|
|
|
|
|
|
} from '@/api/l2/timing'
|
|
|
|
|
|
|
|
|
|
|
|
const PLAN_FIELDS = [
|
|
|
|
|
|
{ key: 'status', label: '状态' },
|
|
|
|
|
|
{ key: 'process_code', label: '工艺编码' },
|
|
|
|
|
|
{ key: 'entry_thick', label: '入口厚度(mm)' },
|
|
|
|
|
|
{ key: 'entry_width', label: '入口宽度(mm)' },
|
|
|
|
|
|
{ key: 'entry_weight', label: '入口重量(t)' },
|
|
|
|
|
|
{ key: 'exit_thick', label: '出口厚度(mm)' },
|
|
|
|
|
|
{ key: 'exit_width', label: '出口宽度(mm)' },
|
|
|
|
|
|
{ key: 'exit_length', label: '出口长度(m)' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_CLASS = {
|
|
|
|
|
|
READY: 'status-ready',
|
|
|
|
|
|
ONLINE: 'status-online',
|
|
|
|
|
|
PRODUCING: 'status-producing',
|
|
|
|
|
|
PRODUCT: 'status-product'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function makeLine(name, data) {
|
|
|
|
|
|
return { name, type: 'line', smooth: true, symbol: 'none', data }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function baseOption(title, xData, series, yName) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
|
|
|
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
|
|
|
legend: { top: 4, right: 8, textStyle: { fontSize: 11 } },
|
|
|
|
|
|
grid: { top: 36, bottom: 28, left: 8, right: 8, containLabel: true },
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
data: xData,
|
|
|
|
|
|
name: 'pos(m)',
|
|
|
|
|
|
nameTextStyle: { fontSize: 10 },
|
|
|
|
|
|
axisLabel: { fontSize: 10 }
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: { type: 'value', name: yName, nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 } },
|
|
|
|
|
|
series
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-25 18:19:29 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TimingAcidPage',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
loading: false,
|
2026-04-27 20:37:59 +08:00
|
|
|
|
perfLoading: false,
|
2026-04-25 18:19:29 +08:00
|
|
|
|
queryForm: { coilId: '' },
|
|
|
|
|
|
planRows: [],
|
|
|
|
|
|
selectedPlan: null,
|
2026-04-27 20:37:59 +08:00
|
|
|
|
perfSeries: null,
|
|
|
|
|
|
perfSegCount: 0,
|
|
|
|
|
|
planFields: PLAN_FIELDS,
|
|
|
|
|
|
pagination: { page: 1, pageSize: 50, total: 0 },
|
|
|
|
|
|
tableHeight: 'calc(100vh - 210px)'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
hasPerfData() {
|
|
|
|
|
|
return this.perfSeries && this.perfSegCount > 0
|
2026-04-25 18:19:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created() {
|
2026-04-27 20:37:59 +08:00
|
|
|
|
// plain instance property — Vue 2 does NOT proxy underscore-prefixed names
|
|
|
|
|
|
this.chartInstances = []
|
|
|
|
|
|
this.resizeHandler = null
|
|
|
|
|
|
this.loadPlanCount()
|
2026-04-25 18:19:29 +08:00
|
|
|
|
this.loadPlanList()
|
|
|
|
|
|
},
|
2026-04-27 20:37:59 +08:00
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
this.disposeCharts()
|
|
|
|
|
|
},
|
2026-04-25 18:19:29 +08:00
|
|
|
|
methods: {
|
2026-04-27 20:37:59 +08:00
|
|
|
|
statusClass(status) {
|
|
|
|
|
|
return STATUS_CLASS[status] || 'status-default'
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadPlanCount() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getTimingPlanCount()
|
|
|
|
|
|
this.pagination.total = res?.data?.total ?? 0
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
},
|
2026-04-25 18:19:29 +08:00
|
|
|
|
async loadPlanList() {
|
|
|
|
|
|
this.loading = true
|
|
|
|
|
|
try {
|
2026-04-27 20:37:59 +08:00
|
|
|
|
const { page, pageSize } = this.pagination
|
|
|
|
|
|
const res = await getTimingPlanList(page, pageSize)
|
|
|
|
|
|
this.planRows = res?.data?.rows || []
|
2026-04-25 18:19:29 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-27 20:37:59 +08:00
|
|
|
|
handlePageChange(page) {
|
|
|
|
|
|
this.pagination.page = page
|
|
|
|
|
|
this.loadPlanList()
|
|
|
|
|
|
},
|
2026-04-25 18:19:29 +08:00
|
|
|
|
async handleSearch() {
|
2026-04-27 20:37:59 +08:00
|
|
|
|
if (!this.queryForm.coilId) return this.loadPlanList()
|
2026-04-25 18:19:29 +08:00
|
|
|
|
this.loading = true
|
|
|
|
|
|
try {
|
2026-04-27 20:37:59 +08:00
|
|
|
|
const res = await getTimingPlanDetail(this.queryForm.coilId)
|
|
|
|
|
|
const row = res?.data?.firstRow || null
|
|
|
|
|
|
if (row) {
|
|
|
|
|
|
this.selectedPlan = row
|
|
|
|
|
|
await this.loadPerf(row)
|
2026-04-25 18:19:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async handlePlanRowClick(row) {
|
|
|
|
|
|
this.selectedPlan = row
|
2026-04-27 20:37:59 +08:00
|
|
|
|
this.perfSeries = null
|
|
|
|
|
|
this.perfSegCount = 0
|
|
|
|
|
|
this.disposeCharts()
|
|
|
|
|
|
await this.loadPerf(row)
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadPerf(plan) {
|
|
|
|
|
|
const encoilId = plan.encoilid || plan.coilid
|
|
|
|
|
|
if (!encoilId) return
|
|
|
|
|
|
this.perfLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getTimingSegByEncoilId(encoilId)
|
|
|
|
|
|
const series = res?.data?.series || null
|
|
|
|
|
|
const rows = res?.data?.rows || []
|
|
|
|
|
|
this.perfSegCount = rows.length
|
|
|
|
|
|
this.perfSeries = series
|
|
|
|
|
|
if (series && rows.length) {
|
|
|
|
|
|
await this.$nextTick()
|
|
|
|
|
|
this.renderCharts(series)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
this.perfSeries = null
|
|
|
|
|
|
this.perfSegCount = 0
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.perfLoading = false
|
2026-04-25 18:19:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-27 20:37:59 +08:00
|
|
|
|
disposeCharts() {
|
|
|
|
|
|
if (this.resizeHandler) {
|
|
|
|
|
|
window.removeEventListener('resize', this.resizeHandler)
|
|
|
|
|
|
this.resizeHandler = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.chartInstances && this.chartInstances.length) {
|
|
|
|
|
|
this.chartInstances.forEach(c => { if (c) c.dispose() })
|
|
|
|
|
|
this.chartInstances = []
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
renderCharts(series) {
|
|
|
|
|
|
this.disposeCharts()
|
|
|
|
|
|
const pick = key => (series[key] || []).map(v => v == null ? null : Number(v).toFixed(2))
|
|
|
|
|
|
const xData = (series.startpos || []).map(v => v == null ? '' : Number(v).toFixed(1))
|
|
|
|
|
|
|
|
|
|
|
|
const c1 = echarts.init(this.$refs.chartSpeed)
|
|
|
|
|
|
c1.setOption(baseOption(
|
|
|
|
|
|
'速度趋势 (m/min)', xData,
|
|
|
|
|
|
[
|
|
|
|
|
|
makeLine('轧制速度 plspeed', pick('plspeed')),
|
|
|
|
|
|
makeLine('剪切速度 trimspeed', pick('trimspeed'))
|
|
|
|
|
|
],
|
|
|
|
|
|
'm/min'
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const c2 = echarts.init(this.$refs.chartMillSpeed)
|
|
|
|
|
|
c2.setOption(baseOption(
|
|
|
|
|
|
'轧机出口速度 (m/min)', xData,
|
|
|
|
|
|
[makeLine('millexitspeed', pick('millexitspeed'))],
|
|
|
|
|
|
'm/min'
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
const c3 = echarts.init(this.$refs.chartTension)
|
|
|
|
|
|
c3.setOption(baseOption(
|
|
|
|
|
|
'张力趋势 (N)', xData,
|
|
|
|
|
|
[
|
|
|
|
|
|
makeLine('出口张力 pltens', pick('pltens')),
|
|
|
|
|
|
makeLine('入口张力 enltens', pick('enltens')),
|
|
|
|
|
|
makeLine('cxltens', pick('cxltens'))
|
|
|
|
|
|
],
|
|
|
|
|
|
'N'
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
this.chartInstances = [c1, c2, c3]
|
|
|
|
|
|
this.resizeHandler = () => this.chartInstances.forEach(c => { if (c) c.resize() })
|
|
|
|
|
|
window.addEventListener('resize', this.resizeHandler)
|
2026-04-25 18:19:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
handleReset() {
|
|
|
|
|
|
this.queryForm.coilId = ''
|
|
|
|
|
|
this.selectedPlan = null
|
2026-04-27 20:37:59 +08:00
|
|
|
|
this.perfSeries = null
|
|
|
|
|
|
this.perfSegCount = 0
|
|
|
|
|
|
this.disposeCharts()
|
|
|
|
|
|
this.pagination.page = 1
|
2026-04-25 18:19:29 +08:00
|
|
|
|
this.loadPlanList()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-04-27 20:37:59 +08:00
|
|
|
|
.filter-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.main-row {
|
|
|
|
|
|
margin: 0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-hint {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding-top: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
|
|
border-left: 1px solid #ebeef5;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-cell {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border-right: 1px solid #ebeef5;
|
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-label {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-bottom: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-value {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.perf-header {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.perf-count {
|
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.charts-wrap {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
max-height: calc(100vh - 310px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-box {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 状态指示点 */
|
|
|
|
|
|
.status-dot {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 7px;
|
|
|
|
|
|
height: 7px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-text {
|
|
|
|
|
|
font-size: 8px;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-ready { background: #909399; }
|
|
|
|
|
|
.status-online { background: #67c23a; }
|
|
|
|
|
|
.status-producing { background: #e6a23c; }
|
|
|
|
|
|
.status-product { background: #409eff; }
|
|
|
|
|
|
.status-default { background: #c0c4cc; }
|
2026-04-25 18:19:29 +08:00
|
|
|
|
</style>
|