Преглед изворни кода

重构-窨井子系统-案件流转管理

zengy пре 2 дана
родитељ
комит
97107534f2

+ 59 - 0
src/api/pipeNetwork/basic.js

@@ -113,6 +113,65 @@ export function verifyManholeCase(data) {
 export function addManholeWorkOrder(data) {
   return request({ url: '/manhole/workOrder/addWorkOrder', method: 'post', data })
 }
+
+// ============ 报警 / 工单(案件流转:复用现有 alarm、ops/workOrder 模块) ============
+// 报警列表(AlarmQuery 过滤:keyword/warningCode/alarmLevel/auditStatus/displayStatus/alarmNo/deviceCode/startTime/endTime/rootTypeId)
+export function getAlarmList(params) {
+  return request({ url: '/api/alarm', method: 'get', params })
+}
+// 报警按等级统计
+export function getAlarmStat(params) {
+  return request({ url: '/api/alarm/statistics', method: 'get', params })
+}
+// 报警 GIS 点位
+export function getAlarmMap(params) {
+  return request({ url: '/api/alarm/map', method: 'get', params })
+}
+// 报警详情
+export function getAlarmDetail(alarmId, params) {
+  return request({ url: '/api/alarm/' + alarmId, method: 'get', params })
+}
+// 报警及关联工单操作时间线(办理经过)
+export function getAlarmTimeline(alarmId, params) {
+  return request({ url: '/api/alarm/' + alarmId + '/timeline', method: 'get', params })
+}
+// 报警审核列表(待审核)
+export function getAlarmReviewList(params) {
+  return request({ url: '/api/alarm/reviews', method: 'get', params })
+}
+// 提交报警审核:确认报警(生成工单) auditStatus=1 / 误报 auditStatus=2
+export function reviewAlarm(alarmId, params, data) {
+  return request({ url: '/api/alarm/' + alarmId + '/reviews', method: 'post', params, data })
+}
+// 处理报警(误报/解除)
+export function handleAlarm(alarmId) {
+  return request({ url: '/api/alarm/handle/' + alarmId, method: 'put' })
+}
+
+// 工单列表
+export function getWorkOrderList(params) {
+  return request({ url: '/api/ops/workOrder', method: 'get', params })
+}
+// 工单详情(含操作日志)
+export function getWorkOrderDetail(orderId) {
+  return request({ url: '/api/ops/workOrder/' + orderId, method: 'get' })
+}
+// 工单操作日志
+export function getWorkOrderLogs(orderId) {
+  return request({ url: '/api/ops/workOrder/' + orderId + '/logs', method: 'get' })
+}
+// 派单(待派单 → 待接单)
+export function dispatchWorkOrder(orderId, data) {
+  return request({ url: '/api/ops/workOrder/' + orderId + '/dispatch', method: 'put', data })
+}
+// 验收(待验收 → 已结案/处理中)
+export function verifyWorkOrder(orderId, data) {
+  return request({ url: '/api/ops/workOrder/' + orderId + '/verify', method: 'put', data })
+}
+// 可指派用户:type=handler(处置人) / verify(核验人)
+export function getAssignableUsers(orderId, type, deptId) {
+  return request({ url: '/api/ops/workOrder/assignable/users', method: 'get', params: { orderId, type, deptId } })
+}
 // 井盖监测采集数据分页(jg_device_data 原始采集记录)
 export function getManholeDataPage(params) {
   return request({ url: '/api/manhole/data/page', method: 'get', params })

+ 29 - 4
src/views/subSystem/alarm/common/audit.vue

@@ -2,7 +2,7 @@
   <div class="order-container alarm-review-page">
     <el-card class="search-card" shadow="never">
       <el-form :inline="true" :model="filters" class="search-form" @submit.prevent="search">
-        <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码 / 名称" @keyup.enter="search" /></el-form-item>
+        <el-form-item label="关键字"><el-input v-model="filters.keyword" clearable placeholder="报警单号 / 设备编码 / 设备名称" @keyup.enter="search" /></el-form-item>
         <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in alarmLevels" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
         <el-form-item label="核实结果"><el-select v-model="filters.auditStatus" clearable placeholder="全部结果"><el-option v-for="item in options.auditStatuses" :key="item.key" :label="item.key === 'UNREVIEWED' ? '待核实' : item.label" :value="item.value" /></el-select></el-form-item>
         <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
@@ -37,7 +37,25 @@
         <el-form ref="formRef" :model="form" :rules="rules" label-width="90px" :disabled="submitting || detailLoading">
           <el-form-item label="核实结果" prop="auditStatus"><el-radio-group v-model="form.auditStatus" class="review-choices"><el-radio v-for="item in reviewChoices" :key="item.key" :label="item.value" border>{{ item.label }}</el-radio></el-radio-group></el-form-item>
           <el-form-item label="核实意见" prop="opinion"><el-input v-model="form.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="选填,可填写核实情况、判定依据或误报原因" /></el-form-item>
-          <el-form-item label="图片"><div class="attachments"><el-upload v-model:file-list="files" :http-request="uploadFile" :before-upload="beforeUpload" :limit="5" :on-exceed="exceedFiles" :disabled="submitting || detailLoading" accept=".jpg,.jpeg,.png"><el-button :icon="Upload">选择图片</el-button><template #tip><div class="el-upload__tip">支持 png / jpg / jpeg,单个文件不超过 10MB</div></template></el-upload></div></el-form-item>
+          <el-form-item label="附件">
+            <div class="attachments">
+              <el-upload
+                v-model:file-list="files"
+                list-type="picture-card"
+                multiple
+                :http-request="uploadFile"
+                :before-upload="beforeUpload"
+                :limit="5"
+                :on-exceed="exceedFiles"
+                :disabled="submitting || detailLoading"
+                accept=".jpg,.jpeg,.png"
+                :class="{ hide: files.length >= 5 }"
+              >
+                <el-icon><Plus /></el-icon>
+              </el-upload>
+              <div class="el-upload__tip">支持 png / jpg / jpeg,单个文件不超过 10MB</div>
+            </div>
+          </el-form-item>
         </el-form>
         <el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon />
       </div>
@@ -59,7 +77,7 @@
 import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
 import { useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
 import { ElMessage } from 'element-plus'
-import { Refresh, Search, Upload } from '@element-plus/icons-vue'
+import { Refresh, Search, Plus } from '@element-plus/icons-vue'
 import { getAlarmReviewOptions, getAlarmReviewPage, getAlarmReviewStatistics, getAlarmReviewDetail, getAlarmLogs, submitAlarmReview, uploadAlarmReviewAttachments } from '@/api/alarmReview'
 import { alarmAuditText } from '@/utils/alarmLabels'
 import AlarmSummary from '../AlarmSummary.vue'
@@ -205,13 +223,20 @@ async function openHistory(row) {
 watch(scopeKey, async () => {
   ++detailRequest; ++historyRequest; reviewVisible.value = false; historyVisible.value = false
   await reset()
+  if (route.query.keyword) { filters.keyword = String(route.query.keyword); await search() }
   if (route.query.alarmId && !error.value) openReview({ id: route.query.alarmId })
 }, { immediate: true })
+// 支持从案件流转页跳转进来时按报警单号预填关键字并定位;路由复用(query 变化)时同样处理
+watch(() => route.fullPath, (nv, ov) => {
+  if (nv === ov) return
+  if (route.query?.keyword) filters.keyword = String(route.query.keyword)
+  search()
+})
 onBeforeRouteLeave(canLeave)
 onBeforeRouteUpdate(canLeave)
 onBeforeUnmount(() => { ++listRequest; ++detailRequest; ++historyRequest })
 </script>
 
 <style scoped lang="scss">
-.order-container{padding:20px;background:#f5f7fa;min-height:calc(100vh - 84px)}.search-card{margin-bottom:20px;border-radius:8px}.search-form{display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;margin:0}.search-form :deep(.el-form-item){margin:0}.search-form :deep(.el-input){width:220px}.search-form :deep(.el-select){width:160px}.search-form :deep(.el-date-editor){width:300px}.search-card :deep(.el-card__body){padding:16px 20px}.table-card{padding:15px 20px 20px;background:#fff;border-radius:8px}.pagination{margin-top:20px;justify-content:flex-end}.tab-count{margin-left:8px;background:#f0f2f5;padding:1px 7px;border-radius:4px;font-size:12px}.load-error{margin-bottom:16px}.review-choices{display:flex;flex-wrap:wrap;gap:12px}.review-choices .el-radio{margin-right:0;min-width:160px}.attachments{width:100%;padding:14px 16px;background:#fafbfc;border:1px dashed #dcdfe6;border-radius:6px}:deep(.el-tabs__header){margin-bottom:18px}:deep(.el-table .cell){line-height:1.7}@media(max-width:900px){.order-container{padding:12px}.pagination{overflow:auto;justify-content:flex-start}}
+.order-container{padding:20px;background:#f5f7fa;min-height:calc(100vh - 84px)}.search-card{margin-bottom:20px;border-radius:8px}.search-form{display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;margin:0}.search-form :deep(.el-form-item){margin:0}.search-form :deep(.el-input){width:220px}.search-form :deep(.el-select){width:160px}.search-form :deep(.el-date-editor){width:300px}.search-card :deep(.el-card__body){padding:16px 20px}.table-card{padding:15px 20px 20px;background:#fff;border-radius:8px}.pagination{margin-top:20px;justify-content:flex-end}.tab-count{margin-left:8px;background:#f0f2f5;padding:1px 7px;border-radius:4px;font-size:12px}.load-error{margin-bottom:16px}.review-choices{display:flex;flex-wrap:wrap;gap:12px}.review-choices .el-radio{margin-right:0;min-width:160px}.attachments{width:100%;padding:14px 16px;background:#fafbfc;border:1px dashed #dcdfe6;border-radius:6px}:deep(.hide .el-upload--picture-card){display:none}:deep(.el-tabs__header){margin-bottom:18px}:deep(.el-table .cell){line-height:1.7}@media(max-width:900px){.order-container{padding:12px}.pagination{overflow:auto;justify-content:flex-start}}
 </style>

+ 674 - 242
src/views/subSystem/manholeCover/caseHandling/circulation.vue

@@ -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>