diff --git a/src/views/qingAn.vue b/src/views/qingAn.vue index 63c33d1..a140b36 100644 --- a/src/views/qingAn.vue +++ b/src/views/qingAn.vue @@ -3,11 +3,13 @@
@@ -64,25 +66,33 @@
-
+
-
等待配置三维模型地址
+
等待配置三维模型地址
-
+
线体任务联动状态
-
+
-
+
{{ row.name }} {{ row.taskNo }} + 预留数据
{{ row.status }}
@@ -121,11 +131,12 @@
+
未查询到数据
-
+
加工设备状态 @@ -136,7 +147,7 @@ 联动信号 状态
-
+
- {{ item.name }} + + {{ item.name }} + 预留数据 +
{{ tag.text }} @@ -156,6 +170,7 @@
+
未查询到数据
@@ -169,7 +184,7 @@ 联动信号 状态
-
+
{{ item.name }}
@@ -180,20 +195,24 @@ {{ item.status }}
+
未查询到数据
-
+
物料库位矩阵
-
+
- {{ group.name }} + + {{ group.name }} + 预留数据 + {{ group.filled }} / {{ group.total }}
@@ -203,6 +222,7 @@
+
未查询到数据
@@ -211,18 +231,27 @@ 报警与异常事件
-
-
+
未查询到数据
+
+
时间 设备 事件内容
-
+
-
+
{{ event.time }} - {{ event.device }} + + {{ event.device }} + 预留数据 +

{{ event.message }}

@@ -236,24 +265,32 @@ 设备运行履历
-
+
未查询到数据
+
{{ item.label }}
-
+
设备
{{ tick }}
-
+
-
+
- {{ item.name }} + + {{ item.name }} + 预留数据 +
+
+ +
@@ -289,11 +377,18 @@ export default { height: 1080, transform: 'scaleY(1) scaleX(1) translate(-50%, -50%)' }, + detailTooltip: { + visible: false, + type: '', + x: 0, + y: 0, + data: null + }, titleText: '', headerDateCn: '', headerTime: '', iframeAddress: '', - mode: 'DT', + mode: '', currentMode: 'DT', srcDt: 'http://localhost:10056/', srcSm: 'http://localhost:10059/', @@ -355,178 +450,12 @@ export default { zoom0: 1 } }, - lineMonitorRows: [ - { - unitKey: 'longRail', - 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' } - ] - }, - { - unitKey: 'shortRail', - 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' } - ] - }, - { - unitKey: 'joint', - 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' } - ] - }, - { - unitKey: 'grinder', - 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: [ - { - unitKey: 'longRail', - name: '加工中心单元库区', - total: 9, - columns: 3, - filled: 6, - slots: this.createSlots(['S1', 'S2', 'S4', 'S6', 'S7', 'S9'], 9) - }, - { - unitKey: 'shortRail', - name: '车床加工单元库区', - total: 9, - columns: 3, - filled: 6, - slots: this.createSlots(['S1', 'S3', 'S4', 'S6', 'S8', 'S9'], 9) - }, - { - unitKey: 'joint', - name: '插齿机单元库区', - total: 6, - columns: 2, - filled: 4, - slots: this.createSlots(['S1', 'S3', 'S5', 'S6'], 6) - }, - { - unitKey: 'grinder', - name: '磨床加工单元库区', - total: 6, - columns: 2, - 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 } - ], + lineMonitorRows: this.getReservedLineMonitorRows(), + equipmentList: this.getReservedEquipmentRows(), + assistList: [], + storageGroups: this.getReservedStorageGroups(), + alarmEvents: this.getReservedAlarmEvents(), + alarmFrequencyTop5: this.getReservedAlarmStats(), historyTicks: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'], historyLegend: [ { label: '关机', type: 'off' }, @@ -534,26 +463,35 @@ export default { { 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 }] } - ] + deviceHistoryList: this.getReservedDeviceHistoryRows() } }, computed: { + isSimulationMode() { + return this.currentMode === 'SM' || this.mode === 'SM' + }, + showDashboardModules() { + return !this.isSimulationMode + }, + detailTooltipStyle() { + const width = 430 + const maxHeight = 470 + const padding = 14 + const left = Math.min( + Math.max(this.detailTooltip.x + 18, padding), + this.style.width - width - padding + ) + const top = Math.min( + Math.max(this.detailTooltip.y + 18, padding), + this.style.height - maxHeight - padding + ) + return { + left: `${left}px`, + top: `${top}px`, + width: `${width}px`, + maxHeight: `${maxHeight}px` + } + }, lineMonitorScrollList() { return [...this.lineMonitorRows, ...this.lineMonitorRows] }, @@ -625,6 +563,7 @@ export default { this.srcDt = this.buildIframeUrl(config.dtIframe || this.srcDt, this.autoPlayCraftCode) this.srcSm = this.buildIframeUrl(this.smIframeBase, this.autoPlayCraftCode, this.selectedAutoPlayRate) this.titleText = config.titleText + this.switchMode('DT') this.updateTime() this.setScale() this.$nextTick(this.initAlarmPieChart) @@ -706,6 +645,40 @@ export default { } }, methods: { + showLineTooltip(row, event) { + this.detailTooltip.visible = true + this.detailTooltip.type = 'line' + this.detailTooltip.data = row + this.updateDetailTooltipPosition(event) + }, + moveDetailTooltip(event) { + if (!this.detailTooltip.visible) return + this.updateDetailTooltipPosition(event) + }, + hideDetailTooltip() { + if (!this.detailTooltip.visible) return + this.detailTooltip.visible = false + this.detailTooltip.data = null + }, + updateDetailTooltipPosition(event) { + if (!event || !this.$el) return + const rect = this.$el.getBoundingClientRect() + const scaleX = rect.width ? this.style.width / rect.width : 1 + const scaleY = rect.height ? this.style.height / rect.height : 1 + this.detailTooltip.x = (event.clientX - rect.left) * scaleX + this.detailTooltip.y = (event.clientY - rect.top) * scaleY + }, + getSignalStateText(state) { + const stateMap = { + on: '已满足', + ok: '正常', + warn: '待确认', + alarm: '异常', + idle: '待机', + off: '未触发' + } + return stateMap[state] || '--' + }, createSlots(filledNames, total) { return Array.from({ length: total }, (_, index) => { const name = `S${index + 1}` @@ -716,6 +689,151 @@ export default { } }) }, + getReservedLineMonitorRows() { + return [ + { + unitKey: 'grinder', + name: '磨床加工单元', + status: '预留待机', + type: 'ok', + taskNo: 'TASK 预留', + startPos: 'S2', + endPos: 'S6', + isReserved: true, + taskInfo: [ + { label: '配方号', value: 'RCP-GR-01' }, + { label: '托盘类型', value: 'Pallet-G' } + ], + 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' } + ] + } + ] + }, + getReservedEquipmentRows() { + return [ + { + key: 'grinder', + name: '磨床加工单元', + status: '预留待机', + type: 'idle', + isReserved: true, + actual: '--', + tags: [ + { text: '点位预留', type: 'warn' }, + { text: '模拟展示', type: 'idle' } + ] + } + ] + }, + getReservedStorageGroups() { + return [ + { + unitKey: 'grinder', + name: '磨床加工单元库区', + total: 6, + columns: 2, + filled: 3, + isReserved: true, + slots: this.createSlots(['S2', 'S4', 'S6'], 6) + } + ] + }, + getReservedAlarmEvents() { + return [ + { + time: '--:--', + device: '磨床加工单元', + level: '预留', + type: 'warn', + isReserved: true, + message: '磨床报警点位预留,当前为模拟展示' + } + ] + }, + getReservedAlarmStats() { + return [ + { name: '磨床加工单元', value: 1, isReserved: true } + ] + }, + getReservedDeviceHistoryRows() { + return [ + { + key: 'grinder', + name: '磨床加工单元', + isReserved: true, + 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 } + ] + } + ] + }, + isNoDataResult(list) { + return !Array.isArray(list) || + list.length === 0 || + (list.length === 1 && list[0] && (list[0].result === '0' || list[0].result === 0)) + }, + isGrinderRecord(record) { + const text = [ + record && record.unitKey, + record && record.key, + record && record.name, + record && record.device, + record && record.UnitKey, + record && record.DeviceKey, + record && record.UnitName, + record && record.DeviceName + ].filter(Boolean).join('|').toLowerCase() + return text.includes('grinder') || text.includes('磨床') + }, + isReverseStatusDevice(record) { + const text = [ + record && record.key, + record && record.name, + record && record.device, + record && record.DeviceKey, + record && record.DeviceName + ].filter(Boolean).join('|').toUpperCase() + return text.includes('新日') || text.includes('MAZAK') || text.includes('XINRI') + }, + getReversedEquipmentStatus(record, status, type) { + if (!this.isReverseStatusDevice(record)) { + return { status, type } + } + if (type === 'alarm' || status === '报警') { + return { status: '待机', type: 'idle' } + } + if (type === 'idle' || status === '待机') { + return { status: '报警', type: 'alarm' } + } + return { status, type } + }, + getReversedHistoryStatus(record, label, type) { + if (!this.isReverseStatusDevice(record)) { + return { label, type } + } + if (type === 'alarm' || label === '报警') { + return { label: '待机', type: 'standby' } + } + if (type === 'standby' || label === '待机') { + return { label: '报警', type: 'alarm' } + } + return { label, type } + }, getStorageSlotGridStyle(group) { const columns = Number(group && group.columns) || 3 return { @@ -729,11 +847,14 @@ export default { this.ExecDatabase(data) .then((response) => { const list = response && response.data ? response.data : null - if (!Array.isArray(list) || list.length === 0) return - if (list.length === 1 && list[0] && (list[0].result === '0' || list[0].result === 0)) return + if (this.isNoDataResult(list)) { + this.storageGroups = this.getReservedStorageGroups() + return + } this.applyStorageMatrixData(list) }) .catch((error) => { + this.storageGroups = this.getReservedStorageGroups() console.error('TV_物料库位矩阵_查询失败', error) }) .then(() => { @@ -751,6 +872,7 @@ export default { name: this.formatStorageValue(item.UnitName || item.unitName), total: Number(item.Total || item.total) || 0, columns: Number(item.Columns || item.columns) || 3, + sortNo: Object.keys(groupMap).length + 1, slots: [] } } @@ -770,11 +892,12 @@ export default { groupMap[key].total = groupMap[key].total || groupMap[key].slots.length groupMap[key].filled = groupMap[key].slots.filter(slot => slot.filled).length }) - this.storageGroups = this.storageGroups.map((group) => { - const nextGroup = groupMap[group.unitKey] - if (!nextGroup) return group - return Object.assign({}, group, nextGroup) - }) + const groups = Object.keys(groupMap) + .map(key => groupMap[key]) + .sort((a, b) => a.sortNo - b.sortNo) + this.storageGroups = groups.some(group => this.isGrinderRecord(group)) + ? groups + : groups.concat(this.getReservedStorageGroups()) }, formatStorageValue(value) { return String(value == null ? '' : value).trim() @@ -795,11 +918,18 @@ export default { this.ExecDatabase(data) .then((response) => { const list = response && response.data ? response.data : null - if (!Array.isArray(list) || list.length === 0) return - if (list.length === 1 && list[0] && (list[0].result === '0' || list[0].result === 0)) return + if (this.isNoDataResult(list)) { + this.alarmEvents = this.getReservedAlarmEvents() + this.alarmFrequencyTop5 = this.getReservedAlarmStats() + this.$nextTick(this.initAlarmPieChart) + return + } this.applyAlarmEventData(list) }) .catch((error) => { + this.alarmEvents = this.getReservedAlarmEvents() + this.alarmFrequencyTop5 = this.getReservedAlarmStats() + this.$nextTick(this.initAlarmPieChart) console.error('TV_报警异常事件_查询失败', error) }) .then(() => { @@ -822,13 +952,13 @@ export default { events.push(this.createAlarmEventRow(item)) } }) - if (events.length) { - this.alarmEvents = events - } - if (stats.length) { - this.alarmFrequencyTop5 = stats - this.$nextTick(this.initAlarmPieChart) - } + this.alarmEvents = events.some(event => this.isGrinderRecord(event)) + ? events + : events.concat(this.getReservedAlarmEvents()) + this.alarmFrequencyTop5 = stats.some(stat => this.isGrinderRecord(stat)) + ? stats + : stats.slice(0, 4).concat(this.getReservedAlarmStats()) + this.$nextTick(this.initAlarmPieChart) }, createAlarmEventRow(item) { const type = this.normalizeAlarmEventType(this.pickRecordValue(item, ['EventType', 'eventType'])) @@ -876,11 +1006,16 @@ export default { this.ExecDatabase(data) .then((response) => { const list = response && response.data ? response.data : null - if (!Array.isArray(list) || list.length === 0) return - if (list.length === 1 && list[0] && (list[0].result === '0' || list[0].result === 0)) return + if (this.isNoDataResult(list)) { + this.equipmentList = this.getReservedEquipmentRows() + this.assistList = [] + return + } this.applyEquipmentStatusData(list) }) .catch((error) => { + this.equipmentList = this.getReservedEquipmentRows() + this.assistList = [] console.error('TV_设备实时状态_查询失败', error) }) .then(() => { @@ -902,16 +1037,19 @@ export default { }) processRows.sort((a, b) => a.sortNo - b.sortNo) assistRows.sort((a, b) => a.sortNo - b.sortNo) - if (processRows.length) { - this.equipmentList = processRows - } - if (assistRows.length) { - this.assistList = assistRows - } + this.equipmentList = processRows.some(row => this.isGrinderRecord(row)) + ? processRows + : processRows.concat(this.getReservedEquipmentRows()) + this.assistList = assistRows }, createEquipmentStatusRow(item) { const tag1Text = this.formatDisplayValue(this.pickRecordValue(item, ['Tag1Text', 'tag1Text'])) const tag2Text = this.formatDisplayValue(this.pickRecordValue(item, ['Tag2Text', 'tag2Text'])) + const key = this.formatDisplayValue(this.pickRecordValue(item, ['DeviceKey', 'deviceKey'])) + const name = this.formatDisplayValue(this.pickRecordValue(item, ['DeviceName', 'deviceName'])) + const statusText = this.formatDisplayValue(this.pickRecordValue(item, ['StatusText', 'statusText'])) + const statusType = this.normalizeStatusPillType(this.pickRecordValue(item, ['StatusType', 'statusType'])) + const displayStatus = this.getReversedEquipmentStatus({ key, name }, statusText, statusType) const tags = [ { text: tag1Text, @@ -923,11 +1061,11 @@ export default { } ].filter(tag => tag.text && tag.text !== '--') return { - key: this.formatDisplayValue(this.pickRecordValue(item, ['DeviceKey', 'deviceKey'])), - name: this.formatDisplayValue(this.pickRecordValue(item, ['DeviceName', 'deviceName'])), + key, + name, actual: this.formatDisplayValue(this.pickRecordValue(item, ['OutputText', 'outputText'])), - status: this.formatDisplayValue(this.pickRecordValue(item, ['StatusText', 'statusText'])), - type: this.normalizeStatusPillType(this.pickRecordValue(item, ['StatusType', 'statusType'])), + status: displayStatus.status, + type: displayStatus.type, sortNo: Number(this.pickRecordValue(item, ['SortNo', 'sortNo'])) || 999, updateTime: this.formatDisplayValue(this.pickRecordValue(item, ['UpdateTime', 'updateTime'])), tags @@ -944,11 +1082,14 @@ export default { this.ExecDatabase(data) .then((response) => { const list = response && response.data ? response.data : null - if (!Array.isArray(list) || list.length === 0) return - if (list.length === 1 && list[0] && (list[0].result === '0' || list[0].result === 0)) return + if (this.isNoDataResult(list)) { + this.deviceHistoryList = this.getReservedDeviceHistoryRows() + return + } this.applyDeviceHistoryData(list) }) .catch((error) => { + this.deviceHistoryList = this.getReservedDeviceHistoryRows() console.error('TV_设备运行履历_查询失败', error) }) .then(() => { @@ -968,7 +1109,7 @@ export default { segments: [] } } - const segment = this.createDeviceHistorySegment(item) + const segment = this.createDeviceHistorySegment(item, rowMap[deviceKey]) if (segment.end > segment.start) { rowMap[deviceKey].segments.push(segment) } @@ -980,16 +1121,19 @@ export default { rows.forEach((row) => { row.segments.sort((a, b) => a.start - b.start) }) - if (rows.length) { - this.deviceHistoryList = rows - } + this.deviceHistoryList = rows.some(row => this.isGrinderRecord(row)) + ? rows + : rows.concat(this.getReservedDeviceHistoryRows()) }, - createDeviceHistorySegment(item) { + createDeviceHistorySegment(item, device) { const start = this.clampHistoryHour(this.pickRecordValue(item, ['StartHour', 'startHour'])) const end = this.clampHistoryHour(this.pickRecordValue(item, ['EndHour', 'endHour'])) + const label = this.formatDisplayValue(this.pickRecordValue(item, ['StatusText', 'statusText'])) + const type = this.normalizeHistoryStatusType(this.pickRecordValue(item, ['StatusType', 'statusType'])) + const displayStatus = this.getReversedHistoryStatus(device, label, type) return { - label: this.formatDisplayValue(this.pickRecordValue(item, ['StatusText', 'statusText'])), - type: this.normalizeHistoryStatusType(this.pickRecordValue(item, ['StatusType', 'statusType'])), + label: displayStatus.label, + type: displayStatus.type, start, end } @@ -1010,11 +1154,14 @@ export default { this.ExecDatabase(data) .then((response) => { const list = response && response.data ? response.data : null - if (!Array.isArray(list) || list.length === 0) return - if (list.length === 1 && list[0] && (list[0].result === '0' || list[0].result === 0)) return + if (this.isNoDataResult(list)) { + this.lineMonitorRows = this.getReservedLineMonitorRows() + return + } this.applyLineMonitorData(list) }) .catch((error) => { + this.lineMonitorRows = this.getReservedLineMonitorRows() console.error('TV_线体任务联动状态_查询失败', error) }) .then(() => { @@ -1029,12 +1176,18 @@ export default { rowMap[row.unitKey] = row } }) - this.lineMonitorRows = this.lineMonitorRows.map((row) => { - if (row.unitKey && rowMap[row.unitKey]) { - return Object.assign({}, row, rowMap[row.unitKey]) - } - return row - }) + const orderMap = { + longRail: 1, + shortRail: 2, + joint: 3, + grinder: 4 + } + const rows = Object.keys(rowMap) + .map(key => rowMap[key]) + .sort((a, b) => (orderMap[a.unitKey] || 99) - (orderMap[b.unitKey] || 99)) + this.lineMonitorRows = rows.some(row => this.isGrinderRecord(row)) + ? rows + : rows.concat(this.getReservedLineMonitorRows()) }, createLineMonitorRow(item) { const getValue = (...keys) => { @@ -1094,6 +1247,9 @@ export default { if (!text || text.toUpperCase() === 'NULL') { return '--' } + if (text.replace(/\s/g, '') === 'PCS离线') { + return '加工未启动' + } return text }, formatTaskNo(value) { @@ -1224,6 +1380,13 @@ export default { }, switchMode(mode) { this.currentMode = mode + if (mode === 'SM') { + this.hideDetailTooltip() + this.disposeAlarmPieChart() + } + if (mode === 'DT') { + this.$nextTick(this.initAlarmPieChart) + } this.mode = '' this.dmtIframeReady = false if (this.iframeReadyTimer) { @@ -1260,8 +1423,15 @@ export default { this.simulationRateDialogVisible = false this.switchMode('SM') }, + disposeAlarmPieChart() { + if (this.alarmPieChart) { + this.alarmPieChart.dispose() + this.alarmPieChart = null + } + }, initAlarmPieChart() { - if (!this.$refs.alarmPieChart) { + if (!this.alarmFrequencyTop5.length || !this.$refs.alarmPieChart) { + this.disposeAlarmPieChart() return } if (!this.alarmPieChart) { @@ -1832,6 +2002,17 @@ body, background: radial-gradient(circle at center, rgba(37, 111, 184, 0.24), rgba(4, 12, 28, 0.92)); } +.qa-screen--simulation .leftLine, +.qa-screen--simulation .rightLine { + display: none; +} + +.main-content--simulation { + left: 0; + right: 0; + bottom: 0; +} + .left-box { position: absolute; top: 0; @@ -1994,6 +2175,299 @@ body, background: rgba(111, 212, 255, 0.22); } +.empty-state { + flex: 1; + min-height: 0; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + color: rgba(180, 211, 230, 0.78); + border: 1px dashed rgba(114, 206, 255, 0.2); + background: + linear-gradient(180deg, rgba(24, 66, 104, 0.16), rgba(7, 20, 34, 0.24)), + rgba(2, 9, 18, 0.24); + font-size: 15px; + font-weight: 700; + letter-spacing: 0; +} + +.reserved-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: max-content; + min-height: 16px; + padding: 1px 6px; + box-sizing: border-box; + color: #ffe8a8; + border: 1px solid rgba(245, 200, 76, 0.38); + background: rgba(101, 73, 14, 0.28); + box-shadow: 0 0 10px rgba(245, 200, 76, 0.14); + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; +} + +.reserved-badge--inline { + margin-left: 6px; + vertical-align: middle; +} + +.detail-tooltip { + position: absolute; + z-index: 90; + padding: 12px; + box-sizing: border-box; + color: #eaf8ff; + border: 1px solid rgba(114, 232, 255, 0.46); + background: + linear-gradient(180deg, rgba(18, 48, 74, 0.94), rgba(4, 14, 27, 0.96)), + rgba(3, 13, 31, 0.94); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 0 24px rgba(44, 143, 255, 0.18), + 0 14px 30px rgba(0, 0, 0, 0.48), + 0 0 24px rgba(80, 237, 255, 0.18); + pointer-events: none; + overflow: hidden; +} + +.detail-tooltip::before, +.detail-tooltip::after { + content: ""; + position: absolute; + width: 22px; + height: 22px; + pointer-events: none; +} + +.detail-tooltip::before { + left: 0; + top: 0; + border-left: 2px solid rgba(128, 234, 255, 0.86); + border-top: 2px solid rgba(128, 234, 255, 0.86); +} + +.detail-tooltip::after { + right: 0; + bottom: 0; + border-right: 2px solid rgba(128, 234, 255, 0.58); + border-bottom: 2px solid rgba(128, 234, 255, 0.58); +} + +.detail-tooltip__head { + min-width: 0; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding-bottom: 9px; + border-bottom: 1px solid rgba(114, 206, 255, 0.18); +} + +.detail-tooltip__head > strong { + min-width: 0; + color: #fff; + font-size: 16px; + line-height: 1.35; + text-shadow: 0 0 12px rgba(114, 232, 255, 0.28); + word-break: break-all; +} + +.detail-tooltip__status { + min-width: 76px; + transform: scale(0.94); + transform-origin: right top; +} + +.detail-tooltip__grid { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 7px; +} + +.detail-tooltip__kv { + min-width: 0; + padding: 7px 8px; + border: 1px solid rgba(114, 206, 255, 0.12); + background: + linear-gradient(90deg, rgba(44, 143, 255, 0.1), transparent 68%), + rgba(0, 0, 0, 0.24); +} + +.detail-tooltip__kv--wide { + grid-column: span 3; +} + +.detail-tooltip__kv span, +.detail-tooltip__section > span { + display: block; + color: #91b5cd; + font-size: 11px; + line-height: 1.2; +} + +.detail-tooltip__kv strong { + display: block; + margin-top: 4px; + color: #ffffff; + font-size: 13px; + line-height: 1.35; + word-break: break-all; +} + +.detail-tooltip__section { + margin-top: 10px; +} + +.detail-tooltip__signals { + margin-top: 6px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.detail-tooltip__signals--handshake { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.detail-tooltip__signal { + min-width: 0; + min-height: 26px; + display: grid; + grid-template-columns: 8px minmax(0, 1fr) auto; + align-items: center; + gap: 6px; + padding: 4px 7px; + box-sizing: border-box; + border: 1px solid rgba(160, 180, 194, 0.16); + background: rgba(36, 48, 60, 0.38); +} + +.detail-tooltip__signal i { + width: 7px; + height: 7px; + border-radius: 50%; + background: #5f7182; +} + +.detail-tooltip__signal strong, +.detail-tooltip__signal em { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + font-style: normal; +} + +.detail-tooltip__signal strong { + color: #d7eefb; +} + +.detail-tooltip__signal em { + color: #9fc3dc; +} + +.detail-tooltip__signal--on, +.detail-tooltip__signal--ok { + color: #d9ffea; + border-color: rgba(36, 216, 139, 0.34); + background: rgba(12, 80, 54, 0.5); +} + +.detail-tooltip__signal--on i, +.detail-tooltip__signal--ok i { + background: var(--scada-green); + box-shadow: 0 0 8px rgba(36, 216, 139, 0.64); +} + +.detail-tooltip__signal--warn { + color: #fff3bf; + border-color: rgba(245, 200, 76, 0.36); + background: rgba(101, 73, 14, 0.48); +} + +.detail-tooltip__signal--warn i { + background: var(--scada-yellow); + box-shadow: 0 0 8px rgba(245, 200, 76, 0.58); +} + +.detail-tooltip__signal--alarm { + color: #ffe3e6; + border-color: rgba(255, 82, 102, 0.44); + background: rgba(101, 18, 32, 0.52); +} + +.detail-tooltip__signal--alarm i { + background: var(--scada-red); + box-shadow: 0 0 8px rgba(255, 82, 102, 0.62); +} + +.detail-tooltip__timeline { + margin-top: 7px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.detail-tooltip__time-row { + min-width: 0; + min-height: 28px; + display: grid; + grid-template-columns: 1fr 54px; + align-items: center; + gap: 8px; + padding: 5px 7px; + box-sizing: border-box; + border: 1px solid rgba(114, 206, 255, 0.14); + background: + linear-gradient(90deg, rgba(44, 143, 255, 0.08), transparent 62%), + rgba(0, 0, 0, 0.24); +} + +.detail-tooltip__time-row > span { + color: #c9e7f5; + font-size: 12px; + white-space: nowrap; +} + +.detail-tooltip__history-state { + justify-self: end; + min-width: 42px; + padding: 2px 5px; + box-sizing: border-box; + color: #c5d4df; + border: 1px solid rgba(160, 180, 194, 0.24); + background: rgba(36, 48, 60, 0.42); + font-size: 11px; + text-align: center; + white-space: nowrap; +} + +.detail-tooltip__history-state--run { + color: #d9ffea; + border-color: rgba(36, 216, 139, 0.42); + background: rgba(12, 80, 54, 0.5); +} + +.detail-tooltip__history-state--standby { + color: #fff3bf; + border-color: rgba(245, 200, 76, 0.38); + background: rgba(101, 73, 14, 0.48); +} + +.detail-tooltip__history-state--alarm { + color: #ffe3e6; + border-color: rgba(255, 82, 102, 0.48); + background: rgba(101, 18, 32, 0.52); +} + .left-title-header, .right-title-header, .center-title-header { @@ -2189,6 +2663,7 @@ body, flex-direction: column; justify-content: space-between; overflow: hidden; + cursor: help; } .monitor-card::before, @@ -2214,6 +2689,7 @@ body, align-items: flex-start; justify-content: space-between; gap: 10px; + font-size: 12px; } .monitor-title strong { @@ -2231,6 +2707,10 @@ body, font-weight: 700; } +.monitor-title .reserved-badge { + margin-top: 4px; +} + .task-summary { margin-top: 5px; padding: 6px 8px; @@ -2466,6 +2946,10 @@ body, white-space: nowrap; } +.equipment-name .reserved-badge--inline { + transform: translateY(-1px); +} + .equipment-program { display: flex; flex-direction: column; @@ -2604,6 +3088,16 @@ body, flex-shrink: 0; } +.storage-head strong { + min-width: 0; + display: inline-flex; + align-items: center; + gap: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .slots { flex: 1; min-height: 0; @@ -2692,6 +3186,13 @@ body, font-size: 12px; } +.event-row--reserved { + border-color: rgba(245, 200, 76, 0.28); + background: + linear-gradient(90deg, rgba(245, 200, 76, 0.12), rgba(4, 14, 27, 0.72)), + rgba(5, 20, 44, 0.66); +} + .event-row p, .event-row strong { margin: 0; @@ -2699,6 +3200,12 @@ body, line-height: 1.35; } +.event-row strong .reserved-badge--inline { + display: flex; + margin-left: 0; + margin-top: 1px; +} + .event-row p { overflow: hidden; text-overflow: ellipsis; @@ -2811,6 +3318,13 @@ body, white-space: nowrap; } +.history-device .reserved-badge--inline { + margin-left: 0; + margin-top: 2px; + transform: scale(0.92); + transform-origin: left center; +} + .history-scale { height: 100%; display: flex; diff --git a/work/庆安数字孪生可视化看板说明书.docx b/work/庆安数字孪生可视化看板说明书.docx new file mode 100644 index 0000000..8eec772 Binary files /dev/null and b/work/庆安数字孪生可视化看板说明书.docx differ