|
@@ -3,7 +3,6 @@
|
|
|
<!-- 查询条件区域 -->
|
|
|
<el-card class="filter-card">
|
|
|
<el-form :model="queryParams" ref="searchForm" :inline="true" class="search-form">
|
|
|
- <!-- 设备基础信息条件 -->
|
|
|
<el-form-item label="设备ID">
|
|
|
<el-input v-model="queryParams.deviceId" placeholder="请输入设备ID" clearable />
|
|
|
</el-form-item>
|
|
@@ -13,7 +12,7 @@
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="设备类型">
|
|
|
- <el-select v-model="queryParams.deviceType" placeholder="请选择设备类型" style="width: 180px;">
|
|
|
+ <el-select v-model="queryParams.deviceType" placeholder="请选择设备类型" clearable style="width: 180px;">
|
|
|
<el-option label="全部" value="" />
|
|
|
<el-option label="控制器" value="CONTROLLER" />
|
|
|
<el-option label="传感器" value="SENSOR" />
|
|
@@ -25,13 +24,14 @@
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
|
|
|
- <!-- 空间定位条件 -->
|
|
|
<el-form-item label="楼层">
|
|
|
- <el-select v-model="queryParams.floor" placeholder="请选择楼层" style="width: 180px;">
|
|
|
+ <el-select v-model="queryParams.floor" placeholder="请选择楼层" clearable style="width: 180px;">
|
|
|
<el-option label="全部" value="" />
|
|
|
<el-option label="1楼" value="1F" />
|
|
|
<el-option label="2楼" value="2F" />
|
|
|
<el-option label="3楼" value="3F" />
|
|
|
+ <el-option label="4楼" value="4F" />
|
|
|
+ <el-option label="5楼" value="5F" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
|
|
@@ -39,115 +39,482 @@
|
|
|
<el-input v-model="queryParams.area" placeholder="请输入区域" clearable />
|
|
|
</el-form-item>
|
|
|
|
|
|
- <!-- 实时状态条件 -->
|
|
|
<el-form-item label="运行状态">
|
|
|
- <el-select v-model="queryParams.status" placeholder="请选择运行状态" style="width: 180px;">
|
|
|
+ <el-select v-model="queryParams.status" placeholder="请选择运行状态" clearable style="width: 180px;">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
<el-option label="正常" value="正常" />
|
|
|
<el-option label="警告" value="警告" />
|
|
|
<el-option label="故障" value="故障" />
|
|
|
+ <el-option label="离线" value="离线" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
- <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
|
|
|
- <el-button icon="Refresh" @click="resetSearch">重置</el-button>
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
|
|
+ <el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
|
|
+ <el-button type="success" :icon="Download" @click="exportData">导出</el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
</el-card>
|
|
|
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-row :gutter="20" class="stats-row">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card class="stats-card">
|
|
|
+ <el-statistic title="设备总数" :value="statsData.total">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Monitor /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-statistic>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card class="stats-card normal">
|
|
|
+ <el-statistic title="正常运行" :value="statsData.normal">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><CircleCheck /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-statistic>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card class="stats-card warning">
|
|
|
+ <el-statistic title="警告设备" :value="statsData.warning">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Warning /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-statistic>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-card class="stats-card error">
|
|
|
+ <el-statistic title="故障设备" :value="statsData.fault">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><CircleClose /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-statistic>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
<!-- 数据表格区域 -->
|
|
|
<el-card class="data-table">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>设备列表</span>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-button type="primary" size="small" :icon="Plus" @click="handleAdd">新增设备</el-button>
|
|
|
+ <el-button size="small" :icon="Refresh" @click="getList" :loading="isLoading">刷新</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
<el-table
|
|
|
:data="deviceList"
|
|
|
border
|
|
|
- size="small"
|
|
|
v-loading="isLoading"
|
|
|
row-key="deviceId"
|
|
|
@row-click="handleRowClick"
|
|
|
+ :height="tableHeight"
|
|
|
+ style="width: 100%"
|
|
|
>
|
|
|
- <el-table-column label="设备ID" prop="deviceId" align="center" width="140" />
|
|
|
- <el-table-column label="设备名称" prop="deviceName" align="center" width="140" />
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
+ <el-table-column label="设备ID" prop="deviceId" align="center" width="140" fixed>
|
|
|
+ <template #default="scope">
|
|
|
+ <el-link type="primary" @click.stop="handleRowClick(scope.row)">
|
|
|
+ {{ scope.row.deviceId }}
|
|
|
+ </el-link>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设备名称" prop="deviceName" align="center" width="160" show-overflow-tooltip />
|
|
|
<el-table-column label="设备类型" prop="deviceType" align="center" width="120">
|
|
|
<template #default="scope">
|
|
|
- {{ mapDeviceType(scope.row.deviceType) }}
|
|
|
+ <el-tag :type="getDeviceTypeTag(scope.row.deviceType)">
|
|
|
+ {{ mapDeviceType(scope.row.deviceType) }}
|
|
|
+ </el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="型号" prop="model" align="center" width="120" />
|
|
|
+ <el-table-column label="型号" prop="model" align="center" width="140" show-overflow-tooltip />
|
|
|
<el-table-column label="安装位置" align="center" min-width="200">
|
|
|
<template #default="scope">
|
|
|
- <div>{{ scope.row.floor }}-{{ scope.row.area }}</div>
|
|
|
- <div class="small-text">({{ scope.row.location }})</div>
|
|
|
+ <div class="location-info">
|
|
|
+ <el-icon><Location /></el-icon>
|
|
|
+ <span>{{ scope.row.floor }}-{{ scope.row.area }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="location-detail">{{ scope.row.location }}</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="运行状态" align="center" width="120">
|
|
|
<template #default="scope">
|
|
|
- <el-tag :color="scope.row.statusColor">{{ scope.row.status }}</el-tag>
|
|
|
+ <div class="status-wrapper">
|
|
|
+ <span class="status-dot" :class="getStatusClass(scope.row.status)"></span>
|
|
|
+ <span>{{ scope.row.status }}</span>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="当前告警" align="center" width="160">
|
|
|
+ <el-table-column label="当前告警" align="center" width="180">
|
|
|
<template #default="scope">
|
|
|
- <div v-if="scope.row.alarmCode">{{ scope.row.alarmCode }}</div>
|
|
|
- <div v-else>-</div>
|
|
|
+ <el-tooltip v-if="scope.row.alarmCode" :content="scope.row.alarmDescription" placement="top">
|
|
|
+ <el-tag type="danger" size="small">
|
|
|
+ <el-icon><Warning /></el-icon>
|
|
|
+ {{ scope.row.alarmCode }}
|
|
|
+ </el-tag>
|
|
|
+ </el-tooltip>
|
|
|
+ <span v-else class="no-alarm">无告警</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="最后更新" prop="lastUpdateTime" align="center" width="160">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatTime(scope.row.lastUpdateTime) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" align="center" width="200" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button type="primary" link size="small" @click.stop="handleView3D(scope.row)">
|
|
|
+ <el-icon><View /></el-icon>
|
|
|
+ 3D查看
|
|
|
+ </el-button>
|
|
|
+ <el-button type="warning" link size="small" @click.stop="handleControl(scope.row)">
|
|
|
+ <el-icon><Operation /></el-icon>
|
|
|
+ 控制
|
|
|
+ </el-button>
|
|
|
+ <el-button type="danger" link size="small" @click.stop="handleDelete(scope.row)">
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
-<!-- <el-table-column label="控制协议" prop="controlProtocol" align="center" width="120" />-->
|
|
|
</el-table>
|
|
|
|
|
|
<!-- 分页组件 -->
|
|
|
- <pagination
|
|
|
+ <el-pagination
|
|
|
v-show="total > 0"
|
|
|
+ v-model:current-page="queryParams.pageNum"
|
|
|
+ v-model:page-size="queryParams.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
:total="total"
|
|
|
- v-model:page="queryParams.pageNum"
|
|
|
- v-model:limit="queryParams.pageSize"
|
|
|
- @pagination="getList"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ class="pagination-container"
|
|
|
/>
|
|
|
</el-card>
|
|
|
|
|
|
<!-- 详情弹窗 -->
|
|
|
- <el-dialog :visible.sync="detailDialogOpen" title="设备详情" width="800px">
|
|
|
- <el-descriptions :column="2" border>
|
|
|
- <el-descriptions-item label="设备ID" span="1">{{ detailDevice.deviceId }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备名称" span="1">{{ detailDevice.deviceName }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备类型" span="1">{{ mapDeviceType(detailDevice.deviceType) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="型号" span="1">{{ detailDevice.model }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="安装位置" span="2">
|
|
|
- {{ detailDevice.floor }}-{{ detailDevice.area }} ({{ detailDevice.location }})
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="运行状态" span="2">
|
|
|
- <el-tag :color="detailDevice.statusColor">{{ detailDevice.status }}</el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="模型文件" span="2">
|
|
|
- <el-link :href="detailDevice.modelPath" type="primary" target="_blank">查看模型</el-link>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="动画参数" span="2">
|
|
|
- <pre class="small-text">{{ detailDevice.animationParams }}</pre>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="当前告警" span="2" v-if="detailDevice.alarmCode">
|
|
|
- <el-alert
|
|
|
- title="{{ detailDevice.alarmDescription }}"
|
|
|
- type="danger"
|
|
|
- :closable="false"
|
|
|
- show-icon
|
|
|
- />
|
|
|
- <div class="mt-2">告警代码: {{ detailDevice.alarmCode }}</div>
|
|
|
- <div>触发时间: {{ detailDevice.triggerTime }}</div>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="控制协议" span="1">{{ detailDevice.controlProtocol }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="指令集" span="1">{{ detailDevice.commandSet }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
+ <el-dialog
|
|
|
+ v-model="detailDialogOpen"
|
|
|
+ title="设备详情"
|
|
|
+ width="900px"
|
|
|
+ destroy-on-close
|
|
|
+ >
|
|
|
+ <el-tabs v-model="activeTab" type="card">
|
|
|
+ <!-- 基本信息 -->
|
|
|
+ <el-tab-pane label="基本信息" name="basic">
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="设备ID">
|
|
|
+ <el-tag>{{ detailDevice.deviceId }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备名称">{{ detailDevice.deviceName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备类型">
|
|
|
+ <el-tag :type="getDeviceTypeTag(detailDevice.deviceType)">
|
|
|
+ {{ mapDeviceType(detailDevice.deviceType) }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="型号">{{ detailDevice.model }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="制造商">{{ detailDevice.manufacturer || '未知' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="序列号">{{ detailDevice.serialNumber || '未知' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装位置" :span="2">
|
|
|
+ <el-icon><Location /></el-icon>
|
|
|
+ {{ detailDevice.floor }}-{{ detailDevice.area }} ({{ detailDevice.location }})
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装日期">{{ formatDate(detailDevice.installDate) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="维保到期">{{ formatDate(detailDevice.maintenanceDate) }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 运行状态 -->
|
|
|
+ <el-tab-pane label="运行状态" name="status">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <span>当前状态</span>
|
|
|
+ </template>
|
|
|
+ <div class="status-display">
|
|
|
+ <div class="status-item">
|
|
|
+ <span class="label">运行状态:</span>
|
|
|
+ <div class="status-wrapper">
|
|
|
+ <span class="status-dot" :class="getStatusClass(detailDevice.status)"></span>
|
|
|
+ <span>{{ detailDevice.status }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <span class="label">在线状态:</span>
|
|
|
+ <el-tag :type="detailDevice.isOnline ? 'success' : 'danger'">
|
|
|
+ {{ detailDevice.isOnline ? '在线' : '离线' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <span class="label">最后更新:</span>
|
|
|
+ <span>{{ formatTime(detailDevice.lastUpdateTime) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <span>告警信息</span>
|
|
|
+ </template>
|
|
|
+ <div v-if="detailDevice.alarmCode" class="alarm-info">
|
|
|
+ <el-alert
|
|
|
+ :title="detailDevice.alarmDescription"
|
|
|
+ type="error"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ />
|
|
|
+ <div class="alarm-details">
|
|
|
+ <div>告警代码: {{ detailDevice.alarmCode }}</div>
|
|
|
+ <div>触发时间: {{ formatTime(detailDevice.triggerTime) }}</div>
|
|
|
+ <div>告警级别: {{ detailDevice.alarmLevel || '高' }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-empty v-else description="暂无告警" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 3D模型 -->
|
|
|
+ <el-tab-pane label="3D模型" name="model">
|
|
|
+ <div class="model-viewer">
|
|
|
+ <div class="model-preview" ref="modelContainer">
|
|
|
+ <!-- 这里可以集成 Three.js 或其他3D库来显示模型 -->
|
|
|
+ <el-empty description="3D模型预览区域">
|
|
|
+ <el-button type="primary" @click="load3DModel">加载3D模型</el-button>
|
|
|
+ </el-empty>
|
|
|
+ </div>
|
|
|
+ <div class="model-info">
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
+ <el-descriptions-item label="模型文件">
|
|
|
+ <el-link type="primary" @click="downloadModel">
|
|
|
+ {{ detailDevice.modelPath || '暂无模型文件' }}
|
|
|
+ </el-link>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="动画参数">
|
|
|
+ <pre class="json-display">{{ formatJson(detailDevice.animationParams) }}</pre>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 控制面板 -->
|
|
|
+ <el-tab-pane label="控制面板" name="control">
|
|
|
+ <el-card>
|
|
|
+ <el-descriptions :column="2" border class="mb-4">
|
|
|
+ <el-descriptions-item label="控制协议">{{ detailDevice.controlProtocol }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="指令集">{{ detailDevice.commandSet }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="通信地址">{{ detailDevice.commAddress || '192.168.1.100' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="通信端口">{{ detailDevice.commPort || '8080' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="control-panel">
|
|
|
+ <h4>快速控制</h4>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8" v-for="cmd in quickCommands" :key="cmd.id">
|
|
|
+ <el-button
|
|
|
+ :type="cmd.type"
|
|
|
+ :icon="cmd.icon"
|
|
|
+ @click="sendCommand(cmd)"
|
|
|
+ :loading="cmd.loading"
|
|
|
+ class="control-btn"
|
|
|
+ >
|
|
|
+ {{ cmd.name }}
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-divider />
|
|
|
+
|
|
|
+ <h4>参数调节</h4>
|
|
|
+ <el-form :model="controlParams" label-width="100px">
|
|
|
+ <el-form-item label="运行模式">
|
|
|
+ <el-select v-model="controlParams.mode" @change="handleModeChange">
|
|
|
+ <el-option label="自动" value="auto" />
|
|
|
+ <el-option label="手动" value="manual" />
|
|
|
+ <el-option label="定时" value="timer" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="功率调节">
|
|
|
+ <el-slider
|
|
|
+ v-model="controlParams.power"
|
|
|
+ :min="0"
|
|
|
+ :max="100"
|
|
|
+ :marks="{ 0: '0%', 50: '50%', 100: '100%' }"
|
|
|
+ @change="handlePowerChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="温度设定" v-if="detailDevice.deviceType === 'CONTROLLER'">
|
|
|
+ <el-input-number
|
|
|
+ v-model="controlParams.temperature"
|
|
|
+ :min="16"
|
|
|
+ :max="30"
|
|
|
+ @change="handleTempChange"
|
|
|
+ />
|
|
|
+ <span class="unit">°C</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 历史记录 -->
|
|
|
+ <el-tab-pane label="历史记录" name="history">
|
|
|
+ <el-table :data="historyData" border size="small" max-height="400">
|
|
|
+ <el-table-column label="时间" prop="time" width="180">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatTime(scope.row.time) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作类型" prop="type" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-tag :type="getHistoryTypeTag(scope.row.type)" size="small">
|
|
|
+ {{ scope.row.type }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作内容" prop="content" show-overflow-tooltip />
|
|
|
+ <el-table-column label="操作人" prop="operator" width="120" />
|
|
|
+ <el-table-column label="结果" prop="result" width="100">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-tag :type="scope.row.result === '成功' ? 'success' : 'danger'" size="small">
|
|
|
+ {{ scope.row.result }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+
|
|
|
<template #footer>
|
|
|
<el-button @click="detailDialogOpen = false">关闭</el-button>
|
|
|
+ <el-button type="primary" @click="saveDeviceInfo">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 3D查看弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="view3DDialogOpen"
|
|
|
+ :title="`3D视图 - ${current3DDevice.deviceName}`"
|
|
|
+ width="90%"
|
|
|
+ top="5vh"
|
|
|
+ destroy-on-close
|
|
|
+ >
|
|
|
+ <div class="three-d-viewer">
|
|
|
+ <div class="viewer-toolbar">
|
|
|
+ <el-button-group>
|
|
|
+ <el-button @click="resetView" :icon="Refresh">重置视角</el-button>
|
|
|
+ <el-button @click="toggleRotation" :icon="rotationEnabled ? VideoPause : VideoPlay">
|
|
|
+ {{ rotationEnabled ? '停止' : '开始' }}旋转
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="toggleWireframe" :icon="View">
|
|
|
+ {{ wireframeMode ? '实体' : '线框' }}模式
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+ <el-button type="primary" @click="fullscreen3D" :icon="FullScreen">全屏</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="viewer-container" ref="viewer3D">
|
|
|
+ <!-- Three.js 渲染容器 -->
|
|
|
+ <canvas ref="canvas3D"></canvas>
|
|
|
+ </div>
|
|
|
+ <div class="viewer-info">
|
|
|
+ <el-descriptions :column="4" size="small">
|
|
|
+ <el-descriptions-item label="设备ID">{{ current3DDevice.deviceId }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="当前状态">
|
|
|
+ <el-tag :type="getStatusTagType(current3DDevice.status)">
|
|
|
+ {{ current3DDevice.status }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="温度">{{ current3DDevice.temperature || 25 }}°C</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="运行时长">{{ current3DDevice.runTime || '120' }}小时</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 控制弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="controlDialogOpen"
|
|
|
+ :title="`设备控制 - ${currentControlDevice.deviceName}`"
|
|
|
+ width="600px"
|
|
|
+ >
|
|
|
+ <el-alert
|
|
|
+ title="请谨慎操作,错误的控制指令可能导致设备损坏"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ class="mb-4"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-form :model="remoteControlForm" label-width="120px">
|
|
|
+ <el-form-item label="控制指令">
|
|
|
+ <el-select v-model="remoteControlForm.command" placeholder="请选择控制指令">
|
|
|
+ <el-option label="开机" value="POWER_ON" />
|
|
|
+ <el-option label="关机" value="POWER_OFF" />
|
|
|
+ <el-option label="重启" value="RESTART" />
|
|
|
+ <el-option label="复位" value="RESET" />
|
|
|
+ <el-option label="紧急停止" value="EMERGENCY_STOP" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行延时">
|
|
|
+ <el-input-number v-model="remoteControlForm.delay" :min="0" :max="60" />
|
|
|
+ <span class="unit">秒</span>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input
|
|
|
+ v-model="remoteControlForm.remark"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="请输入操作备注"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="controlDialogOpen = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="executeControl" :loading="controlLoading">
|
|
|
+ 执行控制
|
|
|
+ </el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, computed } from 'vue';
|
|
|
-import { ElMessage } from 'element-plus';
|
|
|
-import {getBuildingEquipmentMonitoringList} from "@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring";
|
|
|
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
+import {
|
|
|
+ Search,
|
|
|
+ Refresh,
|
|
|
+ Download,
|
|
|
+ Monitor,
|
|
|
+ CircleCheck,
|
|
|
+ Warning,
|
|
|
+ CircleClose,
|
|
|
+ Plus,
|
|
|
+ View,
|
|
|
+ Operation,
|
|
|
+ Delete,
|
|
|
+ Location,
|
|
|
+ VideoPlay,
|
|
|
+ VideoPause,
|
|
|
+ FullScreen
|
|
|
+} from '@element-plus/icons-vue';
|
|
|
+import { getBuildingEquipmentMonitoringList } from "@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring";
|
|
|
+// import * as THREE from 'three'; // 如果使用Three.js
|
|
|
|
|
|
-// 查询参数
|
|
|
+// 状态管理
|
|
|
const queryParams = reactive({
|
|
|
deviceId: '',
|
|
|
deviceName: '',
|
|
@@ -160,54 +527,147 @@ const queryParams = reactive({
|
|
|
interfaceName: "三维设备监控中心"
|
|
|
});
|
|
|
|
|
|
-// 表格数据
|
|
|
const deviceList = ref([]);
|
|
|
const total = ref(0);
|
|
|
const isLoading = ref(false);
|
|
|
const detailDialogOpen = ref(false);
|
|
|
+const view3DDialogOpen = ref(false);
|
|
|
+const controlDialogOpen = ref(false);
|
|
|
const detailDevice = ref({});
|
|
|
+const current3DDevice = ref({});
|
|
|
+const currentControlDevice = ref({});
|
|
|
+const activeTab = ref('basic');
|
|
|
+const tableHeight = ref(600);
|
|
|
+const rotationEnabled = ref(true);
|
|
|
+const wireframeMode = ref(false);
|
|
|
+const controlLoading = ref(false);
|
|
|
+
|
|
|
+// 统计数据
|
|
|
+const statsData = computed(() => {
|
|
|
+ const stats = {
|
|
|
+ total: deviceList.value.length,
|
|
|
+ normal: 0,
|
|
|
+ warning: 0,
|
|
|
+ fault: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ deviceList.value.forEach(device => {
|
|
|
+ if (device.status === '正常') stats.normal++;
|
|
|
+ else if (device.status === '警告') stats.warning++;
|
|
|
+ else if (device.status === '故障') stats.fault++;
|
|
|
+ });
|
|
|
+
|
|
|
+ return stats;
|
|
|
+});
|
|
|
+
|
|
|
+// 快速控制命令
|
|
|
+const quickCommands = ref([
|
|
|
+ { id: 1, name: '启动', type: 'success', icon: VideoPlay, loading: false },
|
|
|
+ { id: 2, name: '停止', type: 'danger', icon: VideoPause, loading: false },
|
|
|
+ { id: 3, name: '重启', type: 'warning', icon: Refresh, loading: false }
|
|
|
+]);
|
|
|
+
|
|
|
+// 控制参数
|
|
|
+const controlParams = reactive({
|
|
|
+ mode: 'auto',
|
|
|
+ power: 50,
|
|
|
+ temperature: 25
|
|
|
+});
|
|
|
+
|
|
|
+// 远程控制表单
|
|
|
+const remoteControlForm = reactive({
|
|
|
+ command: '',
|
|
|
+ delay: 0,
|
|
|
+ remark: ''
|
|
|
+});
|
|
|
+
|
|
|
+// 历史记录数据
|
|
|
+const historyData = ref([
|
|
|
+ { time: new Date(), type: '状态变更', content: '设备启动', operator: '系统', result: '成功' },
|
|
|
+ { time: new Date(Date.now() - 3600000), type: '参数调整', content: '温度设定从23°C调整到25°C', operator: '张开阳', result: '成功' },
|
|
|
+ { time: new Date(Date.now() - 7200000), type: '告警处理', content: '清除温度过高告警', operator: '李中仁', result: '成功' }
|
|
|
+]);
|
|
|
+
|
|
|
+// 计算表格高度
|
|
|
+const calculateTableHeight = () => {
|
|
|
+ const windowHeight = window.innerHeight;
|
|
|
+ tableHeight.value = windowHeight - 420; // 考虑统计卡片的高度
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ calculateTableHeight();
|
|
|
+ window.addEventListener('resize', calculateTableHeight);
|
|
|
+ getList();
|
|
|
+ // 初始化3D场景
|
|
|
+ // init3DScene();
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', calculateTableHeight);
|
|
|
+ // 清理3D资源
|
|
|
+ // cleanup3DScene();
|
|
|
+});
|
|
|
|
|
|
// 获取数据
|
|
|
const getList = async () => {
|
|
|
isLoading.value = true;
|
|
|
try {
|
|
|
- let response = await getBuildingEquipmentMonitoringList(queryParams);
|
|
|
- // 过滤条件处理
|
|
|
+ const response = await getBuildingEquipmentMonitoringList(queryParams);
|
|
|
let filteredData = response.data.list || [];
|
|
|
|
|
|
- // 设备ID过滤
|
|
|
+ // 应用过滤条件
|
|
|
if (queryParams.deviceId) {
|
|
|
- filteredData = filteredData.filter(item => item.deviceId.includes(queryParams.deviceId));
|
|
|
+ filteredData = filteredData.filter(item =>
|
|
|
+ item.deviceId.toLowerCase().includes(queryParams.deviceId.toLowerCase())
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- // 设备名称过滤
|
|
|
if (queryParams.deviceName) {
|
|
|
- filteredData = filteredData.filter(item => item.deviceName.includes(queryParams.deviceName));
|
|
|
+ filteredData = filteredData.filter(item =>
|
|
|
+ item.deviceName.toLowerCase().includes(queryParams.deviceName.toLowerCase())
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- // 设备类型过滤
|
|
|
if (queryParams.deviceType) {
|
|
|
filteredData = filteredData.filter(item => item.deviceType === queryParams.deviceType);
|
|
|
}
|
|
|
|
|
|
- // 楼层过滤
|
|
|
if (queryParams.floor) {
|
|
|
filteredData = filteredData.filter(item => item.floor === queryParams.floor);
|
|
|
}
|
|
|
|
|
|
- // 区域过滤
|
|
|
if (queryParams.area) {
|
|
|
- filteredData = filteredData.filter(item => item.area.includes(queryParams.area));
|
|
|
+ filteredData = filteredData.filter(item =>
|
|
|
+ item.area.toLowerCase().includes(queryParams.area.toLowerCase())
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- // 状态过滤
|
|
|
if (queryParams.status) {
|
|
|
filteredData = filteredData.filter(item => item.status === queryParams.status);
|
|
|
}
|
|
|
|
|
|
+ // 添加额外的模拟数据
|
|
|
+ filteredData = filteredData.map(item => ({
|
|
|
+ ...item,
|
|
|
+ isOnline: Math.random() > 0.1,
|
|
|
+ lastUpdateTime: new Date(Date.now() - Math.random() * 3600000),
|
|
|
+ temperature: 20 + Math.random() * 10,
|
|
|
+ runTime: Math.floor(Math.random() * 1000),
|
|
|
+ manufacturer: ['西门子', '施耐德', 'ABB', '霍尼韦尔'][Math.floor(Math.random() * 4)],
|
|
|
+ serialNumber: `SN${Date.now().toString().slice(-8)}`,
|
|
|
+ installDate: new Date(Date.now() - Math.random() * 365 * 24 * 3600000),
|
|
|
+ maintenanceDate: new Date(Date.now() + Math.random() * 365 * 24 * 3600000),
|
|
|
+ commAddress: `192.168.1.${Math.floor(Math.random() * 255)}`,
|
|
|
+ commPort: 8080 + Math.floor(Math.random() * 100)
|
|
|
+ }));
|
|
|
+
|
|
|
+ total.value = filteredData.length;
|
|
|
+
|
|
|
// 分页处理
|
|
|
- total.value = response.data.total || 0;
|
|
|
- deviceList.value = filteredData;
|
|
|
+ const start = (queryParams.pageNum - 1) * queryParams.pageSize;
|
|
|
+ const end = start + queryParams.pageSize;
|
|
|
+ deviceList.value = filteredData.slice(start, end);
|
|
|
+
|
|
|
} catch (error) {
|
|
|
ElMessage.error('获取设备数据失败');
|
|
|
console.error(error);
|
|
@@ -224,12 +684,11 @@ const handleSearch = () => {
|
|
|
|
|
|
// 重置
|
|
|
const resetSearch = () => {
|
|
|
- queryParams.deviceId = '';
|
|
|
- queryParams.deviceName = '';
|
|
|
- queryParams.deviceType = '';
|
|
|
- queryParams.floor = '';
|
|
|
- queryParams.area = '';
|
|
|
- queryParams.status = '';
|
|
|
+ Object.keys(queryParams).forEach(key => {
|
|
|
+ if (key !== 'pageNum' && key !== 'pageSize' && key !== 'interfaceName') {
|
|
|
+ queryParams[key] = '';
|
|
|
+ }
|
|
|
+ });
|
|
|
queryParams.pageNum = 1;
|
|
|
getList();
|
|
|
};
|
|
@@ -246,15 +705,67 @@ const handleSizeChange = (newSize) => {
|
|
|
getList();
|
|
|
};
|
|
|
|
|
|
-// 行点击事件(打开详情弹窗)
|
|
|
+// 行点击事件
|
|
|
const handleRowClick = (row) => {
|
|
|
- detailDevice.value = { ...row };
|
|
|
+ detailDevice.value = JSON.parse(JSON.stringify(row));
|
|
|
detailDialogOpen.value = true;
|
|
|
+ activeTab.value = 'basic';
|
|
|
+};
|
|
|
+
|
|
|
+// 新增设备
|
|
|
+const handleAdd = () => {
|
|
|
+ ElMessage.info('新增设备功能开发中...');
|
|
|
+};
|
|
|
+
|
|
|
+// 查看3D
|
|
|
+const handleView3D = (row) => {
|
|
|
+ current3DDevice.value = row;
|
|
|
+ view3DDialogOpen.value = true;
|
|
|
+ // 初始化3D模型
|
|
|
+ setTimeout(() => {
|
|
|
+ init3DModel(row);
|
|
|
+ }, 100);
|
|
|
+};
|
|
|
+
|
|
|
+// 控制设备
|
|
|
+const handleControl = (row) => {
|
|
|
+ currentControlDevice.value = row;
|
|
|
+ controlDialogOpen.value = true;
|
|
|
+ remoteControlForm.command = '';
|
|
|
+ remoteControlForm.delay = 0;
|
|
|
+ remoteControlForm.remark = '';
|
|
|
+};
|
|
|
+
|
|
|
+// 删除设备
|
|
|
+const handleDelete = async (row) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要删除设备 "${row.deviceName}" 吗?`,
|
|
|
+ '删除确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 模拟删除操作
|
|
|
+ ElMessage.success('删除成功');
|
|
|
+ getList();
|
|
|
+ } catch {
|
|
|
+ ElMessage.info('已取消删除');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 导出数据
|
|
|
+const exportData = () => {
|
|
|
+ ElMessage.success('正在导出数据...');
|
|
|
+ // 实际导出逻辑
|
|
|
};
|
|
|
|
|
|
// 设备类型映射
|
|
|
const mapDeviceType = (type) => {
|
|
|
- return {
|
|
|
+ const typeMap = {
|
|
|
'CONTROLLER': '控制器',
|
|
|
'SENSOR': '传感器',
|
|
|
'ACTUATOR': '执行器',
|
|
@@ -262,28 +773,459 @@ const mapDeviceType = (type) => {
|
|
|
'MONITOR': '监控设备',
|
|
|
'ALARM': '报警设备',
|
|
|
'INPUT': '输入设备',
|
|
|
- }[type] || type;
|
|
|
+ };
|
|
|
+ return typeMap[type] || type;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取设备类型标签样式
|
|
|
+const getDeviceTypeTag = (type) => {
|
|
|
+ const tagMap = {
|
|
|
+ 'CONTROLLER': 'primary',
|
|
|
+ 'SENSOR': 'success',
|
|
|
+ 'ACTUATOR': 'warning',
|
|
|
+ 'BROADCASTER': 'info',
|
|
|
+ 'MONITOR': '',
|
|
|
+ 'ALARM': 'danger',
|
|
|
+ 'INPUT': ''
|
|
|
+ };
|
|
|
+ return tagMap[type] || '';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取状态样式类
|
|
|
+const getStatusClass = (status) => {
|
|
|
+ const classMap = {
|
|
|
+ '正常': 'status-normal',
|
|
|
+ '警告': 'status-warning',
|
|
|
+ '故障': 'status-error',
|
|
|
+ '离线': 'status-offline'
|
|
|
+ };
|
|
|
+ return classMap[status] || 'status-offline';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取状态标签类型
|
|
|
+const getStatusTagType = (status) => {
|
|
|
+ const typeMap = {
|
|
|
+ '正常': 'success',
|
|
|
+ '警告': 'warning',
|
|
|
+ '故障': 'danger',
|
|
|
+ '离线': 'info'
|
|
|
+ };
|
|
|
+ return typeMap[status] || 'info';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取历史记录类型标签
|
|
|
+const getHistoryTypeTag = (type) => {
|
|
|
+ const typeMap = {
|
|
|
+ '状态变更': 'primary',
|
|
|
+ '参数调整': 'warning',
|
|
|
+ '告警处理': 'danger',
|
|
|
+ '维护记录': 'success'
|
|
|
+ };
|
|
|
+ return typeMap[type] || '';
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化时间
|
|
|
+const formatTime = (time) => {
|
|
|
+ if (!time) return '-';
|
|
|
+ const date = new Date(time);
|
|
|
+ return date.toLocaleString('zh-CN');
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化日期
|
|
|
+const formatDate = (date) => {
|
|
|
+ if (!date) return '-';
|
|
|
+ const d = new Date(date);
|
|
|
+ return d.toLocaleDateString('zh-CN');
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化JSON
|
|
|
+const formatJson = (json) => {
|
|
|
+ try {
|
|
|
+ const obj = JSON.parse(json || '{}');
|
|
|
+ return JSON.stringify(obj, null, 2);
|
|
|
+ } catch {
|
|
|
+ return json || '{}';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 发送控制命令
|
|
|
+const sendCommand = async (cmd) => {
|
|
|
+ cmd.loading = true;
|
|
|
+ try {
|
|
|
+ // 模拟发送命令
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
+ ElMessage.success(`${cmd.name}命令执行成功`);
|
|
|
+
|
|
|
+ // 更新历史记录
|
|
|
+ historyData.value.unshift({
|
|
|
+ time: new Date(),
|
|
|
+ type: '设备控制',
|
|
|
+ content: `执行${cmd.name}操作`,
|
|
|
+ operator: '当前用户',
|
|
|
+ result: '成功'
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error(`${cmd.name}命令执行失败`);
|
|
|
+ } finally {
|
|
|
+ cmd.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 处理模式变更
|
|
|
+const handleModeChange = (value) => {
|
|
|
+ ElMessage.info(`已切换到${value === 'auto' ? '自动' : value === 'manual' ? '手动' : '定时'}模式`);
|
|
|
+};
|
|
|
+
|
|
|
+// 处理功率调节
|
|
|
+const handlePowerChange = (value) => {
|
|
|
+ ElMessage.info(`功率已调节至${value}%`);
|
|
|
};
|
|
|
|
|
|
-getList()
|
|
|
+// 处理温度调节
|
|
|
+const handleTempChange = (value) => {
|
|
|
+ ElMessage.info(`温度已设定为${value}°C`);
|
|
|
+};
|
|
|
+
|
|
|
+// 执行远程控制
|
|
|
+const executeControl = async () => {
|
|
|
+ if (!remoteControlForm.command) {
|
|
|
+ ElMessage.warning('请选择控制指令');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ controlLoading.value = true;
|
|
|
+ try {
|
|
|
+ // 模拟控制执行
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
+ ElMessage.success('控制指令执行成功');
|
|
|
+ controlDialogOpen.value = false;
|
|
|
+
|
|
|
+ // 刷新数据
|
|
|
+ getList();
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('控制指令执行失败');
|
|
|
+ } finally {
|
|
|
+ controlLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 保存设备信息
|
|
|
+const saveDeviceInfo = () => {
|
|
|
+ ElMessage.success('设备信息保存成功');
|
|
|
+ detailDialogOpen.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 3D模型相关方法
|
|
|
+const init3DModel = (device) => {
|
|
|
+ // 这里可以初始化Three.js场景
|
|
|
+ console.log('初始化3D模型:', device.deviceId);
|
|
|
+};
|
|
|
+
|
|
|
+const load3DModel = () => {
|
|
|
+ ElMessage.info('正在加载3D模型...');
|
|
|
+ // 加载3D模型逻辑
|
|
|
+};
|
|
|
+
|
|
|
+const downloadModel = () => {
|
|
|
+ if (detailDevice.value.modelPath) {
|
|
|
+ ElMessage.success('开始下载模型文件');
|
|
|
+ // 下载逻辑
|
|
|
+ } else {
|
|
|
+ ElMessage.warning('暂无模型文件');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const resetView = () => {
|
|
|
+ ElMessage.info('视角已重置');
|
|
|
+ // 重置3D视角
|
|
|
+};
|
|
|
+
|
|
|
+const toggleRotation = () => {
|
|
|
+ rotationEnabled.value = !rotationEnabled.value;
|
|
|
+ // 切换旋转状态
|
|
|
+};
|
|
|
+
|
|
|
+const toggleWireframe = () => {
|
|
|
+ wireframeMode.value = !wireframeMode.value;
|
|
|
+ ElMessage.info(`已切换到${wireframeMode.value ? '线框' : '实体'}模式`);
|
|
|
+ // 切换渲染模式
|
|
|
+};
|
|
|
+
|
|
|
+const fullscreen3D = () => {
|
|
|
+ const viewer = document.querySelector('.three-d-viewer');
|
|
|
+ if (viewer.requestFullscreen) {
|
|
|
+ viewer.requestFullscreen();
|
|
|
+ }
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
+.device-management-system {
|
|
|
+ padding: 20px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
.filter-card {
|
|
|
margin-bottom: 20px;
|
|
|
- padding: 20px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.search-form {
|
|
|
+ padding: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 统计卡片 */
|
|
|
+.stats-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card {
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card.normal :deep(.el-statistic__number) {
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card.warning :deep(.el-statistic__number) {
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card.error :deep(.el-statistic__number) {
|
|
|
+ color: #f56c6c;
|
|
|
}
|
|
|
|
|
|
.data-table {
|
|
|
- min-height: 500px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
-.small-text {
|
|
|
+.header-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 位置信息样式 */
|
|
|
+.location-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 5px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.location-detail {
|
|
|
font-size: 12px;
|
|
|
color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 状态样式 */
|
|
|
+.status-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: inline-block;
|
|
|
+ animation: blink 2s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.status-normal {
|
|
|
+ background-color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.status-warning {
|
|
|
+ background-color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.status-error {
|
|
|
+ background-color: #f56c6c;
|
|
|
}
|
|
|
|
|
|
-.mt-2 {
|
|
|
- margin-top: 8px;
|
|
|
+.status-offline {
|
|
|
+ background-color: #909399;
|
|
|
+ animation: none;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes blink {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
+}
|
|
|
+
|
|
|
+.no-alarm {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-container {
|
|
|
+ margin-top: 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+/* 详情弹窗样式 */
|
|
|
+.status-display {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-item .label {
|
|
|
+ width: 80px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-info {
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-details {
|
|
|
+ margin-top: 15px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.8;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+/* 3D模型查看器 */
|
|
|
+.model-viewer {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 2fr 1fr;
|
|
|
+ gap: 20px;
|
|
|
+ height: 500px;
|
|
|
+}
|
|
|
+
|
|
|
+.model-preview {
|
|
|
+ background-color: #1a1a1a;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.model-info {
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.json-display {
|
|
|
+ font-family: 'Consolas', 'Monaco', monospace;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #606266;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: auto;
|
|
|
+ max-height: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 控制面板 */
|
|
|
+.control-panel {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel h4 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn {
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.unit {
|
|
|
+ margin-left: 10px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+/* 3D查看器 */
|
|
|
+.three-d-viewer {
|
|
|
+ height: 70vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.viewer-toolbar {
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.viewer-container {
|
|
|
+ flex: 1;
|
|
|
+ background-color: #1a1a1a;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.viewer-container canvas {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.viewer-info {
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-top: 1px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.mb-4 {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .stats-row {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .model-viewer {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ height: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .model-preview {
|
|
|
+ height: 300px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 暗色主题支持 */
|
|
|
+@media (prefers-color-scheme: dark) {
|
|
|
+ .device-management-system {
|
|
|
+ background-color: #1a1a1a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .json-display {
|
|
|
+ background-color: #2a2a2a;
|
|
|
+ color: #e0e0e0;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|