|
|
@@ -1,462 +1,303 @@
|
|
|
<!--案件流转管理-->
|
|
|
<template>
|
|
|
- <div class="case-management">
|
|
|
- <!-- 页面头部 -->
|
|
|
- <div class="page-header">
|
|
|
- <div class="header-stats">
|
|
|
- <div class="stat" @click="filterStatus('')">
|
|
|
- <div class="num">{{ totalCases }}</div>
|
|
|
+ <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="stat" @click="filterStatus('pending')">
|
|
|
- <div class="num warning">{{ pendingCount }}</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="stat" @click="filterStatus('processing')">
|
|
|
- <div class="num">{{ processingCount }}</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="stat" @click="filterStatus('completed')">
|
|
|
- <div class="num">{{ completedCount }}</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>
|
|
|
</div>
|
|
|
|
|
|
- <div class="main-layout">
|
|
|
- <!-- 左侧案件列表 -->
|
|
|
- <div class="case-list-panel">
|
|
|
- <div class="list-header">
|
|
|
+ <div class="flex gap-5">
|
|
|
+ <div class="w-[380px] flex flex-col overflow-hidden rounded-[20px] bg-white" v-loading="loading">
|
|
|
+ <div class="flex justify-between border-b border-[#e9ecef] px-4 py-4 font-600">
|
|
|
<span>案件列表</span>
|
|
|
- <el-input v-model="searchKeyword" placeholder="搜索案件编号/位置" prefix-icon="Search" clearable size="small" style="width: 160px" />
|
|
|
+ <el-input v-model="searchKeyword" placeholder="搜索案件编号/设备位置" :prefix-icon="Search" clearable size="small" class="w-[180px]" @keyup.enter="loadCases" />
|
|
|
</div>
|
|
|
- <div class="case-list">
|
|
|
+ <div class="flex-1 overflow-y-auto p-3">
|
|
|
<div
|
|
|
- v-for="caseItem in filteredCases"
|
|
|
- :key="caseItem.id"
|
|
|
- class="case-item"
|
|
|
- :class="{ active: selectedCase?.id === caseItem.id, [caseItem.status]: true }"
|
|
|
- @click="selectCase(caseItem)"
|
|
|
+ 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)"
|
|
|
>
|
|
|
<div class="case-level">
|
|
|
- <el-tag :type="caseItem.level === 'critical' ? 'danger' : 'warning'" size="small">
|
|
|
- {{ caseItem.level === 'critical' ? '严重' : '一般' }}
|
|
|
- </el-tag>
|
|
|
+ <el-tag :type="caseItem.levelTag" size="small">{{ caseItem.levelText }}</el-tag>
|
|
|
</div>
|
|
|
- <div class="case-info">
|
|
|
- <div class="case-title">{{ caseItem.code }}</div>
|
|
|
- <div class="case-location"><el-icon><Location /></el-icon> {{ caseItem.locationShort }}</div>
|
|
|
- <div class="case-time">{{ caseItem.alertTime }}</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>
|
|
|
<div class="case-status">
|
|
|
- <el-tag :type="getStatusType(caseItem.status)" size="small">{{ getStatusText(caseItem.status) }}</el-tag>
|
|
|
+ <el-tag :type="caseItem.statusTag" size="small">{{ caseItem.statusText }}</el-tag>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <el-empty v-if="!paginatedCases.length" description="暂无案件数据" :image-size="80" />
|
|
|
</div>
|
|
|
- <div class="pagination-area">
|
|
|
+ <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>
|
|
|
|
|
|
- <!-- 右侧案件详情与核查操作区 -->
|
|
|
- <div class="detail-panel" v-if="selectedCase">
|
|
|
- <!-- 基本信息与地理位置双栏 -->
|
|
|
- <div class="detail-two-col">
|
|
|
- <div class="info-section">
|
|
|
- <div class="section-title">
|
|
|
+ <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 class="task-role">
|
|
|
- <el-select v-model="selectedRole" placeholder="指定督办角色" size="small" style="width: 140px">
|
|
|
- <el-option label="网格巡查员" value="inspector" />
|
|
|
- <el-option label="维修班组" value="repair" />
|
|
|
- <el-option label="应急中心" value="emergency" />
|
|
|
- </el-select>
|
|
|
- <el-button type="primary" size="small" @click="assignTask">指派督办</el-button>
|
|
|
- </div>
|
|
|
</div>
|
|
|
<el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="案件编号">{{ selectedCase.code }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警级别">
|
|
|
- <el-tag :type="selectedCase.level === 'critical' ? 'danger' : 'warning'">{{ selectedCase.level === 'critical' ? '严重报警' : '一般报警' }}</el-tag>
|
|
|
+ <el-descriptions-item label="案件ID">{{ selectedCase.caseId || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="案件编号">{{ selectedCase.caseNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="报警ID">{{ selectedCase.alarmId || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="案件等级">
|
|
|
+ <el-tag :type="selectedCase.levelTag">{{ selectedCase.levelText }}</el-tag>
|
|
|
</el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警描述">{{ selectedCase.alertDesc }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="报警时间">{{ selectedCase.alertTime }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="当前状态">{{ getStatusText(selectedCase.status) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="指定督办角色">{{ selectedCase.assignRole || '未指定' }}</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="立案人ID">{{ selectedCase.fileUser || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="立案时间">{{ selectedCase.createTimeText }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="核查人ID">{{ selectedCase.verifyUser || '—' }}</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-item label="更新时间">{{ selectedCase.updateTimeText }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</div>
|
|
|
- <div class="location-section">
|
|
|
- <div class="section-title"><el-icon><MapLocation /></el-icon> 地理位置</div>
|
|
|
- <div class="map-mini">
|
|
|
- <div class="mock-map">
|
|
|
- <el-icon :size="36" color="#f56c6c"><Location /></el-icon>
|
|
|
- <div>{{ selectedCase.address }}</div>
|
|
|
- <div class="coords">经度: {{ selectedCase.lng }}, 纬度: {{ selectedCase.lat }}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <!-- 井盖设备信息 -->
|
|
|
- <div class="device-info">
|
|
|
- <div class="sub-title"><el-icon><Monitor /></el-icon> 井盖设备信息</div>
|
|
|
- <el-descriptions :column="2" size="small" border>
|
|
|
- <el-descriptions-item label="设备编号">{{ selectedCase.deviceId }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备类型">{{ selectedCase.deviceType }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="设备型号">{{ selectedCase.deviceModel }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="安装日期">{{ selectedCase.installDate }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="当前状态">{{ selectedCase.deviceStatus }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="最近通讯">{{ selectedCase.lastComm }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 报警多媒体信息 -->
|
|
|
- <div class="media-section">
|
|
|
- <div class="section-title"><el-icon><Picture /></el-icon> 报警多媒体信息</div>
|
|
|
- <div class="media-gallery">
|
|
|
- <el-image v-for="(img, idx) in selectedCase.mediaImages" :key="idx" :src="img" fit="cover" class="media-img" />
|
|
|
- <div v-if="selectedCase.videoUrl" class="video-preview" @click="playVideo">▶ 查看现场视频</div>
|
|
|
- <div v-if="!selectedCase.mediaImages?.length && !selectedCase.videoUrl" class="no-media">暂无多媒体附件</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="设备ID">{{ selectedCase.deviceId || '—' }}</el-descriptions-item>
|
|
|
+ <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-item label="状态更新时间">{{ selectedCase.statusUpdateTimeText }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 办理经过 & 处置信息时间轴 -->
|
|
|
- <div class="process-section">
|
|
|
- <div class="section-title"><el-icon><Document /></el-icon> 办理经过 & 处置信息</div>
|
|
|
- <el-timeline>
|
|
|
- <el-timeline-item v-for="process in selectedCase.processLogs" :key="process.id" :timestamp="process.time" :type="process.type">
|
|
|
- {{ process.content }}
|
|
|
- <span v-if="process.operator" class="operator">操作人: {{ process.operator }}</span>
|
|
|
- </el-timeline-item>
|
|
|
- </el-timeline>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 核查操作栏 (任务督办核心) -->
|
|
|
- <div class="action-bar">
|
|
|
- <div class="section-title"><el-icon><Operation /></el-icon> 案件处置操作</div>
|
|
|
- <div class="action-buttons" v-if="selectedCase.status !== 'completed'">
|
|
|
- <el-button type="primary" @click="startVerification" :disabled="selectedCase.status !== 'pending'">核查</el-button>
|
|
|
- <el-button type="success" @click="completeCase" :disabled="selectedCase.status === 'pending'">办结</el-button>
|
|
|
- <el-button type="warning" @click="redispatchCase" :disabled="selectedCase.status === 'pending'">再次派遣</el-button>
|
|
|
- <el-button @click="viewTaskDetail">查看处理详情</el-button>
|
|
|
+ <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>
|
|
|
- <div v-else class="completed-info">案件已于 {{ selectedCase.completeTime }} 办结,全程督办记录可查。</div>
|
|
|
+ <el-empty v-else description="暂无案件附件" :image-size="80" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 未选中案件的空状态 -->
|
|
|
- <div v-else class="empty-panel">
|
|
|
- <el-empty description="请从左侧选择案件进行核查与督办" :image-size="140" />
|
|
|
+ <div v-else class="flex flex-1 items-center justify-center rounded-[20px] bg-white">
|
|
|
+ <el-empty description="请从左侧选择案件进行核查" :image-size="140" />
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 处理详情弹窗 -->
|
|
|
- <el-dialog v-model="detailDialogVisible" title="任务处理详情 - 全程督办记录" width="600px">
|
|
|
- <el-timeline>
|
|
|
- <el-timeline-item v-for="log in selectedCase?.processLogs" :key="log.id" :timestamp="log.time" :type="log.type">
|
|
|
- {{ log.content }} <span v-if="log.operator" class="operator-dialog">【{{ log.operator }}】</span>
|
|
|
- </el-timeline-item>
|
|
|
- </el-timeline>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="detailDialogVisible = false">关闭</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <!-- 再次派遣对话框 -->
|
|
|
- <el-dialog v-model="redispatchDialogVisible" title="再次派遣" width="400px">
|
|
|
- <el-form>
|
|
|
- <el-form-item label="指派角色">
|
|
|
- <el-select v-model="newAssignRole" placeholder="选择角色">
|
|
|
- <el-option label="网格巡查员" value="inspector" />
|
|
|
- <el-option label="维修班组" value="repair" />
|
|
|
- <el-option label="应急中心" value="emergency" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="派遣备注">
|
|
|
- <el-input type="textarea" v-model="dispatchRemark" placeholder="请输入派遣说明" />
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="redispatchDialogVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" @click="confirmRedispatch">确认派遣</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed } from 'vue'
|
|
|
-import { Tickets, Location, InfoFilled, MapLocation, Monitor, Picture, Document, Operation, Search } from '@element-plus/icons-vue'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-
|
|
|
-// 模拟案件数据
|
|
|
-const mockCases = [
|
|
|
- {
|
|
|
- id: 1, code: 'CASE-20250402-001', level: 'critical', alertDesc: '井盖倾斜角度14.2°,超过阈值8°,有跌落风险', alertTime: '2025-04-02 14:23:15',
|
|
|
- locationShort: '人民路与解放路口', address: '人民路与解放路口东50m', lng: 121.487, lat: 31.249,
|
|
|
- deviceId: 'MN-1001', deviceType: '智能井盖监测仪', deviceModel: 'WATCHMAN-S2', installDate: '2024-03-15', deviceStatus: '倾斜报警', lastComm: '2025-04-02 14:20',
|
|
|
- mediaImages: ['https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'],
|
|
|
- videoUrl: null,
|
|
|
- status: 'pending', // pending, processing, completed
|
|
|
- assignRole: null,
|
|
|
- processLogs: [
|
|
|
- { id: 1, time: '2025-04-02 14:23:20', content: '系统自动生成报警案件', type: 'danger', operator: '系统' }
|
|
|
- ],
|
|
|
- completeTime: null
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2, code: 'CASE-20250402-002', level: 'critical', alertDesc: '井下水位62cm,超过阈值40cm,溢水风险', alertTime: '2025-04-02 13:58:12',
|
|
|
- locationShort: '滨江路化工园区', address: '滨江路化工园区南门', lng: 121.502, lat: 31.235,
|
|
|
- deviceId: 'MN-1002', deviceType: '防爆井盖监测仪', deviceModel: 'WATCHMAN-EX', installDate: '2024-06-20', deviceStatus: '水位超限', lastComm: '2025-04-02 13:55',
|
|
|
- mediaImages: [],
|
|
|
- videoUrl: 'demo.mp4',
|
|
|
- status: 'processing',
|
|
|
- assignRole: 'repair',
|
|
|
- processLogs: [
|
|
|
- { id: 1, time: '2025-04-02 13:58:20', content: '系统自动生成报警案件', type: 'danger', operator: '系统' },
|
|
|
- { id: 2, time: '2025-04-02 14:10:00', content: '已指派维修班组现场勘查', type: 'warning', operator: '调度中心-李工' },
|
|
|
- { id: 3, time: '2025-04-02 14:30:15', content: '维修人员已到达现场,开始排水作业', type: 'info', operator: '维修班组-张三' }
|
|
|
- ],
|
|
|
- completeTime: null
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3, code: 'CASE-20250402-003', level: 'warning', alertDesc: '异常震动112mg,超过阈值80mg,疑似外力冲击', alertTime: '2025-04-02 12:10:33',
|
|
|
- locationShort: '古城街北段', address: '古城街北段树下', lng: 121.478, lat: 31.258,
|
|
|
- deviceId: 'MN-1003', deviceType: '普通井盖监测仪', deviceModel: 'WATCHMAN-Basic', installDate: '2023-11-10', deviceStatus: '震动报警', lastComm: '2025-04-02 12:08',
|
|
|
- mediaImages: ['https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg'],
|
|
|
- videoUrl: null,
|
|
|
- status: 'completed',
|
|
|
- assignRole: 'inspector',
|
|
|
- processLogs: [
|
|
|
- { id: 1, time: '2025-04-02 12:10:40', content: '系统自动生成报警案件', type: 'danger', operator: '系统' },
|
|
|
- { id: 2, time: '2025-04-02 12:35:00', content: '网格员现场核查,确认为重型车辆碾轧导致,井盖轻微移位', type: 'primary', operator: '巡查员-王明' },
|
|
|
- { id: 3, time: '2025-04-02 13:20:00', content: '维修人员复位井盖,异常消除', type: 'success', operator: '维修班组' },
|
|
|
- { id: 4, time: '2025-04-02 14:00:00', content: '案件办结', type: 'success', operator: '系统' }
|
|
|
- ],
|
|
|
- completeTime: '2025-04-02 14:00:00'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4, code: 'CASE-20250402-004', level: 'warning', alertDesc: '井盖异常开启,非正常打开', alertTime: '2025-04-02 11:45:20',
|
|
|
- locationShort: '南港路旧车站', address: '南港路旧车站段', lng: 121.495, lat: 31.228,
|
|
|
- deviceId: 'MN-1005', deviceType: '智能井盖监测仪', deviceModel: 'WATCHMAN-S2', installDate: '2024-01-18', deviceStatus: '开启报警', lastComm: '2025-04-02 11:44',
|
|
|
- mediaImages: [],
|
|
|
- videoUrl: null,
|
|
|
- status: 'pending',
|
|
|
- assignRole: null,
|
|
|
- processLogs: [{ id: 1, time: '2025-04-02 11:45:25', content: '系统自动生成报警案件', type: 'danger', operator: '系统' }],
|
|
|
- completeTime: null
|
|
|
- }
|
|
|
-]
|
|
|
+import {computed, onMounted, ref} from 'vue'
|
|
|
+import {InfoFilled, Location, Monitor, Picture, Search} from '@element-plus/icons-vue'
|
|
|
+import {getManholeCaseDetail, getManholeCasePage} from '@/api/pipeNetwork/basic'
|
|
|
|
|
|
+const cases = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+const detailLoading = ref(false)
|
|
|
const searchKeyword = ref('')
|
|
|
const statusFilter = ref('')
|
|
|
const currentPage = ref(1)
|
|
|
const pageSize = 5
|
|
|
-const selectedCase = ref(mockCases[0])
|
|
|
-const selectedRole = ref('')
|
|
|
-const detailDialogVisible = ref(false)
|
|
|
-const redispatchDialogVisible = ref(false)
|
|
|
-const newAssignRole = ref('')
|
|
|
-const dispatchRemark = ref('')
|
|
|
+const selectedCase = ref(null)
|
|
|
|
|
|
-const filteredCases = computed(() => {
|
|
|
- let list = mockCases
|
|
|
- if (statusFilter.value) list = list.filter(c => c.status === statusFilter.value)
|
|
|
- if (searchKeyword.value) {
|
|
|
- const kw = searchKeyword.value.toLowerCase()
|
|
|
- list = list.filter(c => c.code.toLowerCase().includes(kw) || c.locationShort.toLowerCase().includes(kw))
|
|
|
- }
|
|
|
- return list
|
|
|
-})
|
|
|
+function formatDateTime(value) {
|
|
|
+ if (!value) return '—'
|
|
|
+ return String(value).replace('T', ' ')
|
|
|
+}
|
|
|
|
|
|
-const totalCases = computed(() => mockCases.length)
|
|
|
-const pendingCount = computed(() => mockCases.filter(c => c.status === 'pending').length)
|
|
|
-const processingCount = computed(() => mockCases.filter(c => c.status === 'processing').length)
|
|
|
-const completedCount = computed(() => mockCases.filter(c => c.status === 'completed').length)
|
|
|
+function getCaseLevelMeta(level) {
|
|
|
+ const map = {
|
|
|
+ 1: { text: '严重', tag: 'danger' },
|
|
|
+ 2: { text: '一般', tag: 'warning' },
|
|
|
+ 3: { text: '轻微', tag: 'info' }
|
|
|
+ }
|
|
|
+ return map[Number(level)] || { text: '—', tag: 'info' }
|
|
|
+}
|
|
|
|
|
|
-const filterStatus = (status) => { statusFilter.value = status; currentPage.value = 1 }
|
|
|
-const selectCase = (caseItem) => { selectedCase.value = caseItem; selectedRole.value = caseItem.assignRole || '' }
|
|
|
-const getStatusType = (status) => {
|
|
|
- if (status === 'pending') return 'danger'
|
|
|
- if (status === 'processing') return 'warning'
|
|
|
- return 'success'
|
|
|
+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' }
|
|
|
}
|
|
|
-const getStatusText = (status) => {
|
|
|
- if (status === 'pending') return '待核查'
|
|
|
- if (status === 'processing') return '办理中'
|
|
|
- return '已办结'
|
|
|
+
|
|
|
+function getCaseSourceText(source) {
|
|
|
+ const map = {
|
|
|
+ 1: '系统自动报警',
|
|
|
+ 2: '运维巡检上报',
|
|
|
+ 3: '人工投诉上报',
|
|
|
+ 4: '平台录入'
|
|
|
+ }
|
|
|
+ return map[Number(source)] || '—'
|
|
|
}
|
|
|
|
|
|
-// 核查操作
|
|
|
-const startVerification = () => {
|
|
|
- if (selectedCase.value.status !== 'pending') return
|
|
|
- selectedCase.value.status = 'processing'
|
|
|
- selectedCase.value.processLogs.push({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleString(),
|
|
|
- content: '开始核查,已指派人员现场核实',
|
|
|
- type: 'primary',
|
|
|
- operator: '核查中心'
|
|
|
- })
|
|
|
- ElMessage.success('已启动核查,案件状态更新为办理中')
|
|
|
+function splitAttachUrls(value) {
|
|
|
+ if (!value) return []
|
|
|
+ return String(value)
|
|
|
+ .split(/[,\n;]/)
|
|
|
+ .map(item => item.trim())
|
|
|
+ .filter(Boolean)
|
|
|
}
|
|
|
|
|
|
-// 办结操作
|
|
|
-const completeCase = () => {
|
|
|
- if (selectedCase.value.status === 'completed') return
|
|
|
- selectedCase.value.status = 'completed'
|
|
|
- selectedCase.value.completeTime = new Date().toLocaleString()
|
|
|
- selectedCase.value.processLogs.push({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleString(),
|
|
|
- content: '案件办结,已完成处置并验收',
|
|
|
- type: 'success',
|
|
|
- operator: '督办员'
|
|
|
- })
|
|
|
- ElMessage.success('案件已办结')
|
|
|
+function getOnlineStatusText(status) {
|
|
|
+ const map = {
|
|
|
+ 0: '离线',
|
|
|
+ 1: '在线'
|
|
|
+ }
|
|
|
+ return map[Number(status)] || '—'
|
|
|
}
|
|
|
|
|
|
-// 再次派遣
|
|
|
-const redispatchCase = () => {
|
|
|
- redispatchDialogVisible.value = true
|
|
|
+function getAlarmStatusText(status) {
|
|
|
+ const map = {
|
|
|
+ 0: '正常',
|
|
|
+ 1: '报警'
|
|
|
+ }
|
|
|
+ return map[Number(status)] || '—'
|
|
|
}
|
|
|
-const confirmRedispatch = () => {
|
|
|
- if (newAssignRole.value) {
|
|
|
- selectedCase.value.assignRole = newAssignRole.value
|
|
|
- selectedCase.value.processLogs.push({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleString(),
|
|
|
- content: `再次派遣至角色: ${newAssignRole.value === 'inspector' ? '网格巡查员' : newAssignRole.value === 'repair' ? '维修班组' : '应急中心'},备注: ${dispatchRemark.value || '无'}`,
|
|
|
- type: 'warning',
|
|
|
- operator: '督办中心'
|
|
|
- })
|
|
|
- ElMessage.success('再次派遣成功,已通知对应角色')
|
|
|
- } else {
|
|
|
- ElMessage.warning('请选择指派角色')
|
|
|
+
|
|
|
+function getCurrentStatusText(status) {
|
|
|
+ const map = {
|
|
|
+ 1: '在用',
|
|
|
+ 2: '闲置',
|
|
|
+ 3: '维修',
|
|
|
+ 4: '报废',
|
|
|
+ 5: '待入库'
|
|
|
}
|
|
|
- redispatchDialogVisible.value = false
|
|
|
- newAssignRole.value = ''
|
|
|
- dispatchRemark.value = ''
|
|
|
+ return map[Number(status)] || '—'
|
|
|
}
|
|
|
|
|
|
-const assignTask = () => {
|
|
|
- if (!selectedRole.value) {
|
|
|
- ElMessage.warning('请选择督办角色')
|
|
|
- return
|
|
|
+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)
|
|
|
}
|
|
|
- selectedCase.value.assignRole = selectedRole.value
|
|
|
- selectedCase.value.processLogs.push({
|
|
|
- id: Date.now(),
|
|
|
- time: new Date().toLocaleString(),
|
|
|
- content: `已指派角色 ${selectedRole.value === 'inspector' ? '网格巡查员' : selectedRole.value === 'repair' ? '维修班组' : '应急中心'} 进行全程督办`,
|
|
|
- type: 'primary',
|
|
|
- operator: '系统'
|
|
|
- })
|
|
|
- ElMessage.success(`已指定${selectedRole.value}角色进行任务督办`)
|
|
|
}
|
|
|
|
|
|
-const viewTaskDetail = () => { detailDialogVisible.value = true }
|
|
|
-const playVideo = () => { ElMessage.info('视频播放功能演示') }
|
|
|
+const filteredCases = computed(() => {
|
|
|
+ if (!statusFilter.value) return cases.value
|
|
|
+ return cases.value.filter(item => item.statusKey === statusFilter.value)
|
|
|
+})
|
|
|
|
|
|
-</script>
|
|
|
+const paginatedCases = computed(() => {
|
|
|
+ const start = (currentPage.value - 1) * pageSize
|
|
|
+ return filteredCases.value.slice(start, start + pageSize)
|
|
|
+})
|
|
|
|
|
|
-<style scoped>
|
|
|
-.case-management {
|
|
|
- background: #f0f2f6;
|
|
|
- min-height: 100vh;
|
|
|
- padding: 20px;
|
|
|
-}
|
|
|
-.page-header {
|
|
|
- background: white;
|
|
|
- border-radius: 20px;
|
|
|
- padding: 16px 24px;
|
|
|
- margin-bottom: 20px;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-.header-title { display: flex; align-items: center; gap: 14px; font-size: 20px; font-weight: 600; }
|
|
|
-.header-stats { display: flex; gap: 24px; }
|
|
|
-.stat { text-align: center; cursor: pointer; padding: 4px 16px; border-radius: 30px; }
|
|
|
-.stat:hover { background: #ecf5ff; }
|
|
|
-.num { font-size: 26px; font-weight: bold; }
|
|
|
-.num.warning { color: #e6a23c; }
|
|
|
-.main-layout { display: flex; gap: 20px; }
|
|
|
-.case-list-panel {
|
|
|
- width: 380px;
|
|
|
- background: white;
|
|
|
- border-radius: 20px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-.list-header {
|
|
|
- padding: 16px;
|
|
|
- border-bottom: 1px solid #e9ecef;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-.case-list { flex: 1; overflow-y: auto; padding: 12px; }
|
|
|
-.case-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- padding: 12px;
|
|
|
- margin-bottom: 8px;
|
|
|
- border-radius: 14px;
|
|
|
- background: #fafbfc;
|
|
|
- cursor: pointer;
|
|
|
- border: 1px solid transparent;
|
|
|
-}
|
|
|
-.case-item.active { background: #ecf5ff; border-color: #409eff; }
|
|
|
-.case-item.pending { border-left: 3px solid #f56c6c; }
|
|
|
-.case-item.processing { border-left: 3px solid #e6a23c; }
|
|
|
-.case-info { flex: 1; }
|
|
|
-.case-title { font-weight: 600; margin-bottom: 4px; }
|
|
|
-.case-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; }
|
|
|
-.case-time { font-size: 11px; color: #aaa; }
|
|
|
-.detail-panel {
|
|
|
- flex: 1;
|
|
|
- background: white;
|
|
|
- border-radius: 20px;
|
|
|
- padding: 20px;
|
|
|
- overflow-y: auto;
|
|
|
- max-height: calc(100vh - 140px);
|
|
|
-}
|
|
|
-.detail-two-col { display: flex; gap: 20px; margin-bottom: 20px; }
|
|
|
-.info-section { flex: 1; }
|
|
|
-.location-section { flex: 1; }
|
|
|
-.section-title {
|
|
|
- font-weight: 600;
|
|
|
- font-size: 16px;
|
|
|
- margin-bottom: 14px;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
+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)
|
|
|
+
|
|
|
+async function loadCases() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const keyword = searchKeyword.value.trim()
|
|
|
+ const res = await getManholeCasePage({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 200,
|
|
|
+ caseNo: keyword || undefined,
|
|
|
+ equipmentLocation: keyword || undefined
|
|
|
+ })
|
|
|
+ const records = Array.isArray(res?.data?.records) ? res.data.records : []
|
|
|
+ cases.value = records.map(item => normalizeCase(item))
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await selectCase(first)
|
|
|
+ } catch (error) {
|
|
|
+ cases.value = []
|
|
|
+ selectedCase.value = null
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
-.mock-map {
|
|
|
- background: #e9f5e9;
|
|
|
- border-radius: 16px;
|
|
|
- height: 130px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-bottom: 16px;
|
|
|
+
|
|
|
+async function selectCase(caseItem) {
|
|
|
+ detailLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getManholeCaseDetail(caseItem.caseId)
|
|
|
+ const detail = res?.data || caseItem
|
|
|
+ selectedCase.value = normalizeCase(detail)
|
|
|
+ } catch (error) {
|
|
|
+ selectedCase.value = caseItem
|
|
|
+ } finally {
|
|
|
+ detailLoading.value = false
|
|
|
+ }
|
|
|
}
|
|
|
-.coords { font-size: 11px; color: #666; margin-top: 4px; }
|
|
|
-.device-info { margin-top: 12px; }
|
|
|
-.sub-title { font-weight: 500; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
|
|
-.media-section, .process-section, .action-bar {
|
|
|
- margin-top: 24px;
|
|
|
- border-top: 1px solid #e9ecef;
|
|
|
- padding-top: 16px;
|
|
|
+
|
|
|
+function filterStatus(status) {
|
|
|
+ statusFilter.value = status
|
|
|
+ currentPage.value = 1
|
|
|
+ const first = filteredCases.value[0]
|
|
|
+ if (first) {
|
|
|
+ selectCase(first)
|
|
|
+ } else {
|
|
|
+ selectedCase.value = null
|
|
|
+ }
|
|
|
}
|
|
|
-.media-gallery { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
|
-.media-img { width: 100px; height: 80px; border-radius: 8px; object-fit: cover; cursor: pointer; }
|
|
|
-.video-preview { background: #f0f0f0; padding: 8px 16px; border-radius: 20px; cursor: pointer; display: inline-block; }
|
|
|
-.action-buttons { display: flex; gap: 12px; margin-top: 12px; }
|
|
|
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
|
|
|
-.operator { font-size: 12px; color: #909399; margin-left: 12px; }
|
|
|
-.pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
|
|
|
-</style>
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadCases()
|
|
|
+})
|
|
|
+</script>
|