|
|
@@ -1,317 +1,749 @@
|
|
|
-<!--案件流转管理-->
|
|
|
+<!--案件流转管理:以报警(alarm_data)为源头,关联工单(work_order)完成核查/派单/办结闭环-->
|
|
|
<template>
|
|
|
- <div class="min-h-screen bg-[#f0f2f6] p-5">
|
|
|
- <div class="mb-5 rounded-[20px] bg-white px-6 py-4">
|
|
|
- <div class="flex flex-wrap gap-6">
|
|
|
- <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === '' }" @click="filterStatus('')">
|
|
|
- <div class="text-[26px] font-bold">{{ totalCases }}</div>
|
|
|
- <div>全部案件</div>
|
|
|
- </div>
|
|
|
- <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'pending' }" @click="filterStatus('pending')">
|
|
|
- <div class="text-[26px] font-bold text-[#e6a23c]">{{ pendingCount }}</div>
|
|
|
- <div>待核查</div>
|
|
|
- </div>
|
|
|
- <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'processing' }" @click="filterStatus('processing')">
|
|
|
- <div class="text-[26px] font-bold">{{ processingCount }}</div>
|
|
|
- <div>办理中</div>
|
|
|
- </div>
|
|
|
- <div class="min-w-[120px] cursor-pointer rounded-2xl px-4 py-2 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'completed' }" @click="filterStatus('completed')">
|
|
|
- <div class="text-[26px] font-bold">{{ completedCount }}</div>
|
|
|
- <div>已办结</div>
|
|
|
- </div>
|
|
|
+ <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="flex gap-5">
|
|
|
- <div class="w-[380px] flex flex-col overflow-hidden rounded-[20px] bg-white" v-loading="loading">
|
|
|
- <div class="flex items-center justify-between gap-3 border-b border-[#e9ecef] px-4 py-4 font-600">
|
|
|
- <span>案件列表</span>
|
|
|
- <div class="flex items-center gap-2">
|
|
|
- <el-input
|
|
|
- v-model="searchCaseNo"
|
|
|
- placeholder="案件编号"
|
|
|
- :prefix-icon="Search"
|
|
|
- clearable
|
|
|
- size="small"
|
|
|
- class="w-[140px]"
|
|
|
- @keyup.enter="loadCases"
|
|
|
- />
|
|
|
- <el-input
|
|
|
- v-model="searchEquipmentLocation"
|
|
|
- placeholder="设备位置描述"
|
|
|
- :prefix-icon="Location"
|
|
|
- clearable
|
|
|
- size="small"
|
|
|
- class="w-[140px]"
|
|
|
- @keyup.enter="loadCases"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="flex-1 overflow-y-auto p-3">
|
|
|
+ <!-- 状态页签 + 视图切换 -->
|
|
|
+ <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="caseItem in paginatedCases"
|
|
|
- :key="caseItem.caseId"
|
|
|
- class="mb-2 flex cursor-pointer items-center gap-3 rounded-[14px] border border-transparent bg-[#fafbfc] p-3"
|
|
|
- :class="{ 'border-[#409eff] bg-[#ecf5ff]': selectedCase?.caseId === caseItem.caseId }"
|
|
|
- @click="selectCase(caseItem)"
|
|
|
+ v-for="alarm in paginatedAlarms"
|
|
|
+ :key="alarm.id"
|
|
|
+ class="card"
|
|
|
+ :class="{ on: selectedAlarm?.id === alarm.id }"
|
|
|
+ @click="selectAlarm(alarm)"
|
|
|
>
|
|
|
- <div class="case-level">
|
|
|
- <el-tag :type="caseItem.levelTag" size="small">{{ caseItem.levelText }}</el-tag>
|
|
|
+ <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="flex-1">
|
|
|
- <div class="mb-1 font-600">{{ caseItem.caseNo }}</div>
|
|
|
- <div class="flex items-center gap-1 text-[12px] text-[#6c757d]"><el-icon><Location /></el-icon> 设备编号 {{ caseItem.deviceCode || '—' }}</div>
|
|
|
- <div class="text-[11px] text-[#aaa]">{{ caseItem.createTimeText }}</div>
|
|
|
+ <div class="desc">{{ alarm.alarmContent || '暂无报警描述' }}</div>
|
|
|
+ <div class="r3">
|
|
|
+ <span>{{ alarm.deviceName || alarm.deviceCode || '—' }}</span>
|
|
|
+ <span>{{ formatDateTime(alarm.createTime) }}</span>
|
|
|
</div>
|
|
|
- <div class="case-status">
|
|
|
- <el-tag :type="caseItem.statusTag" size="small">{{ caseItem.statusText }}</el-tag>
|
|
|
+ <div class="wo" v-if="woMap[alarm.id] != null">
|
|
|
+ <span>关联工单</span><b>{{ ORDER_STATUS[woMap[alarm.id]] }}</b>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <el-empty v-if="!paginatedCases.length" description="暂无案件数据" :image-size="80" />
|
|
|
+ <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 class="border-t border-[#eee] p-3 text-center">
|
|
|
- <el-pagination small layout="prev, pager, next" :total="filteredCases.length" :page-size="pageSize" v-model:current-page="currentPage" />
|
|
|
+ </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 class="max-h-[calc(100vh-140px)] flex-1 overflow-y-auto rounded-[20px] bg-white p-5" v-if="selectedCase" v-loading="detailLoading">
|
|
|
- <div class="mb-5 flex gap-5">
|
|
|
- <div class="flex-1">
|
|
|
- <div class="mb-[14px] flex items-center gap-2 text-[16px] font-600">
|
|
|
- <span><el-icon><InfoFilled /></el-icon> 案件信息</span>
|
|
|
+ <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>
|
|
|
- <el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="案件编号">{{ selectedCase.caseNo }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="案件等级">
|
|
|
- <el-tag :type="selectedCase.levelTag">{{ selectedCase.levelText }}</el-tag>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="案件来源">{{ selectedCase.sourceText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="案件描述">{{ selectedCase.caseDesc || '—' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="案件状态">{{ selectedCase.statusText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="立案时间">{{ selectedCase.createTimeText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="核查时间">{{ selectedCase.verifyTimeText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="办结时间">{{ selectedCase.finishTimeText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="处置结果">{{ selectedCase.handleResult || '—' }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
</div>
|
|
|
- <div class="flex-1">
|
|
|
- <div class="mb-[14px] flex items-center gap-2 text-[16px] font-600"><el-icon><Monitor /></el-icon> 设备信息</div>
|
|
|
- <el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="设备编号">{{ selectedCase.deviceCode || '—' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备类别">{{ selectedCase.equipmentTypeName || '—' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="运维状态">{{ selectedCase.currentStatusText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="在线状态">{{ selectedCase.onlineStatusText }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警状态">{{ selectedCase.alarmStatusText }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
+ </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="mt-6 border-t border-[#e9ecef] pt-4">
|
|
|
- <div class="mb-[14px] flex items-center gap-2 text-[16px] font-600"><el-icon><Picture /></el-icon> 案件附件</div>
|
|
|
- <div class="flex flex-wrap gap-3" v-if="selectedCase.attachImages.length">
|
|
|
- <el-image
|
|
|
- v-for="(img, idx) in selectedCase.attachImages"
|
|
|
- :key="idx"
|
|
|
- :src="img"
|
|
|
- fit="cover"
|
|
|
- class="h-[80px] w-[100px] cursor-pointer rounded-lg object-cover"
|
|
|
- :preview-src-list="selectedCase.attachImages"
|
|
|
- preview-teleported
|
|
|
- />
|
|
|
+ <!-- 详情页签 -->
|
|
|
+ <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>
|
|
|
- <el-empty v-else description="暂无案件附件" :image-size="80" />
|
|
|
+ </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="flex flex-1 items-center justify-center rounded-[20px] bg-white">
|
|
|
- <el-empty description="请从左侧选择案件进行核查" :image-size="140" />
|
|
|
+ <div v-else class="panel detailpane center">
|
|
|
+ <div class="empty">请从左侧选择报警查看流转详情</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import {computed, onMounted, ref} from 'vue'
|
|
|
-import {InfoFilled, Location, Monitor, Picture, Search} from '@element-plus/icons-vue'
|
|
|
-import {getManholeCaseDetail, getManholeCasePage} from '@/api/pipeNetwork/basic'
|
|
|
+import { computed, onActivated, onMounted, ref } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import GisMapCanvas from '@/components/GisMapCanvas.vue'
|
|
|
+import {
|
|
|
+ getAlarmDetail,
|
|
|
+ getAlarmList,
|
|
|
+ getAlarmMap,
|
|
|
+ getAlarmTimeline,
|
|
|
+ getWorkOrderDetail,
|
|
|
+ getWorkOrderLogs
|
|
|
+} 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 cases = ref([])
|
|
|
+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 searchCaseNo = ref('')
|
|
|
-const searchEquipmentLocation = ref('')
|
|
|
-const statusFilter = ref('')
|
|
|
+const mapLoading = ref(false)
|
|
|
+const keyword = ref('')
|
|
|
+const levelFilter = ref('')
|
|
|
+const timeRange = ref([])
|
|
|
+const tabStatus = ref('')
|
|
|
const currentPage = ref(1)
|
|
|
-const pageSize = 5
|
|
|
-const selectedCase = ref(null)
|
|
|
+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) {
|
|
|
+function formatDateTime(value, withSeconds = false) {
|
|
|
if (!value) return '—'
|
|
|
- return String(value).replace('T', ' ')
|
|
|
+ return String(value).replace('T', ' ').slice(0, withSeconds ? 19 : 16)
|
|
|
}
|
|
|
-
|
|
|
-function getCaseLevelMeta(level) {
|
|
|
- const map = {
|
|
|
- 1: { text: '严重', tag: 'danger' },
|
|
|
- 2: { text: '一般', tag: 'warning' },
|
|
|
- 3: { text: '轻微', tag: 'info' }
|
|
|
- }
|
|
|
- return map[Number(level)] || { text: '—', tag: 'info' }
|
|
|
+function localDay(date) {
|
|
|
+ const pad = n => String(n).padStart(2, '0')
|
|
|
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
|
}
|
|
|
-
|
|
|
-function getCaseStatusMeta(status) {
|
|
|
- const map = {
|
|
|
- 1: { key: 'pending', text: '待核查', tag: 'danger' },
|
|
|
- 2: { key: 'processing', text: '办理中', tag: 'warning' },
|
|
|
- 3: { key: 'completed', text: '已办结', tag: 'success' },
|
|
|
- 4: { key: 'closed', text: '误报关闭', tag: 'info' }
|
|
|
- }
|
|
|
- return map[Number(status)] || { key: '', text: '—', tag: 'info' }
|
|
|
+// 时间轴时间戳:日期按工单 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 getCaseSourceText(source) {
|
|
|
- const map = {
|
|
|
- 1: '系统自动报警',
|
|
|
- 2: '运维巡检上报',
|
|
|
- 3: '人工投诉上报',
|
|
|
- 4: '平台录入'
|
|
|
- }
|
|
|
- return map[Number(source)] || '—'
|
|
|
+function orderTagClass(s) {
|
|
|
+ if (s === 5) return 'tg-s'
|
|
|
+ if (s === 7) return 'tg-g'
|
|
|
+ if (s === 0) return 'tg-w'
|
|
|
+ return 'tg-b'
|
|
|
}
|
|
|
-
|
|
|
-function splitAttachUrls(value) {
|
|
|
- if (!value) return []
|
|
|
- return String(value)
|
|
|
- .split(/[,\n;]/)
|
|
|
- .map(item => item.trim())
|
|
|
- .filter(Boolean)
|
|
|
+// 报警等级:参考系统其它子系统,按级别映射 el-tag 类型区分颜色(字典未配置 listClass,故硬编码)
|
|
|
+function levelText(l) {
|
|
|
+ const item = alarmLevels.value.find(i => String(i.value) === String(l))
|
|
|
+ return item ? item.label : '—'
|
|
|
}
|
|
|
-
|
|
|
-function getOnlineStatusText(status) {
|
|
|
- const map = {
|
|
|
- 0: '离线',
|
|
|
- 1: '在线'
|
|
|
- }
|
|
|
- return map[Number(status)] || '—'
|
|
|
+function levelTagType(l) {
|
|
|
+ return { '1': 'danger', '2': 'warning', '3': 'primary', '4': 'success' }[String(l)] || 'info'
|
|
|
}
|
|
|
-
|
|
|
-function getAlarmStatusText(status) {
|
|
|
- const map = {
|
|
|
- 0: '正常',
|
|
|
- 1: '报警'
|
|
|
- }
|
|
|
- return map[Number(status)] || '—'
|
|
|
+function auditText(s) {
|
|
|
+ return { 0: '未审核', 1: '确认报警', 2: '误报警' }[Number(s)] || '—'
|
|
|
}
|
|
|
-
|
|
|
-function getCurrentStatusText(status) {
|
|
|
- const map = {
|
|
|
- 1: '在用',
|
|
|
- 2: '闲置',
|
|
|
- 3: '维修',
|
|
|
- 4: '报废',
|
|
|
- 5: '待入库'
|
|
|
- }
|
|
|
- return map[Number(status)] || '—'
|
|
|
+// 与报警日志(AlarmAttachments)一致:仅取图片附件并拼接 minio 完整 URL
|
|
|
+function toImageUrls(value) {
|
|
|
+ return imageAttachments(value).map(item => item.url)
|
|
|
}
|
|
|
-
|
|
|
-function normalizeCase(item) {
|
|
|
- const levelMeta = getCaseLevelMeta(item.caseLevel)
|
|
|
- const statusMeta = getCaseStatusMeta(item.caseStatus)
|
|
|
- return {
|
|
|
- ...item,
|
|
|
- levelText: levelMeta.text,
|
|
|
- levelTag: levelMeta.tag,
|
|
|
- statusKey: statusMeta.key,
|
|
|
- statusText: statusMeta.text,
|
|
|
- statusTag: statusMeta.tag,
|
|
|
- sourceText: getCaseSourceText(item.caseSource),
|
|
|
- createTimeText: formatDateTime(item.createTime),
|
|
|
- verifyTimeText: formatDateTime(item.verifyTime),
|
|
|
- finishTimeText: formatDateTime(item.finishTime),
|
|
|
- updateTimeText: formatDateTime(item.updateTime),
|
|
|
- attachImages: splitAttachUrls(item.attachUrl),
|
|
|
- currentStatusText: getCurrentStatusText(item.equipmentStatus?.currentStatus),
|
|
|
- onlineStatusText: getOnlineStatusText(item.equipmentStatus?.onlineStatus),
|
|
|
- alarmStatusText: getAlarmStatusText(item.equipmentStatus?.alarmStatus),
|
|
|
- statusUpdateTimeText: formatDateTime(item.equipmentStatus?.statusUpdateTime)
|
|
|
- }
|
|
|
+// 报警展示状态:误报 / 待审核 / 处置中 / 已办结
|
|
|
+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 filteredCases = computed(() => {
|
|
|
- if (!statusFilter.value) return cases.value
|
|
|
- return cases.value.filter(item => item.statusKey === statusFilter.value)
|
|
|
+const filteredAlarms = computed(() => {
|
|
|
+ if (!tabStatus.value) return alarms.value
|
|
|
+ return alarms.value.filter(a => statusMeta(a).key === tabStatus.value)
|
|
|
})
|
|
|
-
|
|
|
-const paginatedCases = computed(() => {
|
|
|
+const paginatedAlarms = computed(() => {
|
|
|
const start = (currentPage.value - 1) * pageSize
|
|
|
- return filteredCases.value.slice(start, start + 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 totalCases = computed(() => cases.value.length)
|
|
|
-const pendingCount = computed(() => cases.value.filter(item => item.statusKey === 'pending').length)
|
|
|
-const processingCount = computed(() => cases.value.filter(item => item.statusKey === 'processing').length)
|
|
|
-const completedCount = computed(() => cases.value.filter(item => item.statusKey === 'completed').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
|
|
|
+})
|
|
|
|
|
|
-async function loadCases() {
|
|
|
+// 操作按钮可见性(严格对应现有接口)
|
|
|
+// 前往核查:仅当报警未审核(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 caseNo = searchCaseNo.value.trim()
|
|
|
- const equipmentLocation = searchEquipmentLocation.value.trim()
|
|
|
- const res = await getManholeCasePage({
|
|
|
+ const params = {
|
|
|
pageNum: 1,
|
|
|
- pageSize: 200,
|
|
|
- caseNo: caseNo || undefined,
|
|
|
- equipmentLocation: equipmentLocation || undefined
|
|
|
- })
|
|
|
+ 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 : []
|
|
|
- cases.value = records.map(item => normalizeCase(item))
|
|
|
+ alarms.value = records
|
|
|
currentPage.value = 1
|
|
|
- const first = filteredCases.value[0]
|
|
|
- if (!first) {
|
|
|
- selectedCase.value = null
|
|
|
- return
|
|
|
- }
|
|
|
- if (selectedCase.value) {
|
|
|
- const matched = cases.value.find(item => item.caseId === selectedCase.value.caseId)
|
|
|
- if (matched) {
|
|
|
- await selectCase(matched)
|
|
|
- return
|
|
|
- }
|
|
|
+ const first = filteredAlarms.value[0]
|
|
|
+ if (first) await selectAlarm(first)
|
|
|
+ else {
|
|
|
+ selectedAlarm.value = null
|
|
|
+ detail.value = null
|
|
|
+ workOrder.value = null
|
|
|
}
|
|
|
- await selectCase(first)
|
|
|
- } catch (error) {
|
|
|
- cases.value = []
|
|
|
- selectedCase.value = null
|
|
|
+ } catch (e) {
|
|
|
+ alarms.value = []
|
|
|
+ selectedAlarm.value = null
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function selectCase(caseItem) {
|
|
|
+async function selectAlarm(alarm) {
|
|
|
+ selectedAlarm.value = alarm
|
|
|
+ activeTab.value = 'alarm'
|
|
|
detailLoading.value = true
|
|
|
+ workOrder.value = null
|
|
|
try {
|
|
|
- const res = await getManholeCaseDetail(caseItem.caseId)
|
|
|
- const detail = res?.data || caseItem
|
|
|
- selectedCase.value = normalizeCase(detail)
|
|
|
- } catch (error) {
|
|
|
- selectedCase.value = caseItem
|
|
|
+ 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
|
|
|
+ if (woDetail?.order) {
|
|
|
+ workOrder.value = woDetail.order
|
|
|
+ workOrderLogs.value = Array.isArray(woDetail.logs) ? woDetail.logs : []
|
|
|
+ } else if (detail.value.workOrderId) {
|
|
|
+ // 兜底:旧版后端未带出工单时,仍走工单独立接口(需 ops:workOrder:query 权限)
|
|
|
+ const [wRes, lRes] = await Promise.allSettled([
|
|
|
+ getWorkOrderDetail(detail.value.workOrderId),
|
|
|
+ getWorkOrderLogs(detail.value.workOrderId)
|
|
|
+ ])
|
|
|
+ const wData = wRes.status === 'fulfilled' ? (wRes.value?.data || {}) : {}
|
|
|
+ workOrder.value = wData.order || (wData.orderId ? wData : null)
|
|
|
+ const l = lRes.status === 'fulfilled' ? lRes.value?.data : wData.logs
|
|
|
+ workOrderLogs.value = Array.isArray(l) ? l : (l?.records || [])
|
|
|
+ } else {
|
|
|
+ workOrder.value = null
|
|
|
+ }
|
|
|
+ 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 filterStatus(status) {
|
|
|
- statusFilter.value = status
|
|
|
+function switchTab(key) {
|
|
|
+ tabStatus.value = key
|
|
|
currentPage.value = 1
|
|
|
- const first = filteredCases.value[0]
|
|
|
- if (first) {
|
|
|
- selectCase(first)
|
|
|
- } else {
|
|
|
- selectedCase.value = null
|
|
|
+ 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(() => {
|
|
|
- loadCases()
|
|
|
+ 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: 5; }
|
|
|
+.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>
|