|
|
@@ -1,259 +1,188 @@
|
|
|
<template>
|
|
|
<div class="water-page gis-page">
|
|
|
- <div class="page-heading">
|
|
|
- <div class="page-title">
|
|
|
- <span class="page-title-icon"><el-icon><MapLocation/></el-icon></span>
|
|
|
- <div><h2>监测设备 GIS 一张图</h2><span class="muted">设备点位分布与状态查询</span></div>
|
|
|
- </div>
|
|
|
- <div class="heading-actions">
|
|
|
- <el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
|
|
|
+ <div class="page-head">
|
|
|
+ <div class="page-head-titles">
|
|
|
+ <div class="page-title">
|
|
|
+ <el-icon class="page-title-icon"><MapLocation /></el-icon>
|
|
|
+ <span>监测设备 GIS 一张图</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ <el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
|
|
|
</div>
|
|
|
|
|
|
- <el-card shadow="never" class="water-panel filter-card">
|
|
|
- <template #header>
|
|
|
- <div class="card-title">
|
|
|
- <span class="card-heading"><el-icon><Search/></el-icon><span>设备筛选</span></span>
|
|
|
+ <!-- 设备统计 -->
|
|
|
+ <div class="kpi-row">
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon total"><el-icon><Monitor /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">设备总数(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.total }}</div>
|
|
|
</div>
|
|
|
- </template>
|
|
|
- <el-form :inline="true" class="filter-form">
|
|
|
- <el-form-item label="关键字">
|
|
|
- <el-input v-model="searchKeyword" class="keyword-input" clearable placeholder="设备编号 / 名称 / 位置"
|
|
|
- :prefix-icon="Search" @keyup.enter="loadData"/>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="设备类型">
|
|
|
- <el-select v-model="selectedTypeId" class="status-select" clearable placeholder="全部设备类型">
|
|
|
- <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value"/>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <el-button type="primary" @click="loadData">查询</el-button>
|
|
|
- <el-button @click="resetFilters">重置</el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <el-card shadow="never" class="water-panel map-card">
|
|
|
- <WaterGisPageShell
|
|
|
- embedded
|
|
|
- eyebrow="WATER SUPPLY DEVICE GIS"
|
|
|
- title="监测设备 GIS 一张图"
|
|
|
- :loading="loading"
|
|
|
- >
|
|
|
- <template #actions>
|
|
|
- <el-input
|
|
|
- v-model="searchKeyword"
|
|
|
- class="search-input"
|
|
|
- placeholder="搜索设备编号/名称/位置"
|
|
|
- clearable
|
|
|
- :prefix-icon="Search"
|
|
|
- />
|
|
|
- <el-select v-model="selectedTypeId" class="type-select" placeholder="全部设备类型" clearable>
|
|
|
- <el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
- </el-select>
|
|
|
- <el-button type="primary" :icon="Refresh" @click="loadData">刷新</el-button>
|
|
|
- <el-button :icon="Aim" @click="fitBounds">全图</el-button>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #metrics>
|
|
|
- <div class="metric">
|
|
|
- <div class="metric-icon"><el-icon><Odometer/></el-icon></div>
|
|
|
- <div class="metric-body">
|
|
|
- <span>设备总数</span>
|
|
|
- <div class="metric-value"><strong>{{ summary.total }}</strong><small>台</small></div>
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon online"><el-icon><CircleCheck /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">在线设备(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.online }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="metric">
|
|
|
- <div class="metric-icon"><el-icon><CircleCheck/></el-icon></div>
|
|
|
- <div class="metric-body">
|
|
|
- <span>在线</span>
|
|
|
- <div class="metric-value"><strong>{{ summary.online }}</strong><small>台</small></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="metric">
|
|
|
- <div class="metric-icon"><el-icon><CircleClose/></el-icon></div>
|
|
|
- <div class="metric-body">
|
|
|
- <span>离线</span>
|
|
|
- <div class="metric-value"><strong>{{ summary.offline }}</strong><small>台</small></div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="metric">
|
|
|
- <div class="metric-icon"><el-icon><Warning/></el-icon></div>
|
|
|
- <div class="metric-body">
|
|
|
- <span>异常</span>
|
|
|
- <div class="metric-value"><strong>{{ summary.abnormal }}</strong><small>台</small></div>
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon offline"><el-icon><CircleClose /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">离线设备(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.offline }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #controls>
|
|
|
- <div class="panel-section">
|
|
|
- <h3>状态筛选</h3>
|
|
|
- <el-checkbox-group v-model="statusFilters" class="status-filter">
|
|
|
- <el-checkbox value="normal">正常</el-checkbox>
|
|
|
- <el-checkbox value="alarm">报警</el-checkbox>
|
|
|
- <el-checkbox value="warning">预警</el-checkbox>
|
|
|
- <el-checkbox value="workOrder">关联工单</el-checkbox>
|
|
|
- <el-checkbox value="offline">离线</el-checkbox>
|
|
|
- </el-checkbox-group>
|
|
|
+ </el-card>
|
|
|
+ <el-card shadow="never" class="kpi">
|
|
|
+ <div class="kpi-icon alarm"><el-icon><Warning /></el-icon></div>
|
|
|
+ <div class="kpi-data">
|
|
|
+ <div class="kpi-label">报警设备(台)</div>
|
|
|
+ <div class="kpi-value">{{ statistics.alarm }}</div>
|
|
|
</div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="panel-section">
|
|
|
- <h3>设备类型</h3>
|
|
|
- <div v-if="typeOptions.length" class="type-list">
|
|
|
- <button
|
|
|
- v-for="item in typeOptions"
|
|
|
- :key="item.value"
|
|
|
- type="button"
|
|
|
- :class="{ active: selectedTypeId === item.value }"
|
|
|
- @click="selectedTypeId = selectedTypeId === item.value ? '' : item.value"
|
|
|
- >
|
|
|
- <span>{{ item.label }}</span>
|
|
|
- <strong>{{ item.count }}</strong>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <el-empty v-else description="暂无供水设备类型" :image-size="72" />
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #legend>
|
|
|
- <div class="panel-section legend-panel">
|
|
|
- <h3>图例</h3>
|
|
|
- <div class="legend-row"><i class="legend-dot normal"></i>正常设备</div>
|
|
|
- <div class="legend-row"><i class="legend-dot alarm"></i>报警设备</div>
|
|
|
- <div class="legend-row"><i class="legend-dot warning"></i>预警设备</div>
|
|
|
- <div class="legend-row"><i class="legend-dot workOrder"></i>关联工单设备</div>
|
|
|
- <div class="legend-row"><i class="legend-dot offline"></i>离线设备</div>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #results>
|
|
|
- <div class="panel-section result-section">
|
|
|
- <h3>搜索设备</h3>
|
|
|
- <el-alert v-if="loadError" type="error" :closable="false" show-icon :title="loadError">
|
|
|
- <el-button link type="primary" @click="loadData">重试</el-button>
|
|
|
- </el-alert>
|
|
|
- <el-empty
|
|
|
- v-else-if="!filteredDevices.length"
|
|
|
- description="暂无可显示的监测设备"
|
|
|
- :image-size="80"
|
|
|
- />
|
|
|
- <div v-else class="device-result-list">
|
|
|
- <button v-for="device in filteredDevices" :key="device.equipmentId" type="button" @click="locateDevice(device)">
|
|
|
- <span class="device-name">{{ device.equipmentName || device.equipmentCode }}</span>
|
|
|
- <span class="device-meta">{{ device.equipmentTypeName }} · {{ device.onlineStatusText }}</span>
|
|
|
- <el-tag size="small" :type="statusTagType(device)">{{ statusText(device) }}</el-tag>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #map>
|
|
|
+ <!-- 地图 -->
|
|
|
+ <el-card shadow="never" class="map-card">
|
|
|
+ <div class="map-wrap">
|
|
|
<GisMapCanvas
|
|
|
ref="gisMapRef"
|
|
|
:points="mapPoints"
|
|
|
cluster-label="设备集合"
|
|
|
- :center="{ lng: 110.3965, lat: 28.4638 }"
|
|
|
+ :center="mapCenter"
|
|
|
:zoom="15"
|
|
|
@point-click="openDetail($event.equipmentId)"
|
|
|
@error="mapError = $event"
|
|
|
/>
|
|
|
- <el-alert v-if="mapError" class="map-error" type="warning" show-icon :closable="false" :title="mapError">
|
|
|
+ <div class="map-legend" @mousedown.stop @click.stop @dblclick.stop>
|
|
|
+ <div class="legend-title">点击切换显示</div>
|
|
|
+ <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.online }" @click="toggleLegend('online')">
|
|
|
+ <i class="legend-dot online"></i>在线
|
|
|
+ </button>
|
|
|
+ <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.offline }" @click="toggleLegend('offline')">
|
|
|
+ <i class="legend-dot offline"></i>离线
|
|
|
+ </button>
|
|
|
+ <button type="button" class="legend-row" :class="{ 'is-off': !legendVisible.alarm }" @click="toggleLegend('alarm')">
|
|
|
+ <i class="legend-dot alarm"></i>报警(在线/离线时叠加)
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <el-alert
|
|
|
+ v-if="mapError"
|
|
|
+ class="map-error"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ :title="mapError"
|
|
|
+ >
|
|
|
<el-button link type="primary" @click="loadData">重试</el-button>
|
|
|
</el-alert>
|
|
|
<el-empty
|
|
|
v-if="!loading && !mapError && !mapPoints.length"
|
|
|
class="map-empty"
|
|
|
- description="当前筛选下没有可显示的监测设备"
|
|
|
+ description="当前没有可显示的监测设备"
|
|
|
/>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #drawer>
|
|
|
- <el-drawer v-model="detailVisible" title="设备详情" size="420px" class="detail-drawer" direction="rtl" append-to-body>
|
|
|
- <div v-loading="detailLoading" class="device-detail">
|
|
|
- <el-alert v-if="detailError" type="error" show-icon :closable="false" title="设备详情加载失败,请关闭后重试" />
|
|
|
- <template v-if="selectedDetail">
|
|
|
- <section class="detail-hero">
|
|
|
- <h3>{{ selectedDetail.equipmentName || selectedDetail.equipmentCode }}</h3>
|
|
|
- <p>{{ selectedDetail.equipmentCode }} · {{ selectedDetail.equipmentTypeName || '未知类型' }}</p>
|
|
|
- <div class="status-tags">
|
|
|
- <el-tag :type="deviceStatusTagType(selectedDetail.currentStatus)">设备状态:{{ selectedDetail.currentStatusText }}</el-tag>
|
|
|
- <el-tag :type="selectedDetail.onlineStatus === 1 ? 'success' : 'info'">在线状态:{{ selectedDetail.onlineStatusText }}</el-tag>
|
|
|
- <el-tag :type="selectedDetail.alarmStatus === 1 ? 'danger' : 'success'">报警状态:{{ selectedDetail.alarmStatusText }}</el-tag>
|
|
|
- <el-tag :type="selectedDetail.alertLevel ? 'warning' : 'info'">报警等级:{{ alarmLevelText }}</el-tag>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="detail-section">
|
|
|
- <h4>基础信息</h4>
|
|
|
- <dl>
|
|
|
- <dt>设备位置</dt><dd>{{ selectedDetail.equipmentLocation || '-' }}</dd>
|
|
|
- <dt>经纬度</dt><dd>{{ selectedDetail.longitude }}, {{ selectedDetail.latitude }}</dd>
|
|
|
- <dt>异常原因</dt><dd>{{ selectedDetail.abnormalReasonText || '-' }}</dd>
|
|
|
- <dt>关联工单</dt><dd>{{ selectedDetail.relatedOrderNo || '-' }}</dd>
|
|
|
- <dt>工单状态</dt><dd>{{ selectedDetail.workOrderStatusText }}</dd>
|
|
|
- <dt>责任人</dt><dd>{{ selectedDetail.relatedWorkOrderPerson || '-' }}</dd>
|
|
|
- </dl>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="detail-section">
|
|
|
- <h4>最新数据</h4>
|
|
|
- <template v-if="latestDataEntries.length">
|
|
|
- <dl>
|
|
|
- <dt v-for="item in latestDataEntries" :key="item.label">{{ item.label }}</dt>
|
|
|
- <dd v-for="item in latestDataEntries" :key="`value-${item.label}`">{{ item.value }}</dd>
|
|
|
- </dl>
|
|
|
- <p v-if="selectedDetail.lastDataTime" class="muted">更新时间:{{ selectedDetail.lastDataTime }}</p>
|
|
|
- </template>
|
|
|
- <p v-else class="muted">暂无最新监测数据</p>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="detail-section">
|
|
|
- <h4>维修记录</h4>
|
|
|
- <template v-if="latestMaintainEntries.length">
|
|
|
- <dl>
|
|
|
- <dt v-for="item in latestMaintainEntries" :key="item.label">{{ item.label }}</dt>
|
|
|
- <dd v-for="item in latestMaintainEntries" :key="`value-${item.label}`">{{ item.value }}</dd>
|
|
|
- </dl>
|
|
|
- </template>
|
|
|
- <p v-else class="muted">暂无维修记录</p>
|
|
|
- </section>
|
|
|
- </template>
|
|
|
</div>
|
|
|
- </el-drawer>
|
|
|
- </template>
|
|
|
- </WaterGisPageShell>
|
|
|
- </el-card>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 设备详情 -->
|
|
|
+ <el-dialog
|
|
|
+ title="设备详情"
|
|
|
+ v-model="detailVisible"
|
|
|
+ width="800px"
|
|
|
+ append-to-body
|
|
|
+ class="detail-dialog"
|
|
|
+ >
|
|
|
+ <el-alert
|
|
|
+ v-if="detailError"
|
|
|
+ type="error"
|
|
|
+ show-icon
|
|
|
+ :closable="false"
|
|
|
+ title="设备详情加载失败,请关闭后重试"
|
|
|
+ />
|
|
|
+ <template v-if="selectedDetail?.equipmentInfo">
|
|
|
+ <div class="dlg-summary">
|
|
|
+ <div class="summary-name">{{ selectedDetail.equipmentInfo.equipmentName || "未命名设备" }}</div>
|
|
|
+ <div class="summary-code">{{ selectedDetail.equipmentInfo.equipmentCode }}</div>
|
|
|
+ <span class="summary-tag">
|
|
|
+ {{ selectedDetail.typeInfo?.typeName || "未分类" }}
|
|
|
+ </span>
|
|
|
+ <div class="status-group">
|
|
|
+ <span class="badge" :class="selectedDetail.statusInfo?.onlineStatus === 1 ? 'online' : 'offline'">
|
|
|
+ <span class="badge-dot"></span>
|
|
|
+ <span class="badge-text">{{ selectedDetail.statusInfo?.onlineStatus === 1 ? "在线" : "离线" }}</span>
|
|
|
+ </span>
|
|
|
+ <span v-if="selectedDetail.statusInfo?.alarmStatus === 1" class="badge alarm">
|
|
|
+ <el-icon class="badge-icon"><WarningFilled /></el-icon>
|
|
|
+ <span class="badge-text">报警</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-section-title detail-section-title">基础信息</div>
|
|
|
+ <el-descriptions :column="2" border size="small" class="dlg-grid">
|
|
|
+ <el-descriptions-item label="设备编码">{{ selectedDetail.equipmentInfo.equipmentCode || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备名称">{{ selectedDetail.equipmentInfo.equipmentName || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备类型">{{ selectedDetail.typeInfo?.typeName || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="所属片区">{{ districtLabel(selectedDetail.equipmentInfo.district) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备型号">{{ selectedDetail.equipmentInfo.equipmentModel || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="制造商">{{ selectedDetail.equipmentInfo.manufacturer || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="生产日期">{{ selectedDetail.equipmentInfo.productionDate || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="规格参数">{{ selectedDetail.equipmentInfo.equipmentSpec || "-" }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="form-section-title detail-section-title">资产信息</div>
|
|
|
+ <el-descriptions :column="2" border size="small" class="dlg-grid">
|
|
|
+ <el-descriptions-item label="资产状态">{{ currentStatusMeta(selectedDetail.statusInfo?.currentStatus).text }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="资产价值(元)">{{ selectedDetail.equipmentInfo.assetValue ?? "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="使用年限(年)">{{ selectedDetail.equipmentInfo.useLife ?? "-" }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="form-section-title detail-section-title">部署运维</div>
|
|
|
+ <el-descriptions :column="2" border size="small" class="dlg-grid">
|
|
|
+ <el-descriptions-item label="安装位置" :span="2">{{ selectedDetail.equipmentInfo.equipmentLocation || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="经度">{{ selectedDetail.equipmentInfo.longitude ?? "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="纬度">{{ selectedDetail.equipmentInfo.latitude ?? "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装时间">{{ selectedDetail.equipmentInfo.installTime || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="维护人员">{{ selectedDetail.equipmentInfo.maintainer || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系电话" :span="2">{{ selectedDetail.equipmentInfo.maintainerPhone || "-" }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="备注" :span="2">{{ selectedDetail.equipmentInfo.remark || "-" }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div v-if="selectedDetail.equipmentInfo.equipmentImage" class="detail-image">
|
|
|
+ <div class="field-label">设备图片</div>
|
|
|
+ <el-image
|
|
|
+ :src="resolveImageUrl(selectedDetail.equipmentInfo.equipmentImage)"
|
|
|
+ :preview-src-list="[resolveImageUrl(selectedDetail.equipmentInfo.equipmentImage)]"
|
|
|
+ preview-teleported
|
|
|
+ fit="cover"
|
|
|
+ class="field-media"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-empty v-else description="未选中设备" />
|
|
|
+ <template #footer>
|
|
|
+ <div class="dlg-footer">
|
|
|
+ <el-button class="btn-ghost" @click="detailVisible = false">关 闭</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, nextTick, onMounted, ref } from 'vue'
|
|
|
-import { Aim, CircleCheck, CircleClose, MapLocation, Odometer, Refresh, Search, Warning } from '@element-plus/icons-vue'
|
|
|
+import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
+import { CircleCheck, CircleClose, MapLocation, Monitor, Refresh, Warning, WarningFilled } from '@element-plus/icons-vue'
|
|
|
import GisMapCanvas from '@/components/GisMapCanvas.vue'
|
|
|
-import WaterGisPageShell from '@/components/WaterGisPageShell.vue'
|
|
|
-import { getMonitoringPointColor, getMonitoringPointIcon, getMonitoringPointLabel, getMonitoringPointStatus, getMonitoringPointTagType } from '@/utils/gisMapColors'
|
|
|
-import {
|
|
|
- getDeviceGisAbnormalPoints,
|
|
|
- getDeviceGisAllPoints,
|
|
|
- getDeviceGisDetail,
|
|
|
- getDeviceGisStatistics
|
|
|
-} from '@/api/pipeNetwork/waterSupply'
|
|
|
+import { getDeviceGisAllPoints } from '@/api/pipeNetwork/waterSupply'
|
|
|
+import { getEquipmentDetail } from '@/api/pipeNetwork/pdeviceInform'
|
|
|
import { normalizeDeviceGisPoints, normalizeDeviceRow } from '@/utils/waterSupplyUiModel'
|
|
|
-import { unwrapResponse } from '@/utils/waterSupplyModel'
|
|
|
-import { useDict } from '@/utils/dict'
|
|
|
+import { gisPointStatusIcons } from '@/utils/gisMapColors'
|
|
|
+import { buildMinioAttachmentUrl } from '@/utils/minioAttachment'
|
|
|
|
|
|
-const { alarm_level: alarmLevels } = useDict('alarm_level')
|
|
|
-const alarmLevelText = computed(() => alarmLevels.value.find(item => String(item.value) === String(selectedDetail.value?.alarmLevel))?.label || selectedDetail.value?.alarmLevelText || '—')
|
|
|
+const mapCenter = { lng: 110.3965, lat: 28.4638 }
|
|
|
|
|
|
-const searchKeyword = ref('')
|
|
|
-const selectedTypeId = ref('')
|
|
|
-const statusFilters = ref(['normal', 'alarm', 'warning', 'workOrder', 'offline'])
|
|
|
const allDevices = ref([])
|
|
|
-const abnormalDevices = ref([])
|
|
|
-const gisStatistics = ref({})
|
|
|
const loading = ref(false)
|
|
|
-const loadError = ref('')
|
|
|
const mapError = ref('')
|
|
|
const gisMapRef = ref(null)
|
|
|
const detailVisible = ref(false)
|
|
|
@@ -261,131 +190,147 @@ const detailLoading = ref(false)
|
|
|
const detailError = ref('')
|
|
|
const selectedDetail = ref(null)
|
|
|
|
|
|
-const typeOptions = computed(() => {
|
|
|
- const typeMap = new Map()
|
|
|
- allDevices.value.forEach((device) => {
|
|
|
- const id = device.equipmentTypeId || 'unknown'
|
|
|
- const item = typeMap.get(id) || {
|
|
|
- value: id,
|
|
|
- label: device.equipmentTypeName || '未知类型',
|
|
|
- count: 0
|
|
|
- }
|
|
|
- item.count += 1
|
|
|
- typeMap.set(id, item)
|
|
|
- })
|
|
|
- return [...typeMap.values()].sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
|
|
|
-})
|
|
|
-
|
|
|
-const summary = computed(() => ({
|
|
|
+const statistics = computed(() => ({
|
|
|
total: allDevices.value.length,
|
|
|
online: allDevices.value.filter((device) => Number(device.onlineStatus) === 1).length,
|
|
|
offline: allDevices.value.filter((device) => Number(device.onlineStatus) !== 1).length,
|
|
|
- abnormal: abnormalDevices.value.length
|
|
|
+ alarm: allDevices.value.filter((device) => Number(device.alarmStatus) === 1).length
|
|
|
}))
|
|
|
|
|
|
-const filteredDevices = computed(() => {
|
|
|
- const keyword = searchKeyword.value.trim().toLowerCase()
|
|
|
- return allDevices.value.filter((device) => {
|
|
|
- const matchedType = !selectedTypeId.value || device.equipmentTypeId === selectedTypeId.value
|
|
|
- const matchedKeyword = !keyword || [device.equipmentName, device.equipmentCode, device.equipmentLocation]
|
|
|
- .some((value) => String(value || '').toLowerCase().includes(keyword))
|
|
|
- const statusKey = getStatusKey(device)
|
|
|
- return matchedType && matchedKeyword && statusFilters.value.includes(statusKey)
|
|
|
+// 标记图预加载:在线=绿、离线=灰,报警时在其右上角叠加红色标记
|
|
|
+const iconsReady = ref(false)
|
|
|
+const markerImages = {
|
|
|
+ green: typeof Image !== 'undefined' ? new Image() : null,
|
|
|
+ grey: typeof Image !== 'undefined' ? new Image() : null,
|
|
|
+ red: typeof Image !== 'undefined' ? new Image() : null
|
|
|
+}
|
|
|
+const iconCache = new Map()
|
|
|
+
|
|
|
+function loadMarkerImages() {
|
|
|
+ const entries = [
|
|
|
+ ['green', gisPointStatusIcons.normal],
|
|
|
+ ['grey', gisPointStatusIcons.offline],
|
|
|
+ ['red', gisPointStatusIcons.alarm]
|
|
|
+ ]
|
|
|
+ let pending = entries.length
|
|
|
+ if (!pending) {
|
|
|
+ iconsReady.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const done = () => {
|
|
|
+ pending -= 1
|
|
|
+ if (pending <= 0) iconsReady.value = true
|
|
|
+ }
|
|
|
+ entries.forEach(([key, url]) => {
|
|
|
+ const img = markerImages[key]
|
|
|
+ if (!img) return done()
|
|
|
+ img.onload = done
|
|
|
+ img.onerror = done
|
|
|
+ img.src = url
|
|
|
})
|
|
|
-})
|
|
|
-
|
|
|
-const mapPoints = computed(() => filteredDevices.value.map((device) => ({
|
|
|
- ...device,
|
|
|
- lng: device._longitude,
|
|
|
- lat: device._latitude,
|
|
|
- name: device.equipmentName || device.equipmentCode
|
|
|
-})))
|
|
|
-
|
|
|
-const latestDataEntries = computed(() => {
|
|
|
- const data = selectedDetail.value?.latestData || {}
|
|
|
- return Object.entries(data).map(([key, value]) => ({
|
|
|
- label: key,
|
|
|
- value: value ?? '-'
|
|
|
- }))
|
|
|
-})
|
|
|
-
|
|
|
-const latestMaintainEntries = computed(() => {
|
|
|
- const record = selectedDetail.value?.latestMaintain
|
|
|
- if (!record) return []
|
|
|
- const preferredKeys = ['maintainDate', 'maintainType', 'maintainContent', 'maintainPerson']
|
|
|
- return preferredKeys
|
|
|
- .filter((key) => record[key] !== undefined && record[key] !== null && record[key] !== '')
|
|
|
- .map((key) => ({ label: maintainLabels[key] || key, value: record[key] }))
|
|
|
-})
|
|
|
-
|
|
|
-const maintainLabels = {
|
|
|
- maintainDate: '维修日期',
|
|
|
- maintainType: '维修类型',
|
|
|
- maintainContent: '维修内容',
|
|
|
- maintainPerson: '维修人员'
|
|
|
}
|
|
|
|
|
|
-function getStatusKey(device) {
|
|
|
- return getMonitoringPointStatus(device)
|
|
|
-}
|
|
|
+function composeAlarmIcon(online) {
|
|
|
+ const base = online ? markerImages.green : markerImages.grey
|
|
|
+ const red = markerImages.red
|
|
|
+ if (!base || !red || !base.complete || !red.complete) {
|
|
|
+ return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
|
|
|
+ }
|
|
|
+ const size = 32
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.width = size
|
|
|
+ canvas.height = size
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ ctx.drawImage(base, 0, 0, size, size)
|
|
|
+ const badge = Math.round(size * 0.5)
|
|
|
+ ctx.drawImage(red, size - badge + 2, 1, badge, badge)
|
|
|
+ return canvas.toDataURL()
|
|
|
+}
|
|
|
+
|
|
|
+function getDeviceIcon(device) {
|
|
|
+ const online = Number(device.onlineStatus) === 1
|
|
|
+ const alarm = Number(device.alarmStatus) === 1
|
|
|
+ if (!alarm) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
|
|
|
+ if (!iconsReady.value) return online ? gisPointStatusIcons.normal : gisPointStatusIcons.offline
|
|
|
+ const key = online ? 'online' : 'offline'
|
|
|
+ if (!iconCache.has(key)) iconCache.set(key, composeAlarmIcon(online))
|
|
|
+ return iconCache.get(key)
|
|
|
+}
|
|
|
+
|
|
|
+// 图例状态筛选:隐藏后对应状态的设备在地图上不显示
|
|
|
+const legendVisible = reactive({ online: true, offline: true, alarm: true })
|
|
|
+
|
|
|
+function toggleLegend(key) {
|
|
|
+ legendVisible[key] = !legendVisible[key]
|
|
|
+}
|
|
|
+
|
|
|
+function isDeviceVisible(device) {
|
|
|
+ const online = Number(device.onlineStatus) === 1
|
|
|
+ const alarm = Number(device.alarmStatus) === 1
|
|
|
+ if (online && !legendVisible.online) return false
|
|
|
+ if (!online && !legendVisible.offline) return false
|
|
|
+ if (alarm && !legendVisible.alarm) return false
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const mapPoints = computed(() => {
|
|
|
+ // 依赖 iconsReady 与图例筛选状态,图片就绪或切换筛选后重算
|
|
|
+ void iconsReady.value
|
|
|
+ void legendVisible.online
|
|
|
+ void legendVisible.offline
|
|
|
+ void legendVisible.alarm
|
|
|
+ return allDevices.value
|
|
|
+ .filter(isDeviceVisible)
|
|
|
+ .map((device) => ({
|
|
|
+ ...device,
|
|
|
+ lng: device._longitude,
|
|
|
+ lat: device._latitude,
|
|
|
+ name: device.equipmentName || device.equipmentCode,
|
|
|
+ iconUrl: getDeviceIcon(device)
|
|
|
+ }))
|
|
|
+})
|
|
|
|
|
|
-function getStatusStyle(device) {
|
|
|
- return { color: getMonitoringPointColor(device), text: getMonitoringPointLabel(device) }
|
|
|
-}
|
|
|
+// 所属片区:与后端 DistrictEnum 对齐(0-城南 1-城北 2-太常)
|
|
|
+const districtOptions = [
|
|
|
+ { label: '城南', value: 0 },
|
|
|
+ { label: '城北', value: 1 },
|
|
|
+ { label: '太常', value: 2 }
|
|
|
+]
|
|
|
|
|
|
-function statusText(device) {
|
|
|
- return getStatusStyle(device).text
|
|
|
+function districtLabel(code) {
|
|
|
+ const hit = districtOptions.find((item) => item.value === code)
|
|
|
+ return hit ? hit.label : '-'
|
|
|
}
|
|
|
|
|
|
-function statusTagType(device) {
|
|
|
- return getMonitoringPointTagType(device)
|
|
|
+// 资产状态:0-在用,1-维修,2-报废
|
|
|
+function currentStatusMeta(value) {
|
|
|
+ const map = {
|
|
|
+ 0: { text: '在用', type: 'success' },
|
|
|
+ 1: { text: '维修', type: 'warning' },
|
|
|
+ 2: { text: '报废', type: 'danger' }
|
|
|
+ }
|
|
|
+ return map[value] || { text: '未知', type: 'info' }
|
|
|
}
|
|
|
|
|
|
-function deviceStatusTagType(status) {
|
|
|
- const value = Number(status)
|
|
|
- if (value === 3) return 'warning'
|
|
|
- if (value === 4) return 'danger'
|
|
|
- if (value === 5) return 'info'
|
|
|
- return 'success'
|
|
|
+function resolveImageUrl(url) {
|
|
|
+ return buildMinioAttachmentUrl(url)
|
|
|
}
|
|
|
|
|
|
async function loadData() {
|
|
|
loading.value = true
|
|
|
- loadError.value = ''
|
|
|
+ mapError.value = ''
|
|
|
try {
|
|
|
- const [allResponse, abnormalResponse, statisticsResponse] = await Promise.all([
|
|
|
- getDeviceGisAllPoints(),
|
|
|
- getDeviceGisAbnormalPoints(),
|
|
|
- getDeviceGisStatistics()
|
|
|
- ])
|
|
|
+ const allResponse = await getDeviceGisAllPoints()
|
|
|
allDevices.value = normalizeDeviceGisPoints(allResponse).map(normalizeDeviceRow)
|
|
|
- abnormalDevices.value = normalizeDeviceGisPoints(abnormalResponse).map(normalizeDeviceRow)
|
|
|
- gisStatistics.value = unwrapResponse(statisticsResponse) || {}
|
|
|
await nextTick()
|
|
|
gisMapRef.value?.fitBounds()
|
|
|
} catch (error) {
|
|
|
- loadError.value = '监测设备数据加载失败,请点击右上角“刷新”重试'
|
|
|
+ mapError.value = '监测设备数据加载失败,请点击右上角“刷新”重试'
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function resetFilters() {
|
|
|
- searchKeyword.value = ''
|
|
|
- selectedTypeId.value = ''
|
|
|
- statusFilters.value = ['normal', 'alarm', 'warning', 'workOrder', 'offline']
|
|
|
- loadData()
|
|
|
-}
|
|
|
-
|
|
|
-function fitBounds() {
|
|
|
- gisMapRef.value?.fitBounds()
|
|
|
-}
|
|
|
-
|
|
|
-function locateDevice(device) {
|
|
|
- gisMapRef.value?.locatePoint({ lng: device._longitude, lat: device._latitude })
|
|
|
- openDetail(device.equipmentId)
|
|
|
-}
|
|
|
-
|
|
|
async function openDetail(equipmentId) {
|
|
|
if (!equipmentId) return
|
|
|
detailVisible.value = true
|
|
|
@@ -393,172 +338,349 @@ async function openDetail(equipmentId) {
|
|
|
selectedDetail.value = null
|
|
|
detailError.value = ''
|
|
|
try {
|
|
|
- const detail = unwrapResponse(await getDeviceGisDetail(equipmentId))
|
|
|
- selectedDetail.value = normalizeDeviceRow(detail)
|
|
|
+ const detail = await getEquipmentDetail(equipmentId)
|
|
|
+ selectedDetail.value = detail?.data || detail
|
|
|
} catch (error) {
|
|
|
- selectedDetail.value = null
|
|
|
detailError.value = '设备详情加载失败,请关闭后重试'
|
|
|
} finally {
|
|
|
detailLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-onMounted(loadData)
|
|
|
+onMounted(() => {
|
|
|
+ loadMarkerImages()
|
|
|
+ loadData()
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.header-actions {
|
|
|
+.gis-page {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f9f9f9;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+}
|
|
|
+
|
|
|
+/* 页头 */
|
|
|
+.page-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 10px;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
-.search-input,
|
|
|
-.type-select {
|
|
|
- width: 240px;
|
|
|
+
|
|
|
+.page-head-titles {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
-.panel-section h3 {
|
|
|
- margin: 0 0 8px;
|
|
|
- font-size: 14px;
|
|
|
- color: #29567e;
|
|
|
+.page-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0a1628;
|
|
|
}
|
|
|
-.status-filter {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
+
|
|
|
+.page-title-icon {
|
|
|
+ color: #014db2;
|
|
|
+ font-size: 22px;
|
|
|
}
|
|
|
-.type-list,
|
|
|
-.device-result-list {
|
|
|
+
|
|
|
+/* KPI */
|
|
|
+.kpi-row {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 6px;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
-.type-list {
|
|
|
- max-height: 160px;
|
|
|
+
|
|
|
+.kpi {
|
|
|
+ flex: 1 1 0;
|
|
|
+ min-width: 0;
|
|
|
+ border-radius: 12px;
|
|
|
}
|
|
|
-.type-list button,
|
|
|
-.device-result-list button {
|
|
|
- width: 100%;
|
|
|
- border: 1px solid #dbe8f5;
|
|
|
- background: #fff;
|
|
|
- border-radius: 8px;
|
|
|
- padding: 8px 10px;
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
+
|
|
|
+.kpi :deep(.el-card__body) {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 20px;
|
|
|
}
|
|
|
-.type-list button {
|
|
|
+
|
|
|
+.kpi-icon {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ flex: 0 0 44px;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+.kpi-icon.total {
|
|
|
+ background: #edf3fc;
|
|
|
+ color: #014db2;
|
|
|
+}
|
|
|
+
|
|
|
+.kpi-icon.online {
|
|
|
+ background: #e7f8f1;
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+.kpi-icon.offline {
|
|
|
+ background: #fef4e2;
|
|
|
+ color: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.kpi-icon.alarm {
|
|
|
+ background: #fdecec;
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.kpi-label {
|
|
|
font-size: 13px;
|
|
|
- color: #33556f;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.kpi-value {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1;
|
|
|
+ color: #0a1628;
|
|
|
+}
|
|
|
+
|
|
|
+/* 地图 */
|
|
|
+.map-card {
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-card :deep(.el-card__body) {
|
|
|
+ padding: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-wrap {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: calc(100vh - 340px);
|
|
|
+ min-height: 480px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
-.type-list button.active {
|
|
|
- border-color: #4b9ce7;
|
|
|
- background: #edf7ff;
|
|
|
- color: #1662a8;
|
|
|
+
|
|
|
+.map-legend {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ left: 12px;
|
|
|
+ z-index: 9999;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(255, 255, 255, 0.92);
|
|
|
+ box-shadow: 0 2px 8px rgba(15, 23, 42, 0.12);
|
|
|
+ pointer-events: auto;
|
|
|
}
|
|
|
-.legend-panel {
|
|
|
- border-top: 1px solid #e4eef8;
|
|
|
- padding-top: 12px;
|
|
|
+
|
|
|
+.legend-title {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #94a3b8;
|
|
|
}
|
|
|
+
|
|
|
.legend-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
margin-bottom: 6px;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ font: inherit;
|
|
|
font-size: 13px;
|
|
|
color: #4d667d;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: opacity 0.15s, color 0.15s;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-row:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
|
+
|
|
|
+.legend-row:hover {
|
|
|
+ color: #1f3b57;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-row.is-off {
|
|
|
+ opacity: 0.4;
|
|
|
+ text-decoration: line-through;
|
|
|
+}
|
|
|
+
|
|
|
.legend-dot {
|
|
|
width: 10px;
|
|
|
height: 10px;
|
|
|
border-radius: 50%;
|
|
|
}
|
|
|
-.legend-dot.normal { background: #2ecc71; }
|
|
|
-.legend-dot.alarm { background: #e74c3c; }
|
|
|
-.legend-dot.warning { background: #f1c40f; }
|
|
|
-.legend-dot.workOrder { background: #3498db; }
|
|
|
+
|
|
|
+.legend-dot.online { background: #2ecc71; }
|
|
|
.legend-dot.offline { background: #95a5a6; }
|
|
|
-.result-section {
|
|
|
- flex: 1;
|
|
|
- min-height: 0;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
+.legend-dot.alarm { background: #e74c3c; }
|
|
|
+
|
|
|
+.map-error {
|
|
|
+ position: absolute;
|
|
|
+ left: 16px;
|
|
|
+ bottom: 16px;
|
|
|
+ width: 360px;
|
|
|
+ z-index: 2;
|
|
|
}
|
|
|
-.device-result-list {
|
|
|
- flex: none;
|
|
|
+
|
|
|
+.map-empty {
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 2;
|
|
|
}
|
|
|
-.device-result-list button {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: minmax(0, 1fr) auto;
|
|
|
- gap: 3px 8px;
|
|
|
+
|
|
|
+/* 详情弹窗 */
|
|
|
+.dlg-summary {
|
|
|
+ display: flex;
|
|
|
align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding: 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f5f9ff;
|
|
|
}
|
|
|
-.device-name {
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
- font-weight: 600;
|
|
|
- color: #274b6d;
|
|
|
+
|
|
|
+.summary-name {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #014db2;
|
|
|
}
|
|
|
-.device-meta {
|
|
|
- grid-column: 1 / -1;
|
|
|
+
|
|
|
+.summary-code {
|
|
|
font-size: 12px;
|
|
|
- color: #7793aa;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #6b7280;
|
|
|
}
|
|
|
|
|
|
+.summary-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 3px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #014db2;
|
|
|
+}
|
|
|
|
|
|
-.device-detail {
|
|
|
- min-height: 240px;
|
|
|
+.dlg-grid {
|
|
|
+ margin-top: 16px;
|
|
|
}
|
|
|
-.detail-hero h3,
|
|
|
-.detail-section h4 {
|
|
|
- margin: 0 0 8px;
|
|
|
- color: #1c4b78;
|
|
|
+
|
|
|
+.detail-image {
|
|
|
+ margin-top: 16px;
|
|
|
}
|
|
|
-.detail-hero p {
|
|
|
- margin: 0 0 10px;
|
|
|
- color: #678197;
|
|
|
- font-size: 13px;
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ margin-bottom: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #6b7280;
|
|
|
}
|
|
|
-.status-tags {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 6px;
|
|
|
+
|
|
|
+.field-media {
|
|
|
+ width: 320px;
|
|
|
+ max-width: 100%;
|
|
|
+ height: 180px;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid #e6e6eb;
|
|
|
+}
|
|
|
+
|
|
|
+.form-section-title {
|
|
|
+ margin: 8px 0 12px;
|
|
|
+ padding-left: 10px;
|
|
|
+ border-left: 3px solid #014db2;
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
-.detail-section {
|
|
|
+
|
|
|
+.detail-section-title {
|
|
|
margin-top: 18px;
|
|
|
- padding-top: 12px;
|
|
|
- border-top: 1px solid #e5eef7;
|
|
|
-}
|
|
|
-.detail-section dl {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 88px minmax(0, 1fr);
|
|
|
- gap: 7px 10px;
|
|
|
- margin: 0;
|
|
|
- font-size: 13px;
|
|
|
}
|
|
|
-.detail-section dt {
|
|
|
- color: #71889c;
|
|
|
+
|
|
|
+.status-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
-.detail-section dd {
|
|
|
- margin: 0;
|
|
|
- color: #2b4c67;
|
|
|
- overflow-wrap: anywhere;
|
|
|
+
|
|
|
+.badge {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
}
|
|
|
-.muted {
|
|
|
- margin: 0;
|
|
|
- color: #7d93a6;
|
|
|
+
|
|
|
+.badge-dot {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.badge-text {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.badge.online .badge-dot { background: #10b981; }
|
|
|
+.badge.online .badge-text { color: #10b981; }
|
|
|
+.badge.offline .badge-dot { background: #f59e0b; }
|
|
|
+.badge.offline .badge-text { color: #f59e0b; }
|
|
|
+.badge.alarm .badge-text { color: #ef4444; }
|
|
|
+
|
|
|
+.badge-icon {
|
|
|
font-size: 13px;
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.dlg-footer {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 10px;
|
|
|
}
|
|
|
+
|
|
|
@media (max-width: 1200px) {
|
|
|
- .search-input,
|
|
|
- .type-select {
|
|
|
- width: 180px;
|
|
|
+ .kpi-row {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .kpi {
|
|
|
+ flex: 1 1 45%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .map-wrap {
|
|
|
+ height: 520px;
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
-<style lang="scss">
|
|
|
-@use './waterGisPageTheme.scss';
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .kpi {
|
|
|
+ flex: 1 1 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-head {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|