Files
QingAnVue2TV/src/views/qingAn.vue
2026-07-11 13:47:09 +08:00

2213 lines
61 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.
<!-- eslint-disable vue/max-attributes-per-line -->
<template>
<div
id="screen"
class="qa-screen"
:style="{
width: `${style.width}px`,
height: `${style.height}px`,
transform: style.transform
}"
>
<div class="title-png">
<div class="left-control-bar">
<img class="brand-logo" src="/png/logo.png" alt="庆安" />
<div class="mode-button-group">
<button type="button" class="mode-button" :class="{ 'mode-button--active': currentMode === 'DT' }" @click="changeDtMode">孪生模式</button>
<button type="button" class="mode-button" :class="{ 'mode-button--active': currentMode === 'SM' }" @click="changeSmMode">仿真模式</button>
</div>
</div>
<span class="title-text">{{ titleText }}</span>
<div class="right-control-bar">
<div class="df-header__timebar">
<span class="df-header__year">{{ headerDateCn }}</span>
<span class="df-header__time">{{ headerTime }}</span>
</div>
<div class="view-switch">
<button
v-for="item in viewButtons"
:key="item.value"
type="button"
class="view-switch__btn"
:class="{ 'view-switch__btn--active': activeView === item.value }"
@click="changeView(item.value)"
>
{{ item.label }}
</button>
</div>
</div>
</div>
<div class="leftLine"></div>
<div class="rightLine"></div>
<div class="main-content">
<iframe id="dmtIframe" v-if="mode === 'DT' && srcDt" allowtransparency="true" class="model-bg-frame" :src="srcDt" title="数字孪生模型" @load="handleIframeLoad"></iframe>
<iframe id="dmtIframe" v-if="mode === 'SM' && srcSm" allowtransparency="true" class="model-bg-frame" :src="srcSm" title="仿真模型" @load="handleIframeLoad"></iframe>
<div v-if="!mode || (!srcDt && !srcSm)" class="model-placeholder">等待配置三维模型地址</div>
<div class="left-box">
<section class="qa-panel qa-panel--combined">
<div class="left-title-header">
<span class="left-title-text">线体任务联动状态</span>
</div>
<div class="qa-panel-body line-monitor-body">
<div class="line-monitor-scroll-box">
<scroll class="line-monitor-scroll" :data="lineMonitorScrollList" :class-option="lineMonitorScrollOption">
<div class="line-monitor-content">
<div v-for="(row, index) in lineMonitorScrollList" :key="row.name + index" class="monitor-card">
<div class="monitor-head">
<div class="monitor-title">
<strong>{{ row.name }}</strong>
<span>{{ row.taskNo }}</span>
</div>
<span class="line-status" :class="`line-status--${row.type}`">{{ row.status }}</span>
</div>
<div class="task-summary">
<div class="task-route">
<span>起止位置</span>
<strong>{{ row.startPos }} -> {{ row.endPos }}</strong>
</div>
<div v-for="item in row.taskInfo" :key="item.label" class="task-field">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
<div class="signal-section">
<span class="signal-section__title">线体状态</span>
<div class="signal-grid signal-grid--line">
<div v-for="signal in row.lineSignals" :key="signal.label" class="signal-chip" :class="`signal-chip--${signal.state}`">
<i></i>
<span>{{ signal.label }}</span>
</div>
</div>
</div>
<div class="signal-section">
<span class="signal-section__title">任务握手</span>
<div class="signal-grid signal-grid--handshake">
<div v-for="signal in row.handshakes" :key="signal.label" class="signal-chip" :class="`signal-chip--${signal.state}`">
<i></i>
<span>{{ signal.label }}</span>
</div>
</div>
</div>
</div>
</div>
</scroll>
</div>
</div>
</section>
</div>
<div class="right-box">
<section class="qa-panel">
<div class="right-title-header">
<span class="right-title-text">加工设备状态</span>
</div>
<div class="qa-panel-body equipment-body">
<div class="equipment-head">
<span>设备</span>
<span>产量</span>
<span>联动信号</span>
<span>状态</span>
</div>
<div class="seamless-box">
<scroll class="seamless-scroll" :data="equipmentScrollList" :class-option="equipmentScrollOption">
<div class="seamless-content">
<div
v-for="(item, index) in equipmentScrollList"
:key="item.name + index"
class="equipment-row"
:class="{ 'equipment-row--alarm': item.type === 'alarm' }"
>
<strong class="equipment-name">{{ item.name }}</strong>
<span class="equipment-count">{{ item.actual }}</span>
<div class="equipment-tags">
<span v-for="tag in item.tags" :key="tag.text" class="status-pill status-pill--compact" :class="`status-pill--${tag.type}`">
{{ tag.text }}
</span>
</div>
<span class="status-pill equipment-status" :class="`status-pill--${item.type}`">{{ item.status }}</span>
</div>
</div>
</scroll>
</div>
</div>
</section>
<section class="qa-panel">
<div class="right-title-header">
<span class="right-title-text">辅助设备状态</span>
</div>
<div class="qa-panel-body assist-body">
<div class="assist-head">
<span>设备</span>
<span>联动信号</span>
<span>状态</span>
</div>
<div class="assist-list">
<div v-for="item in assistList" :key="item.name" class="assist-status-row">
<strong class="assist-name">{{ item.name }}</strong>
<div class="assist-tags">
<span v-for="tag in item.tags" :key="tag.text" class="status-pill status-pill--compact" :class="`status-pill--${tag.type}`">
{{ tag.text }}
</span>
</div>
<span class="status-pill assist-status" :class="`status-pill--${item.type}`">{{ item.status }}</span>
</div>
</div>
</div>
</section>
</div>
<div class="bottom-box">
<section class="qa-panel">
<div class="center-title-header">
<span>物料库位矩阵</span>
</div>
<div class="qa-panel-body qa-panel-body--storage">
<div class="storage-groups">
<div v-for="group in storageGroups" :key="group.name" class="storage-card">
<div class="storage-head">
<strong>{{ group.name }}</strong>
<span>{{ group.filled }} / {{ group.total }}</span>
</div>
<div class="slots">
<span v-for="slot in group.slots" :key="slot.name" class="slot" :class="{ 'slot--filled': slot.filled }">
{{ slot.name }}
</span>
</div>
</div>
</div>
</div>
</section>
<section class="qa-panel">
<div class="center-title-header">
<span>报警与异常事件</span>
</div>
<div class="qa-panel-body event-body">
<div ref="alarmPieChart" class="alarm-bar-chart"></div>
<div class="event-head">
<span>时间</span>
<span>设备</span>
<span>事件内容</span>
</div>
<div class="seamless-box">
<scroll class="seamless-scroll" :data="alarmScrollList" :class-option="alarmScrollOption">
<div class="seamless-content">
<div v-for="(event, index) in alarmScrollList" :key="event.time + event.device + index" class="event-row">
<span>{{ event.time }}</span>
<strong>{{ event.device }}</strong>
<p>{{ event.message }}</p>
</div>
</div>
</scroll>
</div>
</div>
</section>
<section class="qa-panel">
<div class="center-title-header">
<span>设备运行履历</span>
</div>
<div class="qa-panel-body history-body">
<div class="history-legend">
<span v-for="item in historyLegend" :key="item.label" class="history-legend-item">
<i :class="`history-legend-dot history-legend-dot--${item.type}`"></i>
{{ item.label }}
</span>
</div>
<div class="history-head">
<span>设备</span>
<div class="history-scale">
<span v-for="tick in historyTicks" :key="tick">{{ tick }}</span>
</div>
</div>
<div class="seamless-box">
<scroll class="seamless-scroll" :data="deviceHistoryScrollList" :class-option="historyScrollOption">
<div class="seamless-content">
<div v-for="(item, index) in deviceHistoryScrollList" :key="item.name + index" class="history-row">
<div class="history-device">
<strong>{{ item.name }}</strong>
</div>
<div class="history-track">
<span
v-for="segment in item.segments"
:key="segment.label + segment.start"
class="history-segment"
:class="`history-segment--${segment.type}`"
:style="getHistorySegmentStyle(segment)"
></span>
</div>
</div>
</div>
</scroll>
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import scroll from 'vue-seamless-scroll'
export default {
name: 'QingAn',
components: { scroll },
data() {
return {
style: {
width: 1920,
height: 1080,
transform: 'scaleY(1) scaleX(1) translate(-50%, -50%)'
},
titleText: '',
headerDateCn: '',
headerTime: '',
iframeAddress: '',
mode: 'DT',
currentMode: 'DT',
srcDt: 'http://localhost:10056/',
srcSm: 'http://localhost:10059/',
autoPlayCraftCode: '',
modeSwitchTimer: null,
cameraSyncTimer: null,
iframeReadyTimer: null,
dmtIframeReady: false,
clockTimer: null,
resizeHandler: null,
alarmPieChart: null,
activeView: 'longRail',
viewButtons: [
{ label: '加工中心单元视角', value: 'longRail' },
{ label: '车床加工单元视角', value: 'shortRail' },
{ label: '插齿机单元视角', value: 'joint' },
{ label: '磨床加工单元视角', value: 'grinder' },
{ label: '全揽视角', value: 'overview' }
],
cameraViews: {
longRail: {
target: { x: -5.725619423653396, y: 1.559219531820475, z: -5.23360272959924 },
position: { x: -18.7530206850084, y: 1.6376732214652037, z: 15.344064598520557 },
zoom0: 1
},
shortRail: {
target: { x: -5.814177085428968, y: 16.255194514605996, z: -5.3456965650308055 },
position: { x: -17.571406723801864, y: 16.32599896951042, z: 13.22564819859711 },
zoom0: 1
},
joint: {
target: { x: 2.7622940494890424, y: 15.83905572715569, z: 0.08552052688719117 },
position: { x: -4.6476929900921515, y: 15.883680193065107, z: 11.790099601955712 },
zoom0: 1
},
grinder: {
target: { x: -10.128808700176803, y: -15.480679803858136, z: 1.9651555238025413 },
position: { x: -21.43448244172167, y: -15.531783203240701, z: 12.347383997813267 },
zoom0: 1
},
overview: {
target: { x: -2.4540886878602937, y: -1.7690793306726371, z: -0.104103172990748 },
position: { x: -44.67609815729721, y: -1.5148097061598855, z: 38.669527626788486 },
zoom0: 1
}
},
lineMonitorRows: [
{
name: '加工中心单元',
status: '执行中',
type: 'on',
taskNo: 'TASK 1001',
startPos: '01',
endPos: '06',
taskInfo: [
{ label: '配方号', value: 'RCP-01' },
{ label: '托盘', value: 'Pallet-A' }
],
lineSignals: [
{ label: 'PCS运行', state: 'on' },
{ label: '允许接收', state: 'on' },
{ label: '自动模式', state: 'on' },
{ label: '急停', state: 'off' }
],
handshakes: [
{ label: '启动', state: 'on' },
{ label: '准备好', state: 'on' },
{ label: '忙碌', state: 'on' },
{ label: '错误', state: 'off' },
{ label: '完成', state: 'off' }
]
},
{
name: '车床加工单元',
status: '准备就绪',
type: 'ok',
taskNo: 'TASK 1002',
startPos: '03',
endPos: '09',
taskInfo: [
{ label: '配方号', value: 'RCP-09' },
{ label: '托盘', value: 'Pallet-C' }
],
lineSignals: [
{ label: 'PCS运行', state: 'on' },
{ label: '允许接收', state: 'on' },
{ label: '自动模式', state: 'on' },
{ label: '急停', state: 'off' }
],
handshakes: [
{ label: '启动', state: 'off' },
{ label: '准备好', state: 'on' },
{ label: '忙碌', state: 'off' },
{ label: '错误', state: 'off' },
{ label: '完成', state: 'off' }
]
},
{
name: '插齿机单元',
status: '异常中断',
type: 'alarm',
taskNo: 'TASK 1003',
startPos: '02',
endPos: '04',
taskInfo: [
{ label: '配方号', value: 'RCP-11' },
{ label: '托盘', value: 'Pallet-B' }
],
lineSignals: [
{ label: 'PCS运行', state: 'on' },
{ label: '允许接收', state: 'off' },
{ label: '自动模式', state: 'warn' },
{ label: '急停', state: 'off' }
],
handshakes: [
{ label: '启动', state: 'on' },
{ label: '准备好', state: 'on' },
{ label: '忙碌', state: 'alarm' },
{ label: '错误', state: 'alarm' },
{ label: '完成', state: 'off' }
]
},
{
name: '磨床加工单元',
status: '预制待命',
type: 'ok',
taskNo: 'TASK 1004',
startPos: '05',
endPos: '10',
taskInfo: [
{ label: '配方号', value: 'RCP-18' },
{ label: '托盘', value: 'Pallet-D' }
],
lineSignals: [
{ label: 'PCS运行', state: 'on' },
{ label: '允许接收', state: 'on' },
{ label: '自动模式', state: 'warn' },
{ label: '急停', state: 'off' }
],
handshakes: [
{ label: '启动', state: 'off' },
{ label: '准备好', state: 'on' },
{ label: '忙碌', state: 'off' },
{ label: '错误', state: 'off' },
{ label: '完成', state: 'on' }
]
}
],
equipmentList: [
{ name: 'F500', target: '1202 件', actual: '1202 件', status: '运行中', type: 'on', tags: [{ text: '上料完成', type: 'ok' }, { text: '下料完成', type: 'ok' }] },
{ name: 'L2000LSY', target: '2206 件', actual: '2206 件', status: '待机', type: 'idle', tags: [{ text: '上料完成', type: 'ok' }, { text: '下料完成', type: 'ok' }] },
{ name: 'L2100SY', target: '3301 件', actual: '3298 件', status: '运行中', type: 'on', tags: [{ text: '上料完成', type: 'ok' }, { text: '下料完成', type: 'ok' }] },
{ name: '新日', target: '4108 件', actual: '4108 件', status: '运行中', type: 'on', tags: [{ text: '上料就绪', type: 'ok' }, { text: '下料就绪', type: 'ok' }] },
{ name: 'MAZAK', target: '-', actual: '-', status: '报警', type: 'alarm', tags: [{ text: '上料就绪', type: 'ok' }, { text: '下料就绪', type: 'ok' }] },
{ name: '磨床单元', target: '860 件', actual: '856 件', status: '待机', type: 'idle', tags: [{ text: '上料就绪', type: 'ok' }, { text: '砂轮就绪', type: 'ok' }] },
{ name: 'YKD5132H', target: '5104 件', actual: '5104 件', status: '待机', type: 'idle', tags: [{ text: '上料就绪', type: 'ok' }, { text: '下料就绪', type: 'ok' }] }
],
assistList: [
{ name: '加工中心单元打标机', status: '待机', type: 'idle', tags: [{ text: '待机中', type: 'idle' }] },
{ name: '车床加工单元打标机', status: '打标中', type: 'on', tags: [{ text: '开机中', type: 'ok' }, { text: '打标中', type: 'on' }] },
{ name: '插齿机单元打标机', status: '待机', type: 'idle', tags: [{ text: '开机中', type: 'ok' }, { text: '待机中', type: 'idle' }] },
{ name: '清洗机', status: '运行中', type: 'on', tags: [{ text: '上料就绪', type: 'ok' }, { text: '运行中', type: 'on' }] },
{ name: '测量仪', status: '报警', type: 'alarm', tags: [{ text: '错误', type: 'alarm' }, { text: '下料就绪', type: 'ok' }] }
],
storageGroups: [
{
name: '加工中心单元库区',
total: 9,
filled: 6,
slots: this.createSlots(['S1', 'S2', 'S4', 'S6', 'S7', 'S9'], 9)
},
{
name: '车床加工单元库区',
total: 9,
filled: 6,
slots: this.createSlots(['S1', 'S3', 'S4', 'S6', 'S8', 'S9'], 9)
},
{
name: '插齿机单元库区',
total: 6,
filled: 4,
slots: this.createSlots(['S1', 'S3', 'S5', 'S6'], 6)
},
{
name: '磨床加工单元库区',
total: 6,
filled: 3,
slots: this.createSlots(['S2', 'S4', 'S6'], 6)
}
],
alarmEvents: [
{ time: '10:26', device: 'MAZAK', level: '严重', type: 'alarm', message: 'ALARM=1建议定位机床侧运行条件。' },
{ time: '10:22', device: '测量仪', level: '严重', type: 'alarm', message: '测量流程中断,等待复测确认。' },
{ time: '10:18', device: 'L2100SY', level: '警告', type: 'warn', message: '目标程序与实际程序不一致。' },
{ time: '10:12', device: '插齿机单元机器人', level: '警告', type: 'warn', message: '自动模式待恢复,需要人工确认。' },
{ time: '10:08', device: '清洗机', level: '警告', type: 'warn', message: '下料等待时间超过设定阈值。' },
{ time: '10:03', device: '车床加工单元', level: '提示', type: 'idle', message: '任务已接收,等待上游放行。' },
{ time: '09:58', device: '磨床加工单元', level: '提示', type: 'idle', message: '预制单元待接入,当前按模拟节拍展示。' }
],
alarmFrequencyTop5: [
{ name: 'MAZAK', value: 12 },
{ name: '测量仪', value: 9 },
{ name: '插齿机单元', value: 7 },
{ name: 'L2100SY', value: 5 },
{ name: '磨床加工单元', value: 4 }
],
historyTicks: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
historyLegend: [
{ label: '关机', type: 'off' },
{ label: '运行', type: 'run' },
{ label: '待机', type: 'standby' },
{ label: '报警', type: 'alarm' }
],
deviceHistoryList: [
{ name: '加工中心单元机器人', segments: [{ label: '待机', type: 'standby', start: 0, end: 3.4 }, { label: '运行', type: 'run', start: 3.4, end: 10.8 }, { label: '待机', type: 'standby', start: 10.8, end: 12.1 }, { label: '运行', type: 'run', start: 12.1, end: 20.4 }, { label: '关机', type: 'off', start: 20.4, end: 24 }] },
{ name: '车床加工单元机器人', segments: [{ label: '运行', type: 'run', start: 0, end: 5.2 }, { label: '待机', type: 'standby', start: 5.2, end: 8.8 }, { label: '运行', type: 'run', start: 8.8, end: 17.6 }, { label: '待机', type: 'standby', start: 17.6, end: 18.7 }, { label: '运行', type: 'run', start: 18.7, end: 24 }] },
{ name: '插齿机单元机器人', segments: [{ label: '关机', type: 'off', start: 0, end: 2.5 }, { label: '运行', type: 'run', start: 2.5, end: 9.4 }, { label: '报警', type: 'alarm', start: 9.4, end: 10.3 }, { label: '待机', type: 'standby', start: 10.3, end: 13.1 }, { label: '运行', type: 'run', start: 13.1, end: 24 }] },
{ name: '磨床加工单元', segments: [{ label: '关机', type: 'off', start: 0, end: 3.2 }, { label: '待机', type: 'standby', start: 3.2, end: 6.4 }, { label: '运行', type: 'run', start: 6.4, end: 15.8 }, { label: '待机', type: 'standby', start: 15.8, end: 17.2 }, { label: '运行', type: 'run', start: 17.2, end: 24 }] },
{ name: 'F500', segments: [{ label: '运行', type: 'run', start: 0, end: 7.8 }, { label: '待机', type: 'standby', start: 7.8, end: 9.2 }, { label: '运行', type: 'run', start: 9.2, end: 18.6 }, { label: '待机', type: 'standby', start: 18.6, end: 19.4 }, { label: '运行', type: 'run', start: 19.4, end: 24 }] },
{ name: 'L2100SY', segments: [{ label: '关机', type: 'off', start: 0, end: 4.1 }, { label: '运行', type: 'run', start: 4.1, end: 11.6 }, { label: '报警', type: 'alarm', start: 11.6, end: 12.8 }, { label: '运行', type: 'run', start: 12.8, end: 24 }] },
{ name: 'L2000LSY', segments: [{ label: '运行', type: 'run', start: 0, end: 6.6 }, { label: '待机', type: 'standby', start: 6.6, end: 7.8 }, { label: '运行', type: 'run', start: 7.8, end: 16.2 }, { label: '待机', type: 'standby', start: 16.2, end: 17.1 }, { label: '运行', type: 'run', start: 17.1, end: 24 }] },
{ name: '新日', segments: [{ label: '关机', type: 'off', start: 0, end: 3.8 }, { label: '运行', type: 'run', start: 3.8, end: 14.6 }, { label: '待机', type: 'standby', start: 14.6, end: 15.4 }, { label: '运行', type: 'run', start: 15.4, end: 24 }] },
{ name: 'MAZAK', segments: [{ label: '运行', type: 'run', start: 0, end: 8.7 }, { label: '报警', type: 'alarm', start: 8.7, end: 10.1 }, { label: '待机', type: 'standby', start: 10.1, end: 11.4 }, { label: '运行', type: 'run', start: 11.4, end: 24 }] },
{ name: 'YKD5132H', segments: [{ label: '关机', type: 'off', start: 0, end: 2.8 }, { label: '运行', type: 'run', start: 2.8, end: 12.2 }, { label: '待机', type: 'standby', start: 12.2, end: 13.5 }, { label: '运行', type: 'run', start: 13.5, end: 24 }] },
{ name: '清洗机', segments: [{ label: '关机', type: 'off', start: 0, end: 4.4 }, { label: '运行', type: 'run', start: 4.4, end: 9.7 }, { label: '待机', type: 'standby', start: 9.7, end: 13.6 }, { label: '运行', type: 'run', start: 13.6, end: 18.8 }, { label: '关机', type: 'off', start: 18.8, end: 24 }] },
{ name: '测量仪', segments: [{ label: '待机', type: 'standby', start: 0, end: 5.1 }, { label: '运行', type: 'run', start: 5.1, end: 13.3 }, { label: '待机', type: 'standby', start: 13.3, end: 14.2 }, { label: '运行', type: 'run', start: 14.2, end: 24 }] },
{ name: '加工中心单元打标机', segments: [{ label: '待机', type: 'standby', start: 0, end: 6.5 }, { label: '运行', type: 'run', start: 6.5, end: 11.2 }, { label: '待机', type: 'standby', start: 11.2, end: 15.8 }, { label: '运行', type: 'run', start: 15.8, end: 24 }] },
{ name: '车床加工单元打标机', segments: [{ label: '待机', type: 'standby', start: 0, end: 4.9 }, { label: '运行', type: 'run', start: 4.9, end: 12.6 }, { label: '待机', type: 'standby', start: 12.6, end: 13.4 }, { label: '运行', type: 'run', start: 13.4, end: 24 }] },
{ name: '插齿机单元打标机', segments: [{ label: '待机', type: 'standby', start: 0, end: 7.1 }, { label: '运行', type: 'run', start: 7.1, end: 10.9 }, { label: '待机', type: 'standby', start: 10.9, end: 16.5 }, { label: '运行', type: 'run', start: 16.5, end: 24 }] }
]
}
},
computed: {
lineMonitorScrollList() {
return [...this.lineMonitorRows, ...this.lineMonitorRows]
},
equipmentScrollList() {
return [...this.equipmentList, ...this.equipmentList]
},
alarmScrollList() {
return [...this.alarmEvents, ...this.alarmEvents]
},
deviceHistoryScrollList() {
return [...this.deviceHistoryList, ...this.deviceHistoryList]
},
lineMonitorScrollOption() {
return {
step: 0.38,
limitMoveNum: 3,
hoverStop: true,
direction: 1,
openTouch: false,
singleHeight: 168,
waitTime: 1200,
autoPlay: true
}
},
equipmentScrollOption() {
return {
step: 0.45,
limitMoveNum: 4,
hoverStop: true,
direction: 1,
openTouch: false,
singleHeight: 44,
waitTime: 1200,
autoPlay: true
}
},
alarmScrollOption() {
return {
step: 0.4,
limitMoveNum: 4,
hoverStop: true,
direction: 1,
openTouch: false,
singleHeight: 36,
waitTime: 1200,
autoPlay: true
}
},
historyScrollOption() {
return {
step: 0.45,
limitMoveNum: 5,
hoverStop: true,
direction: 1,
openTouch: false,
singleHeight: 32,
waitTime: 1000,
autoPlay: true
}
}
},
mounted() {
const config = window.dt_Config || {}
this.iframeAddress = config.iframeAddress ? config.iframeAddress : ''
this.autoPlayCraftCode = config.autoPlayCraftCode || 'E8077755-9B85-46A1-9B8A-85083C00DF26'
this.srcDt = this.buildIframeUrl(config.dtIframe || this.srcDt, this.autoPlayCraftCode)
this.srcSm = this.buildIframeUrl(config.smIframe || this.srcSm, this.autoPlayCraftCode)
this.titleText = config.titleText
this.updateTime()
this.setScale()
this.$nextTick(this.initAlarmPieChart)
this.clockTimer = setInterval(this.updateTime, 1000)
this.resizeHandler = () => {
this.setScale()
if (this.alarmPieChart) {
this.alarmPieChart.resize()
}
}
window.addEventListener('resize', this.resizeHandler)
window.addEventListener('message', this.handleDmtMessage)
},
beforeDestroy() {
if (this.clockTimer) {
clearInterval(this.clockTimer)
this.clockTimer = null
}
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler)
this.resizeHandler = null
}
window.removeEventListener('message', this.handleDmtMessage)
if (this.modeSwitchTimer) {
clearTimeout(this.modeSwitchTimer)
this.modeSwitchTimer = null
}
if (this.cameraSyncTimer) {
clearTimeout(this.cameraSyncTimer)
this.cameraSyncTimer = null
}
if (this.iframeReadyTimer) {
clearTimeout(this.iframeReadyTimer)
this.iframeReadyTimer = null
}
if (this.alarmPieChart) {
this.alarmPieChart.dispose()
this.alarmPieChart = null
}
},
methods: {
createSlots(filledNames, total) {
return Array.from({ length: total }, (_, index) => {
const name = `S${index + 1}`
return {
name,
filled: filledNames.includes(name)
}
})
},
getHistorySegmentStyle(segment) {
const start = Math.max(0, Math.min(24, segment.start))
const end = Math.max(start, Math.min(24, segment.end))
return {
left: `${(start / 24) * 100}%`,
width: `${((end - start) / 24) * 100}%`
}
},
changeView(value) {
this.activeView = value
if (this.dmtIframeReady) {
this.syncActiveCamera()
}
},
buildIframeUrl(baseUrl, craftCode) {
if (!baseUrl) {
return ''
}
let nextUrl = baseUrl
if (craftCode && nextUrl.includes('autoPlayCraftCode=') && !nextUrl.includes(craftCode)) {
const separator = nextUrl.endsWith('=') || nextUrl.endsWith('/') ? '' : '/'
nextUrl = `${nextUrl}${separator}${craftCode}`
}
return this.withTransparentParam(nextUrl)
},
withTransparentParam(url) {
if (!url || url.includes('transparent=true')) {
return url
}
const hashIndex = url.indexOf('#')
if (hashIndex > -1) {
const separator = url.slice(hashIndex).includes('?') ? '&' : '?'
return `${url}${separator}transparent=true`
}
return `${url}${url.endsWith('/') ? '' : '/'}#?transparent=true`
},
handleIframeLoad() {
this.dmtIframeReady = false
if (this.iframeReadyTimer) {
clearTimeout(this.iframeReadyTimer)
this.iframeReadyTimer = null
}
},
handleDmtMessage(event) {
if (!event || !event.data || event.data.type !== 'dmt_load_finish') {
return
}
const iframe = document.getElementById('dmtIframe')
if (!iframe || event.source !== iframe.contentWindow) {
return
}
this.markDmtIframeReady()
},
markDmtIframeReady() {
this.dmtIframeReady = true
if (this.iframeReadyTimer) {
clearTimeout(this.iframeReadyTimer)
this.iframeReadyTimer = null
}
this.syncActiveCamera()
},
syncActiveCamera() {
const camera = this.cameraViews[this.activeView]
if (!camera) return
if (!this.dmtIframeReady) return
if (this.cameraSyncTimer) {
clearTimeout(this.cameraSyncTimer)
}
this.cameraSyncTimer = setTimeout(() => {
const iframe = document.getElementById('dmtIframe')
if (!iframe || !iframe.contentWindow) return
iframe.contentWindow.postMessage({
type: 'set_camera',
data: {
target: camera.target,
position: camera.position,
zoom0: camera.zoom0
}
}, '*')
this.cameraSyncTimer = null
}, 300)
},
switchMode(mode) {
this.currentMode = mode
this.mode = ''
this.dmtIframeReady = false
if (this.iframeReadyTimer) {
clearTimeout(this.iframeReadyTimer)
this.iframeReadyTimer = null
}
if (this.modeSwitchTimer) {
clearTimeout(this.modeSwitchTimer)
}
this.modeSwitchTimer = setTimeout(() => {
this.mode = mode
this.modeSwitchTimer = null
}, 600)
},
changeDtMode() {
this.switchMode('DT')
},
changeSmMode() {
this.switchMode('SM')
},
initAlarmPieChart() {
if (!this.$refs.alarmPieChart) {
return
}
if (this.alarmPieChart) {
this.alarmPieChart.dispose()
}
this.alarmPieChart = echarts.init(this.$refs.alarmPieChart)
const names = this.alarmFrequencyTop5.map(item => item.name)
const values = this.alarmFrequencyTop5.map(item => item.value)
const maxValue = Math.max(...values, 1)
const yAxisMax = Math.max(2, Math.ceil(maxValue / 2) * 2)
this.alarmPieChart.setOption({
grid: {
left: 34,
right: 12,
top: 22,
bottom: 24
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(114, 206, 255, 0.08)'
}
},
formatter(params) {
const item = params && params[0]
return item ? `${item.name}<br/>报警频次:${item.value}` : ''
}
},
xAxis: {
type: 'category',
data: names,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: 'rgba(114, 206, 255, 0.28)'
}
},
axisLabel: {
color: '#b8d3e6',
fontSize: 10,
interval: 0
}
},
yAxis: {
type: 'value',
min: 0,
max: yAxisMax,
interval: yAxisMax / 2,
splitNumber: 2,
minInterval: 1,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#8fb8d3',
fontSize: 10,
formatter: value => `${Math.round(value)}`
},
splitLine: {
lineStyle: {
color: 'rgba(114, 206, 255, 0.12)',
type: 'dashed'
}
}
},
series: [
{
name: '报警频次',
type: 'bar',
data: values,
barWidth: 18,
label: {
show: true,
position: 'top',
color: '#e8f7ff',
fontSize: 10,
formatter: '{c}'
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#8df6ff' },
{ offset: 0.42, color: '#36b8ff' },
{ offset: 1, color: '#135cae' }
]),
borderRadius: 0,
shadowBlur: 10,
shadowColor: 'rgba(80, 237, 255, 0.24)'
}
}
]
})
},
setScale() {
const scaleX = window.innerWidth / this.style.width
const scaleY = window.innerHeight / this.style.height
this.style.transform = `scaleY(${scaleY}) scaleX(${scaleX}) translate(-50%, -50%)`
},
updateTime() {
const now = new Date()
const pad = value => String(value).padStart(2, '0')
this.headerDateCn = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
this.headerTime = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
}
}
</script>
<style>
html,
body,
#app {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #050b14;
}
</style>
<style scoped>
.qa-screen {
--scada-panel: rgba(8, 20, 34, 0.84);
--scada-panel-deep: rgba(3, 10, 20, 0.92);
--scada-border: rgba(91, 213, 255, 0.34);
--scada-border-strong: rgba(128, 234, 255, 0.68);
--scada-cyan: #72e8ff;
--scada-blue: #2c8fff;
--scada-green: #24d88b;
--scada-yellow: #f5c84c;
--scada-red: #ff5266;
--scada-text: #f1f8ff;
--scada-muted: #91b5cd;
position: fixed;
left: 50%;
top: 50%;
transform-origin: 0 0;
overflow: hidden;
color: var(--scada-text);
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
background: #06101f url("/png/背景底图.png") no-repeat center center;
background-size: cover;
background-color: #050b14;
box-shadow: inset 0 0 140px rgba(0, 0, 0, 0.72);
}
.qa-screen::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 25% 22%, rgba(42, 151, 255, 0.18), transparent 28%),
radial-gradient(circle at 74% 26%, rgba(39, 217, 128, 0.08), transparent 26%),
linear-gradient(180deg, rgba(2, 9, 22, 0.35), rgba(3, 13, 31, 0.66));
pointer-events: none;
}
.qa-screen::after {
content: "";
position: absolute;
inset: 0;
z-index: 0;
background:
radial-gradient(circle at 50% 48%, rgba(44, 143, 255, 0.14), transparent 34%);
opacity: 0.42;
pointer-events: none;
}
.title-png {
position: absolute;
top: 0;
left: 0;
z-index: 40;
width: 100%;
height: 100px;
background-image: url("/png/top1.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
display: flex;
justify-content: center;
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.32);
}
.view-switch {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
pointer-events: auto;
}
.view-switch__btn {
height: 30px;
padding: 0;
color: #b9d8ec;
border: 1px solid rgba(111, 224, 255, 0.34);
background:
linear-gradient(180deg, rgba(24, 52, 82, 0.82), rgba(5, 17, 30, 0.9)),
rgba(6, 25, 53, 0.72);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.12),
inset 0 -10px 18px rgba(0, 0, 0, 0.18),
0 6px 14px rgba(0, 0, 0, 0.28);
font-family: inherit;
font-size: 11px;
font-weight: 700;
line-height: 28px;
white-space: nowrap;
cursor: pointer;
}
.view-switch__btn--active {
color: #ffffff;
border-color: rgba(127, 239, 255, 0.95);
background:
linear-gradient(180deg, rgba(82, 207, 255, 0.74), rgba(16, 89, 142, 0.92)),
rgba(13, 70, 118, 0.78);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.34),
inset 0 0 14px rgba(101, 221, 255, 0.34),
0 0 16px rgba(49, 166, 255, 0.46);
}
.view-switch__btn:focus {
outline: none;
}
.mode-button {
position: relative;
flex: 1;
height: 30px;
padding: 0;
color: #e9fbff;
border: 1px solid rgba(75, 245, 209, 0.48);
background:
linear-gradient(90deg, rgba(8, 41, 57, 0.92), rgba(10, 67, 87, 0.78), rgba(8, 41, 57, 0.92)),
rgba(4, 20, 35, 0.78);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
inset 0 0 14px rgba(75, 245, 209, 0.12),
0 0 14px rgba(75, 245, 209, 0.16);
font-size: 13px;
font-weight: 700;
letter-spacing: 0;
cursor: pointer;
pointer-events: auto;
}
.mode-button::before,
.mode-button::after {
content: "";
position: absolute;
top: 6px;
width: 7px;
height: 16px;
border-top: 1px solid rgba(128, 255, 230, 0.74);
border-bottom: 1px solid rgba(128, 255, 230, 0.48);
pointer-events: none;
}
.mode-button::before {
left: 6px;
border-left: 1px solid rgba(128, 255, 230, 0.74);
}
.mode-button::after {
right: 6px;
border-right: 1px solid rgba(128, 255, 230, 0.74);
}
.mode-button:hover {
color: #ffffff;
border-color: rgba(128, 255, 230, 0.82);
background:
linear-gradient(90deg, rgba(12, 55, 73, 0.94), rgba(15, 91, 113, 0.84), rgba(12, 55, 73, 0.94)),
rgba(4, 20, 35, 0.82);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
inset 0 0 18px rgba(75, 245, 209, 0.18),
0 0 18px rgba(75, 245, 209, 0.22);
}
.mode-button--active {
color: #ffffff;
border-color: rgba(128, 255, 230, 0.95);
background:
linear-gradient(90deg, rgba(12, 62, 78, 0.96), rgba(26, 122, 132, 0.88), rgba(12, 62, 78, 0.96)),
rgba(4, 20, 35, 0.86);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.26),
inset 0 0 20px rgba(75, 245, 209, 0.24),
0 0 20px rgba(75, 245, 209, 0.28);
}
.mode-button:focus {
outline: none;
}
.brand-logo {
display: block;
width: 300px;
height: 50px;
box-sizing: border-box;
padding: 6px 12px;
object-fit: contain;
object-position: left center;
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.72);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(231, 242, 248, 0.9)),
rgba(255, 255, 255, 0.92);
box-shadow:
inset 0 0 0 1px rgba(114, 206, 255, 0.14),
0 0 16px rgba(114, 232, 255, 0.18),
0 8px 20px rgba(0, 0, 0, 0.18);
}
.mode-button-group {
display: flex;
width: 300px;
height: 30px;
gap: 10px;
}
.title-text {
margin-right: 37px;
color: #fff;
font-size: 37px;
font-weight: 800;
line-height: 70px;
text-align: center;
text-shadow:
0 0 10px rgba(114, 232, 255, 0.56),
0 0 26px rgba(44, 143, 255, 0.42),
0 3px 0 rgba(0, 0, 0, 0.34);
}
.df-header__timebar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: #ffffff;
height: 26px;
padding: 0 8px;
box-sizing: border-box;
}
.df-header__year {
font-size: 13px;
opacity: 0.9;
}
.df-header__time {
font-size: 18px;
font-weight: 700;
color: #dffcff;
text-shadow: 0 0 12px rgba(114, 232, 255, 0.5);
}
.leftLine,
.rightLine {
position: absolute;
top: 50px;
z-index: 30;
width: 54px;
height: 1020px;
background-size: 100% 100%;
background-position: center;
pointer-events: none;
}
.leftLine {
left: 10px;
background-image: url("/png/左边条.png");
}
.rightLine {
right: 10px;
background-image: url("/png/右边条.png");
}
.main-content {
position: absolute;
z-index: 2;
left: 64px;
right: 64px;
top: 100px;
bottom: 26px;
}
.left-control-bar,
.right-control-bar {
position: absolute;
width: 520px;
z-index: 20;
pointer-events: auto;
}
.left-control-bar {
top: 18px;
left: 80px;
display: flex;
flex-direction: column;
align-items: flex-start;
height: 66px;
gap: 7px;
}
.right-control-bar {
top: 24px;
right: 80px;
height: 58px;
display: grid;
grid-template-rows: 24px 30px;
justify-content: end;
gap: 4px;
box-sizing: border-box;
}
.right-control-bar .df-header__timebar {
width: 236px;
justify-self: end;
}
.right-control-bar .view-switch {
width: 520px;
justify-self: end;
}
.model-bg-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
background: transparent;
z-index: 1;
}
.model-placeholder {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #77d9ff;
font-size: 24px;
background: radial-gradient(circle at center, rgba(37, 111, 184, 0.24), rgba(4, 12, 28, 0.92));
}
.left-box {
position: absolute;
top: 0;
left: 0;
width: 360px;
bottom: 260px;
z-index: 10;
overflow: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.left-box::-webkit-scrollbar,
.line-monitor-body::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.right-box {
position: absolute;
top: 0;
right: 0;
width: 360px;
bottom: 260px;
z-index: 10;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 12px;
}
.bottom-box {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 248px;
z-index: 10;
display: grid;
grid-template-columns: 1.2fr 1fr 1fr;
gap: 12px;
}
.left-box .qa-panel {
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.line-monitor-body {
padding: 8px 12px;
overflow: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.line-monitor-scroll-box {
width: 100%;
height: 100%;
overflow: hidden;
}
.line-monitor-scroll,
.line-monitor-content {
min-height: 100%;
overflow: hidden;
}
.line-monitor-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.right-box .qa-panel:first-child {
flex: 1;
min-height: 0;
}
.right-box .qa-panel:last-child {
flex-shrink: 0;
height: 236px;
}
.right-box .qa-scroll {
overflow: hidden;
scrollbar-width: none;
}
.right-box .qa-scroll::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.qa-panel {
position: relative;
min-height: 0;
overflow: hidden;
border: 1px solid var(--scada-border);
background:
linear-gradient(180deg, rgba(18, 44, 68, 0.72) 0%, rgba(5, 15, 29, 0.9) 100%),
var(--scada-panel);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 0 28px rgba(44, 143, 255, 0.18),
0 14px 30px rgba(0, 0, 0, 0.38),
0 0 18px rgba(44, 143, 255, 0.08);
}
.qa-panel::before,
.qa-panel::after {
content: "";
position: absolute;
z-index: 1;
width: 28px;
height: 28px;
pointer-events: none;
}
.qa-panel::before {
top: 0;
left: 0;
border-top: 2px solid rgba(128, 234, 255, 0.82);
border-left: 2px solid rgba(128, 234, 255, 0.72);
box-shadow: -1px -1px 10px rgba(114, 232, 255, 0.2);
}
.qa-panel::after {
right: 0;
bottom: 0;
border-right: 2px solid rgba(128, 234, 255, 0.58);
border-bottom: 2px solid rgba(128, 234, 255, 0.5);
box-shadow: 1px 1px 10px rgba(114, 232, 255, 0.14);
}
.qa-panel-body {
height: calc(100% - 42px);
padding: 10px 12px 12px;
box-sizing: border-box;
}
.qa-panel-body--storage {
padding: 8px 10px 10px;
display: flex;
min-height: 0;
}
.qa-scroll {
overflow: auto;
}
.qa-scroll::-webkit-scrollbar {
width: 6px;
}
.qa-scroll::-webkit-scrollbar-thumb {
background: rgba(111, 212, 255, 0.22);
}
.left-title-header,
.right-title-header,
.center-title-header {
height: 42px;
display: flex;
align-items: center;
padding: 0 18px;
box-sizing: border-box;
position: relative;
z-index: 2;
}
.left-title-header {
justify-content: flex-start;
padding-left: 18px;
background:
linear-gradient(90deg, rgba(25, 92, 145, 0.5), rgba(8, 24, 46, 0.12)),
linear-gradient(180deg, rgba(255, 255, 255, 0.07), transparent);
}
.right-title-header {
justify-content: flex-start;
padding-left: 18px;
padding-right: 18px;
background:
linear-gradient(90deg, rgba(25, 92, 145, 0.5), rgba(8, 24, 46, 0.12)),
linear-gradient(180deg, rgba(255, 255, 255, 0.07), transparent);
}
.center-title-header {
border-bottom: 1px solid rgba(114, 206, 255, 0.22);
background:
linear-gradient(90deg, rgba(25, 92, 145, 0.5), rgba(8, 24, 46, 0.12)),
linear-gradient(180deg, rgba(255, 255, 255, 0.07), transparent);
}
.left-title-text,
.right-title-text,
.center-title-header span {
color: #ffffff;
font-size: 17px;
font-weight: 800;
text-shadow: 0 0 12px rgba(114, 232, 255, 0.34);
}
.center-title-header::after,
.left-title-header::after,
.right-title-header::after {
content: "";
position: absolute;
left: 16px;
right: 16px;
bottom: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(114, 232, 255, 0.46), transparent);
pointer-events: none;
}
.right-title-text {
text-align: left;
}
.status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 18px;
padding: 2px 9px;
border-radius: 0;
color: #b8ccdc;
border: 1px solid rgba(255, 255, 255, 0.1);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.03)),
rgba(40, 55, 72, 0.42);
font-size: 12px;
white-space: nowrap;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 0 10px rgba(255, 255, 255, 0.04);
text-shadow: none;
}
.status-pill--compact {
padding: 2px 7px;
font-size: 11px;
}
.status-pill--ok {
color: #d9ffea;
border-color: rgba(36, 216, 139, 0.38);
background:
linear-gradient(180deg, rgba(36, 216, 139, 0.34), rgba(11, 73, 49, 0.56)),
rgba(14, 92, 61, 0.45);
box-shadow: 0 0 10px rgba(36, 216, 139, 0.18);
}
.status-pill--on {
color: #e0fbff;
border-color: rgba(114, 232, 255, 0.42);
background:
linear-gradient(180deg, rgba(44, 143, 255, 0.4), rgba(14, 58, 98, 0.58)),
rgba(14, 58, 98, 0.48);
box-shadow: 0 0 14px rgba(44, 143, 255, 0.2);
}
.status-pill--warn {
color: #fff3bf;
border-color: rgba(245, 200, 76, 0.42);
background:
linear-gradient(180deg, rgba(245, 200, 76, 0.38), rgba(101, 73, 14, 0.58)),
rgba(101, 73, 14, 0.44);
box-shadow: 0 0 14px rgba(245, 200, 76, 0.2);
}
.status-pill--alarm {
color: #ffe3e6;
border-color: rgba(255, 82, 102, 0.5);
background:
linear-gradient(180deg, rgba(255, 82, 102, 0.42), rgba(101, 18, 32, 0.62)),
rgba(101, 18, 32, 0.48);
box-shadow: 0 0 12px rgba(255, 82, 102, 0.22);
}
.status-pill--idle {
color: #c5d4df;
border-color: rgba(160, 180, 194, 0.24);
background:
linear-gradient(180deg, rgba(100, 121, 139, 0.28), rgba(36, 48, 60, 0.54)),
rgba(36, 48, 60, 0.42);
}
.line-status {
flex: 0 0 auto;
min-width: 74px;
padding: 4px 10px;
border-radius: 0;
color: #f3fbff;
font-size: 13px;
font-weight: 700;
text-align: center;
letter-spacing: 0;
border: 1px solid rgba(255, 255, 255, 0.12);
text-shadow: none;
}
.line-status--ok {
color: #dfffee;
border-color: rgba(36, 216, 139, 0.52);
background:
linear-gradient(180deg, rgba(36, 216, 139, 0.44), rgba(14, 92, 61, 0.68)),
rgba(14, 92, 61, 0.45);
box-shadow: 0 0 18px rgba(36, 216, 139, 0.26);
}
.line-status--on {
color: #e7f9ff;
border-color: rgba(114, 232, 255, 0.52);
background:
linear-gradient(180deg, rgba(44, 143, 255, 0.5), rgba(16, 79, 126, 0.72)),
rgba(16, 79, 126, 0.48);
box-shadow: 0 0 18px rgba(44, 143, 255, 0.3);
}
.line-status--alarm {
color: #fff2f4;
border-color: rgba(255, 82, 102, 0.58);
background:
linear-gradient(180deg, rgba(255, 82, 102, 0.52), rgba(118, 22, 38, 0.72)),
rgba(118, 22, 38, 0.5);
box-shadow: 0 0 18px rgba(255, 82, 102, 0.34);
}
.monitor-card,
.equipment-row,
.storage-card,
.event-row {
position: relative;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(20, 42, 62, 0.62), rgba(4, 14, 27, 0.74)),
rgba(5, 20, 44, 0.66);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
inset 0 0 18px rgba(42, 133, 205, 0.11);
}
.monitor-card {
flex: 0 0 168px;
min-height: 168px;
margin-bottom: 0;
padding: 8px 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.monitor-card::before,
.equipment-row::before,
.event-row::before,
.history-row::before,
.assist-status-row::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(180deg, transparent, rgba(114, 232, 255, 0.42), transparent);
opacity: 0.7;
pointer-events: none;
}
.monitor-head,
.assist-title,
.storage-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.monitor-title strong {
display: block;
color: #fff;
font-size: 18px;
text-shadow: 0 0 10px rgba(114, 232, 255, 0.28);
}
.monitor-title span {
display: block;
margin-top: 2px;
color: var(--scada-cyan);
font-size: 12px;
font-weight: 700;
}
.task-summary {
margin-top: 5px;
padding: 6px 8px;
border: 1px solid rgba(114, 206, 255, 0.08);
background:
linear-gradient(90deg, rgba(44, 143, 255, 0.1), transparent 55%),
rgba(0, 0, 0, 0.26);
display: grid;
grid-template-columns: 1.2fr 1fr 1fr;
gap: 6px;
}
.task-route,
.task-field {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.task-route span,
.task-field span,
.equipment-program span,
.storage-head span {
color: #9fc3dc;
font-size: 11px;
}
.task-route strong,
.task-field strong {
color: #fff;
font-size: 13px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.signal-section {
margin-top: 6px;
display: grid;
grid-template-columns: 50px minmax(0, 1fr);
align-items: center;
gap: 6px;
}
.signal-section__title {
color: #9fc3dc;
font-size: 11px;
}
.signal-grid {
min-width: 0;
display: grid;
gap: 5px;
}
.signal-grid--line {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.signal-grid--handshake {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.signal-chip {
min-width: 0;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
color: #b8ccdc;
border: 1px solid rgba(160, 180, 194, 0.16);
background: rgba(36, 48, 60, 0.38);
font-size: 10px;
line-height: 1;
white-space: nowrap;
overflow: hidden;
}
.signal-chip i {
flex: 0 0 auto;
width: 6px;
height: 6px;
border-radius: 50%;
background: #5f7182;
}
.signal-chip span {
overflow: hidden;
text-overflow: ellipsis;
}
.signal-chip--on {
color: #d9ffea;
border-color: rgba(36, 216, 139, 0.34);
background: rgba(12, 80, 54, 0.52);
}
.signal-chip--on i {
background: var(--scada-green);
box-shadow: 0 0 8px rgba(36, 216, 139, 0.64);
}
.signal-chip--warn {
color: #fff3bf;
border-color: rgba(245, 200, 76, 0.36);
background: rgba(101, 73, 14, 0.48);
}
.signal-chip--warn i {
background: var(--scada-yellow);
box-shadow: 0 0 8px rgba(245, 200, 76, 0.58);
}
.signal-chip--alarm {
color: #ffe3e6;
border-color: rgba(255, 82, 102, 0.44);
background: rgba(101, 18, 32, 0.52);
}
.signal-chip--alarm i {
background: var(--scada-red);
box-shadow: 0 0 8px rgba(255, 82, 102, 0.62);
}
.signal-chip--off {
color: #9fb0bd;
}
.signal-chip--off i {
background: #5f7182;
}
.dot {
flex: 0 0 auto;
width: 10px;
height: 10px;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.04);
}
.dot--green { background: var(--scada-green); box-shadow: 0 0 0 4px rgba(36, 216, 139, 0.08), 0 0 10px rgba(36, 216, 139, 0.5); }
.dot--blue { background: var(--scada-cyan); box-shadow: 0 0 0 4px rgba(114, 232, 255, 0.08), 0 0 10px rgba(114, 232, 255, 0.46); }
.dot--yellow { background: var(--scada-yellow); box-shadow: 0 0 0 4px rgba(245, 200, 76, 0.08), 0 0 10px rgba(245, 200, 76, 0.45); }
.dot--red { background: var(--scada-red); box-shadow: 0 0 0 4px rgba(255, 82, 102, 0.08), 0 0 10px rgba(255, 82, 102, 0.48); }
.dot--gray { background: #5f7182; }
.monitor-stage {
border-left: 3px solid rgba(53, 178, 255, 0.8);
}
.monitor-stage--ok {
border-left-color: #2be085;
}
.monitor-stage--alarm {
border-left-color: #ff5d6c;
background: rgba(64, 8, 20, 0.38);
}
.equipment-body,
.event-body,
.history-body {
display: flex;
flex-direction: column;
overflow: hidden;
}
.alarm-bar-chart {
flex-shrink: 0;
height: 98px;
margin: -2px 0 5px;
border: 1px solid rgba(114, 206, 255, 0.12);
background:
linear-gradient(180deg, rgba(24, 66, 104, 0.28), rgba(7, 20, 34, 0.12)),
rgba(0, 0, 0, 0.18);
}
.seamless-box {
flex: 1;
min-height: 0;
overflow: hidden;
}
.seamless-scroll,
.seamless-content {
min-height: 100%;
overflow: hidden;
}
.equipment-head,
.equipment-row {
display: grid;
grid-template-columns: 68px 56px minmax(104px, 1fr) 50px;
align-items: center;
gap: 8px;
}
.equipment-head {
flex-shrink: 0;
min-height: 28px;
margin-bottom: 6px;
padding: 0 8px;
color: #a9cce0;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(38, 83, 122, 0.42), rgba(12, 34, 56, 0.42)),
rgba(21, 58, 94, 0.34);
font-size: 12px;
}
.equipment-row {
min-height: 44px;
margin-bottom: 6px;
padding: 8px;
}
.equipment-row--alarm {
border-color: rgba(255, 82, 102, 0.52);
background:
linear-gradient(90deg, rgba(255, 82, 102, 0.22), rgba(64, 8, 20, 0.44)),
rgba(64, 8, 20, 0.48);
}
.equipment-name {
min-width: 0;
color: #fff;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.equipment-program {
display: flex;
flex-direction: column;
}
.equipment-count {
color: #fff;
font-size: 13px;
font-weight: 700;
white-space: nowrap;
}
.equipment-program b {
margin-top: 4px;
color: #fff;
font-size: 13px;
}
.equipment-tags,
.assist-flow {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
overflow: hidden;
}
.equipment-status {
justify-self: end;
min-width: 40px;
}
.assist-body {
display: flex;
flex-direction: column;
padding: 8px 12px 10px;
overflow: hidden;
}
.assist-head,
.assist-status-row {
display: grid;
grid-template-columns: 118px minmax(108px, 1fr) 50px;
align-items: center;
gap: 8px;
}
.assist-head {
flex-shrink: 0;
min-height: 26px;
margin-bottom: 5px;
padding: 0 8px;
color: #a9cce0;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(38, 83, 122, 0.42), rgba(12, 34, 56, 0.42)),
rgba(21, 58, 94, 0.34);
font-size: 12px;
}
.assist-list {
flex: 1;
min-height: 0;
display: grid;
grid-template-rows: repeat(5, minmax(0, 1fr));
gap: 4px;
}
.assist-status-row {
position: relative;
min-height: 0;
padding: 0 8px;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(20, 42, 62, 0.58), rgba(4, 14, 27, 0.72)),
rgba(5, 20, 44, 0.66);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
inset 0 0 14px rgba(42, 133, 205, 0.12);
}
.assist-name {
min-width: 0;
color: #fff;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.assist-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
overflow: hidden;
}
.assist-status {
justify-self: end;
min-width: 40px;
}
.storage-groups {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
align-items: stretch;
width: 100%;
height: 100%;
}
.storage-card {
min-width: 0;
padding: 9px;
display: flex;
flex-direction: column;
}
.storage-head {
margin-bottom: 8px;
flex-shrink: 0;
}
.slots {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: minmax(0, 1fr);
gap: 6px;
}
.slot {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 30px;
color: rgba(184, 202, 214, 0.72);
border: 1px solid rgba(127, 154, 174, 0.24);
background:
linear-gradient(180deg, rgba(84, 99, 114, 0.24), rgba(28, 39, 51, 0.42)),
rgba(56, 68, 82, 0.22);
box-shadow:
inset 0 0 10px rgba(120, 145, 160, 0.08),
inset 0 0 0 1px rgba(255, 255, 255, 0.03);
font-size: 12px;
}
.slot::before {
content: '';
position: absolute;
left: 5px;
top: 5px;
width: 5px;
height: 5px;
border-radius: 50%;
background: rgba(91, 109, 129, 0.75);
box-shadow: 0 0 6px rgba(91, 109, 129, 0.2);
}
.slot--filled {
color: #f1fcff;
font-weight: 700;
border-color: rgba(36, 216, 139, 0.68);
background:
linear-gradient(180deg, rgba(44, 143, 255, 0.34), rgba(36, 216, 139, 0.38)),
rgba(8, 46, 50, 0.38);
box-shadow:
inset 0 0 0 1px rgba(126, 233, 255, 0.28),
inset 0 0 16px rgba(36, 216, 139, 0.22),
0 0 12px rgba(36, 216, 139, 0.18);
}
.slot--filled::before {
background: var(--scada-green);
box-shadow: 0 0 10px rgba(36, 216, 139, 0.72);
}
.event-head,
.event-row {
display: grid;
grid-template-columns: 52px 90px 1fr;
align-items: center;
gap: 8px;
box-sizing: border-box;
}
.event-head {
flex-shrink: 0;
height: 22px;
min-height: 22px;
margin-bottom: 3px;
padding: 0 10px;
color: #a9cce0;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(38, 83, 122, 0.42), rgba(12, 34, 56, 0.42)),
rgba(21, 58, 94, 0.34);
font-size: 12px;
}
.event-row {
min-height: 30px;
margin-bottom: 3px;
padding: 5px 10px;
font-size: 12px;
}
.event-row p,
.event-row strong {
margin: 0;
color: #d5edf8;
line-height: 1.35;
}
.event-row p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.25;
}
.event-row > span:first-child {
color: var(--scada-cyan);
font-weight: 700;
text-shadow: 0 0 8px rgba(114, 232, 255, 0.24);
}
.history-legend {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
gap: 12px;
height: 18px;
margin-bottom: 3px;
padding: 0 10px;
color: #b8d3e6;
font-size: 12px;
}
.history-legend-item {
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.history-legend-dot {
width: 10px;
height: 10px;
border-radius: 2px;
}
.history-legend-dot--run {
background: rgba(36, 216, 139, 0.9);
box-shadow: 0 0 8px rgba(36, 216, 139, 0.42);
}
.history-legend-dot--standby {
background: rgba(243, 198, 78, 0.9);
box-shadow: 0 0 8px rgba(243, 198, 78, 0.36);
}
.history-legend-dot--off {
background: rgba(92, 111, 130, 0.9);
}
.history-legend-dot--alarm {
background: rgba(255, 82, 102, 0.9);
box-shadow: 0 0 8px rgba(255, 82, 102, 0.38);
}
.history-head,
.history-row {
display: grid;
grid-template-columns: 132px 1fr;
align-items: center;
gap: 10px;
box-sizing: border-box;
}
.history-head {
flex-shrink: 0;
height: 22px;
margin-bottom: 3px;
padding: 0 10px;
color: #a9cce0;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(38, 83, 122, 0.42), rgba(12, 34, 56, 0.42)),
rgba(21, 58, 94, 0.34);
font-size: 12px;
}
.history-row {
min-height: 30px;
margin-bottom: 3px;
padding: 4px 10px;
border: 1px solid rgba(114, 206, 255, 0.18);
background:
linear-gradient(180deg, rgba(20, 42, 62, 0.56), rgba(4, 14, 27, 0.72)),
rgba(5, 20, 44, 0.66);
box-shadow: inset 0 0 14px rgba(42, 133, 205, 0.12);
font-size: 12px;
}
.history-row::before {
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(180deg, transparent, rgba(114, 232, 255, 0.42), transparent);
opacity: 0.7;
}
.history-device {
min-width: 0;
}
.history-device strong {
color: #fff;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-scale {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
color: #8fb8d3;
font-size: 11px;
}
.history-scale span {
white-space: nowrap;
}
.history-track {
position: relative;
height: 16px;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(114, 206, 255, 0.13);
background:
repeating-linear-gradient(
to right,
rgba(114, 206, 255, 0.26) 0,
rgba(114, 206, 255, 0.26) 1px,
transparent 1px,
transparent calc(100% / 6)
),
rgba(0, 0, 0, 0.2);
}
.history-segment {
position: absolute;
top: 2px;
bottom: 2px;
min-width: 4px;
overflow: hidden;
}
.history-segment--run {
color: #eafff2;
background: rgba(36, 216, 139, 0.72);
}
.history-segment--standby {
color: #fff2b0;
background: rgba(243, 198, 78, 0.58);
}
.history-segment--off {
color: #b5c8d5;
background: rgba(92, 111, 130, 0.5);
}
.history-segment--alarm {
color: #fff0f1;
background: rgba(255, 82, 102, 0.72);
}
</style>