|
|
@@ -1,388 +1,438 @@
|
|
|
-<!--监测阈值管理-->
|
|
|
<template>
|
|
|
<div class="threshold-management">
|
|
|
- <!-- 主内容区域 -->
|
|
|
<div class="main-content">
|
|
|
- <!-- 左侧:监测点列表 -->
|
|
|
<div class="point-list-panel">
|
|
|
<div class="panel-title">
|
|
|
- <span>监测点列表</span>
|
|
|
- <el-input v-model="searchKeyword" placeholder="搜索设备编号/名称" clearable :prefix-icon="Search" size="small" style="width: 180px" />
|
|
|
+ <span>设备列表</span>
|
|
|
+ <el-button :icon="Refresh" text @click="loadDeviceList">刷新</el-button>
|
|
|
</div>
|
|
|
- <div class="point-list">
|
|
|
+ <el-input
|
|
|
+ v-model="searchKeyword"
|
|
|
+ placeholder="搜索设备编号/名称"
|
|
|
+ clearable
|
|
|
+ :prefix-icon="Search"
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div v-loading="listLoading" class="point-list">
|
|
|
<div
|
|
|
- v-for="point in filteredPoints"
|
|
|
- :key="point.id"
|
|
|
- class="point-item"
|
|
|
- :class="{ active: selectedPoint?.id === point.id }"
|
|
|
- @click="selectPoint(point)"
|
|
|
+ v-for="point in filteredPoints"
|
|
|
+ :key="point.equipmentId"
|
|
|
+ class="point-item"
|
|
|
+ :class="{ active: selectedPoint?.equipmentId === point.equipmentId }"
|
|
|
+ @click="selectPoint(point)"
|
|
|
>
|
|
|
<div class="point-header">
|
|
|
- <span class="point-name">{{ point.name }}</span>
|
|
|
- <el-tag :type="getPointStatusType(point)" size="small">{{ point.status }}</el-tag>
|
|
|
+ <span class="point-name">{{ point.equipmentName || point.equipmentCode || '未命名设备' }}</span>
|
|
|
+ <el-tag :type="getAlarmTagType(point)" size="small">
|
|
|
+ {{ getAlarmText(point) }}
|
|
|
+ </el-tag>
|
|
|
</div>
|
|
|
<div class="point-location">
|
|
|
- <el-icon><Location /></el-icon> {{ point.location }}
|
|
|
+ <el-icon><Location /></el-icon>
|
|
|
+ <span>{{ point.equipmentLocation || '暂无位置' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="point-meta">
|
|
|
+ <span>编号:{{ point.equipmentCode || '-' }}</span>
|
|
|
+ <span>类型:{{ point.equipmentTypeName || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="point-status-row">
|
|
|
+ <el-tag size="small" effect="plain" :type="getOnlineTagType(point.equipmentStatus?.onlineStatus)">
|
|
|
+ {{ getOnlineText(point.equipmentStatus?.onlineStatus) }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag size="small" effect="plain" :type="getUseStatusTagType(point.equipmentStatus?.currentStatus)">
|
|
|
+ {{ getUseStatusText(point.equipmentStatus?.currentStatus) }}
|
|
|
+ </el-tag>
|
|
|
</div>
|
|
|
- <div class="point-device">{{ point.deviceModel }}</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <el-empty
|
|
|
+ v-if="!listLoading && !filteredPoints.length"
|
|
|
+ description="暂无井盖监测设备"
|
|
|
+ :image-size="120"
|
|
|
+ />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 右侧:阈值配置详情区 -->
|
|
|
- <div class="config-panel" v-if="selectedPoint">
|
|
|
- <!-- 设备基本信息卡片 -->
|
|
|
+ <div v-if="selectedPoint" v-loading="detailLoading" class="config-panel">
|
|
|
<div class="info-card">
|
|
|
<div class="card-title">
|
|
|
- <span><el-icon><Monitor /></el-icon> 监测设备基本信息</span>
|
|
|
+ <span><el-icon><Monitor /></el-icon> 设备基本信息</span>
|
|
|
</div>
|
|
|
<el-descriptions :column="2" border>
|
|
|
- <el-descriptions-item label="设备编号">{{ selectedPoint.deviceId }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备类型">{{ selectedPoint.deviceType }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备型号">{{ selectedPoint.deviceModel }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="生产厂商">{{ selectedPoint.manufacturer }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="安装日期">{{ selectedPoint.installDate }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="通讯协议">{{ selectedPoint.protocol }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="监测点ID">{{ selectedPoint.id }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="最近校准">{{ selectedPoint.lastCalibration }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备编号">{{ selectedDetail?.equipmentCode || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备名称">{{ selectedDetail?.equipmentName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备类型">{{ selectedDetail?.equipmentTypeName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备型号">{{ selectedDetail?.equipmentModel || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="生产厂商">{{ selectedDetail?.manufacturer || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装时间">{{ formatDateTime(selectedDetail?.installTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备位置">{{ selectedDetail?.equipmentLocation || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="维护人员">{{ selectedDetail?.maintainer || '-' }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 阈值配置区域 - 多参数标签页 -->
|
|
|
+ <div class="info-card">
|
|
|
+ <div class="card-title">
|
|
|
+ <span><el-icon><Warning /></el-icon> 最新监测状态</span>
|
|
|
+ </div>
|
|
|
+ <div class="status-grid">
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">在线状态</div>
|
|
|
+ <div class="status-value">{{ getOnlineText(selectedDetail?.equipmentStatus?.onlineStatus) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">报警状态</div>
|
|
|
+ <div class="status-value">{{ getAlarmText(selectedDetail) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">倾斜角度</div>
|
|
|
+ <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.tiltAngle, '°') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">电池电量</div>
|
|
|
+ <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.batteryLevel, '%') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">信号强度</div>
|
|
|
+ <div class="status-value">{{ appendUnit(selectedDetail?.manholeData?.signalStrength, '%') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">水浸状态</div>
|
|
|
+ <div class="status-value">{{ getBinaryAlarmText(selectedDetail?.manholeData?.waterInfiltrationAlarmStatus) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">水位状态</div>
|
|
|
+ <div class="status-value">{{ getBinaryAlarmText(selectedDetail?.manholeData?.waterLevelAlarmStatus) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item">
|
|
|
+ <div class="status-label">激活状态</div>
|
|
|
+ <div class="status-value">{{ selectedDetail?.manholeData?.activationStatus || '-' }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="threshold-card">
|
|
|
<div class="card-title">
|
|
|
<span><el-icon><Setting /></el-icon> 报警阈值设置</span>
|
|
|
<div class="card-actions">
|
|
|
- <el-button size="small" @click="resetThresholds">重置</el-button>
|
|
|
- <el-button size="small" type="primary" @click="saveThresholds">保存设置</el-button>
|
|
|
+ <el-button :icon="Refresh" size="small" @click="resetThresholds">重置</el-button>
|
|
|
+ <el-button :loading="saveLoading" size="small" type="primary" @click="saveThresholds">保存设置</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <el-tabs v-model="activeTab" class="threshold-tabs">
|
|
|
- <!-- 倾斜阈值 -->
|
|
|
- <el-tab-pane label="倾斜监测" name="tilt">
|
|
|
- <div class="threshold-config">
|
|
|
- <div class="threshold-item">
|
|
|
- <div class="item-label">
|
|
|
- <span>倾斜角度阈值</span>
|
|
|
- <el-tooltip content="当井盖倾斜角度超过此值时触发报警" placement="top">
|
|
|
- <el-icon><QuestionFilled /></el-icon>
|
|
|
- </el-tooltip>
|
|
|
- </div>
|
|
|
- <div class="item-control">
|
|
|
- <el-input-number v-model="thresholds.tilt.warning" :min="0" :max="45" :step="0.5" size="default" style="width: 120px" />
|
|
|
- <span class="unit">度</span>
|
|
|
- <span class="level-tag warning">预警值</span>
|
|
|
- <el-input-number v-model="thresholds.tilt.critical" :min="0" :max="90" :step="0.5" size="default" style="width: 120px; margin-left: 20px" />
|
|
|
- <span class="unit">度</span>
|
|
|
- <span class="level-tag critical">告警值</span>
|
|
|
- </div>
|
|
|
- <div class="history-suggestion">
|
|
|
- <el-icon><DataLine /></el-icon>
|
|
|
- 历史数据分析建议: 基于近30天数据,95%倾斜角在 <strong>{{ suggested.tilt.suggestedWarning }}°</strong> 以内,建议预警值 ≤ {{ suggested.tilt.suggestedWarning }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-tab-pane>
|
|
|
-
|
|
|
- <!-- 水位/溢水阈值 -->
|
|
|
- <el-tab-pane label="水位监测" name="water">
|
|
|
- <div class="threshold-config">
|
|
|
- <div class="threshold-item">
|
|
|
- <div class="item-label">水位深度阈值</div>
|
|
|
- <div class="item-control">
|
|
|
- <el-input-number v-model="thresholds.water.warning" :min="0" :max="100" :step="1" style="width: 120px" />
|
|
|
- <span class="unit">cm</span>
|
|
|
- <span class="level-tag warning">预警值</span>
|
|
|
- <el-input-number v-model="thresholds.water.critical" :min="0" :max="150" :step="1" style="width: 120px; margin-left: 20px" />
|
|
|
- <span class="unit">cm</span>
|
|
|
- <span class="level-tag critical">告警值</span>
|
|
|
- </div>
|
|
|
- <div class="history-suggestion">
|
|
|
- <el-icon><DataLine /></el-icon>
|
|
|
- 历史最高水位: {{ selectedPoint.historyData?.maxWaterLevel || 32 }}cm,建议告警值设置在 {{ suggested.water.suggestedCritical }}cm
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-tab-pane>
|
|
|
-
|
|
|
- <!-- 震动阈值 -->
|
|
|
- <el-tab-pane label="震动监测" name="vibration">
|
|
|
- <div class="threshold-config">
|
|
|
- <div class="threshold-item">
|
|
|
- <div class="item-label">震动强度阈值</div>
|
|
|
- <div class="item-control">
|
|
|
- <el-input-number v-model="thresholds.vibration.warning" :min="0" :max="200" :step="5" style="width: 120px" />
|
|
|
- <span class="unit">mg</span>
|
|
|
- <span class="level-tag warning">预警值</span>
|
|
|
- <el-input-number v-model="thresholds.vibration.critical" :min="0" :max="300" :step="5" style="width: 120px; margin-left: 20px" />
|
|
|
- <span class="unit">mg</span>
|
|
|
- <span class="level-tag critical">告警值</span>
|
|
|
- </div>
|
|
|
- <div class="history-suggestion">
|
|
|
- <el-icon><DataLine /></el-icon>
|
|
|
- 历史震动峰值: {{ selectedPoint.historyData?.maxVibration || 78 }}mg,建议阈值动态调整
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-tab-pane>
|
|
|
+ <div class="threshold-tip">
|
|
|
+ 当前版本先对接后端已支持的 3 类阈值:倾斜、水位、温度。
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 气体阈值 -->
|
|
|
- <el-tab-pane label="有害气体" name="gas">
|
|
|
- <div class="threshold-config">
|
|
|
- <div class="threshold-item">
|
|
|
- <div class="item-label">H₂S浓度阈值</div>
|
|
|
- <div class="item-control">
|
|
|
- <el-input-number v-model="thresholds.gas.warning" :min="0" :max="50" :step="1" style="width: 120px" />
|
|
|
- <span class="unit">ppm</span>
|
|
|
- <span class="level-tag warning">预警值</span>
|
|
|
- <el-input-number v-model="thresholds.gas.critical" :min="0" :max="100" :step="1" style="width: 120px; margin-left: 20px" />
|
|
|
- <span class="unit">ppm</span>
|
|
|
- <span class="level-tag critical">告警值</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-tab-pane>
|
|
|
+ <el-tabs v-model="activeTab" class="threshold-tabs">
|
|
|
+ <el-tab-pane
|
|
|
+ v-for="item in thresholdConfigList"
|
|
|
+ :key="item.key"
|
|
|
+ :label="item.label"
|
|
|
+ :name="item.key"
|
|
|
+ >
|
|
|
+ <div class="threshold-item">
|
|
|
+ <div class="item-label">{{ item.desc }}</div>
|
|
|
+ <div class="item-control">
|
|
|
+ <span class="field-label">最小值</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="thresholds[item.key].minValue"
|
|
|
+ :controls="false"
|
|
|
+ :precision="item.precision"
|
|
|
+ :step="item.step"
|
|
|
+ class="threshold-input"
|
|
|
+ placeholder="未设置"
|
|
|
+ />
|
|
|
+ <span class="unit">{{ item.unit }}</span>
|
|
|
|
|
|
- <!-- 电池/信号阈值 -->
|
|
|
- <el-tab-pane label="设备状态" name="device">
|
|
|
- <div class="threshold-config">
|
|
|
- <div class="threshold-item">
|
|
|
- <div class="item-label">电池电压阈值</div>
|
|
|
- <div class="item-control">
|
|
|
- <el-input-number v-model="thresholds.battery.warning" :min="0" :max="100" :step="5" style="width: 120px" />
|
|
|
- <span class="unit">%</span>
|
|
|
- <span class="level-tag warning">低电量预警</span>
|
|
|
- </div>
|
|
|
+ <span class="field-label">最大值</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="thresholds[item.key].maxValue"
|
|
|
+ :controls="false"
|
|
|
+ :precision="item.precision"
|
|
|
+ :step="item.step"
|
|
|
+ class="threshold-input"
|
|
|
+ placeholder="未设置"
|
|
|
+ />
|
|
|
+ <span class="unit">{{ item.unit }}</span>
|
|
|
</div>
|
|
|
- <div class="threshold-item">
|
|
|
- <div class="item-label">信号强度阈值</div>
|
|
|
- <div class="item-control">
|
|
|
- <el-input-number v-model="thresholds.signal.warning" :min="0" :max="100" :step="5" style="width: 120px" />
|
|
|
- <span class="unit">%</span>
|
|
|
- <span class="level-tag warning">弱信号预警</span>
|
|
|
- </div>
|
|
|
+ <el-input
|
|
|
+ v-model="thresholds[item.key].remark"
|
|
|
+ maxlength="100"
|
|
|
+ placeholder="请输入备注"
|
|
|
+ />
|
|
|
+ <div class="saved-hint">
|
|
|
+ 预警编码:{{ item.warningCode }}
|
|
|
+ <span v-if="thresholds[item.key].id">,当前记录ID:{{ thresholds[item.key].id }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-tab-pane>
|
|
|
</el-tabs>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 历史监测数据趋势图 (帮助科学设置阈值) -->
|
|
|
- <div class="history-chart-card">
|
|
|
- <div class="card-title">
|
|
|
- <span><el-icon><TrendCharts /></el-icon> 历史监测数据趋势(近30天)</span>
|
|
|
- <el-select v-model="historyMetric" size="small" style="width: 120px">
|
|
|
- <el-option label="倾斜角度" value="tilt" />
|
|
|
- <el-option label="水位深度" value="water" />
|
|
|
- <el-option label="震动强度" value="vibration" />
|
|
|
- <el-option label="气体浓度" value="gas" />
|
|
|
- </el-select>
|
|
|
- </div>
|
|
|
- <div class="chart-container">
|
|
|
- <v-chart :option="historyChartOption" autoresize />
|
|
|
- </div>
|
|
|
-
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 未选中状态 -->
|
|
|
<div v-else class="empty-panel">
|
|
|
- <el-empty description="请从左侧选择一个监测点查看阈值配置" :image-size="140" />
|
|
|
+ <el-empty description="请从左侧选择设备查看阈值配置" :image-size="140" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, watch } from 'vue'
|
|
|
-import { Location, Monitor, Setting, QuestionFilled, DataLine, TrendCharts, Document, Check, Search } from '@element-plus/icons-vue'
|
|
|
-import { use } from 'echarts/core'
|
|
|
-import { CanvasRenderer } from 'echarts/renderers'
|
|
|
-import { LineChart } from 'echarts/charts'
|
|
|
-import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent } from 'echarts/components'
|
|
|
+import {computed, onMounted, ref} from 'vue'
|
|
|
+import {ElMessage} from 'element-plus'
|
|
|
+import {Location, Monitor, Refresh, Search, Setting, Warning} from '@element-plus/icons-vue'
|
|
|
+import {
|
|
|
+ getManholeAlarmDeviceDetail,
|
|
|
+ getManholeAlarmDeviceList,
|
|
|
+ saveManholeAlarmThreshold
|
|
|
+} from '@/api/pipeNetwork/basic'
|
|
|
|
|
|
-use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent])
|
|
|
-
|
|
|
-// 模拟监测点数据
|
|
|
-const monitoringPoints = ref([
|
|
|
+const thresholdConfigList = [
|
|
|
{
|
|
|
- id: 'MP-1001',
|
|
|
- name: '人民路监测点',
|
|
|
- location: '人民路与解放路口东50m',
|
|
|
- status: '正常',
|
|
|
- deviceId: 'MN-1001',
|
|
|
- deviceType: '智能井盖监测仪',
|
|
|
- deviceModel: 'WATCHMAN-S2',
|
|
|
- manufacturer: '华盾科技',
|
|
|
- installDate: '2024-03-15',
|
|
|
- protocol: 'LoRaWAN',
|
|
|
- lastCalibration: '2025-02-10',
|
|
|
- historyData: {
|
|
|
- tiltHistory: [1.2, 1.5, 2.1, 1.8, 2.3, 3.2, 2.8, 2.5, 3.0, 2.2, 1.9, 2.4, 3.5, 4.1, 3.8, 3.2, 2.9, 3.3, 4.5, 5.2, 4.8, 4.2, 3.6, 3.9, 4.3, 5.0, 4.6, 4.1, 3.7, 4.2],
|
|
|
- waterHistory: [5, 6, 7, 8, 10, 12, 15, 18, 20, 22, 25, 28, 30, 32, 35, 38, 36, 34, 33, 35, 37, 40, 42, 39, 36, 34, 32, 30, 28, 26],
|
|
|
- vibrationHistory: [12, 15, 18, 22, 25, 30, 35, 42, 48, 52, 58, 62, 65, 70, 68, 72, 75, 78, 80, 76, 72, 68, 64, 60, 55, 50, 45, 40, 38, 35],
|
|
|
- gasHistory: [0.5, 0.8, 1.2, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 9.8, 9.5, 9.0, 8.5, 8.0, 7.5, 7.0, 6.5, 6.0],
|
|
|
- maxWaterLevel: 42,
|
|
|
- maxVibration: 80
|
|
|
- }
|
|
|
+ key: 'tilt',
|
|
|
+ label: '倾斜监测',
|
|
|
+ warningType: '倾斜预警',
|
|
|
+ warningCode: 'WARN-TILT',
|
|
|
+ unit: '°',
|
|
|
+ step: 0.1,
|
|
|
+ precision: 1,
|
|
|
+ desc: '当倾斜角度小于等于最小值或大于等于最大值时触发报警'
|
|
|
},
|
|
|
{
|
|
|
- id: 'MP-1002',
|
|
|
- name: '滨江路监测点',
|
|
|
- location: '滨江路化工园区南门',
|
|
|
- status: '告警',
|
|
|
- deviceId: 'MN-1002',
|
|
|
- deviceType: '防爆井盖监测仪',
|
|
|
- deviceModel: 'WATCHMAN-EX',
|
|
|
- manufacturer: '华盾科技',
|
|
|
- installDate: '2024-06-20',
|
|
|
- protocol: 'NB-IoT',
|
|
|
- lastCalibration: '2025-01-15',
|
|
|
- historyData: {
|
|
|
- tiltHistory: [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 5.2, 6.5, 7.8, 8.2, 9.0, 8.5, 7.5, 6.8, 5.5, 4.8, 4.0, 3.5, 3.0, 2.5, 2.0, 1.8, 1.5, 1.2, 1.0, 0.8],
|
|
|
- waterHistory: [15, 18, 22, 28, 35, 42, 50, 58, 62, 55, 48, 45, 42, 40, 38, 35, 32, 30, 28, 25, 22, 20, 18, 16, 15, 14, 13, 12, 11, 10],
|
|
|
- vibrationHistory: [20, 25, 32, 40, 48, 55, 62, 70, 78, 85, 92, 98, 105, 110, 115, 108, 100, 92, 85, 78, 70, 62, 55, 48, 42, 38, 35, 32, 28, 25],
|
|
|
- gasHistory: [2, 3, 5, 7, 10, 15, 20, 25, 30, 28, 25, 22, 20, 18, 16, 15, 14, 12, 10, 9, 8, 7, 6, 5, 4, 3.5, 3, 2.5, 2, 1.5],
|
|
|
- maxWaterLevel: 62,
|
|
|
- maxVibration: 115
|
|
|
- }
|
|
|
+ key: 'waterLevel',
|
|
|
+ label: '水位监测',
|
|
|
+ warningType: '水位预警',
|
|
|
+ warningCode: 'WARN-WATER-LEVEL',
|
|
|
+ unit: 'cm',
|
|
|
+ step: 0.1,
|
|
|
+ precision: 1,
|
|
|
+ desc: '用于井盖水位报警阈值配置'
|
|
|
},
|
|
|
{
|
|
|
- id: 'MP-1003',
|
|
|
- name: '高新区监测点',
|
|
|
- location: '高新大道云计算中心旁',
|
|
|
- status: '正常',
|
|
|
- deviceId: 'MN-1004',
|
|
|
- deviceType: '智能井盖监测仪',
|
|
|
- deviceModel: 'WATCHMAN-S2',
|
|
|
- manufacturer: '华盾科技',
|
|
|
- installDate: '2024-09-10',
|
|
|
- protocol: 'LoRaWAN',
|
|
|
- lastCalibration: '2025-03-01',
|
|
|
- historyData: {
|
|
|
- tiltHistory: [0.3, 0.5, 0.4, 0.6, 0.8, 0.7, 0.9, 1.0, 0.8, 0.6, 0.5, 0.7, 0.9, 1.1, 1.0, 0.8, 0.6, 0.5, 0.4, 0.6, 0.8, 0.7, 0.5, 0.3, 0.4, 0.6, 0.9, 1.2, 0.9, 0.7],
|
|
|
- waterHistory: [3, 4, 5, 6, 5, 7, 8, 6, 5, 4, 5, 6, 7, 8, 9, 10, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 8, 9, 7, 6],
|
|
|
- vibrationHistory: [8, 10, 12, 15, 14, 16, 18, 20, 18, 15, 12, 10, 11, 13, 15, 17, 16, 14, 12, 10, 9, 8, 7, 9, 11, 13, 15, 14, 12, 10],
|
|
|
- gasHistory: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2],
|
|
|
- maxWaterLevel: 10,
|
|
|
- maxVibration: 20
|
|
|
- }
|
|
|
+ key: 'temperature',
|
|
|
+ label: '温度监测',
|
|
|
+ warningType: '温度预警',
|
|
|
+ warningCode: 'WARN-TEMPERATURE',
|
|
|
+ unit: '℃',
|
|
|
+ step: 0.1,
|
|
|
+ precision: 1,
|
|
|
+ desc: '用于设备温度报警阈值配置'
|
|
|
}
|
|
|
-])
|
|
|
+]
|
|
|
|
|
|
const searchKeyword = ref('')
|
|
|
-const selectedPoint = ref(monitoringPoints.value[0])
|
|
|
const activeTab = ref('tilt')
|
|
|
-const historyMetric = ref('tilt')
|
|
|
-
|
|
|
-// 当前阈值配置
|
|
|
-const thresholds = ref({
|
|
|
- tilt: { warning: 5, critical: 10 },
|
|
|
- water: { warning: 25, critical: 40 },
|
|
|
- vibration: { warning: 60, critical: 100 },
|
|
|
- gas: { warning: 10, critical: 20 },
|
|
|
- battery: { warning: 20 },
|
|
|
- signal: { warning: 30 }
|
|
|
-})
|
|
|
+const listLoading = ref(false)
|
|
|
+const detailLoading = ref(false)
|
|
|
+const saveLoading = ref(false)
|
|
|
+const deviceList = ref([])
|
|
|
+const selectedPoint = ref(null)
|
|
|
+const selectedDetail = ref(null)
|
|
|
+const thresholds = ref(createEmptyThresholds())
|
|
|
|
|
|
-// 智能建议值(基于历史数据)
|
|
|
-const suggested = computed(() => {
|
|
|
- const hist = selectedPoint.value?.historyData
|
|
|
- if (!hist) return { tilt: { suggestedWarning: 5 }, water: { suggestedCritical: 40 }, vibration: {} }
|
|
|
- const tilt95 = hist.tiltHistory ? Math.ceil(hist.tiltHistory.sort((a,b)=>a-b)[Math.floor(hist.tiltHistory.length*0.95)] * 10) / 10 : 5
|
|
|
- const waterMax = hist.maxWaterLevel || 40
|
|
|
- return {
|
|
|
- tilt: { suggestedWarning: Math.min(tilt95 + 1, 12) },
|
|
|
- water: { suggestedCritical: Math.min(waterMax + 5, 80) },
|
|
|
- vibration: { suggestedPeak: hist.maxVibration || 80 }
|
|
|
- }
|
|
|
-})
|
|
|
+function createEmptyThresholds() {
|
|
|
+ return thresholdConfigList.reduce((acc, item) => {
|
|
|
+ acc[item.key] = {
|
|
|
+ id: '',
|
|
|
+ minValue: null,
|
|
|
+ maxValue: null,
|
|
|
+ remark: ''
|
|
|
+ }
|
|
|
+ return acc
|
|
|
+ }, {})
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeThresholdList(list) {
|
|
|
+ const safeList = Array.isArray(list) ? list : []
|
|
|
+ const result = createEmptyThresholds()
|
|
|
+ thresholdConfigList.forEach((item) => {
|
|
|
+ const matched = safeList.find(threshold => threshold.warningCode === item.warningCode)
|
|
|
+ if (matched) {
|
|
|
+ result[item.key] = {
|
|
|
+ id: matched.id || '',
|
|
|
+ minValue: matched.minValue ?? null,
|
|
|
+ maxValue: matched.maxValue ?? null,
|
|
|
+ remark: matched.remark || ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return result
|
|
|
+}
|
|
|
|
|
|
const filteredPoints = computed(() => {
|
|
|
- if (!searchKeyword.value) return monitoringPoints.value
|
|
|
- const kw = searchKeyword.value.toLowerCase()
|
|
|
- return monitoringPoints.value.filter(p =>
|
|
|
- p.name.toLowerCase().includes(kw) ||
|
|
|
- p.deviceId.toLowerCase().includes(kw) ||
|
|
|
- p.location.toLowerCase().includes(kw)
|
|
|
- )
|
|
|
+ const keyword = searchKeyword.value.trim().toLowerCase()
|
|
|
+ if (!keyword) return deviceList.value
|
|
|
+ return deviceList.value.filter(item => {
|
|
|
+ const name = `${item.equipmentName || ''}`.toLowerCase()
|
|
|
+ const code = `${item.equipmentCode || ''}`.toLowerCase()
|
|
|
+ const location = `${item.equipmentLocation || ''}`.toLowerCase()
|
|
|
+ return name.includes(keyword) || code.includes(keyword) || location.includes(keyword)
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
-const getPointStatusType = (point) => point.status === '正常' ? 'success' : 'danger'
|
|
|
+function getOnlineText(status) {
|
|
|
+ return Number(status) === 1 ? '在线' : '离线'
|
|
|
+}
|
|
|
|
|
|
-const selectPoint = (point) => {
|
|
|
- selectedPoint.value = point
|
|
|
- // 加载该点已有阈值(演示用,模拟从后端加载)
|
|
|
- if (point.id === 'MP-1002') {
|
|
|
- thresholds.value = { tilt: { warning: 8, critical: 15 }, water: { warning: 35, critical: 55 }, vibration: { warning: 80, critical: 120 }, gas: { warning: 15, critical: 25 }, battery: { warning: 20 }, signal: { warning: 30 } }
|
|
|
- } else {
|
|
|
- thresholds.value = { tilt: { warning: 5, critical: 10 }, water: { warning: 25, critical: 40 }, vibration: { warning: 60, critical: 100 }, gas: { warning: 10, critical: 20 }, battery: { warning: 20 }, signal: { warning: 30 } }
|
|
|
- }
|
|
|
+function getOnlineTagType(status) {
|
|
|
+ return Number(status) === 1 ? 'success' : 'info'
|
|
|
+}
|
|
|
+
|
|
|
+function getAlarmText(point) {
|
|
|
+ return Number(point?.equipmentStatus?.alarmStatus) === 1 ? '报警' : '正常'
|
|
|
}
|
|
|
|
|
|
-const historyChartOption = computed(() => {
|
|
|
- const point = selectedPoint.value
|
|
|
- if (!point) return {}
|
|
|
- let data = []
|
|
|
- let yName = ''
|
|
|
- let color = '#409eff'
|
|
|
- switch (historyMetric.value) {
|
|
|
- case 'tilt': data = point.historyData.tiltHistory; yName = '倾斜角度 (°)'; color = '#e6a23c'; break
|
|
|
- case 'water': data = point.historyData.waterHistory; yName = '水位深度 (cm)'; color = '#3b82f6'; break
|
|
|
- case 'vibration': data = point.historyData.vibrationHistory; yName = '震动强度 (mg)'; color = '#f56c6c'; break
|
|
|
- case 'gas': data = point.historyData.gasHistory; yName = '气体浓度 (ppm)'; color = '#9b59b6'; break
|
|
|
+function getAlarmTagType(point) {
|
|
|
+ return Number(point?.equipmentStatus?.alarmStatus) === 1 ? 'danger' : 'success'
|
|
|
+}
|
|
|
+
|
|
|
+function getUseStatusText(status) {
|
|
|
+ const map = {
|
|
|
+ 1: '在用',
|
|
|
+ 2: '闲置',
|
|
|
+ 3: '维修',
|
|
|
+ 4: '报废',
|
|
|
+ 5: '待入库'
|
|
|
}
|
|
|
- const days = data.map((_, idx) => `Day ${idx + 1}`)
|
|
|
- return {
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
- xAxis: { type: 'category', data: days, axisLabel: { rotate: 45, interval: 5 } },
|
|
|
- yAxis: { type: 'value', name: yName },
|
|
|
- series: [{ data, type: 'line', smooth: true, lineStyle: { color, width: 2 }, areaStyle: { opacity: 0.1, color }, symbol: 'circle', symbolSize: 6 }],
|
|
|
- grid: { containLabel: true, left: '10%' }
|
|
|
+ return map[Number(status)] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+function getUseStatusTagType(status) {
|
|
|
+ const map = {
|
|
|
+ 1: 'success',
|
|
|
+ 2: 'info',
|
|
|
+ 3: 'warning',
|
|
|
+ 4: 'danger',
|
|
|
+ 5: ''
|
|
|
}
|
|
|
-})
|
|
|
+ return map[Number(status)] || 'info'
|
|
|
+}
|
|
|
|
|
|
-const getMetricName = (metric) => {
|
|
|
- const map = { tilt: '倾斜角度', water: '水位深度', vibration: '震动强度', gas: '气体浓度' }
|
|
|
- return map[metric]
|
|
|
+function getBinaryAlarmText(status) {
|
|
|
+ return `${status}` === '1' ? '报警' : `${status}` === '0' ? '正常' : '-'
|
|
|
}
|
|
|
|
|
|
-const getSuggestedThreshold = () => {
|
|
|
- const metric = historyMetric.value
|
|
|
- if (metric === 'tilt') return `${suggested.value.tilt.suggestedWarning}°`
|
|
|
- if (metric === 'water') return `${suggested.value.water.suggestedCritical}cm`
|
|
|
- if (metric === 'vibration') return `${selectedPoint.value?.historyData?.maxVibration || 80}mg`
|
|
|
- return '科学建议值'
|
|
|
+function appendUnit(value, unit) {
|
|
|
+ if (value === undefined || value === null || value === '') return '-'
|
|
|
+ return `${value}${unit}`
|
|
|
}
|
|
|
|
|
|
-const applySuggestedThreshold = () => {
|
|
|
- const metric = historyMetric.value
|
|
|
- if (metric === 'tilt') thresholds.value.tilt.warning = suggested.value.tilt.suggestedWarning
|
|
|
- if (metric === 'water') thresholds.value.water.critical = suggested.value.water.suggestedCritical
|
|
|
- if (metric === 'vibration') thresholds.value.vibration.critical = (selectedPoint.value?.historyData?.maxVibration || 80) + 10
|
|
|
- if (metric === 'gas') thresholds.value.gas.warning = 12
|
|
|
- ElMessage.success('已应用AI建议阈值')
|
|
|
+function formatDateTime(value) {
|
|
|
+ return value || '-'
|
|
|
}
|
|
|
|
|
|
-const resetThresholds = () => {
|
|
|
- selectPoint(selectedPoint.value)
|
|
|
- ElMessage.info('已重置为当前监测点默认阈值')
|
|
|
+async function loadDeviceList() {
|
|
|
+ listLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getManholeAlarmDeviceList({
|
|
|
+ isQueryManholeData: true
|
|
|
+ })
|
|
|
+ const list = Array.isArray(res?.data) ? res.data : []
|
|
|
+ deviceList.value = list
|
|
|
+
|
|
|
+ if (!list.length) {
|
|
|
+ selectedPoint.value = null
|
|
|
+ selectedDetail.value = null
|
|
|
+ thresholds.value = createEmptyThresholds()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentId = selectedPoint.value?.equipmentId
|
|
|
+ const currentPoint = list.find(item => item.equipmentId === currentId) || list[0]
|
|
|
+ await selectPoint(currentPoint)
|
|
|
+ } catch (error) {
|
|
|
+ deviceList.value = []
|
|
|
+ selectedPoint.value = null
|
|
|
+ selectedDetail.value = null
|
|
|
+ } finally {
|
|
|
+ listLoading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-const saveThresholds = () => {
|
|
|
- ElMessage.success('阈值配置已保存,将即时生效')
|
|
|
+async function selectPoint(point) {
|
|
|
+ if (!point?.equipmentId) return
|
|
|
+ selectedPoint.value = point
|
|
|
+ await loadDeviceDetail(point.equipmentId)
|
|
|
}
|
|
|
|
|
|
-const exportConfig = () => {
|
|
|
- ElMessage.info('导出阈值配置文件(模拟)')
|
|
|
+async function loadDeviceDetail(id) {
|
|
|
+ detailLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getManholeAlarmDeviceDetail(id)
|
|
|
+ selectedDetail.value = res?.data || null
|
|
|
+ thresholds.value = normalizeThresholdList(selectedDetail.value?.warningThresholdList)
|
|
|
+ } catch (error) {
|
|
|
+ selectedDetail.value = null
|
|
|
+ thresholds.value = createEmptyThresholds()
|
|
|
+ } finally {
|
|
|
+ detailLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function resetThresholds() {
|
|
|
+ if (!selectedPoint.value?.equipmentId) return
|
|
|
+ await loadDeviceDetail(selectedPoint.value.equipmentId)
|
|
|
+ ElMessage.success('已恢复为服务端最新阈值')
|
|
|
}
|
|
|
|
|
|
-const applyAllThresholds = () => {
|
|
|
- ElMessage.success('已对所有监测点应用推荐阈值(演示)')
|
|
|
+function hasThresholdValue(item) {
|
|
|
+ return (
|
|
|
+ (item.minValue !== null && item.minValue !== undefined) ||
|
|
|
+ (item.maxValue !== null && item.maxValue !== undefined)
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+async function saveThresholds() {
|
|
|
+ if (!selectedDetail.value?.equipmentCode) {
|
|
|
+ ElMessage.warning('未获取到设备编码,无法保存')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const invalidConfigs = thresholdConfigList.filter((config) => {
|
|
|
+ const formItem = thresholds.value[config.key]
|
|
|
+ return formItem.id && !hasThresholdValue(formItem)
|
|
|
+ })
|
|
|
+
|
|
|
+ if (invalidConfigs.length) {
|
|
|
+ ElMessage.warning(`${invalidConfigs[0].label}请至少保留一个阈值`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const payloads = thresholdConfigList
|
|
|
+ .map((config) => {
|
|
|
+ const formItem = thresholds.value[config.key]
|
|
|
+ if (!hasThresholdValue(formItem) && !formItem.id) return null
|
|
|
+ return {
|
|
|
+ id: formItem.id || undefined,
|
|
|
+ deviceCode: selectedDetail.value.equipmentCode,
|
|
|
+ warningType: config.warningType,
|
|
|
+ warningCode: config.warningCode,
|
|
|
+ minValue: formItem.minValue,
|
|
|
+ maxValue: formItem.maxValue,
|
|
|
+ remark: formItem.remark || ''
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .filter(Boolean)
|
|
|
+
|
|
|
+ if (!payloads.length) {
|
|
|
+ ElMessage.warning('请先填写至少一项阈值')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ saveLoading.value = true
|
|
|
+ try {
|
|
|
+ await Promise.all(payloads.map(item => saveManholeAlarmThreshold(item)))
|
|
|
+ ElMessage.success('阈值保存成功')
|
|
|
+ await loadDeviceDetail(selectedPoint.value.equipmentId)
|
|
|
+ } finally {
|
|
|
+ saveLoading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
+onMounted(() => {
|
|
|
+ loadDeviceList()
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -391,99 +441,198 @@ import { ElMessage } from 'element-plus'
|
|
|
background: #f0f2f6;
|
|
|
min-height: 100vh;
|
|
|
}
|
|
|
-.page-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- background: white;
|
|
|
- padding: 16px 24px;
|
|
|
- border-radius: 16px;
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-.header-left h2 { margin: 0 0 6px 0; font-weight: 600; }
|
|
|
+
|
|
|
.main-content {
|
|
|
display: flex;
|
|
|
gap: 20px;
|
|
|
}
|
|
|
+
|
|
|
.point-list-panel {
|
|
|
- width: 320px;
|
|
|
- background: white;
|
|
|
+ width: 340px;
|
|
|
+ background: #fff;
|
|
|
border-radius: 16px;
|
|
|
padding: 16px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
+
|
|
|
.panel-title {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 16px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
+
|
|
|
.point-list {
|
|
|
- max-height: calc(100vh - 200px);
|
|
|
+ max-height: calc(100vh - 180px);
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
+
|
|
|
.point-item {
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 12px;
|
|
|
padding: 14px;
|
|
|
margin-bottom: 12px;
|
|
|
- border-radius: 12px;
|
|
|
cursor: pointer;
|
|
|
- transition: all 0.2s;
|
|
|
- border: 1px solid #e4e7ed;
|
|
|
-}
|
|
|
-.point-item:hover { background: #f5f7fa; transform: translateX(2px); }
|
|
|
-.point-item.active { background: #ecf5ff; border-color: #409eff; }
|
|
|
-.point-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
|
|
|
-.point-name { font-weight: 600; }
|
|
|
-.point-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
|
|
|
-.point-device { font-size: 12px; color: #409eff; }
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.point-item:hover {
|
|
|
+ background: #f8fbff;
|
|
|
+ border-color: #bfdbfe;
|
|
|
+}
|
|
|
+
|
|
|
+.point-item.active {
|
|
|
+ background: #ecf5ff;
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.point-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.point-location {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-meta {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #409eff;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-status-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
.config-panel {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 20px;
|
|
|
}
|
|
|
-.info-card, .threshold-card, .history-chart-card {
|
|
|
- background: white;
|
|
|
+
|
|
|
+.info-card,
|
|
|
+.threshold-card {
|
|
|
+ background: #fff;
|
|
|
border-radius: 16px;
|
|
|
- padding: 16px 20px;
|
|
|
- box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
|
+ padding: 18px 20px;
|
|
|
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
|
}
|
|
|
+
|
|
|
.card-title {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 16px;
|
|
|
+ padding-left: 12px;
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
+ font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.card-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-item {
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 1px solid #edf2f7;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-label {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-value {
|
|
|
+ color: #303133;
|
|
|
font-size: 16px;
|
|
|
- border-left: 4px solid #409eff;
|
|
|
- padding-left: 12px;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
+
|
|
|
+.threshold-tip {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f4f9ff;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
.threshold-item {
|
|
|
- margin-bottom: 24px;
|
|
|
- padding-bottom: 16px;
|
|
|
- border-bottom: 1px solid #f0f0f0;
|
|
|
+ padding: 8px 4px 4px;
|
|
|
}
|
|
|
+
|
|
|
.item-label {
|
|
|
- font-weight: 500;
|
|
|
- margin-bottom: 10px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.item-control {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 6px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 16px;
|
|
|
}
|
|
|
-.item-control {
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.threshold-input {
|
|
|
+ width: 160px;
|
|
|
+}
|
|
|
+
|
|
|
+.unit {
|
|
|
+ color: #909399;
|
|
|
+ margin-right: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.saved-hint {
|
|
|
+ margin-top: 10px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-panel {
|
|
|
+ flex: 1;
|
|
|
display: flex;
|
|
|
+ justify-content: center;
|
|
|
align-items: center;
|
|
|
- gap: 6px;
|
|
|
- margin-bottom: 12px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 16px;
|
|
|
}
|
|
|
-.unit { margin-right: 12px; color: #6c757d; }
|
|
|
-.level-tag { font-size: 12px; padding: 2px 8px; border-radius: 12px; margin-left: 8px; }
|
|
|
-.level-tag.warning { background: #fdf6ec; color: #e6a23c; }
|
|
|
-.level-tag.critical { background: #fef0f0; color: #f56c6c; }
|
|
|
-.history-suggestion { background: #f8f9fc; padding: 8px 12px; border-radius: 8px; font-size: 13px; display: flex; align-items: center; gap: 8px; color: #606266; }
|
|
|
-.chart-container { height: 260px; margin: 12px 0; }
|
|
|
-.ai-suggestion { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
|
|
|
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 16px; }
|
|
|
-</style>
|
|
|
+</style>
|