数据贯通完成,规程重构

This commit is contained in:
2026-04-27 20:37:59 +08:00
parent 5b38ef734a
commit b7161e9541
13 changed files with 1492 additions and 861 deletions

View File

@@ -1,151 +1,204 @@
<template>
<div class="timing-page acid-page">
<el-card class="page-card" shadow="never">
<div slot="header" class="card-header">
<span>酸轧实绩页</span>
<el-tag type="success" size="mini">Plan + Seg + Quality</el-tag>
</div>
<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-form :inline="true" :model="queryForm" size="mini" class="query-form">
<el-form-item label="计划号">
<el-input v-model="queryForm.coilId" placeholder="输入 coilId" clearable style="width: 220px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<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>
<el-row :gutter="16">
<el-col :span="8">
<el-card shadow="never" class="sub-card">
<div slot="header" class="sub-header">计划列表</div>
<el-table
:data="planRows"
height="560"
size="mini"
highlight-current-row
@row-click="handlePlanRowClick"
>
<el-table-column prop="COILID" label="COILID" min-width="140" />
<el-table-column prop="HOT_COILID" label="热卷号" min-width="140" />
<el-table-column prop="STATUS" label="状态" width="90" />
</el-table>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="never" class="sub-card">
<div slot="header" class="sub-header">计划详情</div>
<el-empty v-if="!selectedPlan" description="请选择一条计划" />
<el-descriptions v-else :column="3" border size="mini">
<el-descriptions-item label="COILID">{{ selectedPlan.COILID }}</el-descriptions-item>
<el-descriptions-item label="HOT_COILID">{{ selectedPlan.HOT_COILID }}</el-descriptions-item>
<el-descriptions-item label="STATUS">{{ selectedPlan.STATUS }}</el-descriptions-item>
<el-descriptions-item label="ENTRY_THICK">{{ selectedPlan.ENTRY_THICK }}</el-descriptions-item>
<el-descriptions-item label="ENTRY_WIDTH">{{ selectedPlan.ENTRY_WIDTH }}</el-descriptions-item>
<el-descriptions-item label="ENTRY_WEIGHT">{{ selectedPlan.ENTRY_WEIGHT }}</el-descriptions-item>
<el-descriptions-item label="EXIT_THICK">{{ selectedPlan.EXIT_THICK }}</el-descriptions-item>
<el-descriptions-item label="EXIT_WIDTH">{{ selectedPlan.EXIT_WIDTH }}</el-descriptions-item>
<el-descriptions-item label="EXIT_LENGTH">{{ selectedPlan.EXIT_LENGTH }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" class="sub-card" style="margin-top: 16px;">
<div slot="header" class="sub-header">SEG 实绩</div>
<el-alert
v-if="!segView"
title="请选择计划后自动加载对应 SEG"
type="info"
:closable="false"
show-icon
<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"
/>
<template v-else>
<el-row :gutter="16">
<el-col :span="8">
<el-card shadow="never">
<div slot="header">SEGNO</div>
<div class="seg-list">
<el-tag
v-for="item in segView.segNo"
:key="item"
size="mini"
class="seg-tag"
>
{{ item }}
</el-tag>
</div>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="never">
<div slot="header">属性数组</div>
<el-table :data="seriesTable" size="mini" height="240" border>
<el-table-column prop="key" label="属性" width="180" />
<el-table-column prop="values" label="数组值" min-width="300">
<template slot-scope="scope">
<el-tag v-for="(item, index) in scope.row.values" :key="index" size="mini" style="margin-right: 4px; margin-bottom: 4px;">
{{ item }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</template>
</el-card>
</el-col>
</el-row>
</el-card>
</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>
</div>
</template>
<script>
import createTimingFetch from '@/api/l2/timing'
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
}
}
export default {
name: 'TimingAcidPage',
props: {
baseURL: { type: String, required: true }
},
data() {
return {
fetchApi: null,
loading: false,
perfLoading: false,
queryForm: { coilId: '' },
planRows: [],
selectedPlan: null,
segView: null,
seriesTable: []
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
}
},
created() {
this.fetchApi = createTimingFetch(this.baseURL)
// plain instance property — Vue 2 does NOT proxy underscore-prefixed names
this.chartInstances = []
this.resizeHandler = null
this.loadPlanCount()
this.loadPlanList()
},
beforeDestroy() {
this.disposeCharts()
},
methods: {
statusClass(status) {
return STATUS_CLASS[status] || 'status-default'
},
async loadPlanCount() {
try {
const res = await getTimingPlanCount()
this.pagination.total = res?.data?.total ?? 0
} catch (_) {}
},
async loadPlanList() {
this.loading = true
try {
const res = await this.fetchApi.getPlanList()
const rows = res?.data?.rows || res?.rows || []
this.planRows = rows
const { page, pageSize } = this.pagination
const res = await getTimingPlanList(page, pageSize)
this.planRows = res?.data?.rows || []
} finally {
this.loading = false
}
},
handlePageChange(page) {
this.pagination.page = page
this.loadPlanList()
},
async handleSearch() {
if (!this.queryForm.coilId) {
return this.loadPlanList()
}
if (!this.queryForm.coilId) return this.loadPlanList()
this.loading = true
try {
const res = await this.fetchApi.getPlanDetail(this.queryForm.coilId)
const row = res?.data?.firstRow || res?.firstRow || null
this.selectedPlan = row
if (row && row.ENCOILID) {
await this.loadSeg(row.ENCOILID)
const res = await getTimingPlanDetail(this.queryForm.coilId)
const row = res?.data?.firstRow || null
if (row) {
this.selectedPlan = row
await this.loadPerf(row)
}
} finally {
this.loading = false
@@ -153,25 +206,86 @@ export default {
},
async handlePlanRowClick(row) {
this.selectedPlan = row
if (row?.COILID) {
const detail = await this.fetchApi.getPlanDetail(row.COILID)
const plan = detail?.data?.firstRow || detail?.firstRow || row
this.selectedPlan = plan
const encoilId = plan.ENCOILID || plan.ENCOILID || plan.COILID
if (encoilId) await this.loadSeg(encoilId)
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
}
},
async loadSeg(encoilId) {
const res = await this.fetchApi.getSegByEncoilId(encoilId)
const view = res?.data || res
this.segView = view
this.seriesTable = Object.entries(view?.series || {}).map(([key, values]) => ({ key, values }))
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)
},
handleReset() {
this.queryForm.coilId = ''
this.selectedPlan = null
this.segView = null
this.seriesTable = []
this.perfSeries = null
this.perfSegCount = 0
this.disposeCharts()
this.pagination.page = 1
this.loadPlanList()
}
}
@@ -179,11 +293,115 @@ export default {
</script>
<style scoped>
.timing-page { padding: 16px; }
.page-card { border-radius: 12px; }
.card-header, .sub-header { display: flex; align-items: center; justify-content: space-between; font-weight: 600; }
.sub-card { border-radius: 10px; }
.query-form { margin-bottom: 12px; }
.seg-list { display: flex; flex-wrap: wrap; gap: 6px; }
.seg-tag { margin-right: 0; }
.acid-view {
padding: 12px 16px;
}
.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; }
</style>