停机时长修复

This commit is contained in:
2026-05-11 15:44:37 +08:00
parent dcb5f9525e
commit 7b1827ed83
8 changed files with 1633 additions and 389 deletions

View File

@@ -117,3 +117,28 @@ export function getRollHistoryCount(rollId, standId) {
params: { rollId, standId }
})
}
// 出口卷实绩列表分页PLTCM_PDO_EXCOIL
export function getExcoilList(page = 1, pageSize = 50) {
return request({
url: '/sql-server-api/excoil',
method: 'get',
params: { page, pageSize }
})
}
// 出口卷实绩总数
export function getExcoilCount() {
return request({
url: '/sql-server-api/excoil/count',
method: 'get'
})
}
// 工艺预设参数按计划钢卷号查询PLTCM_PRESET_SETUP
export function getPresetSetupByCoilId(coilId) {
return request({
url: '/sql-server-api/preset-setup/' + coilId,
method: 'get'
})
}

View File

@@ -0,0 +1,960 @@
<template>
<div class="actual-container">
<!-- 顶部实绩列表 (PLTCM_PDO_EXCOIL) -->
<div class="top-section">
<el-table
ref="excoilTable"
:data="excoilRows"
size="mini"
highlight-current-row
border
:height="topTableHeight"
style="width:100%"
@row-click="handleRowClick"
>
<el-table-column label="子卷号" min-width="110" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.EXCOILID || row.excoilid }}</template>
</el-table-column>
<el-table-column label="热卷号" min-width="100" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.HOT_COILID || row.hot_coilid || '—' }}</template>
</el-table-column>
<el-table-column label="班" width="40" align="center">
<template slot-scope="{ row }">{{ row.SHIFT || row.shift || '—' }}</template>
</el-table-column>
<el-table-column label="组" width="40" align="center">
<template slot-scope="{ row }">{{ row.CREW || row.crew || '—' }}</template>
</el-table-column>
<el-table-column label="钢种" min-width="80" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.ORDER_QUALITY || row.order_quality || row.GRADE || row.grade || '—' }}</template>
</el-table-column>
<el-table-column label="来料厚度" width="68" align="right">
<template slot-scope="{ row }">{{ row.ENTRY_THICK || row.entry_thick || '—' }}</template>
</el-table-column>
<el-table-column label="出口厚度" width="68" align="right">
<template slot-scope="{ row }">{{ row.EXIT_THICK || row.exit_thick || '—' }}</template>
</el-table-column>
<el-table-column label="偏差上限" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_POS_DEV || row.exit_pos_dev || '0' }}</template>
</el-table-column>
<el-table-column label="偏差下限" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_NEG_DEV || row.exit_neg_dev || '0' }}</template>
</el-table-column>
<el-table-column label="来料宽度" width="60" align="right">
<template slot-scope="{ row }">{{ row.ENTRY_WIDTH || row.entry_width || '—' }}</template>
</el-table-column>
<el-table-column label="出口宽度" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_WIDTH || row.exit_width || '—' }}</template>
</el-table-column>
<el-table-column label="来料重量" width="60" align="right">
<template slot-scope="{ row }">{{ row.USED_ENTRY_WEIGHT || row.used_entry_weight || row.ENTRY_WEIGHT || row.entry_weight || '—' }}</template>
</el-table-column>
<el-table-column label="称重重量" width="60" align="right">
<template slot-scope="{ row }">{{ row.MEAS_EXIT_WEIGHT || row.meas_exit_weight || '—' }}</template>
</el-table-column>
<el-table-column label="包装要求" width="60">
<template slot-scope="{ row }">{{ row.PARK_TYPE || row.park_type || '—' }}</template>
</el-table-column>
<el-table-column label="切边要求" width="60">
<template slot-scope="{ row }">{{ row.SIDE_TRIM || row.side_trim || '—' }}</template>
</el-table-column>
<el-table-column label="成品质量" width="58" align="right">
<template slot-scope="{ row }">{{ row.QUALITY || row.quality || '—' }}</template>
</el-table-column>
<el-table-column label="成品长度" width="60" align="right">
<template slot-scope="{ row }">{{ row.EXIT_LENGTH || row.exit_length || '—' }}</template>
</el-table-column>
<el-table-column label="吨钢长度" width="60" align="right">
<template slot-scope="{ row }">{{ calcLengthPerTon(row) }}</template>
</el-table-column>
<el-table-column label="下线时间" width="140">
<template slot-scope="{ row }">{{ formatDate(row.END_DATE || row.end_date) }}</template>
</el-table-column>
<el-table-column label="状态" width="68" align="center">
<template slot-scope="{ row }">
<el-tag type="primary" size="mini" effect="plain">{{ row.STATUS || row.status || '产出' }}</el-tag>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
small layout="total, prev, pager, next"
:total="pagination.total"
:page-size="pagination.pageSize"
:current-page="pagination.page"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 底部图表区域 -->
<div class="bottom-section">
<div class="chart-section">
<el-tabs v-model="activeTab" size="small" class="chart-tabs" @tab-click="handleTabSwitch">
<!-- 趋势参数左树形目录 + 右单图 -->
<el-tab-pane label="趋势参数" name="trend">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="segLoading" class="no-data-hint">加载中</div>
<div v-else-if="!segData" class="no-data-hint">暂无 SEG 数据</div>
<div v-else class="trend-layout">
<!-- 左侧目录树 -->
<div class="trend-tree">
<div v-for="group in trendGroups" :key="group.label" class="tree-group">
<div class="tree-group-label" @click="toggleGroup(group.label)">
<i :class="expandedGroups[group.label] ? 'el-icon-caret-bottom' : 'el-icon-caret-right'" />
{{ group.label }}
</div>
<div v-show="expandedGroups[group.label]" class="tree-children">
<div
v-for="item in group.children"
:key="item.col"
class="tree-item"
:class="{ active: selectedTrendParam && selectedTrendParam.col === item.col }"
@click="selectTrendParam(item)"
>{{ item.label }}</div>
</div>
</div>
</div>
<!-- 右侧图表 -->
<div class="trend-chart-area">
<div v-if="!selectedTrendParam" class="no-data-hint"> 点击左侧参数查看曲线</div>
<!-- 保持 DOM 存在仅用 v-show 控制显示避免 ref 失效 -->
<div ref="trendSingleChart" :style="{ display: selectedTrendParam ? 'block' : 'none', height: '100%', width: '100%' }" />
</div>
</div>
</el-tab-pane>
<!-- 厚度曲线 -->
<el-tab-pane label="厚度曲线" name="thickness">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
<div v-else-if="!gaugeRows || !gaugeRows.length" class="no-data-hint">暂无厚度数据</div>
<div v-else class="charts-scroll charts-grid">
<div ref="chartGauge1" class="chart-box" />
<div ref="chartGauge2" class="chart-box" />
<div ref="chartGauge3" class="chart-box" />
<div ref="chartGauge4" class="chart-box" />
</div>
</el-tab-pane>
<!-- 带钢板形 热力图 -->
<el-tab-pane label="带钢板形" name="flatness3d">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
<div v-else-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
<div v-else class="charts-scroll">
<div ref="chartFlatness3d" class="chart-box chart-box-tall" />
</div>
</el-tab-pane>
<!-- 板形曲线一行两图 -->
<el-tab-pane label="板形曲线" name="flatness">
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
<div v-else-if="realtimeLoading" class="no-data-hint">加载中</div>
<div v-else-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
<div v-else class="charts-scroll charts-grid">
<div ref="chartFlatDev" class="chart-box" />
<div ref="chartTilt" class="chart-box" />
<div ref="chartWrBend" class="chart-box" />
<div ref="chartIrBend" class="chart-box" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 右侧查找面板 -->
<div class="search-panel">
<div class="panel-title">查找</div>
<div class="search-type-group">
<el-radio v-model="searchType" label="coil">按钢卷号</el-radio>
<div v-if="searchType === 'coil'" class="search-field">
<span class="search-label">钢卷号</span>
<el-input v-model="searchCoilId" size="mini" style="width:140px" placeholder="EXCOILID" />
</div>
</div>
<div class="search-type-group">
<el-radio v-model="searchType" label="time">按时间</el-radio>
<template v-if="searchType === 'time'">
<div class="search-field">
<span class="search-label">开始时间</span>
<el-date-picker v-model="searchStartDate" type="datetime" size="mini" style="width:160px"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" />
</div>
<div class="search-field">
<span class="search-label">结束时间</span>
<el-date-picker v-model="searchEndDate" type="datetime" size="mini" style="width:160px"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" />
</div>
</template>
</div>
<div class="search-actions">
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
<el-button size="mini" @click="handleFindReset">重置</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import 'echarts-gl'
import {
getExcoilList,
getExcoilCount,
getTimingSegByEncoilId,
getTimingRealtimeData
} from '@/api/l2/timing'
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
const TREND_GROUPS = [
{
label: '张力',
children: [
{ label: '开卷张力', col: 'PORTENS' },
{ label: '入口活套张力', col: 'ENLTENS' },
{ label: '拉矫张力', col: 'TLTENS' },
{ label: '酸洗张力', col: 'PLTENS' },
{ label: '出口活套张力', col: 'CXLTENS' },
{ label: '圆盘剪张力', col: 'TRIMTENS' }
]
},
{
label: '速度',
children: [
{ label: '开卷速度', col: 'PORSPEED' },
{ label: '酸洗速度', col: 'PLSPEED' },
{ label: '圆盘剪速度', col: 'TRIMSPEED' },
{ label: '轧机入口速度', col: 'MILLENTRYSPEED' },
{ label: '轧机出口速度', col: 'MILLEXITSPEED' }
]
},
{
label: '拉矫机',
children: [
{ label: '1#插入量', col: 'TLMESH1' },
{ label: '2#插入量', col: 'TLMESH2' },
{ label: '3#插入量', col: 'TLMESH3' },
{ label: '延伸率', col: 'TLELONG' }
]
},
{
label: '酸洗段',
children: [
{ label: '1#温度', col: 'TK1TEMP' },
{ label: '2#温度', col: 'TK2TEMP' },
{ label: '3#温度', col: 'TK3TEMP' },
{ label: '漂洗温度', col: 'RINSETEMP' }
]
}
]
// V_VBDA_GAUGE 厚度曲线4 个图,列名来自 DDL
const GAUGE_COLS = [
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
]
// V_VBDA_SHAPE 板形曲线4 个图,列名来自 DDL
const SHAPE_SCALAR_COLS = [
{ col: 'ABSDEVIATION', title: '总板形偏差 [IU]' },
{ col: 'TILT', title: '末架倾斜量 [mm]' },
{ col: 'WRBEND', title: '工作辊弯辊力 [kN]' },
{ col: 'IRBEND', title: '中间辊弯辊力 [kN]' }
]
/**
* 从数据数组计算合理的 Y 轴范围(带 15% 上下边距)。
* echarts 默认 scale 在数据变化幅度极小时会把轴拉到很大范围,导致曲线看起来是横线。
*/
function calcYRange(vals) {
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
if (!nums.length) return {}
const min = Math.min(...nums)
const 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 makeLine(title, xData, yData) {
const range = calcYRange(yData)
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
tooltip: { trigger: 'axis' },
grid: { top: 36, bottom: 28, left: 8, right: 16, containLabel: true },
xAxis: {
type: 'category', data: xData,
name: 'pos(m)', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
},
yAxis: {
type: 'value',
min: range.min,
max: range.max,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 10 }
},
dataZoom: [
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
{ type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseWheel: true }
],
series: [{
name: title, type: 'line', smooth: false, symbol: 'none',
lineStyle: { width: 1 }, data: yData
}]
}
}
function getRowVal(row, col) {
const v = row[col] !== undefined ? row[col] : row[col.toLowerCase()]
return v == null ? null : Number(v)
}
function xLocData(rows) {
return rows.map(r => {
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
return v == null ? '' : Number(v).toFixed(1)
})
}
export default {
name: 'ActualPerformance',
data() {
return {
excoilLoading: false,
segLoading: false,
realtimeLoading: false,
excoilRows: [],
selectedRow: null,
segData: null,
gaugeRows: null,
shapeRows: null,
activeTab: 'trend',
// 趋势参数树状态
trendGroups: TREND_GROUPS,
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
selectedTrendParam: null,
trendChartInst: null,
// 查找
searchType: 'coil',
searchCoilId: '',
searchStartDate: '',
searchEndDate: '',
pagination: { page: 1, pageSize: 50, total: 0 },
topTableHeight: 'calc(40vh - 80px)',
chartInstances: [],
resizeHandler: null
}
},
created() {
this.loadExcoilCount()
this.loadExcoilList()
},
beforeDestroy() {
this.disposeAllCharts()
},
methods: {
async loadExcoilCount() {
try {
const res = await getExcoilCount()
this.pagination.total = res?.data?.total ?? 0
} catch (_) {}
},
async loadExcoilList() {
this.excoilLoading = true
try {
const res = await getExcoilList(this.pagination.page, this.pagination.pageSize)
this.excoilRows = res?.data?.rows || []
} finally {
this.excoilLoading = false
}
},
handlePageChange(page) {
this.pagination.page = page
this.loadExcoilList()
},
async handleRowClick(row) {
this.selectedRow = row
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.selectedTrendParam = null
this.disposeAllCharts()
// PLTCM_PRO_SEG 用 ENCOILID 查(主键前缀)
const encoilId = row.ENCOILID || row.encoilid
// V_VBDA_GAUGE / V_VBDA_SHAPE 用 EXCOILID 作为 MATID
const excoilId = row.EXCOILID || row.excoilid
await Promise.all([
encoilId ? this.loadSeg(encoilId) : Promise.resolve(),
excoilId ? this.loadRealtime(excoilId) : Promise.resolve()
])
await this.$nextTick()
// 加载完成后自动选中第一个趋势参数
if (this.activeTab === 'trend' && this.segData) {
this.selectTrendParam(TREND_GROUPS[0].children[0])
} else {
this.renderCurrentTab()
}
},
async loadSeg(encoilId) {
this.segLoading = true
try {
const res = await getTimingSegByEncoilId(encoilId)
const rows = res?.data?.rows || []
this.segData = rows.length ? (res?.data?.series || null) : null
} catch (_) {
} finally {
this.segLoading = false
}
},
async loadRealtime(excoilId) {
this.realtimeLoading = true
try {
const res = await getTimingRealtimeData(excoilId)
const g = res?.data?.gauge?.result
const s = res?.data?.shape?.result
this.gaugeRows = Array.isArray(g) ? g : null
this.shapeRows = Array.isArray(s) ? s : null
} catch (_) {
} finally {
this.realtimeLoading = false
}
},
// ── 趋势参数树 ──────────────────────────────
toggleGroup(label) {
this.$set(this.expandedGroups, label, !this.expandedGroups[label])
},
isGroupExpanded(label) {
return !!this.expandedGroups[label]
},
selectTrendParam(item) {
this.selectedTrendParam = item
this.$nextTick(() => this.renderTrendSingleChart())
},
renderTrendSingleChart() {
if (!this.selectedTrendParam || !this.segData) return
const el = this.$refs.trendSingleChart
if (!el) return
// 复用已有实例,避免重复 init
if (!this.trendChartInst || this.trendChartInst.isDisposed()) {
this.trendChartInst = echarts.init(el)
// 滚轮缩放支持
const resizeFn = () => this.trendChartInst && !this.trendChartInst.isDisposed() && this.trendChartInst.resize()
window.addEventListener('resize', resizeFn)
this._trendResizeFn = resizeFn
}
const x = this.segX()
const yData = this.seg(this.selectedTrendParam.col)
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData), true)
},
// ── Tab 切换 ────────────────────────────────
handleTabSwitch() {
this.$nextTick(() => {
if (this.activeTab === 'trend' && this.selectedTrendParam && this.segData) {
this.renderTrendSingleChart()
} else {
this.renderCurrentTab()
}
})
},
renderCurrentTab() {
this.disposeSideCharts()
if (this.activeTab === 'thickness' && this.gaugeRows?.length) this.renderGaugeCharts()
if (this.activeTab === 'flatness3d' && this.shapeRows?.length) this.renderFlatness3d()
if (this.activeTab === 'flatness' && this.shapeRows?.length) this.renderFlatnessCharts()
},
// ── 销毁 ────────────────────────────────────
disposeSideCharts() {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler)
this.resizeHandler = null
}
this.chartInstances.forEach(c => { if (c && !c.isDisposed()) c.dispose() })
this.chartInstances = []
},
disposeAllCharts() {
this.disposeSideCharts()
if (this._trendResizeFn) {
window.removeEventListener('resize', this._trendResizeFn)
this._trendResizeFn = null
}
if (this.trendChartInst && !this.trendChartInst.isDisposed()) {
this.trendChartInst.dispose()
this.trendChartInst = null
}
},
// ── SEG 数据辅助 ─────────────────────────────
seg(col) {
const s = this.segData
// Oracle 返回大写列名,兼容小写
const arr = s[col] !== undefined ? s[col] : (s[col.toLowerCase()] || [])
return arr.map(v => v == null ? null : Number(Number(v).toFixed(3)))
},
segX() {
const s = this.segData
const arr = s['STARTPOS'] !== undefined ? s['STARTPOS'] : (s['startpos'] || [])
return arr.map(v => v == null ? '' : Number(v).toFixed(1))
},
// ── 图表初始化 ───────────────────────────────
makeChart(ref, option) {
const el = this.$refs[ref]
if (!el) return null
const chart = echarts.init(el)
chart.setOption(option)
return chart
},
setupResize() {
this.resizeHandler = () => this.chartInstances.forEach(c => {
if (c && !c.isDisposed()) c.resize()
})
window.addEventListener('resize', this.resizeHandler)
},
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
renderGaugeCharts() {
const rows = this.gaugeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = refs.map((ref, i) => {
const { col, title } = GAUGE_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
return this.makeChart(ref, makeLine(title, xData, yData))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
},
// ── 带钢板形 3D 线图 (V_VBDA_SHAPE) ────────────
// 每个通道画一条独立 line3D通道之间不连面形成镂空效果
renderFlatness3d() {
const rows = this.shapeRows
if (!rows || !rows.length) return
const firstRow = rows[0]
const high = parseInt(getRowVal(firstRow, 'HIGHZONEID')) || 26
const low = parseInt(getRowVal(firstRow, 'LOWZONEID')) || 1
const numZones = Math.min(Math.max(high - low + 1, 1), 26)
const zoneCols = Array.from({ length: numZones }, (_, i) =>
`VALUES${String(low + i).padStart(2, '0')}`
)
// X 方向降采样,最多 200 个点
const step = Math.max(1, Math.floor(rows.length / 200))
const sampled = rows.filter((_, i) => i % step === 0)
const numX = sampled.length
// X 轴标签(位置,单位 m
const xLabels = sampled.map(r => {
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
return v == null ? '' : Number(v).toFixed(0)
})
// 收集值域用于 visualMap
let minV = Infinity, maxV = -Infinity
sampled.forEach(row => {
zoneCols.forEach(col => {
const v = getRowVal(row, col)
if (v != null) {
if (v < minV) minV = v
if (v > maxV) maxV = v
}
})
})
if (!isFinite(minV)) { minV = -30; maxV = 30 }
const absMax = Math.max(Math.abs(minV), Math.abs(maxV))
// ① 沿 X 方向网格线(每通道一条,按 Z 值着色)
const channelLines = zoneCols.map((col, yi) => ({
type: 'line3D',
coordinateSystem: 'cartesian3D',
data: sampled.map((row, xi) => {
const v = getRowVal(row, col)
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
}).filter(Boolean),
lineStyle: { width: 2, opacity: 1 }
}))
// ② 沿 Y 方向网格线(每隔若干位置连通各通道,按 Z 值着色)
const xStride = Math.max(1, Math.floor(numX / 60))
const crossLines = []
for (let xi = 0; xi < numX; xi += xStride) {
const pts = zoneCols.map((col, yi) => {
const v = getRowVal(sampled[xi], col)
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
}).filter(Boolean)
if (pts.length > 1) {
crossLines.push({
type: 'line3D',
coordinateSystem: 'cartesian3D',
data: pts,
lineStyle: { width: 1.5, opacity: 1 }
})
}
}
const series = [...channelLines, ...crossLines]
const option = {
title: { text: '实测平直度 [IU]', textStyle: { fontSize: 13, fontWeight: 'normal' }, top: 6, left: 10 },
tooltip: {},
visualMap: {
show: true,
dimension: 2,
min: -absMax,
max: absMax,
calculable: true,
orient: 'vertical',
right: 10,
top: 'center',
textStyle: { fontSize: 10 },
inRange: {
// 负值红色 → 零值绿色 → 正值蓝紫
color: ['#8B0000','#CC2200','#E84C00','#F46D43',
'#FDAE61','#FEE08B',
'#66BD63','#1A9850','#006837',
'#3288BD','#5E4FA2','#762A83']
}
},
grid3D: {
boxWidth: 200,
boxHeight: 60,
boxDepth: 80,
viewControl: {
projection: 'orthographic',
autoRotate: false,
rotateSensitivity: 1,
zoomSensitivity: 1
},
light: {
main: { intensity: 1.2, shadow: false },
ambient: { intensity: 0.3 }
}
},
xAxis3D: {
type: 'value',
name: '位置',
min: 0,
max: numX - 1,
nameTextStyle: { fontSize: 10 },
axisLabel: {
fontSize: 9,
formatter: v => xLabels[Math.round(v)] || ''
}
},
yAxis3D: {
type: 'value',
name: '通道',
min: 0,
max: numZones - 1,
nameTextStyle: { fontSize: 10 },
axisLabel: {
fontSize: 9,
formatter: v => String(low + Math.round(v))
}
},
zAxis3D: {
type: 'value',
name: 'IU',
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 9 }
},
series
}
const el = this.$refs.chartFlatness3d
if (!el) return
const chart = echarts.init(el)
chart.setOption(option)
this.chartInstances = [chart]
this.setupResize()
},
// ── 板形曲线 (V_VBDA_SHAPE) ──────────────────
renderFlatnessCharts() {
const rows = this.shapeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartFlatDev', 'chartTilt', 'chartWrBend', 'chartIrBend']
const charts = refs.map((ref, i) => {
const { col, title } = SHAPE_SCALAR_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(3))
})
return this.makeChart(ref, makeLine(title, xData, yData))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
},
// ── 查找 ─────────────────────────────────────
handleFindSearch() {
if (this.searchType === 'coil' && this.searchCoilId) {
const found = this.excoilRows.find(r =>
(r.EXCOILID || r.excoilid || '').includes(this.searchCoilId)
)
if (found) {
this.$refs.excoilTable && this.$refs.excoilTable.setCurrentRow(found)
this.handleRowClick(found)
} else {
this.$message.info('当前页未找到该卷号,请翻页查找')
}
} else {
this.pagination.page = 1
this.loadExcoilList()
}
},
handleFindReset() {
this.searchCoilId = ''
this.searchStartDate = ''
this.searchEndDate = ''
this.selectedRow = null
this.segData = null
this.gaugeRows = null
this.shapeRows = null
this.selectedTrendParam = null
this.disposeAllCharts()
this.pagination.page = 1
this.loadExcoilCount()
this.loadExcoilList()
},
calcLengthPerTon(row) {
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
if (!len || !wt || wt === 0) return '—'
return (len / wt).toFixed(2)
},
formatDate(v) {
if (!v) return '—'
return String(v).replace('T', ' ').substring(0, 19)
}
}
}
</script>
<style scoped lang="scss">
.actual-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 8px;
background: #f0f2f5;
gap: 6px;
box-sizing: border-box;
}
.top-section {
flex-shrink: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
overflow: hidden;
}
.table-pagination {
display: flex;
justify-content: flex-end;
padding: 4px 8px;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
.bottom-section {
flex: 1;
min-height: 0;
display: flex;
gap: 6px;
}
.chart-section {
flex: 1;
min-height: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
padding: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
.chart-tabs {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
::v-deep .el-tabs__content {
flex: 1;
overflow: hidden;
min-height: 0;
padding: 0;
}
::v-deep .el-tab-pane {
height: 100%;
}
}
}
/* ── 趋势参数:左树 + 右图 ── */
.trend-layout {
display: flex;
height: 100%;
gap: 0;
}
.trend-tree {
width: 140px;
flex-shrink: 0;
overflow-y: auto;
border-right: 1px solid #ebeef5;
padding: 4px 0;
&::-webkit-scrollbar { width: 3px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
}
.tree-group {
user-select: none;
}
.tree-group-label {
display: flex;
align-items: center;
gap: 4px;
padding: 5px 8px;
font-size: 12px;
font-weight: 600;
color: #303133;
cursor: pointer;
&:hover { background: #f5f7fa; }
i { font-size: 10px; color: #909399; }
}
.tree-children { padding-left: 4px; }
.tree-item {
padding: 4px 8px 4px 18px;
font-size: 12px;
color: #606266;
cursor: pointer;
border-radius: 3px;
margin: 1px 4px;
&:hover { background: #ecf5ff; color: #409eff; }
&.active {
background: #ecf5ff;
color: #409eff;
font-weight: 500;
}
}
.trend-chart-area {
flex: 1;
min-width: 0;
height: 100%;
padding: 4px 4px 4px 8px;
display: flex;
align-items: stretch;
}
/* ── 其他图表 ── */
.charts-scroll {
height: 100%;
overflow-y: auto;
padding: 4px;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
}
/* 一行两图 */
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
align-content: start;
.chart-box { margin-bottom: 0; }
}
.chart-box {
width: 100%;
height: 200px;
margin-bottom: 8px;
}
.chart-box-tall { height: 480px; }
/* ── 查找面板 ── */
.search-panel {
width: 210px;
flex-shrink: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
padding: 10px;
display: flex;
flex-direction: column;
gap: 14px;
overflow-y: auto;
}
.panel-title {
font-size: 13px;
font-weight: 600;
color: #303133;
padding-bottom: 6px;
border-bottom: 1px solid #ebeef5;
}
.search-type-group { display: flex; flex-direction: column; gap: 8px; }
.search-field {
display: flex;
flex-direction: column;
gap: 4px;
padding-left: 20px;
}
.search-label { font-size: 11px; color: #909399; }
.search-actions {
display: flex;
gap: 8px;
margin-top: auto;
}
.no-data-hint {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 120px;
color: #c0c4cc;
font-size: 13px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,11 @@
<span slot="title">品质</span>
</el-menu-item>
<el-menu-item index="performance">
<i class="el-icon-coin"></i>
<i class="el-icon-date"></i>
<span slot="title">计划</span>
</el-menu-item>
<el-menu-item index="actualPerformance">
<i class="el-icon-data-analysis"></i>
<span slot="title">实绩</span>
</el-menu-item>
<el-menu-item index="rollConfig">
@@ -42,13 +46,9 @@
<i class="el-icon-warning"></i>
<span slot="title">停机</span>
</el-menu-item>
<!-- <el-menu-item index="realTime">
<i class="el-icon-time"></i>
<span slot="title">实时</span>
</el-menu-item> -->
</el-menu>
</div>
<div style="flex: 1;">
<div style="flex: 1; overflow: hidden;">
<component :is="currentComponent" />
</div>
</div>
@@ -61,7 +61,7 @@ import Report from './components/Report.vue';
import Shipping from './components/Shipping.vue';
import Quality from './components/Quality.vue';
import Performance from './components/Performance.vue';
import RealTime from './components/RealTime.vue';
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';
@@ -75,7 +75,7 @@ export default {
Shipping,
Quality,
Performance,
RealTime,
ActualPerformance,
RollConfig,
RollHistory,
Stoppage
@@ -94,7 +94,7 @@ export default {
shipping: 'Shipping',
quality: 'Quality',
performance: 'Performance',
realTime: 'RealTime',
actualPerformance: 'ActualPerformance',
rollConfig: 'RollConfig',
rollHistory: 'RollHistory',
stoppage: 'Stoppage',