Files
xgy-oa/klp-ui/src/views/lines/zinc/components/real-time-monitoring.vue
砂糖 86a1ea0694 feat(wms): 添加镀锌卷标签组件并替换旧标签组件
- 新增GalvanisedTag.vue组件用于显示镀锌卷标签
- 在LabelRender中替换SampleTagPreview为新的GalvanisedTag组件
- 调整标签类型判断逻辑,将镀锌卷的labelType从'3'改为'4'
- 移除不再使用的旧标签组件引用和注释代码
2026-01-23 18:05:59 +08:00

771 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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>
<span class="strip-status-value">{{ getRealtimeValueBySource('Entry', 'stripLocation') }}</span>
<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>
<span class="strip-status-value">{{ getRealtimeValueBySource('Entry', 'stripSpeed') }}</span>
<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>
<span class="metric-value">{{ getRealtimeValueBySource('Entry', it.field) }}</span>
<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>
<span class="metric-value">{{ getRealtimeValueBySource('Furnace', it.actualField) }}</span>
<span class="metric-unit">{{ it.unit || getFieldUnit(it.actualField) }}</span>
<span class="metric-set-value" v-if="it.setField">
设定: {{ getRealtimeValueBySource('Furnace', it.setField) }}
</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>
<span class="metric-value">{{ getRealtimeValueBySource('Coat', it.field) }}</span>
<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>
<span class="metric-value">{{ getRealtimeValueBySource('Exit', it.field) }}</span>
<span class="metric-unit">{{ it.unit || getFieldUnit(it.field) }}</span>
</div>
</div>
<div class="chart-box" ref="exitChart"></div>
</div>
</div>
</div>
</template>
<script>
// 引入EChartsVue2常用图表库替换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: [
{ field: 'stripLocation', label: '带钢位置', unit: 'm' },
{ field: 'stripSpeed', label: '带钢速度', unit: 'm/min' },
{ field: 'tensionPorBr1', label: '张力POR-BR1', unit: 'kN' },
{ field: 'tensionBr1Br2', label: '张力BR1-BR2', unit: 'kN' },
{ field: 'tensionBr2Br3', label: '张力BR2-BR3', unit: 'kN' },
],
furnaceMetrics: [
{ 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: '℃' },
],
coatMetrics: [
{ field: 'avrCoatingWeightTop', label: '上层平均涂层重量', unit: 'g/m²' },
{ field: 'avrCoatingWeightBottom', label: '下层平均涂层重量', unit: 'g/m²' },
{ field: 'airKnifePressure', label: '气刀压力', unit: '' },
{ field: 'stripSpeedTmExit', label: 'TM出口速度', unit: 'm/min' }
],
exitMetrics: [
{ field: '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' }
],
// ECharts实例缓存
chartInstances: {
stripSpeed: null,
entry: null,
coat: null,
exit: null
}
}
},
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)
},
},
// 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
console.log('更新数据:', payload)
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)
const speed = payload.appMeasureEntryMessage.stripSpeed || 0
this.pushSeries('stripSpeed', now, { stripSpeed: speed })
},
// 更新最后更新时间
updateLastTime() {
this.lastUpdateTime = new Date().toTimeString().slice(0, 8)
},
// 根据数据源和字段获取实时值
getRealtimeValueBySource(source, field) {
if (!this.latestMeasure) return '--'
// 按source映射到不同的字段前缀/规则
console.log('source:', source, field, this.latestMeasure[`appMeasure${source}Message`])
const value = this.latestMeasure[`appMeasure${source}Message`][field] || 0
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;
grid-template-columns: repeat(8, 1fr);
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>