|
@@ -0,0 +1,406 @@
|
|
|
+<template>
|
|
|
+ <div class="device-monitor-container">
|
|
|
+ <!-- 实时数据页面 -->
|
|
|
+ <div v-show="activeTab === 'realtime'" class="tab-content">
|
|
|
+ <el-card class="box-card">
|
|
|
+ <div slot="header" class="clearfix">
|
|
|
+ <span>实时数据</span>
|
|
|
+ <el-button style="float: right; padding: 3px 0" type="text" @click="refreshRealtimeData">
|
|
|
+ <i class="el-icon-refresh"></i> 刷新 ({{ realtimeCountdown }}s)
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据分组展示 -->
|
|
|
+ <div class="realtime-groups">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8" v-for="group in realtimeGroups" :key="group.groupName">
|
|
|
+ <el-card class="group-card">
|
|
|
+ <div slot="header">
|
|
|
+ <span>{{ group.groupName }}</span>
|
|
|
+ <el-badge :value="group.points.length" class="point-count"></el-badge>
|
|
|
+ </div>
|
|
|
+ <div class="point-list">
|
|
|
+ <div v-for="point in group.points" :key="point.pointId" class="point-item">
|
|
|
+ <div class="point-name">{{ point.pointName }}</div>
|
|
|
+ <div class="point-value">
|
|
|
+ <span :class="['value', getPointStatusClass(point.status)]">
|
|
|
+ {{ point.value }} {{ point.unit }}
|
|
|
+ </span>
|
|
|
+ <el-tag :type="getPointStatusTag(point.status)" size="mini">
|
|
|
+ {{ getPointStatusText(point.status) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="point-time">{{ point.updateTime }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script>
|
|
|
+import * as echarts from 'echarts'
|
|
|
+export default {
|
|
|
+ name: 'DeviceMonitor',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 设备选择
|
|
|
+ deviceForm: {
|
|
|
+ deviceId: '',
|
|
|
+ deviceType: ''
|
|
|
+ },
|
|
|
+ deviceList: [],
|
|
|
+
|
|
|
+ // 当前活动页签
|
|
|
+ activeTab: 'control',
|
|
|
+
|
|
|
+
|
|
|
+ // 实时数据相关
|
|
|
+ realtimeGroups: [],
|
|
|
+ realtimeCountdown: 30,
|
|
|
+ realtimeTimer: null,
|
|
|
+
|
|
|
+ // 设备详情相关
|
|
|
+ deviceDetail: {},
|
|
|
+ deviceDocuments: [],
|
|
|
+ showUploadDialog: false,
|
|
|
+ fileList: [],
|
|
|
+ uploadUrl: '/api/device/upload',
|
|
|
+ uploadHeaders: {
|
|
|
+ 'Authorization': 'Bearer ' + this.getToken()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ mounted() {
|
|
|
+ this.loadDeviceList()
|
|
|
+ this.initRealtimeTimer()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 获取设备列表
|
|
|
+ async loadDeviceList() {
|
|
|
+ try {
|
|
|
+ const response = await this.$deviceApi.getDeviceList()
|
|
|
+ this.deviceList = response.data || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('获取设备列表失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 页签切换
|
|
|
+ handleTabClick(tab) {
|
|
|
+ this.loadCurrentTabData()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 加载当前页签数据
|
|
|
+ loadCurrentTabData() {
|
|
|
+ if (!this.deviceForm.deviceId) {
|
|
|
+ this.$message.warning('请先选择设备')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (this.activeTab) {
|
|
|
+ case 'control':
|
|
|
+ this.loadControlRecords()
|
|
|
+ break
|
|
|
+ case 'realtime':
|
|
|
+ this.loadRealtimeData()
|
|
|
+ break
|
|
|
+ case 'history':
|
|
|
+ this.loadHistoryData()
|
|
|
+ this.loadAvailablePoints()
|
|
|
+ break
|
|
|
+ case 'events':
|
|
|
+ this.loadEventRecords()
|
|
|
+ break
|
|
|
+ case 'details':
|
|
|
+ this.loadDeviceDetail()
|
|
|
+ this.loadDeviceDocuments()
|
|
|
+ break
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 实时数据相关方法 ==========
|
|
|
+ async loadRealtimeData() {
|
|
|
+ try {
|
|
|
+ const response = await this.$deviceApi.getRealtimeData({
|
|
|
+ deviceId: this.deviceForm.deviceId
|
|
|
+ })
|
|
|
+ this.realtimeGroups = response.data || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('获取实时数据失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ refreshRealtimeData() {
|
|
|
+ this.loadRealtimeData()
|
|
|
+ this.realtimeCountdown = 30
|
|
|
+ },
|
|
|
+
|
|
|
+ initRealtimeTimer() {
|
|
|
+ this.realtimeTimer = setInterval(() => {
|
|
|
+ this.realtimeCountdown--
|
|
|
+ if (this.realtimeCountdown <= 0) {
|
|
|
+ if (this.activeTab === 'realtime' && this.deviceForm.deviceId) {
|
|
|
+ this.refreshRealtimeData()
|
|
|
+ } else {
|
|
|
+ this.realtimeCountdown = 30
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ },
|
|
|
+
|
|
|
+ getPointStatusClass(status) {
|
|
|
+ return status === 'normal' ? 'normal' : 'abnormal'
|
|
|
+ },
|
|
|
+
|
|
|
+ getPointStatusTag(status) {
|
|
|
+ return status === 'normal' ? 'success' : 'danger'
|
|
|
+ },
|
|
|
+
|
|
|
+ getPointStatusText(status) {
|
|
|
+ return status === 'normal' ? '正常' : '异常'
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // ========== 设备详情相关方法 ==========
|
|
|
+ async loadDeviceDetail() {
|
|
|
+ try {
|
|
|
+ const response = await this.$deviceApi.getDeviceDetail({
|
|
|
+ deviceId: this.deviceForm.deviceId
|
|
|
+ })
|
|
|
+ this.deviceDetail = response.data || {}
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('获取设备详情失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async loadDeviceDocuments() {
|
|
|
+ try {
|
|
|
+ const response = await this.$deviceApi.getDeviceDocuments({
|
|
|
+ deviceId: this.deviceForm.deviceId
|
|
|
+ })
|
|
|
+ this.deviceDocuments = response.data || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('获取设备文档失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ formatFileSize(bytes) {
|
|
|
+ if (bytes === 0) return '0 Bytes'
|
|
|
+ const k = 1024
|
|
|
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeUpload(file) {
|
|
|
+ const isLt10M = file.size / 1024 / 1024 < 10
|
|
|
+ if (!isLt10M) {
|
|
|
+ this.$message.error('上传文件大小不能超过 10MB!')
|
|
|
+ }
|
|
|
+ return isLt10M
|
|
|
+ },
|
|
|
+
|
|
|
+ handleUploadSuccess(response, file) {
|
|
|
+ this.$message.success('上传成功')
|
|
|
+ this.loadDeviceDocuments()
|
|
|
+ },
|
|
|
+
|
|
|
+ handleUploadError(error, file) {
|
|
|
+ this.$message.error('上传失败')
|
|
|
+ },
|
|
|
+
|
|
|
+ submitUpload() {
|
|
|
+ this.$refs.upload.submit()
|
|
|
+ this.showUploadDialog = false
|
|
|
+ },
|
|
|
+
|
|
|
+ async downloadFile(file) {
|
|
|
+ try {
|
|
|
+ const response = await this.$deviceApi.downloadFile({
|
|
|
+ fileId: file.id
|
|
|
+ })
|
|
|
+
|
|
|
+ const url = window.URL.createObjectURL(new Blob([response.data]))
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = url
|
|
|
+ link.setAttribute('download', file.fileName)
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('下载失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ deleteFile(file) {
|
|
|
+ this.$confirm('确认删除该文件?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(async () => {
|
|
|
+ try {
|
|
|
+ await this.$deviceApi.deleteDocument({
|
|
|
+ fileId: file.id
|
|
|
+ })
|
|
|
+ this.$message.success('删除成功')
|
|
|
+ this.loadDeviceDocuments()
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('删除失败')
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ this.$message.info('已取消删除')
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取认证token
|
|
|
+ getToken() {
|
|
|
+ return localStorage.getItem('token') || sessionStorage.getItem('token') || ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.device-monitor-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-selector {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.function-tabs {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-content {
|
|
|
+ min-height: 500px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 实时数据样式 */
|
|
|
+.realtime-groups {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.group-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-count {
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-list {
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.point-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.point-item:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.point-name {
|
|
|
+ font-weight: bold;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.point-value {
|
|
|
+ flex: 2;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.point-value .value {
|
|
|
+ font-size: 16px;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-value .value.normal {
|
|
|
+ color: #67C23A;
|
|
|
+}
|
|
|
+
|
|
|
+.point-value .value.abnormal {
|
|
|
+ color: #F56C6C;
|
|
|
+}
|
|
|
+
|
|
|
+.point-time {
|
|
|
+ flex: 1;
|
|
|
+ text-align: right;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表容器 */
|
|
|
+.chart-container {
|
|
|
+ margin: 20px 0;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 时间轴样式 */
|
|
|
+.timeline-container {
|
|
|
+ max-height: 600px;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.event-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.event-title {
|
|
|
+ font-weight: bold;
|
|
|
+ margin-right: 10px;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.event-content p {
|
|
|
+ margin: 5px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备信息样式 */
|
|
|
+.device-info {
|
|
|
+ padding: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传对话框样式 */
|
|
|
+.dialog-footer {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式布局 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .device-monitor-container {
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .realtime-groups .el-col {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .point-item {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .point-value {
|
|
|
+ text-align: left;
|
|
|
+ margin: 5px 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|