| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841 |
- <template>
- <div class="water-page alarm-page">
- <div class="page-heading">
- <div class="page-title"><span class="page-title-icon"><el-icon><component :is="pageIcon"/></el-icon></span>
- <div><h2>{{ title }}</h2><span class="muted">{{ subtitle }}</span></div>
- </div>
- <el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
- </div>
- <div v-if="mode === 'manage'" class="metric-grid">
- <div class="metric"><span>实时报警</span><strong class="danger">{{ stats.realtimeAlarmCount ??
- stats.realtimeCount ?? 0 }}</strong><small>条</small></div>
- <div class="metric"><span>今日报警</span><strong>{{ stats.todayAlarmCount ?? stats.todayCount ?? 0
- }}</strong><small>条</small></div>
- <div class="metric"><span>设备异常率</span><strong>{{ stats.deviceAbnormalRate ?? stats.abnormalRate ?? '-'
- }}</strong><small>%</small></div>
- <div class="metric"><span>平均响应时间</span><strong>{{ stats.avgResponseMinutes ??
- stats.averageResponseMinutes ?? '-' }}</strong><small>分钟</small></div>
- </div>
- <el-card v-if="mode === 'manage'" shadow="never" class="realtime-card">
- <div class="card-title"><span>实时异常卡片</span><span class="muted">设备、位置与阈值</span></div>
- <div v-if="!realtime.length" class="empty-inline">当前无未处理报警</div>
- <div v-else class="realtime-grid">
- <button v-for="item in realtime" :key="item.id || item.alarmId" class="realtime-item"
- @click="openDetail(item)">
- <div class="realtime-head">
- <el-tag :type="levelType(item.alarmLevel)">{{ item.alarmLevelText || levelText(item.alarmLevel)
- }}
- </el-tag>
- <b>{{ item.deviceName || item.deviceCode || '监测设备'
- }}</b><span>{{ item.alarmTime || '-' }}</span></div>
- <div class="realtime-body"><span>{{ item.equipmentLocation || '位置暂无' }}</span><strong>{{
- item.actualValue ?? '-' }}</strong><small>阈值 {{ thresholdText(item) }}</small></div>
- </button>
- </div>
- </el-card>
- <el-card shadow="never" class="filter-card">
- <el-form :inline="true" :model="query">
- <el-form-item label="关键字">
- <el-input v-model="query.keyword" clearable placeholder="设备编码/报警类型"/>
- </el-form-item>
- <el-form-item label="报警等级">
- <el-select v-model="query.alarmLevel" clearable placeholder="全部">
- <el-option v-for="item in levels" :key="item.value" v-bind="item"/>
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-select v-model="query.alarmStatus" clearable placeholder="全部">
- <el-option label="未处理" :value="0"/>
- <el-option label="已处理" :value="1"/>
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="loadData">查询</el-button>
- <el-button @click="resetQuery">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- <el-card v-if="mode !== 'map'" shadow="never" class="table-card">
- <div class="card-title"><span>{{ mode === 'audit' ? '待审核报警清单' : '报警历史清单' }}</span></div>
- <el-table :data="page.records" border stripe v-loading="loading">
- <el-table-column type="index" label="#" width="55"/>
- <el-table-column prop="alarmCode" label="报警编码" min-width="145"/>
- <el-table-column prop="deviceCode" label="设备编码" min-width="125"/>
- <el-table-column prop="warningType" label="报警类型" min-width="130"/>
- <el-table-column prop="alarmLevel" label="报警等级" width="100">
- <template #default="{row}">
- <el-tag :type="levelType(row.alarmLevel)">{{ row.alarmLevelText || levelText(row.alarmLevel)
- }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="alarmTime" label="触发时间" min-width="165"/>
- <el-table-column prop="alarmStatus" label="状态" width="95">
- <template #default="{row}">
- <el-tag :type="Number(row.alarmStatus) === 1 ? 'success' : 'danger'">{{ Number(row.alarmStatus)
- === 1 ? '已处理' : '未处理' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="220" fixed="right">
- <template #default="{row}">
- <el-button link type="primary" @click="openDetail(row)">{{ mode === 'audit' ? '审核' : '详情'
- }}
- </el-button>
- <el-button v-if="mode === 'manage' && Number(row.alarmStatus) !== 1" link type="success"
- @click="handleResolve(row)">标记处理
- </el-button>
- <el-button v-if="mode === 'manage' && Number(row.alarmStatus) !== 1" link type="warning"
- @click="openDispatch(row)">派单
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="page.total"
- layout="total, sizes, prev, pager, next" :page-sizes="[10,20,50]" @change="loadData"/>
- </el-card>
- <el-card v-else shadow="never" class="water-panel map-card">
- <WaterGisPageShell
- embedded
- eyebrow="WATER SUPPLY ALARM GIS"
- title="监测报警一张图"
- subtitle="按监测点状态着色"
- :loading="loading"
- >
- <template #actions>
- <el-input v-model="alarmSearch" class="search-input" placeholder="搜索设备/报警类型" clearable
- :prefix-icon="Search"/>
- <el-button :icon="Refresh" @click="loadData">刷新</el-button>
- </template>
- <template #metrics>
- <div class="metric">
- <el-icon class="metric-icon"><Bell/></el-icon>
- <span>报警总数</span><strong>{{ mapAlarmCount }}</strong><small>条</small>
- </div>
- <div class="metric">
- <el-icon class="metric-icon"><Warning/></el-icon>
- <span>高等级报警</span><strong class="danger">{{ mapCriticalCount }}</strong><small>条</small>
- </div>
- <div class="metric">
- <el-icon class="metric-icon"><CircleCheck/></el-icon>
- <span>已处理报警</span><strong>{{ mapResolvedCount }}</strong><small>条</small>
- </div>
- <div class="metric">
- <el-icon class="metric-icon"><Location/></el-icon>
- <span>地图点位</span><strong>{{ mappedCount }}</strong><small>个</small>
- </div>
- </template>
- <template #controls>
- <div class="panel-section alarm-detail">
- <h3>选中报警</h3>
- <template v-if="selectedPoint.id || selectedPoint.alarmId">
- <div class="detail-name">{{ selectedPoint.deviceCode || selectedPoint.name || '报警点' }}</div>
- <el-descriptions :column="1" border size="small">
- <el-descriptions-item label="报警等级">
- <el-tag :type="levelType(selectedPoint.alarmLevel)">{{ selectedPoint.alarmLevelText || levelText(selectedPoint.alarmLevel) }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="报警类型">{{ selectedPoint.warningType || selectedPoint.alarmType || '-' }}</el-descriptions-item>
- <el-descriptions-item label="触发时间">{{ selectedPoint.alarmTime || selectedPoint.createTime || '-' }}</el-descriptions-item>
- <el-descriptions-item label="状态">
- <el-tag :type="Number(selectedPoint.alarmStatus) === 1 ? 'success' : 'danger'">
- {{ Number(selectedPoint.alarmStatus) === 1 ? '已处理' : '未处理' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="位置">{{ selectedPoint.equipmentLocation || selectedPoint.location || '-' }}</el-descriptions-item>
- </el-descriptions>
- <el-button class="detail-action" type="primary" link @click="openDetail(selectedPoint)">查看完整详情</el-button>
- </template>
- <el-empty v-else description="点击点位查看详情" :image-size="65"/>
- </div>
- </template>
- <template #legend>
- <div class="panel-section">
- <h3>报警图层与图例</h3>
- <div class="map-legend">
- <div class="map-legend-item"><i class="map-legend-dot status-dot-normal"/>正常</div>
- <div class="map-legend-item"><i class="map-legend-dot status-dot-alarm"/>报警</div>
- <div class="map-legend-item"><i class="map-legend-dot status-dot-warning"/>预警</div>
- <div class="map-legend-item"><i class="map-legend-dot status-dot-work-order"/>关联工单</div>
- <div class="map-legend-item"><i class="map-legend-dot status-dot-offline"/>离线</div>
- </div>
- </div>
- </template>
- <template #results>
- <div class="panel-section result-section">
- <h3>搜索报警</h3>
- <el-alert v-if="mapError" type="warning" show-icon :closable="false" :title="mapError"/>
- <el-empty v-else-if="!filteredAlarmPoints.length" description="暂无报警点位" :image-size="72"/>
- <div v-else class="device-result-list">
- <button v-for="point in filteredAlarmPoints" :key="point.id || point.alarmId || point.deviceCode"
- type="button" @click="locateAlarmPoint(point)">
- <span class="device-name">{{ point.deviceName || point.deviceCode || point.name || '报警点' }}</span>
- <span class="device-meta">{{ point.warningType || point.alarmType || '监测报警' }} · {{ point.alarmTime || point.createTime || '-' }}</span>
- <el-tag size="small" :type="levelType(point.alarmLevel)">
- {{ point.alarmLevelText || levelText(point.alarmLevel) }}
- </el-tag>
- </button>
- </div>
- </div>
- </template>
- <template #map>
- <GisMapCanvas
- ref="gisMapRef"
- :points="gisMapPoints"
- :center="{ lng: 110.3965, lat: 28.4638 }"
- :zoom="15"
- @point-click="openDetail"
- @map-click="detailVisible = false"
- @error="mapError = $event"
- />
- <el-alert v-if="mapError" class="map-error" type="warning" show-icon :closable="false" :title="mapError"/>
- <el-empty v-if="!gisMapPoints.length && !mapError" class="map-empty" description="当前筛选下没有可显示的报警点位"/>
- </template>
- </WaterGisPageShell>
- </el-card>
- <el-drawer v-model="detailVisible" class="detail-drawer alarm-detail-drawer"
- :title="mode === 'audit' ? '报警审核' : '报警详情'" direction="rtl" size="720px" append-to-body>
- <el-descriptions :column="2" border>
- <el-descriptions-item v-for="(value,key) in detailEntries" :key="key" :label="key">{{ value ?? '-' }}
- </el-descriptions-item>
- </el-descriptions>
- <div v-if="mode === 'audit'" class="audit-form">
- <el-form :model="auditForm" label-width="90px">
- <el-form-item label="审核动作">
- <el-radio-group v-model="auditForm.auditAction">
- <el-radio :value="1">审核</el-radio>
- <el-radio :value="2">解除</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="审核结果">
- <el-radio-group v-model="auditForm.auditResult">
- <el-radio :value="1">属实/通过</el-radio>
- <el-radio :value="2">误报/解除</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="审核意见">
- <el-input v-model="auditForm.auditOpinion" type="textarea" :rows="3" maxlength="500"
- show-word-limit/>
- </el-form-item>
- <el-form-item label="附件地址">
- <el-input v-model="auditForm.attachUrl" placeholder="可填写多个地址,以逗号分隔"/>
- </el-form-item>
- </el-form>
- </div>
- <div v-if="mode === 'audit'" class="flow-section">
- <div class="section-label">审核记录</div>
- <el-timeline>
- <el-timeline-item v-for="item in audits" :key="item.auditId || item.id"
- :timestamp="item.auditTime || item.createTime"><b>{{ item.auditUserName ||
- item.auditUser || '系统用户' }}</b><span> {{ auditResultText(item.auditResult) }}:{{ item.auditOpinion || '-' }}</span>
- </el-timeline-item>
- <el-timeline-item v-if="!audits.length" timestamp="暂无记录">尚未提交审核</el-timeline-item>
- </el-timeline>
- <div class="section-label">全流程跟踪</div>
- <el-timeline>
- <el-timeline-item v-for="item in flow" :key="item.id || item.nodeId || item.time"
- :timestamp="item.time || item.createTime">{{ item.name || item.nodeName ||
- item.description || '-' }}
- </el-timeline-item>
- </el-timeline>
- </div>
- <template #footer>
- <el-button @click="detailVisible=false">取消</el-button>
- <el-button v-if="mode === 'audit'" type="primary" @click="submitAudit">提交审核</el-button>
- </template>
- </el-drawer>
- <el-dialog v-model="dispatchVisible" title="报警维修派单" width="520px">
- <el-form :model="dispatchForm" label-width="100px">
- <el-form-item label="工单优先级">
- <el-select v-model="dispatchForm.orderLevel">
- <el-option label="紧急" :value="0"/>
- <el-option label="一般" :value="1"/>
- <el-option label="低" :value="2"/>
- </el-select>
- </el-form-item>
- <el-form-item label="问题描述">
- <el-input v-model="dispatchForm.orderDesc" type="textarea" :rows="3"/>
- </el-form-item>
- <el-form-item label="计划完成时间">
- <el-date-picker v-model="dispatchForm.planFinishTime" type="datetime"
- value-format="YYYY-MM-DD HH:mm:ss"/>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="dispatchVisible=false">取消</el-button>
- <el-button type="primary" @click="submitDispatch">提交派单</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import {computed, onMounted, ref} from 'vue'
- import {ElMessage} from 'element-plus'
- import {Bell, MapLocation, Refresh, Search} from '@element-plus/icons-vue'
- import {
- auditAlarm,
- dispatchAlarm,
- getAlarmDetail,
- getAlarmFlow,
- getAlarmStats,
- listAlarmAudits,
- listAlarmHistory,
- listAlarmMapPoints,
- listRealtimeAlarms,
- resolveAlarm
- } from '@/api/pipeNetwork/waterSupply'
- import GisMapCanvas from '@/components/GisMapCanvas.vue'
- import WaterGisPageShell from '@/components/WaterGisPageShell.vue'
- import { getMonitoringPointColor, getMonitoringPointIcon } from '@/utils/gisMapColors'
- import {normalizeMapPoints, normalizePage, unwrapResponse} from '@/utils/waterSupplyModel'
- import {getRequestErrorMessage} from '@/utils/waterFacilityQuery'
- const props = defineProps({
- mode: {type: String, default: 'manage'},
- title: {type: String, required: true},
- subtitle: {type: String, default: '供水监测报警全流程管理'}
- });
- const query = ref({pageNum: 1, pageSize: 10, keyword: '', alarmLevel: undefined, alarmStatus: undefined});
- const page = ref({records: [], total: 0});
- const stats = ref({});
- const realtime = ref([]);
- const mapPoints = ref([]);
- const mapError = ref('')
- const alarmSearch = ref('')
- const gisMapRef = ref(null)
- const loading = ref(false);
- const detailVisible = ref(false);
- const detail = ref({});
- const selectedPoint = computed(() => detail.value || {});
- const audits = ref([]);
- const flow = ref([]);
- const dispatchVisible = ref(false);
- // 报警等级 -> 工单优先级:1级→紧急(0),2/3级→一般(1),4级→低(2)
- const ALARM_LEVEL_TO_ORDER_LEVEL = { 1: 0, 2: 1, 3: 1, 4: 2 };
- const dispatchForm = ref({orderLevel: 1, orderDesc: '', planFinishTime: ''});
- const auditForm = ref({auditAction: 1, auditResult: 1, auditOpinion: '', attachUrl: ''})
- const pageIcon = computed(() => props.mode === 'map' ? MapLocation : Bell)
- const levels = [{label: '一级', value: 1}, {label: '二级', value: 2}, {label: '三级', value: 3}, {
- label: '四级',
- value: 4
- }]
- const filteredAlarmPoints = computed(() => {
- const keyword = alarmSearch.value.trim().toLocaleLowerCase();
- if (!keyword) return mapPoints.value;
- return mapPoints.value.filter(point => [
- point.deviceName, point.deviceCode, point.name, point.warningType, point.alarmType
- ].some(value => String(value || '').toLocaleLowerCase().includes(keyword)));
- });
- const gisMapPoints = computed(() => filteredAlarmPoints.value.map(point => ({
- ...point,
- lng: point._longitude ?? point.longitude,
- lat: point._latitude ?? point.latitude,
- name: point.deviceCode || point.name || '报警点',
- color: getMonitoringPointColor(point),
- iconUrl: getMonitoringPointIcon(point)
- })))
- const mapAlarmCount = computed(() => mapPoints.value.length);
- const mapCriticalCount = computed(() => mapPoints.value.filter(point => Number(point.alarmLevel) <= 2).length);
- const mapResolvedCount = computed(() => mapPoints.value.filter(point => Number(point.alarmStatus) === 1).length);
- const mappedCount = computed(() => mapPoints.value.filter(point => Number.isFinite(Number(point.mapX)) && Number.isFinite(Number(point.mapY))).length)
- const detailEntries = computed(() => Object.fromEntries(Object.entries(detail.value || {}).filter(([key]) => !['geometry', 'attachUrl'].includes(key)).slice(0, 22)));
- const levelText = value => ({1: '一级报警', 2: '二级报警', 3: '三级报警', 4: '四级报警'}[value] || '未知等级');
- const levelType = value => ({1: 'danger', 2: 'warning', 3: 'primary', 4: 'success'}[value] || 'info');
- const thresholdText = row => row.minValue != null || row.maxValue != null ? `${row.minValue ?? '-'} ~ ${row.maxValue ?? '-'}` : (row.warningThreshold ?? row.threshold ?? '-');
- const auditResultText = value => Number(value) === 2 ? '误报/已解除' : '属实/已通过'
- async function loadData() {
- loading.value = true
- try {
- if (props.mode === 'manage') {
- const [summary, alarms, history] = await Promise.all([getAlarmStats(), listRealtimeAlarms(), listAlarmHistory({...query.value})])
- stats.value = unwrapResponse(summary) || {}
- realtime.value = Array.isArray(unwrapResponse(alarms)) ? unwrapResponse(alarms) : []
- page.value = normalizePage(history)
- } else if (props.mode === 'audit') {
- page.value = normalizePage(await listAlarmHistory({...query.value, alarmStatus: 0}))
- } else {
- mapPoints.value = normalizeMapPoints(unwrapResponse(await listAlarmMapPoints()) || [])
- }
- } catch (error) {
- page.value = {records: [], total: 0}
- realtime.value = []
- mapPoints.value = []
- console.warn(`[${props.title}] 数据加载失败`, error)
- ElMessage.error(getRequestErrorMessage(error, '数据加载失败,请检查服务状态'))
- } finally {
- loading.value = false
- }
- }
- const resetQuery = () => {
- query.value = {pageNum: 1, pageSize: 10, keyword: '', alarmLevel: undefined, alarmStatus: undefined}
- loadData()
- }
- async function openDetail(row) {
- detail.value = row
- detailVisible.value = true
- const id = row.id || row.alarmId
- if (!id) return
- try {
- const [detailResponse, auditResponse, flowResponse] = await Promise.all([
- getAlarmDetail(id),
- props.mode === 'audit' ? listAlarmAudits(id) : Promise.resolve(null),
- props.mode === 'audit' ? getAlarmFlow(id) : Promise.resolve(null)
- ])
- detail.value = unwrapResponse(detailResponse) || row
- audits.value = unwrapResponse(auditResponse) || []
- flow.value = unwrapResponse(flowResponse) || []
- } catch (error) {
- detail.value = row
- }
- }
- function alarmPointFocus(point) {
- return {
- lng: point._longitude ?? point.longitude,
- lat: point._latitude ?? point.latitude
- };
- }
- function locateAlarmPoint(point) {
- gisMapRef.value?.locatePoint(alarmPointFocus(point));
- openDetail(point);
- }
- async function handleResolve(row) {
- try {
- await resolveAlarm(row.id || row.alarmId, '前端确认处理')
- ElMessage.success('报警已标记处理')
- loadData()
- } catch (error) {
- ElMessage.error(getRequestErrorMessage(error, '处理失败,请检查权限或服务状态'))
- }
- }
- function openDispatch(row) {
- dispatchForm.value = {
- alarmId: row.id || row.alarmId,
- orderLevel: ALARM_LEVEL_TO_ORDER_LEVEL[Number(row.alarmLevel)] ?? 1,
- orderDesc: `${row.warningType || '监测'}异常,请现场核实处理`,
- planFinishTime: ''
- }
- dispatchVisible.value = true
- }
- async function submitDispatch() {
- try {
- await dispatchAlarm(dispatchForm.value)
- ElMessage.success('报警派单已提交')
- dispatchVisible.value = false
- loadData()
- } catch (error) {
- ElMessage.error(getRequestErrorMessage(error, '派单失败,请检查权限或服务状态'))
- }
- }
- async function submitAudit() {
- try {
- await auditAlarm({alarmId: detail.value.id || detail.value.alarmId, ...auditForm.value})
- ElMessage.success('审核已提交')
- detailVisible.value = false
- loadData()
- } catch (error) {
- ElMessage.error(getRequestErrorMessage(error, '审核提交失败,请检查权限或服务状态'))
- }
- }
- onMounted(() => {
- loadData()
- })
- </script>
- <style scoped>
- .water-page {
- padding: 20px;
- background: #f5f7fa;
- min-height: calc(100vh - 84px)
- }
- .page-heading {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px
- }
- .page-heading h2 {
- margin: 0 0 5px;
- font-size: 20px
- }
- .muted {
- font-size: 13px;
- color: #909399
- }
- .filter-card, .realtime-card {
- margin-bottom: 16px
- }
- .filter-card :deep(.el-form-item) {
- margin-top: 8px;
- margin-right: 16px;
- margin-bottom: 8px
- }
- .metric-grid {
- display: grid;
- grid-template-columns:repeat(4, minmax(0, 1fr));
- gap: 12px;
- margin-bottom: 16px
- }
- .metric {
- background: #fff;
- padding: 15px 18px;
- border: 1px solid #ebeef5
- }
- .metric span {
- display: block;
- color: #909399;
- font-size: 13px
- }
- .metric strong {
- font-size: 24px;
- line-height: 34px
- }
- .metric small {
- margin-left: 5px;
- color: #909399
- }
- .danger {
- color: #e45656
- }
- .card-title {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-weight: 600;
- margin-bottom: 14px
- }
- .realtime-grid {
- display: grid;
- grid-template-columns:repeat(3, minmax(0, 1fr));
- gap: 10px
- }
- .realtime-item {
- border: 1px solid #edf0f5;
- background: #fff;
- text-align: left;
- padding: 12px;
- cursor: pointer
- }
- .realtime-item:hover {
- border-color: #8ab8e8;
- background: #f8fbff
- }
- .realtime-head {
- display: flex;
- align-items: center;
- gap: 7px
- }
- .realtime-head b {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #303133;
- font-size: 13px
- }
- .realtime-head span {
- color: #909399;
- font-size: 11px
- }
- .realtime-body {
- display: grid;
- grid-template-columns:1.3fr .7fr;
- gap: 4px;
- margin-top: 10px;
- color: #606266;
- font-size: 12px
- }
- .realtime-body strong {
- font-size: 20px;
- color: #303133;
- text-align: right
- }
- .realtime-body small {
- grid-column: 1/-1;
- color: #909399
- }
- .empty-inline {
- color: #909399;
- font-size: 13px;
- padding: 10px 0
- }
- .table-card :deep(.el-pagination) {
- justify-content: flex-end;
- margin-top: 16px
- }
- .range-sep {
- margin: 0 8px;
- color: #909399
- }
- .map-card {
- min-height: 560px
- }
- .map-card .search-input {
- width: 220px
- }
- .map-card .panel-section h3 {
- margin: 0 0 8px;
- font-size: 14px;
- color: #29567e
- }
- .map-card .alarm-detail {
- margin-top: 0;
- padding-top: 0;
- border-top: 0
- }
- .map-card .device-result-list {
- display: flex;
- flex-direction: column;
- gap: 6px
- }
- .map-card .device-result-list button {
- width: 100%;
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 3px 8px;
- align-items: center;
- border: 1px solid #dbe8f5;
- background: #fff;
- border-radius: 8px;
- padding: 8px 10px;
- text-align: left;
- cursor: pointer
- }
- .map-card .device-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-weight: 600;
- color: #274b6d
- }
- .map-card .device-meta {
- grid-column: 1 / -1;
- font-size: 12px;
- color: #7793aa
- }
- .map-card .map-error {
- position: absolute;
- left: 16px;
- bottom: 16px;
- width: 360px;
- z-index: 1
- }
- .map-card .map-empty {
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- z-index: 1
- }
- .alarm-map {
- position: relative;
- height: 500px;
- background: linear-gradient(135deg, #edf5ff, #f8fbff);
- border: 1px solid #dfe9f7;
- overflow: hidden
- }
- .alarm-point {
- position: absolute;
- transform: translate(-50%, -50%);
- cursor: pointer;
- text-align: center
- }
- .alarm-point span {
- display: block;
- width: 18px;
- height: 18px;
- border-radius: 50%;
- border: 3px solid #fff;
- box-shadow: 0 0 0 5px currentColor;
- background: currentColor
- }
- .alarm-point b {
- font-size: 11px;
- color: #334155;
- white-space: nowrap
- }
- .level-1 {
- color: #f56c6c
- }
- .level-2 {
- color: #e6a23c
- }
- .level-3 {
- color: #409eff
- }
- .level-4 {
- color: #67c23a
- }
- .audit-form, .flow-section {
- border-top: 1px solid #ebeef5;
- margin-top: 18px;
- padding-top: 15px
- }
- .section-label {
- font-weight: 600;
- color: #303133;
- margin: 8px 0 12px
- }
- .flow-section :deep(.el-timeline) {
- padding-left: 5px
- }
- @media (max-width: 1000px) {
- .realtime-grid {
- grid-template-columns:repeat(2, minmax(0, 1fr))
- }
- .metric-grid {
- grid-template-columns:repeat(2, minmax(0, 1fr))
- }
- }
- @media (max-width: 650px) {
- .realtime-grid {
- grid-template-columns:1fr
- }
- .page-heading {
- align-items: flex-start;
- gap: 12px;
- flex-direction: column
- }
- }
- </style>
- <style lang="scss">
- @use './waterSupplyPageTheme.scss';
- .alarm-page .map-workspace .alarm-map {
- height: auto;
- min-height: 560px;
- border: 0;
- background-color: #f8fbff;
- background-image: linear-gradient(#eaf0f6 1px, transparent 1px), linear-gradient(90deg, #eaf0f6 1px, transparent 1px);
- background-size: 40px 40px;
- }
- .alarm-page .alarm-map {
- min-height: 560px;
- height: auto;
- border: 0;
- background-color: #f8fbff;
- background-image: linear-gradient(#eaf0f6 1px, transparent 1px), linear-gradient(90deg, #eaf0f6 1px, transparent 1px);
- background-size: 40px 40px;
- }
- .alarm-page .status-dot-normal {
- background: #2ecc71;
- }
- .alarm-page .status-dot-alarm {
- background: #e74c3c;
- }
- .alarm-page .status-dot-warning {
- background: #f1c40f;
- }
- .alarm-page .status-dot-work-order {
- background: #3498db;
- }
- .alarm-page .status-dot-offline {
- background: #95a5a6;
- }
- .alarm-page .alarm-detail {
- margin-top: 24px;
- padding-top: 16px;
- border-top: 1px solid #e1ebf3;
- }
- .alarm-page .detail-name {
- margin-bottom: 10px;
- color: #17324d;
- font-weight: 700;
- }
- .alarm-page .detail-action {
- margin-top: 10px;
- }
- .alarm-detail-drawer :deep(.el-drawer__body) {
- overflow: auto
- }
- .alarm-detail-drawer :deep(.el-descriptions__label) {
- white-space: nowrap
- }
- .alarm-detail-drawer :deep(.el-descriptions__content) {
- word-break: break-word;
- overflow-wrap: anywhere
- }
- .alarm-detail-drawer :deep(.el-select) {
- width: 100%
- }
- @media (max-width: 768px) {
- .alarm-detail-drawer {
- width: 100% !important
- }
- }
- </style>
|