2025-10-27 13:21:43 +08:00
|
|
|
|
<template>
|
2025-10-31 19:10:08 +08:00
|
|
|
|
<view class="page-container">
|
2025-10-29 15:38:20 +08:00
|
|
|
|
<!-- 简洁标签栏 -->
|
|
|
|
|
|
<view class="tab-container">
|
2026-01-05 14:29:33 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-for="item in tabData"
|
2025-10-29 15:38:20 +08:00
|
|
|
|
:key="item.value"
|
|
|
|
|
|
@click="currentTab = item.value"
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ 'tab-active': currentTab === item.value }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text class="tab-label">{{ item.text }}</text>
|
|
|
|
|
|
<view class="tab-indicator" v-if="currentTab === item.value"></view>
|
|
|
|
|
|
</view>
|
2025-10-27 13:21:43 +08:00
|
|
|
|
</view>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
<!-- 刷新按钮 -->
|
2026-01-05 14:29:33 +08:00
|
|
|
|
<view class="refresh-btn-fixed" @click="refreshAll">
|
|
|
|
|
|
<text class="refresh-icon" :class="{ rotating: isRefreshing }">⟳</text>
|
|
|
|
|
|
</view>
|
2025-10-27 13:21:43 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<!-- Tab1:实时监控 -->
|
|
|
|
|
|
<scroll-view
|
|
|
|
|
|
scroll-y
|
|
|
|
|
|
class="scroll-container"
|
|
|
|
|
|
v-if="currentTab === 1"
|
|
|
|
|
|
>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
<!-- 顶部状态栏 -->
|
|
|
|
|
|
<view class="status-bar">
|
|
|
|
|
|
<view class="status-item">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<text class="status-label">网络状态</text>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
<text class="status-value" :class="isConnected ? 'status-通畅' : 'status-异常'">{{ isConnected ? '已连接' : '未连接' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="status-divider"></view>
|
|
|
|
|
|
<view class="status-item">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<text class="status-label">更新</text>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
<text class="status-value status-time">{{ lastUpdateTime }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="status-divider"></view>
|
|
|
|
|
|
<view class="status-item">
|
|
|
|
|
|
<text class="status-label">设备数</text>
|
|
|
|
|
|
<text class="status-value">{{ deviceDefs.length }}</text>
|
|
|
|
|
|
</view>
|
2026-01-15 20:18:37 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 带钢位置和速度模块 -->
|
|
|
|
|
|
<view class="section">
|
|
|
|
|
|
<view class="section-title">带钢状态</view>
|
|
|
|
|
|
<view class="strip-status-grid">
|
|
|
|
|
|
<!-- 带钢位置卡片 -->
|
|
|
|
|
|
<view class="strip-status-card">
|
|
|
|
|
|
<text class="strip-status-label">带钢位置</text>
|
|
|
|
|
|
<text class="strip-status-value">{{ getRealtimeValueBySource('ENTRY', 'stripLocation') }}</text>
|
|
|
|
|
|
<text class="strip-status-unit">m</text>
|
|
|
|
|
|
<view class="progress-container">
|
|
|
|
|
|
<view class="progress-bar">
|
|
|
|
|
|
<view class="progress-fill" :style="{ width: stripLocationProgress + '%' }"></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="progress-text">{{ stripLocationProgress.toFixed(1) }}%</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- 带钢速度卡片 -->
|
|
|
|
|
|
<view class="strip-status-card">
|
|
|
|
|
|
<text class="strip-status-label">带钢速度</text>
|
|
|
|
|
|
<text class="strip-status-value">{{ getRealtimeValueBySource('ENTRY', 'stripSpeed') }}</text>
|
|
|
|
|
|
<text class="strip-status-unit">m/min</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- 带钢速度折线图 -->
|
|
|
|
|
|
<view class="chart-box">
|
|
|
|
|
|
<qiun-data-charts type="line" :chartData="stripSpeedChartData" :opts="stripSpeedChartOpts" />
|
2026-01-15 17:37:57 +08:00
|
|
|
|
</view>
|
2025-10-27 13:21:43 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<!-- 实时数据卡片 + 统计图(按酸轧页风格,不按设备分块) -->
|
|
|
|
|
|
<view class="section">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="section-title">入口段</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<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>
|
2025-10-27 13:21:43 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view class="section">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="section-title">退火炉段</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view class="metrics-grid-3">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="metric-box" v-for="it in furnaceMetrics" :key="'furnace_' + it.actualField">
|
|
|
|
|
|
<text class="metric-name">{{ it.label || getFieldLabel(it.actualField) }}</text>
|
|
|
|
|
|
<text class="metric-value">{{ getRealtimeValueBySource('FURNACE', it.actualField) }}</text>
|
|
|
|
|
|
<text class="metric-unit">{{ it.unit || getFieldUnit(it.actualField) }}</text>
|
|
|
|
|
|
<text class="metric-set-value" v-if="it.setField">
|
|
|
|
|
|
设定: {{ getRealtimeValueBySource('FURNACE', it.setField) }}
|
|
|
|
|
|
</text>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-10-27 13:21:43 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view class="section">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="section-title">后处理/涂层段</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<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>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view class="section">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="section-title">出口段</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<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>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<!-- 生产统计 / 实绩 -->
|
|
|
|
|
|
<scroll-view scroll-y class="scroll-container" v-if="currentTab === 2">
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<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>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
</view>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view class="section">
|
|
|
|
|
|
<view class="section-title">生产实绩明细</view>
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="detail-cards" v-if="reportDetails && reportDetails.length">
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view
|
2026-01-15 20:18:37 +08:00
|
|
|
|
class="detail-card"
|
|
|
|
|
|
v-for="(item, index) in reportDetails"
|
|
|
|
|
|
:key="(item.exitMatId || '') + '_' + (item.onlineTime || index) + '_' + index"
|
2026-01-15 17:37:57 +08:00
|
|
|
|
>
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="detail-card-header">
|
|
|
|
|
|
<text class="detail-card-title">成品卷号:{{ item.exitMatId }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="detail-card-body">
|
|
|
|
|
|
<view class="detail-row">
|
|
|
|
|
|
<text class="detail-label">原料卷号</text>
|
|
|
|
|
|
<text class="detail-value">{{ item.entryMatId || '—' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="detail-row">
|
|
|
|
|
|
<text class="detail-label">班/组</text>
|
|
|
|
|
|
<text class="detail-value">{{ item.groupNo || '—' }} / {{ item.shiftNo || '—' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="detail-row">
|
|
|
|
|
|
<text class="detail-label">规格</text>
|
|
|
|
|
|
<text class="detail-value">
|
|
|
|
|
|
{{ formatNum(item.exitThickness) }} × {{ formatNum(item.exitWidth) }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="detail-row">
|
|
|
|
|
|
<text class="detail-label">长度</text>
|
|
|
|
|
|
<text class="detail-value">{{ formatNum(item.exitLength) }} m</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="detail-row">
|
|
|
|
|
|
<text class="detail-label">重量</text>
|
|
|
|
|
|
<text class="detail-value">{{ formatNum(item.actualWeight) }} t</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="chart-box" v-else>
|
|
|
|
|
|
<text class="placeholder-text">暂无生产明细记录。</text>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-10-27 13:21:43 +08:00
|
|
|
|
</scroll-view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 停机统计 / 停机记录 -->
|
|
|
|
|
|
<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>
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="stoppage-cards" v-if="stoppageList && stoppageList.length">
|
2026-01-15 17:37:57 +08:00
|
|
|
|
<view
|
2026-01-15 20:18:37 +08:00
|
|
|
|
class="stoppage-card"
|
|
|
|
|
|
v-for="(item, index) in stoppageList"
|
|
|
|
|
|
:key="item.stopid || index"
|
2026-01-15 17:37:57 +08:00
|
|
|
|
>
|
2026-01-15 20:18:37 +08:00
|
|
|
|
<view class="stoppage-card-header">
|
|
|
|
|
|
<text class="stoppage-card-title">停机记录 #{{ index + 1 }}</text>
|
|
|
|
|
|
<text class="stoppage-card-duration">{{ formatDuration(item.duration) }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stoppage-card-body">
|
|
|
|
|
|
<view class="stoppage-row">
|
|
|
|
|
|
<text class="stoppage-label">开始时间</text>
|
|
|
|
|
|
<text class="stoppage-value">{{ item.startDate || '—' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stoppage-row">
|
|
|
|
|
|
<text class="stoppage-label">结束时间</text>
|
|
|
|
|
|
<text class="stoppage-value">{{ item.endDate || '—' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stoppage-row">
|
|
|
|
|
|
<text class="stoppage-label">区域</text>
|
|
|
|
|
|
<text class="stoppage-value">{{ item.area || '—' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stoppage-row">
|
|
|
|
|
|
<text class="stoppage-label">设备</text>
|
|
|
|
|
|
<text class="stoppage-value">{{ item.seton || '—' }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stoppage-row" v-if="item.remark">
|
|
|
|
|
|
<text class="stoppage-label">原因</text>
|
|
|
|
|
|
<text class="stoppage-value stoppage-remark">{{ item.remark }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-01-15 17:37:57 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="chart-box" v-else>
|
|
|
|
|
|
<text class="placeholder-text">暂无停机记录。</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
2025-10-27 13:21:43 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-01-05 14:29:33 +08:00
|
|
|
|
import { listDeviceEnumAll } from '@/api/pocket/deviceEnum'
|
|
|
|
|
|
import { getDeviceFieldMetaAll } from '@/api/pocket/deviceFieldMeta'
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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'
|
2026-01-05 14:29:33 +08:00
|
|
|
|
import { createMeasureSocket } from '@/utils/socketMeasure'
|
2025-10-31 14:50:19 +08:00
|
|
|
|
|
2025-10-27 13:21:43 +08:00
|
|
|
|
export default {
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2025-10-31 19:10:08 +08:00
|
|
|
|
currentTab: 1,
|
|
|
|
|
|
tabData: [
|
2026-01-15 17:37:57 +08:00
|
|
|
|
{ text: '实时监控', value: 1 },
|
|
|
|
|
|
{ text: '生产统计', value: 2 },
|
|
|
|
|
|
{ text: '停机统计', value: 3 }
|
2025-10-27 13:21:43 +08:00
|
|
|
|
],
|
2025-10-31 19:10:08 +08:00
|
|
|
|
isRefreshing: false,
|
2026-01-05 14:29:33 +08:00
|
|
|
|
isConnected: false,
|
|
|
|
|
|
lastUpdateTime: '—',
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 记录起始时间,用于计算运行时长
|
|
|
|
|
|
startTime: null,
|
|
|
|
|
|
// socket数据是否已补充到折线图(仅补充一次)
|
|
|
|
|
|
socketDataAppended: false,
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
// socket最新消息
|
|
|
|
|
|
latestMeasure: null,
|
|
|
|
|
|
|
|
|
|
|
|
// 设备定义(后端 DeviceEnum 全量)
|
|
|
|
|
|
deviceDefs: [],
|
|
|
|
|
|
|
|
|
|
|
|
// 字段元数据:fieldName -> {label, unit, description}
|
|
|
|
|
|
fieldMeta: {},
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
// 实时数据卡片定义(按酸轧页组织,不按设备分块)
|
|
|
|
|
|
entryMetrics: [
|
|
|
|
|
|
{ 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: [
|
2026-01-15 20:18:37 +08:00
|
|
|
|
{ actualField: 'jcf1FurnaceTemperatureActual', setField: 'jcf1FurnaceTemperatureSet', label: 'JCF1炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'jcf2FurnaceTemperatureActual', setField: 'jcf2FurnaceTemperatureSet', label: 'JCF2炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'lbzFurnaceTemperatureActual', setField: 'lbzFurnaceTemperatureSet', label: 'LBZ炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'lthFurnaceTemperatureActual', setField: 'lthFurnaceTemperatureSet', label: 'LTH炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'nof1FurnaceTemperatureActual', setField: 'nof1FurnaceTemperatureSet', label: 'NOF1炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'nof2FurnaceTemperatureActual', setField: 'nof2FurnaceTemperatureSet', label: 'NOF2炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'nof3FurnaceTemperatureActual', setField: 'nof3FurnaceTemperatureSet', label: 'NOF3炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'nof4FurnaceTemperatureActual', setField: 'nof4FurnaceTemperatureSet', label: 'NOF4炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'nof5FurnaceTemperatureActual', setField: 'nof5FurnaceTemperatureSet', label: 'NOF5炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'phFurnaceTemperatureActual', setField: null, label: 'PH炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'rtf1FurnaceTemperatureActual', setField: 'rtf1FurnaceTemperatureSet', label: 'RTF1炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'rtf2FurnaceTemperatureActual', setField: 'rtf2FurnaceTemperatureSet', label: 'RTF2炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'sfFurnaceTemperatureActual', setField: 'sfFurnaceTemperatureSet', label: 'SF炉温', unit: '℃' },
|
|
|
|
|
|
{ actualField: 'tdsFurnaceTemperatureActual', setField: 'tdsFurnaceTemperatureSet', label: 'TDS炉温', unit: '℃' },
|
2026-01-15 17:37:57 +08:00
|
|
|
|
],
|
|
|
|
|
|
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: [
|
2026-01-15 20:18:37 +08:00
|
|
|
|
{ field: 'alkaliConcentration', label: '碱液浓度', unit: '' },
|
|
|
|
|
|
{ field: 'alkaliTemperature', label: '碱液温度', unit: '℃' },
|
|
|
|
|
|
{ field: 'celCapacity', label: '电池容量', unit: '' },
|
|
|
|
|
|
{ field: 'celLength', label: '电池长度', unit: 'm' },
|
|
|
|
|
|
{ field: 'cleaningCurrent', label: '清洗电流', unit: 'A' },
|
|
|
|
|
|
{ field: 'cleaningVoltage', label: '清洗电压', unit: 'V' },
|
|
|
|
|
|
{ field: 'dryingTemperature', label: '干燥温度', unit: '℃' },
|
|
|
|
|
|
{ field: 'entryCoilId', label: '入口卷号', unit: '' },
|
|
|
|
|
|
{ field: 'hotAirFlow', label: '热风流量', unit: '' },
|
|
|
|
|
|
{ field: 'hotAirPressure', label: '热风压力', unit: '' },
|
|
|
|
|
|
{ field: 'payOffReelNumber', label: '放卷卷号', unit: '' },
|
|
|
|
|
|
{ field: 'rinseConductivity', label: '冲洗电导率', unit: '' },
|
|
|
|
|
|
{ field: 'rinseTemperature', label: '冲洗温度', unit: '℃' },
|
|
|
|
|
|
{ field: 'stripLocation', label: '带钢位置', unit: 'm' },
|
|
|
|
|
|
{ field: 'stripSpeed', label: '带钢速度', unit: 'm/min' },
|
|
|
|
|
|
{ field: 'tensionBr1Br2', label: '张力 BR1-BR2', unit: 'daN' },
|
|
|
|
|
|
{ field: 'tensionBr2Br3', label: '张力 BR2-BR3', unit: 'daN' },
|
|
|
|
|
|
{ field: 'tensionPorBr1', label: '入口张力 POR-BR1', unit: 'daN' },
|
2026-01-15 17:37:57 +08:00
|
|
|
|
{ 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,
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 带钢速度折线图最大点数(单独配置)
|
|
|
|
|
|
stripSpeedMaxPoints: 30,
|
2026-01-15 17:37:57 +08:00
|
|
|
|
chartSeries: {
|
|
|
|
|
|
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
2026-01-15 20:18:37 +08:00
|
|
|
|
furnace: {
|
|
|
|
|
|
time: [],
|
|
|
|
|
|
jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [],
|
|
|
|
|
|
lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [],
|
|
|
|
|
|
nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [],
|
|
|
|
|
|
nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [],
|
|
|
|
|
|
nof5FurnaceTemperatureActual: [],
|
|
|
|
|
|
phFurnaceTemperatureActual: [],
|
|
|
|
|
|
rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [],
|
|
|
|
|
|
sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [],
|
|
|
|
|
|
potTemperature: []
|
|
|
|
|
|
},
|
2026-01-15 17:37:57 +08:00
|
|
|
|
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
2026-01-15 20:18:37 +08:00
|
|
|
|
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] },
|
|
|
|
|
|
// 带钢速度实时折线图(持续刷新)
|
|
|
|
|
|
stripSpeed: { time: [], stripSpeed: [] }
|
2026-01-15 17:37:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 生产统计(实绩)
|
|
|
|
|
|
currentPlan: null,
|
|
|
|
|
|
currentProcess: null,
|
|
|
|
|
|
reportSummary: null,
|
|
|
|
|
|
reportDetails: [],
|
|
|
|
|
|
reportLoading: false,
|
|
|
|
|
|
|
|
|
|
|
|
// 停机记录
|
|
|
|
|
|
stoppageList: [],
|
|
|
|
|
|
stoppageLoading: false,
|
|
|
|
|
|
stoppageMonth: '', // yyyy-MM
|
|
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 组件销毁标志,用于防止异步操作在组件销毁后更新数据
|
|
|
|
|
|
isDestroyed: false,
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
lineChartOpts: {
|
2026-01-05 14:29:33 +08:00
|
|
|
|
color: ['#0066cc', '#409eff', '#66b1ff', '#a0cfff', '#d9ecff', '#ecf5ff'],
|
2025-10-31 19:10:08 +08:00
|
|
|
|
padding: [15, 15, 0, 15],
|
|
|
|
|
|
enableScroll: false,
|
2026-01-05 14:29:33 +08:00
|
|
|
|
legend: { show: true, position: 'top', fontSize: 10, lineHeight: 14, itemGap: 6 },
|
|
|
|
|
|
dataLabel: false,
|
|
|
|
|
|
dataPointShape: false,
|
|
|
|
|
|
xAxis: { disableGrid: true, rotateLabel: true, itemCount: 5, labelCount: 5, fontSize: 10 },
|
2025-10-31 19:10:08 +08:00
|
|
|
|
yAxis: {
|
2026-01-05 14:29:33 +08:00
|
|
|
|
gridType: 'dash',
|
2025-10-31 19:10:08 +08:00
|
|
|
|
dashLength: 4,
|
2026-01-05 14:29:33 +08:00
|
|
|
|
gridColor: '#e4e7ed',
|
2025-10-31 19:10:08 +08:00
|
|
|
|
showTitle: true,
|
|
|
|
|
|
fontSize: 10,
|
2026-01-05 14:29:33 +08:00
|
|
|
|
data: [{ title: '数值' }]
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
extra: { line: { type: 'curve', width: 2, activeType: 'hollow' } }
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 带钢速度折线图配置(Y轴固定为100-150,完全禁用动画)
|
|
|
|
|
|
stripSpeedChartOpts: {
|
|
|
|
|
|
color: ['#0066cc'],
|
|
|
|
|
|
padding: [15, 15, 0, 15],
|
|
|
|
|
|
enableScroll: false,
|
|
|
|
|
|
legend: { show: true, position: 'top', fontSize: 10, lineHeight: 14, itemGap: 6 },
|
|
|
|
|
|
dataLabel: false,
|
|
|
|
|
|
dataPointShape: false,
|
|
|
|
|
|
animation: false, // 禁用动画,直接显示到值上
|
|
|
|
|
|
animationDuration: 0, // 动画时长为0,确保无动画效果
|
|
|
|
|
|
xAxis: { disableGrid: true, rotateLabel: true, itemCount: 5, labelCount: 5, fontSize: 10 },
|
|
|
|
|
|
yAxis: {
|
|
|
|
|
|
gridType: 'dash',
|
|
|
|
|
|
dashLength: 4,
|
|
|
|
|
|
gridColor: '#e4e7ed',
|
|
|
|
|
|
showTitle: true,
|
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
|
data: [{
|
|
|
|
|
|
title: '速度(m/min)',
|
|
|
|
|
|
min: 80,
|
|
|
|
|
|
max: 150
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
extra: { line: { type: 'curve', width: 2, activeType: 'hollow' } }
|
|
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
socketClient: null
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-27 13:21:43 +08:00
|
|
|
|
mounted() {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
// 记录起始时间
|
|
|
|
|
|
this.startTime = new Date()
|
2026-01-05 14:29:33 +08:00
|
|
|
|
this.initSocket()
|
|
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
Promise.all([this.loadDeviceDefs(), this.loadFieldMeta(), this.loadHistoryForCharts()]).then(() => {
|
|
|
|
|
|
// Promise完成后检查组件是否已销毁
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
|
// 错误处理中也要检查
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
})
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
this.updateLastTime()
|
2026-01-15 17:37:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
stoppageMonthLabel() {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return '选择月份'
|
2026-01-15 17:37:57 +08:00
|
|
|
|
if (!this.stoppageMonth) return '选择月份'
|
|
|
|
|
|
const parts = (this.stoppageMonth || '').split('-')
|
|
|
|
|
|
if (parts.length < 2) return this.stoppageMonth
|
|
|
|
|
|
const [y, m] = parts
|
|
|
|
|
|
return `${y}年${m}月`
|
2026-01-15 20:18:37 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 带钢位置进度(位置/3000)
|
|
|
|
|
|
stripLocationProgress() {
|
|
|
|
|
|
if (this.isDestroyed) return 0
|
|
|
|
|
|
const m = this.latestMeasure || {}
|
|
|
|
|
|
const entry = m.appMeasureEntryMessage || {}
|
|
|
|
|
|
const location = entry.stripLocation !== null && entry.stripLocation !== undefined
|
|
|
|
|
|
? parseFloat(entry.stripLocation) || 0
|
|
|
|
|
|
: 0
|
|
|
|
|
|
const maxLocation = 3000
|
|
|
|
|
|
const progress = (location / maxLocation) * 100
|
|
|
|
|
|
return Math.min(Math.max(progress, 0), 100)
|
|
|
|
|
|
},
|
|
|
|
|
|
// 带钢速度折线图数据(使用计算属性,确保数据引用稳定)
|
|
|
|
|
|
stripSpeedChartData() {
|
|
|
|
|
|
if (this.isDestroyed) return { categories: [], series: [] }
|
|
|
|
|
|
const g = this.chartSeries.stripSpeed
|
|
|
|
|
|
if (!g) return { categories: [], series: [] }
|
|
|
|
|
|
// 返回新对象,但保持数组引用稳定,避免触发重新初始化
|
|
|
|
|
|
const timeArray = g.time || []
|
|
|
|
|
|
const speedArray = g.stripSpeed || []
|
|
|
|
|
|
return {
|
|
|
|
|
|
categories: timeArray,
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
name: '带钢速度',
|
|
|
|
|
|
data: speedArray
|
|
|
|
|
|
}]
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
watch: {
|
|
|
|
|
|
currentTab(newVal) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
beforeDestroy() {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 先设置销毁标志,防止异步回调访问已销毁的组件
|
|
|
|
|
|
this.isDestroyed = true
|
|
|
|
|
|
|
|
|
|
|
|
// 设置闭包中的销毁标志(必须在关闭socket之前,且要立即执行)
|
|
|
|
|
|
// 这会取消所有待执行的异步更新
|
|
|
|
|
|
if (this._clearSocketFlag) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this._clearSocketFlag()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
// 清空引用,防止再次调用
|
|
|
|
|
|
this._clearSocketFlag = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理定时器
|
|
|
|
|
|
if (this._scrollTimer) {
|
|
|
|
|
|
clearTimeout(this._scrollTimer)
|
|
|
|
|
|
this._scrollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭socket连接并清空回调(必须立即执行)
|
|
|
|
|
|
if (this.socketClient) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先清空回调,防止关闭过程中触发回调
|
|
|
|
|
|
if (this.socketClient.clearCallbacks) {
|
|
|
|
|
|
this.socketClient.clearCallbacks()
|
|
|
|
|
|
}
|
|
|
|
|
|
// 立即关闭socket
|
|
|
|
|
|
this.socketClient.close()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
// 立即清空引用
|
|
|
|
|
|
this.socketClient = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空所有响应式数据引用,防止后续访问
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.latestMeasure = null
|
|
|
|
|
|
this.chartSeries = null
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// ignore
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-27 13:21:43 +08:00
|
|
|
|
methods: {
|
2026-01-05 14:29:33 +08:00
|
|
|
|
async loadDeviceDefs() {
|
|
|
|
|
|
const res = await listDeviceEnumAll()
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-05 14:29:33 +08:00
|
|
|
|
this.deviceDefs = (res && res.data) || []
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
async loadFieldMeta() {
|
|
|
|
|
|
const res = await getDeviceFieldMetaAll()
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-05 14:29:33 +08:00
|
|
|
|
this.fieldMeta = (res && res.data) || {}
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 加载历史快照,填充折线图的"历史部分"
|
2026-01-15 17:37:57 +08:00
|
|
|
|
async loadHistoryForCharts() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用设备快照作为折线图唯一数据源,避免每条 socket 消息都触发重绘
|
|
|
|
|
|
// 每次加载前先清空本地缓存,防止重复累加
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 注意:stripSpeed 数据不清空,保持实时更新连续性
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.chartSeries = {
|
|
|
|
|
|
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
2026-01-15 20:18:37 +08:00
|
|
|
|
furnace: {
|
|
|
|
|
|
time: [],
|
|
|
|
|
|
jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [],
|
|
|
|
|
|
lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [],
|
|
|
|
|
|
nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [],
|
|
|
|
|
|
nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [],
|
|
|
|
|
|
nof5FurnaceTemperatureActual: [],
|
|
|
|
|
|
phFurnaceTemperatureActual: [],
|
|
|
|
|
|
rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [],
|
|
|
|
|
|
sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [],
|
|
|
|
|
|
potTemperature: []
|
|
|
|
|
|
},
|
2026-01-15 17:37:57 +08:00
|
|
|
|
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
2026-01-15 20:18:37 +08:00
|
|
|
|
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] },
|
|
|
|
|
|
// stripSpeed 保持现有数据,不清空,确保实时更新连续性
|
|
|
|
|
|
stripSpeed: this.chartSeries.stripSpeed || { time: [], stripSpeed: [] }
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const limit = this.chartMaxPoints
|
|
|
|
|
|
const tasks = []
|
|
|
|
|
|
|
|
|
|
|
|
const deviceMap = {
|
|
|
|
|
|
entry: { deviceCode: 'POR1', fields: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'] },
|
|
|
|
|
|
furnace: {
|
|
|
|
|
|
deviceCode: 'FUR2',
|
2026-01-15 20:18:37 +08:00
|
|
|
|
fields: [
|
|
|
|
|
|
'jcf1FurnaceTemperatureActual', 'jcf2FurnaceTemperatureActual',
|
|
|
|
|
|
'lbzFurnaceTemperatureActual', 'lthFurnaceTemperatureActual',
|
|
|
|
|
|
'nof1FurnaceTemperatureActual', 'nof2FurnaceTemperatureActual',
|
|
|
|
|
|
'nof3FurnaceTemperatureActual', 'nof4FurnaceTemperatureActual',
|
|
|
|
|
|
'nof5FurnaceTemperatureActual',
|
|
|
|
|
|
'phFurnaceTemperatureActual',
|
|
|
|
|
|
'rtf1FurnaceTemperatureActual', 'rtf2FurnaceTemperatureActual',
|
|
|
|
|
|
'sfFurnaceTemperatureActual', 'tdsFurnaceTemperatureActual',
|
|
|
|
|
|
'potTemperature'
|
|
|
|
|
|
]
|
2026-01-15 17:37:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
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) => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
const rows = (res && res.data) || []
|
|
|
|
|
|
// 按时间升序
|
|
|
|
|
|
const list = rows.slice().reverse()
|
|
|
|
|
|
list.forEach((row) => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
initSocket() {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 使用闭包保存组件引用和销毁标志,避免直接访问 this
|
|
|
|
|
|
const vm = this
|
|
|
|
|
|
let isDestroyed = false
|
|
|
|
|
|
// 保存所有待执行的异步操作ID,用于取消
|
|
|
|
|
|
const pendingUpdates = []
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
// 实时测量数据 WebSocket
|
|
|
|
|
|
this.socketClient = createMeasureSocket({
|
|
|
|
|
|
type: 'track_measure',
|
|
|
|
|
|
onOpen: () => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
vm.isConnected = true
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 组件已销毁,忽略错误
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
onClose: () => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
vm.isConnected = false
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 组件已销毁,忽略错误
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
onError: () => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
vm.isConnected = false
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 组件已销毁,忽略错误
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
onMessage: (data) => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 在函数最开始就检查,避免任何后续操作
|
|
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
// 双重检查:如果组件实例不存在,直接返回
|
|
|
|
|
|
if (!vm) {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 try-catch 包裹整个函数,防止任何错误
|
2026-01-05 14:29:33 +08:00
|
|
|
|
try {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 兼容字符串 / 已解析对象两种情况
|
2026-01-15 17:37:57 +08:00
|
|
|
|
let payload = null
|
|
|
|
|
|
if (typeof data === 'string') {
|
|
|
|
|
|
payload = JSON.parse(data)
|
|
|
|
|
|
} else if (data && typeof data === 'object') {
|
|
|
|
|
|
if (typeof data.data === 'string') {
|
|
|
|
|
|
payload = JSON.parse(data.data)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload = data
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 再次检查,防止在解析过程中组件被销毁
|
|
|
|
|
|
if (!payload || isDestroyed || !vm) return
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 requestAnimationFrame 延迟更新
|
|
|
|
|
|
// 如果组件已销毁,requestAnimationFrame 回调不会执行
|
|
|
|
|
|
const updateFrame = () => {
|
|
|
|
|
|
// 多重检查:确保组件仍然存在
|
|
|
|
|
|
if (isDestroyed || !vm) return
|
|
|
|
|
|
|
|
|
|
|
|
// 检查 Vue 实例状态 - 使用更安全的方式
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 检查组件是否真的还存在且可用
|
|
|
|
|
|
if (!vm || typeof vm !== 'object') {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 尝试访问 Vue 实例的内部属性来验证
|
|
|
|
|
|
if (vm._isDestroyed === true || vm._isBeingDestroyed === true) {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安全地更新数据 - 使用 Object.defineProperty 或直接赋值
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 直接赋值,但确保组件存在
|
|
|
|
|
|
if (vm && !vm._isDestroyed && !vm._isBeingDestroyed) {
|
|
|
|
|
|
vm.latestMeasure = payload
|
|
|
|
|
|
if (!vm.socketDataAppended) {
|
|
|
|
|
|
vm.appendChartPoint(payload)
|
|
|
|
|
|
vm.socketDataAppended = true
|
|
|
|
|
|
}
|
|
|
|
|
|
vm.appendStripSpeedPoint(payload)
|
|
|
|
|
|
vm.updateLastTime()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 如果更新失败,设置销毁标志
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 requestAnimationFrame 或 setTimeout,并保存ID以便取消
|
|
|
|
|
|
let frameId = null
|
|
|
|
|
|
if (typeof requestAnimationFrame !== 'undefined') {
|
|
|
|
|
|
frameId = requestAnimationFrame(updateFrame)
|
|
|
|
|
|
pendingUpdates.push(frameId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
frameId = setTimeout(updateFrame, 0)
|
|
|
|
|
|
pendingUpdates.push(frameId)
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 解析错误或其他错误,静默处理
|
|
|
|
|
|
if (!isDestroyed) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.error('解析 track_measure 数据失败:', e)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 忽略日志错误
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 保存清空标志的函数引用
|
|
|
|
|
|
this._clearSocketFlag = () => {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
// 取消所有待执行的异步更新
|
|
|
|
|
|
pendingUpdates.forEach(id => {
|
2026-01-05 14:29:33 +08:00
|
|
|
|
try {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (typeof cancelAnimationFrame !== 'undefined') {
|
|
|
|
|
|
cancelAnimationFrame(id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
clearTimeout(id)
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// ignore
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
2026-01-15 20:18:37 +08:00
|
|
|
|
})
|
|
|
|
|
|
pendingUpdates.length = 0
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
this.socketClient.connect()
|
2026-01-05 14:29:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
// 加载生产统计 / 实绩数据
|
|
|
|
|
|
async loadReportData() {
|
|
|
|
|
|
if (this.reportLoading) return
|
|
|
|
|
|
this.reportLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 当前计划 & 当前工艺
|
|
|
|
|
|
try {
|
|
|
|
|
|
const planRes = await getCurrentPlan()
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.currentPlan = (planRes && planRes.data) || null
|
|
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.currentPlan = null
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const procRes = await getCurrentProcess()
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.currentProcess = (procRes && procRes.data) || null
|
|
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.currentProcess = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 实绩汇总 & 明细:不传筛选条件,后端按默认时间范围处理
|
2026-01-15 20:18:37 +08:00
|
|
|
|
let summaryData = null
|
|
|
|
|
|
let detailsData = []
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const sumRes = await getReportSummary({})
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
// 处理可能的多种返回结构:直接数据、{data: {...}}、{rows: [...], total: ...}
|
|
|
|
|
|
if (sumRes) {
|
|
|
|
|
|
if (sumRes.data && typeof sumRes.data === 'object' && !Array.isArray(sumRes.data)) {
|
|
|
|
|
|
summaryData = sumRes.data
|
|
|
|
|
|
} else {
|
|
|
|
|
|
summaryData = sumRes
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
console.error('加载实绩汇总失败:', e)
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const detRes = await getReportDetails({})
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
// 处理可能的多种返回结构
|
|
|
|
|
|
if (detRes) {
|
|
|
|
|
|
if (Array.isArray(detRes)) {
|
|
|
|
|
|
detailsData = detRes
|
|
|
|
|
|
} else if (detRes.data && Array.isArray(detRes.data)) {
|
|
|
|
|
|
detailsData = detRes.data
|
|
|
|
|
|
} else if (detRes.rows && Array.isArray(detRes.rows)) {
|
|
|
|
|
|
detailsData = detRes.rows
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
console.error('加载实绩明细失败:', e)
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 统一处理汇总数据:修正coilCount
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
|
|
|
|
|
|
if (summaryData) {
|
|
|
|
|
|
// 如果coilCount不存在或看起来像是页数(通常页数很小),尝试从其他字段获取
|
|
|
|
|
|
if (summaryData.total !== undefined &&
|
|
|
|
|
|
(summaryData.coilCount === undefined ||
|
|
|
|
|
|
(summaryData.coilCount < 10 && summaryData.total > summaryData.coilCount))) {
|
|
|
|
|
|
// 优先使用total字段
|
|
|
|
|
|
summaryData.coilCount = summaryData.total
|
|
|
|
|
|
} else if (detailsData.length > 0 &&
|
|
|
|
|
|
(summaryData.coilCount === undefined || summaryData.coilCount < detailsData.length)) {
|
|
|
|
|
|
// 如果total不存在,使用明细数组长度作为总数
|
|
|
|
|
|
summaryData.coilCount = detailsData.length
|
|
|
|
|
|
}
|
|
|
|
|
|
this.reportSummary = summaryData
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.reportSummary = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.reportDetails = detailsData
|
2026-01-15 17:37:57 +08:00
|
|
|
|
} finally {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (!this.isDestroyed) {
|
|
|
|
|
|
this.reportLoading = false
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 加载停机记录
|
|
|
|
|
|
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 })
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.stoppageList = (res && res.data) || []
|
|
|
|
|
|
} catch (e) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
this.stoppageList = []
|
|
|
|
|
|
} finally {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (!this.isDestroyed) {
|
|
|
|
|
|
this.stoppageLoading = false
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
getFieldLabel(field) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return field || ''
|
2026-01-05 14:29:33 +08:00
|
|
|
|
const meta = this.fieldMeta[field]
|
|
|
|
|
|
return (meta && meta.label) || field
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
getFieldUnit(field) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return ''
|
2026-01-05 14:29:33 +08:00
|
|
|
|
const meta = this.fieldMeta[field]
|
|
|
|
|
|
return (meta && meta.unit) || ''
|
2025-10-31 19:10:08 +08:00
|
|
|
|
},
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
// 根据 sourceType + 字段名取实时值(不按设备分块)
|
|
|
|
|
|
getRealtimeValueBySource(sourceType, field) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return '—'
|
2026-01-05 14:29:33 +08:00
|
|
|
|
const m = this.latestMeasure || {}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
let msg = {}
|
|
|
|
|
|
switch (sourceType) {
|
2026-01-05 14:29:33 +08:00
|
|
|
|
case 'ENTRY':
|
2026-01-15 17:37:57 +08:00
|
|
|
|
msg = m.appMeasureEntryMessage || {}
|
|
|
|
|
|
break
|
2026-01-05 14:29:33 +08:00
|
|
|
|
case 'FURNACE':
|
2026-01-15 17:37:57 +08:00
|
|
|
|
msg = m.appMeasureFurnaceMessage || {}
|
|
|
|
|
|
break
|
2026-01-05 14:29:33 +08:00
|
|
|
|
case 'COAT':
|
2026-01-15 17:37:57 +08:00
|
|
|
|
msg = m.appMeasureCoatMessage || {}
|
|
|
|
|
|
break
|
2026-01-05 14:29:33 +08:00
|
|
|
|
case 'EXIT':
|
2026-01-15 17:37:57 +08:00
|
|
|
|
msg = m.appMeasureExitMessage || {}
|
|
|
|
|
|
break
|
2026-01-05 14:29:33 +08:00
|
|
|
|
default:
|
2026-01-15 17:37:57 +08:00
|
|
|
|
msg = {}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
const v = msg ? msg[field] : null
|
2026-01-15 17:37:57 +08:00
|
|
|
|
if (typeof v === 'string') return v || '—'
|
2026-01-05 14:29:33 +08:00
|
|
|
|
return this.formatValue(v)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
// 追加一帧到前端历史缓存(用于统计折线图)
|
|
|
|
|
|
appendChartPoint(payload) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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, {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
jcf1FurnaceTemperatureActual: furnace.jcf1FurnaceTemperatureActual,
|
|
|
|
|
|
jcf2FurnaceTemperatureActual: furnace.jcf2FurnaceTemperatureActual,
|
|
|
|
|
|
lbzFurnaceTemperatureActual: furnace.lbzFurnaceTemperatureActual,
|
|
|
|
|
|
lthFurnaceTemperatureActual: furnace.lthFurnaceTemperatureActual,
|
|
|
|
|
|
nof1FurnaceTemperatureActual: furnace.nof1FurnaceTemperatureActual,
|
|
|
|
|
|
nof2FurnaceTemperatureActual: furnace.nof2FurnaceTemperatureActual,
|
|
|
|
|
|
nof3FurnaceTemperatureActual: furnace.nof3FurnaceTemperatureActual,
|
|
|
|
|
|
nof4FurnaceTemperatureActual: furnace.nof4FurnaceTemperatureActual,
|
|
|
|
|
|
nof5FurnaceTemperatureActual: furnace.nof5FurnaceTemperatureActual,
|
2026-01-15 17:37:57 +08:00
|
|
|
|
phFurnaceTemperatureActual: furnace.phFurnaceTemperatureActual,
|
|
|
|
|
|
rtf1FurnaceTemperatureActual: furnace.rtf1FurnaceTemperatureActual,
|
2026-01-15 20:18:37 +08:00
|
|
|
|
rtf2FurnaceTemperatureActual: furnace.rtf2FurnaceTemperatureActual,
|
|
|
|
|
|
sfFurnaceTemperatureActual: furnace.sfFurnaceTemperatureActual,
|
|
|
|
|
|
tdsFurnaceTemperatureActual: furnace.tdsFurnaceTemperatureActual,
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return
|
2026-01-15 17:37:57 +08:00
|
|
|
|
const g = this.chartSeries[group]
|
|
|
|
|
|
if (!g) return
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 带钢速度折线图使用单独的最大点数限制
|
|
|
|
|
|
const maxPoints = group === 'stripSpeed' ? this.stripSpeedMaxPoints : this.chartMaxPoints
|
|
|
|
|
|
|
|
|
|
|
|
// 直接修改数组内容,保持数组引用不变,避免触发图表重新初始化
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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)
|
|
|
|
|
|
})
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果超过最大点数,去掉最早的数据
|
|
|
|
|
|
if (g.time.length > maxPoints) {
|
2026-01-15 17:37:57 +08:00
|
|
|
|
g.time.shift()
|
|
|
|
|
|
Object.keys(values).forEach((k) => {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (g[k] && g[k].length > maxPoints) {
|
|
|
|
|
|
g[k].shift()
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 生成多序列折线图数据
|
|
|
|
|
|
toGroupLineChart(group) {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (this.isDestroyed) return { categories: [], series: [] }
|
2026-01-15 17:37:57 +08:00
|
|
|
|
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] || []
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 追加带钢速度点到折线图(持续刷新)
|
|
|
|
|
|
appendStripSpeedPoint(payload) {
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
const entry = payload.appMeasureEntryMessage || {}
|
|
|
|
|
|
if (entry && entry.stripSpeed !== null && entry.stripSpeed !== undefined) {
|
|
|
|
|
|
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 speed = Number(entry.stripSpeed)
|
|
|
|
|
|
if (Number.isFinite(speed)) {
|
|
|
|
|
|
this.pushSeries('stripSpeed', t, { stripSpeed: speed })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 17:37:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
formatNum(v) {
|
|
|
|
|
|
if (v === null || v === undefined || Number.isNaN(Number(v))) return '—'
|
|
|
|
|
|
return Number(v).toFixed(2)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 不再按设备分块展示:pickSourceMsg/getRealtimeFieldValue 已弃用
|
|
|
|
|
|
|
|
|
|
|
|
// 快捷导航(已移除)
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async refreshAll() {
|
|
|
|
|
|
if (this.isRefreshing) return
|
|
|
|
|
|
this.isRefreshing = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([this.loadDeviceDefs(), this.loadFieldMeta()])
|
|
|
|
|
|
this.updateLastTime()
|
|
|
|
|
|
} finally {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
if (!this.isDestroyed) {
|
|
|
|
|
|
this.isRefreshing = false
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
updateLastTime() {
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有起始时间,使用当前时间作为起始时间
|
|
|
|
|
|
if (!this.startTime) {
|
|
|
|
|
|
this.startTime = new Date()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
const now = new Date()
|
2026-01-15 20:18:37 +08:00
|
|
|
|
const diff = now - this.startTime // 时间差(毫秒)
|
|
|
|
|
|
|
|
|
|
|
|
// 计算天数、小时数、分钟数
|
|
|
|
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
|
|
|
|
|
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
|
|
|
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化显示
|
|
|
|
|
|
let timeStr = ''
|
|
|
|
|
|
if (days > 0) {
|
|
|
|
|
|
timeStr += `${days}天`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hours > 0 || days > 0) {
|
|
|
|
|
|
timeStr += `${hours}小时`
|
|
|
|
|
|
}
|
|
|
|
|
|
timeStr += `${minutes}分钟`
|
|
|
|
|
|
|
|
|
|
|
|
this.lastUpdateTime = timeStr || '0分钟'
|
2026-01-05 14:29:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
formatTime(str) {
|
|
|
|
|
|
if (!str) return ''
|
|
|
|
|
|
const d = new Date(str)
|
|
|
|
|
|
const h = String(d.getHours()).padStart(2, '0')
|
|
|
|
|
|
const m = String(d.getMinutes()).padStart(2, '0')
|
|
|
|
|
|
return `${h}:${m}`
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
// 格式化持续时间:将分钟数转换为"X天X小时X分钟"格式
|
|
|
|
|
|
formatDuration(minutes) {
|
|
|
|
|
|
if (minutes === null || minutes === undefined || minutes === '') return '—'
|
|
|
|
|
|
const totalMinutes = Math.floor(Number(minutes))
|
|
|
|
|
|
if (isNaN(totalMinutes) || totalMinutes < 0) return '—'
|
|
|
|
|
|
|
|
|
|
|
|
const days = Math.floor(totalMinutes / (24 * 60))
|
|
|
|
|
|
const hours = Math.floor((totalMinutes % (24 * 60)) / 60)
|
|
|
|
|
|
const mins = Math.floor(totalMinutes % 60)
|
|
|
|
|
|
|
|
|
|
|
|
let result = ''
|
|
|
|
|
|
if (days > 0) {
|
|
|
|
|
|
result += `${days}天`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hours > 0 || days > 0) {
|
|
|
|
|
|
result += `${hours}小时`
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mins > 0 || result === '') {
|
|
|
|
|
|
result += `${mins}分钟`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result || '0分钟'
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
formatValue(v) {
|
|
|
|
|
|
if (v === null || v === undefined || v === '') return '—'
|
|
|
|
|
|
const n = Number(v)
|
|
|
|
|
|
if (Number.isNaN(n)) return '—'
|
|
|
|
|
|
return n.toFixed(2)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.page-container {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 15:38:20 +08:00
|
|
|
|
.tab-container {
|
2025-10-31 19:10:08 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-bottom: 2rpx solid #e4e7ed;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 15:38:20 +08:00
|
|
|
|
.tab-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
text-align: center;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
padding: 28rpx 0;
|
2025-10-29 15:38:20 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.tab-label {
|
|
|
|
|
|
font-size: 28rpx;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
color: #606266;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
font-weight: 400;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
&.tab-active {
|
|
|
|
|
|
.tab-label {
|
|
|
|
|
|
color: #0066cc;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
2025-10-29 15:38:20 +08:00
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.tab-indicator {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
2025-10-31 19:10:08 +08:00
|
|
|
|
width: 60rpx;
|
|
|
|
|
|
height: 3rpx;
|
|
|
|
|
|
background: #0066cc;
|
|
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.refresh-btn-fixed {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 32rpx;
|
|
|
|
|
|
bottom: 120rpx;
|
|
|
|
|
|
width: 96rpx;
|
|
|
|
|
|
height: 96rpx;
|
|
|
|
|
|
background: #0066cc;
|
|
|
|
|
|
border-radius: 50%;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
display: flex;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-shadow: 0 8rpx 20rpx rgba(0, 102, 204, 0.4);
|
|
|
|
|
|
z-index: 999;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
&:active {
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.refresh-icon {
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
line-height: 1;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
&.rotating {
|
|
|
|
|
|
animation: rotate 1s linear infinite;
|
|
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
/* 快速导航菜单(固定在左下角,模仿 acidity.vue) */
|
|
|
|
|
|
.nav-menu-fixed {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: 32rpx;
|
|
|
|
|
|
bottom: 120rpx;
|
|
|
|
|
|
z-index: 998;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-toggle {
|
|
|
|
|
|
width: 96rpx;
|
|
|
|
|
|
height: 96rpx;
|
|
|
|
|
|
background: #409eff;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-shadow: 0 8rpx 20rpx rgba(64, 158, 255, 0.4);
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-toggle-icon {
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-items {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
animation: slideUp 0.3s ease;
|
|
|
|
|
|
max-height: 60vh;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes slideUp {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(20rpx);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-item {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 2rpx solid #409eff;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
padding: 16rpx 24rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(64, 158, 255, 0.2);
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
background: #f0f9ff;
|
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-label {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
@keyframes rotate {
|
2026-01-05 14:29:33 +08:00
|
|
|
|
from {
|
|
|
|
|
|
transform: rotate(0deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.scroll-container {
|
|
|
|
|
|
height: calc(100vh - 96rpx);
|
|
|
|
|
|
padding: 24rpx;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
.placeholder-box {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx dashed #dcdfe6;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 28rpx 20rpx;
|
|
|
|
|
|
margin-bottom: 18rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.placeholder-text {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.status-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
background: #fff;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
padding: 24rpx 32rpx;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.status-item {
|
|
|
|
|
|
flex: 1;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
display: flex;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.status-label {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #909399;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.status-value {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #303133;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
&.status-通畅 {
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.status-异常 {
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
&.status-time {
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 24rpx;
|
2025-10-29 15:38:20 +08:00
|
|
|
|
}
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.status-divider {
|
|
|
|
|
|
width: 1rpx;
|
|
|
|
|
|
height: 40rpx;
|
|
|
|
|
|
background: #e4e7ed;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.section {
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 30rpx;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
font-weight: 500;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
color: #303133;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
padding-left: 16rpx;
|
|
|
|
|
|
border-left: 4rpx solid #0066cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
.strip-status-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-label {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-value {
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #0066cc;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-unit {
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-bar {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 8rpx;
|
|
|
|
|
|
background: #e4e7ed;
|
|
|
|
|
|
border-radius: 4rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: linear-gradient(90deg, #0066cc 0%, #409eff 100%);
|
|
|
|
|
|
border-radius: 4rpx;
|
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-text {
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.device-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
margin-bottom: 18rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-title {
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-sub {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-top: 6rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-toggle {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #0066cc;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.metrics-grid-3 {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
2026-01-05 14:29:33 +08:00
|
|
|
|
gap: 16rpx;
|
2025-10-29 15:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.metric-box {
|
2025-10-29 15:38:20 +08:00
|
|
|
|
background: #fff;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8rpx;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
padding: 18rpx 12rpx;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
text-align: center;
|
2025-10-29 15:38:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:10:08 +08:00
|
|
|
|
.metric-name {
|
|
|
|
|
|
display: block;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
font-size: 22rpx;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
color: #909399;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
margin-bottom: 12rpx;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-value {
|
|
|
|
|
|
display: block;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
font-size: 44rpx;
|
|
|
|
|
|
font-weight: 600;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
color: #0066cc;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
line-height: 1;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
|
|
|
|
|
|
&.small {
|
|
|
|
|
|
font-size: 34rpx;
|
|
|
|
|
|
}
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-unit {
|
|
|
|
|
|
display: block;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 20:18:37 +08:00
|
|
|
|
.metric-set-value {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-top: 4rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.device-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-top: 14rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
|
padding: 12rpx 18rpx;
|
|
|
|
|
|
border: 2rpx solid #0066cc;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-text {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #0066cc;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-tip {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.history-strong {
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-selector {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-chip {
|
|
|
|
|
|
padding: 10rpx 14rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
border: 2rpx solid #dcdfe6;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-chip.active {
|
|
|
|
|
|
border-color: #0066cc;
|
|
|
|
|
|
background: #f0f9ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chip-text {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-chip.active .chip-text {
|
|
|
|
|
|
color: #0066cc;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-tip {
|
2025-10-31 19:10:08 +08:00
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
color: #909399;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
margin-left: auto;
|
2025-10-31 19:10:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-box {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
padding: 24rpx 16rpx;
|
2025-10-27 13:21:43 +08:00
|
|
|
|
}
|
2025-12-07 12:54:39 +08:00
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.raw-box {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
padding: 24rpx 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-card {
|
2026-01-15 17:37:57 +08:00
|
|
|
|
background: #fff;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 32rpx 24rpx;
|
|
|
|
|
|
text-align: center;
|
2026-01-15 17:37:57 +08:00
|
|
|
|
border: 1rpx solid #e4e7ed;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-card:nth-child(2) {
|
2026-01-15 17:37:57 +08:00
|
|
|
|
background: #fff;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-card:nth-child(3) {
|
2026-01-15 17:37:57 +08:00
|
|
|
|
background: #fff;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 24rpx;
|
2026-01-15 17:37:57 +08:00
|
|
|
|
color: #909399;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-value {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
font-weight: 700;
|
2026-01-15 17:37:57 +08:00
|
|
|
|
color: #303133;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-unit {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 22rpx;
|
2026-01-15 17:37:57 +08:00
|
|
|
|
color: #909399;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:37:57 +08:00
|
|
|
|
.stoppage-filter {
|
|
|
|
|
|
margin: 0 0 20rpx 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-month-btn {
|
|
|
|
|
|
padding: 12rpx 24rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
border: 1rpx solid #dcdfe6;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-month-text {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.status-distribution {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-item-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 24rpx;
|
2025-12-07 12:54:39 +08:00
|
|
|
|
display: flex;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
justify-content: space-between;
|
2025-12-07 12:54:39 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.status-type {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-weight: 500;
|
2025-12-07 12:54:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.status-count {
|
|
|
|
|
|
font-size: 36rpx;
|
2025-12-07 12:54:39 +08:00
|
|
|
|
font-weight: 600;
|
2026-01-05 14:29:33 +08:00
|
|
|
|
color: #0066cc;
|
2025-12-07 12:54:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 14:29:33 +08:00
|
|
|
|
.stats-table {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-header {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
|
padding: 20rpx 16rpx;
|
|
|
|
|
|
border-bottom: 1rpx solid #e4e7ed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
|
|
|
|
|
padding: 20rpx 16rpx;
|
|
|
|
|
|
border-bottom: 1rpx solid #e4e7ed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-row:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-col {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-header .stats-col {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
2025-12-07 12:54:39 +08:00
|
|
|
|
}
|
2026-01-15 20:18:37 +08:00
|
|
|
|
|
|
|
|
|
|
.detail-cards {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(0, 102, 204, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card-header {
|
|
|
|
|
|
background: linear-gradient(135deg, #0066cc 0%, #409eff 100%);
|
|
|
|
|
|
padding: 20rpx 24rpx;
|
|
|
|
|
|
border-bottom: 1rpx solid #e4e7ed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card-title {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card-body {
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 12rpx 0;
|
|
|
|
|
|
border-bottom: 1rpx solid #f5f7fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-row:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-label {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
min-width: 120rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-value {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-cards {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #e4e7ed;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-card:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(245, 108, 108, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-card-header {
|
|
|
|
|
|
background: linear-gradient(135deg, #f56c6c 0%, #ff7875 100%);
|
|
|
|
|
|
padding: 20rpx 24rpx;
|
|
|
|
|
|
border-bottom: 1rpx solid #e4e7ed;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-card-title {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-card-duration {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
padding: 6rpx 16rpx;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-card-body {
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: 12rpx 0;
|
|
|
|
|
|
border-bottom: 1rpx solid #f5f7fa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-row:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-label {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
min-width: 120rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-value {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stoppage-remark {
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
white-space: normal;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
2025-10-31 19:10:08 +08:00
|
|
|
|
</style>
|