WaterAlarmPage.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. <template>
  2. <div class="water-page alarm-page">
  3. <div class="page-heading">
  4. <div class="page-title"><span class="page-title-icon"><el-icon><component :is="pageIcon"/></el-icon></span>
  5. <div><h2>{{ title }}</h2><span class="muted">{{ subtitle }}</span></div>
  6. </div>
  7. <el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
  8. </div>
  9. <div v-if="mode === 'manage'" class="metric-grid">
  10. <div class="metric"><span>实时报警</span><strong class="danger">{{ stats.realtimeAlarmCount ??
  11. stats.realtimeCount ?? 0 }}</strong><small>条</small></div>
  12. <div class="metric"><span>今日报警</span><strong>{{ stats.todayAlarmCount ?? stats.todayCount ?? 0
  13. }}</strong><small>条</small></div>
  14. <div class="metric"><span>设备异常率</span><strong>{{ stats.deviceAbnormalRate ?? stats.abnormalRate ?? '-'
  15. }}</strong><small>%</small></div>
  16. <div class="metric"><span>平均响应时间</span><strong>{{ stats.avgResponseMinutes ??
  17. stats.averageResponseMinutes ?? '-' }}</strong><small>分钟</small></div>
  18. </div>
  19. <el-card v-if="mode === 'manage'" shadow="never" class="realtime-card">
  20. <div class="card-title"><span>实时异常卡片</span><span class="muted">设备、位置与阈值</span></div>
  21. <div v-if="!realtime.length" class="empty-inline">当前无未处理报警</div>
  22. <div v-else class="realtime-grid">
  23. <button v-for="item in realtime" :key="item.id || item.alarmId" class="realtime-item"
  24. @click="openDetail(item)">
  25. <div class="realtime-head">
  26. <el-tag :type="levelType(item.alarmLevel)">{{ item.alarmLevelText || levelText(item.alarmLevel)
  27. }}
  28. </el-tag>
  29. <b>{{ item.deviceName || item.deviceCode || '监测设备'
  30. }}</b><span>{{ item.alarmTime || '-' }}</span></div>
  31. <div class="realtime-body"><span>{{ item.equipmentLocation || '位置暂无' }}</span><strong>{{
  32. item.actualValue ?? '-' }}</strong><small>阈值 {{ thresholdText(item) }}</small></div>
  33. </button>
  34. </div>
  35. </el-card>
  36. <el-card shadow="never" class="filter-card">
  37. <el-form :inline="true" :model="query">
  38. <el-form-item label="关键字">
  39. <el-input v-model="query.keyword" clearable placeholder="设备编码/报警类型"/>
  40. </el-form-item>
  41. <el-form-item label="报警等级">
  42. <el-select v-model="query.alarmLevel" clearable placeholder="全部">
  43. <el-option v-for="item in levels" :key="item.value" v-bind="item"/>
  44. </el-select>
  45. </el-form-item>
  46. <el-form-item label="状态">
  47. <el-select v-model="query.alarmStatus" clearable placeholder="全部">
  48. <el-option label="未处理" :value="0"/>
  49. <el-option label="已处理" :value="1"/>
  50. </el-select>
  51. </el-form-item>
  52. <el-form-item>
  53. <el-button type="primary" @click="loadData">查询</el-button>
  54. <el-button @click="resetQuery">重置</el-button>
  55. </el-form-item>
  56. </el-form>
  57. </el-card>
  58. <el-card v-if="mode !== 'map'" shadow="never" class="table-card">
  59. <div class="card-title"><span>{{ mode === 'audit' ? '待审核报警清单' : '报警历史清单' }}</span></div>
  60. <el-table :data="page.records" border stripe v-loading="loading">
  61. <el-table-column type="index" label="#" width="55"/>
  62. <el-table-column prop="alarmCode" label="报警编码" min-width="145"/>
  63. <el-table-column prop="deviceCode" label="设备编码" min-width="125"/>
  64. <el-table-column prop="warningType" label="报警类型" min-width="130"/>
  65. <el-table-column prop="alarmLevel" label="报警等级" width="100">
  66. <template #default="{row}">
  67. <el-tag :type="levelType(row.alarmLevel)">{{ row.alarmLevelText || levelText(row.alarmLevel)
  68. }}
  69. </el-tag>
  70. </template>
  71. </el-table-column>
  72. <el-table-column prop="alarmTime" label="触发时间" min-width="165"/>
  73. <el-table-column prop="alarmStatus" label="状态" width="95">
  74. <template #default="{row}">
  75. <el-tag :type="Number(row.alarmStatus) === 1 ? 'success' : 'danger'">{{ Number(row.alarmStatus)
  76. === 1 ? '已处理' : '未处理' }}
  77. </el-tag>
  78. </template>
  79. </el-table-column>
  80. <el-table-column label="操作" width="220" fixed="right">
  81. <template #default="{row}">
  82. <el-button link type="primary" @click="openDetail(row)">{{ mode === 'audit' ? '审核' : '详情'
  83. }}
  84. </el-button>
  85. <el-button v-if="mode === 'manage' && Number(row.alarmStatus) !== 1" link type="success"
  86. @click="handleResolve(row)">标记处理
  87. </el-button>
  88. <el-button v-if="mode === 'manage' && Number(row.alarmStatus) !== 1" link type="warning"
  89. @click="openDispatch(row)">派单
  90. </el-button>
  91. </template>
  92. </el-table-column>
  93. </el-table>
  94. <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="page.total"
  95. layout="total, sizes, prev, pager, next" :page-sizes="[10,20,50]" @change="loadData"/>
  96. </el-card>
  97. <el-card v-else shadow="never" class="water-panel map-card">
  98. <WaterGisPageShell
  99. embedded
  100. eyebrow="WATER SUPPLY ALARM GIS"
  101. title="监测报警一张图"
  102. subtitle="按监测点状态着色"
  103. :loading="loading"
  104. >
  105. <template #actions>
  106. <el-input v-model="alarmSearch" class="search-input" placeholder="搜索设备/报警类型" clearable
  107. :prefix-icon="Search"/>
  108. <el-button :icon="Refresh" @click="loadData">刷新</el-button>
  109. </template>
  110. <template #metrics>
  111. <div class="metric">
  112. <el-icon class="metric-icon"><Bell/></el-icon>
  113. <span>报警总数</span><strong>{{ mapAlarmCount }}</strong><small>条</small>
  114. </div>
  115. <div class="metric">
  116. <el-icon class="metric-icon"><Warning/></el-icon>
  117. <span>高等级报警</span><strong class="danger">{{ mapCriticalCount }}</strong><small>条</small>
  118. </div>
  119. <div class="metric">
  120. <el-icon class="metric-icon"><CircleCheck/></el-icon>
  121. <span>已处理报警</span><strong>{{ mapResolvedCount }}</strong><small>条</small>
  122. </div>
  123. <div class="metric">
  124. <el-icon class="metric-icon"><Location/></el-icon>
  125. <span>地图点位</span><strong>{{ mappedCount }}</strong><small>个</small>
  126. </div>
  127. </template>
  128. <template #controls>
  129. <div class="panel-section alarm-detail">
  130. <h3>选中报警</h3>
  131. <template v-if="selectedPoint.id || selectedPoint.alarmId">
  132. <div class="detail-name">{{ selectedPoint.deviceCode || selectedPoint.name || '报警点' }}</div>
  133. <el-descriptions :column="1" border size="small">
  134. <el-descriptions-item label="报警等级">
  135. <el-tag :type="levelType(selectedPoint.alarmLevel)">{{ selectedPoint.alarmLevelText || levelText(selectedPoint.alarmLevel) }}</el-tag>
  136. </el-descriptions-item>
  137. <el-descriptions-item label="报警类型">{{ selectedPoint.warningType || selectedPoint.alarmType || '-' }}</el-descriptions-item>
  138. <el-descriptions-item label="触发时间">{{ selectedPoint.alarmTime || selectedPoint.createTime || '-' }}</el-descriptions-item>
  139. <el-descriptions-item label="状态">
  140. <el-tag :type="Number(selectedPoint.alarmStatus) === 1 ? 'success' : 'danger'">
  141. {{ Number(selectedPoint.alarmStatus) === 1 ? '已处理' : '未处理' }}
  142. </el-tag>
  143. </el-descriptions-item>
  144. <el-descriptions-item label="位置">{{ selectedPoint.equipmentLocation || selectedPoint.location || '-' }}</el-descriptions-item>
  145. </el-descriptions>
  146. <el-button class="detail-action" type="primary" link @click="openDetail(selectedPoint)">查看完整详情</el-button>
  147. </template>
  148. <el-empty v-else description="点击点位查看详情" :image-size="65"/>
  149. </div>
  150. </template>
  151. <template #legend>
  152. <div class="panel-section">
  153. <h3>报警图层与图例</h3>
  154. <div class="map-legend">
  155. <div class="map-legend-item"><i class="map-legend-dot status-dot-normal"/>正常</div>
  156. <div class="map-legend-item"><i class="map-legend-dot status-dot-alarm"/>报警</div>
  157. <div class="map-legend-item"><i class="map-legend-dot status-dot-warning"/>预警</div>
  158. <div class="map-legend-item"><i class="map-legend-dot status-dot-work-order"/>关联工单</div>
  159. <div class="map-legend-item"><i class="map-legend-dot status-dot-offline"/>离线</div>
  160. </div>
  161. </div>
  162. </template>
  163. <template #results>
  164. <div class="panel-section result-section">
  165. <h3>搜索报警</h3>
  166. <el-alert v-if="mapError" type="warning" show-icon :closable="false" :title="mapError"/>
  167. <el-empty v-else-if="!filteredAlarmPoints.length" description="暂无报警点位" :image-size="72"/>
  168. <div v-else class="device-result-list">
  169. <button v-for="point in filteredAlarmPoints" :key="point.id || point.alarmId || point.deviceCode"
  170. type="button" @click="locateAlarmPoint(point)">
  171. <span class="device-name">{{ point.deviceName || point.deviceCode || point.name || '报警点' }}</span>
  172. <span class="device-meta">{{ point.warningType || point.alarmType || '监测报警' }} · {{ point.alarmTime || point.createTime || '-' }}</span>
  173. <el-tag size="small" :type="levelType(point.alarmLevel)">
  174. {{ point.alarmLevelText || levelText(point.alarmLevel) }}
  175. </el-tag>
  176. </button>
  177. </div>
  178. </div>
  179. </template>
  180. <template #map>
  181. <GisMapCanvas
  182. ref="gisMapRef"
  183. :points="gisMapPoints"
  184. :center="{ lng: 110.3965, lat: 28.4638 }"
  185. :zoom="15"
  186. @point-click="openDetail"
  187. @map-click="detailVisible = false"
  188. @error="mapError = $event"
  189. />
  190. <el-alert v-if="mapError" class="map-error" type="warning" show-icon :closable="false" :title="mapError"/>
  191. <el-empty v-if="!gisMapPoints.length && !mapError" class="map-empty" description="当前筛选下没有可显示的报警点位"/>
  192. </template>
  193. </WaterGisPageShell>
  194. </el-card>
  195. <el-drawer v-model="detailVisible" class="detail-drawer alarm-detail-drawer"
  196. :title="mode === 'audit' ? '报警审核' : '报警详情'" direction="rtl" size="720px" append-to-body>
  197. <el-descriptions :column="2" border>
  198. <el-descriptions-item v-for="(value,key) in detailEntries" :key="key" :label="key">{{ value ?? '-' }}
  199. </el-descriptions-item>
  200. </el-descriptions>
  201. <div v-if="mode === 'audit'" class="audit-form">
  202. <el-form :model="auditForm" label-width="90px">
  203. <el-form-item label="审核动作">
  204. <el-radio-group v-model="auditForm.auditAction">
  205. <el-radio :value="1">审核</el-radio>
  206. <el-radio :value="2">解除</el-radio>
  207. </el-radio-group>
  208. </el-form-item>
  209. <el-form-item label="审核结果">
  210. <el-radio-group v-model="auditForm.auditResult">
  211. <el-radio :value="1">属实/通过</el-radio>
  212. <el-radio :value="2">误报/解除</el-radio>
  213. </el-radio-group>
  214. </el-form-item>
  215. <el-form-item label="审核意见">
  216. <el-input v-model="auditForm.auditOpinion" type="textarea" :rows="3" maxlength="500"
  217. show-word-limit/>
  218. </el-form-item>
  219. <el-form-item label="附件地址">
  220. <el-input v-model="auditForm.attachUrl" placeholder="可填写多个地址,以逗号分隔"/>
  221. </el-form-item>
  222. </el-form>
  223. </div>
  224. <div v-if="mode === 'audit'" class="flow-section">
  225. <div class="section-label">审核记录</div>
  226. <el-timeline>
  227. <el-timeline-item v-for="item in audits" :key="item.auditId || item.id"
  228. :timestamp="item.auditTime || item.createTime"><b>{{ item.auditUserName ||
  229. item.auditUser || '系统用户' }}</b><span> {{ auditResultText(item.auditResult) }}:{{ item.auditOpinion || '-' }}</span>
  230. </el-timeline-item>
  231. <el-timeline-item v-if="!audits.length" timestamp="暂无记录">尚未提交审核</el-timeline-item>
  232. </el-timeline>
  233. <div class="section-label">全流程跟踪</div>
  234. <el-timeline>
  235. <el-timeline-item v-for="item in flow" :key="item.id || item.nodeId || item.time"
  236. :timestamp="item.time || item.createTime">{{ item.name || item.nodeName ||
  237. item.description || '-' }}
  238. </el-timeline-item>
  239. </el-timeline>
  240. </div>
  241. <template #footer>
  242. <el-button @click="detailVisible=false">取消</el-button>
  243. <el-button v-if="mode === 'audit'" type="primary" @click="submitAudit">提交审核</el-button>
  244. </template>
  245. </el-drawer>
  246. <el-dialog v-model="dispatchVisible" title="报警维修派单" width="520px">
  247. <el-form :model="dispatchForm" label-width="100px">
  248. <el-form-item label="工单优先级">
  249. <el-select v-model="dispatchForm.orderLevel">
  250. <el-option label="紧急" :value="0"/>
  251. <el-option label="一般" :value="1"/>
  252. <el-option label="低" :value="2"/>
  253. </el-select>
  254. </el-form-item>
  255. <el-form-item label="问题描述">
  256. <el-input v-model="dispatchForm.orderDesc" type="textarea" :rows="3"/>
  257. </el-form-item>
  258. <el-form-item label="计划完成时间">
  259. <el-date-picker v-model="dispatchForm.planFinishTime" type="datetime"
  260. value-format="YYYY-MM-DD HH:mm:ss"/>
  261. </el-form-item>
  262. </el-form>
  263. <template #footer>
  264. <el-button @click="dispatchVisible=false">取消</el-button>
  265. <el-button type="primary" @click="submitDispatch">提交派单</el-button>
  266. </template>
  267. </el-dialog>
  268. </div>
  269. </template>
  270. <script setup>
  271. import {computed, onMounted, ref} from 'vue'
  272. import {ElMessage} from 'element-plus'
  273. import {Bell, MapLocation, Refresh, Search} from '@element-plus/icons-vue'
  274. import {
  275. auditAlarm,
  276. dispatchAlarm,
  277. getAlarmDetail,
  278. getAlarmFlow,
  279. getAlarmStats,
  280. listAlarmAudits,
  281. listAlarmHistory,
  282. listAlarmMapPoints,
  283. listRealtimeAlarms,
  284. resolveAlarm
  285. } from '@/api/pipeNetwork/waterSupply'
  286. import GisMapCanvas from '@/components/GisMapCanvas.vue'
  287. import WaterGisPageShell from '@/components/WaterGisPageShell.vue'
  288. import { getMonitoringPointColor, getMonitoringPointIcon } from '@/utils/gisMapColors'
  289. import {normalizeMapPoints, normalizePage, unwrapResponse} from '@/utils/waterSupplyModel'
  290. import {getRequestErrorMessage} from '@/utils/waterFacilityQuery'
  291. const props = defineProps({
  292. mode: {type: String, default: 'manage'},
  293. title: {type: String, required: true},
  294. subtitle: {type: String, default: '供水监测报警全流程管理'}
  295. });
  296. const query = ref({pageNum: 1, pageSize: 10, keyword: '', alarmLevel: undefined, alarmStatus: undefined});
  297. const page = ref({records: [], total: 0});
  298. const stats = ref({});
  299. const realtime = ref([]);
  300. const mapPoints = ref([]);
  301. const mapError = ref('')
  302. const alarmSearch = ref('')
  303. const gisMapRef = ref(null)
  304. const loading = ref(false);
  305. const detailVisible = ref(false);
  306. const detail = ref({});
  307. const selectedPoint = computed(() => detail.value || {});
  308. const audits = ref([]);
  309. const flow = ref([]);
  310. const dispatchVisible = ref(false);
  311. // 报警等级 -> 工单优先级:1级→紧急(0),2/3级→一般(1),4级→低(2)
  312. const ALARM_LEVEL_TO_ORDER_LEVEL = { 1: 0, 2: 1, 3: 1, 4: 2 };
  313. const dispatchForm = ref({orderLevel: 1, orderDesc: '', planFinishTime: ''});
  314. const auditForm = ref({auditAction: 1, auditResult: 1, auditOpinion: '', attachUrl: ''})
  315. const pageIcon = computed(() => props.mode === 'map' ? MapLocation : Bell)
  316. const levels = [{label: '一级', value: 1}, {label: '二级', value: 2}, {label: '三级', value: 3}, {
  317. label: '四级',
  318. value: 4
  319. }]
  320. const filteredAlarmPoints = computed(() => {
  321. const keyword = alarmSearch.value.trim().toLocaleLowerCase();
  322. if (!keyword) return mapPoints.value;
  323. return mapPoints.value.filter(point => [
  324. point.deviceName, point.deviceCode, point.name, point.warningType, point.alarmType
  325. ].some(value => String(value || '').toLocaleLowerCase().includes(keyword)));
  326. });
  327. const gisMapPoints = computed(() => filteredAlarmPoints.value.map(point => ({
  328. ...point,
  329. lng: point._longitude ?? point.longitude,
  330. lat: point._latitude ?? point.latitude,
  331. name: point.deviceCode || point.name || '报警点',
  332. color: getMonitoringPointColor(point),
  333. iconUrl: getMonitoringPointIcon(point)
  334. })))
  335. const mapAlarmCount = computed(() => mapPoints.value.length);
  336. const mapCriticalCount = computed(() => mapPoints.value.filter(point => Number(point.alarmLevel) <= 2).length);
  337. const mapResolvedCount = computed(() => mapPoints.value.filter(point => Number(point.alarmStatus) === 1).length);
  338. const mappedCount = computed(() => mapPoints.value.filter(point => Number.isFinite(Number(point.mapX)) && Number.isFinite(Number(point.mapY))).length)
  339. const detailEntries = computed(() => Object.fromEntries(Object.entries(detail.value || {}).filter(([key]) => !['geometry', 'attachUrl'].includes(key)).slice(0, 22)));
  340. const levelText = value => ({1: '一级报警', 2: '二级报警', 3: '三级报警', 4: '四级报警'}[value] || '未知等级');
  341. const levelType = value => ({1: 'danger', 2: 'warning', 3: 'primary', 4: 'success'}[value] || 'info');
  342. const thresholdText = row => row.minValue != null || row.maxValue != null ? `${row.minValue ?? '-'} ~ ${row.maxValue ?? '-'}` : (row.warningThreshold ?? row.threshold ?? '-');
  343. const auditResultText = value => Number(value) === 2 ? '误报/已解除' : '属实/已通过'
  344. async function loadData() {
  345. loading.value = true
  346. try {
  347. if (props.mode === 'manage') {
  348. const [summary, alarms, history] = await Promise.all([getAlarmStats(), listRealtimeAlarms(), listAlarmHistory({...query.value})])
  349. stats.value = unwrapResponse(summary) || {}
  350. realtime.value = Array.isArray(unwrapResponse(alarms)) ? unwrapResponse(alarms) : []
  351. page.value = normalizePage(history)
  352. } else if (props.mode === 'audit') {
  353. page.value = normalizePage(await listAlarmHistory({...query.value, alarmStatus: 0}))
  354. } else {
  355. mapPoints.value = normalizeMapPoints(unwrapResponse(await listAlarmMapPoints()) || [])
  356. }
  357. } catch (error) {
  358. page.value = {records: [], total: 0}
  359. realtime.value = []
  360. mapPoints.value = []
  361. console.warn(`[${props.title}] 数据加载失败`, error)
  362. ElMessage.error(getRequestErrorMessage(error, '数据加载失败,请检查服务状态'))
  363. } finally {
  364. loading.value = false
  365. }
  366. }
  367. const resetQuery = () => {
  368. query.value = {pageNum: 1, pageSize: 10, keyword: '', alarmLevel: undefined, alarmStatus: undefined}
  369. loadData()
  370. }
  371. async function openDetail(row) {
  372. detail.value = row
  373. detailVisible.value = true
  374. const id = row.id || row.alarmId
  375. if (!id) return
  376. try {
  377. const [detailResponse, auditResponse, flowResponse] = await Promise.all([
  378. getAlarmDetail(id),
  379. props.mode === 'audit' ? listAlarmAudits(id) : Promise.resolve(null),
  380. props.mode === 'audit' ? getAlarmFlow(id) : Promise.resolve(null)
  381. ])
  382. detail.value = unwrapResponse(detailResponse) || row
  383. audits.value = unwrapResponse(auditResponse) || []
  384. flow.value = unwrapResponse(flowResponse) || []
  385. } catch (error) {
  386. detail.value = row
  387. }
  388. }
  389. function alarmPointFocus(point) {
  390. return {
  391. lng: point._longitude ?? point.longitude,
  392. lat: point._latitude ?? point.latitude
  393. };
  394. }
  395. function locateAlarmPoint(point) {
  396. gisMapRef.value?.locatePoint(alarmPointFocus(point));
  397. openDetail(point);
  398. }
  399. async function handleResolve(row) {
  400. try {
  401. await resolveAlarm(row.id || row.alarmId, '前端确认处理')
  402. ElMessage.success('报警已标记处理')
  403. loadData()
  404. } catch (error) {
  405. ElMessage.error(getRequestErrorMessage(error, '处理失败,请检查权限或服务状态'))
  406. }
  407. }
  408. function openDispatch(row) {
  409. dispatchForm.value = {
  410. alarmId: row.id || row.alarmId,
  411. orderLevel: ALARM_LEVEL_TO_ORDER_LEVEL[Number(row.alarmLevel)] ?? 1,
  412. orderDesc: `${row.warningType || '监测'}异常,请现场核实处理`,
  413. planFinishTime: ''
  414. }
  415. dispatchVisible.value = true
  416. }
  417. async function submitDispatch() {
  418. try {
  419. await dispatchAlarm(dispatchForm.value)
  420. ElMessage.success('报警派单已提交')
  421. dispatchVisible.value = false
  422. loadData()
  423. } catch (error) {
  424. ElMessage.error(getRequestErrorMessage(error, '派单失败,请检查权限或服务状态'))
  425. }
  426. }
  427. async function submitAudit() {
  428. try {
  429. await auditAlarm({alarmId: detail.value.id || detail.value.alarmId, ...auditForm.value})
  430. ElMessage.success('审核已提交')
  431. detailVisible.value = false
  432. loadData()
  433. } catch (error) {
  434. ElMessage.error(getRequestErrorMessage(error, '审核提交失败,请检查权限或服务状态'))
  435. }
  436. }
  437. onMounted(() => {
  438. loadData()
  439. })
  440. </script>
  441. <style scoped>
  442. .water-page {
  443. padding: 20px;
  444. background: #f5f7fa;
  445. min-height: calc(100vh - 84px)
  446. }
  447. .page-heading {
  448. display: flex;
  449. justify-content: space-between;
  450. align-items: center;
  451. margin-bottom: 16px
  452. }
  453. .page-heading h2 {
  454. margin: 0 0 5px;
  455. font-size: 20px
  456. }
  457. .muted {
  458. font-size: 13px;
  459. color: #909399
  460. }
  461. .filter-card, .realtime-card {
  462. margin-bottom: 16px
  463. }
  464. .filter-card :deep(.el-form-item) {
  465. margin-top: 8px;
  466. margin-right: 16px;
  467. margin-bottom: 8px
  468. }
  469. .metric-grid {
  470. display: grid;
  471. grid-template-columns:repeat(4, minmax(0, 1fr));
  472. gap: 12px;
  473. margin-bottom: 16px
  474. }
  475. .metric {
  476. background: #fff;
  477. padding: 15px 18px;
  478. border: 1px solid #ebeef5
  479. }
  480. .metric span {
  481. display: block;
  482. color: #909399;
  483. font-size: 13px
  484. }
  485. .metric strong {
  486. font-size: 24px;
  487. line-height: 34px
  488. }
  489. .metric small {
  490. margin-left: 5px;
  491. color: #909399
  492. }
  493. .danger {
  494. color: #e45656
  495. }
  496. .card-title {
  497. display: flex;
  498. justify-content: space-between;
  499. align-items: center;
  500. font-weight: 600;
  501. margin-bottom: 14px
  502. }
  503. .realtime-grid {
  504. display: grid;
  505. grid-template-columns:repeat(3, minmax(0, 1fr));
  506. gap: 10px
  507. }
  508. .realtime-item {
  509. border: 1px solid #edf0f5;
  510. background: #fff;
  511. text-align: left;
  512. padding: 12px;
  513. cursor: pointer
  514. }
  515. .realtime-item:hover {
  516. border-color: #8ab8e8;
  517. background: #f8fbff
  518. }
  519. .realtime-head {
  520. display: flex;
  521. align-items: center;
  522. gap: 7px
  523. }
  524. .realtime-head b {
  525. flex: 1;
  526. overflow: hidden;
  527. text-overflow: ellipsis;
  528. white-space: nowrap;
  529. color: #303133;
  530. font-size: 13px
  531. }
  532. .realtime-head span {
  533. color: #909399;
  534. font-size: 11px
  535. }
  536. .realtime-body {
  537. display: grid;
  538. grid-template-columns:1.3fr .7fr;
  539. gap: 4px;
  540. margin-top: 10px;
  541. color: #606266;
  542. font-size: 12px
  543. }
  544. .realtime-body strong {
  545. font-size: 20px;
  546. color: #303133;
  547. text-align: right
  548. }
  549. .realtime-body small {
  550. grid-column: 1/-1;
  551. color: #909399
  552. }
  553. .empty-inline {
  554. color: #909399;
  555. font-size: 13px;
  556. padding: 10px 0
  557. }
  558. .table-card :deep(.el-pagination) {
  559. justify-content: flex-end;
  560. margin-top: 16px
  561. }
  562. .range-sep {
  563. margin: 0 8px;
  564. color: #909399
  565. }
  566. .map-card {
  567. min-height: 560px
  568. }
  569. .map-card .search-input {
  570. width: 220px
  571. }
  572. .map-card .panel-section h3 {
  573. margin: 0 0 8px;
  574. font-size: 14px;
  575. color: #29567e
  576. }
  577. .map-card .alarm-detail {
  578. margin-top: 0;
  579. padding-top: 0;
  580. border-top: 0
  581. }
  582. .map-card .device-result-list {
  583. display: flex;
  584. flex-direction: column;
  585. gap: 6px
  586. }
  587. .map-card .device-result-list button {
  588. width: 100%;
  589. display: grid;
  590. grid-template-columns: minmax(0, 1fr) auto;
  591. gap: 3px 8px;
  592. align-items: center;
  593. border: 1px solid #dbe8f5;
  594. background: #fff;
  595. border-radius: 8px;
  596. padding: 8px 10px;
  597. text-align: left;
  598. cursor: pointer
  599. }
  600. .map-card .device-name {
  601. overflow: hidden;
  602. text-overflow: ellipsis;
  603. white-space: nowrap;
  604. font-weight: 600;
  605. color: #274b6d
  606. }
  607. .map-card .device-meta {
  608. grid-column: 1 / -1;
  609. font-size: 12px;
  610. color: #7793aa
  611. }
  612. .map-card .map-error {
  613. position: absolute;
  614. left: 16px;
  615. bottom: 16px;
  616. width: 360px;
  617. z-index: 1
  618. }
  619. .map-card .map-empty {
  620. position: absolute;
  621. left: 50%;
  622. top: 50%;
  623. transform: translate(-50%, -50%);
  624. z-index: 1
  625. }
  626. .alarm-map {
  627. position: relative;
  628. height: 500px;
  629. background: linear-gradient(135deg, #edf5ff, #f8fbff);
  630. border: 1px solid #dfe9f7;
  631. overflow: hidden
  632. }
  633. .alarm-point {
  634. position: absolute;
  635. transform: translate(-50%, -50%);
  636. cursor: pointer;
  637. text-align: center
  638. }
  639. .alarm-point span {
  640. display: block;
  641. width: 18px;
  642. height: 18px;
  643. border-radius: 50%;
  644. border: 3px solid #fff;
  645. box-shadow: 0 0 0 5px currentColor;
  646. background: currentColor
  647. }
  648. .alarm-point b {
  649. font-size: 11px;
  650. color: #334155;
  651. white-space: nowrap
  652. }
  653. .level-1 {
  654. color: #f56c6c
  655. }
  656. .level-2 {
  657. color: #e6a23c
  658. }
  659. .level-3 {
  660. color: #409eff
  661. }
  662. .level-4 {
  663. color: #67c23a
  664. }
  665. .audit-form, .flow-section {
  666. border-top: 1px solid #ebeef5;
  667. margin-top: 18px;
  668. padding-top: 15px
  669. }
  670. .section-label {
  671. font-weight: 600;
  672. color: #303133;
  673. margin: 8px 0 12px
  674. }
  675. .flow-section :deep(.el-timeline) {
  676. padding-left: 5px
  677. }
  678. @media (max-width: 1000px) {
  679. .realtime-grid {
  680. grid-template-columns:repeat(2, minmax(0, 1fr))
  681. }
  682. .metric-grid {
  683. grid-template-columns:repeat(2, minmax(0, 1fr))
  684. }
  685. }
  686. @media (max-width: 650px) {
  687. .realtime-grid {
  688. grid-template-columns:1fr
  689. }
  690. .page-heading {
  691. align-items: flex-start;
  692. gap: 12px;
  693. flex-direction: column
  694. }
  695. }
  696. </style>
  697. <style lang="scss">
  698. @use './waterSupplyPageTheme.scss';
  699. .alarm-page .map-workspace .alarm-map {
  700. height: auto;
  701. min-height: 560px;
  702. border: 0;
  703. background-color: #f8fbff;
  704. background-image: linear-gradient(#eaf0f6 1px, transparent 1px), linear-gradient(90deg, #eaf0f6 1px, transparent 1px);
  705. background-size: 40px 40px;
  706. }
  707. .alarm-page .alarm-map {
  708. min-height: 560px;
  709. height: auto;
  710. border: 0;
  711. background-color: #f8fbff;
  712. background-image: linear-gradient(#eaf0f6 1px, transparent 1px), linear-gradient(90deg, #eaf0f6 1px, transparent 1px);
  713. background-size: 40px 40px;
  714. }
  715. .alarm-page .status-dot-normal {
  716. background: #2ecc71;
  717. }
  718. .alarm-page .status-dot-alarm {
  719. background: #e74c3c;
  720. }
  721. .alarm-page .status-dot-warning {
  722. background: #f1c40f;
  723. }
  724. .alarm-page .status-dot-work-order {
  725. background: #3498db;
  726. }
  727. .alarm-page .status-dot-offline {
  728. background: #95a5a6;
  729. }
  730. .alarm-page .alarm-detail {
  731. margin-top: 24px;
  732. padding-top: 16px;
  733. border-top: 1px solid #e1ebf3;
  734. }
  735. .alarm-page .detail-name {
  736. margin-bottom: 10px;
  737. color: #17324d;
  738. font-weight: 700;
  739. }
  740. .alarm-page .detail-action {
  741. margin-top: 10px;
  742. }
  743. .alarm-detail-drawer :deep(.el-drawer__body) {
  744. overflow: auto
  745. }
  746. .alarm-detail-drawer :deep(.el-descriptions__label) {
  747. white-space: nowrap
  748. }
  749. .alarm-detail-drawer :deep(.el-descriptions__content) {
  750. word-break: break-word;
  751. overflow-wrap: anywhere
  752. }
  753. .alarm-detail-drawer :deep(.el-select) {
  754. width: 100%
  755. }
  756. @media (max-width: 768px) {
  757. .alarm-detail-drawer {
  758. width: 100% !important
  759. }
  760. }
  761. </style>