@@ -19,7 +19,12 @@
< text class = "refresh-icon" : class = "{ rotating: isRefreshing }" > ⟳ < / text >
< / view >
< scroll-view scroll -y class = "scroll-container" v-if = "currentTab === 1" @scroll="onScroll" >
<!-- Tab1 : 实时监控 -- >
< scroll-view
scroll -y
class = "scroll-container"
v-if = "currentTab === 1"
>
<!-- 顶部状态栏 -- >
< view class = "status-bar" >
< view class = "status-item" >
@@ -36,50 +41,213 @@
< text class = "status-label" > 设备数 < / text >
< text class = "status-value" > { { deviceDefs . length } } < / text >
< / view >
< view class = "status-divider" > < / view >
< view class = "status-item status-toggle" @click ="toggleChartPause" >
< text class = "status-label" > 折线图 < / text >
< text
class = "status-value status-time"
: class = "isChartPaused ? 'status-异常' : 'status-通畅'"
>
{ { isChartPaused ? '已暂停刷新' : '实时刷新中' } }
< / text >
< / view >
< / view >
< view v-for = "dev in deviceDefs" :key="'device_' + dev.deviceCode" :id="'device-' + dev.deviceCode" class="device-section" >
< view class = "section-title" > { { dev . desc } } ( { { dev . deviceCode } } ) < / view >
< view v-if = "fieldTrendMap[dev.deviceCode]" >
< view v-for = "fieldName in (dev.paramFields || [])" :key="dev.deviceCode + '_' + fieldName" class="field-chart-container" >
< view v-if = "fieldTrendMap[dev.deviceCode][fieldName]" >
< view class = "chart-header" >
< text class = "chart-title" > { { getFieldLabel ( fieldName ) } } < / text >
< text class = "chart-unit" > { { getFieldUnit ( fieldName ) } } < / text >
< / view >
< view class = "chart-box" >
< qiun-data-charts type = "line" : chartData = "toLineChart(fieldTrendMap[dev.deviceCode][fieldName], fieldName)" :opts = "lineChartOpts" / >
< / view >
< view class = "stats-grid" >
< view class = "stat-item" > < text class = "stat-label" > AVG < / text > < text class = "stat-value" > { { formatNum ( fieldTrendMap [ dev . deviceCode ] [ fieldName ] . avg ) } } < / text > < / view >
< view class = "stat-item" > < text class = "stat-label" > MAX < / text > < text class = "stat-value" > { { formatNum ( fieldTrendMap [ dev . deviceCode ] [ fieldName ] . max ) } } < / text > < / view >
< view class = "stat-item" > < text class = "stat-label" > MIN < / text > < text class = "stat-value" > { { formatNum ( fieldTrendMap [ dev . deviceCode ] [ fieldName ] . min ) } } < / text > < / view >
< view class = "stat-item" > < text class = "stat-label" > LAST < / text > < text class = "stat-value" > { { formatNum ( fieldTrendMap [ dev . deviceCode ] [ fieldName ] . last ) } } < / text > < / view >
< / view >
< / view >
< view v-else class = "placeholder-box" >
< text class = "placeholder-text" > 等待 { { getFieldLabel ( fieldName ) } } 数据 ... < / text >
< / view >
<!-- 实时数据卡片 + 统计图 ( 按酸轧页风格 , 不按设备分块 ) -- >
< view class = "section" >
< view class = "section-title" > 入口段 ( 实时 ) < / view >
< view class = "metrics-grid-3" >
< view class = "metric-box" v-for = "it in entryMetrics" :key="'entry_' + it.field" >
< text class = "metric-name" > { { it . label || getFieldLabel ( it . field ) } } < / text >
< text class = "metric-value" > { { getRealtimeValueBySource ( 'ENTRY' , it . field ) } } < / text >
< text class = "metric-unit" > { { it . unit || getFieldUnit ( it . field ) } } < / text >
< / view >
< / view >
< view class = "chart-box" >
< qiun-data-charts type = "line" :chartData = "toGroupLineChart('entry')" :opts = "lineChartOpts" / >
< / view >
< / view >
< view v-else class = "placeholder-box " >
< text class = "placeholder-text" > 等待 { { dev . desc } } 数据 ... ( 滚动到此处加载 ) < / text >
< view class = "section " >
< view class = "section-title" > 退火炉段 ( 实时 ) < / view >
< view class = "metrics-grid-3" >
< view class = "metric-box" v-for = "it in furnaceMetrics" :key="'furnace_' + it.field" >
< text class = "metric-name" > { { it . label || getFieldLabel ( it . field ) } } < / text >
< text class = "metric-value" > { { getRealtimeValueBySource ( 'FURNACE' , it . field ) } } < / text >
< text class = "metric-unit" > { { it . unit || getFieldUnit ( it . field ) } } < / text >
< / view >
< / view >
< view class = "chart-box" >
< qiun-data-charts type = "line" :chartData = "toGroupLineChart('furnace')" :opts = "lineChartOpts" / >
< / view >
< / view >
< view class = "section" >
< view class = "section-title" > 后处理 / 涂层段 ( 实时 ) < / view >
< view class = "metrics-grid-3" >
< view class = "metric-box" v-for = "it in coatMetrics" :key="'coat_' + it.field" >
< text class = "metric-name" > { { it . label || getFieldLabel ( it . field ) } } < / text >
< text class = "metric-value" > { { getRealtimeValueBySource ( 'COAT' , it . field ) } } < / text >
< text class = "metric-unit" > { { it . unit || getFieldUnit ( it . field ) } } < / text >
< / view >
< / view >
< view class = "chart-box" >
< qiun-data-charts type = "line" :chartData = "toGroupLineChart('coat')" :opts = "lineChartOpts" / >
< / view >
< / view >
< view class = "section" >
< view class = "section-title" > 出口段 ( 实时 ) < / view >
< view class = "metrics-grid-3" >
< view class = "metric-box" v-for = "it in exitMetrics" :key="'exit_' + it.field" >
< text class = "metric-name" > { { it . label || getFieldLabel ( it . field ) } } < / text >
< text class = "metric-value" > { { getRealtimeValueBySource ( 'EXIT' , it . field ) } } < / text >
< text class = "metric-unit" > { { it . unit || getFieldUnit ( it . field ) } } < / text >
< / view >
< / view >
< view class = "chart-box" >
< qiun-data-charts type = "line" :chartData = "toGroupLineChart('exit')" :opts = "lineChartOpts" / >
< / view >
< / view >
< / scroll-view >
<!-- 生产统计 / 实绩 -- >
< scroll-view scroll -y class = "scroll-container" v-if = "currentTab === 2" >
< view class = "section" >
< view class = "section-title" > 当前生产情况 < / view >
< view class = "chart-box" v-if = "currentPlan" >
< view class = "stats-table" >
< view class = "stats-header" >
< text class = "stats-col" > 当前钢卷 < / text >
< text class = "stats-col" > 钢种 < / text >
< text class = "stats-col" > 规格 ( 厚 × 宽 ) < / text >
< text class = "stats-col" > 计划长度 < / text >
< text class = "stats-col" > 计划重量 < / text >
< / view >
< view class = "stats-row" >
< text class = "stats-col" > { { currentPlan . exitMatId || '—' } } < / text >
< text class = "stats-col" > { { currentPlan . steelGrade || '—' } } < / text >
< text class = "stats-col" >
{ { formatNum ( currentPlan . exitThickness ) } } × { { formatNum ( currentPlan . exitWidth ) } }
< / text >
< text class = "stats-col" > { { formatNum ( currentPlan . exitLength ) } } < / text >
< text class = "stats-col" > { { formatNum ( currentPlan . exitWeight ) } } < / text >
< / view >
< / view >
< view class = "history-tip" v-if = "!currentPlan" >
< text > 暂无当前生产计划数据 。 < / text >
< / view >
< / view >
< / view >
< view class = "section" >
< view class = "section-title" > 生产实绩汇总 < / view >
< view class = "overview-grid" v-if = "reportSummary" >
< view class = "overview-card" >
< text class = "overview-label" > 钢卷总数 < / text >
< text class = "overview-value" > { { reportSummary . coilCount || 0 } } < / text >
< text class = "overview-unit" > 卷 < / text >
< / view >
< view class = "overview-card" >
< text class = "overview-label" > 总实际重量 < / text >
< text class = "overview-value" > { { formatNum ( reportSummary . totalActualWeight ) } } < / text >
< text class = "overview-unit" > t < / text >
< / view >
< view class = "overview-card" >
< text class = "overview-label" > 成材率 < / text >
< text class = "overview-value" >
{ { reportSummary . yieldRate != null ? ( reportSummary . yieldRate * 100 ) . toFixed ( 1 ) : '—' } }
< / text >
< text class = "overview-unit" > % < / text >
< / view >
< / view >
< view class = "chart-box" v-else >
< text class = "placeholder-text" > 暂无实绩汇总数据 。 < / text >
< / view >
< / view >
< view class = "section" >
< view class = "section-title" > 生产实绩明细 < / view >
< view class = "stats-table" v-if = "reportDetails && reportDetails.length" >
< view class = "stats-header" >
< text class = "stats-col" > 成品卷号 < / text >
< text class = "stats-col" > 原料卷号 < / text >
< text class = "stats-col" > 班 / 组 < / text >
< text class = "stats-col" > 规格 ( 厚 × 宽 ) < / text >
< text class = "stats-col" > 长度 / 重量 < / text >
< / view >
< view
class = "stats-row"
v-for = "item in reportDetails"
: key = "item.exitMatId + '_' + item.onlineTime"
>
< text class = "stats-col" > { { item . exitMatId } } < / text >
< text class = "stats-col" > { { item . entryMatId } } < / text >
< text class = "stats-col" > { { item . groupNo || '—' } } / { { item . shiftNo || '—' } } < / text >
< text class = "stats-col" >
{ { formatNum ( item . exitThickness ) } } × { { formatNum ( item . exitWidth ) } }
< / text >
< text class = "stats-col" >
{ { formatNum ( item . exitLength ) } } m / { { formatNum ( item . actualWeight ) } } t
< / text >
< / view >
< / view >
< view class = "chart-box" v-else >
< text class = "placeholder-text" > 暂无生产明细记录 。 < / text >
< / view >
< / view >
< / scroll-view >
<!-- 停机统计 / 停机记录 -- >
< scroll-view scroll -y class = "scroll-container" v-if = "currentTab === 3" >
< view class = "section" >
< view class = "section-title" > 停机记录 < / view >
<!-- 月份选择器 -- >
< view class = "stoppage-filter" >
< picker mode = "date" fields = "month" :value = "stoppageMonth" @change ="onStoppageMonthChange" >
< view class = "stoppage-month-btn" >
< text class = "stoppage-month-text" > { { stoppageMonthLabel } } < / text >
< / view >
< / picker >
< / view >
< view class = "stats-table" v-if = "stoppageList && stoppageList.length" >
< view class = "stats-header" >
< text class = "stats-col" > 开始时间 < / text >
< text class = "stats-col" > 结束时间 < / text >
< text class = "stats-col" > 持续时间 < / text >
< text class = "stats-col" > 区域 / 设备 < / text >
< text class = "stats-col" > 原因 < / text >
< / view >
< view
class = "stats-row"
v-for = "item in stoppageList"
:key = "item.stopid"
>
< text class = "stats-col" > { { item . startDate } } < / text >
< text class = "stats-col" > { { item . endDate } } < / text >
< text class = "stats-col" > { { item . duration } } < / text >
< text class = "stats-col" >
{ { item . area || '—' } } / { { item . seton || '—' } }
< / text >
< text class = "stats-col" > { { item . remark || '—' } } < / text >
< / view >
< / view >
< view class = "chart-box" v-else >
< text class = "placeholder-text" > 暂无停机记录 。 < / text >
< / view >
< / view >
< / scroll-view >
< / view >
< / template >
< script >
import { listDeviceEnumAll } from '@/api/pocket/deviceEnum'
import { getDeviceFieldMetaAll } from '@/api/pocket/deviceFieldMeta'
import { listDeviceSnapshotLatest } from '@/api/pocket/deviceSnapshot'
import { getCurrentPlan , getCurrentProcess } from '@/api/business/dashboard'
import { getReportSummary , getReportDetails } from '@/api/business/report'
import { listStoppage } from '@/api/business/stoppage'
import { createMeasureSocket } from '@/utils/socketMeasure'
export default {
@@ -87,11 +255,15 @@ export default {
return {
currentTab : 1 ,
tabData : [
{ text : '实时监控' , value : 1 }
{ text : '实时监控' , value : 1 } ,
{ text : '生产统计' , value : 2 } ,
{ text : '停机统计' , value : 3 }
] ,
isRefreshing : false ,
isConnected : false ,
lastUpdateTime : '—' ,
// 折线图实时刷新开关(避免查看某个点时被新数据刷掉)
isChartPaused : false ,
// socket最新消息
latestMeasure : null ,
@@ -108,6 +280,55 @@ export default {
// 懒加载:已订阅的设备列表
subscribedDeviceCodes : [ ] ,
// 实时数据卡片定义(按酸轧页组织,不按设备分块)
entryMetrics : [
{ field : 'entryCoilId' , label : '入口卷号' , unit : '' } ,
{ field : 'stripLocation' , label : '带钢位置' , unit : 'm' } ,
{ field : 'stripSpeed' , label : '带钢速度' , unit : 'm/min' } ,
{ field : 'tensionPorBr1' , label : '入口张力 POR-BR1' , unit : 'daN' } ,
{ field : 'tensionBr1Br2' , label : '张力 BR1-BR2' , unit : 'daN' } ,
{ field : 'tensionBr2Br3' , label : '张力 BR2-BR3' , unit : 'daN' }
] ,
furnaceMetrics : [
{ field : 'phFurnaceTemperatureActual' , label : 'PH炉温' , unit : '℃' } ,
{ field : 'rtf1FurnaceTemperatureActual' , label : 'RTF1炉温' , unit : '℃' } ,
{ field : 'potTemperature' , label : '锌锅温度' , unit : '℃' } ,
{ field : 'potPower' , label : '锌锅功率' , unit : '' }
] ,
coatMetrics : [
{ field : 'avrCoatingWeightTop' , label : '上层平均涂层重量' , unit : 'g/m²' } ,
{ field : 'avrCoatingWeightBottom' , label : '下层平均涂层重量' , unit : 'g/m²' } ,
{ field : 'airKnifePressure' , label : '气刀压力' , unit : '' } ,
{ field : 'stripSpeedTmExit' , label : 'TM出口速度' , unit : 'm/min' }
] ,
exitMetrics : [
{ field : 'tensionBr8Br9' , label : '张力 BR8-BR9' , unit : 'daN' } ,
{ field : 'tensionBr9Tr' , label : '张力 BR9-TR' , unit : 'daN' } ,
{ field : 'speedExitSection' , label : '出口速度' , unit : 'm/min' } ,
{ field : 'coilLength' , label : '钢卷长度' , unit : 'm' }
] ,
// 前端历史缓存(打开页面即可出趋势)
chartMaxPoints : 60 ,
chartSeries : {
entry : { time : [ ] , stripSpeed : [ ] , tensionPorBr1 : [ ] , tensionBr1Br2 : [ ] , tensionBr2Br3 : [ ] } ,
furnace : { time : [ ] , phFurnaceTemperatureActual : [ ] , rtf1FurnaceTemperatureActual : [ ] , potTemperature : [ ] } ,
coat : { time : [ ] , avrCoatingWeightTop : [ ] , avrCoatingWeightBottom : [ ] , airKnifePressure : [ ] , stripSpeedTmExit : [ ] } ,
exit : { time : [ ] , tensionBr8Br9 : [ ] , tensionBr9Tr : [ ] , speedExitSection : [ ] }
} ,
// 生产统计(实绩)
currentPlan : null ,
currentProcess : null ,
reportSummary : null ,
reportDetails : [ ] ,
reportLoading : false ,
// 停机记录
stoppageList : [ ] ,
stoppageLoading : false ,
stoppageMonth : '' , // yyyy-MM
lineChartOpts : {
color : [ '#0066cc' , '#409eff' , '#66b1ff' , '#a0cfff' , '#d9ecff' , '#ecf5ff' ] ,
padding : [ 15 , 15 , 0 , 15 ] ,
@@ -129,32 +350,71 @@ export default {
socketClient : null ,
fieldTrendSocket : null ,
fieldTrendSubscribed : false ,
subscribedDeviceCodes : [ ]
fieldTrendSubscribed : false
}
} ,
mounted ( ) {
this . initSocket ( )
Promise . all ( [ this . loadDeviceDefs ( ) , this . loadFieldMeta ( ) ] ) . then ( ( ) => {
// 页面初始化完成后,默认订阅首屏设备(懒加载)
this . $nextTick ( ( ) => {
this . updateVisibleDeviceSubscriptions ( )
} )
} )
Promise . all ( [ this . loadDeviceDefs ( ) , this . loadFieldMeta ( ) , this . loadHistoryForCharts ( ) ]) . then ( ( ) => { } )
this . updateLastTime ( )
// 周期性从后端快照刷新折线图数据(降低刷新频率,避免频繁打断用户查看)
// 这里用 60 秒刷一次,后端快照是每 5 分钟一条,折线图变化会比较平滑
this . _historyTimer = setInterval ( ( ) => {
if ( ! this . isChartPaused ) {
this . loadHistoryForCharts ( )
}
} , 60 * 1000 )
} ,
computed : {
stoppageMonthLabel ( ) {
if ( ! this . stoppageMonth ) return '选择月份'
const parts = ( this . stoppageMonth || '' ) . split ( '-' )
if ( parts . length < 2 ) return this . stoppageMonth
const [ y , m ] = parts
return ` ${ y } 年 ${ m } 月 `
}
} ,
watch : {
currentTab ( newVal ) {
if ( newVal === 2 ) {
this . loadReportData ( )
} else if ( newVal === 3 ) {
// 默认当前月
if ( ! this . stoppageMonth ) {
const now = new Date ( )
const y = now . getFullYear ( )
const m = String ( now . getMonth ( ) + 1 ) . padStart ( 2 , '0' )
this . stoppageMonth = ` ${ y } - ${ m } `
}
this . loadStoppageData ( )
}
}
} ,
beforeDestroy ( ) {
if ( this . socketClient ) this . socketClient . close ( )
if ( this . fieldTrendSocket ) this . fieldTrendSocket . close ( )
if ( this . _historyTimer ) {
clearInterval ( this . _historyTimer )
this . _historyTimer = null
}
} ,
methods : {
async loadDeviceDefs ( ) {
const res = await listDeviceEnumAll ( )
// 打印响应,排查是否拿到 data 数组(避免页面空白)
try {
console . log ( res )
} catch ( e ) {
// ignore
}
this . deviceDefs = ( res && res . data ) || [ ]
@@ -162,9 +422,82 @@ export default {
async loadFieldMeta ( ) {
const res = await getDeviceFieldMetaAll ( )
// 打印响应,排查字段元数据是否加载成功
try {
const keys = res && res . data && typeof res . data === 'object' ? Object . keys ( res . data ) . length : - 1
console . log ( '[resp]/api/deviceFieldMeta/all:' , {
code : res && res . code ,
msg : res && res . msg ,
dataType : res && res . data ? Object . prototype . toString . call ( res . data ) : 'null' ,
keyCount : keys
} )
} catch ( e ) {
// ignore
}
this . fieldMeta = ( res && res . data ) || { }
} ,
// 加载历史快照,填充折线图的“历史部分”
async loadHistoryForCharts ( ) {
try {
// 使用设备快照作为折线图唯一数据源,避免每条 socket 消息都触发重绘
// 每次加载前先清空本地缓存,防止重复累加
this . chartSeries = {
entry : { time : [ ] , stripSpeed : [ ] , tensionPorBr1 : [ ] , tensionBr1Br2 : [ ] , tensionBr2Br3 : [ ] } ,
furnace : { time : [ ] , phFurnaceTemperatureActual : [ ] , rtf1FurnaceTemperatureActual : [ ] , potTemperature : [ ] } ,
coat : { time : [ ] , avrCoatingWeightTop : [ ] , avrCoatingWeightBottom : [ ] , airKnifePressure : [ ] , stripSpeedTmExit : [ ] } ,
exit : { time : [ ] , tensionBr8Br9 : [ ] , tensionBr9Tr : [ ] , speedExitSection : [ ] }
}
const limit = this . chartMaxPoints
const tasks = [ ]
const deviceMap = {
entry : { deviceCode : 'POR1' , fields : [ 'stripSpeed' , 'tensionPorBr1' , 'tensionBr1Br2' , 'tensionBr2Br3' ] } ,
furnace : {
deviceCode : 'FUR2' ,
fields : [ 'phFurnaceTemperatureActual' , 'rtf1FurnaceTemperatureActual' , 'potTemperature' ]
} ,
coat : {
deviceCode : 'COAT' ,
fields : [ 'avrCoatingWeightTop' , 'avrCoatingWeightBottom' , 'airKnifePressure' , 'stripSpeedTmExit' ]
} ,
exit : { deviceCode : 'TR' , fields : [ 'tensionBr8Br9' , 'tensionBr9Tr' , 'speedExitSection' ] }
}
Object . keys ( deviceMap ) . forEach ( ( group ) => {
const cfg = deviceMap [ group ]
tasks . push (
listDeviceSnapshotLatest ( { limit , deviceCode : cfg . deviceCode } )
. then ( ( res ) => {
const rows = ( res && res . data ) || [ ]
// 按时间升序
const list = rows . slice ( ) . reverse ( )
list . forEach ( ( row ) => {
const t = ( row . createTime || '' ) . slice ( 11 , 19 ) // HH:mm:ss
let snap = { }
try {
snap = row . snapshotData ? JSON . parse ( row . snapshotData ) : { }
} catch ( e ) {
snap = { }
}
const values = { }
cfg . fields . forEach ( ( f ) => {
values [ f ] = snap [ f ]
} )
this . pushSeries ( group , t , values )
} )
} )
. catch ( ( ) => { } )
)
} )
await Promise . all ( tasks )
} catch ( e ) {
// ignore
}
} ,
initSocket ( ) {
// 实时测量数据 WebSocket
this . socketClient = createMeasureSocket ( {
@@ -181,10 +514,24 @@ export default {
} ,
onMessage : ( data ) => {
try {
this . latestMeasure = JSON . parse ( data )
// 兼容字符串 / 已解析对象两种情况,并打日志方便排查
let payload = null
if ( typeof data === 'string' ) {
payload = JSON . parse ( data )
} else if ( data && typeof data === 'object' ) {
// uni 有时会包一层 { data: 'xxx' }
if ( typeof data . data === 'string' ) {
payload = JSON . parse ( data . data )
} else {
payload = data
}
}
if ( ! payload ) return
this . latestMeasure = payload
// 实时卡片仍然使用最新测量值,折线图改为走后端快照,避免高频刷新
this . updateLastTime ( )
} catch ( e ) {
// ignore
console . error ( '解析 track_measure 数据失败:' , e , data )
}
}
} )
@@ -225,6 +572,76 @@ export default {
this . fieldTrendSocket . connect ( )
} ,
// 加载生产统计 / 实绩数据
async loadReportData ( ) {
if ( this . reportLoading ) return
this . reportLoading = true
try {
// 当前计划 & 当前工艺
try {
const planRes = await getCurrentPlan ( )
this . currentPlan = ( planRes && planRes . data ) || null
} catch ( e ) {
this . currentPlan = null
}
try {
const procRes = await getCurrentProcess ( )
this . currentProcess = ( procRes && procRes . data ) || null
} catch ( e ) {
this . currentProcess = null
}
// 实绩汇总 & 明细:不传筛选条件,后端按默认时间范围处理
try {
const sumRes = await getReportSummary ( { } )
this . reportSummary = sumRes || null
} catch ( e ) {
this . reportSummary = null
}
try {
const detRes = await getReportDetails ( { } )
this . reportDetails = ( detRes && Array . isArray ( detRes ) ? detRes : [ ] ) || [ ]
} catch ( e ) {
this . reportDetails = [ ]
}
} finally {
this . reportLoading = false
}
} ,
// 加载停机记录
async loadStoppageData ( ) {
if ( this . stoppageLoading ) return
this . stoppageLoading = true
try {
// 计算当月起止日期( yyyy-MM-dd)
const month = this . stoppageMonth
let startDate = ''
let endDate = ''
if ( month ) {
const [ y , m ] = month . split ( '-' ) . map ( ( s ) => Number ( s ) )
const first = new Date ( y , m - 1 , 1 )
const last = new Date ( y , m , 0 )
const pad = ( n ) => String ( n ) . padStart ( 2 , '0' )
startDate = ` ${ first . getFullYear ( ) } - ${ pad ( first . getMonth ( ) + 1 ) } - ${ pad ( first . getDate ( ) ) } `
endDate = ` ${ last . getFullYear ( ) } - ${ pad ( last . getMonth ( ) + 1 ) } - ${ pad ( last . getDate ( ) ) } `
}
const res = await listStoppage ( { startDate , endDate } )
this . stoppageList = ( res && res . data ) || [ ]
} catch ( e ) {
this . stoppageList = [ ]
} finally {
this . stoppageLoading = false
}
} ,
onStoppageMonthChange ( e ) {
// e.detail.value 为 yyyy-MM-dd, 这里截取前 7 位当作月份
const v = e . detail && e . detail . value
this . stoppageMonth = v ? v . slice ( 0 , 7 ) : ''
this . loadStoppageData ( )
} ,
// 滚动事件:节流触发订阅更新
onScroll ( ) {
if ( this . _scrollTimer ) return
@@ -311,34 +728,116 @@ export default {
}
} ,
// 根据 sourceType + 字段名取实时值(不按设备分块)
getRealtimeValueBySource ( sourceType , field ) {
const m = this . latestMeasure || { }
let msg = { }
switch ( sourceType ) {
case 'ENTRY' :
msg = m . appMeasureEntryMessage || { }
break
case 'FURNACE' :
msg = m . appMeasureFurnaceMessage || { }
break
case 'COAT' :
msg = m . appMeasureCoatMessage || { }
break
case 'EXIT' :
msg = m . appMeasureExitMessage || { }
break
default :
msg = { }
}
const v = msg ? msg [ field ] : null
if ( typeof v === 'string' ) return v || '—'
return this . formatValue ( v )
} ,
// 追加一帧到前端历史缓存(用于统计折线图)
appendChartPoint ( payload ) {
const now = new Date ( )
const hh = String ( now . getHours ( ) ) . padStart ( 2 , '0' )
const mm = String ( now . getMinutes ( ) ) . padStart ( 2 , '0' )
const ss = String ( now . getSeconds ( ) ) . padStart ( 2 , '0' )
const t = ` ${ hh } : ${ mm } : ${ ss } `
const entry = payload . appMeasureEntryMessage || { }
const furnace = payload . appMeasureFurnaceMessage || { }
const coat = payload . appMeasureCoatMessage || { }
const exit = payload . appMeasureExitMessage || { }
this . pushSeries ( 'entry' , t , {
stripSpeed : entry . stripSpeed ,
tensionPorBr1 : entry . tensionPorBr1 ,
tensionBr1Br2 : entry . tensionBr1Br2 ,
tensionBr2Br3 : entry . tensionBr2Br3
} )
this . pushSeries ( 'furnace' , t , {
phFurnaceTemperatureActual : furnace . phFurnaceTemperatureActual ,
rtf1FurnaceTemperatureActual : furnace . rtf1FurnaceTemperatureActual ,
potTemperature : furnace . potTemperature
} )
this . pushSeries ( 'coat' , t , {
avrCoatingWeightTop : coat . avrCoatingWeightTop ,
avrCoatingWeightBottom : coat . avrCoatingWeightBottom ,
airKnifePressure : coat . airKnifePressure ,
stripSpeedTmExit : coat . stripSpeedTmExit
} )
this . pushSeries ( 'exit' , t , {
tensionBr8Br9 : exit . tensionBr8Br9 ,
tensionBr9Tr : exit . tensionBr9Tr ,
speedExitSection : exit . speedExitSection
} )
} ,
pushSeries ( group , time , values ) {
const g = this . chartSeries [ group ]
if ( ! g ) return
g . time . push ( time )
Object . keys ( values ) . forEach ( ( k ) => {
if ( ! g [ k ] ) return
const raw = values [ k ]
const num = raw === null || raw === undefined || raw === '' ? null : Number ( raw )
g [ k ] . push ( Number . isFinite ( num ) ? num : null )
} )
if ( g . time . length > this . chartMaxPoints ) {
g . time . shift ( )
Object . keys ( values ) . forEach ( ( k ) => {
if ( g [ k ] && g [ k ] . length > this . chartMaxPoints ) g [ k ] . shift ( )
} )
}
} ,
// 生成多序列折线图数据
toGroupLineChart ( group ) {
const g = this . chartSeries [ group ]
if ( ! g ) return { categories : [ ] , series : [ ] }
const seriesKeys = Object . keys ( g ) . filter ( ( k ) => k !== 'time' )
return {
categories : g . time || [ ] ,
series : seriesKeys . map ( ( k ) => ( {
name : this . getFieldLabel ( k ) ,
data : g [ k ] || [ ]
} ) )
}
} ,
// 切换折线图实时刷新状态
toggleChartPause ( ) {
this . isChartPaused = ! this . isChartPaused
} ,
formatNum ( v ) {
if ( v === null || v === undefined || Number . isNaN ( Number ( v ) ) ) return '—'
return Number ( v ) . toFixed ( 2 )
} ,
// 根据 DeviceEnum.sourceType 决定取哪个 message
pickSourceMsg ( dev ) {
const m = this . latestMeasure || { }
if ( ! dev || ! dev . sourceType ) return { }
switch ( dev . sourceType ) {
case 'ENTRY' :
return m . appMeasureEntryMessage || { }
case 'FURNACE' :
return m . appMeasureFurnaceMessage || { }
case 'COAT' :
return m . appMeasureCoatMessage || { }
case 'EXIT' :
return m . appMeasureExitMessage || { }
default :
return { }
}
} ,
// 不再按设备分块展示: pickSourceMsg/getRealtimeFieldValue 已弃用
getRealtimeFieldValue ( dev , field ) {
const msg = this . pickSourceMsg ( dev )
const v = msg ? msg [ field ] : null
return this . formatValue ( v )
} ,
// 快捷导航(已移除)
@@ -457,6 +956,86 @@ export default {
}
}
/* 快速导航菜单(固定在左下角,模仿 acidity.vue) */
. nav - menu - fixed {
position : fixed ;
left : 32 rpx ;
bottom : 120 rpx ;
z - index : 998 ;
display : flex ;
flex - direction : column ;
align - items : flex - start ;
gap : 12 rpx ;
}
. nav - toggle {
width : 96 rpx ;
height : 96 rpx ;
background : # 409 eff ;
border - radius : 50 % ;
display : flex ;
align - items : center ;
justify - content : center ;
box - shadow : 0 8 rpx 20 rpx rgba ( 64 , 158 , 255 , 0.4 ) ;
transition : all 0.3 s ease ;
& : active {
opacity : 0.8 ;
transform : scale ( 0.95 ) ;
}
}
. nav - toggle - icon {
font - size : 48 rpx ;
color : # fff ;
display : block ;
line - height : 1 ;
}
. nav - items {
display : flex ;
flex - direction : column ;
gap : 8 rpx ;
animation : slideUp 0.3 s ease ;
max - height : 60 vh ;
overflow : auto ;
}
@ keyframes slideUp {
from {
opacity : 0 ;
transform : translateY ( 20 rpx ) ;
}
to {
opacity : 1 ;
transform : translateY ( 0 ) ;
}
}
. nav - item {
background : # fff ;
border : 2 rpx solid # 409 eff ;
border - radius : 8 rpx ;
padding : 16 rpx 24 rpx ;
display : flex ;
align - items : center ;
justify - content : center ;
box - shadow : 0 4 rpx 12 rpx rgba ( 64 , 158 , 255 , 0.2 ) ;
transition : all 0.2 s ease ;
& : active {
background : # f0f9ff ;
transform : scale ( 0.95 ) ;
}
}
. nav - label {
font - size : 26 rpx ;
color : # 409 eff ;
font - weight : 500 ;
white - space : nowrap ;
}
@ keyframes rotate {
from {
transform : rotate ( 0 deg ) ;
@@ -471,6 +1050,19 @@ export default {
padding : 24 rpx ;
}
. placeholder - box {
background : # fff ;
border : 1 rpx dashed # dcdfe6 ;
border - radius : 12 rpx ;
padding : 28 rpx 20 rpx ;
margin - bottom : 18 rpx ;
}
. placeholder - text {
font - size : 24 rpx ;
color : # 909399 ;
}
. status - bar {
display : flex ;
align - items : center ;
@@ -696,27 +1288,25 @@ export default {
}
. overview - card {
background : linear - gradient ( 135 deg , # 667 eea 0 % , # 764 ba2 100 % ) ;
background : # fff ;
border - radius : 12 rpx ;
padding : 32 rpx 24 rpx ;
text - align : center ;
box - shadow : 0 4 rpx 12 rpx rgba ( 102 , 126 , 234 , 0.3 ) ;
border : 1 rpx solid # e4e7ed ;
}
. overview - card : nth - child ( 2 ) {
background : linear - gradient ( 135 deg , # f093fb 0 % , # f5576c 100 % ) ;
box - shadow : 0 4 rpx 12 rpx rgba ( 245 , 87 , 108 , 0.3 ) ;
background : # fff ;
}
. overview - card : nth - child ( 3 ) {
background : linear - gradient ( 135 deg , # 4 facfe 0 % , # 00 f2fe 100 % ) ;
box - shadow : 0 4 rpx 12 rpx rgba ( 79 , 172 , 254 , 0.3 ) ;
background : # fff ;
}
. overview - label {
display : block ;
font - size : 24 rpx ;
color : rgba ( 255 , 255 , 255 , 0.9 ) ;
color : # 909399 ;
margin - bottom : 16 rpx ;
}
@@ -724,17 +1314,35 @@ export default {
display : block ;
font - size : 48 rpx ;
font - weight : 700 ;
color : # fff ;
color : # 303133 ;
line - height : 1.2 ;
}
. overview - unit {
display : block ;
font - size : 22 rpx ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
color : # 909399 ;
margin - top : 8 rpx ;
}
. stoppage - filter {
margin : 0 0 20 rpx 0 ;
display : flex ;
justify - content : flex - end ;
}
. stoppage - month - btn {
padding : 12 rpx 24 rpx ;
border - radius : 999 rpx ;
border : 1 rpx solid # dcdfe6 ;
background : # fff ;
}
. stoppage - month - text {
font - size : 24 rpx ;
color : # 606266 ;
}
. status - distribution {
display : grid ;
grid - template - columns : repeat ( 2 , 1 fr ) ;