Selaa lähdekoodia

feat:应急指挥管理

null 1 viikko sitten
vanhempi
commit
c56884ed22

+ 199 - 0
src/api/dispatch.js

@@ -0,0 +1,199 @@
+import request from '@/utils/request'
+
+// ============ 接报管理 ============
+export function getEventReportPage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/report/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getEventReportById(id) {
+  return request({ url: '/api/dispatch/report/getById/' + id, method: 'get' })
+}
+export function addEventReport(data) {
+  return request({ url: '/api/dispatch/report/save', method: 'post', data })
+}
+export function updateEventReport(data) {
+  return request({ url: '/api/dispatch/report/updateById', method: 'put', data })
+}
+export function deleteEventReport(id) {
+  return request({ url: '/api/dispatch/report/deleteById', method: 'delete', params: { id } })
+}
+export function acceptEventReport(id) {
+  return request({ url: '/api/dispatch/report/accept/' + id, method: 'put' })
+}
+export function rejectEventReport(id, remark) {
+  return request({ url: '/api/dispatch/report/reject/' + id, method: 'put', params: { remark } })
+}
+
+// ============ 初判核实 ============
+export function getEventVerifyPage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/verify/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getEventVerifyById(id) {
+  return request({ url: '/api/dispatch/verify/getById/' + id, method: 'get' })
+}
+export function addEventVerify(data) {
+  return request({ url: '/api/dispatch/verify/save', method: 'post', data })
+}
+export function updateEventVerify(data) {
+  return request({ url: '/api/dispatch/verify/updateById', method: 'put', data })
+}
+export function deleteEventVerify(id) {
+  return request({ url: '/api/dispatch/verify/deleteById', method: 'delete', params: { id } })
+}
+export function verifyEvent(data) {
+  return request({ url: '/api/dispatch/verify/verify', method: 'put', data })
+}
+
+// ============ 转接报送 ============
+export function getEventTransferPage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/transfer/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getEventTransferById(id) {
+  return request({ url: '/api/dispatch/transfer/getById/' + id, method: 'get' })
+}
+export function addEventTransfer(data) {
+  return request({ url: '/api/dispatch/transfer/save', method: 'post', data })
+}
+export function updateEventTransfer(data) {
+  return request({ url: '/api/dispatch/transfer/updateById', method: 'put', data })
+}
+export function deleteEventTransfer(id) {
+  return request({ url: '/api/dispatch/transfer/deleteById', method: 'delete', params: { id } })
+}
+export function doTransfer(data) {
+  return request({ url: '/api/dispatch/transfer/doTransfer', method: 'put', data })
+}
+export function doEventReport(data) {
+  return request({ url: '/api/dispatch/transfer/doReport', method: 'put', data })
+}
+export function getTransferLogs(transferId) {
+  return request({ url: '/api/dispatch/transfer/getTransferLogs/' + transferId, method: 'get' })
+}
+
+// ============ 工作催办 ============
+export function getEventUrgePage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/urge/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getEventUrgeById(id) {
+  return request({ url: '/api/dispatch/urge/getById/' + id, method: 'get' })
+}
+export function addEventUrge(data) {
+  return request({ url: '/api/dispatch/urge/save', method: 'post', data })
+}
+export function updateEventUrge(data) {
+  return request({ url: '/api/dispatch/urge/updateById', method: 'put', data })
+}
+export function deleteEventUrge(id) {
+  return request({ url: '/api/dispatch/urge/deleteById', method: 'delete', params: { id } })
+}
+export function doUrge(data) {
+  return request({ url: '/api/dispatch/urge/doUrge', method: 'post', data })
+}
+export function closeUrge(id) {
+  return request({ url: '/api/dispatch/urge/closeUrge/' + id, method: 'put' })
+}
+export function getUrgeDetails(urgeId) {
+  return request({ url: '/api/dispatch/urge/getUrgeDetails/' + urgeId, method: 'get' })
+}
+
+// ============ 跟踪解除 ============
+export function getEventTrackPage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/track/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getEventTrackById(id) {
+  return request({ url: '/api/dispatch/track/getById/' + id, method: 'get' })
+}
+export function addEventTrack(data) {
+  return request({ url: '/api/dispatch/track/save', method: 'post', data })
+}
+export function updateEventTrack(data) {
+  return request({ url: '/api/dispatch/track/updateById', method: 'put', data })
+}
+export function deleteEventTrack(id) {
+  return request({ url: '/api/dispatch/track/deleteById', method: 'delete', params: { id } })
+}
+export function addTrackLog(data) {
+  return request({ url: '/api/dispatch/track/addTrackLog', method: 'post', data })
+}
+export function releaseEvent(id) {
+  return request({ url: '/api/dispatch/track/release/' + id, method: 'put' })
+}
+export function closeEvent(id) {
+  return request({ url: '/api/dispatch/track/close/' + id, method: 'put' })
+}
+export function getTrackLogs(trackId) {
+  return request({ url: '/api/dispatch/track/getTrackLogs/' + trackId, method: 'get' })
+}
+
+// ============ 应急事件分级 ============
+export function getEventGradePage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/grade/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function addEventGrade(data) {
+  return request({ url: '/api/dispatch/grade/save', method: 'post', data })
+}
+export function updateEventGrade(data) {
+  return request({ url: '/api/dispatch/grade/updateById', method: 'put', data })
+}
+export function deleteEventGrade(id) {
+  return request({ url: '/api/dispatch/grade/deleteById', method: 'delete', params: { id } })
+}
+export function getGradesByCategory(category) {
+  return request({ url: '/api/dispatch/grade/listByCategory', method: 'get', params: { category } })
+}
+
+// ============ 应急预案分类 ============
+export function getPlanCategoryTree() {
+  return request({ url: '/api/dispatch/plan-category/getCategoryTree', method: 'get' })
+}
+export function addPlanCategory(data) {
+  return request({ url: '/api/dispatch/plan-category/save', method: 'post', data })
+}
+export function updatePlanCategory(data) {
+  return request({ url: '/api/dispatch/plan-category/updateById', method: 'put', data })
+}
+export function deletePlanCategory(id) {
+  return request({ url: '/api/dispatch/plan-category/deleteById', method: 'delete', params: { id } })
+}
+
+// ============ 应急预案 ============
+export function getEventPlanPage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/plan/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getEventPlanById(id) {
+  return request({ url: '/api/dispatch/plan/getById/' + id, method: 'get' })
+}
+export function addEventPlan(data) {
+  return request({ url: '/api/dispatch/plan/save', method: 'post', data })
+}
+export function updateEventPlan(data) {
+  return request({ url: '/api/dispatch/plan/updateById', method: 'put', data })
+}
+export function deleteEventPlan(id) {
+  return request({ url: '/api/dispatch/plan/deleteById', method: 'delete', params: { id } })
+}
+
+// ============ 应急预案审批 ============
+export function getPlanApprovalPage(pageNum, pageSize, params) {
+  return request({ url: '/api/dispatch/plan-approval/findByPage', method: 'get', params: { pageNum, pageSize, ...params } })
+}
+export function getPlanApprovalById(id) {
+  return request({ url: '/api/dispatch/plan-approval/getById/' + id, method: 'get' })
+}
+export function submitPlanForApproval(id) {
+  return request({ url: '/api/dispatch/plan-approval/submitForApproval/' + id, method: 'put' })
+}
+export function createPlanApproval(planId) {
+  return request({ url: '/api/dispatch/plan-approval/submit/' + planId, method: 'post' })
+}
+export function approvePlan(id) {
+  return request({ url: '/api/dispatch/plan-approval/approve/' + id, method: 'put' })
+}
+export function rejectPlan(id) {
+  return request({ url: '/api/dispatch/plan-approval/reject/' + id, method: 'put' })
+}
+export function publishPlan(id) {
+  return request({ url: '/api/dispatch/plan-approval/publish/' + id, method: 'put' })
+}
+export function getApprovalSteps(approvalId) {
+  return request({ url: '/api/dispatch/plan-approval-step/listByApprovalId', method: 'get', params: { approvalId } })
+}

+ 93 - 66
src/views/subSystem/dispatch/disOnDuty/cphs.vue

@@ -3,19 +3,19 @@
     <!-- 搜索区 -->
     <div class="filter-panel">
       <div class="filter-form">
-        <el-select v-model="filters.category" placeholder="事件类别" clearable style="width:160px">
+        <el-select v-model="searchForm.category" placeholder="事件类别" clearable style="width:160px">
           <el-option label="管道泄漏" value="管道泄漏" />
           <el-option label="塌陷" value="塌陷" />
           <el-option label="积水" value="积水" />
           <el-option label="设备故障" value="设备故障" />
         </el-select>
-        <el-select v-model="filters.level" placeholder="事件等级" clearable style="width:140px">
+        <el-select v-model="searchForm.level" placeholder="事件等级" clearable style="width:140px">
           <el-option label="一般" value="一般" />
           <el-option label="较大" value="较大" />
           <el-option label="重大" value="重大" />
           <el-option label="特别重大" value="特别重大" />
         </el-select>
-        <el-select v-model="filters.verifyStatus" placeholder="核实状态" clearable style="width:140px">
+        <el-select v-model="searchForm.verifyStatus" placeholder="核实状态" clearable style="width:140px">
           <el-option label="待核实" value="待核实" />
           <el-option label="已核实" value="已核实" />
           <el-option label="不实事件" value="不实事件" />
@@ -27,7 +27,7 @@
 
     <!-- 数据表格 -->
     <div class="table-panel">
-      <el-table :data="pagedList" border stripe style="width:100%">
+      <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
         <el-table-column prop="eventNo" label="事件编号" width="140" />
         <el-table-column prop="category" label="事件类别" width="110" />
         <el-table-column prop="level" label="事件等级" width="100">
@@ -43,10 +43,11 @@
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.status }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="160" fixed="right">
+        <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
             <el-button v-if="row.status === '待核实'" type="primary" link size="small" @click="handleVerify(row)">核实</el-button>
             <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -56,7 +57,7 @@
           v-model:page-size="pagination.pageSize"
           :page-sizes="[10, 20, 50]"
           layout="total, sizes, prev, pager, next, jumper"
-          :total="filteredList.length"
+          :total="total"
           @size-change="handleSizeChange"
           @current-change="handleCurrentChange"
         />
@@ -121,11 +122,14 @@
       <div v-if="currentRow" class="detail-content">
         <el-form label-width="100px" size="default">
           <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
+            <el-col :span="8">
+              <el-form-item label="接报编号">{{ currentRow.eventNo }}</el-form-item>
             </el-col>
-            <el-col :span="12">
-              <el-form-item label="事件标识">{{ currentRow.eventId || '未生成' }}</el-form-item>
+            <el-col :span="8">
+              <el-form-item label="事件标识">{{ currentRow.eventId || '待生成' }}</el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="关联接报ID">{{ currentRow.reportId }}</el-form-item>
             </el-col>
           </el-row>
           <el-row :gutter="20">
@@ -171,63 +175,61 @@
 </template>
 
 <script setup name="Cphs">
-import { ref, computed } from 'vue'
-
-// ==================== mock 数据 ====================
-const eventList = ref([
-  { eventNo: 'JB20250613001', category: '塌陷', level: '较大', location: '人民路与建设路交叉口', judgeTime: '2025-06-13 08:35:00', description: '路面出现明显塌陷,面积约2平方米', verifier: '', verifyTime: '', opinion: '', status: '待核实', eventId: '' },
-  { eventNo: 'JB20250613002', category: '设备故障', level: '一般', location: '幸福路123号门前', judgeTime: '2025-06-13 09:15:00', description: '市民反映排水井盖缺失,存在安全隐患', verifier: '', verifyTime: '', opinion: '', status: '待核实', eventId: '' },
-  { eventNo: 'JB20250613003', category: '积水', level: '一般', location: '科技大道中段', judgeTime: '2025-06-13 07:55:00', description: '视频监控发现路面积水严重,影响通行', verifier: '张三', verifyTime: '2025-06-13 08:10:00', opinion: '经核实,积水深度约10cm,因降雨导致', status: '已核实', eventId: 'EVT20250613003' },
-  { eventNo: 'JB20250613004', category: '管道泄漏', level: '重大', location: '和平路步行街', judgeTime: '2025-06-12 16:30:00', description: '市应急办转发:步行街燃气管道疑似泄漏', verifier: '李四', verifyTime: '2025-06-12 17:00:00', opinion: '已现场确认,燃气管道微泄漏,已采取临时措施', status: '已核实', eventId: 'EVT20250613004' },
-  { eventNo: 'JB20250613005', category: '管道泄漏', level: '一般', location: '长江路北侧辅道', judgeTime: '2025-06-13 10:05:00', description: '供水管道破裂,水涌至路面', verifier: '王五', verifyTime: '2025-06-13 10:30:00', opinion: '现场查看后未发现管道破裂,系消防栓放水', status: '不实事件', eventId: '' },
-  { eventNo: 'JB20250613006', category: '积水', level: '较大', location: '黄河路56号小区', judgeTime: '2025-06-13 11:25:00', description: '居民反映小区内污水外溢,气味刺鼻', verifier: '', verifyTime: '', opinion: '', status: '待核实', eventId: '' },
-  { eventNo: 'JB20250613007', category: '积水', level: '一般', location: '太湖路与松江路交叉口', judgeTime: '2025-06-13 06:45:00', description: '早高峰时段路口积水深度约15cm', verifier: '赵六', verifyTime: '2025-06-13 07:00:00', opinion: '积水属实,因排水口堵塞导致', status: '已核实', eventId: 'EVT20250613007' },
-  { eventNo: 'JB20250613008', category: '设备故障', level: '较大', location: '珠江路工业园A区', judgeTime: '2025-06-12 20:30:00', description: '省厅转发:园区内消防设施压力异常', verifier: '', verifyTime: '', opinion: '', status: '待核实', eventId: '' },
-  { eventNo: 'JB20250613009', category: '塌陷', level: '一般', location: '中山路与解放路交叉口', judgeTime: '2025-06-13 13:05:00', description: '路灯杆倾斜,存在倒塌风险', verifier: '', verifyTime: '', opinion: '', status: '待核实', eventId: '' },
-  { eventNo: 'JB20250613010', category: '设备故障', level: '一般', location: '新华路88号大厦', judgeTime: '2025-06-13 14:35:00', description: '大厦物业反映地下室排水泵故障', verifier: '孙七', verifyTime: '2025-06-13 15:00:00', opinion: '排水泵电机烧毁,需更换', status: '已核实', eventId: 'EVT20250613010' }
-])
-
-// ==================== 搜索筛选 ====================
-const filters = ref({ category: '', level: '', verifyStatus: '' })
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getEventVerifyPage, deleteEventVerify, verifyEvent } from '@/api/dispatch'
+
+// ==================== 数据状态 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const searchForm = ref({ category: '', level: '', verifyStatus: '' })
+const pagination = ref({ page: 1, pageSize: 10 })
 
-const filteredList = computed(() => {
-  let list = eventList.value
-  if (filters.value.category) {
-    list = list.filter(o => o.category === filters.value.category)
+// ==================== 加载数据 ====================
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (searchForm.value.category) params.category = searchForm.value.category
+    if (searchForm.value.level) params.level = searchForm.value.level
+    if (searchForm.value.verifyStatus) params.status = searchForm.value.verifyStatus
+    const res = await getEventVerifyPage(pagination.value.page, pagination.value.pageSize, params)
+    tableData.value = res.data?.rows || res.data?.records || res.data || []
+    total.value = res.data?.total || 0
+  } catch (error) {
+    ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
+  } finally {
+    loading.value = false
   }
-  if (filters.value.level) {
-    list = list.filter(o => o.level === filters.value.level)
-  }
-  if (filters.value.verifyStatus) {
-    list = list.filter(o => o.status === filters.value.verifyStatus)
-  }
-  return list
+}
+
+onMounted(() => {
+  loadData()
 })
 
+// ==================== 搜索筛选 ====================
 function handleSearch() {
   pagination.value.page = 1
+  loadData()
 }
 
 function handleReset() {
-  filters.value = { category: '', level: '', verifyStatus: '' }
+  searchForm.value = { category: '', level: '', verifyStatus: '' }
   pagination.value.page = 1
+  loadData()
 }
 
 // ==================== 分页 ====================
-const pagination = ref({ page: 1, pageSize: 10 })
-
-const pagedList = computed(() => {
-  const start = (pagination.value.page - 1) * pagination.value.pageSize
-  return filteredList.value.slice(start, start + pagination.value.pageSize)
-})
-
 function handleSizeChange(val) {
   pagination.value.pageSize = val
   pagination.value.page = 1
+  loadData()
 }
 
 function handleCurrentChange(val) {
   pagination.value.page = val
+  loadData()
 }
 
 // ==================== 标签样式 ====================
@@ -248,6 +250,7 @@ function levelTagType(level) {
 // ==================== 核实 ====================
 const verifyVisible = ref(false)
 const verifyFormRef = ref(null)
+const currentVerifyId = ref(null)
 const verifyForm = ref({ eventNo: '', location: '', description: '', category: '', level: '', opinion: '', isReal: '真实' })
 const verifyRules = {
   category: [{ required: true, message: '请选择事件类别', trigger: 'change' }],
@@ -257,29 +260,53 @@ const verifyRules = {
 }
 
 function handleVerify(row) {
-  verifyForm.value = { eventNo: row.eventNo, location: row.location, description: row.description, category: row.category, level: row.level, opinion: '', isReal: '真实' }
+  currentVerifyId.value = row.id
+  verifyForm.value = {
+    eventNo: row.eventNo,
+    location: row.location,
+    description: row.description,
+    category: row.category,
+    level: row.level,
+    opinion: '',
+    isReal: '真实'
+  }
   verifyVisible.value = true
 }
 
-function confirmVerify() {
-  verifyFormRef.value.validate(valid => {
-    if (!valid) return
-    const idx = eventList.value.findIndex(o => o.eventNo === verifyForm.value.eventNo)
-    if (idx > -1) {
-      eventList.value[idx].category = verifyForm.value.category
-      eventList.value[idx].level = verifyForm.value.level
-      eventList.value[idx].opinion = verifyForm.value.opinion
-      eventList.value[idx].verifier = '当前用户'
-      eventList.value[idx].verifyTime = new Date().toLocaleString()
-      if (verifyForm.value.isReal === '真实') {
-        eventList.value[idx].status = '已核实'
-        eventList.value[idx].eventId = 'EVT' + eventList.value[idx].eventNo.replace('JB', '')
-      } else {
-        eventList.value[idx].status = '不实事件'
-      }
-    }
+async function confirmVerify() {
+  try {
+    await verifyFormRef.value.validate()
+  } catch {
+    return
+  }
+  try {
+    await verifyEvent({
+      id: currentVerifyId.value,
+      category: verifyForm.value.category,
+      level: verifyForm.value.level,
+      opinion: verifyForm.value.opinion,
+      isReal: verifyForm.value.isReal
+    })
+    ElMessage.success('核实成功')
     verifyVisible.value = false
-  })
+    loadData()
+  } catch (error) {
+    ElMessage.error('核实失败:' + (error.message || '未知错误'))
+  }
+}
+
+// ==================== 删除 ====================
+async function handleDelete(row) {
+  try {
+    await ElMessageBox.confirm('确定要删除该核实记录吗?', '删除确认', { type: 'warning' })
+    await deleteEventVerify(row.id)
+    ElMessage.success('删除成功')
+    loadData()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('删除失败:' + (error.message || '未知错误'))
+    }
+  }
 }
 
 // ==================== 详情 ====================

+ 87 - 76
src/views/subSystem/dispatch/disOnDuty/gzcb.vue

@@ -3,13 +3,13 @@
     <!-- 搜索区 -->
     <div class="filter-panel">
       <div class="filter-form">
-        <el-select v-model="filters.urgeStatus" placeholder="催办状态" clearable style="width:140px">
+        <el-select v-model="searchForm.urgeStatus" placeholder="催办状态" clearable style="width:140px">
           <el-option label="待催办" value="待催办" />
           <el-option label="催办中" value="催办中" />
           <el-option label="已完成" value="已完成" />
         </el-select>
-        <el-input v-model="filters.receiver" placeholder="目标接收人" clearable style="width:140px" />
-        <el-input v-model="filters.eventNo" placeholder="事件编号" clearable style="width:160px" />
+        <el-input v-model="searchForm.receiver" placeholder="目标接收人" clearable style="width:140px" />
+        <el-input v-model="searchForm.eventNo" placeholder="事件编号" clearable style="width:160px" />
         <el-button type="primary" @click="handleSearch">搜索</el-button>
         <el-button @click="handleReset">重置</el-button>
       </div>
@@ -17,7 +17,7 @@
 
     <!-- 数据表格 -->
     <div class="table-panel">
-      <el-table :data="pagedList" border stripe style="width:100%">
+      <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
         <el-table-column prop="eventNo" label="事件编号" width="140" />
         <el-table-column prop="summary" label="事件摘要" show-overflow-tooltip />
         <el-table-column prop="receiver" label="目标接收人" width="110" />
@@ -29,10 +29,11 @@
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.status }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="180" fixed="right">
+        <el-table-column label="操作" width="220" fixed="right">
           <template #default="{ row }">
             <el-button v-if="row.status !== '已完成'" type="primary" link size="small" @click="handleUrge(row)">催办</el-button>
             <el-button type="primary" link size="small" @click="handleViewRecords(row)">查看记录</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -42,7 +43,7 @@
           v-model:page-size="pagination.pageSize"
           :page-sizes="[10, 20, 50]"
           layout="total, sizes, prev, pager, next, jumper"
-          :total="filteredList.length"
+          :total="total"
           @size-change="handleSizeChange"
           @current-change="handleCurrentChange"
         />
@@ -96,7 +97,7 @@
       <div v-if="currentRow">
         <p style="margin-bottom:12px;font-weight:600">事件编号:{{ currentRow.eventNo }} | 事件摘要:{{ currentRow.summary }}</p>
         <el-table :data="currentRow.records" border stripe style="width:100%">
-          <el-table-column prop="time" label="催办时间" width="160" />
+          <el-table-column prop="urgeTime" label="催办时间" width="160" />
           <el-table-column prop="method" label="催办方式" width="120" />
           <el-table-column prop="content" label="催办内容" show-overflow-tooltip />
           <el-table-column prop="result" label="结果" width="100">
@@ -114,77 +115,61 @@
 </template>
 
 <script setup name="Gzcb">
-import { ref, computed } from 'vue'
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getEventUrgePage, deleteEventUrge, doUrge, getUrgeDetails } from '@/api/dispatch'
 
-// ==================== mock 数据 ====================
-const eventList = ref([
-  { eventNo: 'EVT20250613003', summary: '科技大道中段路面积水严重', receiver: '张三', receiverDept: '排水管理部', receiverPhone: '13800138001', urgeCount: 0, lastUrgeTime: '-', status: '待催办', records: [] },
-  { eventNo: 'EVT20250613004', summary: '和平路步行街燃气管道疑似泄漏', receiver: '李四', receiverDept: '市政部门', receiverPhone: '13900139002', urgeCount: 2, lastUrgeTime: '2025-06-13 09:00:00', status: '催办中', records: [
-    { time: '2025-06-12 18:00:00', method: '电话', content: '请尽快安排人员到现场处置燃气泄漏事件', result: '已反馈' },
-    { time: '2025-06-13 09:00:00', method: '系统消息', content: '燃气泄漏事件处置进度提醒', result: '未反馈' }
-  ] },
-  { eventNo: 'EVT20250613007', summary: '太湖路与松江路交叉口积水', receiver: '王五', receiverDept: '排水管理部', receiverPhone: '13700137003', urgeCount: 1, lastUrgeTime: '2025-06-13 08:30:00', status: '催办中', records: [
-    { time: '2025-06-13 08:30:00', method: '短信', content: '积水事件请尽快疏通排水口', result: '已反馈' }
-  ] },
-  { eventNo: 'EVT20250613010', summary: '新华路88号大厦地下室排水泵故障', receiver: '赵六', receiverDept: '设备运维部', receiverPhone: '13600136004', urgeCount: 0, lastUrgeTime: '-', status: '待催办', records: [] },
-  { eventNo: 'EVT20250613011', summary: '人民路与建设路交叉口路面塌陷', receiver: '孙七', receiverDept: '管网维护部', receiverPhone: '13500135005', urgeCount: 3, lastUrgeTime: '2025-06-13 14:00:00', status: '催办中', records: [
-    { time: '2025-06-13 10:00:00', method: '电话', content: '塌陷区域请尽快设置围挡', result: '已反馈' },
-    { time: '2025-06-13 12:00:00', method: '系统消息', content: '塌陷修复进度跟进', result: '未反馈' },
-    { time: '2025-06-13 14:00:00', method: '短信', content: '请反馈塌陷修复预计完成时间', result: '未反馈' }
-  ] },
-  { eventNo: 'EVT20250613012', summary: '珠江路工业园A区消防设施压力异常', receiver: '周八', receiverDept: '设备运维部', receiverPhone: '13400134006', urgeCount: 0, lastUrgeTime: '-', status: '待催办', records: [] },
-  { eventNo: 'EVT20250613013', summary: '黄河路56号小区污水外溢', receiver: '吴九', receiverDept: '排水管理部', receiverPhone: '13300133007', urgeCount: 1, lastUrgeTime: '2025-06-13 13:00:00', status: '催办中', records: [
-    { time: '2025-06-13 13:00:00', method: '电话', content: '污水外溢影响居民生活,请尽快处理', result: '已反馈' }
-  ] },
-  { eventNo: 'EVT20250613014', summary: '中山路与解放路交叉口路灯杆倾斜', receiver: '郑十', receiverDept: '管网维护部', receiverPhone: '13200132008', urgeCount: 0, lastUrgeTime: '-', status: '待催办', records: [] },
-  { eventNo: 'EVT20250613015', summary: '长江路北侧供水管道破裂', receiver: '刘一', receiverDept: '市政部门', receiverPhone: '13100131009', urgeCount: 2, lastUrgeTime: '2025-06-13 12:00:00', status: '已完成', records: [
-    { time: '2025-06-13 11:00:00', method: '系统消息', content: '供水管道破裂请尽快止水维修', result: '已反馈' },
-    { time: '2025-06-13 12:00:00', method: '电话', content: '维修进度确认', result: '已反馈' }
-  ] },
-  { eventNo: 'EVT20250613016', summary: '幸福路排水井盖缺失', receiver: '陈二', receiverDept: '排水管理部', receiverPhone: '13000130010', urgeCount: 0, lastUrgeTime: '-', status: '待催办', records: [] }
-])
-
-// ==================== 搜索筛选 ====================
-const filters = ref({ urgeStatus: '', receiver: '', eventNo: '' })
+// ==================== 数据状态 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const searchForm = ref({ urgeStatus: '', receiver: '', eventNo: '' })
+const pagination = ref({ page: 1, pageSize: 10 })
 
-const filteredList = computed(() => {
-  let list = eventList.value
-  if (filters.value.urgeStatus) {
-    list = list.filter(o => o.status === filters.value.urgeStatus)
+// ==================== 加载数据 ====================
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (searchForm.value.urgeStatus) params.status = searchForm.value.urgeStatus
+    if (searchForm.value.receiver) params.receiver = searchForm.value.receiver
+    if (searchForm.value.eventNo) params.eventNo = searchForm.value.eventNo
+    const res = await getEventUrgePage(pagination.value.page, pagination.value.pageSize, params)
+    tableData.value = res.data?.rows || res.data?.records || res.data || []
+    total.value = res.data?.total || 0
+  } catch (error) {
+    ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
+  } finally {
+    loading.value = false
   }
-  if (filters.value.receiver) {
-    list = list.filter(o => o.receiver.includes(filters.value.receiver))
-  }
-  if (filters.value.eventNo) {
-    list = list.filter(o => o.eventNo.includes(filters.value.eventNo))
-  }
-  return list
+}
+
+onMounted(() => {
+  loadData()
 })
 
+// ==================== 搜索筛选 ====================
 function handleSearch() {
   pagination.value.page = 1
+  loadData()
 }
 
 function handleReset() {
-  filters.value = { urgeStatus: '', receiver: '', eventNo: '' }
+  searchForm.value = { urgeStatus: '', receiver: '', eventNo: '' }
   pagination.value.page = 1
+  loadData()
 }
 
 // ==================== 分页 ====================
-const pagination = ref({ page: 1, pageSize: 10 })
-
-const pagedList = computed(() => {
-  const start = (pagination.value.page - 1) * pagination.value.pageSize
-  return filteredList.value.slice(start, start + pagination.value.pageSize)
-})
-
 function handleSizeChange(val) {
   pagination.value.pageSize = val
   pagination.value.page = 1
+  loadData()
 }
 
 function handleCurrentChange(val) {
   pagination.value.page = val
+  loadData()
 }
 
 // ==================== 标签样式 ====================
@@ -198,6 +183,7 @@ function statusTagType(status) {
 // ==================== 催办 ====================
 const urgeVisible = ref(false)
 const urgeFormRef = ref(null)
+const currentUrgeId = ref(null)
 const urgeForm = ref({ eventNo: '', summary: '', receiver: '', receiverDept: '', receiverPhone: '', methods: [], content: '' })
 const urgeRules = {
   methods: [{ required: true, message: '请选择催办方式', trigger: 'change', type: 'array' }],
@@ -205,44 +191,69 @@ const urgeRules = {
 }
 
 function handleUrge(row) {
+  currentUrgeId.value = row.id
   urgeForm.value = {
     eventNo: row.eventNo,
-    summary: row.summary,
+    summary: row.summary || '',
     receiver: row.receiver,
     receiverDept: row.receiverDept,
     receiverPhone: row.receiverPhone,
     methods: [],
-    content: '关于【' + row.summary + '】的处置进度,请尽快反馈处理情况。'
+    content: '关于【' + (row.summary || '') + '】的处置进度,请尽快反馈处理情况。'
   }
   urgeVisible.value = true
 }
 
-function confirmUrge() {
-  urgeFormRef.value.validate(valid => {
-    if (!valid) return
-    const idx = eventList.value.findIndex(o => o.eventNo === urgeForm.value.eventNo)
-    if (idx > -1) {
-      eventList.value[idx].urgeCount += 1
-      eventList.value[idx].lastUrgeTime = new Date().toLocaleString()
-      eventList.value[idx].status = '催办中'
-      eventList.value[idx].records.push({
-        time: new Date().toLocaleString(),
-        method: urgeForm.value.methods.join('、'),
-        content: urgeForm.value.content,
-        result: '未反馈'
-      })
-    }
+async function confirmUrge() {
+  try {
+    await urgeFormRef.value.validate()
+  } catch {
+    return
+  }
+  try {
+    await doUrge({
+      urgeId: currentUrgeId.value,
+      method: urgeForm.value.methods.join('、'),
+      content: urgeForm.value.content
+    })
+    ElMessage.success('催办已发送')
     urgeVisible.value = false
-  })
+    loadData()
+  } catch (error) {
+    ElMessage.error('催办失败:' + (error.message || '未知错误'))
+  }
+}
+
+// ==================== 删除 ====================
+async function handleDelete(row) {
+  try {
+    await ElMessageBox.confirm('确定要删除该催办记录吗?', '删除确认', { type: 'warning' })
+    await deleteEventUrge(row.id)
+    ElMessage.success('删除成功')
+    loadData()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('删除失败:' + (error.message || '未知错误'))
+    }
+  }
 }
 
 // ==================== 查看记录 ====================
 const recordsVisible = ref(false)
 const currentRow = ref(null)
 
-function handleViewRecords(row) {
+async function handleViewRecords(row) {
   currentRow.value = row
   recordsVisible.value = true
+  // 尝试从服务器加载催办详情
+  try {
+    const res = await getUrgeDetails(row.id)
+    if (res.data) {
+      currentRow.value = { ...row, records: res.data.records || res.data || [] }
+    }
+  } catch {
+    // 如果加载失败,使用行内的 records 数据
+  }
 }
 </script>
 

+ 143 - 156
src/views/subSystem/dispatch/disOnDuty/gzjc.vue

@@ -3,19 +3,19 @@
     <!-- 搜索区 -->
     <div class="filter-panel">
       <div class="filter-form">
-        <el-select v-model="filters.eventStatus" placeholder="事件状态" clearable style="width:140px">
+        <el-select v-model="searchForm.eventStatus" placeholder="事件状态" clearable style="width:140px">
           <el-option label="处理中" value="处理中" />
           <el-option label="已解除" value="已解除" />
           <el-option label="已关闭" value="已关闭" />
         </el-select>
-        <el-select v-model="filters.category" placeholder="事件类别" clearable style="width:140px">
+        <el-select v-model="searchForm.category" placeholder="事件类别" clearable style="width:140px">
           <el-option label="管道泄漏" value="管道泄漏" />
           <el-option label="塌陷" value="塌陷" />
           <el-option label="积水" value="积水" />
           <el-option label="设备故障" value="设备故障" />
         </el-select>
         <el-date-picker
-          v-model="filters.dateRange"
+          v-model="searchForm.dateRange"
           type="daterange"
           range-separator="至"
           start-placeholder="开始日期"
@@ -30,7 +30,7 @@
 
     <!-- 数据表格 -->
     <div class="table-panel">
-      <el-table :data="pagedList" border stripe style="width:100%">
+      <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
         <el-table-column prop="eventNo" label="事件编号" width="140" />
         <el-table-column prop="category" label="事件类别" width="110" />
         <el-table-column prop="level" label="事件等级" width="100">
@@ -43,15 +43,16 @@
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.status }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="lastVisitTime" label="最近回访时间" width="150" />
-        <el-table-column prop="nextVisitTime" label="下次回访时间" width="150" />
-        <el-table-column prop="handler" label="负责人" width="100" />
-        <el-table-column label="操作" width="220" fixed="right">
+        <el-table-column prop="lastVisitTime" label="最近回访时间" min-width="150" />
+        <el-table-column prop="nextVisitTime" label="下次回访时间" min-width="150" />
+        <el-table-column prop="handler" label="负责人" min-width="100" />
+        <el-table-column label="操作" width="260" fixed="right">
           <template #default="{ row }">
             <el-button v-if="row.status === '处理中'" type="primary" link size="small" @click="handleTrack(row)">跟踪</el-button>
             <el-button v-if="row.status === '处理中'" type="primary" link size="small" @click="handleClose(row)">解除</el-button>
             <el-button v-if="row.status === '已解除'" type="primary" link size="small" @click="handleClose(row)">关闭</el-button>
             <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -61,7 +62,7 @@
           v-model:page-size="pagination.pageSize"
           :page-sizes="[10, 20, 50]"
           layout="total, sizes, prev, pager, next, jumper"
-          :total="filteredList.length"
+          :total="total"
           @size-change="handleSizeChange"
           @current-change="handleCurrentChange"
         />
@@ -140,42 +141,42 @@
       <div v-if="currentRow" class="detail-content">
         <el-form label-width="100px" size="default">
           <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="8">
               <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="8">
               <el-form-item label="事件类别">{{ currentRow.category }}</el-form-item>
             </el-col>
+            <el-col :span="8">
+              <el-form-item label="事件等级">
+                <el-tag v-if="currentRow.level" :type="levelTagType(currentRow.level)" size="small">{{ currentRow.level }}</el-tag>
+              </el-form-item>
+            </el-col>
           </el-row>
           <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="事件等级">{{ currentRow.level }}</el-form-item>
-            </el-col>
-            <el-col :span="12">
+            <el-col :span="8">
               <el-form-item label="当前状态">
                 <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.status }}</el-tag>
               </el-form-item>
             </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="负责人">{{ currentRow.handler }}</el-form-item>
+            <el-col :span="8">
+              <el-form-item label="负责人">{{ currentRow.handler || '-' }}</el-form-item>
             </el-col>
-            <el-col :span="12">
-              <el-form-item label="发生地点">{{ currentRow.location }}</el-form-item>
+            <el-col :span="8">
+              <el-form-item label="发生地点">{{ currentRow.location || '-' }}</el-form-item>
             </el-col>
           </el-row>
-          <el-form-item label="事件描述">
-            <div class="desc-text">{{ currentRow.description }}</div>
-          </el-form-item>
           <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="8">
               <el-form-item label="最近回访">{{ currentRow.lastVisitTime || '-' }}</el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="8">
               <el-form-item label="下次回访">{{ currentRow.nextVisitTime || '-' }}</el-form-item>
             </el-col>
           </el-row>
+          <el-form-item label="事件描述">
+            <div class="desc-text">{{ currentRow.description || '-' }}</div>
+          </el-form-item>
           <div class="section-title">处置时间线</div>
           <el-timeline>
             <el-timeline-item
@@ -198,115 +199,64 @@
 </template>
 
 <script setup name="Gzjc">
-import { ref, computed } from 'vue'
-
-// ==================== mock 数据 ====================
-const eventList = ref([
-  { eventNo: 'EVT20250613003', category: '积水', level: '一般', status: '处理中', lastVisitTime: '2025-06-13 10:00:00', nextVisitTime: '2025-06-13 16:00:00', handler: '张三', location: '科技大道中段', description: '路面积水严重,影响通行', timeline: [
-    { time: '2025-06-13 07:55', title: '事件接报', content: '视频监控发现积水', type: 'primary' },
-    { time: '2025-06-13 08:10', title: '事件核实', content: '核实积水深度约10cm', type: 'success' },
-    { time: '2025-06-13 08:30', title: '事件转接', content: '转接至排水管理部', type: 'warning' },
-    { time: '2025-06-13 10:00', title: '跟踪记录', content: '已安排疏通车前往,预计2小时完成', type: 'primary' }
-  ] },
-  { eventNo: 'EVT20250613004', category: '管道泄漏', level: '重大', status: '处理中', lastVisitTime: '2025-06-13 08:00:00', nextVisitTime: '2025-06-13 12:00:00', handler: '李四', location: '和平路步行街', description: '燃气管道疑似泄漏', timeline: [
-    { time: '2025-06-12 16:30', title: '事件接报', content: '市应急办转发', type: 'primary' },
-    { time: '2025-06-12 17:00', title: '事件核实', content: '已现场确认微泄漏', type: 'success' },
-    { time: '2025-06-12 17:30', title: '事件转接', content: '转接至市政部门', type: 'warning' },
-    { time: '2025-06-13 08:00', title: '跟踪记录', content: '已关闭阀门,正在抢修', type: 'primary' }
-  ] },
-  { eventNo: 'EVT20250613007', category: '积水', level: '一般', status: '已解除', lastVisitTime: '2025-06-13 09:30:00', nextVisitTime: '-', handler: '王五', location: '太湖路与松江路交叉口', description: '路口积水深度约15cm', timeline: [
-    { time: '2025-06-13 06:45', title: '事件接报', content: '交警监控共享平台发现', type: 'primary' },
-    { time: '2025-06-13 07:00', title: '事件核实', content: '积水属实,排水口堵塞', type: 'success' },
-    { time: '2025-06-13 07:30', title: '事件转接', content: '转接至排水管理部', type: 'warning' },
-    { time: '2025-06-13 09:00', title: '跟踪记录', content: '排水口已疏通,积水消退', type: 'primary' },
-    { time: '2025-06-13 09:30', title: '事件解除', content: '解除原因:处置完成', type: 'success' }
-  ] },
-  { eventNo: 'EVT20250613010', category: '设备故障', level: '一般', status: '处理中', lastVisitTime: '2025-06-13 15:30:00', nextVisitTime: '2025-06-14 09:00:00', handler: '赵六', location: '新华路88号大厦', description: '地下室排水泵故障', timeline: [
-    { time: '2025-06-13 14:35', title: '事件接报', content: '物业反映排水泵故障', type: 'primary' },
-    { time: '2025-06-13 15:00', title: '事件核实', content: '排水泵电机烧毁', type: 'success' },
-    { time: '2025-06-13 15:30', title: '跟踪记录', content: '已联系供应商备货更换', type: 'primary' }
-  ] },
-  { eventNo: 'EVT20250613011', category: '塌陷', level: '较大', status: '处理中', lastVisitTime: '2025-06-13 11:00:00', nextVisitTime: '2025-06-13 17:00:00', handler: '孙七', location: '人民路与建设路交叉口', description: '路面塌陷面积约2平方米', timeline: [
-    { time: '2025-06-13 08:35', title: '事件接报', content: '巡查上报', type: 'primary' },
-    { time: '2025-06-13 08:50', title: '事件核实', content: '塌陷属实,已设围挡', type: 'success' },
-    { time: '2025-06-13 09:00', title: '事件转接', content: '转接至管网维护部', type: 'warning' },
-    { time: '2025-06-13 11:00', title: '跟踪记录', content: '已安排施工队,预计3天修复', type: 'primary' }
-  ] },
-  { eventNo: 'EVT20250613012', category: '设备故障', level: '较大', status: '处理中', lastVisitTime: '-', nextVisitTime: '2025-06-13 22:00:00', handler: '周八', location: '珠江路工业园A区', description: '消防设施压力异常', timeline: [
-    { time: '2025-06-12 20:30', title: '事件接报', content: '省厅转发', type: 'primary' },
-    { time: '2025-06-12 21:00', title: '事件核实', content: '压力传感器故障', type: 'success' }
-  ] },
-  { eventNo: 'EVT20250613013', category: '积水', level: '较大', status: '已关闭', lastVisitTime: '2025-06-13 14:00:00', nextVisitTime: '-', handler: '吴九', location: '黄河路56号小区', description: '小区内污水外溢', timeline: [
-    { time: '2025-06-13 11:25', title: '事件接报', content: '市民热线反映', type: 'primary' },
-    { time: '2025-06-13 11:45', title: '事件核实', content: '污水外溢属实', type: 'success' },
-    { time: '2025-06-13 12:00', title: '事件转接', content: '转接至排水管理部', type: 'warning' },
-    { time: '2025-06-13 14:00', title: '跟踪记录', content: '化粪池已清掏,外溢停止', type: 'primary' },
-    { time: '2025-06-13 14:30', title: '事件解除', content: '解除原因:处置完成', type: 'success' },
-    { time: '2025-06-13 15:00', title: '事件关闭', content: '事件已归档', type: 'info' }
-  ] },
-  { eventNo: 'EVT20250613014', category: '塌陷', level: '一般', status: '处理中', lastVisitTime: '-', nextVisitTime: '2025-06-13 18:00:00', handler: '郑十', location: '中山路与解放路交叉口', description: '路灯杆倾斜', timeline: [
-    { time: '2025-06-13 13:05', title: '事件接报', content: '巡查上报', type: 'primary' },
-    { time: '2025-06-13 13:20', title: '事件核实', content: '灯杆倾斜约15度', type: 'success' }
-  ] },
-  { eventNo: 'EVT20250613015', category: '管道泄漏', level: '较大', status: '已解除', lastVisitTime: '2025-06-13 13:00:00', nextVisitTime: '-', handler: '刘一', location: '长江路北侧辅道', description: '供水管道破裂', timeline: [
-    { time: '2025-06-13 10:05', title: '事件接报', content: '巡查上报', type: 'primary' },
-    { time: '2025-06-13 10:20', title: '事件核实', content: 'DN300管道破裂', type: 'success' },
-    { time: '2025-06-13 10:30', title: '事件转接', content: '转接至市政部门', type: 'warning' },
-    { time: '2025-06-13 12:00', title: '跟踪记录', content: '管道已修复,恢复供水', type: 'primary' },
-    { time: '2025-06-13 13:00', title: '事件解除', content: '解除原因:处置完成', type: 'success' }
-  ] },
-  { eventNo: 'EVT20250613016', category: '设备故障', level: '重大', status: '处理中', lastVisitTime: '2025-06-13 10:00:00', nextVisitTime: '2025-06-13 16:00:00', handler: '陈二', location: '幸福路123号门前', description: '排水井盖缺失', timeline: [
-    { time: '2025-06-13 09:15', title: '事件接报', content: '市民热线反映', type: 'primary' },
-    { time: '2025-06-13 09:30', title: '事件核实', content: '井盖缺失,已设警示', type: 'success' },
-    { time: '2025-06-13 10:00', title: '跟踪记录', content: '已联系仓库调配备用井盖', type: 'primary' }
-  ] }
-])
-
-// ==================== 搜索筛选 ====================
-const filters = ref({ eventStatus: '', category: '', dateRange: null })
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getEventTrackPage, deleteEventTrack, addTrackLog, releaseEvent, closeEvent, getTrackLogs } from '@/api/dispatch'
+
+// ==================== 数据状态 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const searchForm = ref({ eventStatus: '', category: '', dateRange: null })
+const pagination = ref({ page: 1, pageSize: 10 })
 
-const filteredList = computed(() => {
-  let list = eventList.value
-  if (filters.value.eventStatus) {
-    list = list.filter(o => o.status === filters.value.eventStatus)
-  }
-  if (filters.value.category) {
-    list = list.filter(o => o.category === filters.value.category)
-  }
-  if (filters.value.dateRange && filters.value.dateRange.length === 2) {
-    const [start, end] = filters.value.dateRange
-    list = list.filter(o => {
-      const t = o.timeline[0] && o.timeline[0].time ? o.timeline[0].time : ''
-      return t >= start && t <= end + ' 23:59:59'
-    })
+// ==================== 加载数据 ====================
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (searchForm.value.eventStatus) params.status = searchForm.value.eventStatus
+    if (searchForm.value.category) params.category = searchForm.value.category
+    if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
+      params.startDate = searchForm.value.dateRange[0]
+      params.endDate = searchForm.value.dateRange[1]
+    }
+    const res = await getEventTrackPage(pagination.value.page, pagination.value.pageSize, params)
+    tableData.value = res.data?.rows || res.data?.records || res.data || []
+    total.value = res.data?.total || 0
+  } catch (error) {
+    ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
+  } finally {
+    loading.value = false
   }
-  return list
+}
+
+onMounted(() => {
+  loadData()
 })
 
+// ==================== 搜索筛选 ====================
 function handleSearch() {
   pagination.value.page = 1
+  loadData()
 }
 
 function handleReset() {
-  filters.value = { eventStatus: '', category: '', dateRange: null }
+  searchForm.value = { eventStatus: '', category: '', dateRange: null }
   pagination.value.page = 1
+  loadData()
 }
 
 // ==================== 分页 ====================
-const pagination = ref({ page: 1, pageSize: 10 })
-
-const pagedList = computed(() => {
-  const start = (pagination.value.page - 1) * pagination.value.pageSize
-  return filteredList.value.slice(start, start + pagination.value.pageSize)
-})
-
 function handleSizeChange(val) {
   pagination.value.pageSize = val
   pagination.value.page = 1
+  loadData()
 }
 
 function handleCurrentChange(val) {
   pagination.value.page = val
+  loadData()
 }
 
 // ==================== 标签样式 ====================
@@ -327,6 +277,7 @@ function levelTagType(level) {
 // ==================== 跟踪 ====================
 const trackVisible = ref(false)
 const trackFormRef = ref(null)
+const currentTrackId = ref(null)
 const trackForm = ref({ eventNo: '', content: '', assessment: '趋于平稳', needUpgrade: '否', nextVisitTime: '' })
 const trackRules = {
   content: [{ required: true, message: '请输入跟踪内容', trigger: 'blur' }],
@@ -337,31 +288,51 @@ const trackRules = {
 
 function handleTrack(row) {
   currentRow.value = row
+  currentTrackId.value = row.id
   trackForm.value = { eventNo: row.eventNo, content: '', assessment: '趋于平稳', needUpgrade: '否', nextVisitTime: '' }
+  // 尝试加载跟踪日志
+  loadTrackLogs(row.id)
   trackVisible.value = true
 }
 
-function confirmTrack() {
-  trackFormRef.value.validate(valid => {
-    if (!valid) return
-    const idx = eventList.value.findIndex(o => o.eventNo === trackForm.value.eventNo)
-    if (idx > -1) {
-      eventList.value[idx].timeline.push({
-        time: new Date().toLocaleString(),
-        title: '跟踪记录',
-        content: trackForm.value.content + '(态势:' + trackForm.value.assessment + ',升级:' + trackForm.value.needUpgrade + ')',
-        type: 'primary'
-      })
-      eventList.value[idx].lastVisitTime = new Date().toLocaleString()
-      eventList.value[idx].nextVisitTime = trackForm.value.nextVisitTime
+async function loadTrackLogs(trackId) {
+  try {
+    const res = await getTrackLogs(trackId)
+    if (res.data && currentRow.value) {
+      currentRow.value = { ...currentRow.value, timeline: res.data || [] }
     }
+  } catch {
+    // 使用行内已有数据
+  }
+}
+
+async function confirmTrack() {
+  try {
+    await trackFormRef.value.validate()
+  } catch {
+    return
+  }
+  try {
+    await addTrackLog({
+      trackId: currentTrackId.value,
+      title: '跟踪记录',
+      content: trackForm.value.content,
+      assessment: trackForm.value.assessment,
+      needUpgrade: trackForm.value.needUpgrade,
+      type: 'primary'
+    })
+    ElMessage.success('跟踪记录保存成功')
     trackVisible.value = false
-  })
+    loadData()
+  } catch (error) {
+    ElMessage.error('保存失败:' + (error.message || '未知错误'))
+  }
 }
 
 // ==================== 解除/关闭 ====================
 const closeVisible = ref(false)
 const closeFormRef = ref(null)
+const currentCloseRow = ref(null)
 const closeForm = ref({ eventNo: '', summary: '', reason: '', remark: '' })
 const closeRules = {
   reason: [{ required: true, message: '请选择解除原因', trigger: 'change' }],
@@ -370,47 +341,63 @@ const closeRules = {
 const closeTitle = ref('事件解除')
 
 function handleClose(row) {
-  currentRow.value = row
+  currentCloseRow.value = row
   closeTitle.value = row.status === '处理中' ? '事件解除' : '事件关闭'
-  closeForm.value = { eventNo: row.eventNo, summary: row.description, reason: '', remark: '' }
+  closeForm.value = { eventNo: row.eventNo, summary: row.description || '', reason: '', remark: '' }
   closeVisible.value = true
 }
 
-function confirmClose() {
-  closeFormRef.value.validate(valid => {
-    if (!valid) return
-    const idx = eventList.value.findIndex(o => o.eventNo === closeForm.value.eventNo)
-    if (idx > -1) {
-      if (eventList.value[idx].status === '处理中') {
-        eventList.value[idx].status = '已解除'
-        eventList.value[idx].timeline.push({
-          time: new Date().toLocaleString(),
-          title: '事件解除',
-          content: '解除原因:' + closeForm.value.reason + ';' + closeForm.value.remark,
-          type: 'success'
-        })
-        eventList.value[idx].nextVisitTime = '-'
-      } else if (eventList.value[idx].status === '已解除') {
-        eventList.value[idx].status = '已关闭'
-        eventList.value[idx].timeline.push({
-          time: new Date().toLocaleString(),
-          title: '事件关闭',
-          content: '事件已归档:' + closeForm.value.remark,
-          type: 'info'
-        })
-      }
+async function confirmClose() {
+  try {
+    await closeFormRef.value.validate()
+  } catch {
+    return
+  }
+  try {
+    if (currentCloseRow.value.status === '处理中') {
+      await releaseEvent(currentCloseRow.value.id)
+      ElMessage.success('事件已解除')
+    } else if (currentCloseRow.value.status === '已解除') {
+      await closeEvent(currentCloseRow.value.id)
+      ElMessage.success('事件已关闭')
     }
     closeVisible.value = false
-  })
+    loadData()
+  } catch (error) {
+    ElMessage.error('操作失败:' + (error.message || '未知错误'))
+  }
+}
+
+// ==================== 删除 ====================
+async function handleDelete(row) {
+  try {
+    await ElMessageBox.confirm('确定要删除该跟踪记录吗?', '删除确认', { type: 'warning' })
+    await deleteEventTrack(row.id)
+    ElMessage.success('删除成功')
+    loadData()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('删除失败:' + (error.message || '未知错误'))
+    }
+  }
 }
 
 // ==================== 详情 ====================
 const detailVisible = ref(false)
 const currentRow = ref(null)
 
-function handleDetail(row) {
+async function handleDetail(row) {
   currentRow.value = row
   detailVisible.value = true
+  // 尝试加载最新的跟踪日志
+  try {
+    const res = await getTrackLogs(row.id)
+    if (res.data && res.data.length) {
+      currentRow.value = { ...row, timeline: res.data }
+    }
+  } catch {
+    // 使用行内已有数据
+  }
 }
 </script>
 

+ 121 - 72
src/views/subSystem/dispatch/disOnDuty/jbgl.vue

@@ -3,14 +3,14 @@
     <!-- 搜索区 -->
     <div class="filter-panel">
       <div class="filter-form">
-        <el-select v-model="filters.source" placeholder="事件来源" clearable style="width:160px">
+        <el-select v-model="searchForm.source" placeholder="事件来源" clearable style="width:160px">
           <el-option label="巡查上报" value="巡查上报" />
           <el-option label="呼叫中心" value="呼叫中心" />
           <el-option label="视频监控" value="视频监控" />
           <el-option label="上级下达" value="上级下达" />
         </el-select>
         <el-date-picker
-          v-model="filters.dateRange"
+          v-model="searchForm.dateRange"
           type="daterange"
           range-separator="至"
           start-placeholder="开始日期"
@@ -18,7 +18,7 @@
           style="width:260px"
           value-format="YYYY-MM-DD"
         />
-        <el-select v-model="filters.status" placeholder="事件状态" clearable style="width:140px">
+        <el-select v-model="searchForm.status" placeholder="事件状态" clearable style="width:140px">
           <el-option label="待受理" value="待受理" />
           <el-option label="已受理" value="已受理" />
           <el-option label="已驳回" value="已驳回" />
@@ -33,7 +33,7 @@
 
     <!-- 数据表格 -->
     <div class="table-panel">
-      <el-table :data="pagedList" border stripe style="width:100%">
+      <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
         <el-table-column prop="eventNo" label="事件编号" width="140" />
         <el-table-column prop="source" label="事件来源" width="110" />
         <el-table-column prop="location" label="发生地点" show-overflow-tooltip />
@@ -45,10 +45,12 @@
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.status }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="160" fixed="right">
+        <el-table-column label="操作" width="220" fixed="right">
           <template #default="{ row }">
             <el-button v-if="row.status === '待受理'" type="primary" link size="small" @click="handleAccept(row)">受理</el-button>
+            <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
             <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -58,7 +60,7 @@
           v-model:page-size="pagination.pageSize"
           :page-sizes="[10, 20, 50]"
           layout="total, sizes, prev, pager, next, jumper"
-          :total="filteredList.length"
+          :total="total"
           @size-change="handleSizeChange"
           @current-change="handleCurrentChange"
         />
@@ -121,8 +123,8 @@
       </template>
     </el-dialog>
 
-    <!-- 新增接报弹窗 -->
-    <el-dialog v-model="addVisible" title="新增接报" width="600px" :close-on-click-modal="false">
+    <!-- 新增/编辑接报弹窗 -->
+    <el-dialog v-model="addVisible" :title="editingId ? '编辑接报' : '新增接报'" width="600px" :close-on-click-modal="false">
       <el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="100px">
         <el-form-item label="事件来源" prop="source">
           <el-select v-model="addForm.source" placeholder="请选择" style="width:100%">
@@ -144,74 +146,86 @@
         <el-form-item label="接报人" prop="reporter">
           <el-input v-model="addForm.reporter" placeholder="请输入接报人" />
         </el-form-item>
+        <el-form-item label="接报时间" prop="reportTime">
+          <el-date-picker v-model="addForm.reportTime" type="datetime" placeholder="选择日期时间" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" />
+        </el-form-item>
+        <el-form-item label="来源渠道" prop="channel">
+          <el-select v-model="addForm.channel" placeholder="请选择来源渠道" style="width:100%">
+            <el-option label="12345市民热线" value="12345市民热线" />
+            <el-option label="移动终端巡查系统" value="移动终端巡查系统" />
+            <el-option label="省应急管理厅平台" value="省应急管理厅平台" />
+            <el-option label="交警监控共享平台" value="交警监控共享平台" />
+            <el-option label="其他" value="其他" />
+          </el-select>
+        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="addVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmAdd">确认</el-button>
+        <el-button type="primary" @click="handleSubmit">确认</el-button>
       </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup name="Jbgl">
-import { ref, computed } from 'vue'
-
-// ==================== mock 数据 ====================
-const eventList = ref([
-  { eventNo: 'JB20250613001', source: '巡查上报', location: '人民路与建设路交叉口', happenTime: '2025-06-13 08:30:00', description: '路面出现明显塌陷,面积约2平方米', reporter: '张三', reportTime: '2025-06-13 08:35:00', channel: '移动终端巡查系统', status: '待受理' },
-  { eventNo: 'JB20250613002', source: '呼叫中心', location: '幸福路123号门前', happenTime: '2025-06-13 09:10:00', description: '市民反映排水井盖缺失,存在安全隐患', reporter: '李四', reportTime: '2025-06-13 09:15:00', channel: '12345市民热线', status: '待受理' },
-  { eventNo: 'JB20250613003', source: '视频监控', location: '科技大道中段', happenTime: '2025-06-13 07:50:00', description: '视频监控发现路面积水严重,影响通行', reporter: '王五', reportTime: '2025-06-13 07:55:00', channel: '智慧城管视频监控平台', status: '已受理' },
-  { eventNo: 'JB20250613004', source: '上级下达', location: '和平路步行街', happenTime: '2025-06-12 16:00:00', description: '市应急办转发:步行街燃气管道疑似泄漏', reporter: '赵六', reportTime: '2025-06-12 16:30:00', channel: '市应急指挥平台', status: '已受理' },
-  { eventNo: 'JB20250613005', source: '巡查上报', location: '长江路北侧辅道', happenTime: '2025-06-13 10:00:00', description: '供水管道破裂,水涌至路面', reporter: '孙七', reportTime: '2025-06-13 10:05:00', channel: '移动终端巡查系统', status: '已驳回' },
-  { eventNo: 'JB20250613006', source: '呼叫中心', location: '黄河路56号小区', happenTime: '2025-06-13 11:20:00', description: '居民反映小区内污水外溢,气味刺鼻', reporter: '周八', reportTime: '2025-06-13 11:25:00', channel: '12345市民热线', status: '待受理' },
-  { eventNo: 'JB20250613007', source: '视频监控', location: '太湖路与松江路交叉口', happenTime: '2025-06-13 06:40:00', description: '早高峰时段路口积水深度约15cm', reporter: '吴九', reportTime: '2025-06-13 06:45:00', channel: '交警监控共享平台', status: '已受理' },
-  { eventNo: 'JB20250613008', source: '上级下达', location: '珠江路工业园A区', happenTime: '2025-06-12 20:00:00', description: '省厅转发:园区内消防设施压力异常', reporter: '郑十', reportTime: '2025-06-12 20:30:00', channel: '省应急管理厅平台', status: '待受理' },
-  { eventNo: 'JB20250613009', source: '巡查上报', location: '中山路与解放路交叉口', happenTime: '2025-06-13 13:00:00', description: '路灯杆倾斜,存在倒塌风险', reporter: '刘一', reportTime: '2025-06-13 13:05:00', channel: '移动终端巡查系统', status: '待受理' },
-  { eventNo: 'JB20250613010', source: '呼叫中心', location: '新华路88号大厦', happenTime: '2025-06-13 14:30:00', description: '大厦物业反映地下室排水泵故障', reporter: '陈二', reportTime: '2025-06-13 14:35:00', channel: '12345市民热线', status: '已受理' }
-])
-
-// ==================== 搜索筛选 ====================
-const filters = ref({ source: '', dateRange: null, status: '' })
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getEventReportPage, addEventReport, updateEventReport, deleteEventReport, acceptEventReport } from '@/api/dispatch'
+
+// ==================== 数据状态 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const searchForm = ref({ source: '', dateRange: null, status: '' })
+const pagination = ref({ page: 1, pageSize: 10 })
 
-const filteredList = computed(() => {
-  let list = eventList.value
-  if (filters.value.source) {
-    list = list.filter(o => o.source === filters.value.source)
+// ==================== 加载数据 ====================
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (searchForm.value.source) params.source = searchForm.value.source
+    if (searchForm.value.status) params.status = searchForm.value.status
+    if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
+      params.startDate = searchForm.value.dateRange[0]
+      params.endDate = searchForm.value.dateRange[1]
+    }
+    const res = await getEventReportPage(pagination.value.page, pagination.value.pageSize, params)
+    tableData.value = res.data?.rows || res.data?.records || res.data || []
+    total.value = res.data?.total || 0
+  } catch (error) {
+    ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
+  } finally {
+    loading.value = false
   }
-  if (filters.value.status) {
-    list = list.filter(o => o.status === filters.value.status)
-  }
-  if (filters.value.dateRange && filters.value.dateRange.length === 2) {
-    const [start, end] = filters.value.dateRange
-    list = list.filter(o => o.happenTime >= start && o.happenTime <= end + ' 23:59:59')
-  }
-  return list
+}
+
+onMounted(() => {
+  loadData()
 })
 
+// ==================== 搜索筛选 ====================
 function handleSearch() {
   pagination.value.page = 1
+  loadData()
 }
 
 function handleReset() {
-  filters.value = { source: '', dateRange: null, status: '' }
+  searchForm.value = { source: '', dateRange: null, status: '' }
   pagination.value.page = 1
+  loadData()
 }
 
 // ==================== 分页 ====================
-const pagination = ref({ page: 1, pageSize: 10 })
-
-const pagedList = computed(() => {
-  const start = (pagination.value.page - 1) * pagination.value.pageSize
-  return filteredList.value.slice(start, start + pagination.value.pageSize)
-})
-
 function handleSizeChange(val) {
   pagination.value.pageSize = val
   pagination.value.page = 1
+  loadData()
 }
 
 function handleCurrentChange(val) {
   pagination.value.page = val
+  loadData()
 }
 
 // ==================== 状态标签样式 ====================
@@ -231,12 +245,16 @@ function handleAccept(row) {
   acceptVisible.value = true
 }
 
-function confirmAccept() {
-  if (currentRow.value) {
-    const idx = eventList.value.findIndex(o => o.eventNo === currentRow.value.eventNo)
-    if (idx > -1) eventList.value[idx].status = '已受理'
+async function confirmAccept() {
+  if (!currentRow.value) return
+  try {
+    await acceptEventReport(currentRow.value.id)
+    ElMessage.success('受理成功')
+    acceptVisible.value = false
+    loadData()
+  } catch (error) {
+    ElMessage.error('受理失败:' + (error.message || '未知错误'))
   }
-  acceptVisible.value = false
 }
 
 // ==================== 详情 ====================
@@ -247,42 +265,73 @@ function handleDetail(row) {
   detailVisible.value = true
 }
 
-// ==================== 新增接报 ====================
+// ==================== 新增/编辑接报 ====================
 const addVisible = ref(false)
 const addFormRef = ref(null)
-const addForm = ref({ source: '', location: '', happenTime: '', description: '', reporter: '' })
+const editingId = ref(null)
+const addForm = ref({ source: '', location: '', happenTime: '', description: '', reporter: '', reportTime: '', channel: '' })
 const addRules = {
   source: [{ required: true, message: '请选择事件来源', trigger: 'change' }],
   location: [{ required: true, message: '请输入发生地点', trigger: 'blur' }],
   happenTime: [{ required: true, message: '请选择发生时间', trigger: 'change' }],
   description: [{ required: true, message: '请输入事件描述', trigger: 'blur' }],
-  reporter: [{ required: true, message: '请输入接报人', trigger: 'blur' }]
+  reporter: [{ required: true, message: '请输入接报人', trigger: 'blur' }],
+  reportTime: [{ required: true, message: '请选择接报时间', trigger: 'change' }],
+  channel: [{ required: true, message: '请输入来源渠道', trigger: 'blur' }]
 }
 
 function handleAdd() {
-  addForm.value = { source: '', location: '', happenTime: '', description: '', reporter: '' }
+  editingId.value = null
+  addForm.value = { source: '', location: '', happenTime: '', description: '', reporter: '', reportTime: '', channel: '' }
   addVisible.value = true
 }
 
-function confirmAdd() {
-  addFormRef.value.validate(valid => {
+function handleEdit(row) {
+  editingId.value = row.id
+  addForm.value = {
+    source: row.source,
+    location: row.location,
+    happenTime: row.happenTime,
+    description: row.description,
+    reporter: row.reporter,
+    reportTime: row.reportTime,
+    channel: row.channel
+  }
+  addVisible.value = true
+}
+
+function handleSubmit() {
+  addFormRef.value.validate(async (valid) => {
     if (!valid) return
-    const now = new Date()
-    const no = 'JB' + now.getFullYear() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + String(eventList.value.length + 1).padStart(3, '0')
-    eventList.value.unshift({
-      eventNo: no,
-      source: addForm.value.source,
-      location: addForm.value.location,
-      happenTime: addForm.value.happenTime,
-      description: addForm.value.description,
-      reporter: addForm.value.reporter,
-      reportTime: addForm.value.happenTime,
-      channel: '人工录入',
-      status: '待受理'
-    })
-    addVisible.value = false
+    try {
+      if (editingId.value) {
+        await updateEventReport({ id: editingId.value, ...addForm.value })
+        ElMessage.success('编辑成功')
+      } else {
+        await addEventReport(addForm.value)
+        ElMessage.success('新增成功')
+      }
+      addVisible.value = false
+      loadData()
+    } catch (error) {
+      ElMessage.error('操作失败:' + (error.message || '未知错误'))
+    }
   })
 }
+
+// ==================== 删除 ====================
+async function handleDelete(row) {
+  try {
+    await ElMessageBox.confirm('确定要删除该接报记录吗?', '删除确认', { type: 'warning' })
+    await deleteEventReport(row.id)
+    ElMessage.success('删除成功')
+    loadData()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('删除失败:' + (error.message || '未知错误'))
+    }
+  }
+}
 </script>
 
 <style scoped>

+ 186 - 84
src/views/subSystem/dispatch/disOnDuty/zjbs.vue

@@ -3,13 +3,13 @@
     <!-- 搜索区 -->
     <div class="filter-panel">
       <div class="filter-form">
-        <el-input v-model="filters.eventNo" placeholder="事件编号" clearable style="width:160px" />
-        <el-select v-model="filters.transferStatus" placeholder="转接状态" clearable style="width:140px">
+        <el-input v-model="searchForm.eventNo" placeholder="事件编号" clearable style="width:160px" />
+        <el-select v-model="searchForm.transferStatus" placeholder="转接状态" clearable style="width:140px">
           <el-option label="待转接" value="待转接" />
           <el-option label="已转接" value="已转接" />
           <el-option label="已报送" value="已报送" />
         </el-select>
-        <el-select v-model="filters.receiveDept" placeholder="接收部门" clearable style="width:160px">
+        <el-select v-model="searchForm.receiveDept" placeholder="接收部门" clearable style="width:160px">
           <el-option label="市政部门" value="市政部门" />
           <el-option label="消防部门" value="消防部门" />
           <el-option label="排水管理部" value="排水管理部" />
@@ -24,7 +24,8 @@
 
     <!-- 数据表格 -->
     <div class="table-panel">
-      <el-table :data="pagedList" border stripe style="width:100%">
+      <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
+        <el-table-column prop="reportEventNo" label="接报编号" width="140" show-overflow-tooltip />
         <el-table-column prop="eventNo" label="事件编号" width="140" />
         <el-table-column prop="category" label="事件类别" width="110" />
         <el-table-column prop="level" label="事件等级" width="100">
@@ -32,19 +33,20 @@
             <el-tag :type="levelTagType(row.level)" size="small">{{ row.level }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="transferDept" label="转接部门" width="120" />
-        <el-table-column prop="reportLevel" label="报送层级" width="120" />
-        <el-table-column prop="transferTime" label="转接时间" width="150" />
+        <el-table-column prop="transferDept" label="转接部门" min-width="120" />
+        <el-table-column prop="reportTargets" label="报送层级" min-width="120" />
+        <el-table-column prop="transferTime" label="转接时间" min-width="150" />
         <el-table-column prop="status" label="状态" width="100">
           <template #default="{ row }">
             <el-tag :type="statusTagType(row.status)" size="small">{{ row.status }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
+        <el-table-column label="操作" width="240" fixed="right">
           <template #default="{ row }">
             <el-button v-if="row.status === '待转接'" type="primary" link size="small" @click="handleTransfer(row)">转接</el-button>
             <el-button v-if="row.status === '待转接' || row.status === '已转接'" type="primary" link size="small" @click="handleReport(row)">报送</el-button>
             <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
+            <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -54,7 +56,7 @@
           v-model:page-size="pagination.pageSize"
           :page-sizes="[10, 20, 50]"
           layout="total, sizes, prev, pager, next, jumper"
-          :total="filteredList.length"
+          :total="total"
           @size-change="handleSizeChange"
           @current-change="handleCurrentChange"
         />
@@ -105,14 +107,27 @@
           <div class="desc-text">{{ reportForm.summary }}</div>
         </el-form-item>
         <el-form-item label="附件上传">
-          <el-upload action="#" :auto-upload="false" :file-list="reportForm.fileList">
+          <el-upload
+            ref="uploadRef"
+            :action="uploadUrl"
+            :headers="uploadHeaders"
+            :limit="5"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :on-exceed="handleUploadExceed"
+            :file-list="reportForm.fileList"
+            :before-upload="handleBeforeUpload"
+          >
             <el-button type="primary">选择文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">单个文件不超过 50MB,最多上传 5 个文件</div>
+            </template>
           </el-upload>
         </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="reportVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirmReport">确认报送</el-button>
+        <el-button type="primary" @click="confirmReport" :loading="submitting">确认报送</el-button>
       </template>
     </el-dialog>
 
@@ -121,21 +136,43 @@
       <div v-if="currentRow" class="detail-content">
         <el-form label-width="100px" size="default">
           <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="6">
+              <el-form-item label="接报编号">{{ currentRow.reportEventNo || '-' }}</el-form-item>
+            </el-col>
+            <el-col :span="6">
               <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
             </el-col>
-            <el-col :span="12">
+            <el-col :span="6">
               <el-form-item label="事件类别">{{ currentRow.category }}</el-form-item>
             </el-col>
+            <el-col :span="6">
+              <el-form-item label="关联核实ID">{{ currentRow.verifyId }}</el-form-item>
+            </el-col>
           </el-row>
           <el-row :gutter="20">
-            <el-col :span="12">
+            <el-col :span="8">
               <el-form-item label="事件等级">{{ currentRow.level }}</el-form-item>
             </el-col>
+            <el-col :span="8">
+              <el-form-item label="转接部门">{{ currentRow.transferDept }}</el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="当前状态">
+                <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.status }}</el-tag>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="转接时间">{{ currentRow.transferTime || '-' }}</el-form-item>
+            </el-col>
             <el-col :span="12">
-              <el-form-item label="当前状态">{{ currentRow.status }}</el-form-item>
+              <el-form-item label="报送层级">{{ currentRow.reportTargets || '-' }}</el-form-item>
             </el-col>
           </el-row>
+          <el-form-item label="事件摘要">
+            <div class="desc-text">{{ currentRow.summary || '-' }}</div>
+          </el-form-item>
         </el-form>
         <div class="section-title">流转历史</div>
         <el-timeline>
@@ -143,7 +180,7 @@
             v-for="(item, index) in currentRow.history"
             :key="index"
             :type="item.type"
-            :timestamp="item.time"
+            :timestamp="item.actionTime"
           >
             {{ item.content }}
           </el-timeline-item>
@@ -157,21 +194,10 @@
 </template>
 
 <script setup name="Zjbs">
-import { ref, computed } from 'vue'
-
-// ==================== mock 数据 ====================
-const eventList = ref([
-  { eventNo: 'EVT20250613003', category: '积水', level: '一般', transferDept: '-', reportLevel: '-', transferTime: '-', status: '待转接', summary: '科技大道中段路面积水严重,影响通行', history: [{ time: '2025-06-13 07:55', content: '事件接报', type: 'primary' }, { time: '2025-06-13 08:10', content: '事件核实完成', type: 'success' }] },
-  { eventNo: 'EVT20250613004', category: '管道泄漏', level: '重大', transferDept: '市政部门', reportLevel: '-', transferTime: '2025-06-12 17:30:00', status: '已转接', summary: '和平路步行街燃气管道疑似泄漏', history: [{ time: '2025-06-12 16:30', content: '事件接报', type: 'primary' }, { time: '2025-06-12 17:00', content: '事件核实完成', type: 'success' }, { time: '2025-06-12 17:30', content: '转接至市政部门', type: 'warning' }] },
-  { eventNo: 'EVT20250613007', category: '积水', level: '一般', transferDept: '排水管理部', reportLevel: '区级应急办', transferTime: '2025-06-13 07:30:00', status: '已报送', summary: '太湖路与松江路交叉口积水', history: [{ time: '2025-06-13 06:45', content: '事件接报', type: 'primary' }, { time: '2025-06-13 07:00', content: '事件核实完成', type: 'success' }, { time: '2025-06-13 07:30', content: '转接至排水管理部', type: 'warning' }, { time: '2025-06-13 08:00', content: '报送至区级应急办', type: 'danger' }] },
-  { eventNo: 'EVT20250613010', category: '设备故障', level: '一般', transferDept: '-', reportLevel: '-', transferTime: '-', status: '待转接', summary: '新华路88号大厦地下室排水泵故障', history: [{ time: '2025-06-13 14:35', content: '事件接报', type: 'primary' }, { time: '2025-06-13 15:00', content: '事件核实完成', type: 'success' }] },
-  { eventNo: 'EVT20250613011', category: '塌陷', level: '较大', transferDept: '管网维护部', reportLevel: '-', transferTime: '2025-06-13 09:00:00', status: '已转接', summary: '人民路与建设路交叉口路面塌陷', history: [{ time: '2025-06-13 08:35', content: '事件接报', type: 'primary' }, { time: '2025-06-13 08:50', content: '事件核实完成', type: 'success' }, { time: '2025-06-13 09:00', content: '转接至管网维护部', type: 'warning' }] },
-  { eventNo: 'EVT20250613012', category: '设备故障', level: '较大', transferDept: '-', reportLevel: '-', transferTime: '-', status: '待转接', summary: '珠江路工业园A区消防设施压力异常', history: [{ time: '2025-06-12 20:30', content: '事件接报', type: 'primary' }, { time: '2025-06-12 21:00', content: '事件核实完成', type: 'success' }] },
-  { eventNo: 'EVT20250613013', category: '积水', level: '较大', transferDept: '排水管理部', reportLevel: '-', transferTime: '2025-06-13 12:00:00', status: '已转接', summary: '黄河路56号小区污水外溢', history: [{ time: '2025-06-13 11:25', content: '事件接报', type: 'primary' }, { time: '2025-06-13 11:45', content: '事件核实完成', type: 'success' }, { time: '2025-06-13 12:00', content: '转接至排水管理部', type: 'warning' }] },
-  { eventNo: 'EVT20250613014', category: '塌陷', level: '一般', transferDept: '-', reportLevel: '-', transferTime: '-', status: '待转接', summary: '中山路与解放路交叉口路灯杆倾斜', history: [{ time: '2025-06-13 13:05', content: '事件接报', type: 'primary' }, { time: '2025-06-13 13:20', content: '事件核实完成', type: 'success' }] },
-  { eventNo: 'EVT20250613015', category: '管道泄漏', level: '较大', transferDept: '市政部门', reportLevel: '市级应急办', transferTime: '2025-06-13 10:30:00', status: '已报送', summary: '长江路北侧供水管道破裂', history: [{ time: '2025-06-13 10:05', content: '事件接报', type: 'primary' }, { time: '2025-06-13 10:20', content: '事件核实完成', type: 'success' }, { time: '2025-06-13 10:30', content: '转接至市政部门', type: 'warning' }, { time: '2025-06-13 11:00', content: '报送至市级应急办', type: 'danger' }] },
-  { eventNo: 'EVT20250613016', category: '设备故障', level: '重大', transferDept: '-', reportLevel: '-', transferTime: '-', status: '待转接', summary: '幸福路排水井盖缺失', history: [{ time: '2025-06-13 09:15', content: '事件接报', type: 'primary' }, { time: '2025-06-13 09:30', content: '事件核实完成', type: 'success' }] }
-])
+import { ref, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import { getEventTransferPage, deleteEventTransfer, doTransfer, doEventReport } from '@/api/dispatch'
 
 // ==================== 推荐部门映射 ====================
 const recommendMap = {
@@ -181,47 +207,57 @@ const recommendMap = {
   '设备故障': '设备运维部'
 }
 
-// ==================== 搜索筛选 ====================
-const filters = ref({ eventNo: '', transferStatus: '', receiveDept: '' })
+// ==================== 数据状态 ====================
+const tableData = ref([])
+const total = ref(0)
+const loading = ref(false)
+const searchForm = ref({ eventNo: '', transferStatus: '', receiveDept: '' })
+const pagination = ref({ page: 1, pageSize: 10 })
 
-const filteredList = computed(() => {
-  let list = eventList.value
-  if (filters.value.eventNo) {
-    list = list.filter(o => o.eventNo.includes(filters.value.eventNo))
-  }
-  if (filters.value.transferStatus) {
-    list = list.filter(o => o.status === filters.value.transferStatus)
+// ==================== 加载数据 ====================
+async function loadData() {
+  loading.value = true
+  try {
+    const params = {}
+    if (searchForm.value.eventNo) params.eventNo = searchForm.value.eventNo
+    if (searchForm.value.transferStatus) params.status = searchForm.value.transferStatus
+    if (searchForm.value.receiveDept) params.transferDept = searchForm.value.receiveDept
+    const res = await getEventTransferPage(pagination.value.page, pagination.value.pageSize, params)
+    tableData.value = res.data?.rows || res.data?.records || res.data || []
+    total.value = res.data?.total || 0
+  } catch (error) {
+    ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
+  } finally {
+    loading.value = false
   }
-  if (filters.value.receiveDept) {
-    list = list.filter(o => o.transferDept === filters.value.receiveDept || o.transferDept === '-')
-  }
-  return list
+}
+
+onMounted(() => {
+  loadData()
 })
 
+// ==================== 搜索筛选 ====================
 function handleSearch() {
   pagination.value.page = 1
+  loadData()
 }
 
 function handleReset() {
-  filters.value = { eventNo: '', transferStatus: '', receiveDept: '' }
+  searchForm.value = { eventNo: '', transferStatus: '', receiveDept: '' }
   pagination.value.page = 1
+  loadData()
 }
 
 // ==================== 分页 ====================
-const pagination = ref({ page: 1, pageSize: 10 })
-
-const pagedList = computed(() => {
-  const start = (pagination.value.page - 1) * pagination.value.pageSize
-  return filteredList.value.slice(start, start + pagination.value.pageSize)
-})
-
 function handleSizeChange(val) {
   pagination.value.pageSize = val
   pagination.value.page = 1
+  loadData()
 }
 
 function handleCurrentChange(val) {
   pagination.value.page = val
+  loadData()
 }
 
 // ==================== 标签样式 ====================
@@ -242,15 +278,17 @@ function levelTagType(level) {
 // ==================== 转接 ====================
 const transferVisible = ref(false)
 const transferFormRef = ref(null)
+const currentTransferId = ref(null)
 const transferForm = ref({ eventNo: '', summary: '', recommendDept: '', depts: [], remark: '' })
 const transferRules = {
   depts: [{ required: true, message: '请选择转接部门', trigger: 'change', type: 'array' }]
 }
 
 function handleTransfer(row) {
+  currentTransferId.value = row.id
   transferForm.value = {
     eventNo: row.eventNo,
-    summary: row.summary,
+    summary: row.summary || row.description || '',
     recommendDept: recommendMap[row.category] || '相关部门',
     depts: [],
     remark: ''
@@ -258,52 +296,116 @@ function handleTransfer(row) {
   transferVisible.value = true
 }
 
-function confirmTransfer() {
-  transferFormRef.value.validate(valid => {
-    if (!valid) return
-    const idx = eventList.value.findIndex(o => o.eventNo === transferForm.value.eventNo)
-    if (idx > -1) {
-      eventList.value[idx].transferDept = transferForm.value.depts.join('、')
-      eventList.value[idx].transferTime = new Date().toLocaleString()
-      eventList.value[idx].status = '已转接'
-      eventList.value[idx].history.push({
-        time: new Date().toLocaleString(),
-        content: '转接至' + transferForm.value.depts.join('、'),
-        type: 'warning'
-      })
-    }
+async function confirmTransfer() {
+  try {
+    await transferFormRef.value.validate()
+  } catch {
+    return
+  }
+  try {
+    await doTransfer({
+      id: currentTransferId.value,
+      transferDept: transferForm.value.depts.join('、'),
+      remark: transferForm.value.remark
+    })
+    ElMessage.success('转接成功')
     transferVisible.value = false
-  })
+    loadData()
+  } catch (error) {
+    ElMessage.error('转接失败:' + (error.message || '未知错误'))
+  }
 }
 
 // ==================== 报送 ====================
 const reportVisible = ref(false)
 const reportFormRef = ref(null)
-const reportForm = ref({ eventNo: '', summary: '', reportTargets: [], fileList: [] })
+const uploadRef = ref(null)
+const currentReportId = ref(null)
+const submitting = ref(false)
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload'
+const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
+const reportForm = ref({ eventNo: '', summary: '', reportTargets: [], fileList: [], attachments: [] })
 const reportRules = {
   reportTargets: [{ required: true, message: '请选择报送层级', trigger: 'change', type: 'array' }]
 }
 
 function handleReport(row) {
-  reportForm.value = { eventNo: row.eventNo, summary: row.summary, reportTargets: [], fileList: [] }
+  currentReportId.value = row.id
+  reportForm.value = {
+    eventNo: row.eventNo,
+    summary: row.summary || row.description || '',
+    reportTargets: [],
+    fileList: [],
+    attachments: []
+  }
   reportVisible.value = true
 }
 
-function confirmReport() {
-  reportFormRef.value.validate(valid => {
-    if (!valid) return
-    const idx = eventList.value.findIndex(o => o.eventNo === reportForm.value.eventNo)
-    if (idx > -1) {
-      eventList.value[idx].reportLevel = reportForm.value.reportTargets.join('、')
-      eventList.value[idx].status = '已报送'
-      eventList.value[idx].history.push({
-        time: new Date().toLocaleString(),
-        content: '报送至' + reportForm.value.reportTargets.join('、'),
-        type: 'danger'
-      })
-    }
+function handleBeforeUpload(file) {
+  const isLt50M = file.size / 1024 / 1024 < 50
+  if (!isLt50M) {
+    ElMessage.error('文件大小不能超过 50MB')
+    return false
+  }
+  return true
+}
+
+function handleUploadSuccess(res) {
+  if (res.code === 200) {
+    reportForm.value.attachments.push({
+      fileName: res.originalFilename,
+      url: res.url
+    })
+  } else {
+    ElMessage.error(res.msg || '上传失败')
+  }
+}
+
+function handleUploadError() {
+  ElMessage.error('文件上传失败')
+}
+
+function handleUploadExceed() {
+  ElMessage.warning('附件数量不能超过 5 个')
+}
+
+async function confirmReport() {
+  try {
+    await reportFormRef.value.validate()
+  } catch {
+    return
+  }
+  submitting.value = true
+  try {
+    await doEventReport({
+      id: currentReportId.value,
+      reportTargets: reportForm.value.reportTargets,
+      attachments: reportForm.value.attachments
+    })
+    ElMessage.success('报送成功')
     reportVisible.value = false
-  })
+    loadData()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('报送失败:' + (error.message || '未知错误'))
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+
+// ==================== 删除 ====================
+async function handleDelete(row) {
+  try {
+    await ElMessageBox.confirm('确定要删除该转接记录吗?', '删除确认', { type: 'warning' })
+    await deleteEventTransfer(row.id)
+    ElMessage.success('删除成功')
+    loadData()
+  } catch (error) {
+    if (error !== 'cancel' && error !== 'close') {
+      ElMessage.error('删除失败:' + (error.message || '未知错误'))
+    }
+  }
 }
 
 // ==================== 详情 ====================

+ 86 - 49
src/views/subSystem/dispatch/disPlanManage/DisEventGrade.vue

@@ -34,7 +34,11 @@
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="序号" type="index" width="60" align="center" />
       <el-table-column prop="category" label="事件类别" min-width="120" />
-      <el-table-column prop="level" label="事件等级" width="100" align="center" />
+      <el-table-column prop="level" label="事件等级" width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="levelTagType(row.level)" size="small">{{ row.level }}</el-tag>
+        </template>
+      </el-table-column>
       <el-table-column prop="scenario" label="适用场景" min-width="140" />
       <el-table-column prop="criteria" label="判断标准" min-width="200" show-overflow-tooltip />
       <el-table-column prop="status" label="状态" width="90" align="center">
@@ -56,7 +60,7 @@
       v-model:current-page="pagination.page"
       v-model:page-size="pagination.size"
       :page-sizes="[10, 20, 50]"
-      :total="filteredList.length"
+      :total="tableData.length"
       layout="total, sizes, prev, pager, next, jumper"
       @size-change="onSizeChange"
       @current-change="onPageChange"
@@ -66,16 +70,21 @@
     <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close>
       <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px">
         <el-form-item label="事件类别" prop="category">
-          <el-input v-model="form.category" placeholder="请输入事件类别" />
+          <el-select v-model="form.category" placeholder="请选择事件类别" style="width: 100%">
+            <el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
+          </el-select>
         </el-form-item>
         <el-form-item label="事件等级" prop="level">
           <el-select v-model="form.level" placeholder="请选择" style="width: 100%">
             <el-option v-for="item in levelOptions" :key="item" :label="item" :value="item" />
           </el-select>
         </el-form-item>
-        <el-form-item label="事件类别" prop="subCategory">
+        <el-form-item label="类别细分" prop="subCategory">
           <el-input v-model="form.subCategory" placeholder="请输入类别细分" />
         </el-form-item>
+        <el-form-item label="适用场景" prop="scenario">
+          <el-input v-model="form.scenario" placeholder="请输入适用场景" />
+        </el-form-item>
         <el-form-item label="分级分类判断标准" prop="criteria">
           <el-input
             v-model="form.criteria"
@@ -91,7 +100,7 @@
           </el-radio-group>
         </el-form-item>
         <el-form-item label="备注说明">
-          <el-input v-model="form.remark" placeholder="请输入备注说明" />
+          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注说明" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -103,8 +112,9 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { getEventGradePage, addEventGrade, updateEventGrade, deleteEventGrade } from '@/api/dispatch'
 
 // ---------- 下拉选项 ----------
 const categoryOptions = ['管网泄漏', '路面积水', '设施损坏', '管道堵塞']
@@ -114,44 +124,37 @@ const statusOptions = ['全部', '启用', '停用']
 // ---------- 查询参数 ----------
 const queryParams = ref({ category: '', level: '', status: '' })
 
-// ---------- Mock 数据 ----------
-let idCounter = 9
-const tableData = ref([
-  { id: 1, category: '管网泄漏', level: '一级', subCategory: '主干管破裂', scenario: '管网破裂', criteria: '主干管道压力骤降超过30%,伴随地表沉降', status: '启用', remark: '' },
-  { id: 2, category: '路面积水', level: '二级', subCategory: '立交桥下积水', scenario: '暴雨导致立交桥下积水', criteria: '积水深度超过15cm且持续上涨', status: '启用', remark: '' },
-  { id: 3, category: '设施损坏', level: '三级', subCategory: '泵站故障', scenario: '排水泵站设备故障', criteria: '单台水泵停机超过4小时', status: '启用', remark: '' },
-  { id: 4, category: '管道堵塞', level: '四级', subCategory: '支管堵塞', scenario: '居民区支管堵塞', criteria: '污水外溢影响范围超3户', status: '启用', remark: '' },
-  { id: 5, category: '管网泄漏', level: '二级', subCategory: '支管接口渗漏', scenario: '管道接口渗漏', criteria: '渗漏量超过5L/min', status: '停用', remark: '已修订标准' },
-  { id: 6, category: '路面积水', level: '一级', subCategory: '低洼路段淹没', scenario: '台风暴雨低洼路段淹没', criteria: '积水深度超过50cm', status: '启用', remark: '' },
-  { id: 7, category: '设施损坏', level: '二级', subCategory: '阀门井盖缺失', scenario: '阀门井盖被盗或损坏', criteria: '主干路井盖缺失', status: '启用', remark: '' },
-  { id: 8, category: '管道堵塞', level: '三级', subCategory: '主管道淤积', scenario: '长期未清淤导致管道淤积', criteria: '管道有效过水面积减少超40%', status: '停用', remark: '' },
-])
-
-// ---------- 过滤 & 分页 ----------
-const filteredList = computed(() => {
-  return tableData.value.filter(item => {
-    if (queryParams.value.category && item.category !== queryParams.value.category) return false
-    if (queryParams.value.level && item.level !== queryParams.value.level) return false
-    if (queryParams.value.status && queryParams.value.status !== '全部' && item.status !== queryParams.value.status) return false
-    return true
-  })
-})
+// ---------- 数据 ----------
+const tableData = ref([])
+
+async function loadData() {
+  try {
+    const res = await getEventGradePage(1, 999, queryParams.value)
+    tableData.value = res?.data?.records ?? res?.data ?? res ?? []
+  } catch {
+    ElMessage.error('加载数据失败')
+  }
+}
 
+onMounted(() => { loadData() })
+
+// ---------- 分页 ----------
 const pagination = ref({ page: 1, size: 10 })
 
 const pageList = computed(() => {
   const start = (pagination.value.page - 1) * pagination.value.size
-  return filteredList.value.slice(start, start + pagination.value.size)
+  return tableData.value.slice(start, start + pagination.value.size)
 })
 
 const onSizeChange = () => { pagination.value.page = 1 }
 const onPageChange = () => {}
 
 // ---------- 搜索 / 重置 ----------
-const handleSearch = () => { pagination.value.page = 1 }
+const handleSearch = () => { pagination.value.page = 1; loadData() }
 const handleReset = () => {
   queryParams.value = { category: '', level: '', status: '' }
   pagination.value.page = 1
+  loadData()
 }
 
 // ---------- 多选 ----------
@@ -163,13 +166,14 @@ const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
 const defaultForm = () => ({
-  id: null, category: '', level: '', subCategory: '', criteria: '', status: '启用', remark: ''
+  id: null, category: '', level: '', subCategory: '', scenario: '', criteria: '', status: '启用', remark: ''
 })
 const form = ref(defaultForm())
 
 const formRules = {
   category: [{ required: true, message: '请输入事件类别', trigger: 'blur' }],
   level: [{ required: true, message: '请选择事件等级', trigger: 'change' }],
+  scenario: [{ required: true, message: '请输入适用场景', trigger: 'blur' }],
   criteria: [{ required: true, message: '请填写判断标准', trigger: 'blur' }],
   status: [{ required: true, message: '请选择状态', trigger: 'change' }],
 }
@@ -187,40 +191,73 @@ const handleEdit = (row) => {
 }
 
 const handleSubmit = () => {
-  formRef.value?.validate((valid) => {
+  formRef.value?.validate(async (valid) => {
     if (!valid) return
-    if (form.value.id) {
-      const idx = tableData.value.findIndex(i => i.id === form.value.id)
-      if (idx !== -1) tableData.value[idx] = { ...form.value }
-    } else {
-      form.value.id = ++idCounter
-      form.value.scenario = form.value.subCategory || '暂未填写'
-      tableData.value.unshift({ ...form.value })
+    try {
+      if (form.value.id) {
+        await updateEventGrade(form.value)
+      } else {
+        await addEventGrade(form.value)
+      }
+      dialogVisible.value = false
+      ElMessage.success('保存成功')
+      loadData()
+    } catch {
+      ElMessage.error('保存失败')
     }
-    dialogVisible.value = false
-    ElMessage.success('保存成功')
   })
 }
 
 // ---------- 删除 ----------
 const handleDelete = (row) => {
-  ElMessageBox.confirm('确认删除该条记录?', '提示', { type: 'warning' }).then(() => {
-    tableData.value = tableData.value.filter(i => i.id !== row.id)
-    ElMessage.success('删除成功')
+  ElMessageBox.confirm('确认删除该条记录?', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await deleteEventGrade(row.id)
+      ElMessage.success('删除成功')
+      loadData()
+    } catch {
+      ElMessage.error('删除失败')
+    }
   }).catch(() => {})
 }
 
 const handleBatchDelete = () => {
   if (!selectedRows.value.length) return ElMessage.warning('请先选择记录')
-  ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录?`, '提示', { type: 'warning' }).then(() => {
-    const ids = selectedRows.value.map(i => i.id)
-    tableData.value = tableData.value.filter(i => !ids.includes(i.id))
-    ElMessage.success('删除成功')
+  ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录?`, '提示', { type: 'warning' }).then(async () => {
+    try {
+      await Promise.all(selectedRows.value.map(i => deleteEventGrade(i.id)))
+      ElMessage.success('删除成功')
+      loadData()
+    } catch {
+      ElMessage.error('删除失败')
+    }
   }).catch(() => {})
 }
 
+function levelTagType(level) {
+  if (level === '一级') return 'danger'
+  if (level === '二级') return 'warning'
+  if (level === '三级') return 'primary'
+  return 'info'
+}
+
 // ---------- 导出 ----------
-const handleExport = () => { ElMessage.success('导出成功(Mock)') }
+const handleExport = () => {
+  if (!tableData.value.length) return ElMessage.warning('暂无数据可导出')
+  const headers = ['事件类别', '事件等级', '类别细分', '适用场景', '判断标准', '状态', '备注']
+  const keys = ['category', 'level', 'subCategory', 'scenario', 'criteria', 'status', 'remark']
+  const csvContent = [
+    '' + headers.join(','),
+    ...tableData.value.map(row => keys.map(k => '"' + (row[k] || '') + '"').join(','))
+  ].join('\n')
+  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
+  const link = document.createElement('a')
+  link.href = URL.createObjectURL(blob)
+  link.download = '应急事件分级_' + new Date().toISOString().slice(0, 10) + '.csv'
+  link.click()
+  URL.revokeObjectURL(link.href)
+  ElMessage.success('导出成功')
+}
 </script>
 
 <style scoped lang="scss">

+ 74 - 70
src/views/subSystem/dispatch/disPlanManage/DisPlanApproval.vue

@@ -4,7 +4,7 @@
     <!-- 搜索区 -->
     <el-form :model="queryParams" :inline="true" class="search-form">
       <el-form-item label="预案名称">
-        <el-input v-model="queryParams.name" placeholder="请输入预案名称" clearable style="width: 220px" />
+        <el-input v-model="queryParams.planName" placeholder="请输入预案名称" clearable style="width: 220px" />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" @click="handleSearch">搜索</el-button>
@@ -39,7 +39,7 @@
       v-model:current-page="pagination.page"
       v-model:page-size="pagination.size"
       :page-sizes="[10, 20, 50]"
-      :total="filteredList.length"
+      :total="tableData.length"
       layout="total, sizes, prev, pager, next, jumper"
       @size-change="onSizeChange"
       @current-change="onPageChange"
@@ -142,66 +142,42 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { getPlanApprovalPage, getPlanApprovalById, approvePlan, rejectPlan, publishPlan, getApprovalSteps } from '@/api/dispatch'
 
 // ---------- 查询 ----------
-const queryParams = ref({ name: '' })
-
-// ---------- Mock 数据 ----------
-const tableData = ref([
-  {
-    id: 1, planName: '排水应急管理IV级预案', planType: '应急管理预案', planLevel: 'I级预案',
-    reviewers: '专家组: 王宏,李敏,张启 领导: 陈卫国',
-    auditRecord: '专家李敏: 响应等级描述建议更精确,已采纳。领导陈卫国:同意专家评审意见,准予通过。',
-    status: '终审中', version: '2.1'
-  },
-  {
-    id: 2, planName: '排水应急管理I级预案', planType: '应急管理预案', planLevel: 'III级预案',
-    reviewers: '赵岩(安全总监), 刘景明(外部专家)',
-    auditRecord: '安全总监初审通过,专家建议增加化学冲洗工艺修订内容。',
-    status: '待修正', version: '1.3'
-  },
-  {
-    id: 3, planName: '排水应急管理II级预案', planType: '应急管理预案', planLevel: 'III级预案',
-    reviewers: '周敏(应急办), 侯宽(管控部), 何秀英(专家)',
-    auditRecord: '专家何秀英评审通过;领导要求更新标准后重新提交,已修正后复审批准。',
-    status: '已发布', version: '2.0'
-  },
-  {
-    id: 4, planName: '防汛应急预案III级', planType: '应急管理预案', planLevel: 'II级预案',
-    reviewers: '张华(安全总监), 王建(专家组)',
-    auditRecord: '安全总监初审退回,建议补充撤离路线图。',
-    status: '待修正', version: '1.1'
-  },
-  {
-    id: 5, planName: '供水应急II级预案', planType: '应急管理预案', planLevel: 'II级预案',
-    reviewers: '李明(应急办), 陈红(专家), 赵刚(领导)',
-    auditRecord: '全部环节通过,已签发归档。',
-    status: '已发布', version: '1.0'
-  },
-])
-
-// ---------- 过滤 & 分页 ----------
-const filteredList = computed(() => {
-  if (!queryParams.value.name) return tableData.value
-  const kw = queryParams.value.name.toLowerCase()
-  return tableData.value.filter(i => i.planName.toLowerCase().includes(kw))
-})
+const queryParams = ref({ planName: '' })
+
+// ---------- 数据 ----------
+const tableData = ref([])
+
+async function loadData() {
+  try {
+    const res = await getPlanApprovalPage(1, 999, queryParams.value)
+    tableData.value = res?.data?.records ?? res?.data ?? res ?? []
+  } catch {
+    ElMessage.error('加载数据失败')
+  }
+}
 
+onMounted(() => { loadData() })
+
+// ---------- 分页 ----------
 const pagination = ref({ page: 1, size: 10 })
 const pageList = computed(() => {
   const start = (pagination.value.page - 1) * pagination.value.size
-  return filteredList.value.slice(start, start + pagination.value.size)
+  return tableData.value.slice(start, start + pagination.value.size)
 })
 const onSizeChange = () => { pagination.value.page = 1 }
 const onPageChange = () => {}
 
 // ---------- 搜索 / 重置 ----------
-const handleSearch = () => { pagination.value.page = 1 }
+const handleSearch = () => { pagination.value.page = 1; loadData() }
 const handleReset = () => {
-  queryParams.value.name = ''
+  queryParams.value.planName = ''
   pagination.value.page = 1
+  loadData()
 }
 
 // ---------- 状态 Tag 类型 ----------
@@ -217,20 +193,26 @@ const currentRow = ref({})
 
 // ---------- 查看详情弹窗 ----------
 const detailVisible = ref(false)
-const handleView = (row) => {
+const handleView = async (row) => {
   currentRow.value = { ...row }
+  try {
+    const res = await getApprovalSteps(row.id)
+    currentRow.value.approvalSteps = res?.data?.records ?? res?.data ?? res ?? []
+  } catch { /* ignore */ }
   detailVisible.value = true
 }
 
 // ---------- 发布 ----------
 const handlePublish = () => {
-  ElMessageBox.confirm('确认发布该预案?发布后将在应急处置中启用。', '提示', { type: 'warning' }).then(() => {
-    const idx = tableData.value.findIndex(i => i.id === currentRow.value.id)
-    if (idx !== -1) {
-      tableData.value[idx].status = '已发布'
+  ElMessageBox.confirm('确认发布该预案?发布后将在应急处置中启用。', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await publishPlan(currentRow.value.id)
       currentRow.value.status = '已发布'
+      ElMessage.success('发布成功')
+      loadData()
+    } catch {
+      ElMessage.error('发布失败')
     }
-    ElMessage.success('发布成功')
   }).catch(() => {})
 }
 
@@ -238,28 +220,50 @@ const handlePublish = () => {
 const approveVisible = ref(false)
 const approveSteps = ref([])
 
-const handleApprove = (row) => {
+const handleApprove = async (row) => {
   currentRow.value = { ...row }
-  approveSteps.value = [
-    { title: '环节一:预案起草人提交', approver: '安全总监', permission: '初审/退回', colorClass: 'blue', opinion: '' },
-    { title: '环节二:专家技术评审', approver: '专家组(3人)', permission: '修正建议/通过', colorClass: 'green', opinion: '' },
-    { title: '环节三:领导层终审', approver: '应急总指挥', permission: '批准/驳回', colorClass: 'blue', opinion: '' },
-  ]
+  try {
+    const res = await getApprovalSteps(row.id)
+    const steps = res?.data?.records ?? res?.data ?? res ?? []
+    if (steps.length > 0) {
+      approveSteps.value = steps
+    } else {
+      approveSteps.value = [
+        { title: '环节一:预案起草人提交', approver: '安全总监', permission: '初审/退回', colorClass: 'blue', opinion: '' },
+        { title: '环节二:专家技术评审', approver: '专家组(3人)', permission: '修正建议/通过', colorClass: 'green', opinion: '' },
+        { title: '环节三:领导层终审', approver: '应急总指挥', permission: '批准/驳回', colorClass: 'blue', opinion: '' },
+      ]
+    }
+  } catch {
+    approveSteps.value = [
+      { title: '环节一:预案起草人提交', approver: '安全总监', permission: '初审/退回', colorClass: 'blue', opinion: '' },
+      { title: '环节二:专家技术评审', approver: '专家组(3人)', permission: '修正建议/通过', colorClass: 'green', opinion: '' },
+      { title: '环节三:领导层终审', approver: '应急总指挥', permission: '批准/驳回', colorClass: 'blue', opinion: '' },
+    ]
+  }
   approveVisible.value = true
 }
 
-const handleReject = () => {
-  approveVisible.value = false
-  const idx = tableData.value.findIndex(i => i.id === currentRow.value.id)
-  if (idx !== -1) tableData.value[idx].status = '待修正'
-  ElMessage.success('已驳回,状态变更为待修正')
+const handleReject = async () => {
+  try {
+    await rejectPlan(currentRow.value.id)
+    approveVisible.value = false
+    ElMessage.success('已驳回,状态变更为待修正')
+    loadData()
+  } catch {
+    ElMessage.error('驳回失败')
+  }
 }
 
-const handleComplete = () => {
-  approveVisible.value = false
-  const idx = tableData.value.findIndex(i => i.id === currentRow.value.id)
-  if (idx !== -1) tableData.value[idx].status = '终审中'
-  ElMessage.success('审批完成')
+const handleComplete = async () => {
+  try {
+    await approvePlan(currentRow.value.id)
+    approveVisible.value = false
+    ElMessage.success('审批完成')
+    loadData()
+  } catch {
+    ElMessage.error('审批失败')
+  }
 }
 </script>
 

+ 158 - 87
src/views/subSystem/dispatch/disPlanManage/DisPlanEditing.vue

@@ -4,7 +4,7 @@
     <!-- 顶部操作区 -->
     <el-form :model="queryParams" :inline="true" class="search-form">
       <el-form-item label="预案名称">
-        <el-input v-model="queryParams.name" placeholder="请输入预案名称" clearable style="width: 200px" />
+        <el-input v-model="queryParams.planName" placeholder="请输入预案名称" clearable style="width: 200px" />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" @click="handleSearch">搜索</el-button>
@@ -27,7 +27,6 @@
           ref="treeRef"
           :data="treeData"
           :props="treeProps"
-          show-checkbox
           node-key="id"
           default-expand-all
           highlight-current
@@ -45,16 +44,19 @@
           <el-table-column prop="planLevel" label="预案级别" width="100" align="center" />
           <el-table-column prop="attachName" label="附件名称" min-width="200" show-overflow-tooltip />
           <el-table-column label="附件下载" width="90" align="center">
-            <template #default>
-              <el-button link type="primary" @click="handleDownload"><el-icon><Download /></el-icon></el-button>
+            <template #default="{ row }">
+              <el-button link type="primary" @click="handleDownload(row)" :disabled="!row.attachPath">
+                <el-icon><Download /></el-icon>
+              </el-button>
             </template>
           </el-table-column>
           <el-table-column prop="publishDate" label="发布时间" width="120" align="center" />
           <el-table-column prop="compileUnit" label="编制单位" width="100" />
-          <el-table-column label="操作" width="140" align="center">
+          <el-table-column label="操作" width="200" align="center">
             <template #default="{ row }">
               <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
               <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+              <el-button v-if="row.status !== '已发布'" link type="success" @click="handleSubmitApproval(row)">提交审批</el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -65,7 +67,7 @@
           v-model:current-page="pagination.page"
           v-model:page-size="pagination.size"
           :page-sizes="[10, 20, 50]"
-          :total="filteredList.length"
+          :total="tableData.length"
           layout="total, sizes, prev, pager, next, jumper"
           @size-change="onSizeChange"
           @current-change="onPageChange"
@@ -76,6 +78,18 @@
     <!-- 新增/修改预案弹窗 -->
     <el-dialog v-model="dialogVisible" :title="dialogTitle" width="580px" destroy-on-close>
       <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+        <el-form-item label="预案分类" prop="categoryId">
+          <el-tree-select
+            v-model="form.categoryId"
+            :data="treeData"
+            :props="treeProps"
+            node-key="id"
+            placeholder="请选择预案分类"
+            check-strictly
+            :render-after-expand="false"
+            style="width: 100%"
+          />
+        </el-form-item>
         <el-form-item label="预案名称" prop="planName">
           <el-input v-model="form.planName" placeholder="请输入预案名称" />
         </el-form-item>
@@ -95,18 +109,19 @@
         </el-form-item>
         <el-form-item label="附件上传">
           <el-upload
-            class="upload-area"
-            action="#"
-            :auto-upload="false"
+            ref="uploadRef"
+            :action="uploadUrl"
+            :headers="uploadHeaders"
             :limit="1"
-            accept=".doc"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
             :on-exceed="() => ElMessage.warning('只能上传一个文件')"
             :file-list="form.fileList"
-            @change="handleFileChange"
+            :before-upload="handleBeforeUpload"
           >
             <el-button type="primary">添加文件</el-button>
           </el-upload>
-          <div class="upload-tip">只能上传文件类型为.doc的文件,且不超过500kb</div>
+          <div class="upload-tip">支持上传任意格式文件,单个文件不超过 50MB</div>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -118,9 +133,11 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Download } from '@element-plus/icons-vue'
+import { getToken } from '@/utils/auth'
+import { getEventPlanPage, getEventPlanById, addEventPlan, updateEventPlan, deleteEventPlan, getPlanCategoryTree, createPlanApproval } from '@/api/dispatch'
 
 // ---------- 常量 ----------
 const levelOptions = ['一级', '二级', '三级', '四级']
@@ -128,71 +145,61 @@ const levelOptions = ['一级', '二级', '三级', '四级']
 // ---------- 分类树 ----------
 const treeRef = ref(null)
 const treeProps = { children: 'children', label: 'label' }
-const treeData = ref([
-  {
-    id: 1, label: 'Trunk 节点',
-    children: [
-      {
-        id: 10, label: 'Branch 节点 - 应急管理',
-        children: [
-          { id: 101, label: 'Leaf 节点 - 排水应急' },
-          { id: 102, label: 'Leaf 节点 - 供水应急' },
-        ]
-      },
-      {
-        id: 20, label: 'Branch 节点 - 工程预案',
-        children: [
-          { id: 201, label: 'Leaf 节点 - 管道抢修' },
-          { id: 202, label: 'Leaf 节点 - 设施维护' },
-        ]
-      },
-      {
-        id: 30, label: 'Branch 节点 - 专项预案',
-        children: [
-          { id: 301, label: 'Leaf 节点 - 防汛预案' },
-        ]
-      }
-    ]
+const treeData = ref([])
+
+async function loadCategoryTree() {
+  try {
+    const res = await getPlanCategoryTree()
+    treeData.value = res?.data ?? res ?? []
+  } catch {
+    ElMessage.error('加载分类树失败')
   }
-])
+}
 
 const selectedTreeNode = ref(null)
-const handleNodeClick = (node) => { selectedTreeNode.value = node }
+const handleNodeClick = (node) => {
+  selectedTreeNode.value = node
+  pagination.value.page = 1
+  loadData()
+}
 
 // ---------- 查询 ----------
-const queryParams = ref({ name: '' })
-
-// ---------- Mock 数据 ----------
-let idCounter = 7
-const tableData = ref([
-  { id: 1, planName: '排水应急管理IV级预案', planType: '应急管理预案', planLevel: '四级', attachName: '排水应急行动指南IV级预案.docx', publishDate: '2026.05.19', compileUnit: '排水公司', version: '1.0', fileList: [] },
-  { id: 2, planName: '排水应急管理I级预案', planType: '应急管理预案', planLevel: '一级', attachName: '排水应急I级预案.docx', publishDate: '2026.05.19', compileUnit: '燃气公司', version: '2.0', fileList: [] },
-  { id: 3, planName: '管道抢修III级预案', planType: '工程预案', planLevel: '三级', attachName: '管道抢修III级预案.doc', publishDate: '2026.04.10', compileUnit: '供水公司', version: '1.0', fileList: [] },
-  { id: 4, planName: '防汛应急预案II级', planType: '应急管理预案', planLevel: '二级', attachName: '防汛应急II级预案.doc', publishDate: '2026.03.22', compileUnit: '排水公司', version: '1.1', fileList: [] },
-  { id: 5, planName: '设施维护专项预案', planType: '工程预案', planLevel: '三级', attachName: '设施维护预案.doc', publishDate: '2026.02.15', compileUnit: '供水公司', version: '1.0', fileList: [] },
-  { id: 6, planName: '供水应急管理II级预案', planType: '应急管理预案', planLevel: '二级', attachName: '供水应急II级预案.docx', publishDate: '2026.01.08', compileUnit: '供水公司', version: '1.0', fileList: [] },
-])
-
-// ---------- 过滤 & 分页 ----------
-const filteredList = computed(() => {
-  if (!queryParams.value.name) return tableData.value
-  const kw = queryParams.value.name.toLowerCase()
-  return tableData.value.filter(i => i.planName.toLowerCase().includes(kw))
-})
+const queryParams = ref({ planName: '' })
 
+// ---------- 数据 ----------
+const tableData = ref([])
+
+async function loadData() {
+  try {
+    const params = { ...queryParams.value }
+    if (selectedTreeNode.value) {
+      params.categoryId = selectedTreeNode.value.id
+    }
+    const res = await getEventPlanPage(1, 999, params)
+    tableData.value = res?.data?.records ?? res?.data ?? res ?? []
+  } catch {
+    ElMessage.error('加载数据失败')
+  }
+}
+
+onMounted(() => { loadData(); loadCategoryTree() })
+
+// ---------- 分页 ----------
 const pagination = ref({ page: 1, size: 10 })
 const pageList = computed(() => {
   const start = (pagination.value.page - 1) * pagination.value.size
-  return filteredList.value.slice(start, start + pagination.value.size)
+  return tableData.value.slice(start, start + pagination.value.size)
 })
 const onSizeChange = () => { pagination.value.page = 1 }
 const onPageChange = () => {}
 
 // ---------- 搜索 / 重置 ----------
-const handleSearch = () => { pagination.value.page = 1 }
+const handleSearch = () => { pagination.value.page = 1; loadData() }
 const handleReset = () => {
-  queryParams.value.name = ''
+  queryParams.value.planName = ''
+  selectedTreeNode.value = null
   pagination.value.page = 1
+  loadData()
 }
 
 // ---------- 多选 ----------
@@ -204,7 +211,7 @@ const dialogVisible = ref(false)
 const dialogTitle = ref('新增预案')
 const formRef = ref(null)
 const defaultForm = () => ({
-  id: null, planName: '', planLevel: '', planType: '', version: '1.0', compileUnit: '', attachName: '', fileList: []
+  id: null, planName: '', planLevel: '', planType: '', version: '1.0', compileUnit: '', categoryId: selectedTreeNode.value?.id || null, attachName: '', attachPath: '', fileList: []
 })
 const form = ref(defaultForm())
 
@@ -214,6 +221,9 @@ const formRules = {
   planType: [{ required: true, message: '请输入预案类型', trigger: 'blur' }],
 }
 
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload'
+const uploadHeaders = { Authorization: 'Bearer ' + getToken() }
+
 const handleAdd = () => {
   form.value = defaultForm()
   dialogTitle.value = '新增预案'
@@ -221,54 +231,115 @@ const handleAdd = () => {
 }
 
 const handleEdit = (row) => {
-  form.value = { ...row, fileList: row.attachName ? [{ name: row.attachName, status: 'success' }] : [] }
+  form.value = {
+    ...row,
+    fileList: row.attachName ? [{ name: row.attachName, url: row.attachPath }] : []
+  }
   dialogTitle.value = '修改预案'
   dialogVisible.value = true
 }
 
-const handleFileChange = (file) => {
-  if (file && file.name) {
-    form.value.attachName = file.name
-    form.value.fileList = [file]
+function handleBeforeUpload(file) {
+  const isLt50M = file.size / 1024 / 1024 < 50
+  if (!isLt50M) {
+    ElMessage.error('文件大小不能超过 50MB')
+    return false
   }
+  return true
+}
+
+function handleUploadSuccess(res) {
+  if (res.code === 200) {
+    form.value.attachName = res.originalFilename
+    form.value.attachPath = res.url
+  } else {
+    ElMessage.error(res.msg || '上传失败')
+  }
+}
+
+function handleUploadError() {
+  ElMessage.error('文件上传失败')
 }
 
 const handleSubmit = () => {
-  formRef.value?.validate((valid) => {
+  formRef.value?.validate(async (valid) => {
     if (!valid) return
-    if (form.value.id) {
-      const idx = tableData.value.findIndex(i => i.id === form.value.id)
-      if (idx !== -1) tableData.value[idx] = { ...form.value }
-    } else {
-      form.value.id = ++idCounter
-      form.value.publishDate = new Date().toISOString().slice(0, 10).replace(/-/g, '.')
-      tableData.value.unshift({ ...form.value })
+    try {
+      if (form.value.id) {
+        await updateEventPlan(form.value)
+      } else {
+        await addEventPlan(form.value)
+      }
+      dialogVisible.value = false
+      ElMessage.success('保存成功')
+      loadData()
+    } catch {
+      ElMessage.error('保存失败')
     }
-    dialogVisible.value = false
-    ElMessage.success('保存成功')
   })
 }
 
 // ---------- 删除 ----------
 const handleDelete = (row) => {
-  ElMessageBox.confirm('确认删除该条预案?', '提示', { type: 'warning' }).then(() => {
-    tableData.value = tableData.value.filter(i => i.id !== row.id)
-    ElMessage.success('删除成功')
+  ElMessageBox.confirm('确认删除该条预案?', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await deleteEventPlan(row.id)
+      ElMessage.success('删除成功')
+      loadData()
+    } catch {
+      ElMessage.error('删除失败')
+    }
   }).catch(() => {})
 }
 
 const handleBatchDelete = () => {
   if (!selectedRows.value.length) return ElMessage.warning('请先选择记录')
-  ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条预案?`, '提示', { type: 'warning' }).then(() => {
-    const ids = selectedRows.value.map(i => i.id)
-    tableData.value = tableData.value.filter(i => !ids.includes(i.id))
-    ElMessage.success('删除成功')
+  ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条预案?`, '提示', { type: 'warning' }).then(async () => {
+    try {
+      await Promise.all(selectedRows.value.map(i => deleteEventPlan(i.id)))
+      ElMessage.success('删除成功')
+      loadData()
+    } catch {
+      ElMessage.error('删除失败')
+    }
   }).catch(() => {})
 }
 
 // ---------- 下载 / 导出 ----------
-const handleDownload = () => { ElMessage.success('附件下载(Mock)') }
-const handleExport = () => { ElMessage.success('导出成功(Mock)') }
+const handleDownload = (row) => {
+  if (!row.attachPath) return
+  window.open(row.attachPath, '_blank')
+}
+
+const handleExport = () => {
+  if (!tableData.value.length) return ElMessage.warning('暂无数据可导出')
+  const headers = ['预案名称', '预案类型', '预案级别', '附件名称', '发布时间', '编制单位']
+  const keys = ['planName', 'planType', 'planLevel', 'attachName', 'publishDate', 'compileUnit']
+  const csvContent = [
+    '' + headers.join(','),
+    ...tableData.value.map(row => keys.map(k => '"' + (row[k] || '') + '"').join(','))
+  ].join('\n')
+  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
+  const link = document.createElement('a')
+  link.href = URL.createObjectURL(blob)
+  link.download = '应急预案_' + new Date().toISOString().slice(0, 10) + '.csv'
+  link.click()
+  URL.revokeObjectURL(link.href)
+  ElMessage.success('导出成功')
+}
+
+const handleSubmitApproval = (row) => {
+  ElMessageBox.confirm('确认将该预案提交审批?', '提示', { type: 'warning' }).then(async () => {
+    try {
+      await createPlanApproval(row.id)
+      ElMessage.success('提交审批成功')
+      loadData()
+    } catch {
+      ElMessage.error('提交审批失败')
+    }
+  }).catch(() => {})
+}
+
 const handleDownloadTemplate = () => { ElMessage.success('模版下载成功(Mock)') }
 </script>