| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- <!--案件流转管理:以报警(alarm_data)为源头,关联工单(work_order)完成核查/派单/办结闭环-->
- <template>
- <div class="page">
- <!-- 筛选 -->
- <div class="panel filter">
- <div class="fl">
- <label>关键字</label>
- <el-input v-model="keyword" placeholder="报警单号 / 设备编号" clearable style="width:210px" @keyup.enter="loadAlarms" />
- </div>
- <div class="fl">
- <label>报警等级</label>
- <el-select v-model="levelFilter" placeholder="全部" clearable style="width:120px" @change="loadAlarms">
- <el-option v-for="lv in alarmLevels" :key="lv.value" :label="lv.label" :value="lv.value" />
- </el-select>
- </div>
- <div class="fl">
- <label>报警时间</label>
- <el-date-picker
- v-model="timeRange"
- type="datetimerange"
- value-format="YYYY-MM-DD HH:mm:ss"
- range-separator="至"
- start-placeholder="开始时间"
- end-placeholder="结束时间"
- style="width:320px"
- />
- </div>
- <button class="btn-pri" @click="loadAlarms">查询</button>
- <button class="btn-mini" @click="resetFilter">重置</button>
- </div>
- <!-- 状态页签 + 视图切换 -->
- <div class="panel statustabs">
- <div
- v-for="kpi in kpis"
- :key="kpi.key"
- class="st"
- :class="{ on: tabStatus === kpi.key }"
- @click="switchTab(kpi.key)"
- >
- {{ kpi.label }}<i>{{ kpi.count }}</i>
- </div>
- <div class="viewswitch">
- <button :class="{ on: viewMode === 'list' }" @click="viewMode = 'list'">表格视图</button>
- <button :class="{ on: viewMode === 'map' }" @click="viewMode = 'map'">地图视图</button>
- </div>
- </div>
- <!-- 工作区 -->
- <div class="workarea">
- <!-- 左:报警列表 -->
- <div class="panel listpane" v-loading="loading">
- <div class="hd"><b>报警列表</b><span class="cnt">共 {{ filteredAlarms.length }} 条</span></div>
- <div class="listbody">
- <div
- v-for="alarm in paginatedAlarms"
- :key="alarm.id"
- class="card"
- :class="{ on: selectedAlarm?.id === alarm.id }"
- @click="selectAlarm(alarm)"
- >
- <div class="r1">
- <span class="no">{{ alarm.alarmNo }}</span>
- <el-tag :type="levelTagType(alarm.alarmLevel)" size="small">{{ levelText(alarm.alarmLevel) }}</el-tag>
- <span class="tag sm" :class="statusMeta(alarm).cls" style="margin-left:auto">{{ statusMeta(alarm).text }}</span>
- </div>
- <div class="desc">{{ alarm.alarmContent || '暂无报警描述' }}</div>
- <div class="r3">
- <span>{{ alarm.deviceName || alarm.deviceCode || '—' }}</span>
- <span>{{ formatDateTime(alarm.createTime) }}</span>
- </div>
- <div class="wo" v-if="woMap[alarm.id] != null">
- <span>关联工单</span><b>{{ ORDER_STATUS[woMap[alarm.id]] }}</b>
- </div>
- </div>
- <div v-if="!paginatedAlarms.length" class="empty">暂无报警数据</div>
- </div>
- <div class="listfoot">
- <el-pagination small layout="prev, pager, next" :total="filteredAlarms.length" :page-size="pageSize" v-model:current-page="currentPage" />
- </div>
- </div>
- <!-- 右:地图 / 详情 -->
- <div v-if="viewMode === 'map'" class="panel detailpane" v-loading="mapLoading">
- <GisMapCanvas :points="mapPoints" @point-click="onPointClick" />
- <div class="map-legend">
- <div><i style="background:#E6A23C"></i>待审核</div>
- <div><i style="background:#409EFF"></i>处置中</div>
- <div><i style="background:#67C23A"></i>已办结</div>
- <div><i style="background:#909399"></i>误报</div>
- </div>
- </div>
- <div v-else-if="selectedAlarm" class="panel detailpane" v-loading="detailLoading">
- <!-- 头部 -->
- <div class="dhead">
- <div class="dstat">
- <div class="statbox">
- <span class="lb">报警状态</span>
- <span class="vl" :class="statusMeta(selectedAlarm).cls">{{ statusMeta(selectedAlarm).text }}</span>
- </div>
- <span class="arw">→</span>
- <div class="statbox derived">
- <span class="lb">关联工单</span>
- <span class="vl">{{ workOrder ? (ORDER_STATUS[workOrder.orderStatus] || '—') : '未生成工单' }}</span>
- </div>
- <div class="statbox derived" v-if="workOrder">
- <span class="lb">工单号</span>
- <span class="vl mono">{{ workOrder.orderNo }}</span>
- </div>
- </div>
- </div>
- <!-- 步骤条 -->
- <div class="steps">
- <div
- v-for="(s, i) in stepNames"
- :key="s"
- class="step"
- :class="{ done: i < activeStep || activeStep === stepNames.length, cur: i === activeStep && activeStep < stepNames.length }"
- >
- <span class="idx"><template v-if="i < activeStep || activeStep === stepNames.length">✓</template><template v-else>{{ i + 1 }}</template></span>
- <span class="nm">{{ s }}</span>
- <span class="bar" v-if="i < stepNames.length - 1"></span>
- </div>
- </div>
- <!-- 详情页签 -->
- <div class="dtabs">
- <div v-for="t in detailTabs" :key="t.key" class="dtab" :class="{ on: activeTab === t.key }" @click="activeTab = t.key">
- {{ t.label }}
- </div>
- </div>
- <div class="dbody">
- <!-- 报警信息 -->
- <template v-if="activeTab === 'alarm'">
- <div class="dl">
- <div class="it"><span class="k">报警单号</span><span class="v mono">{{ detail?.alarmNo || '—' }}</span></div>
- <div class="it"><span class="k">报警等级</span><span class="v"><el-tag :type="levelTagType(detail?.alarmLevel)">{{ levelText(detail?.alarmLevel) }}</el-tag></span></div>
- <div class="it"><span class="k">报警审核</span><span class="v">{{ auditText(detail?.auditStatus) }}</span></div>
- <div class="it"><span class="k">实际值</span><span class="v">{{ detail?.actualValue ?? '—' }}</span></div>
- <div class="it"><span class="k">阈值</span><span class="v">{{ detail?.minValue ?? '—' }} / {{ detail?.maxValue ?? '—' }}</span></div>
- <div class="it"><span class="k">报警时间</span><span class="v">{{ formatDateTime(detail?.createTime ?? selectedAlarm.createTime, true) }}</span></div>
- <div class="it one"><span class="k">报警描述</span><span class="v">{{ detail?.alarmContent || '—' }}</span></div>
- </div>
- <div class="sec-t mt14">报警附件</div>
- <div class="media" v-if="alarmImages.length">
- <el-image
- v-for="(img, idx) in alarmImages"
- :key="idx"
- :src="img"
- fit="cover"
- class="mi-img"
- :preview-src-list="alarmImages"
- preview-teleported
- />
- </div>
- <div v-else class="empty" style="padding:20px 0">暂无报警附件</div>
- </template>
- <!-- 关联工单 -->
- <template v-else-if="activeTab === 'workorder'">
- <template v-if="workOrder">
- <div class="wo-card">
- <div class="hd3">
- <span class="wn">{{ workOrder.orderNo }}</span>
- <span class="tag" :class="orderTagClass(workOrder.orderStatus)">{{ ORDER_STATUS[workOrder.orderStatus] || '—' }}</span>
- <span class="spacer"></span>
- <span class="hint">处置人:{{ workOrder.receiveUserName || '—' }} · 核验人:{{ workOrder.verifyUserName || '—' }}</span>
- </div>
- <div class="bd3">
- <div class="dl">
- <div class="it"><span class="k">设备编码</span><span class="v mono">{{ workOrder.deviceCode || '—' }}</span></div>
- <div class="it"><span class="k">设备名称</span><span class="v">{{ workOrder.deviceName || '—' }}</span></div>
- <div class="it"><span class="k">故障发生时间</span><span class="v">{{ formatDateTime(workOrder.faultTime, true) }}</span></div>
- <div class="it"><span class="k">计划完成时间</span><span class="v">{{ formatDateTime(workOrder.planFinishTime, true) }}</span></div>
- <div class="it"><span class="k">实际完成时间</span><span class="v">{{ formatDateTime(workOrder.realFinishTime, true) }}</span></div>
- <div class="it"><span class="k">验收时间</span><span class="v">{{ formatDateTime(workOrder.verifyTime, true) }}</span></div>
- <div class="it one"><span class="k">问题描述</span><span class="v">{{ workOrder.orderDesc || '—' }}</span></div>
- <div class="it one"><span class="k">维修处置结果</span><span class="v">{{ workOrder.repairResult || '—' }}</span></div>
- </div>
- <div class="media mt14" v-if="woImages.length">
- <el-image v-for="(img, idx) in woImages" :key="idx" :src="img" fit="cover" class="mi-img" :preview-src-list="woImages" preview-teleported />
- </div>
- </div>
- </div>
- </template>
- <div v-else class="empty">暂无关联工单,请先核查报警</div>
- </template>
- <!-- 案件经过:整合报警日志与工单日志 -->
- <template v-else-if="activeTab === 'flow'">
- <el-timeline v-if="flowList.length" class="flow-tl">
- <el-timeline-item
- v-for="(item, idx) in flowList"
- :key="idx"
- :timestamp="flowTimestamp(item.time)"
- placement="top"
- :type="item.source === 'alarm' ? 'primary' : 'warning'"
- :color="item.source === 'alarm' ? '' : '#e6a23c'"
- >
- <div class="flow-card">
- <div class="flow-hd">
- <el-tag size="small" :type="item.source === 'alarm' ? 'primary' : 'warning'" effect="plain">
- {{ item.source === 'alarm' ? '报警' : '工单' }}
- </el-tag>
- <b class="flow-type">{{ item.typeLabel }}</b>
- <span class="flow-op">{{ item.operator }}</span>
- </div>
- <div v-if="item.content" class="flow-desc">{{ item.content }}</div>
- <div v-if="attachmentsOf(item.attachments).length" class="media mt14">
- <el-image
- v-for="(a, i) in attachmentsOf(item.attachments)"
- :key="i"
- :src="a.url"
- fit="cover"
- class="mi-img"
- :preview-src-list="attachmentsOf(item.attachments).map(x => x.url)"
- preview-teleported
- />
- </div>
- </div>
- </el-timeline-item>
- </el-timeline>
- <div v-else class="empty">暂无案件经过</div>
- </template>
- <!-- 设备信息 -->
- <template v-else>
- <div class="dl">
- <div class="it"><span class="k">设备编码</span><span class="v mono">{{ detail?.deviceCode || '—' }}</span></div>
- <div class="it"><span class="k">设备名称</span><span class="v">{{ detail?.deviceName || '—' }}</span></div>
- <div class="it"><span class="k">设备类型</span><span class="v">{{ detail?.equipmentTypeName || '—' }}</span></div>
- <div class="it"><span class="k">安装位置</span><span class="v">{{ detail?.equipmentLocation || '—' }}</span></div>
- <div class="it"><span class="k">经度</span><span class="v">{{ detail?.longitude || '—' }}</span></div>
- <div class="it"><span class="k">纬度</span><span class="v">{{ detail?.latitude || '—' }}</span></div>
- </div>
- </template>
- </div>
- <!-- 底部操作:本页只做总览,所有写操作跳转既有页面 -->
- <div class="dfoot">
- <el-button v-if="canReview" type="primary" @click="goAudit">前往核查</el-button>
- <el-button v-if="showWorkOrderBtn" type="warning" @click="goWorkOrder">工单处置</el-button>
- </div>
- </div>
- <div v-else class="panel detailpane center">
- <div class="empty">请从左侧选择报警查看流转详情</div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { computed, onActivated, onMounted, ref } from 'vue'
- import { useRouter } from 'vue-router'
- import GisMapCanvas from '@/components/GisMapCanvas.vue'
- import {
- getAlarmDetail,
- getAlarmList,
- getAlarmMap,
- getAlarmTimeline
- } from '@/api/pipeNetwork/basic'
- import { useDict } from '@/utils/dict'
- import { imageAttachments } from '@/views/subSystem/alarm/common/js/alarmReviewModel'
- import { orderLogText } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
- const router = useRouter()
- const { alarm_level: alarmLevels } = useDict('alarm_level')
- // 窨井设备根类型 ID(报警模块按 rootTypeId 隔离子系统;如与实际不符请调整)
- const ROOT_TYPE_MANHOLE = 4
- const ORDER_STATUS = {
- 0: '待派单', 1: '待接单', 2: '已接单', 3: '处理中', 4: '待验收', 5: '已结案', 6: '延期中', 7: '已取消'
- }
- const stepNames = ['报警生成', '核实', '派单', '处置', '核查', '办结']
- const detailTabs = computed(() => [
- { key: 'alarm', label: '报警信息' },
- { key: 'workorder', label: '关联工单' },
- { key: 'flow', label: '案件经过' },
- { key: 'device', label: '设备信息' }
- ])
- const alarms = ref([])
- const loading = ref(false)
- const detailLoading = ref(false)
- const mapLoading = ref(false)
- const keyword = ref('')
- const levelFilter = ref('')
- const timeRange = ref([])
- const tabStatus = ref('')
- const currentPage = ref(1)
- const pageSize = 8
- const selectedAlarm = ref(null)
- const detail = ref(null)
- const workOrder = ref(null)
- const timeline = ref([])
- const workOrderLogs = ref([])
- const viewMode = ref('list')
- const activeTab = ref('alarm')
- const mapPoints = ref([])
- const woMap = ref({})
- function formatDateTime(value, withSeconds = false) {
- if (!value) return '—'
- return String(value).replace('T', ' ').slice(0, withSeconds ? 19 : 16)
- }
- function localDay(date) {
- const pad = n => String(n).padStart(2, '0')
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
- }
- // 时间轴时间戳:日期按工单 dayLabel 的“今天/昨天”相对词,时刻按工单 logTime 只取 HH:mm
- function flowTimestamp(value) {
- const raw = String(value || '').replace('T', ' ').trim()
- if (!raw) return '—'
- const day = raw.slice(0, 10)
- const hm = raw.slice(11, 16) || '--:--'
- if (day === localDay(new Date())) return `今天 ${hm}`
- if (day === localDay(new Date(Date.now() - 86400000))) return `昨天 ${hm}`
- return `${(+day.slice(5, 7))}月${(+day.slice(8, 10))}日 ${hm}`
- }
- function orderTagClass(s) {
- if (s === 5) return 'tg-s'
- if (s === 7) return 'tg-g'
- if (s === 0) return 'tg-w'
- return 'tg-b'
- }
- // 报警等级:参考系统其它子系统,按级别映射 el-tag 类型区分颜色(字典未配置 listClass,故硬编码)
- function levelText(l) {
- const item = alarmLevels.value.find(i => String(i.value) === String(l))
- return item ? item.label : '—'
- }
- function levelTagType(l) {
- return { '1': 'danger', '2': 'warning', '3': 'primary', '4': 'success' }[String(l)] || 'info'
- }
- function auditText(s) {
- return { 0: '未审核', 1: '确认报警', 2: '误报警' }[Number(s)] || '—'
- }
- // 与报警日志(AlarmAttachments)一致:仅取图片附件并拼接 minio 完整 URL
- function toImageUrls(value) {
- return imageAttachments(value).map(item => item.url)
- }
- // 报警展示状态:误报 / 待审核 / 处置中 / 已办结
- function statusMeta(alarm) {
- if (alarm.auditStatus === 2) return { key: 'false', text: '误报', cls: 'tg-g' }
- if (alarm.auditStatus === 0) return { key: 'pending', text: '待审核', cls: 'tg-w' }
- const wo = woMap.value[alarm.id]
- if (wo === 5) return { key: 'completed', text: '已办结', cls: 'tg-s' }
- return { key: 'processing', text: '处置中', cls: 'tg-b' }
- }
- const filteredAlarms = computed(() => {
- if (!tabStatus.value) return alarms.value
- return alarms.value.filter(a => statusMeta(a).key === tabStatus.value)
- })
- const paginatedAlarms = computed(() => {
- const start = (currentPage.value - 1) * pageSize
- return filteredAlarms.value.slice(start, start + pageSize)
- })
- const kpis = computed(() => [
- { key: '', label: '全部', count: alarms.value.length },
- { key: 'pending', label: '待审核', count: alarms.value.filter(a => statusMeta(a).key === 'pending').length },
- { key: 'processing', label: '处置中', count: alarms.value.filter(a => statusMeta(a).key === 'processing').length },
- { key: 'completed', label: '已办结', count: alarms.value.filter(a => statusMeta(a).key === 'completed').length },
- { key: 'false', label: '误报', count: alarms.value.filter(a => statusMeta(a).key === 'false').length }
- ])
- // 步骤进度
- const stepStates = computed(() => {
- const a = detail.value
- const wo = workOrder.value
- return [
- true,
- !!a && a.auditStatus !== 0,
- !!wo && wo.orderStatus >= 1,
- !!wo && wo.orderStatus >= 3,
- !!wo && wo.orderStatus >= 4,
- !!wo && wo.orderStatus === 5
- ]
- })
- const activeStep = computed(() => {
- const s = stepStates.value
- const i = s.findIndex(x => !x)
- return i === -1 ? s.length : i
- })
- // 操作按钮可见性(严格对应现有接口)
- // 前往核查:仅当报警未审核(audit_status=0)时显示
- const canReview = computed(() => !!detail.value && detail.value.auditStatus === 0)
- // 工单处置:仅当报警已关联工单(alarm_data.work_order_id 不为空)且工单未结案(order_status!=5)时显示
- const showWorkOrderBtn = computed(() => {
- const wo = workOrder.value
- return !!detail.value?.workOrderId && !!wo && Number(wo.orderStatus) !== 5
- })
- const alarmImages = computed(() => {
- const imgs = []
- timeline.value.forEach(t => {
- if (t.attachments) imgs.push(...toImageUrls(t.attachments))
- if (t.attachUrl) imgs.push(...toImageUrls(t.attachUrl))
- })
- if (detail.value?.attachUrl) imgs.push(...toImageUrls(detail.value.attachUrl))
- return [...new Set(imgs)]
- })
- const woImages = computed(() => [
- ...toImageUrls(workOrder.value?.maintenanceBeforeImg),
- ...toImageUrls(workOrder.value?.maintenanceAfterImg)
- ])
- // 案件经过:整合 alarm_log 与 work_order_log,归一化后按时间倒序
- function normalizeAlarmLog(l) {
- return {
- source: 'alarm',
- time: l.operTime,
- operator: l.operUserName || '系统',
- typeLabel: l.operTypeName || '报警操作',
- content: l.operDesc || '',
- attachments: l.attachUrl
- }
- }
- function normalizeWorkOrderLog(l) {
- return {
- source: 'workOrder',
- time: l.operTime,
- operator: l.operUserName || '系统',
- typeLabel: orderLogText(l.operType),
- content: l.operDesc || '',
- attachments: l.attachUrl
- }
- }
- function attachmentsOf(value) {
- return imageAttachments(value).map(i => ({ url: i.url, name: i.name }))
- }
- const flowList = computed(() => {
- const items = [
- ...timeline.value.map(normalizeAlarmLog),
- ...workOrderLogs.value.map(normalizeWorkOrderLog)
- ]
- // 按操作时间数值降序(新→旧),兼容带 T 的 ISO 与空格分隔格式
- const toTs = t => {
- const s = String(t || '').trim()
- if (!s) return 0
- let v = new Date(s).getTime()
- if (!Number.isFinite(v)) v = new Date(s.replace('T', ' ').replace(/-/g, '/')).getTime()
- return Number.isFinite(v) ? v : 0
- }
- // 同一时刻:工单日志在上,报警日志在下(先报警后派单,工单动作更新)
- const rank = x => x.source === 'workOrder' ? 0 : 1
- items.sort((a, b) => (toTs(b.time) - toTs(a.time)) || (rank(a) - rank(b)))
- return items
- })
- async function loadAlarms() {
- loading.value = true
- try {
- const params = {
- pageNum: 1,
- pageSize: 100,
- rootTypeId: ROOT_TYPE_MANHOLE,
- keyword: keyword.value.trim() || undefined,
- alarmLevel: levelFilter.value || undefined
- }
- if (timeRange.value && timeRange.value.length === 2) {
- params.startTime = timeRange.value[0]
- params.endTime = timeRange.value[1]
- }
- const res = await getAlarmList(params)
- const records = Array.isArray(res?.data?.records) ? res.data.records : []
- alarms.value = records
- currentPage.value = 1
- const first = filteredAlarms.value[0]
- if (first) await selectAlarm(first)
- else {
- selectedAlarm.value = null
- detail.value = null
- workOrder.value = null
- }
- } catch (e) {
- alarms.value = []
- selectedAlarm.value = null
- } finally {
- loading.value = false
- }
- }
- async function selectAlarm(alarm) {
- selectedAlarm.value = alarm
- activeTab.value = 'alarm'
- detailLoading.value = true
- workOrder.value = null
- try {
- const [dRes, tRes] = await Promise.all([
- getAlarmDetail(alarm.id),
- getAlarmTimeline(alarm.id)
- ])
- detail.value = dRes?.data || alarm
- const t = tRes?.data
- timeline.value = Array.isArray(t) ? t : (t?.records || [])
- workOrderLogs.value = []
- // 报警详情接口已按 alarm_data.work_order_id 关联出工单(workOrderDetail = { order, logs })
- const woDetail = detail.value.workOrderDetail
- workOrder.value = woDetail?.order || null
- workOrderLogs.value = Array.isArray(woDetail?.logs) ? woDetail.logs : []
- if (workOrder.value) woMap.value[alarm.id] = workOrder.value.orderStatus
- } catch (e) {
- detail.value = alarm
- workOrder.value = null
- workOrderLogs.value = []
- } finally {
- detailLoading.value = false
- }
- }
- function switchTab(key) {
- tabStatus.value = key
- currentPage.value = 1
- const first = filteredAlarms.value[0]
- if (first) selectAlarm(first)
- else {
- selectedAlarm.value = null
- detail.value = null
- workOrder.value = null
- }
- }
- function resetFilter() {
- keyword.value = ''
- levelFilter.value = ''
- timeRange.value = []
- loadAlarms()
- }
- const onPointClick = (p) => {
- const a = alarms.value.find(x => x.id === p.id)
- if (a) selectAlarm(a)
- }
- async function loadMap() {
- mapLoading.value = true
- try {
- const res = await getAlarmMap({ rootTypeId: ROOT_TYPE_MANHOLE })
- const list = res?.data || []
- mapPoints.value = list
- .map(p => ({
- id: p.id,
- name: p.deviceName || p.deviceCode || '设备',
- lng: Number(p.longitude),
- lat: Number(p.latitude),
- status: statusMeta(p).key,
- code: p.deviceCode,
- type: p.warningCode
- }))
- .filter(p => p.lng && p.lat)
- } finally {
- mapLoading.value = false
- }
- }
- // 核查:跳转窨井报警审核页,带报警单号作为关键字,审核页会自动填入并定位到该报警
- function goAudit() {
- if (!selectedAlarm.value) return
- router.push({
- path: '/subSystem/manholeCover/alarm/alarmAudit',
- query: { rootTypeId: '4', keyword: selectedAlarm.value.alarmNo }
- })
- }
- // 工单操作:跳转工单处置页,带工单号作为关键字,处置页会自动填入并定位到该工单
- function goWorkOrder() {
- if (!workOrder.value) return
- router.push({
- path: '/subSystem/manholeCover/caseHandling/workOrderDispose',
- query: { keyword: workOrder.value.orderNo || '', rootTypeId: '4' }
- })
- }
- onMounted(() => {
- loadAlarms()
- loadMap()
- })
- // 从其他页面返回时刷新状态(菜单多为 keep-alive)
- onActivated(() => {
- loadAlarms()
- loadMap()
- })
- </script>
- <style scoped>
- .page {
- height: calc(100vh - 110px);
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding: 10px;
- background: #f0f2f5;
- font-size: 13px;
- color: #303133;
- overflow: hidden;
- }
- .panel { background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
- /* ===== 筛选 ===== */
- .filter { padding: 12px 14px; display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; }
- .fl { display: flex; align-items: center; gap: 8px; }
- .fl label { color: #606266; white-space: nowrap; }
- .btn-pri { background: #409eff; color: #fff; padding: 0 16px; height: 32px; border-radius: 4px; cursor: pointer; border: none; font-size: 13px; }
- .btn-pri:hover { background: #66b1ff; }
- .btn-mini { height: 32px; padding: 0 14px; border: 1px solid #dcdfe6; background: #fff; color: #606266; border-radius: 4px; cursor: pointer; font-size: 13px; }
- .btn-mini:hover { color: #409eff; border-color: #66b1ff; }
- /* ===== 状态页签 ===== */
- .statustabs { display: flex; align-items: center; gap: 2px; padding: 0 14px; height: 44px; flex: none; }
- .st { padding: 0 14px; color: #606266; height: 44px; line-height: 44px; cursor: pointer; position: relative; white-space: nowrap; }
- .st i { font-style: normal; color: #909399; margin-left: 3px; }
- .st:hover { color: #409eff; }
- .st.on { color: #409eff; }
- .st.on::after { content: ''; position: absolute; left: 12px; right: 12px; bottom: 0; height: 2px; background: #409eff; }
- .viewswitch { margin-left: auto; display: flex; border: 1px solid #dcdfe6; border-radius: 4px; overflow: hidden; }
- .viewswitch button { padding: 0 12px; height: 26px; font-size: 12px; color: #606266; background: #fff; border: none; cursor: pointer; }
- .viewswitch button.on { background: #409eff; color: #fff; }
- /* ===== 工作区 ===== */
- .workarea { flex: 1; display: flex; gap: 10px; min-height: 0; }
- /* 左列表 */
- .listpane { width: 330px; flex: none; display: flex; flex-direction: column; min-height: 0; }
- .listpane .hd { padding: 10px 14px; border-bottom: 1px solid #ebeef5; display: flex; align-items: center; justify-content: space-between; }
- .listpane .hd b { font-weight: 500; }
- .listpane .hd .cnt { color: #909399; }
- .listbody { flex: 1; overflow-y: auto; padding: 8px; }
- .listfoot { border-top: 1px solid #ebeef5; padding: 6px; text-align: center; }
- .card { border: 1px solid #e4e7ed; border-radius: 4px; padding: 10px 12px; margin-bottom: 8px; cursor: pointer; transition: 0.15s; background: #fff; }
- .card:hover { border-color: #66b1ff; }
- .card.on { border-color: #409eff; background: #f5f9ff; }
- .card .r1 { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; }
- .card .no { font-size: 12px; color: #909399; font-family: Menlo, Consolas, monospace; }
- .card .desc { font-size: 13px; color: #303133; line-height: 1.5; margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
- .card .r3 { display: flex; justify-content: space-between; color: #909399; font-size: 12px; gap: 8px; }
- .card .wo { margin-top: 6px; padding-top: 6px; border-top: 1px dashed #ebeef5; font-size: 11px; color: #909399; display: flex; justify-content: space-between; }
- .card .wo b { font-weight: 400; color: #606266; }
- /* 标签 */
- .tag { display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: 3px; font-size: 12px; line-height: 1; white-space: nowrap; }
- .tag.sm { height: 18px; font-size: 11px; padding: 0 5px; }
- .tg-w { color: #b88230; background: #fdf6ec; border: 1px solid #faecd8; }
- .tg-b { color: #337ecc; background: #ecf5ff; border: 1px solid #d9ecff; }
- .tg-s { color: #529b2e; background: #f0f9eb; border: 1px solid #e1f3d8; }
- .tg-d { color: #c45656; background: #fef0f0; border: 1px solid #fde2e2; }
- .tg-g { color: #73767a; background: #f4f4f5; border: 1px solid #e9e9eb; }
- .tg-p { color: #5a52c4; background: #f0effe; border: 1px solid #e0defa; }
- /* 右详情 */
- .detailpane { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; overflow: hidden; }
- .detailpane.center { align-items: center; justify-content: center; }
- .dhead { padding: 13px 18px 12px; border-bottom: 1px solid #ebeef5; flex: none; }
- .dstat { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
- .statbox { border: 1px solid #e4e7ed; border-radius: 4px; padding: 6px 12px; display: flex; align-items: center; gap: 9px; background: #fafcff; }
- .statbox .lb { font-size: 12px; color: #909399; }
- .statbox .vl { font-size: 13px; color: #303133; font-weight: 500; }
- .statbox.derived { background: #fff; }
- .statbox .mono { font-family: Menlo, Consolas, monospace; color: #409eff; }
- .dstat .arw { color: #dcdfe6; font-size: 12px; }
- /* 步骤条 */
- .steps { display: flex; align-items: center; padding: 13px 18px; background: #fafcff; border-bottom: 1px solid #ebeef5; flex: none; }
- .step { display: flex; align-items: center; gap: 8px; flex: 1; position: relative; }
- .step .idx { width: 24px; height: 24px; border-radius: 50%; border: 1px solid #dcdfe6; background: #fff; color: #909399; display: flex; align-items: center; justify-content: center; font-size: 12px; flex: none; }
- .step .nm { font-size: 13px; color: #909399; white-space: nowrap; }
- .step.done .idx { background: #67c23a; border-color: #67c23a; color: #fff; }
- .step.done .nm { color: #606266; }
- .step.cur .idx { background: #409eff; border-color: #409eff; color: #fff; box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.18); }
- .step.cur .nm { color: #409eff; font-weight: 500; }
- .step .bar { height: 1px; background: #dcdfe6; flex: 1; margin: 0 10px; }
- .step.done .bar { background: #67c23a; }
- .step:last-child { flex: none; }
- .step:last-child .bar { display: none; }
- /* 详情页签 */
- .dtabs { display: flex; gap: 2px; padding: 0 18px; border-bottom: 1px solid #ebeef5; flex: none; }
- .dtab { padding: 0 16px; height: 40px; line-height: 40px; font-size: 13px; color: #606266; cursor: pointer; position: relative; }
- .dtab:hover { color: #409eff; }
- .dtab.on { color: #409eff; }
- .dtab.on::after { content: ''; position: absolute; left: 16px; right: 16px; bottom: -1px; height: 2px; background: #409eff; }
- .dtab .cnt { font-size: 11px; color: #909399; margin-left: 4px; }
- .dbody { flex: 1; overflow-y: auto; padding: 16px 18px; }
- .dfoot { border-top: 1px solid #ebeef5; padding: 10px 18px; display: flex; justify-content: flex-end; gap: 10px; flex: none; }
- /* dl 网格(键值对) */
- .dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 26px; }
- .dl .it { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px dashed #ebeef5; align-items: flex-start; }
- .dl .it.one { grid-column: 1 / -1; }
- .dl .it .k { color: #909399; width: 100px; flex: none; }
- .dl .it .v { color: #303133; flex: 1; word-break: break-all; line-height: 1.6; }
- .mono { font-family: Menlo, Consolas, monospace; font-size: 12px; }
- .sec-t { font-size: 13px; font-weight: 500; color: #303133; padding: 2px 0 10px; display: flex; align-items: center; gap: 8px; }
- .sec-t::before { content: ''; width: 3px; height: 13px; background: #409eff; border-radius: 2px; }
- .mt14 { margin-top: 14px; }
- /* 多媒体网格 */
- .media { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 10px; }
- .mi-img { width: 100%; height: 90px; border-radius: 4px; border: 1px solid #e4e7ed; cursor: pointer; }
- /* 工单卡片 */
- .wo-card { border: 1px solid #e4e7ed; border-radius: 4px; overflow: hidden; }
- .wo-card .hd3 { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: #fafcff; border-bottom: 1px solid #ebeef5; flex-wrap: wrap; }
- .wo-card .hd3 .wn { font-family: Menlo, Consolas, monospace; font-size: 13px; font-weight: 500; }
- .wo-card .hd3 .spacer { flex: 1; }
- .wo-card .hd3 .hint { font-size: 12px; color: #909399; }
- .wo-card .bd3 { padding: 4px 12px 12px; }
- /* 案件经过时间轴(参考工单处置记录) */
- .flow-tl { padding: 4px 0; }
- /* 竖线:与工单处置记录一致,蓝色 1px 短划线(4px 实 / 4px 空),末项不画线 */
- .flow-tl :deep(.el-timeline-item__tail) {
- border-left: none;
- left: 5px;
- width: 1px;
- background: repeating-linear-gradient(to bottom, #3b5bdb 0 4px, transparent 4px 8px);
- }
- .flow-tl :deep(.el-timeline-item:last-child .el-timeline-item__tail) { display: none; }
- .flow-card { background: #fafcff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px 12px; }
- .flow-hd { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }
- .flow-hd .flow-type { font-weight: 500; color: #303133; }
- .flow-hd .flow-op { margin-left: auto; color: #909399; font-size: 12px; }
- .flow-desc { font-size: 13px; color: #606266; line-height: 1.7; margin-top: 6px; white-space: pre-wrap; }
- /* 地图图例 */
- .map-legend { position: absolute; left: 10px; bottom: 10px; background: rgba(255, 255, 255, 0.94); border: 1px solid #dcdfe6; border-radius: 4px; padding: 7px 9px; font-size: 12px; color: #606266; display: flex; flex-direction: column; gap: 5px; z-index: 900; }
- .map-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
- .empty { padding: 44px 0; text-align: center; color: #909399; font-size: 13px; }
- </style>
|