|
@@ -1,1231 +1,1972 @@
|
|
|
<template>
|
|
|
<div class="device-management-system">
|
|
|
<!-- 查询条件区域 -->
|
|
|
- <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>
|
|
|
-
|
|
|
- <el-form-item label="设备名称">
|
|
|
- <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
+ <div class="search-form">
|
|
|
+ <el-form :model="queryParams" ref="searchForm" :inline="true">
|
|
|
<el-form-item label="设备类型">
|
|
|
- <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" />
|
|
|
- <el-option label="执行器" value="ACTUATOR" />
|
|
|
- <el-option label="广播设备" value="BROADCASTER" />
|
|
|
- <el-option label="监控设备" value="MONITOR" />
|
|
|
- <el-option label="报警设备" value="ALARM" />
|
|
|
- <el-option label="输入设备" value="INPUT" />
|
|
|
+ <el-select
|
|
|
+ v-model="queryParams.device_type"
|
|
|
+ placeholder="请选择设备类型"
|
|
|
+ clearable
|
|
|
+ style="width: 240px;"
|
|
|
+ @change="handleDeviceTypeChange"
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="(label, value) in deviceTypes"
|
|
|
+ :key="value"
|
|
|
+ :label="`${label}(${value})`"
|
|
|
+ :value="value"
|
|
|
+ />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
|
|
|
- <el-form-item label="楼层">
|
|
|
- <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>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :icon="Search"
|
|
|
+ @click="handleSearch"
|
|
|
+ :loading="isLoading"
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ :icon="Refresh"
|
|
|
+ @click="resetSearch"
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
</el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
|
|
|
- <el-form-item label="区域">
|
|
|
- <el-input v-model="queryParams.area" placeholder="请输入区域" clearable />
|
|
|
- </el-form-item>
|
|
|
+ <!-- 数据表格区域 -->
|
|
|
+ <div class="table-container">
|
|
|
+ <!-- 表格头部 -->
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="header-left">
|
|
|
+ <el-icon class="header-icon"><Monitor /></el-icon>
|
|
|
+ <span class="header-title">设备列表</span>
|
|
|
+ </div>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-tooltip content="刷新数据" placement="top">
|
|
|
+ <el-button
|
|
|
+ size="default"
|
|
|
+ :icon="Refresh"
|
|
|
+ @click="getList"
|
|
|
+ :loading="isLoading"
|
|
|
+ circle
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip :content="isCardView ? '切换到表格视图' : '切换到卡片视图'" placement="top">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="default"
|
|
|
+ :icon="View"
|
|
|
+ @click="toggleTableView"
|
|
|
+ circle
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <el-form-item label="运行状态">
|
|
|
- <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>
|
|
|
+ <!-- 默认空状态 -->
|
|
|
+ <div v-if="!queryParams.device_type" class="empty-state">
|
|
|
+ <div class="empty-content">
|
|
|
+ <div class="empty-icon">
|
|
|
+ <el-icon size="120"><Monitor /></el-icon>
|
|
|
+ </div>
|
|
|
+ <h3 class="empty-title">欢迎使用设备管理系统</h3>
|
|
|
+ <p class="empty-description">请选择设备类型开始查询设备信息</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <el-form-item>
|
|
|
- <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>
|
|
|
+ <!-- 表格视图 -->
|
|
|
+ <div v-else-if="!isCardView" class="table-view">
|
|
|
+ <el-table
|
|
|
+ :data="deviceList"
|
|
|
+ v-loading="isLoading"
|
|
|
+ :height="tableHeight"
|
|
|
+ style="width: 100%"
|
|
|
+ @row-click="handleRowClick"
|
|
|
+ empty-text="暂无设备数据"
|
|
|
+ border
|
|
|
+ :row-class-name="tableRowClassName"
|
|
|
+ highlight-current-row
|
|
|
+ >
|
|
|
+ <el-table-column label="设备名称" width="200" fixed="left">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="device-info">
|
|
|
+ <div class="device-name">
|
|
|
+ <el-link
|
|
|
+ type="primary"
|
|
|
+ @click.stop="handleRowClick(scope.row)"
|
|
|
+ >
|
|
|
+ {{ scope.row.deviceName }}
|
|
|
+ </el-link>
|
|
|
+ </div>
|
|
|
+ <div class="device-type">{{ deviceTypes[queryParams.device_type] }}</div>
|
|
|
+ </div>
|
|
|
</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>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 动态列 -->
|
|
|
+ <el-table-column
|
|
|
+ v-for="column in dynamicColumns"
|
|
|
+ :key="column.prop"
|
|
|
+ :label="column.label"
|
|
|
+ :prop="column.prop"
|
|
|
+ :width="column.width"
|
|
|
+ align="center"
|
|
|
+ show-overflow-tooltip
|
|
|
+ >
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="cell-content">
|
|
|
+ <div
|
|
|
+ :class="['status-indicator', getCellClass(column.prop, scope.row[column.prop])]"
|
|
|
+ v-if="column.type === 'status'"
|
|
|
+ >
|
|
|
+ <el-tag
|
|
|
+ :type="scope.row[column.prop] === '是' ? 'success' : 'info'"
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ <el-icon v-if="scope.row[column.prop] === '是'"><CircleCheck /></el-icon>
|
|
|
+ <el-icon v-else><CircleClose /></el-icon>
|
|
|
+ {{ scope.row[column.prop] }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-else-if="column.type === 'number'"
|
|
|
+ :class="['number-display', getNumberClass(column.prop, scope.row[column.prop])]"
|
|
|
+ >
|
|
|
+ <span class="number-value">{{ formatNumber(scope.row[column.prop]) }}</span>
|
|
|
+ <span class="unit" v-if="column.unit">{{ column.unit }}</span>
|
|
|
+ </div>
|
|
|
+ <span v-else class="text-value">{{ scope.row[column.prop] }}</span>
|
|
|
+ </div>
|
|
|
</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>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="操作" width="200" fixed="right" align="center">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click.stop="handleView(scope.row)"
|
|
|
+ icon="View"
|
|
|
+ >
|
|
|
+ 详情
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ size="small"
|
|
|
+ @click.stop="handleViewConfiguration(scope.row)"
|
|
|
+ icon="Setting"
|
|
|
+ >
|
|
|
+ 组态信息
|
|
|
+ </el-button>
|
|
|
</template>
|
|
|
- </el-statistic>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 数据表格区域 -->
|
|
|
- <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 v-else class="card-view">
|
|
|
+ <div class="cards-grid">
|
|
|
+ <div
|
|
|
+ v-for="device in deviceList"
|
|
|
+ :key="device.deviceName"
|
|
|
+ class="device-card-wrapper"
|
|
|
+ >
|
|
|
+ <el-card class="device-card" @click="handleRowClick(device)">
|
|
|
+ <template #header>
|
|
|
+ <div class="device-card-header">
|
|
|
+ <div class="device-title">
|
|
|
+ <div class="device-avatar-large">
|
|
|
+ <el-icon class="device-type-icon"><Monitor /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="device-info">
|
|
|
+ <h4 class="device-name">{{ device.deviceName }}</h4>
|
|
|
+ <p class="device-type">{{ deviceTypes[queryParams.device_type] }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-tag
|
|
|
+ :type="getDeviceStatusType(device)"
|
|
|
+ size="large"
|
|
|
+ effect="dark"
|
|
|
+ >
|
|
|
+ {{ getDeviceStatus(device) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="device-card-content">
|
|
|
+ <div class="parameters-grid">
|
|
|
+ <div
|
|
|
+ v-for="(value, key) in getCardDisplayData(device)"
|
|
|
+ :key="key"
|
|
|
+ class="parameter-item"
|
|
|
+ >
|
|
|
+ <div class="parameter-label">{{ key }}</div>
|
|
|
+ <div class="parameter-value" :class="getCellClass(key, value)">
|
|
|
+ {{ formatCardValue(key, value) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="device-card-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="default"
|
|
|
+ @click.stop="handleView(device)"
|
|
|
+ >
|
|
|
+ <el-icon><View /></el-icon>
|
|
|
+ 查看详情
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ size="default"
|
|
|
+ @click.stop="handleViewConfiguration(device)"
|
|
|
+ >
|
|
|
+ <el-icon><Setting /></el-icon>
|
|
|
+ 组态信息
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-card>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </template>
|
|
|
|
|
|
- <el-table
|
|
|
- :data="deviceList"
|
|
|
- border
|
|
|
- v-loading="isLoading"
|
|
|
- row-key="deviceId"
|
|
|
- @row-click="handleRowClick"
|
|
|
- :height="tableHeight"
|
|
|
- style="width: 100%"
|
|
|
- >
|
|
|
- <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">
|
|
|
- <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="140" show-overflow-tooltip />
|
|
|
- <el-table-column label="安装位置" align="center" min-width="200">
|
|
|
- <template #default="scope">
|
|
|
- <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">
|
|
|
- <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="180">
|
|
|
- <template #default="scope">
|
|
|
- <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>
|
|
|
+ <div v-if="deviceList.length === 0 && !isLoading" class="empty-result">
|
|
|
+ <el-empty description="暂无设备数据">
|
|
|
+ <template #image>
|
|
|
+ <div class="empty-image">
|
|
|
+ <el-icon size="80"><Monitor /></el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-empty>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
<!-- 分页组件 -->
|
|
|
- <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"
|
|
|
- layout="total, sizes, prev, pager, next, jumper"
|
|
|
- @size-change="handleSizeChange"
|
|
|
- @current-change="handlePageChange"
|
|
|
- class="pagination-container"
|
|
|
- />
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- 详情弹窗 -->
|
|
|
+ <div v-show="total > 0" class="pagination-container">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="queryParams.pageNum"
|
|
|
+ v-model:page-size="queryParams.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ background
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备详情弹窗 -->
|
|
|
<el-dialog
|
|
|
v-model="detailDialogOpen"
|
|
|
title="设备详情"
|
|
|
width="900px"
|
|
|
+ top="5vh"
|
|
|
destroy-on-close
|
|
|
+ class="detail-dialog"
|
|
|
>
|
|
|
- <el-tabs v-model="activeTab" type="card">
|
|
|
+ <div v-if="currentDevice" class="device-detail">
|
|
|
<!-- 基本信息 -->
|
|
|
- <el-tab-pane label="基本信息" name="basic">
|
|
|
- <el-descriptions :column="2" border>
|
|
|
- <el-descriptions-item label="设备ID">
|
|
|
- <el-tag>{{ detailDevice.deviceId }}</el-tag>
|
|
|
+ <div class="detail-section">
|
|
|
+ <h3>基本信息</h3>
|
|
|
+ <el-descriptions :column="3" border>
|
|
|
+ <el-descriptions-item label="设备名称" :span="2">
|
|
|
+ <el-tag type="primary" size="large" effect="dark">
|
|
|
+ {{ currentDevice.deviceName }}
|
|
|
+ </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>
|
|
|
+ <span>{{ deviceTypes[queryParams.device_type] || '未知' }}</span>
|
|
|
</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 label="设备状态" :span="3">
|
|
|
+ <el-tag :type="getDeviceStatusType(currentDevice)" size="large" effect="dark">
|
|
|
+ <el-icon style="margin-right: 4px;">
|
|
|
+ <CircleCheck v-if="getDeviceStatus(currentDevice) === '运行中'" />
|
|
|
+ <Warning v-else-if="getDeviceStatus(currentDevice) === '故障'" />
|
|
|
+ <Monitor v-else />
|
|
|
+ </el-icon>
|
|
|
+ {{ getDeviceStatus(currentDevice) }}
|
|
|
+ </el-tag>
|
|
|
</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">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 参数表格 -->
|
|
|
+ <div class="detail-section">
|
|
|
+ <h3>设备参数</h3>
|
|
|
+ <el-table
|
|
|
+ :data="getDetailTableData(currentDevice)"
|
|
|
+ border
|
|
|
+ max-height="450"
|
|
|
+ >
|
|
|
+ <el-table-column label="参数名称" prop="name" width="200">
|
|
|
<template #default="scope">
|
|
|
- {{ formatTime(scope.row.time) }}
|
|
|
+ <div class="param-name">
|
|
|
+ <el-icon class="param-icon"><Setting /></el-icon>
|
|
|
+ <span>{{ scope.row.name }}</span>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作类型" prop="type" width="120">
|
|
|
+ <el-table-column label="当前值" prop="value" align="center">
|
|
|
<template #default="scope">
|
|
|
- <el-tag :type="getHistoryTypeTag(scope.row.type)" size="small">
|
|
|
- {{ scope.row.type }}
|
|
|
- </el-tag>
|
|
|
+ <div class="param-value-cell">
|
|
|
+ <span :class="['param-value', getCellClass(scope.row.name, scope.row.value)]">
|
|
|
+ {{ formatDetailValue(scope.row.name, scope.row.value) }}
|
|
|
+ </span>
|
|
|
+ <span v-if="scope.row.unit" class="param-unit">{{ scope.row.unit }}</span>
|
|
|
+ </div>
|
|
|
</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">
|
|
|
+ <el-table-column label="状态" prop="status" width="120" align="center">
|
|
|
<template #default="scope">
|
|
|
- <el-tag :type="scope.row.result === '成功' ? 'success' : 'danger'" size="small">
|
|
|
- {{ scope.row.result }}
|
|
|
+ <el-tag
|
|
|
+ :type="scope.row.status === '正常' ? 'success' : 'warning'"
|
|
|
+ size="default"
|
|
|
+ effect="dark"
|
|
|
+ >
|
|
|
+ <el-icon style="margin-right: 4px;">
|
|
|
+ <CircleCheck v-if="scope.row.status === '正常'" />
|
|
|
+ <Warning v-else />
|
|
|
+ </el-icon>
|
|
|
+ {{ scope.row.status }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
- </el-tab-pane>
|
|
|
- </el-tabs>
|
|
|
-
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<template #footer>
|
|
|
- <el-button @click="detailDialogOpen = false">关闭</el-button>
|
|
|
- <el-button type="primary" @click="saveDeviceInfo">保存</el-button>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="detailDialogOpen = false">关闭</el-button>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
- <!-- 3D查看弹窗 -->
|
|
|
+ <!-- 组态信息弹窗 -->
|
|
|
<el-dialog
|
|
|
- v-model="view3DDialogOpen"
|
|
|
- :title="`3D视图 - ${current3DDevice.deviceName}`"
|
|
|
- width="90%"
|
|
|
- top="5vh"
|
|
|
- destroy-on-close
|
|
|
+ v-model="configurationDialogOpen"
|
|
|
+ :title="`组态信息 - ${currentConfigDevice?.deviceName || ''}`"
|
|
|
+ top="16vh"
|
|
|
>
|
|
|
- <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 class="configuration-iframe-container">
|
|
|
+ <iframe
|
|
|
+ v-if="currentVideoUrl"
|
|
|
+ :src="currentVideoUrl"
|
|
|
+ class="configuration-iframe"
|
|
|
+ frameborder="0"
|
|
|
+ allowfullscreen
|
|
|
+ ></iframe>
|
|
|
</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>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="configurationDialogOpen = false">关闭</el-button>
|
|
|
+ <el-button type="primary" @click="refreshConfiguration">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ 刷新
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup>
|
|
|
-import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
|
|
-import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
+<script setup name="BuildingEquipmentMonitoring">
|
|
|
+import { ref, reactive, onMounted } from 'vue';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
import {
|
|
|
Search,
|
|
|
Refresh,
|
|
|
- Download,
|
|
|
Monitor,
|
|
|
CircleCheck,
|
|
|
- Warning,
|
|
|
CircleClose,
|
|
|
- Plus,
|
|
|
View,
|
|
|
- Operation,
|
|
|
- Delete,
|
|
|
- Location,
|
|
|
- VideoPlay,
|
|
|
- VideoPause,
|
|
|
- FullScreen
|
|
|
+ Setting,
|
|
|
+ Warning,
|
|
|
+ Close
|
|
|
} from '@element-plus/icons-vue';
|
|
|
-import { getBuildingEquipmentMonitoringList } from "@/api/buildingEquipmentMonitoring/buildingEquipmentMonitoring";
|
|
|
-// import * as THREE from 'three'; // 如果使用Three.js
|
|
|
-
|
|
|
-// 状态管理
|
|
|
-const queryParams = reactive({
|
|
|
- deviceId: '',
|
|
|
- deviceName: '',
|
|
|
- deviceType: '',
|
|
|
- floor: '',
|
|
|
- area: '',
|
|
|
- status: '',
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 10,
|
|
|
- interfaceName: "三维设备监控中心"
|
|
|
-});
|
|
|
+import axios from 'axios';
|
|
|
|
|
|
+// 响应式数据
|
|
|
+const deviceTypes = ref({});
|
|
|
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 currentDevice = ref({});
|
|
|
+const dynamicColumns = ref([]);
|
|
|
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++;
|
|
|
- });
|
|
|
+const isCardView = ref(false);
|
|
|
|
|
|
- return stats;
|
|
|
-});
|
|
|
+// 组态信息相关
|
|
|
+const configurationDialogOpen = ref(false);
|
|
|
+const currentConfigDevice = ref(null);
|
|
|
+const currentVideoUrl = ref('');
|
|
|
|
|
|
-// 快速控制命令
|
|
|
-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 queryParams = reactive({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ device_type: ''
|
|
|
});
|
|
|
|
|
|
-// 远程控制表单
|
|
|
-const remoteControlForm = reactive({
|
|
|
- command: '',
|
|
|
- delay: 0,
|
|
|
- remark: ''
|
|
|
+// 生命周期
|
|
|
+onMounted(() => {
|
|
|
+ loadDeviceTypes();
|
|
|
+ calculateTableHeight();
|
|
|
+ window.addEventListener('resize', calculateTableHeight);
|
|
|
});
|
|
|
|
|
|
-// 历史记录数据
|
|
|
-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; // 考虑统计卡片的高度
|
|
|
+ tableHeight.value = windowHeight - 400;
|
|
|
};
|
|
|
|
|
|
-onMounted(() => {
|
|
|
- calculateTableHeight();
|
|
|
- window.addEventListener('resize', calculateTableHeight);
|
|
|
- getList();
|
|
|
- // 初始化3D场景
|
|
|
- // init3DScene();
|
|
|
-});
|
|
|
-
|
|
|
-onUnmounted(() => {
|
|
|
- window.removeEventListener('resize', calculateTableHeight);
|
|
|
- // 清理3D资源
|
|
|
- // cleanup3DScene();
|
|
|
-});
|
|
|
-
|
|
|
-// 获取数据
|
|
|
-const getList = async () => {
|
|
|
- isLoading.value = true;
|
|
|
+// 加载设备类型
|
|
|
+const loadDeviceTypes = async () => {
|
|
|
try {
|
|
|
- const response = await getBuildingEquipmentMonitoringList(queryParams);
|
|
|
- let filteredData = response.data.list || [];
|
|
|
-
|
|
|
- // 应用过滤条件
|
|
|
- if (queryParams.deviceId) {
|
|
|
- filteredData = filteredData.filter(item =>
|
|
|
- item.deviceId.toLowerCase().includes(queryParams.deviceId.toLowerCase())
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- if (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);
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/intell/deviceType`);
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ deviceTypes.value = response.data.data;
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('加载设备类型失败');
|
|
|
+ console.error(error);
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- if (queryParams.area) {
|
|
|
- filteredData = filteredData.filter(item =>
|
|
|
- item.area.toLowerCase().includes(queryParams.area.toLowerCase())
|
|
|
- );
|
|
|
- }
|
|
|
+// 获取设备列表
|
|
|
+const getList = async () => {
|
|
|
+ if (!queryParams.device_type) {
|
|
|
+ ElMessage.warning('请先选择设备类型');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (queryParams.status) {
|
|
|
- filteredData = filteredData.filter(item => item.status === queryParams.status);
|
|
|
+ isLoading.value = true;
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ pageNum: queryParams.pageNum,
|
|
|
+ pageSize: queryParams.pageSize,
|
|
|
+ device_type: queryParams.device_type
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/intell/getDevices`, { params });
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const { data, total: totalCount } = response.data;
|
|
|
+ total.value = totalCount;
|
|
|
+
|
|
|
+ // 转换数据格式
|
|
|
+ const convertedData = Object.entries(data).map(([deviceName, deviceData]) => ({
|
|
|
+ deviceName,
|
|
|
+ ...deviceData
|
|
|
+ }));
|
|
|
+
|
|
|
+ deviceList.value = convertedData;
|
|
|
+ generateDynamicColumns(convertedData);
|
|
|
}
|
|
|
-
|
|
|
- // 添加额外的模拟数据
|
|
|
- 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;
|
|
|
-
|
|
|
- // 分页处理
|
|
|
- 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);
|
|
|
} finally {
|
|
|
isLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 搜索
|
|
|
-const handleSearch = () => {
|
|
|
- queryParams.pageNum = 1;
|
|
|
- getList();
|
|
|
+// 获取组态信息 - 简化版本,只设置URL
|
|
|
+const getConfigurationInfo = async (device) => {
|
|
|
+ // 构建组态信息URL
|
|
|
+ currentVideoUrl.value = `${__LOCAL_API__}/intell/configurationDiagram?id=${queryParams.device_type}-${device.deviceName}`;
|
|
|
};
|
|
|
|
|
|
-// 重置
|
|
|
-const resetSearch = () => {
|
|
|
- Object.keys(queryParams).forEach(key => {
|
|
|
- if (key !== 'pageNum' && key !== 'pageSize' && key !== 'interfaceName') {
|
|
|
- queryParams[key] = '';
|
|
|
+// 刷新组态信息
|
|
|
+const refreshConfiguration = () => {
|
|
|
+ if (currentConfigDevice.value && currentVideoUrl.value) {
|
|
|
+ // 通过重新设置src来刷新iframe
|
|
|
+ const iframe = document.querySelector('.configuration-iframe');
|
|
|
+ if (iframe) {
|
|
|
+ const currentSrc = iframe.src;
|
|
|
+ iframe.src = '';
|
|
|
+ setTimeout(() => {
|
|
|
+ iframe.src = currentSrc;
|
|
|
+ }, 100);
|
|
|
}
|
|
|
- });
|
|
|
- queryParams.pageNum = 1;
|
|
|
- getList();
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
-// 分页
|
|
|
-const handlePageChange = (newPage) => {
|
|
|
- queryParams.pageNum = newPage;
|
|
|
- getList();
|
|
|
-};
|
|
|
+// 生成动态表格列
|
|
|
+const generateDynamicColumns = (data) => {
|
|
|
+ if (data.length === 0) {
|
|
|
+ dynamicColumns.value = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
-const handleSizeChange = (newSize) => {
|
|
|
- queryParams.pageSize = newSize;
|
|
|
- queryParams.pageNum = 1;
|
|
|
- getList();
|
|
|
-};
|
|
|
+ const columns = [];
|
|
|
+ const firstDevice = data[0];
|
|
|
|
|
|
-// 行点击事件
|
|
|
-const handleRowClick = (row) => {
|
|
|
- detailDevice.value = JSON.parse(JSON.stringify(row));
|
|
|
- detailDialogOpen.value = true;
|
|
|
- activeTab.value = 'basic';
|
|
|
+ Object.keys(firstDevice).forEach(key => {
|
|
|
+ if (key === 'deviceName') return;
|
|
|
+
|
|
|
+ const column = {
|
|
|
+ prop: key,
|
|
|
+ label: key,
|
|
|
+ width: getColumnWidth(key),
|
|
|
+ type: getColumnType(key),
|
|
|
+ unit: getColumnUnit(key)
|
|
|
+ };
|
|
|
+
|
|
|
+ columns.push(column);
|
|
|
+ });
|
|
|
+
|
|
|
+ dynamicColumns.value = columns;
|
|
|
};
|
|
|
|
|
|
-// 新增设备
|
|
|
-const handleAdd = () => {
|
|
|
- ElMessage.info('新增设备功能开发中...');
|
|
|
+// 获取列宽度
|
|
|
+const getColumnWidth = (columnName) => {
|
|
|
+ const widthMap = {
|
|
|
+ '冬夏季模式': 120,
|
|
|
+ '运行状态': 120,
|
|
|
+ '自动状态': 120,
|
|
|
+ '故障报警': 120,
|
|
|
+ '过滤网压差': 120,
|
|
|
+ '防冻开关': 120,
|
|
|
+ '风机压差': 120,
|
|
|
+ '送风温度': 120,
|
|
|
+ '回风温度': 120,
|
|
|
+ '新风温度': 120,
|
|
|
+ '送风湿度': 120,
|
|
|
+ '回风二氧化碳': 140,
|
|
|
+ '水阀反馈': 120,
|
|
|
+ '回风阀反馈': 120,
|
|
|
+ '新风阀反馈': 120
|
|
|
+ };
|
|
|
+ return widthMap[columnName] || 130;
|
|
|
};
|
|
|
|
|
|
-// 查看3D
|
|
|
-const handleView3D = (row) => {
|
|
|
- current3DDevice.value = row;
|
|
|
- view3DDialogOpen.value = true;
|
|
|
- // 初始化3D模型
|
|
|
- setTimeout(() => {
|
|
|
- init3DModel(row);
|
|
|
- }, 100);
|
|
|
+// 获取列类型
|
|
|
+const getColumnType = (columnName) => {
|
|
|
+ const statusColumns = ['冬夏季模式', '运行状态', '自动状态', '故障报警', '过滤网压差', '防冻开关', '风机压差'];
|
|
|
+ const numberColumns = ['送风温度', '回风温度', '新风温度', '送风湿度', '回风二氧化碳', '水阀反馈', '回风阀反馈', '新风阀反馈'];
|
|
|
+
|
|
|
+ if (statusColumns.includes(columnName)) return 'status';
|
|
|
+ if (numberColumns.includes(columnName)) return 'number';
|
|
|
+ return 'text';
|
|
|
};
|
|
|
|
|
|
-// 控制设备
|
|
|
-const handleControl = (row) => {
|
|
|
- currentControlDevice.value = row;
|
|
|
- controlDialogOpen.value = true;
|
|
|
- remoteControlForm.command = '';
|
|
|
- remoteControlForm.delay = 0;
|
|
|
- remoteControlForm.remark = '';
|
|
|
+// 获取列单位
|
|
|
+const getColumnUnit = (columnName) => {
|
|
|
+ const unitMap = {
|
|
|
+ '送风温度': '°C',
|
|
|
+ '回风温度': '°C',
|
|
|
+ '新风温度': '°C',
|
|
|
+ '送风湿度': '%',
|
|
|
+ '回风二氧化碳': 'ppm',
|
|
|
+ '水阀反馈': '%',
|
|
|
+ '回风阀反馈': '%',
|
|
|
+ '新风阀反馈': '%'
|
|
|
+ };
|
|
|
+ return unitMap[columnName] || '';
|
|
|
};
|
|
|
|
|
|
-// 删除设备
|
|
|
-const handleDelete = async (row) => {
|
|
|
- try {
|
|
|
- await ElMessageBox.confirm(
|
|
|
- `确定要删除设备 "${row.deviceName}" 吗?`,
|
|
|
- '删除确认',
|
|
|
- {
|
|
|
- confirmButtonText: '确定',
|
|
|
- cancelButtonText: '取消',
|
|
|
- type: 'warning',
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 模拟删除操作
|
|
|
- ElMessage.success('删除成功');
|
|
|
- getList();
|
|
|
- } catch {
|
|
|
- ElMessage.info('已取消删除');
|
|
|
+// 获取单元格样式类
|
|
|
+const getCellClass = (columnName, value) => {
|
|
|
+ const type = getColumnType(columnName);
|
|
|
+
|
|
|
+ if (type === 'status') {
|
|
|
+ return value === '是' ? 'status-active' : 'status-inactive';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'number') {
|
|
|
+ const numValue = parseFloat(value);
|
|
|
+ if (isNaN(numValue)) return 'value-invalid';
|
|
|
+ if (numValue === 0) return 'value-zero';
|
|
|
+ if (columnName.includes('温度') && (numValue < 18 || numValue > 35)) return 'value-warning';
|
|
|
+ if (columnName.includes('湿度') && (numValue < 30 || numValue > 80)) return 'value-warning';
|
|
|
+ if (columnName.includes('反馈') && numValue > 95) return 'value-high';
|
|
|
+ return 'value-normal';
|
|
|
}
|
|
|
-};
|
|
|
|
|
|
-// 导出数据
|
|
|
-const exportData = () => {
|
|
|
- ElMessage.success('正在导出数据...');
|
|
|
- // 实际导出逻辑
|
|
|
+ return '';
|
|
|
};
|
|
|
|
|
|
-// 设备类型映射
|
|
|
-const mapDeviceType = (type) => {
|
|
|
- const typeMap = {
|
|
|
- 'CONTROLLER': '控制器',
|
|
|
- 'SENSOR': '传感器',
|
|
|
- 'ACTUATOR': '执行器',
|
|
|
- 'BROADCASTER': '广播设备',
|
|
|
- 'MONITOR': '监控设备',
|
|
|
- 'ALARM': '报警设备',
|
|
|
- 'INPUT': '输入设备',
|
|
|
- };
|
|
|
- return typeMap[type] || type;
|
|
|
+// 获取数字样式类
|
|
|
+const getNumberClass = (columnName, value) => {
|
|
|
+ const numValue = parseFloat(value);
|
|
|
+ if (isNaN(numValue)) return 'number-invalid';
|
|
|
+ if (numValue === 0) return 'number-zero';
|
|
|
+ if (numValue > 90) return 'number-high';
|
|
|
+ return 'number-normal';
|
|
|
};
|
|
|
|
|
|
-// 获取设备类型标签样式
|
|
|
-const getDeviceTypeTag = (type) => {
|
|
|
- const tagMap = {
|
|
|
- 'CONTROLLER': 'primary',
|
|
|
- 'SENSOR': 'success',
|
|
|
- 'ACTUATOR': 'warning',
|
|
|
- 'BROADCASTER': 'info',
|
|
|
- 'MONITOR': '',
|
|
|
- 'ALARM': 'danger',
|
|
|
- 'INPUT': ''
|
|
|
- };
|
|
|
- return tagMap[type] || '';
|
|
|
+// 格式化数字
|
|
|
+const formatNumber = (value) => {
|
|
|
+ const num = parseFloat(value);
|
|
|
+ if (isNaN(num)) return value;
|
|
|
+ return num.toFixed(2);
|
|
|
};
|
|
|
|
|
|
-// 获取状态样式类
|
|
|
-const getStatusClass = (status) => {
|
|
|
- const classMap = {
|
|
|
- '正常': 'status-normal',
|
|
|
- '警告': 'status-warning',
|
|
|
- '故障': 'status-error',
|
|
|
- '离线': 'status-offline'
|
|
|
- };
|
|
|
- return classMap[status] || 'status-offline';
|
|
|
+// 获取设备状态
|
|
|
+const getDeviceStatus = (device) => {
|
|
|
+ if (device.故障报警 === '是') return '故障';
|
|
|
+ if (device.运行状态 === '是') return '运行中';
|
|
|
+ if (device.自动状态 === '是') return '自动';
|
|
|
+ return '待机';
|
|
|
};
|
|
|
|
|
|
-// 获取状态标签类型
|
|
|
-const getStatusTagType = (status) => {
|
|
|
+// 获取设备状态类型
|
|
|
+const getDeviceStatusType = (device) => {
|
|
|
+ const status = getDeviceStatus(device);
|
|
|
const typeMap = {
|
|
|
- '正常': 'success',
|
|
|
- '警告': 'warning',
|
|
|
'故障': 'danger',
|
|
|
- '离线': 'info'
|
|
|
+ '运行中': 'success',
|
|
|
+ '自动': 'primary',
|
|
|
+ '待机': 'info'
|
|
|
};
|
|
|
return typeMap[status] || 'info';
|
|
|
};
|
|
|
|
|
|
-// 获取历史记录类型标签
|
|
|
-const getHistoryTypeTag = (type) => {
|
|
|
- const typeMap = {
|
|
|
- '状态变更': 'primary',
|
|
|
- '参数调整': 'warning',
|
|
|
- '告警处理': 'danger',
|
|
|
- '维护记录': 'success'
|
|
|
- };
|
|
|
- return typeMap[type] || '';
|
|
|
-};
|
|
|
+// 获取卡片显示数据
|
|
|
+const getCardDisplayData = (device) => {
|
|
|
+ const importantKeys = ['运行状态', '故障报警', '送风温度', '水阀反馈', '自动状态', '过滤网压差'];
|
|
|
+ const result = {};
|
|
|
|
|
|
-// 格式化时间
|
|
|
-const formatTime = (time) => {
|
|
|
- if (!time) return '-';
|
|
|
- const date = new Date(time);
|
|
|
- return date.toLocaleString('zh-CN');
|
|
|
-};
|
|
|
+ importantKeys.forEach(key => {
|
|
|
+ if (device[key] !== undefined) {
|
|
|
+ result[key] = device[key];
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
-// 格式化日期
|
|
|
-const formatDate = (date) => {
|
|
|
- if (!date) return '-';
|
|
|
- const d = new Date(date);
|
|
|
- return d.toLocaleDateString('zh-CN');
|
|
|
-};
|
|
|
+ const allKeys = Object.keys(device).filter(key => key !== 'deviceName');
|
|
|
+ let count = Object.keys(result).length;
|
|
|
|
|
|
-// 格式化JSON
|
|
|
-const formatJson = (json) => {
|
|
|
- try {
|
|
|
- const obj = JSON.parse(json || '{}');
|
|
|
- return JSON.stringify(obj, null, 2);
|
|
|
- } catch {
|
|
|
- return json || '{}';
|
|
|
+ for (const key of allKeys) {
|
|
|
+ if (count >= 6) break;
|
|
|
+ if (!result[key]) {
|
|
|
+ result[key] = device[key];
|
|
|
+ count++;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ return result;
|
|
|
};
|
|
|
|
|
|
-// 发送控制命令
|
|
|
-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 formatCardValue = (key, value) => {
|
|
|
+ const type = getColumnType(key);
|
|
|
+ if (type === 'number') {
|
|
|
+ const unit = getColumnUnit(key);
|
|
|
+ return `${formatNumber(value)}${unit}`;
|
|
|
}
|
|
|
+ return value;
|
|
|
};
|
|
|
|
|
|
-// 处理模式变更
|
|
|
-const handleModeChange = (value) => {
|
|
|
- ElMessage.info(`已切换到${value === 'auto' ? '自动' : value === 'manual' ? '手动' : '定时'}模式`);
|
|
|
-};
|
|
|
+// 获取详情表格数据
|
|
|
+const getDetailTableData = (device) => {
|
|
|
+ const data = [];
|
|
|
+ Object.entries(device).forEach(([key, value]) => {
|
|
|
+ if (key === 'deviceName') return;
|
|
|
+
|
|
|
+ const type = getColumnType(key);
|
|
|
+ let status = '正常';
|
|
|
+
|
|
|
+ if (type === 'status' && value === '是' && (key.includes('故障') || key.includes('压差'))) {
|
|
|
+ status = '异常';
|
|
|
+ } else if (type === 'number') {
|
|
|
+ const numValue = parseFloat(value);
|
|
|
+ if (isNaN(numValue) || numValue === 0) {
|
|
|
+ status = '异常';
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-// 处理功率调节
|
|
|
-const handlePowerChange = (value) => {
|
|
|
- ElMessage.info(`功率已调节至${value}%`);
|
|
|
-};
|
|
|
+ data.push({
|
|
|
+ name: key,
|
|
|
+ value: value,
|
|
|
+ unit: getColumnUnit(key),
|
|
|
+ status: status
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
-// 处理温度调节
|
|
|
-const handleTempChange = (value) => {
|
|
|
- ElMessage.info(`温度已设定为${value}°C`);
|
|
|
+ return data;
|
|
|
};
|
|
|
|
|
|
-// 执行远程控制
|
|
|
-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 formatDetailValue = (name, value) => {
|
|
|
+ const type = getColumnType(name);
|
|
|
+ if (type === 'number') {
|
|
|
+ return formatNumber(value);
|
|
|
}
|
|
|
+ return value;
|
|
|
};
|
|
|
|
|
|
-// 保存设备信息
|
|
|
-const saveDeviceInfo = () => {
|
|
|
- ElMessage.success('设备信息保存成功');
|
|
|
- detailDialogOpen.value = false;
|
|
|
+// 表格行样式
|
|
|
+const tableRowClassName = ({ row }) => {
|
|
|
+ if (row.故障报警 === '是') return 'warning-row';
|
|
|
+ if (row.运行状态 !== '是') return 'offline-row';
|
|
|
+ return '';
|
|
|
};
|
|
|
|
|
|
-// 3D模型相关方法
|
|
|
-const init3DModel = (device) => {
|
|
|
- // 这里可以初始化Three.js场景
|
|
|
- console.log('初始化3D模型:', device.deviceId);
|
|
|
+// 事件处理
|
|
|
+const handleSearch = () => {
|
|
|
+ queryParams.pageNum = 1;
|
|
|
+ getList();
|
|
|
};
|
|
|
|
|
|
-const load3DModel = () => {
|
|
|
- ElMessage.info('正在加载3D模型...');
|
|
|
- // 加载3D模型逻辑
|
|
|
+const resetSearch = () => {
|
|
|
+ queryParams.device_type = '';
|
|
|
+ queryParams.pageNum = 1;
|
|
|
+ deviceList.value = [];
|
|
|
+ dynamicColumns.value = [];
|
|
|
+ total.value = 0;
|
|
|
};
|
|
|
|
|
|
-const downloadModel = () => {
|
|
|
- if (detailDevice.value.modelPath) {
|
|
|
- ElMessage.success('开始下载模型文件');
|
|
|
- // 下载逻辑
|
|
|
+const handleDeviceTypeChange = () => {
|
|
|
+ queryParams.pageNum = 1;
|
|
|
+ if (queryParams.device_type) {
|
|
|
+ getList();
|
|
|
} else {
|
|
|
- ElMessage.warning('暂无模型文件');
|
|
|
+ deviceList.value = [];
|
|
|
+ dynamicColumns.value = [];
|
|
|
+ total.value = 0;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const resetView = () => {
|
|
|
- ElMessage.info('视角已重置');
|
|
|
- // 重置3D视角
|
|
|
+const handlePageChange = (newPage) => {
|
|
|
+ queryParams.pageNum = newPage;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+const handleSizeChange = (newSize) => {
|
|
|
+ queryParams.pageSize = newSize;
|
|
|
+ queryParams.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+const toggleTableView = () => {
|
|
|
+ isCardView.value = !isCardView.value;
|
|
|
};
|
|
|
|
|
|
-const toggleRotation = () => {
|
|
|
- rotationEnabled.value = !rotationEnabled.value;
|
|
|
- // 切换旋转状态
|
|
|
+const handleRowClick = (row) => {
|
|
|
+ currentDevice.value = row;
|
|
|
+ detailDialogOpen.value = true;
|
|
|
};
|
|
|
|
|
|
-const toggleWireframe = () => {
|
|
|
- wireframeMode.value = !wireframeMode.value;
|
|
|
- ElMessage.info(`已切换到${wireframeMode.value ? '线框' : '实体'}模式`);
|
|
|
- // 切换渲染模式
|
|
|
+const handleView = (device) => {
|
|
|
+ currentDevice.value = device;
|
|
|
+ detailDialogOpen.value = true;
|
|
|
};
|
|
|
|
|
|
-const fullscreen3D = () => {
|
|
|
- const viewer = document.querySelector('.three-d-viewer');
|
|
|
- if (viewer.requestFullscreen) {
|
|
|
- viewer.requestFullscreen();
|
|
|
- }
|
|
|
+// 查看组态信息
|
|
|
+const handleViewConfiguration = (device) => {
|
|
|
+ currentConfigDevice.value = device;
|
|
|
+ configurationDialogOpen.value = true;
|
|
|
+ getConfigurationInfo(device);
|
|
|
};
|
|
|
+
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
+/* ===== 基础样式 ===== */
|
|
|
.device-management-system {
|
|
|
padding: 20px;
|
|
|
- background-color: #f5f7fa;
|
|
|
- min-height: 100vh;
|
|
|
-}
|
|
|
-
|
|
|
-.filter-card {
|
|
|
- margin-bottom: 20px;
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: 90vh;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
+ line-height: 1.6;
|
|
|
+ animation: fadeIn 0.5s ease-in-out;
|
|
|
}
|
|
|
|
|
|
+/* ===== 搜索表单样式 ===== */
|
|
|
.search-form {
|
|
|
- padding: 10px 0;
|
|
|
-}
|
|
|
-
|
|
|
-/* 统计卡片 */
|
|
|
-.stats-row {
|
|
|
+ background: #fff;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
margin-bottom: 20px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ transition: box-shadow 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-.stats-card {
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
- transition: all 0.3s;
|
|
|
+.search-form:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
|
}
|
|
|
|
|
|
-.stats-card:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
- box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
|
|
|
+.search-form .el-form {
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
-.stats-card.normal :deep(.el-statistic__number) {
|
|
|
- color: #67c23a;
|
|
|
+.search-form .el-form-item {
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
-.stats-card.warning :deep(.el-statistic__number) {
|
|
|
- color: #e6a23c;
|
|
|
+.search-form .el-form-item__label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
}
|
|
|
|
|
|
-.stats-card.error :deep(.el-statistic__number) {
|
|
|
- color: #f56c6c;
|
|
|
+/* ===== 表格容器样式 ===== */
|
|
|
+.table-container {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ transition: box-shadow 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-.data-table {
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+.table-container:hover {
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
|
|
}
|
|
|
|
|
|
-.card-header {
|
|
|
+/* ===== 表格头部样式 ===== */
|
|
|
+.table-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
|
|
}
|
|
|
|
|
|
-.header-actions {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-/* 位置信息样式 */
|
|
|
-.location-info {
|
|
|
+.header-left {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- gap: 5px;
|
|
|
- font-weight: 500;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.location-detail {
|
|
|
- font-size: 12px;
|
|
|
- color: #909399;
|
|
|
- margin-top: 4px;
|
|
|
+.header-icon {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #409eff;
|
|
|
}
|
|
|
|
|
|
-/* 状态样式 */
|
|
|
-.status-wrapper {
|
|
|
+.header-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.header-actions {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- gap: 6px;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.status-dot {
|
|
|
- width: 8px;
|
|
|
- height: 8px;
|
|
|
- border-radius: 50%;
|
|
|
- display: inline-block;
|
|
|
- animation: blink 2s infinite;
|
|
|
+/* ===== 表格样式优化 ===== */
|
|
|
+.table-view {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格基础样式 */
|
|
|
+:deep(.el-table) {
|
|
|
+ border-radius: 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格头部样式 */
|
|
|
+:deep(.el-table th) {
|
|
|
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: 14px 12px;
|
|
|
+ border-bottom: 2px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格单元格样式 */
|
|
|
+:deep(.el-table td) {
|
|
|
+ padding: 12px;
|
|
|
+ border-bottom: 1px solid #f0f2f5;
|
|
|
+}
|
|
|
+
|
|
|
+/* 去掉隔行变色,统一背景色 */
|
|
|
+:deep(.el-table__row) {
|
|
|
+ background-color: #ffffff;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+/* 悬停效果 */
|
|
|
+:deep(.el-table__row:hover) {
|
|
|
+ background-color: #f0f9ff !important;
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+/* 选中行样式 - 加深颜色便于区分 */
|
|
|
+:deep(.el-table__row.current-row) {
|
|
|
+ background-color: #e6f4ff !important;
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__row.current-row:hover) {
|
|
|
+ background-color: #d4edff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格行状态样式 */
|
|
|
+:deep(.warning-row) {
|
|
|
+ background-color: #fef0f0 !important;
|
|
|
+ border-left: 4px solid #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.warning-row:hover) {
|
|
|
+ background-color: #fde2e2 !important;
|
|
|
}
|
|
|
|
|
|
-.status-normal {
|
|
|
- background-color: #67c23a;
|
|
|
+:deep(.offline-row) {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+ color: #909399;
|
|
|
}
|
|
|
|
|
|
-.status-warning {
|
|
|
- background-color: #e6a23c;
|
|
|
+:deep(.offline-row:hover) {
|
|
|
+ background-color: #ebeef5 !important;
|
|
|
}
|
|
|
|
|
|
-.status-error {
|
|
|
- background-color: #f56c6c;
|
|
|
+/* ===== 设备信息样式 ===== */
|
|
|
+.device-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 8px;
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-.status-offline {
|
|
|
- background-color: #909399;
|
|
|
- animation: none;
|
|
|
+.device-info:hover {
|
|
|
+ background: rgba(64, 158, 255, 0.05);
|
|
|
+ transform: translateY(-2px);
|
|
|
}
|
|
|
|
|
|
-@keyframes blink {
|
|
|
- 0%, 100% { opacity: 1; }
|
|
|
- 50% { opacity: 0.5; }
|
|
|
+.device-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
|
|
|
-.no-alarm {
|
|
|
+.device-type {
|
|
|
color: #909399;
|
|
|
font-size: 12px;
|
|
|
+ background: #f0f2f5;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: inline-block;
|
|
|
+ width: fit-content;
|
|
|
}
|
|
|
|
|
|
-.pagination-container {
|
|
|
- margin-top: 20px;
|
|
|
+/* ===== 单元格内容样式 ===== */
|
|
|
+.cell-content {
|
|
|
display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 4px;
|
|
|
}
|
|
|
|
|
|
-/* 详情弹窗样式 */
|
|
|
-.status-display {
|
|
|
- padding: 20px;
|
|
|
+.status-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
}
|
|
|
|
|
|
-.status-item {
|
|
|
+.number-display {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 15px;
|
|
|
- font-size: 14px;
|
|
|
+ gap: 2px;
|
|
|
}
|
|
|
|
|
|
-.status-item .label {
|
|
|
- width: 80px;
|
|
|
- color: #606266;
|
|
|
+.number-value {
|
|
|
+ font-weight: 600;
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-.alarm-info {
|
|
|
- padding: 10px;
|
|
|
+.number-value:hover {
|
|
|
+ transform: scale(1.05);
|
|
|
}
|
|
|
|
|
|
-.alarm-details {
|
|
|
- margin-top: 15px;
|
|
|
- font-size: 14px;
|
|
|
- line-height: 1.8;
|
|
|
- color: #606266;
|
|
|
+.unit {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 数值状态样式 ===== */
|
|
|
+.value-normal .number-value {
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.value-warning .number-value {
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.value-high .number-value {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.value-zero .number-value {
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.value-invalid .number-value {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 卡片视图样式 ===== */
|
|
|
+.card-view {
|
|
|
+ padding: 20px;
|
|
|
}
|
|
|
|
|
|
-/* 3D模型查看器 */
|
|
|
-.model-viewer {
|
|
|
+.cards-grid {
|
|
|
display: grid;
|
|
|
- grid-template-columns: 2fr 1fr;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
|
gap: 20px;
|
|
|
- height: 500px;
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
-.model-preview {
|
|
|
- background-color: #1a1a1a;
|
|
|
- border-radius: 4px;
|
|
|
+.device-card-wrapper {
|
|
|
+ animation: cardFadeIn 0.6s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card:hover {
|
|
|
+ transform: translateY(-4px);
|
|
|
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.device-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-avatar-large {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card-header .device-info {
|
|
|
+ flex: 1;
|
|
|
}
|
|
|
|
|
|
-.model-info {
|
|
|
- overflow-y: auto;
|
|
|
+.device-card-header .device-name {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
|
|
|
-.json-display {
|
|
|
- font-family: 'Consolas', 'Monaco', monospace;
|
|
|
+.device-card-header .device-type {
|
|
|
font-size: 12px;
|
|
|
- line-height: 1.5;
|
|
|
- color: #606266;
|
|
|
- background-color: #f5f7fa;
|
|
|
- padding: 10px;
|
|
|
- border-radius: 4px;
|
|
|
- overflow: auto;
|
|
|
- max-height: 200px;
|
|
|
+ color: #909399;
|
|
|
+ margin: 4px 0 0 0;
|
|
|
}
|
|
|
|
|
|
-/* 控制面板 */
|
|
|
-.control-panel {
|
|
|
- padding: 20px;
|
|
|
+.device-card-content {
|
|
|
+ padding: 0;
|
|
|
}
|
|
|
|
|
|
-.control-panel h4 {
|
|
|
- margin-bottom: 15px;
|
|
|
- color: #303133;
|
|
|
+.parameters-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
-.control-btn {
|
|
|
- width: 100%;
|
|
|
- margin-bottom: 10px;
|
|
|
+.parameter-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-.unit {
|
|
|
- margin-left: 10px;
|
|
|
+.parameter-item:hover {
|
|
|
+ background: #e3f2fd;
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.parameter-label {
|
|
|
+ font-size: 12px;
|
|
|
color: #909399;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
|
|
|
-/* 3D查看器 */
|
|
|
-.three-d-viewer {
|
|
|
- height: 70vh;
|
|
|
+.parameter-value {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card-actions {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 0;
|
|
|
}
|
|
|
|
|
|
-.viewer-toolbar {
|
|
|
- padding: 10px;
|
|
|
- background-color: #f5f7fa;
|
|
|
- border-bottom: 1px solid #e4e7ed;
|
|
|
+/* ===== 空状态样式 ===== */
|
|
|
+.empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 80px 20px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-content {
|
|
|
+ max-width: 400px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-icon {
|
|
|
+ color: #c0c4cc;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ animation: float 3s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-title {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0 0 12px 0;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-description {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin: 0 0 24px 0;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-result {
|
|
|
+ padding: 40px 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-image {
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 分页样式 ===== */
|
|
|
+.pagination-container {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding: 20px 24px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-top: 1px solid #f0f2f5;
|
|
|
}
|
|
|
|
|
|
-.viewer-container {
|
|
|
- flex: 1;
|
|
|
- background-color: #1a1a1a;
|
|
|
- position: relative;
|
|
|
+/* ===== 组态信息弹窗样式 ===== */
|
|
|
+.configuration-iframe-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 70vh; /* 调整高度 */
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 8px;
|
|
|
overflow: hidden;
|
|
|
+ background: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
-.viewer-container canvas {
|
|
|
+.configuration-iframe {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
+ border: none;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗样式覆盖 */
|
|
|
+:deep(.el-dialog) {
|
|
|
+ margin: 0 auto !important;
|
|
|
+ border-radius: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+:deep(.el-dialog__header) {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: #fff;
|
|
|
+ padding: 20px 24px;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+:deep(.el-dialog__title) {
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dialog__headerbtn .el-dialog__close) {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 组态弹窗特殊处理 */
|
|
|
+:deep(.el-dialog__body) {
|
|
|
+ padding: 16px !important; /* 减少内边距 */
|
|
|
+ max-height: none;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .configuration-iframe-container {
|
|
|
+ height: 80vh;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .configuration-iframe-container {
|
|
|
+ height: 75vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-dialog) {
|
|
|
+ width: 98% !important;
|
|
|
+ top: 1vh !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 480px) {
|
|
|
+ .configuration-iframe-container {
|
|
|
+ height: 70vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-dialog) {
|
|
|
+ width: 100% !important;
|
|
|
+ top: 0 !important;
|
|
|
+ margin: 0 !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 设备详情弹窗内容样式 ===== */
|
|
|
+.device-detail {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-section {
|
|
|
+ background: #f8f9fa;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-section h3 {
|
|
|
+ margin: 0 0 16px 0;
|
|
|
+ color: #303133;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-section h3::before {
|
|
|
+ content: '';
|
|
|
+ width: 4px;
|
|
|
+ height: 16px;
|
|
|
+ background: #409eff;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 参数表格样式 ===== */
|
|
|
+.param-name {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.param-icon {
|
|
|
+ color: #409eff;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.param-value-cell {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.param-value {
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.param-unit {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 按钮样式优化 ===== */
|
|
|
+.el-button {
|
|
|
+ border-radius: 6px;
|
|
|
+ font-weight: 500;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--primary {
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--primary:hover {
|
|
|
+ background: linear-gradient(135deg, #3a8ee6 0%, #337ecc 100%);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--success {
|
|
|
+ background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--success:hover {
|
|
|
+ background: linear-gradient(135deg, #5daf34 0%, #529b2e 100%);
|
|
|
+ box-shadow: 0 4px 12px rgba(103, 194, 58, 0.4);
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button[circle] {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button[circle]:hover {
|
|
|
+ transform: rotate(180deg) scale(1.1);
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 标签样式优化 ===== */
|
|
|
+.el-tag {
|
|
|
+ border-radius: 4px;
|
|
|
+ font-weight: 500;
|
|
|
+ border: none;
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 4px 8px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag:hover {
|
|
|
+ transform: scale(1.05);
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--success {
|
|
|
+ background: linear-gradient(135deg, #4CAF50 0%, #009688 100%);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--danger {
|
|
|
+ background: linear-gradient(135deg, #f56c6c 0%, #f25c5c 100%);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--warning {
|
|
|
+ background: linear-gradient(135deg, #e6a23c 0%, #d19e3c 100%);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--info {
|
|
|
+ background: linear-gradient(135deg, #673AB7 0%, #9E9E9E 100%);
|
|
|
+ color: #fff;
|
|
|
}
|
|
|
|
|
|
-.viewer-info {
|
|
|
- padding: 10px;
|
|
|
- background-color: #f5f7fa;
|
|
|
- border-top: 1px solid #e4e7ed;
|
|
|
+.el-tag--primary {
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
|
|
|
+ color: #fff;
|
|
|
}
|
|
|
|
|
|
-.mb-4 {
|
|
|
- margin-bottom: 16px;
|
|
|
+/* ===== 动画定义 ===== */
|
|
|
+@keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes cardFadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px) scale(0.95);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0) scale(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes dialogSlideIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: scale(0.8) translateY(-50px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1) translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes float {
|
|
|
+ 0%, 100% {
|
|
|
+ transform: translateY(0px);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: translateY(-10px);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-/* 响应式设计 */
|
|
|
+/* ===== 响应式设计 ===== */
|
|
|
@media (max-width: 1200px) {
|
|
|
- .stats-row {
|
|
|
- margin-bottom: 10px;
|
|
|
+ .device-management-system {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cards-grid {
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .configuration-iframe-container {
|
|
|
+ height: 70vh;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .device-management-system {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-header {
|
|
|
+ padding: 16px;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header-actions {
|
|
|
+ align-self: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .cards-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
- .model-viewer {
|
|
|
+ .parameters-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
- height: auto;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .parameter-item {
|
|
|
+ padding: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-card-actions {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-table td) {
|
|
|
+ padding: 8px 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-table th) {
|
|
|
+ padding: 8px 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-info,
|
|
|
+ .cell-content {
|
|
|
+ font-size: 11px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination-container {
|
|
|
+ padding: 16px;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-dialog) {
|
|
|
+ width: 95% !important;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-section {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .configuration-iframe-container {
|
|
|
+ height: 60vh;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 480px) {
|
|
|
+ .device-management-system {
|
|
|
+ padding: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form .el-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form .el-form-item {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form .el-input,
|
|
|
+ .search-form .el-select {
|
|
|
+ width: 100% !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-header {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header-title {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-state {
|
|
|
+ padding: 40px 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-title {
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-description {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-card {
|
|
|
+ margin: 0 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-card-header {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .configuration-iframe-container {
|
|
|
+ height: 50vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dialog-footer {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dialog-footer .el-button {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 滚动条样式 ===== */
|
|
|
+:deep(.el-dialog__body)::-webkit-scrollbar,
|
|
|
+:deep(.el-table__body-wrapper)::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dialog__body)::-webkit-scrollbar-track,
|
|
|
+:deep(.el-table__body-wrapper)::-webkit-scrollbar-track {
|
|
|
+ background: #f1f1f1;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dialog__body)::-webkit-scrollbar-thumb,
|
|
|
+:deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb {
|
|
|
+ background: #c1c1c1;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dialog__body)::-webkit-scrollbar-thumb:hover,
|
|
|
+:deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #a8a8a8;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 输入框和选择器样式优化 ===== */
|
|
|
+:deep(.el-input__wrapper) {
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input__wrapper:hover) {
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input__wrapper.is-focus) {
|
|
|
+ box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-select .el-input__wrapper) {
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 分页样式优化 ===== */
|
|
|
+:deep(.el-pagination) {
|
|
|
+ --el-pagination-button-color: #606266;
|
|
|
+ --el-pagination-hover-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-pagination .el-pager li) {
|
|
|
+ border-radius: 6px;
|
|
|
+ margin: 0 2px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-pagination .el-pager li:hover) {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-pagination .el-pager li.is-active) {
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
|
|
|
+ color: #fff;
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-pagination__total) {
|
|
|
+ margin-right: 16px;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-pagination__sizes) {
|
|
|
+ margin-right: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 描述列表样式优化 ===== */
|
|
|
+:deep(.el-descriptions) {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-descriptions__label) {
|
|
|
+ background: #fafbfc !important;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ border-right: 1px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-descriptions__content) {
|
|
|
+ background: #fff !important;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 表格固定列阴影 ===== */
|
|
|
+:deep(.el-table__fixed-right) {
|
|
|
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__fixed-left) {
|
|
|
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 工具提示样式 ===== */
|
|
|
+:deep(.el-tooltip__popper) {
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
+ border-radius: 6px;
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 加载动画优化 ===== */
|
|
|
+:deep(.el-loading-mask) {
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
+ backdrop-filter: blur(2px);
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-loading-spinner) {
|
|
|
+ animation: loadingRotate 1.5s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes loadingRotate {
|
|
|
+ from {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 表格边框优化 ===== */
|
|
|
+:deep(.el-table--border) {
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table--border::after) {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 空数据状态优化 ===== */
|
|
|
+:deep(.el-table__empty-block) {
|
|
|
+ padding: 60px 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table__empty-text) {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 焦点样式增强 ===== */
|
|
|
+.el-button:focus,
|
|
|
+.el-input:focus,
|
|
|
+.el-select:focus {
|
|
|
+ outline: 2px solid #409eff;
|
|
|
+ outline-offset: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 选择状态样式 ===== */
|
|
|
+::selection {
|
|
|
+ background: rgba(64, 158, 255, 0.3);
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 表单验证样式 ===== */
|
|
|
+:deep(.el-form-item.is-error .el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px #f56c6c inset;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-form-item.is-success .el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px #67c23a inset;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 性能优化 ===== */
|
|
|
+.device-management-system * {
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.device-card:hover,
|
|
|
+:deep(.el-table__row:hover),
|
|
|
+.parameter-item:hover {
|
|
|
+ will-change: transform;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 无障碍支持 ===== */
|
|
|
+@media (prefers-reduced-motion: reduce) {
|
|
|
+ *,
|
|
|
+ *::before,
|
|
|
+ *::after {
|
|
|
+ animation-duration: 0.01ms !important;
|
|
|
+ animation-iteration-count: 1 !important;
|
|
|
+ transition-duration: 0.01ms !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 高对比度模式 ===== */
|
|
|
+@media (prefers-contrast: high) {
|
|
|
+ .device-management-system {
|
|
|
+ background: #fff;
|
|
|
+ color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form,
|
|
|
+ .table-container,
|
|
|
+ .device-card {
|
|
|
+ border: 2px solid #000;
|
|
|
+ background: #fff;
|
|
|
}
|
|
|
|
|
|
- .model-preview {
|
|
|
- height: 300px;
|
|
|
+ .el-button--primary {
|
|
|
+ background: #000;
|
|
|
+ color: #fff;
|
|
|
+ border: 2px solid #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button--success {
|
|
|
+ background: #006400;
|
|
|
+ color: #fff;
|
|
|
+ border: 2px solid #006400;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-tag {
|
|
|
+ border: 1px solid #000;
|
|
|
+ color: #000;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-table__row.current-row) {
|
|
|
+ background-color: #000 !important;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .configuration-iframe-container {
|
|
|
+ border: 2px solid #000;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* 暗色主题支持 */
|
|
|
-@media (prefers-color-scheme: dark) {
|
|
|
+/* ===== 打印样式 ===== */
|
|
|
+@media print {
|
|
|
+ .search-form,
|
|
|
+ .pagination-container,
|
|
|
+ .header-actions,
|
|
|
+ .el-button,
|
|
|
+ .dialog-footer {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
.device-management-system {
|
|
|
- background-color: #1a1a1a;
|
|
|
+ background: #fff;
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-container {
|
|
|
+ box-shadow: none;
|
|
|
+ border: 1px solid #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-header {
|
|
|
+ background: #f5f7fa !important;
|
|
|
+ color: #000 !important;
|
|
|
+ border-bottom: 2px solid #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-table th) {
|
|
|
+ background: #f5f7fa !important;
|
|
|
+ color: #000 !important;
|
|
|
+ border: 1px solid #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-table td) {
|
|
|
+ background: #fff !important;
|
|
|
+ color: #000 !important;
|
|
|
+ border: 1px solid #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-table__row) {
|
|
|
+ background-color: #fff !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-card {
|
|
|
+ break-inside: avoid;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border: 1px solid #000;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 最终样式确保 ===== */
|
|
|
+.device-management-system {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保表格行选中状态优先级最高 */
|
|
|
+:deep(.el-table tbody tr.current-row > td) {
|
|
|
+ background-color: #e6f4ff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table tbody tr.current-row:hover > td) {
|
|
|
+ background-color: #d4edff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保警告行状态 */
|
|
|
+:deep(.el-table tbody tr.warning-row > td) {
|
|
|
+ background-color: #fef0f0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table tbody tr.warning-row:hover > td) {
|
|
|
+ background-color: #fde2e2 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保离线行状态 */
|
|
|
+:deep(.el-table tbody tr.offline-row > td) {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table tbody tr.offline-row:hover > td) {
|
|
|
+ background-color: #ebeef5 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保弹窗层级正确 */
|
|
|
+:deep(.el-overlay-dialog) {
|
|
|
+ z-index: 2000;
|
|
|
+ top: 10vh;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保按钮组在小屏幕上的布局 */
|
|
|
+@media (max-width: 600px) {
|
|
|
+ .device-card-actions {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
- .json-display {
|
|
|
- background-color: #2a2a2a;
|
|
|
- color: #e0e0e0;
|
|
|
+ .device-card-actions .el-button {
|
|
|
+ width: 100%;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* 最终确保所有交互元素的可访问性 */
|
|
|
+.el-button,
|
|
|
+.el-link,
|
|
|
+.device-card,
|
|
|
+.parameter-item {
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button:disabled,
|
|
|
+.el-link:disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保文本选择的一致性 */
|
|
|
+.device-name,
|
|
|
+.parameter-label,
|
|
|
+.parameter-value {
|
|
|
+ user-select: text;
|
|
|
+}
|
|
|
+
|
|
|
+/* 最终的全局重置确保 */
|
|
|
+* {
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+*:focus {
|
|
|
+ outline: none;
|
|
|
+}
|
|
|
+
|
|
|
+*:focus-visible {
|
|
|
+ outline: 2px solid #409eff;
|
|
|
+ outline-offset: 2px;
|
|
|
+}
|
|
|
</style>
|