|
@@ -19,7 +19,10 @@
|
|
|
|
|
|
|
|
<!-- 告警类型统计(15天内) -->
|
|
<!-- 告警类型统计(15天内) -->
|
|
|
<div class="panel">
|
|
<div class="panel">
|
|
|
- <h3 class="panel-title">告警类型统计(15天内)</h3>
|
|
|
|
|
|
|
+ <h3 class="panel-title">
|
|
|
|
|
+ 告警类型统计(15天内)
|
|
|
|
|
+ <span class="title-count">{{ alarmRangeText }}</span>
|
|
|
|
|
+ </h3>
|
|
|
<div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
|
|
<div ref="alarmChartRef" class="chart" v-loading="loading.alarm" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -58,15 +61,13 @@
|
|
|
<h3 class="panel-title">设备列表 <span class="title-count">共 {{ filteredDevices.length }} 台</span></h3>
|
|
<h3 class="panel-title">设备列表 <span class="title-count">共 {{ filteredDevices.length }} 台</span></h3>
|
|
|
<div class="filter-bar">
|
|
<div class="filter-bar">
|
|
|
<el-select v-model="deviceTypeFilter" size="small" placeholder="设备类型" clearable style="width: 120px">
|
|
<el-select v-model="deviceTypeFilter" size="small" placeholder="设备类型" clearable style="width: 120px">
|
|
|
- <el-option label="供水窨井" value="5" />
|
|
|
|
|
- <el-option label="排水窨井" value="9" />
|
|
|
|
|
- <el-option label="燃气窨井" value="10" />
|
|
|
|
|
|
|
+ <el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</div>
|
|
</div>
|
|
|
<el-table :data="filteredDevices" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.device">
|
|
<el-table :data="filteredDevices" class="panel-table" :header-cell-style="tableHeaderStyle" size="small" v-loading="loading.device">
|
|
|
<el-table-column prop="name" label="设备名称" min-width="100" show-overflow-tooltip />
|
|
<el-table-column prop="name" label="设备名称" min-width="100" show-overflow-tooltip />
|
|
|
<el-table-column label="类型" width="70">
|
|
<el-table-column label="类型" width="70">
|
|
|
- <template #default="{ row }">{{ typeText(row.typeId) }}</template>
|
|
|
|
|
|
|
+ <template #default="{ row }">{{ typeText(row.typeId, row.typeName) }}</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
<el-table-column label="电量" width="60">
|
|
<el-table-column label="电量" width="60">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
@@ -108,6 +109,8 @@ import { ORDER_STATUS_GROUP } from '@/views/subSystem/pipeNetwork/ops/workOrder/
|
|
|
import { getAlarmDataPage } from '@/api/drainage'
|
|
import { getAlarmDataPage } from '@/api/drainage'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import locationIcon from '@/assets/images/location.png'
|
|
import locationIcon from '@/assets/images/location.png'
|
|
|
|
|
+// 台账存 WGS84(设备表单选点经 bd09ToWgs84 回填),百度地图必须转 BD-09 再画
|
|
|
|
|
+import { convertCoord } from '@/utils/coordTransform'
|
|
|
|
|
|
|
|
// ==================== 状态 ====================
|
|
// ==================== 状态 ====================
|
|
|
const loading = reactive({ work: false, alarm: false, device: false })
|
|
const loading = reactive({ work: false, alarm: false, device: false })
|
|
@@ -117,6 +120,15 @@ const workOrders = ref([])
|
|
|
const allDevices = ref([])
|
|
const allDevices = ref([])
|
|
|
const dynamicLogs = ref([])
|
|
const dynamicLogs = ref([])
|
|
|
const deviceTypeFilter = ref('')
|
|
const deviceTypeFilter = ref('')
|
|
|
|
|
+const alarmRangeText = ref(formatAlarmRange())
|
|
|
|
|
+
|
|
|
|
|
+/** 告警统计区间文案,空数据时也展示,避免面板看不出统计范围 */
|
|
|
|
|
+function formatAlarmRange() {
|
|
|
|
|
+ const end = new Date()
|
|
|
|
|
+ const start = new Date(end.getTime() - ALARM_RANGE_DAYS * 24 * 3600 * 1000)
|
|
|
|
|
+ const label = date => `${date.getMonth() + 1}-${String(date.getDate()).padStart(2, '0')}`
|
|
|
|
|
+ return `${label(start)} ~ ${label(end)}`
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
// 图表引用
|
|
// 图表引用
|
|
|
const alarmChartRef = ref(null)
|
|
const alarmChartRef = ref(null)
|
|
@@ -125,7 +137,8 @@ const deviceTypeChartRef = ref(null)
|
|
|
let alarmChart = null, areaChart = null, deviceTypeChart = null
|
|
let alarmChart = null, areaChart = null, deviceTypeChart = null
|
|
|
|
|
|
|
|
// ==================== 常量映射 ====================
|
|
// ==================== 常量映射 ====================
|
|
|
-const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
|
|
|
|
|
|
|
+/** 历史遗留窨井类型编码,仅用于后端未返回 typeName 时兜底展示 */
|
|
|
|
|
+const LEGACY_TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
|
|
|
|
|
|
|
|
// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
|
|
// 所属片区:后端 DistrictEnum(0-城南 1-城北 2-太常)
|
|
|
const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
|
|
const DISTRICT_MAP = { 0: '城南', 1: '城北', 2: '太常' }
|
|
@@ -140,7 +153,16 @@ const priorityClass = (l) => ({ 0: 'high', 1: 'medium', 2: 'low' }[l] || 'low')
|
|
|
const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
|
|
const TODO_ORDER_STATUS = ORDER_STATUS_GROUP.todo.join(',')
|
|
|
/** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
|
|
/** 设备父类别ID(equipment_type.parent_id),与工单列表页保持同一口径 */
|
|
|
const DEVICE_TYPE_PARENT_ID = '4'
|
|
const DEVICE_TYPE_PARENT_ID = '4'
|
|
|
-const typeText = (id) => TYPE_MAP[id] || '-'
|
|
|
|
|
|
|
+/** 窨井设备大类根ID(EquipmentTypeEnum.ROOT_MANHOLE_DEVICE),报警查询按大类隔离 */
|
|
|
|
|
+const MANHOLE_ROOT_TYPE_ID = 4
|
|
|
|
|
+/** 报警查询后端限制 pageSize 不超过 100 */
|
|
|
|
|
+const ALARM_PAGE_SIZE = 100
|
|
|
|
|
+const ALARM_MAX_PAGES = 3
|
|
|
|
|
+/** 告警统计窗口(天) */
|
|
|
|
|
+const ALARM_RANGE_DAYS = 15
|
|
|
|
|
+/** 无设备数据时仍展示的区域维度,取自 DistrictEnum(0-城南 1-城北 2-太常) */
|
|
|
|
|
+const DEFAULT_DISTRICTS = ['城南', '城北', '太常']
|
|
|
|
|
+const typeText = (id, name) => name || LEGACY_TYPE_MAP[id] || (id ? `类型${id}` : '-')
|
|
|
const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
|
|
const statusText = (s) => ({ normal: '在线', alert: '告警', offline: '离线' }[s] || s)
|
|
|
const batteryTag = (b) => {
|
|
const batteryTag = (b) => {
|
|
|
if (b == null) return 'info'
|
|
if (b == null) return 'info'
|
|
@@ -150,9 +172,20 @@ const batteryTag = (b) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==================== 筛选后设备列表 ====================
|
|
// ==================== 筛选后设备列表 ====================
|
|
|
|
|
+/** 设备类型下拉项:跟着台账实际类型走,不再写死 5/9/10 */
|
|
|
|
|
+const deviceTypeOptions = computed(() => {
|
|
|
|
|
+ const map = new Map()
|
|
|
|
|
+ allDevices.value.forEach(d => {
|
|
|
|
|
+ const value = String(d.typeId ?? '')
|
|
|
|
|
+ if (!value || map.has(value)) return
|
|
|
|
|
+ map.set(value, { value, label: typeText(d.typeId, d.typeName) })
|
|
|
|
|
+ })
|
|
|
|
|
+ return Array.from(map.values())
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const filteredDevices = computed(() => {
|
|
const filteredDevices = computed(() => {
|
|
|
if (!deviceTypeFilter.value) return allDevices.value
|
|
if (!deviceTypeFilter.value) return allDevices.value
|
|
|
- return allDevices.value.filter(d => d.typeId === deviceTypeFilter.value)
|
|
|
|
|
|
|
+ return allDevices.value.filter(d => String(d.typeId) === deviceTypeFilter.value)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// ==================== 数据加载 ====================
|
|
// ==================== 数据加载 ====================
|
|
@@ -197,6 +230,9 @@ async function loadDevices() {
|
|
|
refreshMarkers()
|
|
refreshMarkers()
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
ElMessage.error('设备数据加载失败')
|
|
ElMessage.error('设备数据加载失败')
|
|
|
|
|
+ // 加载失败也铺出空态骨架,面板不留白
|
|
|
|
|
+ renderAreaChart()
|
|
|
|
|
+ renderDeviceTypeChart()
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.device = false
|
|
loading.device = false
|
|
|
}
|
|
}
|
|
@@ -219,33 +255,63 @@ async function loadWorkOrders() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function formatDateTime(date) {
|
|
|
|
|
+ const pad = value => String(value).padStart(2, '0')
|
|
|
|
|
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 报警类型展示:优先报警内容,其次报警编码 */
|
|
|
|
|
+function alarmTypeText(record) {
|
|
|
|
|
+ return record.alarmContent || record.warningCode || '其他告警'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 拉取近 15 天的窨井报警
|
|
|
|
|
+ *
|
|
|
|
|
+ * rootTypeId 是报警查询的必填项(后端校验「设备大类不能为空」),
|
|
|
|
|
+ * pageSize 上限 100,因此按页累加,最多取 3 页用于首页统计。
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchRecentAlarms() {
|
|
|
|
|
+ const params = {
|
|
|
|
|
+ rootTypeId: MANHOLE_ROOT_TYPE_ID,
|
|
|
|
|
+ startTime: formatDateTime(new Date(Date.now() - 15 * 24 * 3600 * 1000))
|
|
|
|
|
+ }
|
|
|
|
|
+ const records = []
|
|
|
|
|
+ for (let page = 1; page <= ALARM_MAX_PAGES; page++) {
|
|
|
|
|
+ const res = await getAlarmDataPage(page, ALARM_PAGE_SIZE, params)
|
|
|
|
|
+ const pageRecords = res.rows || res.data?.records || res.data || []
|
|
|
|
|
+ records.push(...pageRecords)
|
|
|
|
|
+ if (pageRecords.length < ALARM_PAGE_SIZE) break
|
|
|
|
|
+ }
|
|
|
|
|
+ return records
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function loadAlarmData() {
|
|
async function loadAlarmData() {
|
|
|
loading.alarm = true
|
|
loading.alarm = true
|
|
|
|
|
+ alarmRangeText.value = formatAlarmRange()
|
|
|
try {
|
|
try {
|
|
|
- const res = await getAlarmDataPage(1, 500, {})
|
|
|
|
|
- const records = res.rows || res.data?.records || res.data || []
|
|
|
|
|
- const now = new Date()
|
|
|
|
|
- const fifteenDaysAgo = new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000)
|
|
|
|
|
- // 筛选15天内的告警
|
|
|
|
|
- const recentAlarms = records.filter(r => {
|
|
|
|
|
- const t = new Date(r.createTime)
|
|
|
|
|
- return t >= fifteenDaysAgo
|
|
|
|
|
- })
|
|
|
|
|
- // 告警类型统计
|
|
|
|
|
|
|
+ const records = await fetchRecentAlarms()
|
|
|
|
|
+ const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 3600 * 1000)
|
|
|
|
|
+ const recentAlarms = records.filter(r => !r.createTime || new Date(String(r.createTime).replace(/-/g, '/')) >= fifteenDaysAgo)
|
|
|
|
|
+
|
|
|
const typeMap = {}
|
|
const typeMap = {}
|
|
|
recentAlarms.forEach(r => {
|
|
recentAlarms.forEach(r => {
|
|
|
- const type = r.warningType || '其他'
|
|
|
|
|
|
|
+ const type = alarmTypeText(r)
|
|
|
typeMap[type] = (typeMap[type] || 0) + 1
|
|
typeMap[type] = (typeMap[type] || 0) + 1
|
|
|
})
|
|
})
|
|
|
renderAlarmChart(typeMap)
|
|
renderAlarmChart(typeMap)
|
|
|
|
|
+
|
|
|
// 动态信息(最新10条)
|
|
// 动态信息(最新10条)
|
|
|
dynamicLogs.value = records.slice(0, 10).map(r => ({
|
|
dynamicLogs.value = records.slice(0, 10).map(r => ({
|
|
|
time: r.createTime || '',
|
|
time: r.createTime || '',
|
|
|
- event: `${r.warningType || '告警'} - ${r.deviceName || r.deviceCode || ''}`,
|
|
|
|
|
|
|
+ event: `${alarmTypeText(r)} - ${r.deviceName || r.deviceCode || ''}`,
|
|
|
alarmStatus: r.alarmStatus ?? 0
|
|
alarmStatus: r.alarmStatus ?? 0
|
|
|
}))
|
|
}))
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
|
|
+ console.error(e)
|
|
|
ElMessage.error('告警数据加载失败')
|
|
ElMessage.error('告警数据加载失败')
|
|
|
|
|
+ // 加载失败也铺出空态骨架(含统计区间),面板不留白
|
|
|
|
|
+ renderAlarmChart({})
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.alarm = false
|
|
loading.alarm = false
|
|
|
}
|
|
}
|
|
@@ -256,19 +322,30 @@ function renderAlarmChart(typeMap) {
|
|
|
if (!alarmChartRef.value) return
|
|
if (!alarmChartRef.value) return
|
|
|
if (!alarmChart) alarmChart = echarts.init(alarmChartRef.value)
|
|
if (!alarmChart) alarmChart = echarts.init(alarmChartRef.value)
|
|
|
const colors = ['#ff3333', '#ffb333', '#33cc33', '#3399ff', '#9966ff', '#ff6699', '#33cccc']
|
|
const colors = ['#ff3333', '#ffb333', '#33cc33', '#3399ff', '#9966ff', '#ff6699', '#33cccc']
|
|
|
- const data = Object.entries(typeMap).map(([name, value], i) => ({
|
|
|
|
|
|
|
+ const data = Object.entries(typeMap || {}).map(([name, value], i) => ({
|
|
|
name, value, itemStyle: { color: colors[i % colors.length] }
|
|
name, value, itemStyle: { color: colors[i % colors.length] }
|
|
|
}))
|
|
}))
|
|
|
|
|
+ const hasData = data.length > 0
|
|
|
alarmChart.setOption({
|
|
alarmChart.setOption({
|
|
|
backgroundColor: 'transparent',
|
|
backgroundColor: 'transparent',
|
|
|
|
|
+ // 无告警时保留统计区间与说明,避免面板看起来是空的
|
|
|
|
|
+ title: hasData ? { show: false } : {
|
|
|
|
|
+ text: '近 15 天暂无告警',
|
|
|
|
|
+ subtext: `统计区间 ${alarmRangeText.value}`,
|
|
|
|
|
+ left: 'center',
|
|
|
|
|
+ top: 'middle',
|
|
|
|
|
+ textStyle: { color: '#9ba8c0', fontSize: 12, fontWeight: 'normal' },
|
|
|
|
|
+ subtextStyle: { color: '#6f7d95', fontSize: 11 }
|
|
|
|
|
+ },
|
|
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
|
- legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 } },
|
|
|
|
|
- series: [{
|
|
|
|
|
|
|
+ legend: { bottom: 0, textStyle: { color: '#ccc', fontSize: 10 }, show: hasData },
|
|
|
|
|
+ // 空数据时不挂饼图系列,否则会残留一个灰色的空圆环
|
|
|
|
|
+ series: hasData ? [{
|
|
|
type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
|
|
type: 'pie', radius: ['35%', '60%'], center: ['50%', '40%'],
|
|
|
label: { color: '#ccc', fontSize: 10 },
|
|
label: { color: '#ccc', fontSize: 10 },
|
|
|
- data: data.length ? data : [{ name: '暂无数据', value: 0 }]
|
|
|
|
|
- }]
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ data
|
|
|
|
|
+ }] : []
|
|
|
|
|
+ }, true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderAreaChart() {
|
|
function renderAreaChart() {
|
|
@@ -281,47 +358,88 @@ function renderAreaChart() {
|
|
|
const shortArea = area.length > 6 ? area.slice(0, 6) + '...' : area
|
|
const shortArea = area.length > 6 ? area.slice(0, 6) + '...' : area
|
|
|
areaMap[shortArea] = (areaMap[shortArea] || 0) + 1
|
|
areaMap[shortArea] = (areaMap[shortArea] || 0) + 1
|
|
|
})
|
|
})
|
|
|
- const entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
|
|
|
|
|
+ let entries = Object.entries(areaMap).sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
|
|
|
+ const hasData = entries.length > 0
|
|
|
|
|
+ // 无设备时仍按默认区域维度铺出骨架,面板不会空成一片
|
|
|
|
|
+ if (!hasData) {
|
|
|
|
|
+ entries = DEFAULT_DISTRICTS.map(name => [name, 0])
|
|
|
|
|
+ }
|
|
|
areaChart.setOption({
|
|
areaChart.setOption({
|
|
|
backgroundColor: 'transparent',
|
|
backgroundColor: 'transparent',
|
|
|
|
|
+ title: hasData ? { show: false } : {
|
|
|
|
|
+ text: '暂无设备,按默认区域维度展示',
|
|
|
|
|
+ left: 'center',
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ textStyle: { color: '#6f7d95', fontSize: 11, fontWeight: 'normal' }
|
|
|
|
|
+ },
|
|
|
tooltip: { trigger: 'axis' },
|
|
tooltip: { trigger: 'axis' },
|
|
|
- grid: { left: '12%', right: '8%', bottom: '18%', top: '8%' },
|
|
|
|
|
|
|
+ grid: { left: '12%', right: '8%', bottom: '18%', top: '18%' },
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'category', data: entries.map(e => e[0]),
|
|
type: 'category', data: entries.map(e => e[0]),
|
|
|
axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
|
|
axisLabel: { color: '#ccc', fontSize: 10, interval: 0, rotate: 30 }
|
|
|
},
|
|
},
|
|
|
- yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
|
|
|
|
|
- series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' } }]
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value', minInterval: 1,
|
|
|
|
|
+ axisLabel: { color: '#ccc' },
|
|
|
|
|
+ splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [{ type: 'bar', data: entries.map(e => e[1]), itemStyle: { color: '#3399ff' }, barWidth: '40%' }]
|
|
|
|
|
+ }, true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderDeviceTypeChart() {
|
|
function renderDeviceTypeChart() {
|
|
|
if (!deviceTypeChartRef.value) return
|
|
if (!deviceTypeChartRef.value) return
|
|
|
if (!deviceTypeChart) deviceTypeChart = echarts.init(deviceTypeChartRef.value)
|
|
if (!deviceTypeChart) deviceTypeChart = echarts.init(deviceTypeChartRef.value)
|
|
|
- const typeCount = { '5': 0, '9': 0, '10': 0 }
|
|
|
|
|
- allDevices.value.forEach(d => { if (typeCount[d.typeId] != null) typeCount[d.typeId]++ })
|
|
|
|
|
- const labels = ['供水窨井', '排水窨井', '燃气窨井']
|
|
|
|
|
- const values = [typeCount['5'], typeCount['9'], typeCount['10']]
|
|
|
|
|
- const colors = ['#3399ff', '#67c23a', '#e6a23c']
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 按台账实际存在的设备类型聚合,避免把不存在的旧类型当成固定三分类
|
|
|
|
|
+ const countMap = new Map()
|
|
|
|
|
+ allDevices.value.forEach(d => {
|
|
|
|
|
+ const label = typeText(d.typeId, d.typeName)
|
|
|
|
|
+ countMap.set(label, (countMap.get(label) || 0) + 1)
|
|
|
|
|
+ })
|
|
|
|
|
+ // 有设备但类型名为空时,至少给出一个占位分类,面板不留白
|
|
|
|
|
+ if (!countMap.size && allDevices.value.length) {
|
|
|
|
|
+ countMap.set('未分类', allDevices.value.length)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const labels = Array.from(countMap.keys())
|
|
|
|
|
+ const values = Array.from(countMap.values())
|
|
|
|
|
+ const palette = ['#3399ff', '#67c23a', '#e6a23c', '#f56c6c', '#9b59b6']
|
|
|
|
|
+
|
|
|
deviceTypeChart.setOption({
|
|
deviceTypeChart.setOption({
|
|
|
backgroundColor: 'transparent',
|
|
backgroundColor: 'transparent',
|
|
|
tooltip: { trigger: 'axis' },
|
|
tooltip: { trigger: 'axis' },
|
|
|
grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
|
|
grid: { left: '12%', right: '8%', bottom: '15%', top: '8%' },
|
|
|
|
|
+ title: labels.length ? undefined : {
|
|
|
|
|
+ text: '暂无窨井设备',
|
|
|
|
|
+ subtext: '设备台账中未查到窨井类设备',
|
|
|
|
|
+ left: 'center',
|
|
|
|
|
+ top: 'middle',
|
|
|
|
|
+ textStyle: { color: '#ccc', fontSize: 12, fontWeight: 'normal' },
|
|
|
|
|
+ subtextStyle: { color: '#888', fontSize: 11 }
|
|
|
|
|
+ },
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'category', data: labels,
|
|
type: 'category', data: labels,
|
|
|
axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
|
|
axisLabel: { color: '#ccc', fontSize: 11, interval: 0 }
|
|
|
},
|
|
},
|
|
|
- yAxis: { type: 'value', axisLabel: { color: '#ccc' }, splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } } },
|
|
|
|
|
- series: [{
|
|
|
|
|
- type: 'bar', data: values.map((v, i) => ({ value: v, itemStyle: { color: colors[i] } })),
|
|
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value', minInterval: 1,
|
|
|
|
|
+ axisLabel: { color: '#ccc' },
|
|
|
|
|
+ splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: labels.length ? [{
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ data: values.map((v, i) => ({ value: v, itemStyle: { color: palette[i % palette.length] } })),
|
|
|
barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
|
|
barWidth: '40%', label: { show: true, position: 'top', color: '#ccc' }
|
|
|
- }]
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ }] : []
|
|
|
|
|
+ }, true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==================== 百度地图 ====================
|
|
// ==================== 百度地图 ====================
|
|
|
let mapInstance = null
|
|
let mapInstance = null
|
|
|
|
|
+let mapFitted = false
|
|
|
const markerMap = new Map()
|
|
const markerMap = new Map()
|
|
|
|
|
+/** 兜底中心(沅陵县城),WGS84 口径,与设备台账一致 */
|
|
|
const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
|
|
const YUANLING_CENTER = { lng: 110.393, lat: 28.452 }
|
|
|
|
|
|
|
|
function initMap() {
|
|
function initMap() {
|
|
@@ -329,11 +447,30 @@ function initMap() {
|
|
|
if (!container || typeof BMapGL === 'undefined') return
|
|
if (!container || typeof BMapGL === 'undefined') return
|
|
|
try {
|
|
try {
|
|
|
mapInstance = new BMapGL.Map('homeMap')
|
|
mapInstance = new BMapGL.Map('homeMap')
|
|
|
- mapInstance.centerAndZoom(new BMapGL.Point(YUANLING_CENTER.lng, YUANLING_CENTER.lat), 13)
|
|
|
|
|
|
|
+ const [centerLng, centerLat] = convertCoord(YUANLING_CENTER.lng, YUANLING_CENTER.lat)
|
|
|
|
|
+ mapInstance.centerAndZoom(new BMapGL.Point(centerLng, centerLat), 15)
|
|
|
mapInstance.enableScrollWheelZoom(true)
|
|
mapInstance.enableScrollWheelZoom(true)
|
|
|
} catch (e) { console.error('地图初始化失败', e) }
|
|
} catch (e) { console.error('地图初始化失败', e) }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 有设备时按设备范围取视野;点太近时限制放大层级,避免只剩一栋楼 */
|
|
|
|
|
+function fitMapToDevices(points) {
|
|
|
|
|
+ if (!mapInstance || !points.length) return
|
|
|
|
|
+ if (points.length === 1) {
|
|
|
|
|
+ mapInstance.centerAndZoom(points[0], 16)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const lngs = points.map(p => p.lng)
|
|
|
|
|
+ const lats = points.map(p => p.lat)
|
|
|
|
|
+ const span = Math.max(Math.max(...lngs) - Math.min(...lngs), Math.max(...lats) - Math.min(...lats))
|
|
|
|
|
+ if (span < 0.002) {
|
|
|
|
|
+ const mid = new BMapGL.Point((Math.max(...lngs) + Math.min(...lngs)) / 2, (Math.max(...lats) + Math.min(...lats)) / 2)
|
|
|
|
|
+ mapInstance.centerAndZoom(mid, 16)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mapInstance.setViewport(points, { margins: [50, 50, 50, 50] })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function getStatusColor(status) {
|
|
function getStatusColor(status) {
|
|
|
if (status === 'alert') return '#F56C6C'
|
|
if (status === 'alert') return '#F56C6C'
|
|
|
if (status === 'offline') return '#909399'
|
|
if (status === 'offline') return '#909399'
|
|
@@ -344,8 +481,13 @@ function refreshMarkers() {
|
|
|
if (!mapInstance) return
|
|
if (!mapInstance) return
|
|
|
markerMap.forEach(label => mapInstance.removeOverlay(label))
|
|
markerMap.forEach(label => mapInstance.removeOverlay(label))
|
|
|
markerMap.clear()
|
|
markerMap.clear()
|
|
|
|
|
+ const devicePoints = []
|
|
|
allDevices.value.forEach(d => {
|
|
allDevices.value.forEach(d => {
|
|
|
if (d.lng == null || d.lat == null) return
|
|
if (d.lng == null || d.lat == null) return
|
|
|
|
|
+ // 关键:台账是 WGS84,百度地图是 BD-09,不转换会偏约 1 公里
|
|
|
|
|
+ const [bdLng, bdLat] = convertCoord(Number(d.lng), Number(d.lat))
|
|
|
|
|
+ const point = new BMapGL.Point(bdLng, bdLat)
|
|
|
|
|
+ devicePoints.push(point)
|
|
|
const color = getStatusColor(d.status)
|
|
const color = getStatusColor(d.status)
|
|
|
const isAttention = d.status === 'alert' || d.status === 'offline'
|
|
const isAttention = d.status === 'alert' || d.status === 'offline'
|
|
|
const attentionText = d.status === 'alert' ? '告警' : '离线'
|
|
const attentionText = d.status === 'alert' ? '告警' : '离线'
|
|
@@ -362,13 +504,18 @@ function refreshMarkers() {
|
|
|
<div><img src="${locationIcon}" alt="" style="width:22px;height:22px;display:block;margin:0 auto;"/></div>
|
|
<div><img src="${locationIcon}" alt="" style="width:22px;height:22px;display:block;margin:0 auto;"/></div>
|
|
|
</div>`
|
|
</div>`
|
|
|
const label = new BMapGL.Label(html, {
|
|
const label = new BMapGL.Label(html, {
|
|
|
- position: new BMapGL.Point(d.lng, d.lat),
|
|
|
|
|
|
|
+ position: point,
|
|
|
offset: new BMapGL.Size(isAttention ? -62 : -25, isAttention ? -68 : -40)
|
|
offset: new BMapGL.Size(isAttention ? -62 : -25, isAttention ? -68 : -40)
|
|
|
})
|
|
})
|
|
|
label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: isAttention ? '20' : '10' })
|
|
label.setStyle({ border: 'none', background: 'transparent', padding: '0', zIndex: isAttention ? '20' : '10' })
|
|
|
mapInstance.addOverlay(label)
|
|
mapInstance.addOverlay(label)
|
|
|
markerMap.set(d.id, label)
|
|
markerMap.set(d.id, label)
|
|
|
})
|
|
})
|
|
|
|
|
+ // 只在首次拿到设备时校准视野,后续刷新不打断用户手动缩放
|
|
|
|
|
+ if (!mapFitted && devicePoints.length) {
|
|
|
|
|
+ fitMapToDevices(devicePoints)
|
|
|
|
|
+ mapFitted = true
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==================== 生命周期 ====================
|
|
// ==================== 生命周期 ====================
|