|
@@ -1,920 +1,1221 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="app-container">
|
|
|
|
|
- <!-- GIS 地图展示区 -->
|
|
|
|
|
- <div class="map-section" style="margin-bottom: 16px;">
|
|
|
|
|
- <div class="map-tabs">
|
|
|
|
|
- <div class="map-tab active">易积水点分布</div>
|
|
|
|
|
|
|
+ <div class="app-container waterlogging-page">
|
|
|
|
|
+ <!-- 易积水点分布 -->
|
|
|
|
|
+ <el-card shadow="never" class="map-card">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <div class="card-title">
|
|
|
|
|
+ <span>易积水点分布</span>
|
|
|
|
|
+ <span class="card-sub">按当前筛选条件叠加展示,共 {{ mapRecords.length }} 个带坐标点位</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-actions">
|
|
|
|
|
+ <el-button size="small" :icon="Refresh" :loading="loading" @click="loadAll">刷新</el-button>
|
|
|
|
|
+ <el-button size="small" :icon="Aim" @click="fitMap">全图</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="map-wrap">
|
|
|
|
|
+ <GisMapCanvas ref="mapRef" :points="mapPoints" :lines="[]" :enable-clustering="true"
|
|
|
|
|
+ cluster-label="积水点" @ready="onMapReady" @point-click="handleMapPointClick" />
|
|
|
|
|
+ <div v-if="!mapPoints.length" class="map-empty">
|
|
|
|
|
+ {{ loading ? '正在加载…' : '当前筛选条件下没有可展示的点位(需录入经纬度)' }}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div id="yjsdglMainMap" class="map-container" style="height: 350px;"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 搜索区域 -->
|
|
|
|
|
- <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
|
|
|
|
|
- <el-form-item label="点位名称">
|
|
|
|
|
- <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 200px" />
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item label="地点">
|
|
|
|
|
- <el-input v-model="queryParams.area" placeholder="请输入" clearable style="width: 200px" />
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item>
|
|
|
|
|
- <el-button type="primary" @click="handleQuery">搜索</el-button>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-form>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 操作按钮区域 -->
|
|
|
|
|
- <div class="toolbar">
|
|
|
|
|
- <el-button type="primary" @click="handleAdd">新增</el-button>
|
|
|
|
|
- <el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">删除</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 数据表格 -->
|
|
|
|
|
- <el-table v-loading="loading" :data="filteredList" border style="width: 100%"
|
|
|
|
|
- @selection-change="handleSelectionChange">
|
|
|
|
|
- <el-table-column type="selection" width="55" align="center" />
|
|
|
|
|
- <el-table-column label="监测点名称" prop="name" min-width="160" />
|
|
|
|
|
- <el-table-column label="地点" prop="area" min-width="100" />
|
|
|
|
|
- <el-table-column label="数据时间" prop="time" min-width="120" />
|
|
|
|
|
- <el-table-column label="易积水点数量" prop="waterloggingCount" min-width="100" />
|
|
|
|
|
- <el-table-column label="积水量" prop="waterVolume" min-width="100" />
|
|
|
|
|
- <el-table-column label="积水程度" min-width="120">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-tag :type="getWaterloggingLevelType(row.waterloggingLevel)" size="small">
|
|
|
|
|
- {{ getWaterloggingLevelLabel(row.waterloggingLevel) }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column label="关联设备" min-width="160">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- {{ equipmentNameMap[row.id] || '' }}
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column label="操作" width="240" align="center">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-button link type="primary" @click="handleView(row)">查看</el-button>
|
|
|
|
|
- <el-button link type="primary" @click="handleEdit(row)">修改</el-button>
|
|
|
|
|
- <el-button link type="primary" @click="handleBind(row)">关联设备</el-button>
|
|
|
|
|
- <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- </el-table>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 分页 -->
|
|
|
|
|
- <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
|
|
|
|
|
- :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
- style="margin-top: 16px; justify-content: flex-end;" @size-change="loadData" @current-change="loadData" />
|
|
|
|
|
-
|
|
|
|
|
- <!-- 查看详情对话框 -->
|
|
|
|
|
- <el-drawer v-model="dialogVisible" title="详情" size="700px" :before-close="handleClose" @opened="onDialogOpened" class="detail-drawer" direction="rtl">
|
|
|
|
|
- <div class="detail-info">
|
|
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
- <el-col :span="12">
|
|
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">监测点名称:</span>
|
|
|
|
|
- <span class="info-value">{{ currentRow.name }}</span>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <div class="map-legend">
|
|
|
|
|
+ <span class="legend-title">积水程度:</span>
|
|
|
|
|
+ <span v-for="item in levelOptions" :key="item.value" class="legend-item">
|
|
|
|
|
+ <i :style="{ background: levelColor(item.value) }"></i>{{ item.label }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="legend-tip">点击地图标记可直接查看关联摄像头</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 查询条件 -->
|
|
|
|
|
+ <el-card shadow="never" class="filter-card">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <div class="card-title"><span>查询条件</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <el-form :model="query" :inline="true" label-width="80px" class="filter-form">
|
|
|
|
|
+ <el-form-item label="点位编码">
|
|
|
|
|
+ <el-input v-model="query.pointCode" placeholder="请输入" clearable style="width: 160px"
|
|
|
|
|
+ @keyup.enter="handleQuery" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="点位名称">
|
|
|
|
|
+ <el-input v-model="query.pointName" placeholder="请输入" clearable style="width: 160px"
|
|
|
|
|
+ @keyup.enter="handleQuery" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="所属区域">
|
|
|
|
|
+ <el-input v-model="query.district" placeholder="请输入" clearable style="width: 160px"
|
|
|
|
|
+ @keyup.enter="handleQuery" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="积水程度">
|
|
|
|
|
+ <el-select v-model="query.waterloggingLevel" placeholder="请选择" clearable style="width: 140px">
|
|
|
|
|
+ <el-option v-for="item in levelOptions" :key="item.value" :label="item.label"
|
|
|
|
|
+ :value="Number(item.value)" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="风险等级">
|
|
|
|
|
+ <el-select v-model="query.riskLevel" placeholder="请选择" clearable style="width: 130px">
|
|
|
|
|
+ <el-option v-for="item in riskOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
|
|
|
|
|
+ <el-button :icon="RefreshLeft" @click="resetQuery">重置</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 点位清单 -->
|
|
|
|
|
+ <el-card shadow="never" class="table-card">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <div class="card-title"><span>点位清单</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="toolbar">
|
|
|
|
|
+ <el-button v-hasPermi="['drainage:waterlogging:add']" type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
|
|
|
|
|
+ <el-button v-hasPermi="['drainage:waterlogging:remove']" type="danger" :icon="Delete"
|
|
|
|
|
+ :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
|
|
|
|
+ 删除{{ selectedIds.length ? `(${selectedIds.length})` : '' }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table v-loading="loading" :data="tableData" border stripe style="width: 100%"
|
|
|
|
|
+ @selection-change="handleSelectionChange" @row-dblclick="openDetail">
|
|
|
|
|
+ <el-table-column type="selection" width="50" align="center" />
|
|
|
|
|
+ <el-table-column label="点位编码" prop="pointCode" min-width="110" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="点位名称" prop="pointName" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="所属区域" prop="district" min-width="110" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="位置描述" prop="location" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="积水程度" width="110" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag size="small" :type="levelTagType(row.waterloggingLevel)">{{ levelLabel(row.waterloggingLevel) }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="积水量(m³)" width="110" align="right">
|
|
|
|
|
+ <template #default="{ row }">{{ formatVolume(row.waterVolume) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="积水深度(cm)" width="115" align="right">
|
|
|
|
|
+ <template #default="{ row }">{{ formatNumber(row.waterDepth) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="风险等级" width="100" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag size="small" :type="riskTagType(row.riskLevel)">{{ riskLabel(row.riskLevel) }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="关联摄像头" min-width="160">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <template v-if="cameraListOf(row).length">
|
|
|
|
|
+ <el-tag v-for="cam in cameraListOf(row).slice(0, 2)" :key="cam.videoId" size="small" type="info"
|
|
|
|
|
+ class="camera-tag">{{ cam.deviceName }}</el-tag>
|
|
|
|
|
+ <span v-if="cameraListOf(row).length > 2" class="text-muted">等 {{ cameraListOf(row).length }} 个</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <span v-else class="text-muted">未关联</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="260" align="center" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
|
|
|
|
+ <el-button link type="primary" :disabled="!cameraListOf(row).length"
|
|
|
|
|
+ @click="playCameras(row)">视频</el-button>
|
|
|
|
|
+ <el-button v-hasPermi="['drainage:waterlogging:edit']" link type="primary" @click="handleEdit(row)">修改</el-button>
|
|
|
|
|
+ <el-button v-hasPermi="['drainage:waterlogging:edit']" link type="primary"
|
|
|
|
|
+ @click="openBind(row)">关联摄像头</el-button>
|
|
|
|
|
+ <el-button v-hasPermi="['drainage:waterlogging:remove']" link type="danger"
|
|
|
|
|
+ @click="handleDelete(row)">删除</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
|
|
|
|
|
+ :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
+ class="pager" @size-change="handleSizeChange" @current-change="loadPage" />
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 详情抽屉(只读) -->
|
|
|
|
|
+ <el-drawer v-model="detailVisible" title="易积水点详情" size="700px" direction="rtl"
|
|
|
|
|
+ @opened="onDetailOpened">
|
|
|
|
|
+ <el-descriptions :column="2" border class="detail-desc">
|
|
|
|
|
+ <el-descriptions-item label="点位编码">{{ fmt(detailRow.pointCode) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="点位名称">{{ fmt(detailRow.pointName) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="所属区域">{{ fmt(detailRow.district) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="积水程度">
|
|
|
|
|
+ <el-tag size="small" :type="levelTagType(detailRow.waterloggingLevel)">{{ levelLabel(detailRow.waterloggingLevel) }}</el-tag>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="积水量(m³)">{{ formatVolume(detailRow.waterVolume) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="积水深度(cm)">{{ formatNumber(detailRow.waterDepth) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="积水面积(m²)">{{ formatNumber(detailRow.waterArea) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="风险等级">
|
|
|
|
|
+ <el-tag size="small" :type="riskTagType(detailRow.riskLevel)">{{ riskLabel(detailRow.riskLevel) }}</el-tag>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="责任单位">{{ fmt(detailRow.dutyUnit) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="责任人">{{ fmt(detailRow.dutyPerson) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="联系电话">{{ fmt(detailRow.dutyPhone) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="创建时间">{{ fmt(detailRow.createTime) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="经度">{{ fmt(detailRow.longitude) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="纬度">{{ fmt(detailRow.latitude) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="位置描述" :span="2">{{ fmt(detailRow.location) }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="备注" :span="2">{{ fmt(detailRow.remark) }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="detail-section">
|
|
|
|
|
+ <div class="detail-section-title">关联摄像头</div>
|
|
|
|
|
+ <div v-if="detailCameras.length" class="camera-cards">
|
|
|
|
|
+ <div v-for="cam in detailCameras" :key="cam.videoId" class="camera-card">
|
|
|
|
|
+ <div class="camera-card-main">
|
|
|
|
|
+ <div class="camera-name">{{ cam.deviceName || cam.deviceCode }}</div>
|
|
|
|
|
+ <div class="camera-meta">{{ cam.districtName }} · {{ cam.location || '未填写位置' }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-button link type="primary" @click="playVideo(cam)">查看视频</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-empty v-else description="该点位尚未关联摄像头" :image-size="70" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="detail-section">
|
|
|
|
|
+ <div class="detail-section-title">点位位置</div>
|
|
|
|
|
+ <div class="detail-map-wrap">
|
|
|
|
|
+ <GisMapCanvas v-if="detailVisible" ref="detailMapRef" :points="detailPoints" :lines="[]"
|
|
|
|
|
+ :enable-clustering="false" :show-marker-name="false" @ready="onDetailMapReady" />
|
|
|
|
|
+ <div v-if="!detailPoints.length" class="map-empty">该点位未录入坐标</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="detailVisible = false">关 闭</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-drawer>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 新增 / 修改 -->
|
|
|
|
|
+ <el-dialog v-model="formVisible" :title="formTitle" width="820px" append-to-body
|
|
|
|
|
+ :close-on-click-modal="false" @opened="onFormOpened" @closed="onFormClosed">
|
|
|
|
|
+ <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="点位编码" prop="pointCode">
|
|
|
|
|
+ <el-input v-model="form.pointCode" placeholder="请输入点位编码" maxlength="64" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">地点:</span>
|
|
|
|
|
- <span class="info-value">{{ currentRow.area }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-form-item label="点位名称" prop="pointName">
|
|
|
|
|
+ <el-input v-model="form.pointName" placeholder="请输入点位名称" maxlength="200" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">数据时间:</span>
|
|
|
|
|
- <span class="info-value">{{ currentRow.time }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-form-item label="所属区域" prop="district">
|
|
|
|
|
+ <el-select v-model="form.district" placeholder="请选择或输入" clearable filterable allow-create
|
|
|
|
|
+ style="width: 100%">
|
|
|
|
|
+ <el-option v-for="item in filterOptions.district" :key="item" :label="item" :value="item" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">易积水点数量:</span>
|
|
|
|
|
- <span class="info-value">{{ currentRow.waterloggingPoints.length }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-form-item label="位置描述" prop="location">
|
|
|
|
|
+ <el-input v-model="form.location" placeholder="请输入位置描述" maxlength="500" clearable />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">积水量:</span>
|
|
|
|
|
- <span class="info-value">{{ currentRow.waterVolume }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-form-item label="积水程度" prop="waterloggingLevel">
|
|
|
|
|
+ <el-select v-model="form.waterloggingLevel" placeholder="请选择积水程度" style="width: 100%">
|
|
|
|
|
+ <el-option v-for="item in levelOptions" :key="item.value" :label="item.label"
|
|
|
|
|
+ :value="Number(item.value)" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">积水程度:</span>
|
|
|
|
|
- <span class="info-value">
|
|
|
|
|
- <el-tag :type="getWaterloggingLevelType(currentRow.waterloggingLevel)" size="small">
|
|
|
|
|
- {{ getWaterloggingLevelLabel(currentRow.waterloggingLevel) }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-form-item label="积水量(m³)" prop="waterVolume">
|
|
|
|
|
+ <el-input-number v-model="form.waterVolume" :min="0" :precision="2" :controls="false"
|
|
|
|
|
+ placeholder="请输入积水量" style="width: 100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 关联设备 -->
|
|
|
|
|
- <div v-if="viewEquipmentNames.length" class="detail-info" style="margin-top: 12px;">
|
|
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
- <el-col :span="24">
|
|
|
|
|
- <div class="info-item">
|
|
|
|
|
- <span class="info-label">关联设备:</span>
|
|
|
|
|
- <span class="info-value">{{ viewEquipmentNames.join('、') }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="积水深度(cm)" prop="waterDepth">
|
|
|
|
|
+ <el-input-number v-model="form.waterDepth" :min="0" :precision="2" :controls="false"
|
|
|
|
|
+ placeholder="请输入积水深度" style="width: 100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="积水面积(m²)" prop="waterArea">
|
|
|
|
|
+ <el-input-number v-model="form.waterArea" :min="0" :precision="2" :controls="false"
|
|
|
|
|
+ placeholder="请输入积水面积" style="width: 100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 地图区域 -->
|
|
|
|
|
- <div class="map-section">
|
|
|
|
|
- <div id="yjsdglViewMap" class="map-container"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-drawer>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 新增/修改对话框 -->
|
|
|
|
|
- <el-dialog v-model="editDialogVisible" :title="editDialogTitle" width="800px" :before-close="handleEditClose"
|
|
|
|
|
- @opened="onEditDialogOpened">
|
|
|
|
|
- <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="100px">
|
|
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="监测点名称" prop="name">
|
|
|
|
|
- <el-input v-model="editForm.name" placeholder="请输入监测点名称" />
|
|
|
|
|
|
|
+ <el-form-item label="风险等级" prop="riskLevel">
|
|
|
|
|
+ <el-select v-model="form.riskLevel" placeholder="请选择风险等级" clearable style="width: 100%">
|
|
|
|
|
+ <el-option v-for="item in riskOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
|
+ </el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="地点" prop="area">
|
|
|
|
|
- <el-input v-model="editForm.area" placeholder="请输入地点" />
|
|
|
|
|
|
|
+ <el-form-item label="责任单位" prop="dutyUnit">
|
|
|
|
|
+ <el-input v-model="form.dutyUnit" placeholder="请输入责任单位" maxlength="200" clearable />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="数据时间" prop="time">
|
|
|
|
|
- <el-date-picker v-model="editForm.time" type="date" placeholder="请选择日期" value-format="YYYY-MM-DD"
|
|
|
|
|
- style="width: 100%" />
|
|
|
|
|
|
|
+ <el-form-item label="责任人" prop="dutyPerson">
|
|
|
|
|
+ <el-input v-model="form.dutyPerson" placeholder="请输入责任人" maxlength="64" clearable />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="易积水点数量">
|
|
|
|
|
- <el-input :model-value="editWaterloggingPoints.length" disabled />
|
|
|
|
|
|
|
+ <el-form-item label="联系电话" prop="dutyPhone">
|
|
|
|
|
+ <el-input v-model="form.dutyPhone" placeholder="请输入联系电话" maxlength="32" clearable />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
- <el-row :gutter="20">
|
|
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="积水量" prop="waterVolume">
|
|
|
|
|
- <el-input v-model="editForm.waterVolume" type="number" placeholder="请输入积水量" />
|
|
|
|
|
|
|
+ <el-form-item label="经度" prop="longitude">
|
|
|
|
|
+ <el-input-number v-model="form.longitude" :precision="8" :controls="false" placeholder="地图点选或手输"
|
|
|
|
|
+ style="width: 100%" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="积水程度" prop="waterloggingLevel">
|
|
|
|
|
- <el-select v-model="editForm.waterloggingLevel" placeholder="请选择积水程度" style="width: 100%">
|
|
|
|
|
- <el-option label="未积水" :value="0" />
|
|
|
|
|
- <el-option label="轻度积水" :value="1" />
|
|
|
|
|
- <el-option label="中度积水" :value="2" />
|
|
|
|
|
- <el-option label="严重积水" :value="3" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
|
|
+ <el-form-item label="纬度" prop="latitude">
|
|
|
|
|
+ <el-input-number v-model="form.latitude" :precision="8" :controls="false" placeholder="地图点选或手输"
|
|
|
|
|
+ style="width: 100%" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
+ <el-form-item label="备注" prop="remark">
|
|
|
|
|
+ <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" show-word-limit
|
|
|
|
|
+ placeholder="请输入备注" />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
|
|
|
|
|
- <!-- 地图标点区域 -->
|
|
|
|
|
- <div class="map-section" style="margin-top: 16px;">
|
|
|
|
|
- <div class="map-tabs">
|
|
|
|
|
- <div class="map-tab active">易积水点</div>
|
|
|
|
|
- <div class="map-tab-action" @click="clearEditMarkers">清除所有标记</div>
|
|
|
|
|
|
|
+ <div class="form-map-section">
|
|
|
|
|
+ <div class="form-map-title">
|
|
|
|
|
+ <span>位置标注</span>
|
|
|
|
|
+ <span class="form-map-tip">在地图上点击可定位,坐标自动回填(内部按 WGS84 存储)</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div id="yjsdglEditMap" class="map-container" style="height: 350px;"></div>
|
|
|
|
|
- <div class="marker-count">
|
|
|
|
|
- 已标记 {{ editWaterloggingPoints.length }} 个易积水点
|
|
|
|
|
|
|
+ <div class="form-map-wrap">
|
|
|
|
|
+ <GisMapCanvas v-if="formVisible" ref="formMapRef" :points="formPoints" :lines="[]"
|
|
|
|
|
+ :enable-clustering="false" :show-marker-name="false" @ready="onFormMapReady"
|
|
|
|
|
+ @map-click="handleMapPick" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
- <el-button @click="handleEditClose">取 消</el-button>
|
|
|
|
|
- <el-button type="primary" @click="handleEditSubmit">确 定</el-button>
|
|
|
|
|
|
|
+ <el-button @click="formVisible = false">取 消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
|
|
|
- <!-- 关联设备弹框 -->
|
|
|
|
|
- <el-dialog v-model="bindDialogVisible" title="关联设备" width="700px" @close="closeBindDialog">
|
|
|
|
|
- <el-form :inline="true" :model="bindQueryParams" style="margin-bottom: 12px;">
|
|
|
|
|
- <el-form-item label="设备名称">
|
|
|
|
|
- <el-input v-model="bindQueryParams.equipmentName" placeholder="请输入设备名称" clearable />
|
|
|
|
|
|
|
+ <!-- 关联摄像头 -->
|
|
|
|
|
+ <el-dialog v-model="bindVisible" title="关联摄像头" width="780px" append-to-body :close-on-click-modal="false">
|
|
|
|
|
+ <div class="bind-tip">
|
|
|
|
|
+ 当前点位:<strong>{{ bindRow.pointName }}</strong>
|
|
|
|
|
+ <span class="text-muted">(已选 {{ bindSelected.length }} 个)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-form :inline="true" class="bind-filter">
|
|
|
|
|
+ <el-form-item label="摄像头名称">
|
|
|
|
|
+ <el-input v-model="bindKeyword" placeholder="请输入名称或编码" clearable style="width: 220px" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
<el-form-item>
|
|
|
- <el-button type="primary" @click="handleBindQuery">搜索</el-button>
|
|
|
|
|
|
|
+ <el-button @click="bindSelected = []">清空选择</el-button>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
- <el-table v-loading="bindLoading" :data="bindTableData" border row-key="equipmentId"
|
|
|
|
|
|
|
+ <el-table v-loading="bindLoading" :data="bindCandidates" border height="380" row-key="videoId"
|
|
|
@selection-change="handleBindSelectionChange" ref="bindTableRef">
|
|
@selection-change="handleBindSelectionChange" ref="bindTableRef">
|
|
|
- <el-table-column type="selection" width="55" align="center" :reserve-selection="true"
|
|
|
|
|
- :selectable="isEquipmentSelectable" />
|
|
|
|
|
- <el-table-column label="设备名称" prop="equipmentName" min-width="120" />
|
|
|
|
|
- <el-table-column label="设备编号" prop="equipmentCode" min-width="120" />
|
|
|
|
|
- <el-table-column label="设备类型" prop="equipmentTypeName" min-width="100" />
|
|
|
|
|
- <el-table-column label="安装位置" prop="installLocation" min-width="120" />
|
|
|
|
|
- <el-table-column label="关联状态" min-width="180">
|
|
|
|
|
|
|
+ <el-table-column type="selection" width="50" align="center" :selectable="isCameraSelectable"
|
|
|
|
|
+ :reserve-selection="true" />
|
|
|
|
|
+ <el-table-column label="摄像头名称" prop="deviceName" min-width="150" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="设备编码" prop="deviceCode" min-width="150" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="所属片区" prop="districtName" width="100" />
|
|
|
|
|
+ <el-table-column label="安装位置" prop="location" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="关联状态" min-width="160">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <span v-if="selectedEquipmentIds.includes(String(row.equipmentId))" style="color: #67c23a;">当前选择</span>
|
|
|
|
|
- <span v-else-if="boundEquipmentMap[String(row.equipmentId)]" style="color: #e6a23c;">
|
|
|
|
|
- 已关联:{{ boundEquipmentMap[String(row.equipmentId)] }}
|
|
|
|
|
- </span>
|
|
|
|
|
- <span v-else>未关联</span>
|
|
|
|
|
|
|
+ <span v-if="row.pointId === bindRow.pointId" class="status-self">当前点位</span>
|
|
|
|
|
+ <span v-else-if="row.pointId" class="status-occupied">已关联:{{ row.pointName || '其他点位' }}</span>
|
|
|
|
|
+ <span v-else class="text-muted">未关联</span>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
</el-table>
|
|
</el-table>
|
|
|
- <el-pagination v-model:current-page="bindPageNum" v-model:page-size="bindPageSize" :total="bindTotal"
|
|
|
|
|
- :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
- style="margin-top: 12px; justify-content: flex-end;" @size-change="loadBindData" @current-change="loadBindData" />
|
|
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
- <el-button @click="closeBindDialog">取 消</el-button>
|
|
|
|
|
- <el-button type="primary" @click="submitBind">确 定</el-button>
|
|
|
|
|
|
|
+ <el-button @click="bindVisible = false">取 消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="bindSubmitting" @click="submitBind">确 定</el-button>
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 摄像头实时视频 -->
|
|
|
|
|
+ <el-dialog v-model="playerVisible" :title="playerTitle" width="760px" append-to-body destroy-on-close>
|
|
|
|
|
+ <div class="player-wrap">
|
|
|
|
|
+ <DrainageLiveTile v-if="playerVideoId" :key="playerVideoId" :video-id="playerVideoId" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="playerOptions.length > 1" class="player-switch">
|
|
|
|
|
+ <span class="text-muted">该点位共 {{ playerOptions.length }} 个摄像头:</span>
|
|
|
|
|
+ <el-button v-for="cam in playerOptions" :key="cam.videoId" size="small"
|
|
|
|
|
+ :type="cam.videoId === playerVideoId ? 'primary' : ''" @click="playerVideoId = cam.videoId">
|
|
|
|
|
+ {{ cam.deviceName }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup name="Yjsdgl">
|
|
<script setup name="Yjsdgl">
|
|
|
-import { ref, computed, nextTick, onMounted } from 'vue'
|
|
|
|
|
|
|
+import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
-import locationIcon from '@/assets/images/location.png'
|
|
|
|
|
|
|
+import { Aim, Delete, Plus, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
|
|
|
|
|
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
|
|
|
|
|
+import DrainageLiveTile from '@/views/subSystem/drainage/spjc/DrainageLiveTile.vue'
|
|
|
|
|
+import { useDict } from '@/utils/dict'
|
|
|
|
|
+import { bd09ToWgs84 } from '@/utils/coordTransform'
|
|
|
|
|
+import { getDrainageVideoMap } from '@/api/drainageVideo'
|
|
|
import {
|
|
import {
|
|
|
getWaterloggingPage,
|
|
getWaterloggingPage,
|
|
|
|
|
+ getWaterloggingMap,
|
|
|
|
|
+ getWaterloggingDetail,
|
|
|
|
|
+ getWaterloggingFilterOptions,
|
|
|
addWaterlogging,
|
|
addWaterlogging,
|
|
|
updateWaterlogging,
|
|
updateWaterlogging,
|
|
|
deleteWaterlogging,
|
|
deleteWaterlogging,
|
|
|
- getEquipmentPage,
|
|
|
|
|
- getEquipmentById,
|
|
|
|
|
- bindWaterloggingEquipment,
|
|
|
|
|
- getDrainageBoundEquipmentIds
|
|
|
|
|
|
|
+ batchDeleteWaterlogging,
|
|
|
|
|
+ bindWaterloggingCameras
|
|
|
} from '@/api/drainage'
|
|
} from '@/api/drainage'
|
|
|
|
|
|
|
|
-// 地图标记创建(用Label+img代替Marker)
|
|
|
|
|
-function createMapMarker(map, bPoint, name) {
|
|
|
|
|
- const html = `<div style="text-align:center;cursor:pointer;">
|
|
|
|
|
- <div style="color:#fff;background:#409eff;border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${name || ''}</div>
|
|
|
|
|
- <div><img src="${locationIcon}" style="width:28px;height:28px;display:block;margin:0 auto;"/></div>
|
|
|
|
|
- </div>`
|
|
|
|
|
- const label = new BMapGL.Label(html, {
|
|
|
|
|
- position: bPoint,
|
|
|
|
|
- offset: new BMapGL.Size(-30, -50)
|
|
|
|
|
- })
|
|
|
|
|
- label.setStyle({
|
|
|
|
|
- border: 'none',
|
|
|
|
|
- background: 'transparent',
|
|
|
|
|
- padding: '0'
|
|
|
|
|
- })
|
|
|
|
|
- map.addOverlay(label)
|
|
|
|
|
- return label
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const { drainage_waterlogging_level: levelDict, drainage_waterlogging_risk_level: riskDict } =
|
|
|
|
|
+ useDict('drainage_waterlogging_level', 'drainage_waterlogging_risk_level')
|
|
|
|
|
|
|
|
-// 积水程度选项
|
|
|
|
|
-const waterloggingLevelOptions = [
|
|
|
|
|
- { value: 0, label: '未积水', type: 'success' },
|
|
|
|
|
- { value: 1, label: '轻度积水', type: 'info' },
|
|
|
|
|
- { value: 2, label: '中度积水', type: 'warning' },
|
|
|
|
|
- { value: 3, label: '严重积水', type: 'danger' }
|
|
|
|
|
|
|
+/** 积水程度兜底(字典未配置时页面仍可用) */
|
|
|
|
|
+const LEVEL_FALLBACK = [
|
|
|
|
|
+ { value: '0', label: '未积水' },
|
|
|
|
|
+ { value: '1', label: '轻度积水' },
|
|
|
|
|
+ { value: '2', label: '中度积水' },
|
|
|
|
|
+ { value: '3', label: '严重积水' }
|
|
|
]
|
|
]
|
|
|
|
|
+const levelOptions = computed(() => (levelDict.value && levelDict.value.length ? levelDict.value : LEVEL_FALLBACK))
|
|
|
|
|
|
|
|
-function getWaterloggingLevelLabel(level) {
|
|
|
|
|
- const option = waterloggingLevelOptions.find(item => item.value === level)
|
|
|
|
|
- return option ? option.label : '未知'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function getWaterloggingLevelType(level) {
|
|
|
|
|
- const option = waterloggingLevelOptions.find(item => item.value === level)
|
|
|
|
|
- return option ? option.type : 'info'
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 安全解析 waterloggingPoints(后端返回 JSON 字符串,需转为数组使用)
|
|
|
|
|
-function parseWaterloggingPoints(raw) {
|
|
|
|
|
- if (!raw) return []
|
|
|
|
|
- if (Array.isArray(raw)) return raw
|
|
|
|
|
- if (typeof raw === 'string') {
|
|
|
|
|
- try {
|
|
|
|
|
- const parsed = JSON.parse(raw)
|
|
|
|
|
- return Array.isArray(parsed) ? parsed : []
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.warn('waterloggingPoints JSON 解析失败:', raw, e)
|
|
|
|
|
- return []
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return []
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ==================== 搜索参数 ====================
|
|
|
|
|
-const queryParams = ref({
|
|
|
|
|
- name: '',
|
|
|
|
|
- area: ''
|
|
|
|
|
|
|
+/** 风险等级兜底 */
|
|
|
|
|
+const RISK_FALLBACK = [
|
|
|
|
|
+ { value: '0', label: '低风险' },
|
|
|
|
|
+ { value: '1', label: '中风险' },
|
|
|
|
|
+ { value: '2', label: '高风险' }
|
|
|
|
|
+]
|
|
|
|
|
+const riskOptions = computed(() => (riskDict.value && riskDict.value.length ? riskDict.value : RISK_FALLBACK))
|
|
|
|
|
+
|
|
|
|
|
+/** 图例色块,与列表标签语义保持一致 */
|
|
|
|
|
+const LEVEL_COLORS = { '0': '#67c23a', '1': '#409eff', '2': '#e6a23c', '3': '#f56c6c' }
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 查询条件 ====================
|
|
|
|
|
+const emptyQuery = () => ({
|
|
|
|
|
+ pointCode: '',
|
|
|
|
|
+ pointName: '',
|
|
|
|
|
+ district: '',
|
|
|
|
|
+ waterloggingLevel: null,
|
|
|
|
|
+ riskLevel: ''
|
|
|
})
|
|
})
|
|
|
|
|
+const query = ref(emptyQuery())
|
|
|
|
|
+const filterOptions = ref({ district: [] })
|
|
|
|
|
|
|
|
-// ==================== 表格数据 ====================
|
|
|
|
|
|
|
+// ==================== 列表 ====================
|
|
|
const tableData = ref([])
|
|
const tableData = ref([])
|
|
|
const total = ref(0)
|
|
const total = ref(0)
|
|
|
const pageNum = ref(1)
|
|
const pageNum = ref(1)
|
|
|
const pageSize = ref(10)
|
|
const pageSize = ref(10)
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
|
|
+const selectedIds = ref([])
|
|
|
|
|
|
|
|
-// 加载分页数据
|
|
|
|
|
-async function loadData() {
|
|
|
|
|
- loading.value = true
|
|
|
|
|
|
|
+// ==================== 地图 ====================
|
|
|
|
|
+const mapRecords = ref([])
|
|
|
|
|
+const mapRef = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 摄像头资源 ====================
|
|
|
|
|
+/** 全部视频设备,用于把 cameraIds 翻译成名称、以及关联弹窗候选 */
|
|
|
|
|
+const videoResources = ref([])
|
|
|
|
|
+const videoMap = ref({})
|
|
|
|
|
+
|
|
|
|
|
+async function loadVideoResources() {
|
|
|
try {
|
|
try {
|
|
|
- const res = await getWaterloggingPage(pageNum.value, pageSize.value, queryParams.value)
|
|
|
|
|
- // 兼容直接返回Page对象(无code包装)和AjaxResult包装两种格式
|
|
|
|
|
- const pageData = res.code !== undefined ? res.data : res
|
|
|
|
|
- tableData.value = pageData.records || []
|
|
|
|
|
- total.value = pageData.total || 0
|
|
|
|
|
- renderMainMapMarkers()
|
|
|
|
|
- resolveEquipmentNames()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('加载易积水点数据失败', e)
|
|
|
|
|
- } finally {
|
|
|
|
|
- loading.value = false
|
|
|
|
|
|
|
+ const res = await getDrainageVideoMap()
|
|
|
|
|
+ const data = res && res.code !== undefined ? (res.data || []) : (res || [])
|
|
|
|
|
+ videoResources.value = Array.isArray(data) ? data : []
|
|
|
|
|
+ const map = {}
|
|
|
|
|
+ videoResources.value.forEach((item) => {
|
|
|
|
|
+ map[String(item.videoId)] = item
|
|
|
|
|
+ })
|
|
|
|
|
+ videoMap.value = map
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ // 摄像头资源拿不到不影响点位本身展示,只是名称退化为 ID
|
|
|
|
|
+ console.warn('视频资源加载失败', error)
|
|
|
|
|
+ videoResources.value = []
|
|
|
|
|
+ videoMap.value = {}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 关联设备名称映射
|
|
|
|
|
-const equipmentNameMap = ref({})
|
|
|
|
|
-
|
|
|
|
|
-async function resolveEquipmentNames() {
|
|
|
|
|
- const map = {}
|
|
|
|
|
- for (const row of tableData.value) {
|
|
|
|
|
- if (row.equipmentId) {
|
|
|
|
|
- const ids = row.equipmentId.split(',')
|
|
|
|
|
- const names = []
|
|
|
|
|
- for (const id of ids) {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await getEquipmentById(id.trim())
|
|
|
|
|
- const data = res.code !== undefined ? res.data : res
|
|
|
|
|
- const info = data && data.equipmentInfo ? data.equipmentInfo : data
|
|
|
|
|
- if (info && info.equipmentName) names.push(info.equipmentName)
|
|
|
|
|
- } catch (e) { /* skip */ }
|
|
|
|
|
- }
|
|
|
|
|
- map[row.id] = names.join('、')
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- equipmentNameMap.value = map
|
|
|
|
|
|
|
+function parseCameraIds(value) {
|
|
|
|
|
+ if (!value) return []
|
|
|
|
|
+ return String(value).split(',').map((id) => id.trim()).filter(Boolean)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-onMounted(() => {
|
|
|
|
|
- loadData()
|
|
|
|
|
- setTimeout(() => { initMainMap() }, 500)
|
|
|
|
|
-})
|
|
|
|
|
|
|
+/** 某点位关联的摄像头对象列表(找不到资源时用 ID 兜底) */
|
|
|
|
|
+function cameraListOf(row) {
|
|
|
|
|
+ return parseCameraIds(row?.cameraIds).map((id) => videoMap.value[id] || { videoId: id, deviceName: id })
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// ==================== 顶部主地图 ====================
|
|
|
|
|
-let mainMapInstance = null
|
|
|
|
|
|
|
+// ==================== 详情 ====================
|
|
|
|
|
+const detailVisible = ref(false)
|
|
|
|
|
+const detailRow = ref({})
|
|
|
|
|
+const detailMapRef = ref(null)
|
|
|
|
|
|
|
|
-function initMainMap() {
|
|
|
|
|
- const container = document.getElementById('yjsdglMainMap')
|
|
|
|
|
- if (!container) return
|
|
|
|
|
- try {
|
|
|
|
|
- mainMapInstance = new BMapGL.Map('yjsdglMainMap')
|
|
|
|
|
- const centerPoint = new BMapGL.Point(110.393, 28.452)
|
|
|
|
|
- mainMapInstance.centerAndZoom(centerPoint, 12)
|
|
|
|
|
- mainMapInstance.enableScrollWheelZoom(true)
|
|
|
|
|
- renderMainMapMarkers()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('百度地图初始化失败:', e)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const detailCameras = computed(() => cameraListOf(detailRow.value))
|
|
|
|
|
|
|
|
-function renderMainMapMarkers() {
|
|
|
|
|
- if (!mainMapInstance) return
|
|
|
|
|
- mainMapInstance.clearOverlays()
|
|
|
|
|
|
|
+// ==================== 视频播放 ====================
|
|
|
|
|
+const playerVisible = ref(false)
|
|
|
|
|
+const playerVideoId = ref('')
|
|
|
|
|
+const playerTitle = ref('')
|
|
|
|
|
+const playerOptions = ref([])
|
|
|
|
|
|
|
|
- const allPoints = []
|
|
|
|
|
- tableData.value.forEach(row => {
|
|
|
|
|
- const points = parseWaterloggingPoints(row.waterloggingPoints)
|
|
|
|
|
- points.forEach(p => {
|
|
|
|
|
- if (p.lng && p.lat) {
|
|
|
|
|
- const bPoint = new BMapGL.Point(p.lng, p.lat)
|
|
|
|
|
- createMapMarker(mainMapInstance, bPoint, p.name || row.name)
|
|
|
|
|
- allPoints.push(bPoint)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- })
|
|
|
|
|
|
|
+function playVideo(camera) {
|
|
|
|
|
+ playerOptions.value = [camera]
|
|
|
|
|
+ playerVideoId.value = camera.videoId
|
|
|
|
|
+ playerTitle.value = camera.deviceName || '实时视频'
|
|
|
|
|
+ playerVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (allPoints.length > 0) {
|
|
|
|
|
- mainMapInstance.setViewport(allPoints)
|
|
|
|
|
|
|
+/** 列表/地图点击摄像头入口:有多个时弹窗内切换 */
|
|
|
|
|
+function playCameras(row) {
|
|
|
|
|
+ const cameras = cameraListOf(row)
|
|
|
|
|
+ if (!cameras.length) {
|
|
|
|
|
+ ElMessage.warning('该点位尚未关联摄像头')
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
+ playerOptions.value = cameras
|
|
|
|
|
+ playerVideoId.value = cameras[0].videoId
|
|
|
|
|
+ playerTitle.value = `${row.pointName || '易积水点'} · 实时视频`
|
|
|
|
|
+ playerVisible.value = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 多选
|
|
|
|
|
-const selectedIds = ref([])
|
|
|
|
|
-function handleSelectionChange(selection) {
|
|
|
|
|
- selectedIds.value = selection.map(item => item.id)
|
|
|
|
|
|
|
+// ==================== 关联摄像头 ====================
|
|
|
|
|
+const bindVisible = ref(false)
|
|
|
|
|
+const bindLoading = ref(false)
|
|
|
|
|
+const bindSubmitting = ref(false)
|
|
|
|
|
+const bindRow = ref({})
|
|
|
|
|
+const bindKeyword = ref('')
|
|
|
|
|
+const bindSelected = ref([])
|
|
|
|
|
+const bindTableRef = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+const bindCandidates = computed(() => {
|
|
|
|
|
+ const keyword = bindKeyword.value.trim().toLowerCase()
|
|
|
|
|
+ if (!keyword) return videoResources.value
|
|
|
|
|
+ return videoResources.value.filter((item) =>
|
|
|
|
|
+ `${item.deviceName || ''} ${item.deviceCode || ''}`.toLowerCase().includes(keyword)
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function isCameraSelectable(row) {
|
|
|
|
|
+ // 只允许选择「未关联」或「已关联到当前点位」的摄像头
|
|
|
|
|
+ return !row.pointId || row.pointId === bindRow.value.pointId
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 表格数据直接用 tableData(已是分页数据)
|
|
|
|
|
-const filteredList = computed(() => tableData.value)
|
|
|
|
|
|
|
+function openBind(row) {
|
|
|
|
|
+ bindRow.value = { ...row }
|
|
|
|
|
+ bindKeyword.value = ''
|
|
|
|
|
+ bindSelected.value = parseCameraIds(row.cameraIds)
|
|
|
|
|
+ bindVisible.value = true
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ syncBindSelection()
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// 搜索
|
|
|
|
|
-function handleQuery() {
|
|
|
|
|
- pageNum.value = 1
|
|
|
|
|
- loadData()
|
|
|
|
|
|
|
+function syncBindSelection() {
|
|
|
|
|
+ if (!bindTableRef.value) return
|
|
|
|
|
+ bindTableRef.value.clearSelection()
|
|
|
|
|
+ bindCandidates.value.forEach((item) => {
|
|
|
|
|
+ if (bindSelected.value.includes(String(item.videoId))) {
|
|
|
|
|
+ bindTableRef.value.toggleRowSelection(item, true)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ==================== 查看详情对话框 ====================
|
|
|
|
|
-const dialogVisible = ref(false)
|
|
|
|
|
-const currentRow = ref({})
|
|
|
|
|
-const viewEquipmentNames = ref([])
|
|
|
|
|
-let viewMapInstance = null
|
|
|
|
|
|
|
+function handleBindSelectionChange(selection) {
|
|
|
|
|
+ bindSelected.value = selection.map((item) => String(item.videoId))
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-async function handleView(row) {
|
|
|
|
|
- currentRow.value = {
|
|
|
|
|
- ...row,
|
|
|
|
|
- // 将 JSON 字符串解析为数组,供模板和地图渲染使用
|
|
|
|
|
- waterloggingPoints: parseWaterloggingPoints(row.waterloggingPoints)
|
|
|
|
|
- }
|
|
|
|
|
- dialogVisible.value = true
|
|
|
|
|
- viewEquipmentNames.value = []
|
|
|
|
|
- if (row.equipmentId) {
|
|
|
|
|
- const ids = row.equipmentId.split(',')
|
|
|
|
|
- const names = []
|
|
|
|
|
- for (const id of ids) {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await getEquipmentById(id.trim())
|
|
|
|
|
- const data = res.code !== undefined ? res.data : res
|
|
|
|
|
- const info = data && data.equipmentInfo ? data.equipmentInfo : data
|
|
|
|
|
- if (info && info.equipmentName) names.push(info.equipmentName)
|
|
|
|
|
- } catch (e) { /* skip */ }
|
|
|
|
|
|
|
+async function submitBind() {
|
|
|
|
|
+ if (!bindRow.value.pointId) return
|
|
|
|
|
+ bindSubmitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await bindWaterloggingCameras(bindRow.value.pointId, bindSelected.value)
|
|
|
|
|
+ if (res && (res.code === 0 || res.code === 200)) {
|
|
|
|
|
+ ElMessage.success('关联成功')
|
|
|
|
|
+ bindVisible.value = false
|
|
|
|
|
+ await Promise.all([loadAll(), loadVideoResources()])
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res?.msg || '关联失败')
|
|
|
}
|
|
}
|
|
|
- viewEquipmentNames.value = names
|
|
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('关联摄像头失败', error)
|
|
|
|
|
+ ElMessage.error('关联失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ bindSubmitting.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleClose() {
|
|
|
|
|
- dialogVisible.value = false
|
|
|
|
|
- if (viewMapInstance) {
|
|
|
|
|
- viewMapInstance = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// ==================== 数据加载 ====================
|
|
|
|
|
+function buildParams() {
|
|
|
|
|
+ const params = {}
|
|
|
|
|
+ Object.keys(query.value).forEach((key) => {
|
|
|
|
|
+ const value = query.value[key]
|
|
|
|
|
+ if (value !== '' && value !== null && value !== undefined) {
|
|
|
|
|
+ params[key] = value
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return params
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function onDialogOpened() {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- initViewMap()
|
|
|
|
|
- }, 300)
|
|
|
|
|
|
|
+async function loadAll() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ const params = buildParams()
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [pageRes, mapRes] = await Promise.all([
|
|
|
|
|
+ getWaterloggingPage(pageNum.value, pageSize.value, params),
|
|
|
|
|
+ getWaterloggingMap(params)
|
|
|
|
|
+ ])
|
|
|
|
|
+ const pageData = pageRes && pageRes.code !== undefined ? (pageRes.data || {}) : (pageRes || {})
|
|
|
|
|
+ tableData.value = pageData.records || []
|
|
|
|
|
+ total.value = pageData.total || 0
|
|
|
|
|
+ const mapData = mapRes && mapRes.code !== undefined ? (mapRes.data || []) : (mapRes || [])
|
|
|
|
|
+ mapRecords.value = Array.isArray(mapData) ? mapData : []
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载易积水点数据失败', error)
|
|
|
|
|
+ ElMessage.error('易积水点数据加载失败,请稍后重试')
|
|
|
|
|
+ tableData.value = []
|
|
|
|
|
+ mapRecords.value = []
|
|
|
|
|
+ total.value = 0
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function initViewMap() {
|
|
|
|
|
- const container = document.getElementById('yjsdglViewMap')
|
|
|
|
|
- if (!container) return
|
|
|
|
|
|
|
+async function loadPage() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getWaterloggingPage(pageNum.value, pageSize.value, buildParams())
|
|
|
|
|
+ const pageData = res && res.code !== undefined ? (res.data || {}) : (res || {})
|
|
|
|
|
+ tableData.value = pageData.records || []
|
|
|
|
|
+ total.value = pageData.total || 0
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载易积水点列表失败', error)
|
|
|
|
|
+ ElMessage.error('易积水点列表加载失败,请稍后重试')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+async function loadFilterOptions() {
|
|
|
try {
|
|
try {
|
|
|
- viewMapInstance = new BMapGL.Map('yjsdglViewMap')
|
|
|
|
|
- const centerPoint = new BMapGL.Point(110.393, 28.452)
|
|
|
|
|
- viewMapInstance.centerAndZoom(centerPoint, 15)
|
|
|
|
|
- viewMapInstance.enableScrollWheelZoom(true)
|
|
|
|
|
- renderViewMarkers()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('百度地图初始化失败:', e)
|
|
|
|
|
|
|
+ const res = await getWaterloggingFilterOptions()
|
|
|
|
|
+ const data = res && res.code !== undefined ? (res.data || {}) : (res || {})
|
|
|
|
|
+ filterOptions.value = { district: data.district || [] }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('易积水点筛选下拉项加载失败', error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function renderViewMarkers() {
|
|
|
|
|
- if (!viewMapInstance) return
|
|
|
|
|
- viewMapInstance.clearOverlays()
|
|
|
|
|
|
|
+function handleQuery() {
|
|
|
|
|
+ pageNum.value = 1
|
|
|
|
|
+ loadAll()
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const points = currentRow.value.waterloggingPoints || []
|
|
|
|
|
|
|
+function handleSizeChange() {
|
|
|
|
|
+ pageNum.value = 1
|
|
|
|
|
+ loadPage()
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- points.forEach(point => {
|
|
|
|
|
- const bPoint = new BMapGL.Point(point.lng, point.lat)
|
|
|
|
|
- createMapMarker(viewMapInstance, bPoint, point.name)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+function resetQuery() {
|
|
|
|
|
+ query.value = emptyQuery()
|
|
|
|
|
+ pageNum.value = 1
|
|
|
|
|
+ loadAll()
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (points.length > 0) {
|
|
|
|
|
- const viewPoints = points.map(p => new BMapGL.Point(p.lng, p.lat))
|
|
|
|
|
- viewMapInstance.setViewport(viewPoints)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// ==================== 地图数据 ====================
|
|
|
|
|
+function toCoord(lng, lat) {
|
|
|
|
|
+ const x = Number(lng)
|
|
|
|
|
+ const y = Number(lat)
|
|
|
|
|
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
|
|
|
+ if (x === 0 && y === 0) return null
|
|
|
|
|
+ return { lng: x, lat: y }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ==================== 新增/修改对话框 ====================
|
|
|
|
|
-const editDialogVisible = ref(false)
|
|
|
|
|
-const editDialogTitle = ref('新增监测点')
|
|
|
|
|
-const editFormRef = ref(null)
|
|
|
|
|
-const isEditMode = ref(false)
|
|
|
|
|
-const editRowId = ref(null)
|
|
|
|
|
|
|
+const mapPoints = computed(() =>
|
|
|
|
|
+ mapRecords.value
|
|
|
|
|
+ .map((row) => {
|
|
|
|
|
+ const coord = toCoord(row.longitude, row.latitude)
|
|
|
|
|
+ if (!coord) return null
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: row.pointId,
|
|
|
|
|
+ name: row.pointName || row.pointCode || '积水点',
|
|
|
|
|
+ ...coord,
|
|
|
|
|
+ statusKey: levelStatusKey(row.waterloggingLevel),
|
|
|
|
|
+ raw: row
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .filter(Boolean)
|
|
|
|
|
+)
|
|
|
|
|
|
|
|
-const editForm = ref({
|
|
|
|
|
- name: '',
|
|
|
|
|
- area: '',
|
|
|
|
|
- time: '',
|
|
|
|
|
- waterVolume: null,
|
|
|
|
|
- waterloggingLevel: 0
|
|
|
|
|
-})
|
|
|
|
|
|
|
+/** 积水程度 → 地图图标状态(复用 gisMapColors 的状态色) */
|
|
|
|
|
+const LEVEL_STATUS_KEY = { '0': 'normal', '1': 'workOrder', '2': 'warning', '3': 'alarm' }
|
|
|
|
|
|
|
|
-const editRules = {
|
|
|
|
|
- name: [{ required: true, message: '请输入监测点名称', trigger: 'blur' }],
|
|
|
|
|
- area: [{ required: true, message: '请输入地点', trigger: 'blur' }],
|
|
|
|
|
- time: [{ required: true, message: '请选择数据时间', trigger: 'change' }]
|
|
|
|
|
|
|
+function levelStatusKey(level) {
|
|
|
|
|
+ return LEVEL_STATUS_KEY[String(level)] || 'normal'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 编辑弹窗地图相关
|
|
|
|
|
-let editMapInstance = null
|
|
|
|
|
-let editWaterloggingPoints = ref([])
|
|
|
|
|
-let editWaterloggingMarkerCount = ref(0)
|
|
|
|
|
-
|
|
|
|
|
-function handleAdd() {
|
|
|
|
|
- isEditMode.value = false
|
|
|
|
|
- editDialogTitle.value = '新增监测点'
|
|
|
|
|
- editRowId.value = null
|
|
|
|
|
- editForm.value = {
|
|
|
|
|
- name: '',
|
|
|
|
|
- area: '',
|
|
|
|
|
- time: '',
|
|
|
|
|
- waterVolume: null,
|
|
|
|
|
- waterloggingLevel: 0
|
|
|
|
|
- }
|
|
|
|
|
- editWaterloggingPoints.value = []
|
|
|
|
|
- editWaterloggingMarkerCount.value = 0
|
|
|
|
|
- editDialogVisible.value = true
|
|
|
|
|
|
|
+function levelColor(level) {
|
|
|
|
|
+ return LEVEL_COLORS[String(level)] || '#909399'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleEdit(row) {
|
|
|
|
|
- isEditMode.value = true
|
|
|
|
|
- editDialogTitle.value = '修改监测点'
|
|
|
|
|
- editRowId.value = row.id
|
|
|
|
|
- editForm.value = {
|
|
|
|
|
- name: row.name,
|
|
|
|
|
- area: row.area,
|
|
|
|
|
- time: row.time,
|
|
|
|
|
- waterVolume: row.waterVolume,
|
|
|
|
|
- waterloggingLevel: row.waterloggingLevel ?? 0
|
|
|
|
|
- }
|
|
|
|
|
- // 深拷贝标记点数据(waterloggingPoints 从后端返回为 JSON 字符串,需先解析为数组)
|
|
|
|
|
- editWaterloggingPoints.value = parseWaterloggingPoints(row.waterloggingPoints).map(p => ({ ...p }))
|
|
|
|
|
- editWaterloggingMarkerCount.value = editWaterloggingPoints.value.length
|
|
|
|
|
- editDialogVisible.value = true
|
|
|
|
|
|
|
+function fitMap() {
|
|
|
|
|
+ mapRef.value?.fitBounds()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleEditClose() {
|
|
|
|
|
- editDialogVisible.value = false
|
|
|
|
|
- if (editMapInstance) {
|
|
|
|
|
- editMapInstance = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function onMapReady() {
|
|
|
|
|
+ nextTick(() => fitMap())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function onEditDialogOpened() {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- initEditMap()
|
|
|
|
|
- }, 300)
|
|
|
|
|
|
|
+watch(
|
|
|
|
|
+ mapPoints,
|
|
|
|
|
+ () => {
|
|
|
|
|
+ nextTick(() => fitMap())
|
|
|
|
|
+ },
|
|
|
|
|
+ { flush: 'post' }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+function handleMapPointClick(point) {
|
|
|
|
|
+ const row = point?.raw || point
|
|
|
|
|
+ if (!row) return
|
|
|
|
|
+ if (cameraListOf(row).length) {
|
|
|
|
|
+ playCameras(row)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.info('该点位尚未关联摄像头')
|
|
|
|
|
+ openDetail(row)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function initEditMap() {
|
|
|
|
|
- const container = document.getElementById('yjsdglEditMap')
|
|
|
|
|
- if (!container) return
|
|
|
|
|
|
|
+// ==================== 详情操作 ====================
|
|
|
|
|
+const detailPoints = computed(() => {
|
|
|
|
|
+ const coord = toCoord(detailRow.value.longitude, detailRow.value.latitude)
|
|
|
|
|
+ if (!coord) return []
|
|
|
|
|
+ return [{
|
|
|
|
|
+ id: detailRow.value.pointId,
|
|
|
|
|
+ name: detailRow.value.pointName || '积水点',
|
|
|
|
|
+ ...coord,
|
|
|
|
|
+ statusKey: levelStatusKey(detailRow.value.waterloggingLevel)
|
|
|
|
|
+ }]
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
|
|
+async function openDetail(row) {
|
|
|
|
|
+ detailRow.value = { ...row }
|
|
|
|
|
+ detailVisible.value = true
|
|
|
try {
|
|
try {
|
|
|
- editMapInstance = new BMapGL.Map('yjsdglEditMap')
|
|
|
|
|
- const centerPoint = new BMapGL.Point(110.393, 28.452)
|
|
|
|
|
- editMapInstance.centerAndZoom(centerPoint, 15)
|
|
|
|
|
- editMapInstance.enableScrollWheelZoom(true)
|
|
|
|
|
-
|
|
|
|
|
- // 点击地图添加标记
|
|
|
|
|
- editMapInstance.addEventListener('click', function (e) {
|
|
|
|
|
- addEditMarker(e.latlng)
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // 渲染已有标记
|
|
|
|
|
- renderEditMarkers()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('百度地图初始化失败:', e)
|
|
|
|
|
|
|
+ const res = await getWaterloggingDetail(row.pointId)
|
|
|
|
|
+ const data = res && res.code !== undefined ? res.data : res
|
|
|
|
|
+ if (data && typeof data === 'object') {
|
|
|
|
|
+ detailRow.value = data
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('易积水点详情接口未取到数据,使用列表数据展示', error)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function addEditMarker(point) {
|
|
|
|
|
- // 只能标记一个点位,再次点击重新定位
|
|
|
|
|
- if (editWaterloggingPoints.value.length >= 1) {
|
|
|
|
|
- editWaterloggingPoints.value[0] = {
|
|
|
|
|
- name: editWaterloggingPoints.value[0].name,
|
|
|
|
|
- lng: point.lng,
|
|
|
|
|
- lat: point.lat
|
|
|
|
|
- }
|
|
|
|
|
- renderEditMarkers(false)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function onDetailOpened() {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ setTimeout(() => detailMapRef.value?.fitBounds(), 200)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const count = ++editWaterloggingMarkerCount.value
|
|
|
|
|
- editWaterloggingPoints.value.push({
|
|
|
|
|
- name: `易积水点${count}`,
|
|
|
|
|
- lng: point.lng,
|
|
|
|
|
- lat: point.lat
|
|
|
|
|
|
|
+function onDetailMapReady() {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ setTimeout(() => detailMapRef.value?.fitBounds(), 120)
|
|
|
})
|
|
})
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 统一重绘,保证地图与数据完全一致
|
|
|
|
|
- renderEditMarkers(false)
|
|
|
|
|
|
|
+// ==================== 新增 / 修改 ====================
|
|
|
|
|
+const formVisible = ref(false)
|
|
|
|
|
+const formTitle = ref('新增易积水点')
|
|
|
|
|
+const formRef = ref(null)
|
|
|
|
|
+const submitting = ref(false)
|
|
|
|
|
+const editingId = ref('')
|
|
|
|
|
+const formMapRef = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+const emptyForm = () => ({
|
|
|
|
|
+ pointId: '',
|
|
|
|
|
+ pointCode: '',
|
|
|
|
|
+ pointName: '',
|
|
|
|
|
+ district: '',
|
|
|
|
|
+ location: '',
|
|
|
|
|
+ longitude: null,
|
|
|
|
|
+ latitude: null,
|
|
|
|
|
+ waterloggingLevel: 0,
|
|
|
|
|
+ waterDepth: null,
|
|
|
|
|
+ waterVolume: null,
|
|
|
|
|
+ waterArea: null,
|
|
|
|
|
+ riskLevel: '',
|
|
|
|
|
+ dutyUnit: '',
|
|
|
|
|
+ dutyPerson: '',
|
|
|
|
|
+ dutyPhone: '',
|
|
|
|
|
+ remark: ''
|
|
|
|
|
+})
|
|
|
|
|
+const form = ref(emptyForm())
|
|
|
|
|
+
|
|
|
|
|
+const formRules = {
|
|
|
|
|
+ pointCode: [{ required: true, message: '请输入点位编码', trigger: 'blur' }],
|
|
|
|
|
+ pointName: [{ required: true, message: '请输入点位名称', trigger: 'blur' }],
|
|
|
|
|
+ waterloggingLevel: [{ required: true, message: '请选择积水程度', trigger: 'change' }]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function renderEditMarkers(shouldCenter = true) {
|
|
|
|
|
- if (!editMapInstance) return
|
|
|
|
|
- editMapInstance.clearOverlays()
|
|
|
|
|
|
|
+const formPoints = computed(() => {
|
|
|
|
|
+ const coord = toCoord(form.value.longitude, form.value.latitude)
|
|
|
|
|
+ if (!coord) return []
|
|
|
|
|
+ return [{
|
|
|
|
|
+ id: 'form',
|
|
|
|
|
+ name: form.value.pointName || '待保存点位',
|
|
|
|
|
+ ...coord,
|
|
|
|
|
+ statusKey: levelStatusKey(form.value.waterloggingLevel)
|
|
|
|
|
+ }]
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
- const points = editWaterloggingPoints.value
|
|
|
|
|
- if (shouldCenter) {
|
|
|
|
|
- const center = points.length > 0
|
|
|
|
|
- ? new BMapGL.Point(points[0].lng, points[0].lat)
|
|
|
|
|
- : new BMapGL.Point(110.393, 28.452)
|
|
|
|
|
- editMapInstance.centerAndZoom(center, 15)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function handleAdd() {
|
|
|
|
|
+ editingId.value = ''
|
|
|
|
|
+ formTitle.value = '新增易积水点'
|
|
|
|
|
+ form.value = emptyForm()
|
|
|
|
|
+ formVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- points.forEach((point, index) => {
|
|
|
|
|
- const bPoint = new BMapGL.Point(point.lng, point.lat)
|
|
|
|
|
- const label = createMapMarker(editMapInstance, bPoint, point.name)
|
|
|
|
|
|
|
+function handleEdit(row) {
|
|
|
|
|
+ editingId.value = row.pointId
|
|
|
|
|
+ formTitle.value = '修改易积水点'
|
|
|
|
|
+ form.value = {
|
|
|
|
|
+ ...emptyForm(),
|
|
|
|
|
+ ...row,
|
|
|
|
|
+ waterloggingLevel: row.waterloggingLevel === null || row.waterloggingLevel === undefined
|
|
|
|
|
+ ? 0 : Number(row.waterloggingLevel),
|
|
|
|
|
+ longitude: row.longitude === null || row.longitude === undefined ? null : Number(row.longitude),
|
|
|
|
|
+ latitude: row.latitude === null || row.latitude === undefined ? null : Number(row.latitude),
|
|
|
|
|
+ waterDepth: row.waterDepth === null || row.waterDepth === undefined ? null : Number(row.waterDepth),
|
|
|
|
|
+ waterVolume: row.waterVolume === null || row.waterVolume === undefined ? null : Number(row.waterVolume),
|
|
|
|
|
+ waterArea: row.waterArea === null || row.waterArea === undefined ? null : Number(row.waterArea)
|
|
|
|
|
+ }
|
|
|
|
|
+ formVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 右键删除标记
|
|
|
|
|
- label.addEventListener('rightclick', () => {
|
|
|
|
|
- editWaterloggingPoints.value.splice(index, 1)
|
|
|
|
|
- renderEditMarkers(false)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+function onFormOpened() {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ setTimeout(() => formMapRef.value?.fitBounds(), 200)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function clearEditMarkers() {
|
|
|
|
|
- editWaterloggingPoints.value = []
|
|
|
|
|
- editWaterloggingMarkerCount.value = 0
|
|
|
|
|
- renderEditMarkers()
|
|
|
|
|
|
|
+function onFormClosed() {
|
|
|
|
|
+ formRef.value?.resetFields()
|
|
|
|
|
+ form.value = emptyForm()
|
|
|
|
|
+ editingId.value = ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleEditSubmit() {
|
|
|
|
|
- editFormRef.value.validate(async valid => {
|
|
|
|
|
- if (!valid) return
|
|
|
|
|
|
|
+function onFormMapReady() {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ setTimeout(() => formMapRef.value?.fitBounds(), 120)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const formData = {
|
|
|
|
|
- name: editForm.value.name,
|
|
|
|
|
- area: editForm.value.area,
|
|
|
|
|
- time: editForm.value.time,
|
|
|
|
|
- waterVolume: editForm.value.waterVolume,
|
|
|
|
|
- waterloggingLevel: editForm.value.waterloggingLevel,
|
|
|
|
|
- waterloggingCount: editWaterloggingPoints.value.length,
|
|
|
|
|
- waterloggingPoints: JSON.stringify(editWaterloggingPoints.value.map(p => ({ name: p.name, lng: p.lng, lat: p.lat })))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+/** 地图点选:百度坐标 → WGS84 后回填,与后端存储口径一致 */
|
|
|
|
|
+function handleMapPick(point) {
|
|
|
|
|
+ const lng = Number(point?.lng)
|
|
|
|
|
+ const lat = Number(point?.lat)
|
|
|
|
|
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
|
|
|
|
|
+ const [wgsLng, wgsLat] = bd09ToWgs84(lng, lat)
|
|
|
|
|
+ form.value.longitude = Number(wgsLng.toFixed(8))
|
|
|
|
|
+ form.value.latitude = Number(wgsLat.toFixed(8))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildPayload() {
|
|
|
|
|
+ const source = form.value
|
|
|
|
|
+ return {
|
|
|
|
|
+ pointCode: source.pointCode,
|
|
|
|
|
+ pointName: source.pointName,
|
|
|
|
|
+ district: source.district || null,
|
|
|
|
|
+ location: source.location || null,
|
|
|
|
|
+ longitude: source.longitude,
|
|
|
|
|
+ latitude: source.latitude,
|
|
|
|
|
+ waterloggingLevel: source.waterloggingLevel,
|
|
|
|
|
+ waterDepth: source.waterDepth,
|
|
|
|
|
+ waterVolume: source.waterVolume,
|
|
|
|
|
+ waterArea: source.waterArea,
|
|
|
|
|
+ riskLevel: source.riskLevel || null,
|
|
|
|
|
+ dutyUnit: source.dutyUnit || null,
|
|
|
|
|
+ dutyPerson: source.dutyPerson || null,
|
|
|
|
|
+ dutyPhone: source.dutyPhone || null,
|
|
|
|
|
+ remark: source.remark || null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+function submitForm() {
|
|
|
|
|
+ formRef.value?.validate(async (valid) => {
|
|
|
|
|
+ if (!valid) return
|
|
|
|
|
+ submitting.value = true
|
|
|
try {
|
|
try {
|
|
|
- if (isEditMode.value) {
|
|
|
|
|
- formData.id = editRowId.value
|
|
|
|
|
- const res = await updateWaterlogging(formData)
|
|
|
|
|
- if (res.code === 0 || res.code === 200) {
|
|
|
|
|
- ElMessage.success('修改成功')
|
|
|
|
|
- } else {
|
|
|
|
|
- ElMessage.error(res.msg || '修改失败')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const payload = buildPayload()
|
|
|
|
|
+ const res = editingId.value
|
|
|
|
|
+ ? await updateWaterlogging({ ...payload, pointId: editingId.value })
|
|
|
|
|
+ : await addWaterlogging(payload)
|
|
|
|
|
+ if (res && (res.code === 0 || res.code === 200)) {
|
|
|
|
|
+ ElMessage.success(editingId.value ? '修改成功' : '新增成功')
|
|
|
|
|
+ formVisible.value = false
|
|
|
|
|
+ await Promise.all([loadAll(), loadFilterOptions()])
|
|
|
} else {
|
|
} else {
|
|
|
- const res = await addWaterlogging(formData)
|
|
|
|
|
- if (res.code === 0 || res.code === 200) {
|
|
|
|
|
- ElMessage.success('新增成功')
|
|
|
|
|
- } else {
|
|
|
|
|
- ElMessage.error(res.msg || '新增失败')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- editDialogVisible.value = false
|
|
|
|
|
- if (editMapInstance) {
|
|
|
|
|
- editMapInstance = null
|
|
|
|
|
|
|
+ ElMessage.error(res?.msg || (editingId.value ? '修改失败' : '新增失败'))
|
|
|
}
|
|
}
|
|
|
- loadData()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('提交失败', e)
|
|
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('保存易积水点失败', error)
|
|
|
ElMessage.error('操作失败')
|
|
ElMessage.error('操作失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ==================== 删除功能 ====================
|
|
|
|
|
|
|
+// ==================== 删除 ====================
|
|
|
|
|
+function handleSelectionChange(selection) {
|
|
|
|
|
+ selectedIds.value = selection.map((item) => item.pointId)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function handleDelete(row) {
|
|
function handleDelete(row) {
|
|
|
- ElMessageBox.confirm(`确认删除监测点"${row.name}"吗?`, '提示', {
|
|
|
|
|
|
|
+ ElMessageBox.confirm(`确认删除易积水点「${row.pointName}」吗?`, '提示', {
|
|
|
confirmButtonText: '确定',
|
|
confirmButtonText: '确定',
|
|
|
cancelButtonText: '取消',
|
|
cancelButtonText: '取消',
|
|
|
type: 'warning'
|
|
type: 'warning'
|
|
|
}).then(async () => {
|
|
}).then(async () => {
|
|
|
try {
|
|
try {
|
|
|
- const res = await deleteWaterlogging(row.id)
|
|
|
|
|
- if (res.code === 0 || res.code === 200) {
|
|
|
|
|
|
|
+ const res = await deleteWaterlogging(row.pointId)
|
|
|
|
|
+ if (res && (res.code === 0 || res.code === 200)) {
|
|
|
ElMessage.success('删除成功')
|
|
ElMessage.success('删除成功')
|
|
|
- loadData()
|
|
|
|
|
|
|
+ if (tableData.value.length === 1 && pageNum.value > 1) pageNum.value -= 1
|
|
|
|
|
+ await loadAll()
|
|
|
} else {
|
|
} else {
|
|
|
- ElMessage.error(res.msg || '删除失败')
|
|
|
|
|
|
|
+ ElMessage.error(res?.msg || '删除失败')
|
|
|
}
|
|
}
|
|
|
- } catch (e) {
|
|
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('删除易积水点失败', error)
|
|
|
ElMessage.error('删除失败')
|
|
ElMessage.error('删除失败')
|
|
|
}
|
|
}
|
|
|
- }).catch(() => { })
|
|
|
|
|
|
|
+ }).catch(() => {})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleBatchDelete() {
|
|
function handleBatchDelete() {
|
|
|
- if (selectedIds.value.length === 0) return
|
|
|
|
|
- ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条数据吗?`, '提示', {
|
|
|
|
|
|
|
+ const ids = [...selectedIds.value]
|
|
|
|
|
+ if (!ids.length) return
|
|
|
|
|
+ ElMessageBox.confirm(`确认删除选中的 ${ids.length} 个点位吗?`, '提示', {
|
|
|
confirmButtonText: '确定',
|
|
confirmButtonText: '确定',
|
|
|
cancelButtonText: '取消',
|
|
cancelButtonText: '取消',
|
|
|
type: 'warning'
|
|
type: 'warning'
|
|
|
}).then(async () => {
|
|
}).then(async () => {
|
|
|
try {
|
|
try {
|
|
|
- await Promise.all(selectedIds.value.map(id => deleteWaterlogging(id)))
|
|
|
|
|
- selectedIds.value = []
|
|
|
|
|
|
|
+ const res = await batchDeleteWaterlogging(ids)
|
|
|
|
|
+ if (!res || (res.code !== 0 && res.code !== 200)) {
|
|
|
|
|
+ ElMessage.error(res?.msg || '删除失败')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
ElMessage.success('删除成功')
|
|
ElMessage.success('删除成功')
|
|
|
- loadData()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
|
|
+ selectedIds.value = []
|
|
|
|
|
+ if (tableData.value.length === ids.length && pageNum.value > 1) pageNum.value -= 1
|
|
|
|
|
+ await loadAll()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('批量删除易积水点失败', error)
|
|
|
ElMessage.error('删除失败')
|
|
ElMessage.error('删除失败')
|
|
|
}
|
|
}
|
|
|
- }).catch(() => { })
|
|
|
|
|
|
|
+ }).catch(() => {})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ==================== 关联设备 ====================
|
|
|
|
|
-const bindDialogVisible = ref(false)
|
|
|
|
|
-const bindLoading = ref(false)
|
|
|
|
|
-const bindTableData = ref([])
|
|
|
|
|
-const bindTotal = ref(0)
|
|
|
|
|
-const bindPageNum = ref(1)
|
|
|
|
|
-const bindPageSize = ref(10)
|
|
|
|
|
-const bindQueryParams = ref({ equipmentName: '', equipmentTypeId: 'drainage' })
|
|
|
|
|
-const selectedEquipmentIds = ref([])
|
|
|
|
|
-const currentPartitionId = ref('')
|
|
|
|
|
-const bindTableRef = ref(null)
|
|
|
|
|
-const boundEquipmentMap = ref({})
|
|
|
|
|
-
|
|
|
|
|
-function handleBind(row) {
|
|
|
|
|
- currentPartitionId.value = row.id
|
|
|
|
|
- bindDialogVisible.value = true
|
|
|
|
|
- bindPageNum.value = 1
|
|
|
|
|
- bindQueryParams.value.equipmentName = ''
|
|
|
|
|
- if (row.equipmentId) {
|
|
|
|
|
- selectedEquipmentIds.value = row.equipmentId.split(',').map(id => id.trim()).filter(Boolean)
|
|
|
|
|
- } else {
|
|
|
|
|
- selectedEquipmentIds.value = []
|
|
|
|
|
- }
|
|
|
|
|
- nextTick(() => {
|
|
|
|
|
- loadBindData()
|
|
|
|
|
- })
|
|
|
|
|
|
|
+// ==================== 展示辅助 ====================
|
|
|
|
|
+function dictItem(dictRef, value) {
|
|
|
|
|
+ const list = dictRef?.value || []
|
|
|
|
|
+ return list.find((item) => String(item.value) === String(value))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleBindQuery() {
|
|
|
|
|
- bindPageNum.value = 1
|
|
|
|
|
- loadBindData()
|
|
|
|
|
|
|
+function levelLabel(value) {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '-'
|
|
|
|
|
+ const fallback = LEVEL_FALLBACK.find((item) => item.value === String(value))
|
|
|
|
|
+ return dictItem(levelDict, value)?.label || fallback?.label || '-'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function loadBindData() {
|
|
|
|
|
- bindLoading.value = true
|
|
|
|
|
- try {
|
|
|
|
|
- const [equipmentResult, boundResult] = await Promise.allSettled([
|
|
|
|
|
- getEquipmentPage(bindPageNum.value, bindPageSize.value, bindQueryParams.value),
|
|
|
|
|
- getDrainageBoundEquipmentIds('waterlogging', currentPartitionId.value)
|
|
|
|
|
- ])
|
|
|
|
|
- if (equipmentResult.status === 'rejected') {
|
|
|
|
|
- throw equipmentResult.reason
|
|
|
|
|
- }
|
|
|
|
|
- const res = equipmentResult.value
|
|
|
|
|
- const pageData = res.code !== undefined ? res.data : res
|
|
|
|
|
- bindTableData.value = pageData.records || []
|
|
|
|
|
- bindTotal.value = pageData.total || 0
|
|
|
|
|
- if (boundResult.status === 'fulfilled') {
|
|
|
|
|
- const boundRes = boundResult.value
|
|
|
|
|
- const boundData = boundRes.code !== undefined ? boundRes.data : boundRes
|
|
|
|
|
- boundEquipmentMap.value = boundData && typeof boundData === 'object' ? boundData : {}
|
|
|
|
|
- } else {
|
|
|
|
|
- boundEquipmentMap.value = {}
|
|
|
|
|
- console.warn('加载设备关联状态失败,设备列表仍正常显示', boundResult.reason)
|
|
|
|
|
- }
|
|
|
|
|
- await nextTick()
|
|
|
|
|
- if (selectedEquipmentIds.value.length > 0 && bindTableRef.value) {
|
|
|
|
|
- bindTableData.value.forEach(row => {
|
|
|
|
|
- if (selectedEquipmentIds.value.includes(row.equipmentId)) {
|
|
|
|
|
- bindTableRef.value.toggleRowSelection(row, true)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- bindTableData.value = []
|
|
|
|
|
- bindTotal.value = 0
|
|
|
|
|
- console.error('加载设备列表失败', e)
|
|
|
|
|
- } finally {
|
|
|
|
|
- bindLoading.value = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function levelTagType(value) {
|
|
|
|
|
+ const item = dictItem(levelDict, value)
|
|
|
|
|
+ const listClass = item?.listClass || item?.elTagType || ''
|
|
|
|
|
+ if (['success', 'warning', 'danger', 'info'].includes(listClass)) return listClass
|
|
|
|
|
+ const fallback = { '0': 'success', '1': 'info', '2': 'warning', '3': 'danger' }
|
|
|
|
|
+ return fallback[String(value)] || 'info'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleBindSelectionChange(selection) {
|
|
|
|
|
- selectedEquipmentIds.value = selection.map(item => String(item.equipmentId))
|
|
|
|
|
|
|
+function riskLabel(value) {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '-'
|
|
|
|
|
+ const fallback = RISK_FALLBACK.find((item) => item.value === String(value))
|
|
|
|
|
+ return dictItem(riskDict, value)?.label || fallback?.label || '-'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function isEquipmentSelectable(row) {
|
|
|
|
|
- const id = String(row.equipmentId)
|
|
|
|
|
- return !boundEquipmentMap.value[id] || selectedEquipmentIds.value.includes(id)
|
|
|
|
|
|
|
+function riskTagType(value) {
|
|
|
|
|
+ const item = dictItem(riskDict, value)
|
|
|
|
|
+ const listClass = item?.listClass || item?.elTagType || ''
|
|
|
|
|
+ if (['success', 'warning', 'danger', 'info'].includes(listClass)) return listClass
|
|
|
|
|
+ const fallback = { '0': 'success', '1': 'warning', '2': 'danger' }
|
|
|
|
|
+ return fallback[String(value)] || 'info'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function closeBindDialog() {
|
|
|
|
|
- bindDialogVisible.value = false
|
|
|
|
|
- bindTableData.value = []
|
|
|
|
|
- selectedEquipmentIds.value = []
|
|
|
|
|
- boundEquipmentMap.value = {}
|
|
|
|
|
|
|
+function formatVolume(value) {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '--'
|
|
|
|
|
+ const num = Number(value)
|
|
|
|
|
+ return Number.isFinite(num) ? num.toFixed(2) : '--'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function submitBind() {
|
|
|
|
|
- if (selectedEquipmentIds.value.length === 0) {
|
|
|
|
|
- ElMessage.warning('请选择要关联的设备')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await bindWaterloggingEquipment({
|
|
|
|
|
- partitionId: currentPartitionId.value,
|
|
|
|
|
- equipmentIds: selectedEquipmentIds.value
|
|
|
|
|
- })
|
|
|
|
|
- if (res.code === 0 || res.code === 200) {
|
|
|
|
|
- ElMessage.success('关联成功')
|
|
|
|
|
- closeBindDialog()
|
|
|
|
|
- loadData()
|
|
|
|
|
- } else {
|
|
|
|
|
- ElMessage.error(res.msg || '关联失败')
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('关联设备失败', e)
|
|
|
|
|
- ElMessage.error('关联失败')
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function formatNumber(value) {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '--'
|
|
|
|
|
+ const num = Number(value)
|
|
|
|
|
+ return Number.isFinite(num) ? String(num) : '--'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function fmt(value) {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '-'
|
|
|
|
|
+ return value
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadFilterOptions()
|
|
|
|
|
+ loadVideoResources()
|
|
|
|
|
+ loadAll()
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.app-container {
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
|
|
+.waterlogging-page {
|
|
|
|
|
+ padding: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.search-form {
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
|
|
+.card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.toolbar {
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
|
|
+.card-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-sub {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.detail-info {
|
|
|
|
|
|
|
+.map-card,
|
|
|
|
|
+.filter-card,
|
|
|
|
|
+.table-card {
|
|
|
margin-bottom: 16px;
|
|
margin-bottom: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.info-item {
|
|
|
|
|
|
|
+.map-wrap {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ height: 360px;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-empty {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.72);
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-legend {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ padding: 8px 4px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-title {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-item {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-item i {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 10px;
|
|
|
|
|
+ height: 10px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-tip {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-form {
|
|
|
|
|
+ margin-bottom: -18px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar {
|
|
|
margin-bottom: 12px;
|
|
margin-bottom: 12px;
|
|
|
- line-height: 32px;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.info-label {
|
|
|
|
|
|
|
+.camera-tag {
|
|
|
|
|
+ margin-right: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-muted {
|
|
|
color: #909399;
|
|
color: #909399;
|
|
|
- font-size: 14px;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pager {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.info-value {
|
|
|
|
|
|
|
+.detail-desc {
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-section {
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-section-title {
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.camera-cards {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.camera-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.camera-name {
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
|
|
+ color: #303133;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.map-section {
|
|
|
|
|
|
|
+.camera-meta {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-map-wrap {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ height: 260px;
|
|
|
border: 1px solid #e4e7ed;
|
|
border: 1px solid #e4e7ed;
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.map-tabs {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- background: #f5f7fa;
|
|
|
|
|
- border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
|
|
+.form-map-section {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.map-tab {
|
|
|
|
|
- padding: 8px 24px;
|
|
|
|
|
|
|
+.form-map-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
- cursor: default;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- background: #409eff;
|
|
|
|
|
- user-select: none;
|
|
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-map-tip {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ color: #909399;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.map-tab-action {
|
|
|
|
|
- padding: 8px 24px;
|
|
|
|
|
|
|
+.form-map-wrap {
|
|
|
|
|
+ height: 300px;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bind-tip {
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
- cursor: pointer;
|
|
|
|
|
- color: #f56c6c;
|
|
|
|
|
- transition: all 0.3s;
|
|
|
|
|
- user-select: none;
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
|
|
+ color: #303133;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.map-tab-action:hover {
|
|
|
|
|
- color: #f78989;
|
|
|
|
|
- text-decoration: underline;
|
|
|
|
|
|
|
+.bind-filter {
|
|
|
|
|
+ margin-bottom: -8px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.map-container {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 400px;
|
|
|
|
|
|
|
+.status-self {
|
|
|
|
|
+ color: #67c23a;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.marker-count {
|
|
|
|
|
- padding: 8px 16px;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- background: #f5f7fa;
|
|
|
|
|
- border-top: 1px solid #e4e7ed;
|
|
|
|
|
|
|
+.status-occupied {
|
|
|
|
|
+ color: #e6a23c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.player-wrap {
|
|
|
|
|
+ height: 420px;
|
|
|
|
|
+ background: #101820;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.player-switch {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|