2026-01-19 13:29:44 +08:00
|
|
|
|
<template>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<div class="realtime-monitoring">
|
|
|
|
|
|
<!-- 替换uniapp scroll-view为普通div + 滚动样式 -->
|
|
|
|
|
|
<div class="scroll-container" v-if="currentTab === 1" ref="scrollContainer">
|
|
|
|
|
|
<!-- 顶部状态栏 -->
|
|
|
|
|
|
<div class="status-bar">
|
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
|
<span class="status-label">网络状态</span>
|
|
|
|
|
|
<span class="status-value" :class="isConnected ? 'status-通畅' : 'status-异常'">
|
|
|
|
|
|
{{ isConnected ? '已连接' : '未连接' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="status-divider"></div>
|
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
|
<span class="status-label">更新</span>
|
|
|
|
|
|
<span class="status-value status-time">{{ lastUpdateTime }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="status-divider"></div>
|
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
|
<span class="status-label">设备数</span>
|
|
|
|
|
|
<span class="status-value">{{ deviceDefs.length }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 带钢位置和速度模块 -->
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">带钢状态</div>
|
|
|
|
|
|
<div class="strip-status-grid">
|
|
|
|
|
|
<!-- 带钢位置卡片 -->
|
|
|
|
|
|
<div class="strip-status-card">
|
|
|
|
|
|
<span class="strip-status-label">带钢位置</span>
|
2026-01-23 14:45:23 +08:00
|
|
|
|
<span class="strip-status-value">{{ getRealtimeValueBySource('Entry', 'stripLocation') }}</span>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<span class="strip-status-unit">m</span>
|
|
|
|
|
|
<div class="progress-container">
|
|
|
|
|
|
<div class="progress-bar">
|
|
|
|
|
|
<div class="progress-fill" :style="{ width: stripLocationProgress + '%' }"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="progress-text">{{ stripLocationProgress.toFixed(1) }}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 带钢速度卡片 -->
|
|
|
|
|
|
<div class="strip-status-card">
|
|
|
|
|
|
<span class="strip-status-label">带钢速度</span>
|
2026-01-23 14:45:23 +08:00
|
|
|
|
<span class="strip-status-value">{{ getRealtimeValueBySource('Entry', 'stripSpeed') }}</span>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<span class="strip-status-unit">m/min</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 带钢速度折线图:替换为ECharts容器 -->
|
|
|
|
|
|
<div class="chart-box" ref="stripSpeedChart"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 实时数据卡片 + 统计图 -->
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">入口段</div>
|
|
|
|
|
|
<div class="metrics-grid-3">
|
|
|
|
|
|
<div class="metric-box" v-for="it in entryMetrics" :key="'entry_' + it.field">
|
|
|
|
|
|
<span class="metric-name">{{ it.label || getFieldLabel(it.field) }}</span>
|
2026-01-23 14:45:23 +08:00
|
|
|
|
<span class="metric-value">{{ getRealtimeValueBySource('Entry', it.field) }}</span>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-box" ref="entryChart"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">退火炉段</div>
|
|
|
|
|
|
<div class="metrics-grid-3">
|
|
|
|
|
|
<div class="metric-box" v-for="it in furnaceMetrics" :key="'furnace_' + it.actualField">
|
|
|
|
|
|
<span class="metric-name">{{ it.label || getFieldLabel(it.actualField) }}</span>
|
2026-01-23 14:45:23 +08:00
|
|
|
|
<span class="metric-value">{{ getRealtimeValueBySource('Furnace', it.actualField) }}</span>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<span class="metric-unit">{{ it.unit || getFieldUnit(it.actualField) }}</span>
|
|
|
|
|
|
<span class="metric-set-value" v-if="it.setField">
|
2026-01-23 14:45:23 +08:00
|
|
|
|
设定: {{ getRealtimeValueBySource('Furnace', it.setField) }}
|
2026-01-23 14:01:52 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">后处理/涂层段</div>
|
|
|
|
|
|
<div class="metrics-grid-3">
|
|
|
|
|
|
<div class="metric-box" v-for="it in coatMetrics" :key="'coat_' + it.field">
|
|
|
|
|
|
<span class="metric-name">{{ it.label || getFieldLabel(it.field) }}</span>
|
2026-01-23 14:45:23 +08:00
|
|
|
|
<span class="metric-value">{{ getRealtimeValueBySource('Coat', it.field) }}</span>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-box" ref="coatChart"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
|
<div class="section-title">出口段</div>
|
|
|
|
|
|
<div class="metrics-grid-3">
|
|
|
|
|
|
<div class="metric-box" v-for="it in exitMetrics" :key="'exit_' + it.field">
|
|
|
|
|
|
<span class="metric-name">{{ it.label || getFieldLabel(it.field) }}</span>
|
2026-01-23 14:45:23 +08:00
|
|
|
|
<span class="metric-value">{{ getRealtimeValueBySource('Exit', it.field) }}</span>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-box" ref="exitChart"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-19 13:29:44 +08:00
|
|
|
|
</div>
|
2026-01-23 14:01:52 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// 引入ECharts(Vue2常用图表库,替换uniapp的qiun-data-charts)
|
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
import WebSocketManager from '@/utils/websocketManager';
|
|
|
|
|
|
import { listDeviceEnumAll } from '@/api/pocket/deviceEnum'
|
|
|
|
|
|
import { getDeviceFieldMetaAll } from '@/api/pocket/deviceFieldMeta'
|
|
|
|
|
|
import { listDeviceSnapshotLatest } from '@/api/pocket/deviceSnapshot'
|
|
|
|
|
|
// 模拟API请求(实际项目替换为真实接口)
|
|
|
|
|
|
const mockApi = {
|
|
|
|
|
|
listDeviceEnumAll: () => Promise.resolve({ data: [] }),
|
|
|
|
|
|
getDeviceFieldMetaAll: () => Promise.resolve({ data: {} }),
|
|
|
|
|
|
listDeviceSnapshotLatest: ({ limit, deviceCode }) => Promise.resolve({ data: [] })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'RealTimeMonitoring',
|
|
|
|
|
|
// Vue2必须有data函数,返回响应式数据
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
currentTab: 1, // 默认显示标签1
|
|
|
|
|
|
isConnected: false, // 网络连接状态
|
|
|
|
|
|
lastUpdateTime: '--', // 最后更新时间
|
|
|
|
|
|
deviceDefs: [], // 设备定义列表
|
|
|
|
|
|
fieldMeta: {}, // 字段元数据
|
|
|
|
|
|
isDestroyed: false, // 组件销毁标志
|
|
|
|
|
|
chartMaxPoints: 30, // 图表最大数据点
|
|
|
|
|
|
chartSeries: { // 图表系列数据
|
|
|
|
|
|
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
|
|
|
|
|
furnace: { time: [], jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [], lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [], nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [], nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [], nof5FurnaceTemperatureActual: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [], sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [], potTemperature: [] },
|
|
|
|
|
|
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
|
|
|
|
|
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] },
|
|
|
|
|
|
stripSpeed: { time: [], stripSpeed: [] }
|
|
|
|
|
|
},
|
|
|
|
|
|
latestMeasure: null, // 最新测量数据
|
|
|
|
|
|
socketDataAppended: false, // socket数据是否已追加
|
|
|
|
|
|
// 各段指标配置
|
|
|
|
|
|
entryMetrics: [
|
2026-01-23 14:45:23 +08:00
|
|
|
|
{ field: 'stripLocation', label: '带钢位置', unit: 'm' },
|
2026-01-23 14:01:52 +08:00
|
|
|
|
{ field: 'stripSpeed', label: '带钢速度', unit: 'm/min' },
|
|
|
|
|
|
{ field: 'tensionPorBr1', label: '张力POR-BR1', unit: 'kN' },
|
|
|
|
|
|
{ field: 'tensionBr1Br2', label: '张力BR1-BR2', unit: 'kN' },
|
2026-01-23 14:45:23 +08:00
|
|
|
|
{ field: 'tensionBr2Br3', label: '张力BR2-BR3', unit: 'kN' },
|
|
|
|
|
|
|
2026-01-23 14:01:52 +08:00
|
|
|
|
],
|
|
|
|
|
|
furnaceMetrics: [
|
2026-01-23 14:45:23 +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-23 14:01:52 +08:00
|
|
|
|
],
|
|
|
|
|
|
coatMetrics: [
|
2026-01-23 14:45:23 +08:00
|
|
|
|
{ field: 'avrCoatingWeightTop', label: '上层平均涂层重量', unit: 'g/m²' },
|
|
|
|
|
|
{ field: 'avrCoatingWeightBottom', label: '下层平均涂层重量', unit: 'g/m²' },
|
|
|
|
|
|
{ field: 'airKnifePressure', label: '气刀压力', unit: '' },
|
|
|
|
|
|
{ field: 'stripSpeedTmExit', label: 'TM出口速度', unit: 'm/min' }
|
2026-01-23 14:01:52 +08:00
|
|
|
|
],
|
|
|
|
|
|
exitMetrics: [
|
2026-01-23 14:45:23 +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' },
|
|
|
|
|
|
{ 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' }
|
2026-01-23 14:01:52 +08:00
|
|
|
|
],
|
|
|
|
|
|
// ECharts实例缓存
|
|
|
|
|
|
chartInstances: {
|
|
|
|
|
|
stripSpeed: null,
|
|
|
|
|
|
entry: null,
|
|
|
|
|
|
coat: null,
|
|
|
|
|
|
exit: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-23 14:45:23 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
// 带钢位置进度(位置/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)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-23 14:01:52 +08:00
|
|
|
|
// Vue2生命周期钩子
|
|
|
|
|
|
created() {
|
|
|
|
|
|
// 组件创建时加载初始数据
|
|
|
|
|
|
this.loadDeviceDefs()
|
|
|
|
|
|
this.loadFieldMeta()
|
|
|
|
|
|
this.loadHistoryForCharts()
|
|
|
|
|
|
// 初始化WebSocket连接
|
|
|
|
|
|
this.initSocket()
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
|
|
|
|
|
// DOM挂载后初始化图表
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.initCharts()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
// 组件销毁前清理资源
|
|
|
|
|
|
this.isDestroyed = true
|
|
|
|
|
|
// 关闭WebSocket连接
|
|
|
|
|
|
if (this.socket) {
|
|
|
|
|
|
this.socket.disconnectAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
// 销毁ECharts实例
|
|
|
|
|
|
Object.values(this.chartInstances).forEach(instance => {
|
|
|
|
|
|
if (instance) {
|
|
|
|
|
|
instance.dispose()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
// 执行socket清理逻辑
|
|
|
|
|
|
if (this._clearSocketFlag) {
|
|
|
|
|
|
this._clearSocketFlag()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
// 加载设备定义
|
|
|
|
|
|
async loadDeviceDefs() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await listDeviceEnumAll()
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
this.deviceDefs = res.data || []
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载设备定义失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 加载字段元数据
|
|
|
|
|
|
async loadFieldMeta() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getDeviceFieldMetaAll()
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
this.fieldMeta = res.data || {}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载字段元数据失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 加载图表历史数据
|
|
|
|
|
|
async loadHistoryForCharts() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 重置图表系列(保留带钢速度数据)
|
|
|
|
|
|
this.chartSeries = {
|
|
|
|
|
|
entry: { time: [], stripSpeed: [], tensionPorBr1: [], tensionBr1Br2: [], tensionBr2Br3: [] },
|
|
|
|
|
|
furnace: { time: [], jcf1FurnaceTemperatureActual: [], jcf2FurnaceTemperatureActual: [], lbzFurnaceTemperatureActual: [], lthFurnaceTemperatureActual: [], nof1FurnaceTemperatureActual: [], nof2FurnaceTemperatureActual: [], nof3FurnaceTemperatureActual: [], nof4FurnaceTemperatureActual: [], nof5FurnaceTemperatureActual: [], phFurnaceTemperatureActual: [], rtf1FurnaceTemperatureActual: [], rtf2FurnaceTemperatureActual: [], sfFurnaceTemperatureActual: [], tdsFurnaceTemperatureActual: [], potTemperature: [] },
|
|
|
|
|
|
coat: { time: [], avrCoatingWeightTop: [], avrCoatingWeightBottom: [], airKnifePressure: [], stripSpeedTmExit: [] },
|
|
|
|
|
|
exit: { time: [], tensionBr8Br9: [], tensionBr9Tr: [], speedExitSection: [] },
|
|
|
|
|
|
stripSpeed: this.chartSeries.stripSpeed || { time: [], stripSpeed: [] }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const limit = this.chartMaxPoints
|
|
|
|
|
|
const tasks = []
|
|
|
|
|
|
|
|
|
|
|
|
const deviceMap = {
|
|
|
|
|
|
entry: { deviceCode: 'POR1', fields: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'] },
|
|
|
|
|
|
furnace: {
|
|
|
|
|
|
deviceCode: 'FUR2',
|
|
|
|
|
|
fields: [
|
|
|
|
|
|
'jcf1FurnaceTemperatureActual', 'jcf2FurnaceTemperatureActual',
|
|
|
|
|
|
'lbzFurnaceTemperatureActual', 'lthFurnaceTemperatureActual',
|
|
|
|
|
|
'nof1FurnaceTemperatureActual', 'nof2FurnaceTemperatureActual',
|
|
|
|
|
|
'nof3FurnaceTemperatureActual', 'nof4FurnaceTemperatureActual',
|
|
|
|
|
|
'nof5FurnaceTemperatureActual', 'phFurnaceTemperatureActual',
|
|
|
|
|
|
'rtf1FurnaceTemperatureActual', 'rtf2FurnaceTemperatureActual',
|
|
|
|
|
|
'sfFurnaceTemperatureActual', 'tdsFurnaceTemperatureActual',
|
|
|
|
|
|
'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) => {
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
const rows = res.data || []
|
|
|
|
|
|
const list = rows.slice().reverse() // 按时间升序
|
|
|
|
|
|
list.forEach((row) => {
|
|
|
|
|
|
if (this.isDestroyed) return
|
|
|
|
|
|
const t = (row.createTime || new Date().toTimeString()).slice(0, 8) // 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] || 0
|
|
|
|
|
|
})
|
|
|
|
|
|
this.pushSeries(group, t, values)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => { })
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all(tasks)
|
|
|
|
|
|
// 历史数据加载完成后更新图表
|
|
|
|
|
|
this.updateCharts()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载图表历史数据失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
initSocket() {
|
|
|
|
|
|
const vm = this
|
|
|
|
|
|
let isDestroyed = false
|
|
|
|
|
|
const pendingUpdates = []
|
|
|
|
|
|
|
|
|
|
|
|
// 替换为真实的WebSocket地址
|
|
|
|
|
|
const wsUrl = 'ws://140.143.206.120:18081/websocket'
|
|
|
|
|
|
const socket = new WebSocketManager(wsUrl)
|
|
|
|
|
|
this.socket = socket
|
|
|
|
|
|
|
|
|
|
|
|
console.log('初始化WebSocket连接')
|
|
|
|
|
|
socket.connect('track_measure', {
|
|
|
|
|
|
onOpen: () => {
|
|
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
vm.isConnected = true
|
|
|
|
|
|
console.log('WebSocket连接成功')
|
|
|
|
|
|
},
|
|
|
|
|
|
onClose: () => {
|
|
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
vm.isConnected = false
|
|
|
|
|
|
// 断线重连逻辑
|
|
|
|
|
|
if (!vm.isDestroyed) {
|
|
|
|
|
|
setTimeout(() => vm.initSocket(), 3000)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
if (isDestroyed) return
|
|
|
|
|
|
vm.isConnected = false
|
|
|
|
|
|
console.error('WebSocket错误:', error)
|
|
|
|
|
|
},
|
|
|
|
|
|
onMessage: (event) => {
|
|
|
|
|
|
if (!vm) {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 解析WebSocket消息
|
|
|
|
|
|
let payload = null
|
|
|
|
|
|
if (typeof event.data === 'string') {
|
|
|
|
|
|
payload = JSON.parse(event)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload = event
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// if (!payload || isDestroyed || !vm) return
|
|
|
|
|
|
|
|
|
|
|
|
// 异步更新数据,避免阻塞UI
|
|
|
|
|
|
const updateFrame = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
vm.latestMeasure = payload
|
|
|
|
|
|
|
2026-01-23 14:45:23 +08:00
|
|
|
|
console.log('更新数据:', payload)
|
2026-01-23 14:01:52 +08:00
|
|
|
|
vm.appendStripSpeedPoint(payload)
|
|
|
|
|
|
vm.updateLastTime()
|
|
|
|
|
|
// 实时更新图表
|
|
|
|
|
|
vm.updateCharts()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
console.error('更新实时数据失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用requestAnimationFrame优化渲染
|
|
|
|
|
|
let frameId = requestAnimationFrame(updateFrame)
|
|
|
|
|
|
pendingUpdates.push(frameId)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!isDestroyed) {
|
|
|
|
|
|
console.error('解析WebSocket数据失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 保存清理函数
|
|
|
|
|
|
this._clearSocketFlag = () => {
|
|
|
|
|
|
isDestroyed = true
|
|
|
|
|
|
// 取消所有待执行的更新
|
|
|
|
|
|
pendingUpdates.forEach(id => cancelAnimationFrame(id))
|
|
|
|
|
|
pendingUpdates.length = 0
|
|
|
|
|
|
// 关闭socket
|
|
|
|
|
|
socket.disconnectAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 初始化ECharts图表实例
|
|
|
|
|
|
initCharts() {
|
|
|
|
|
|
// 带钢速度图表
|
|
|
|
|
|
this.chartInstances.stripSpeed = echarts.init(this.$refs.stripSpeedChart)
|
|
|
|
|
|
this.chartInstances.stripSpeed.setOption({
|
|
|
|
|
|
title: { text: '带钢速度趋势' },
|
|
|
|
|
|
xAxis: { type: 'category', data: this.chartSeries.stripSpeed.time },
|
|
|
|
|
|
yAxis: { type: 'value', name: 'm/min' },
|
|
|
|
|
|
series: [{ name: '带钢速度', type: 'line', data: this.chartSeries.stripSpeed.stripSpeed }],
|
|
|
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
|
|
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 入口段图表
|
|
|
|
|
|
this.chartInstances.entry = echarts.init(this.$refs.entryChart)
|
|
|
|
|
|
// 涂层段图表
|
|
|
|
|
|
this.chartInstances.coat = echarts.init(this.$refs.coatChart)
|
|
|
|
|
|
// 出口段图表
|
|
|
|
|
|
this.chartInstances.exit = echarts.init(this.$refs.exitChart)
|
|
|
|
|
|
|
|
|
|
|
|
// 监听窗口大小变化,自适应图表
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
|
Object.values(this.chartInstances).forEach(instance => {
|
|
|
|
|
|
if (instance) instance.resize()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 更新图表数据
|
|
|
|
|
|
updateCharts() {
|
|
|
|
|
|
// 更新带钢速度图表
|
|
|
|
|
|
if (this.chartInstances?.stripSpeed) {
|
|
|
|
|
|
this.chartInstances.stripSpeed.setOption({
|
|
|
|
|
|
xAxis: { data: this.chartSeries.stripSpeed.time },
|
|
|
|
|
|
series: [{ data: this.chartSeries.stripSpeed.stripSpeed }]
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新入口段图表
|
|
|
|
|
|
if (this.chartInstances?.entry) {
|
|
|
|
|
|
this.chartInstances.entry.setOption(this.toGroupLineChart('entry'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新涂层段图表
|
|
|
|
|
|
if (this.chartInstances?.coat) {
|
|
|
|
|
|
this.chartInstances.coat.setOption(this.toGroupLineChart('coat'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新出口段图表
|
|
|
|
|
|
if (this.chartInstances?.exit) {
|
|
|
|
|
|
this.chartInstances.exit.setOption(this.toGroupLineChart('exit'))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 向图表系列追加数据
|
|
|
|
|
|
pushSeries(group, time, values) {
|
|
|
|
|
|
const series = this.chartSeries[group]
|
|
|
|
|
|
if (!series) return
|
|
|
|
|
|
|
|
|
|
|
|
// 控制数据点数量不超过最大值
|
|
|
|
|
|
if (series.time.length >= this.chartMaxPoints) {
|
|
|
|
|
|
series.time.shift()
|
|
|
|
|
|
Object.keys(values).forEach(key => {
|
|
|
|
|
|
if (series[key]) series[key].shift()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新数据
|
|
|
|
|
|
series.time.push(time)
|
|
|
|
|
|
Object.keys(values).forEach(key => {
|
|
|
|
|
|
if (series[key]) {
|
|
|
|
|
|
series[key].push(values[key] || 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 追加图表数据点(从WebSocket)
|
|
|
|
|
|
appendChartPoint(payload) {
|
|
|
|
|
|
const now = new Date().toTimeString().slice(0, 8)
|
|
|
|
|
|
// 按设备分组处理payload数据
|
|
|
|
|
|
const groups = {
|
|
|
|
|
|
entry: ['stripSpeed', 'tensionPorBr1', 'tensionBr1Br2', 'tensionBr2Br3'],
|
|
|
|
|
|
coat: ['avrCoatingWeightTop', 'avrCoatingWeightBottom', 'airKnifePressure', 'stripSpeedTmExit'],
|
|
|
|
|
|
exit: ['tensionBr8Br9', 'tensionBr9Tr', 'speedExitSection']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Object.keys(groups).forEach(group => {
|
|
|
|
|
|
const values = {}
|
|
|
|
|
|
groups[group].forEach(field => {
|
|
|
|
|
|
values[field] = payload[field] || 0
|
|
|
|
|
|
})
|
|
|
|
|
|
this.pushSeries(group, now, values)
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
// 追加带钢速度数据点
|
|
|
|
|
|
appendStripSpeedPoint(payload) {
|
|
|
|
|
|
const now = new Date().toTimeString().slice(0, 8)
|
2026-01-23 14:45:23 +08:00
|
|
|
|
const speed = payload.appMeasureEntryMessage.stripSpeed || 0
|
2026-01-23 14:01:52 +08:00
|
|
|
|
this.pushSeries('stripSpeed', now, { stripSpeed: speed })
|
|
|
|
|
|
},
|
|
|
|
|
|
// 更新最后更新时间
|
|
|
|
|
|
updateLastTime() {
|
|
|
|
|
|
this.lastUpdateTime = new Date().toTimeString().slice(0, 8)
|
|
|
|
|
|
},
|
|
|
|
|
|
// 根据数据源和字段获取实时值
|
|
|
|
|
|
getRealtimeValueBySource(source, field) {
|
|
|
|
|
|
if (!this.latestMeasure) return '--'
|
|
|
|
|
|
// 按source映射到不同的字段前缀/规则
|
2026-01-23 14:45:23 +08:00
|
|
|
|
console.log('source:', source, field, this.latestMeasure[`appMeasure${source}Message`])
|
|
|
|
|
|
const value = this.latestMeasure[`appMeasure${source}Message`][field] || 0
|
2026-01-23 14:01:52 +08:00
|
|
|
|
return value.toFixed(2) // 格式化显示
|
|
|
|
|
|
},
|
|
|
|
|
|
// 获取字段标签
|
|
|
|
|
|
getFieldLabel(field) {
|
|
|
|
|
|
return this.fieldMeta[field]?.label || field
|
|
|
|
|
|
},
|
|
|
|
|
|
// 获取字段单位
|
|
|
|
|
|
getFieldUnit(field) {
|
|
|
|
|
|
return this.fieldMeta[field]?.unit || ''
|
|
|
|
|
|
},
|
|
|
|
|
|
// 生成分组折线图配置
|
|
|
|
|
|
toGroupLineChart(group) {
|
|
|
|
|
|
const seriesData = this.chartSeries[group]
|
|
|
|
|
|
if (!seriesData) return {}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建ECharts系列配置
|
|
|
|
|
|
const series = []
|
|
|
|
|
|
// 过滤掉time字段,生成每个指标的折线
|
|
|
|
|
|
Object.keys(seriesData).forEach(key => {
|
|
|
|
|
|
if (key === 'time') return
|
|
|
|
|
|
series.push({
|
|
|
|
|
|
name: this.getFieldLabel(key),
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: seriesData[key],
|
|
|
|
|
|
smooth: true // 平滑曲线
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: { text: `${this.getGroupTitle(group)}趋势` },
|
|
|
|
|
|
xAxis: { type: 'category', data: seriesData.time },
|
|
|
|
|
|
yAxis: { type: 'value' },
|
|
|
|
|
|
series,
|
|
|
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
|
|
|
legend: { top: 20 },
|
|
|
|
|
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 获取分组标题
|
|
|
|
|
|
getGroupTitle(group) {
|
|
|
|
|
|
const titles = {
|
|
|
|
|
|
entry: '入口段',
|
|
|
|
|
|
coat: '后处理/涂层段',
|
|
|
|
|
|
exit: '出口段'
|
|
|
|
|
|
}
|
|
|
|
|
|
return titles[group] || group
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.realtime-monitoring {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: calc(100vh - 124px);
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scroll-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 状态栏样式 */
|
|
|
|
|
|
.status-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 12px 15px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin: 0 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-value {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-通畅 {
|
|
|
|
|
|
color: #00b42a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-异常 {
|
|
|
|
|
|
color: #f53f3f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-divider {
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
background-color: #eee;
|
|
|
|
|
|
margin: 0 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 通用区块样式 */
|
|
|
|
|
|
.section {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 带钢状态样式 */
|
|
|
|
|
|
.strip-status-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-card {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border: 1px solid #eee;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-value {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strip-status-unit {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-container {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-bar {
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
background-color: #eee;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background-color: #1890ff;
|
|
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-text {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 指标网格样式 */
|
|
|
|
|
|
.metrics-grid-3 {
|
|
|
|
|
|
display: grid;
|
2026-01-23 14:45:23 +08:00
|
|
|
|
grid-template-columns: repeat(8, 1fr);
|
2026-01-23 14:01:52 +08:00
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-box {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #eee;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-name {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-value {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-unit {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-set-value {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 图表容器样式 */
|
|
|
|
|
|
.chart-box {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 300px;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|