Browse Source

应急调度系统静态页面

LAPTOP-JI2IUVG1\26646 1 month ago
parent
commit
4deb03bbc6

+ 314 - 6
src/views/subSystem/dispatch/disAssess/DisInquiry.vue

@@ -1,13 +1,321 @@
 <!--数据查询与统计-->
-<script setup>
+<template>
+  <div class="app-container">
+    <div class="inquiry-layout">
+      <!-- 左侧条件面板 -->
+      <div class="left-panel">
+        <el-card shadow="hover">
+          <template #header>
+            <div class="panel-title">多维条件查询</div>
+          </template>
+          <el-form :model="queryForm" label-width="80px" label-position="top" size="default">
+            <el-form-item label="事件类型">
+              <el-select v-model="queryForm.eventType" placeholder="请选择" clearable style="width: 100%">
+                <el-option v-for="item in eventTypeOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="发生时间">
+              <el-date-picker v-model="queryForm.eventTime" type="date" placeholder="请选择日期"
+                value-format="YYYY-MM-DD" style="width: 100%" clearable />
+            </el-form-item>
+            <el-form-item label="事件等级">
+              <el-select v-model="queryForm.eventLevel" placeholder="请选择" clearable style="width: 100%">
+                <el-option v-for="item in eventLevelOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="地域">
+              <el-select v-model="queryForm.region" placeholder="请选择" clearable style="width: 100%">
+                <el-option v-for="item in regionOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="责任部门">
+              <el-select v-model="queryForm.department" placeholder="请选择" clearable style="width: 100%">
+                <el-option v-for="item in departmentOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="损失情况">
+              <el-select v-model="queryForm.lossRange" placeholder="请选择" clearable style="width: 100%">
+                <el-option v-for="item in lossRangeOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleQuery" style="width: 100%">执行查询</el-button>
+            </el-form-item>
+            <el-form-item>
+              <el-button @click="handleReset" style="width: 100%">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
 
-</script>
+      <!-- 右侧结果区 -->
+      <div class="right-panel">
+        <!-- 统计卡片行 -->
+        <div class="stat-cards">
+          <div class="stat-card">
+            <div class="stat-value">{{ stats.totalEvents }}</div>
+            <div class="stat-label">事件总数</div>
+          </div>
+          <div class="stat-card stat-card-warning">
+            <div class="stat-value">{{ stats.totalLoss }}</div>
+            <div class="stat-label">直接经济损失(万元)</div>
+          </div>
+          <div class="stat-card stat-card-info">
+            <div class="stat-value">{{ stats.avgLoss }}</div>
+            <div class="stat-label">平均损失(万元)</div>
+          </div>
+          <div class="stat-card stat-card-danger">
+            <div class="stat-value">{{ stats.majorEvents }}</div>
+            <div class="stat-label">重大及以上事件数</div>
+          </div>
+        </div>
 
-<template>
-<div>5.2.3.5.1. 数据查询与统计
-  事件查询用列表的方式展示,对各级各类突发事件数据按照类型、时间、等级、地域、责任部门、损失情况六个维度进行单一条件、组合条件和模糊匹配三种方式的查询;对各级各类突发事件数据按照类型、时间、等级、地域、责任部门、损失情况六个维度进行汇总统计及分析。</div>
+        <!-- 事件列表表格 -->
+        <el-card shadow="hover">
+          <template #header>
+            <div class="panel-title">事件列表 <span class="total-count">共{{ filteredData.length }}条数据</span></div>
+          </template>
+          <el-table :data="pagedData" border stripe style="width: 100%" size="default">
+            <el-table-column prop="name" label="事件名称" min-width="180" show-overflow-tooltip />
+            <el-table-column prop="type" label="类型" width="120" />
+            <el-table-column prop="time" label="时间" width="140" />
+            <el-table-column prop="level" label="等级" width="100">
+              <template #default="{ row }">
+                <el-tag :type="levelTagType(row.level)" size="small" effect="dark">{{ row.level }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="region" label="地域" width="110" />
+            <el-table-column prop="department" label="责任部门" width="130" />
+            <el-table-column prop="loss" label="损失(万元)" width="110" align="right" />
+          </el-table>
+          <el-pagination
+            v-model:current-page="pageNum"
+            v-model:page-size="pageSize"
+            :total="filteredData.length"
+            layout="total, prev, pager, next, sizes"
+            :page-sizes="[10, 20, 50]"
+            style="margin-top: 16px; justify-content: flex-end"
+          />
+        </el-card>
+      </div>
+    </div>
+  </div>
 </template>
 
+<script setup>
+import { ref, reactive, computed } from 'vue'
+
+// 查询条件选项
+const eventTypeOptions = ['全部', '自然灾害', '事故灾难', '公共卫生', '社会安全', '管网泄漏', '管道堵塞']
+const eventLevelOptions = ['全部', '一般', '较大', '重大', '特别重大']
+const regionOptions = ['全部', '太常片区', '城东片区', '城南片区', '城北片区']
+const departmentOptions = ['全部', '应急管理局', '消防救援局', '市政管理局', '水务局']
+const lossRangeOptions = ['全部', '100万以下', '100-500万', '500-1000万', '1000万以上']
+
+// 查询表单
+const queryForm = reactive({
+  eventType: '',
+  eventTime: '',
+  eventLevel: '',
+  region: '',
+  department: '',
+  lossRange: ''
+})
+
+// Mock 数据
+const allData = ref([
+  { id: 1, name: '太常片区暴雨内涝事件', type: '自然灾害', time: '2025-06-15', level: '重大', region: '太常片区', department: '应急管理局', loss: 860 },
+  { id: 2, name: '城东主管道泄漏事故', type: '管网泄漏', time: '2025-05-22', level: '较大', region: '城东片区', department: '水务局', loss: 320 },
+  { id: 3, name: '城南化工厂氯气泄漏', type: '事故灾难', time: '2025-04-10', level: '特别重大', region: '城南片区', department: '消防救援局', loss: 1580 },
+  { id: 4, name: '城北管道堵塞溢流', type: '管道堵塞', time: '2025-07-03', level: '一般', region: '城北片区', department: '市政管理局', loss: 45 },
+  { id: 5, name: '太常片区燃气管道破裂', type: '管网泄漏', time: '2025-03-18', level: '重大', region: '太常片区', department: '应急管理局', loss: 720 },
+  { id: 6, name: '城东餐饮食物中毒', type: '公共卫生', time: '2025-08-01', level: '较大', region: '城东片区', department: '应急管理局', loss: 180 },
+  { id: 7, name: '城南地铁施工坍塌', type: '事故灾难', time: '2025-02-14', level: '重大', region: '城南片区', department: '消防救援局', loss: 950 },
+  { id: 8, name: '城北片区群体性事件', type: '社会安全', time: '2025-09-20', level: '一般', region: '城北片区', department: '应急管理局', loss: 60 },
+  { id: 9, name: '太常片区排水管网堵塞', type: '管道堵塞', time: '2025-05-08', level: '较大', region: '太常片区', department: '市政管理局', loss: 260 },
+  { id: 10, name: '城东供水主管爆裂', type: '管网泄漏', time: '2025-01-25', level: '特别重大', region: '城东片区', department: '水务局', loss: 2100 },
+  { id: 11, name: '城南片区暴雨内涝', type: '自然灾害', time: '2025-07-18', level: '较大', region: '城南片区', department: '应急管理局', loss: 480 },
+  { id: 12, name: '城北燃气泄漏事故', type: '管网泄漏', time: '2025-04-28', level: '一般', region: '城北片区', department: '消防救援局', loss: 88 },
+  { id: 13, name: '太常片区路面塌陷', type: '事故灾难', time: '2025-06-30', level: '重大', region: '太常片区', department: '市政管理局', loss: 680 },
+  { id: 14, name: '城东排水管道堵塞', type: '管道堵塞', time: '2025-03-05', level: '一般', region: '城东片区', department: '水务局', loss: 35 },
+  { id: 15, name: '城南供水管网破裂', type: '管网泄漏', time: '2025-08-12', level: '重大', region: '城南片区', department: '水务局', loss: 780 },
+  { id: 16, name: '城北片区洪涝灾害', type: '自然灾害', time: '2025-09-05', level: '特别重大', region: '城北片区', department: '应急管理局', loss: 1850 }
+])
+
+// 分页
+const pageNum = ref(1)
+const pageSize = ref(10)
+
+// 根据条件筛选
+const filteredData = computed(() => {
+  let data = [...allData.value]
+  if (queryForm.eventType && queryForm.eventType !== '全部') {
+    data = data.filter(d => d.type === queryForm.eventType)
+  }
+  if (queryForm.eventTime) {
+    data = data.filter(d => d.time === queryForm.eventTime)
+  }
+  if (queryForm.eventLevel && queryForm.eventLevel !== '全部') {
+    data = data.filter(d => d.level === queryForm.eventLevel)
+  }
+  if (queryForm.region && queryForm.region !== '全部') {
+    data = data.filter(d => d.region === queryForm.region)
+  }
+  if (queryForm.department && queryForm.department !== '全部') {
+    data = data.filter(d => d.department === queryForm.department)
+  }
+  if (queryForm.lossRange && queryForm.lossRange !== '全部') {
+    data = data.filter(d => {
+      switch (queryForm.lossRange) {
+        case '100万以下': return d.loss < 100
+        case '100-500万': return d.loss >= 100 && d.loss < 500
+        case '500-1000万': return d.loss >= 500 && d.loss < 1000
+        case '1000万以上': return d.loss >= 1000
+        default: return true
+      }
+    })
+  }
+  return data
+})
+
+// 分页后数据
+const pagedData = computed(() => {
+  const start = (pageNum.value - 1) * pageSize.value
+  return filteredData.value.slice(start, start + pageSize.value)
+})
+
+// 统计数据
+const stats = computed(() => {
+  const data = filteredData.value
+  const totalEvents = data.length
+  const totalLoss = data.reduce((sum, d) => sum + d.loss, 0)
+  const avgLoss = totalEvents > 0 ? (totalLoss / totalEvents).toFixed(1) : 0
+  const majorEvents = data.filter(d => d.level === '重大' || d.level === '特别重大').length
+  return { totalEvents, totalLoss, avgLoss, majorEvents }
+})
+
+// 等级tag颜色
+const levelTagType = (level) => {
+  const map = { '一般': 'info', '较大': 'warning', '重大': 'danger', '特别重大': 'danger' }
+  return map[level] || 'info'
+}
+
+// 查询
+const handleQuery = () => {
+  pageNum.value = 1
+}
+
+// 重置
+const handleReset = () => {
+  Object.assign(queryForm, {
+    eventType: '', eventTime: '', eventLevel: '', region: '', department: '', lossRange: ''
+  })
+  pageNum.value = 1
+}
+</script>
+
 <style scoped lang="scss">
+.app-container {
+  padding: 16px;
+}
+
+.inquiry-layout {
+  display: flex;
+  gap: 16px;
+  min-height: calc(100vh - 120px);
+}
+
+.left-panel {
+  width: 25%;
+  min-width: 260px;
+  flex-shrink: 0;
+
+  .el-card {
+    height: 100%;
+  }
+}
+
+.right-panel {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.panel-title {
+  font-weight: 600;
+  font-size: 15px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+
+  .total-count {
+    font-weight: 400;
+    font-size: 13px;
+    color: #909399;
+  }
+}
+
+// 统计卡片
+.stat-cards {
+  display: flex;
+  gap: 16px;
+}
+
+.stat-card {
+  flex: 1;
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  border: 1px solid #ebeef5;
+  text-align: center;
+  transition: box-shadow 0.3s;
+
+  &:hover {
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  }
+
+  .stat-value {
+    font-size: 28px;
+    font-weight: 700;
+    color: #303133;
+    line-height: 1.2;
+  }
+
+  .stat-label {
+    font-size: 13px;
+    color: #909399;
+    margin-top: 8px;
+  }
+}
+
+.stat-card-warning {
+  border-left: 3px solid #e6a23c;
+
+  .stat-value { color: #e6a23c; }
+}
+
+.stat-card-info {
+  border-left: 3px solid #409eff;
+
+  .stat-value { color: #409eff; }
+}
+
+.stat-card-danger {
+  border-left: 3px solid #f56c6c;
+
+  .stat-value { color: #f56c6c; }
+}
+
+// 左侧表单间距调整
+.left-panel {
+  :deep(.el-form-item) {
+    margin-bottom: 16px;
+  }
 
-</style>
+  :deep(.el-form-item__label) {
+    padding-bottom: 4px;
+  }
+}
+</style>

+ 419 - 6
src/views/subSystem/dispatch/disAssess/DisSitAnalysis.vue

@@ -1,12 +1,425 @@
-<!-- 应急态势分析-->
-<script setup>
+<!--应急态势分析-->
+<template>
+  <div class="app-container">
+    <div class="analysis-layout">
+      <!-- 左侧条件面板 -->
+      <div class="left-panel">
+        <el-card shadow="hover">
+          <template #header>
+            <div class="panel-title">专题态势分析</div>
+          </template>
+          <el-form :model="analysisForm" label-width="100px" label-position="top" size="default">
+            <el-form-item label="事件类型">
+              <el-select v-model="analysisForm.eventType" placeholder="请选择" style="width: 100%">
+                <el-option v-for="item in eventTypeOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="分析时间范围">
+              <el-select v-model="analysisForm.timeRange" placeholder="请选择" style="width: 100%">
+                <el-option v-for="item in timeRangeOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="地域">
+              <el-select v-model="analysisForm.region" placeholder="请选择" style="width: 100%">
+                <el-option v-for="item in regionOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleAnalysis" style="width: 100%">分析</el-button>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="success" plain @click="handleExport" style="width: 100%">导出分析报告</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
 
-</script>
+      <!-- 右侧分析展示区 -->
+      <div class="right-panel">
+        <!-- 顶部统计卡片 -->
+        <div class="stat-cards">
+          <div class="stat-card">
+            <div class="stat-label">综合风险指数</div>
+            <div class="stat-value-row">
+              <span class="stat-number">{{ riskData.index }}</span>
+              <span class="stat-trend" :class="riskData.indexTrend > 0 ? 'trend-up' : 'trend-down'">
+                <el-icon v-if="riskData.indexTrend > 0"><Top /></el-icon>
+                <el-icon v-else><Bottom /></el-icon>
+                {{ Math.abs(riskData.indexTrend) }}%
+              </span>
+            </div>
+          </div>
+          <div class="stat-card stat-card-warning">
+            <div class="stat-label">预警事件数</div>
+            <div class="stat-value-row">
+              <span class="stat-number">{{ riskData.warnCount }}</span>
+            </div>
+          </div>
+          <div class="stat-card stat-card-info">
+            <div class="stat-label">应急覆盖率</div>
+            <div class="stat-value-row">
+              <span class="stat-number">{{ riskData.coverRate }}%</span>
+              <span class="stat-trend" :class="riskData.coverTrend > 0 ? 'trend-up' : 'trend-down'">
+                <el-icon v-if="riskData.coverTrend > 0"><Top /></el-icon>
+                <el-icon v-else><Bottom /></el-icon>
+                {{ Math.abs(riskData.coverTrend) }}%
+              </span>
+            </div>
+          </div>
+          <div class="stat-card stat-card-danger">
+            <div class="stat-label">超期管网占比</div>
+            <div class="stat-value-row">
+              <span class="stat-number">{{ riskData.overdueRate }}%</span>
+            </div>
+          </div>
+        </div>
 
-<template>
-<div>根据管网实际管理需求,制定专题态势分析,为制定有针对性的专题应急预案提供依据。</div>
+        <!-- 管网压力负荷趋势 -->
+        <el-card shadow="hover" class="trend-card">
+          <template #header>
+            <div class="card-header-row">
+              <span class="panel-title">管网压力负荷趋势</span>
+              <el-date-picker v-model="trendDate" type="date" placeholder="选择时间"
+                value-format="YYYY-MM-DD" style="width: 180px" size="default" />
+            </div>
+          </template>
+          <div class="circle-grid">
+            <div v-for="(item, idx) in monthData" :key="idx" class="circle-item">
+              <el-progress type="circle" :percentage="item.percentage" :width="90" :stroke-width="8"
+                :color="getCircleColor(item.percentage)">
+                <template #default>
+                  <div class="circle-text">
+                    <div class="circle-value">{{ item.percentage }}%</div>
+                  </div>
+                </template>
+              </el-progress>
+              <div class="circle-month">{{ item.month }}</div>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 分析报告 -->
+        <el-card shadow="hover" class="report-card">
+          <template #header>
+            <div class="panel-title">分析报告</div>
+          </template>
+          <div class="report-content">
+            <div class="report-section">
+              <h4>区域过滤信息</h4>
+              <p>当前分析区域:<strong>{{ analysisForm.region || '全部' }}</strong>,事件类型:<strong>{{ analysisForm.eventType }}</strong>,时间范围:<strong>{{ analysisForm.timeRange }}</strong>。</p>
+            </div>
+            <div class="report-section">
+              <h4>风险指数分析</h4>
+              <p>综合风险指数为 <strong>{{ riskData.index }}</strong>,较上期{{ riskData.indexTrend > 0 ? '上升' : '下降' }} {{ Math.abs(riskData.indexTrend) }}%。当前管网系统整体风险处于{{ riskData.index > 15 ? '较高' : riskData.index > 8 ? '中等' : '较低' }}水平,建议加强对高风险区域的监控频次和应急资源前置部署。</p>
+              <p>预警事件共计 <strong>{{ riskData.warnCount }}</strong> 起,超期管网占比 <strong>{{ riskData.overdueRate }}%</strong>,需重点关注管网老化区域及压力异常管段。</p>
+            </div>
+            <div class="report-section">
+              <h4>应急资源覆盖分析</h4>
+              <p>当前应急覆盖率为 <strong>{{ riskData.coverRate }}%</strong>,较上期{{ riskData.coverTrend > 0 ? '上升' : '下降' }} {{ Math.abs(riskData.coverTrend) }}%。{{ riskData.coverRate < 80 ? '覆盖率低于80%,存在应急盲区,建议增补应急站点和物资储备。' : '覆盖率处于合理区间,应继续保持并优化薄弱环节。' }}</p>
+              <p><strong>建议:</strong>{{ riskData.coverRate < 80 ? '1. 在城东片区增设应急物资储备点;2. 优化城南片区应急力量布局,缩短响应时间;3. 加强太常片区管网巡检频次。' : '1. 持续优化应急资源分配机制;2. 加强跨区域应急协作演练;3. 定期评估覆盖盲区并动态调整。' }}</p>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+  </div>
 </template>
 
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Top, Bottom } from '@element-plus/icons-vue'
+
+// 下拉选项
+const eventTypeOptions = ['漏失风险态势', '管网压力态势', '积水内涝态势', '设备故障态势']
+const timeRangeOptions = ['最近一周', '最近一月', '最近三月', '最近半年', '最近一年']
+const regionOptions = ['全部', '太常片区', '城东片区', '华南片区', '城北片区']
+
+// 表单
+const analysisForm = reactive({
+  eventType: '漏失风险态势',
+  timeRange: '最近三月',
+  region: '全部'
+})
+
+const trendDate = ref('')
+
+// 风险数据
+const riskData = reactive({
+  index: 12,
+  indexTrend: 10,
+  warnCount: 23,
+  coverRate: 78,
+  coverTrend: -4,
+  overdueRate: 15.6
+})
+
+// 12个月环形进度数据
+const monthData = ref([
+  { month: '1月', percentage: 62 },
+  { month: '2月', percentage: 58 },
+  { month: '3月', percentage: 71 },
+  { month: '4月', percentage: 75 },
+  { month: '5月', percentage: 83 },
+  { month: '6月', percentage: 91 },
+  { month: '7月', percentage: 88 },
+  { month: '8月', percentage: 79 },
+  { month: '9月', percentage: 68 },
+  { month: '10月', percentage: 55 },
+  { month: '11月', percentage: 47 },
+  { month: '12月', percentage: 52 }
+])
+
+// 环形颜色
+const getCircleColor = (percentage) => {
+  if (percentage >= 80) return '#f56c6c'
+  if (percentage >= 60) return '#e6a23c'
+  return '#67c23a'
+}
+
+// 分析
+const handleAnalysis = () => {
+  // 模拟不同条件下的数据变化
+  const typeDataMap = {
+    '漏失风险态势': { index: 12, indexTrend: 10, warnCount: 23, coverRate: 78, coverTrend: -4, overdueRate: 15.6 },
+    '管网压力态势': { index: 9, indexTrend: -3, warnCount: 15, coverRate: 82, coverTrend: 2, overdueRate: 12.3 },
+    '积水内涝态势': { index: 18, indexTrend: 15, warnCount: 35, coverRate: 71, coverTrend: -8, overdueRate: 22.4 },
+    '设备故障态势': { index: 7, indexTrend: -5, warnCount: 8, coverRate: 88, coverTrend: 3, overdueRate: 8.1 }
+  }
+
+  const selected = typeDataMap[analysisForm.eventType]
+  if (selected) {
+    Object.assign(riskData, selected)
+  }
+
+  // 模拟月度数据变化
+  const baseMultiplier = analysisForm.eventType === '积水内涝态势' ? 1.2 : analysisForm.eventType === '设备故障态势' ? 0.7 : 1
+  monthData.value = monthData.value.map(item => ({
+    ...item,
+    percentage: Math.min(100, Math.max(10, Math.round(item.percentage * baseMultiplier * (0.8 + Math.random() * 0.4))))
+  }))
+
+  ElMessage.success('分析完成')
+}
+
+// 导出报告
+const handleExport = () => {
+  const reportText = `
+应急态势分析报告
+================
+
+【区域过滤信息】
+分析区域: ${analysisForm.region || '全部'}
+事件类型: ${analysisForm.eventType}
+时间范围: ${analysisForm.timeRange}
+
+【核心指标】
+综合风险指数: ${riskData.index} (${riskData.indexTrend > 0 ? '上升' : '下降'}${Math.abs(riskData.indexTrend)}%)
+预警事件数: ${riskData.warnCount}
+应急覆盖率: ${riskData.coverRate}% (${riskData.coverTrend > 0 ? '上升' : '下降'}${Math.abs(riskData.coverTrend)}%)
+超期管网占比: ${riskData.overdueRate}%
+
+【月度压力负荷】
+${monthData.value.map(m => `${m.month}: ${m.percentage}%`).join('\n')}
+
+报告生成时间: ${new Date().toLocaleString()}
+  `.trim()
+
+  const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8' })
+  const url = URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.download = `态势分析报告_${analysisForm.eventType}_${new Date().toISOString().slice(0, 10)}.txt`
+  link.click()
+  URL.revokeObjectURL(url)
+
+  ElMessage.success('分析报告已导出')
+}
+</script>
+
 <style scoped lang="scss">
+.app-container {
+  padding: 16px;
+}
+
+.analysis-layout {
+  display: flex;
+  gap: 16px;
+  min-height: calc(100vh - 120px);
+}
+
+// 左侧面板
+.left-panel {
+  width: 25%;
+  min-width: 260px;
+  flex-shrink: 0;
+
+  .el-card {
+    height: 100%;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 16px;
+  }
+
+  :deep(.el-form-item__label) {
+    padding-bottom: 4px;
+  }
+}
+
+// 右侧面板
+.right-panel {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.panel-title {
+  font-weight: 600;
+  font-size: 15px;
+}
+
+// 统计卡片
+.stat-cards {
+  display: flex;
+  gap: 16px;
+}
+
+.stat-card {
+  flex: 1;
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  border: 1px solid #ebeef5;
+  text-align: center;
+  transition: box-shadow 0.3s;
+
+  &:hover {
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  }
+
+  .stat-label {
+    font-size: 13px;
+    color: #909399;
+    margin-bottom: 8px;
+  }
+
+  .stat-value-row {
+    display: flex;
+    align-items: baseline;
+    justify-content: center;
+    gap: 6px;
+  }
+
+  .stat-number {
+    font-size: 28px;
+    font-weight: 700;
+    color: #303133;
+    line-height: 1.2;
+  }
+
+  .stat-trend {
+    font-size: 13px;
+    font-weight: 600;
+    display: inline-flex;
+    align-items: center;
+    gap: 2px;
+  }
+
+  .trend-up {
+    color: #f56c6c;
+  }
+
+  .trend-down {
+    color: #67c23a;
+  }
+}
+
+.stat-card-warning {
+  border-left: 3px solid #e6a23c;
+  .stat-number { color: #e6a23c; }
+}
+
+.stat-card-info {
+  border-left: 3px solid #409eff;
+  .stat-number { color: #409eff; }
+}
+
+.stat-card-danger {
+  border-left: 3px solid #f56c6c;
+  .stat-number { color: #f56c6c; }
+}
+
+// 趋势卡片
+.trend-card {
+  .card-header-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+
+// 环形进度图
+.circle-grid {
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  gap: 16px;
+  padding: 8px 0;
+}
+
+.circle-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+}
+
+.circle-text {
+  text-align: center;
+}
+
+.circle-value {
+  font-size: 13px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.circle-month {
+  font-size: 13px;
+  color: #606266;
+  text-align: center;
+}
+
+// 分析报告
+.report-card {
+  .report-content {
+    line-height: 1.8;
+    color: #303133;
+    font-size: 14px;
+  }
+
+  .report-section {
+    margin-bottom: 20px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    h4 {
+      font-size: 15px;
+      font-weight: 600;
+      color: #303133;
+      margin-bottom: 8px;
+      padding-left: 10px;
+      border-left: 3px solid #409eff;
+    }
 
-</style>
+    p {
+      margin-bottom: 6px;
+      text-indent: 2em;
+    }
+  }
+}
+</style>

+ 278 - 529
src/views/subSystem/dispatch/disAssess/DisSummary.vue

@@ -1,238 +1,189 @@
 <!--应急总结评估-->
 <template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>📋 应急事件事后评估与案例库系统</h2>
-        <p>事件记录 · 多维度评估 · 总结报告 · 案例入库</p>
-      </div>
-      <div class="badge-evaluate">🏆 标准化评估模型</div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧:事件列表与基本记录 -->
+  <div class="app-container">
+    <div class="summary-layout">
+      <!-- 左侧事件面板 -->
       <div class="left-panel">
-        <div class="card">
-          <div class="card-header">📌 应急事件列表</div>
-          <div class="card-body">
-            <div class="event-list">
-              <div
-                  v-for="event in events"
-                  :key="event.id"
-                  class="event-item"
-                  :class="{ selected: currentEventId === event.id }"
-                  @click="selectEvent(event.id)"
-              >
-                <div class="event-title">🔥 {{ event.name }}</div>
-                <div class="event-meta">
-                  {{ event.time }} | {{ event.level }}
-                  <span v-if="event.evaluated" class="status-badge evaluated">✅ 已评估</span>
-                  <span v-else class="status-badge pending">⏳ 待评估</span>
-                  <span v-if="event.caseAdded" class="status-badge case-added">📚 已入库</span>
-                </div>
+        <el-card shadow="hover" class="event-list-card">
+          <template #header>
+            <div class="panel-title">应急事件列表</div>
+          </template>
+          <div class="event-list">
+            <div
+              v-for="event in events"
+              :key="event.id"
+              class="event-item"
+              :class="{ selected: currentEventId === event.id }"
+              @click="selectEvent(event.id)"
+            >
+              <div class="event-name">
+                {{ event.name }}
+                <el-tag :type="levelTagType(event.level)" size="small" effect="dark" style="margin-left: 6px">{{ event.level }}</el-tag>
+              </div>
+              <div class="event-meta">{{ event.location }}</div>
+              <div class="event-status">
+                <el-tag v-if="event.evaluated" type="success" size="small">已评估</el-tag>
+                <el-tag v-else type="info" size="small">待评估</el-tag>
               </div>
             </div>
           </div>
-        </div>
-
-        <div class="card">
-          <div class="card-header">📝 事件基本记录</div>
-          <div class="card-body">
-            <div class="form-group">
-              <label class="form-label">事件名称</label>
-              <input type="text" v-model="currentEvent.name" class="form-input" readonly>
-            </div>
-            <div class="form-group">
-              <label class="form-label">发生时间</label>
-              <input type="text" v-model="currentEvent.time" class="form-input" readonly>
-            </div>
-            <div class="form-group">
-              <label class="form-label">事件地点</label>
-              <input type="text" v-model="currentEvent.location" class="form-input" readonly>
-            </div>
-            <div class="form-group">
-              <label class="form-label">事件等级</label>
-              <input type="text" v-model="currentEvent.level" class="form-input" readonly>
-            </div>
-            <div class="form-group">
-              <label class="form-label">事件类型</label>
-              <input type="text" v-model="currentEvent.type" class="form-input" readonly>
-            </div>
-            <div class="form-group">
-              <label class="form-label">简要描述</label>
-              <textarea v-model="currentEvent.desc" class="form-textarea" rows="2" readonly></textarea>
-            </div>
-          </div>
-        </div>
+          <el-button type="primary" plain style="width: 100%; margin-top: 12px" @click="handleRefresh">刷新事件列表</el-button>
+        </el-card>
+
+        <el-card shadow="hover" class="event-detail-card" v-if="currentEvent">
+          <template #header>
+            <div class="panel-title">事件基本记录</div>
+          </template>
+          <el-form label-width="80px" size="default" class="detail-form">
+            <el-form-item label="事件名称">
+              <el-input :model-value="currentEvent.name" readonly />
+            </el-form-item>
+            <el-form-item label="发生时间">
+              <el-input :model-value="currentEvent.time" readonly />
+            </el-form-item>
+            <el-form-item label="事件地点">
+              <el-input :model-value="currentEvent.location" readonly />
+            </el-form-item>
+            <el-form-item label="事件等级">
+              <el-input :model-value="currentEvent.level" readonly />
+            </el-form-item>
+            <el-form-item label="事件类型">
+              <el-input :model-value="currentEvent.type" readonly />
+            </el-form-item>
+            <el-form-item label="简要描述">
+              <el-input :model-value="currentEvent.desc" type="textarea" :rows="3" readonly />
+            </el-form-item>
+          </el-form>
+        </el-card>
       </div>
 
-      <!-- 右侧:评估表单 + 报告生成 -->
+      <!-- 右侧评估面板 -->
       <div class="right-panel">
-        <div class="card">
-          <div class="card-header">
-            📊 应急事件事后评估
-            <span class="evaluate-status" :class="{ evaluated: currentEvent.evaluated }">
-              {{ currentEvent.evaluated ? '✅ 已评估' : '⏳ 未评估' }}
-            </span>
-          </div>
-          <div class="card-body">
-            <!-- 应急组织体系评估 -->
-            <div class="score-group">
-              <div class="score-row">
-                <span class="score-label">🏛️ 应急组织体系</span>
-                <div class="score-stars">
-                  <span
-                      v-for="s in 5"
-                      :key="s"
-                      class="star"
-                      :class="{ active: s <= Math.floor(assessments.org.score), half: s === Math.floor(assessments.org.score) + 1 && assessments.org.score % 1 >= 0.5 }"
-                      @click="setScore('org', s)"
-                  >{{ getStarChar(s, assessments.org.score) }}</span>
-                </div>
-                <input type="number" v-model.number="assessments.org.score" class="score-input" min="0" max="5" step="0.5" @input="syncScore('org')">
-              </div>
-              <textarea v-model="assessments.org.comment" class="form-textarea" placeholder="评语建议(组织架构、指挥体系等)" rows="2"></textarea>
+        <el-card shadow="hover">
+          <template #header>
+            <div class="panel-title-row">
+              <span class="panel-title">应急事件事后评估</span>
+              <el-tag v-if="currentEvent?.evaluated" type="success" effect="dark" size="large">已评估</el-tag>
+              <el-tag v-else type="warning" effect="dark" size="large">未评估</el-tag>
             </div>
+          </template>
 
-            <!-- 应急能力评估 -->
-            <div class="score-group">
-              <div class="score-row">
-                <span class="score-label">⚡ 应急能力</span>
-                <div class="score-stars">
-                  <span
-                      v-for="s in 5"
-                      :key="s"
-                      class="star"
-                      :class="{ active: s <= Math.floor(assessments.cap.score), half: s === Math.floor(assessments.cap.score) + 1 && assessments.cap.score % 1 >= 0.5 }"
-                      @click="setScore('cap', s)"
-                  >{{ getStarChar(s, assessments.cap.score) }}</span>
-                </div>
-                <input type="number" v-model.number="assessments.cap.score" class="score-input" min="0" max="5" step="0.5" @input="syncScore('cap')">
-              </div>
-              <textarea v-model="assessments.cap.comment" class="form-textarea" placeholder="评语建议(响应速度、资源调配等)" rows="2"></textarea>
+          <!-- 应急组织体系 -->
+          <div class="assess-group">
+            <div class="assess-header">
+              <span class="assess-title">应急组织体系</span>
+              <el-rate v-model="assessments.org.score" :max="5" allow-half show-score score-template="{value}分" />
             </div>
+            <el-input v-model="assessments.org.comment" type="textarea" :rows="2" placeholder="组织架构、指挥体系等" />
+          </div>
 
-            <!-- 处置效果评估 -->
-            <div class="score-group">
-              <div class="score-row">
-                <span class="score-label">🛠️ 处置效果</span>
-                <div class="score-stars">
-                  <span
-                      v-for="s in 5"
-                      :key="s"
-                      class="star"
-                      :class="{ active: s <= Math.floor(assessments.effect.score), half: s === Math.floor(assessments.effect.score) + 1 && assessments.effect.score % 1 >= 0.5 }"
-                      @click="setScore('effect', s)"
-                  >{{ getStarChar(s, assessments.effect.score) }}</span>
-                </div>
-                <input type="number" v-model.number="assessments.effect.score" class="score-input" min="0" max="5" step="0.5" @input="syncScore('effect')">
-              </div>
-              <textarea v-model="assessments.effect.comment" class="form-textarea" placeholder="评语建议(措施有效性、协同配合等)" rows="2"></textarea>
+          <!-- 应急能力 -->
+          <div class="assess-group">
+            <div class="assess-header">
+              <span class="assess-title">应急能力</span>
+              <el-rate v-model="assessments.cap.score" :max="5" allow-half show-score score-template="{value}分" />
             </div>
+            <el-input v-model="assessments.cap.comment" type="textarea" :rows="2" placeholder="响应速度、资源调配等" />
+          </div>
 
-            <!-- 灾害损失评估 -->
-            <div class="score-group">
-              <div class="score-row">
-                <span class="score-label">💰 灾害损失</span>
-                <div class="score-stars">
-                  <span
-                      v-for="s in 5"
-                      :key="s"
-                      class="star"
-                      :class="{ active: s <= Math.floor(assessments.loss.score), half: s === Math.floor(assessments.loss.score) + 1 && assessments.loss.score % 1 >= 0.5 }"
-                      @click="setScore('loss', s)"
-                  >{{ getStarChar(s, assessments.loss.score) }}</span>
-                </div>
-                <input type="number" v-model.number="assessments.loss.score" class="score-input" min="0" max="5" step="0.5" @input="syncScore('loss')">
-              </div>
-              <textarea v-model="assessments.loss.comment" class="form-textarea" placeholder="评语建议(人员伤亡、经济损失等)" rows="2"></textarea>
+          <!-- 处置效果 -->
+          <div class="assess-group">
+            <div class="assess-header">
+              <span class="assess-title">处置效果</span>
+              <el-rate v-model="assessments.effect.score" :max="5" allow-half show-score score-template="{value}分" />
             </div>
+            <el-input v-model="assessments.effect.comment" type="textarea" :rows="2" placeholder="措施有效性、协同配合等" />
+          </div>
 
-            <div class="action-buttons">
-              <button @click="generateReport" class="btn-primary">📄 生成总结评估报告</button>
-              <button @click="saveToCaseLibrary" class="btn-success">📚 录入案例库</button>
-              <button @click="resetEvaluation" class="btn-outline">重置评估</button>
+          <!-- 灾害损失 -->
+          <div class="assess-group">
+            <div class="assess-header">
+              <span class="assess-title">灾害损失</span>
+              <el-rate v-model="assessments.loss.score" :max="5" allow-half show-score score-template="{value}分" />
             </div>
+            <el-input v-model="assessments.loss.comment" type="textarea" :rows="2" placeholder="人员伤亡、经济损失等" />
           </div>
-        </div>
 
-        <!-- 总结评估报告预览 -->
-        <div class="report-preview">
-          <div class="report-title">
-            📑 总结评估报告
-            <span v-if="reportTime" class="report-time">生成于 {{ reportTime }}</span>
+          <!-- 操作按钮 -->
+          <div class="action-row">
+            <el-button type="primary" @click="generateReport">生成评估报告下载</el-button>
+            <el-button type="success" @click="saveToCaseLibrary">录入案例库</el-button>
+            <el-button @click="resetEvaluation">重新评估</el-button>
           </div>
-          <div class="report-content" v-html="reportContent">
-            请先选择应急事件并完成各项评估,点击「生成总结评估报告」。
-          </div>
-        </div>
+        </el-card>
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
-
-// 评估维度配置
-const assessmentKeys = ['org', 'cap', 'effect', 'loss']
-const assessmentNames = {
-  org: '应急组织体系',
-  cap: '应急能力',
-  effect: '处置效果',
-  loss: '灾害损失'
-}
+import { ref, reactive, computed, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
 
-// 事件数据
+// Mock 事件数据
 const events = ref([
   {
-    id: 1, name: "化工园区氯气泄漏事故", time: "2025-05-15 08:30", location: "华东化工园区",
-    level: "重大", type: "事故灾难", desc: "储罐区氯气阀门失效导致泄漏,影响周边2km范围。",
+    id: 1, name: '化工园区氯气泄漏事故', time: '2025-05-15 08:30', location: '华东化工园区',
+    level: '重大', type: '事故灾难', desc: '储罐区氯气阀门失效导致泄漏,影响周边2km范围,已启动二级应急响应。',
     evaluated: false, caseAdded: false,
     assessments: { org: { score: 0, comment: '' }, cap: { score: 0, comment: '' }, effect: { score: 0, comment: '' }, loss: { score: 0, comment: '' } }
   },
   {
-    id: 2, name: "特大暴雨洪涝灾害", time: "2025-07-20 14:00", location: "华南沿海城市",
-    level: "特别重大", type: "自然灾害", desc: "连续强降雨引发城市内涝,受灾人口超10万。",
+    id: 2, name: '特大暴雨洪涝灾害', time: '2025-07-20 14:00', location: '华南沿海城市',
+    level: '特别重大', type: '自然灾害', desc: '连续强降雨引发城市内涝,受灾人口超10万,多处基础设施受损。',
     evaluated: false, caseAdded: false,
     assessments: { org: { score: 0, comment: '' }, cap: { score: 0, comment: '' }, effect: { score: 0, comment: '' }, loss: { score: 0, comment: '' } }
   },
   {
-    id: 3, name: "地铁施工坍塌事故", time: "2025-03-10 10:15", location: "城西地铁5号线",
-    level: "较大", type: "事故灾难", desc: "隧道施工段发生坍塌,3人被困。",
+    id: 3, name: '地铁施工坍塌事故', time: '2025-03-10 10:15', location: '城西地铁5号线',
+    level: '较大', type: '事故灾难', desc: '隧道施工段发生坍塌,3人被困,已组织现场救援。',
+    evaluated: false, caseAdded: false,
+    assessments: { org: { score: 0, comment: '' }, cap: { score: 0, comment: '' }, effect: { score: 0, comment: '' }, loss: { score: 0, comment: '' } }
+  },
+  {
+    id: 4, name: '太常片区供水管道爆裂', time: '2025-01-25 06:45', location: '太常片区主干道',
+    level: '重大', type: '管网泄漏', desc: 'DN800供水主管爆裂,影响周边3万居民用水,已紧急调度供水车。',
+    evaluated: true, caseAdded: true,
+    assessments: { org: { score: 4, comment: '组织架构较为清晰,指挥层级明确' }, cap: { score: 3.5, comment: '响应速度较快,但资源调配稍显滞后' }, effect: { score: 4, comment: '现场处置得当,各部门协同配合良好' }, loss: { score: 3, comment: '经济损失较大,但人员伤亡控制较好' } }
+  },
+  {
+    id: 5, name: '城南片区排水管网堵塞', time: '2025-04-08 16:30', location: '城南片区低洼路段',
+    level: '一般', type: '管道堵塞', desc: '排水管网严重堵塞导致路面积水,影响交通通行。',
     evaluated: false, caseAdded: false,
     assessments: { org: { score: 0, comment: '' }, cap: { score: 0, comment: '' }, effect: { score: 0, comment: '' }, loss: { score: 0, comment: '' } }
   }
 ])
 
 const currentEventId = ref(1)
-const assessments = ref({
+const assessments = reactive({
   org: { score: 0, comment: '' },
   cap: { score: 0, comment: '' },
   effect: { score: 0, comment: '' },
   loss: { score: 0, comment: '' }
 })
-const reportContent = ref('请先选择应急事件并完成各项评估,点击「生成总结评估报告」。')
-const reportTime = ref('')
 
-// 当前事件
+// 当前选中事件
 const currentEvent = computed(() => {
-  const event = events.value.find(e => e.id === currentEventId.value)
-  return event || events.value[0]
+  return events.value.find(e => e.id === currentEventId.value) || events.value[0]
 })
 
-// 加载事件评估数据到表单
+// 加载评估数据到表单
 const loadAssessmentsToForm = () => {
-  if (currentEvent.value.evaluated && currentEvent.value.assessments) {
-    assessments.value = JSON.parse(JSON.stringify(currentEvent.value.assessments))
+  const event = currentEvent.value
+  if (event && event.evaluated && event.assessments) {
+    assessments.org.score = event.assessments.org.score
+    assessments.org.comment = event.assessments.org.comment
+    assessments.cap.score = event.assessments.cap.score
+    assessments.cap.comment = event.assessments.cap.comment
+    assessments.effect.score = event.assessments.effect.score
+    assessments.effect.comment = event.assessments.effect.comment
+    assessments.loss.score = event.assessments.loss.score
+    assessments.loss.comment = event.assessments.loss.comment
   } else {
-    assessments.value = {
-      org: { score: 0, comment: '' },
-      cap: { score: 0, comment: '' },
-      effect: { score: 0, comment: '' },
-      loss: { score: 0, comment: '' }
-    }
+    assessments.org.score = 0; assessments.org.comment = ''
+    assessments.cap.score = 0; assessments.cap.comment = ''
+    assessments.effect.score = 0; assessments.effect.comment = ''
+    assessments.loss.score = 0; assessments.loss.comment = ''
   }
 }
 
@@ -240,51 +191,23 @@ const loadAssessmentsToForm = () => {
 const selectEvent = (id) => {
   currentEventId.value = id
   loadAssessmentsToForm()
-  reportContent.value = '请先完成评估并生成报告。'
-  reportTime.value = ''
-}
-
-// 设置评分
-const setScore = (key, value) => {
-  assessments.value[key].score = value
 }
 
-// 同步评分(确保范围)
-const syncScore = (key) => {
-  let score = assessments.value[key].score
-  if (isNaN(score)) score = 0
-  score = Math.min(5, Math.max(0, score))
-  assessments.value[key].score = Math.round(score * 2) / 2
-}
-
-// 获取星号字符
-const getStarChar = (position, score) => {
-  if (position <= Math.floor(score)) return '★'
-  if (position === Math.floor(score) + 1 && score % 1 >= 0.5) return '½'
-  return '☆'
-}
-
-// 生成建议
-const generateSuggestions = (scores) => {
-  const suggestions = []
-  if (scores.org < 3.5) suggestions.push('· 完善应急组织架构,明确指挥权责,加强跨部门协调机制。')
-  if (scores.cap < 3.5) suggestions.push('· 提升应急响应能力,加强演练频次和资源储备。')
-  if (scores.effect < 3.5) suggestions.push('· 优化应急处置流程,提高现场处置效率。')
-  if (scores.loss < 3) suggestions.push('· 强化风险防控,降低灾害损失。')
-  if (suggestions.length === 0) suggestions.push('· 整体表现良好,继续保持并优化细节。')
-  return suggestions.join('\n')
+// 等级tag颜色
+const levelTagType = (level) => {
+  const map = { '一般': 'info', '较大': 'warning', '重大': 'danger', '特别重大': 'danger' }
+  return map[level] || 'info'
 }
 
 // 生成评估报告
 const generateReport = () => {
   const event = currentEvent.value
   const scores = {
-    org: assessments.value.org.score,
-    cap: assessments.value.cap.score,
-    effect: assessments.value.effect.score,
-    loss: assessments.value.loss.score
+    org: assessments.org.score,
+    cap: assessments.cap.score,
+    effect: assessments.effect.score,
+    loss: assessments.loss.score
   }
-
   const totalScore = (scores.org + scores.cap + scores.effect + scores.loss) / 4
   const totalPercent = (totalScore / 5 * 100).toFixed(1)
 
@@ -294,10 +217,9 @@ const generateReport = () => {
   else if (totalScore >= 2.5) grade = '一般'
   else grade = '较差'
 
-  const report = `
-========================================
-      应急事件总结评估报告
-========================================
+  const reportText = `
+应急事件总结评估报告
+====================
 
 【基本信息】
 事件名称: ${event.name}
@@ -309,403 +231,230 @@ const generateReport = () => {
 
 【评估维度】
 1. 应急组织体系: ${scores.org}/5分
-   评语: ${assessments.value.org.comment || '无'}
+   评语: ${assessments.org.comment || '无'}
 2. 应急能力: ${scores.cap}/5分
-   评语: ${assessments.value.cap.comment || '无'}
+   评语: ${assessments.cap.comment || '无'}
 3. 处置效果: ${scores.effect}/5分
-   评语: ${assessments.value.effect.comment || '无'}
+   评语: ${assessments.effect.comment || '无'}
 4. 灾害损失: ${scores.loss}/5分
-   评语: ${assessments.value.loss.comment || '无'}
+   评语: ${assessments.loss.comment || '无'}
 
 【综合评分】
 总分: ${totalScore.toFixed(1)}/5 (${totalPercent}%)
 评价等级: ${grade}
 
-【总结与建议】
-${generateSuggestions(scores)}
-
 报告生成时间: ${new Date().toLocaleString()}
-========================================
-  `
+  `.trim()
 
   // 保存到事件对象
   event.evaluated = true
-  event.assessments = JSON.parse(JSON.stringify(assessments.value))
-  event.report = report
+  event.assessments = {
+    org: { score: scores.org, comment: assessments.org.comment },
+    cap: { score: scores.cap, comment: assessments.cap.comment },
+    effect: { score: scores.effect, comment: assessments.effect.comment },
+    loss: { score: scores.loss, comment: assessments.loss.comment }
+  }
+
+  // 生成下载
+  const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8' })
+  const url = URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.download = `评估报告_${event.name}_${new Date().toISOString().slice(0, 10)}.txt`
+  link.click()
+  URL.revokeObjectURL(url)
 
-  reportContent.value = report.replace(/\n/g, '<br>')
-  reportTime.value = new Date().toLocaleString()
+  ElMessage.success('评估报告已生成并下载')
 }
 
 // 录入案例库
 const saveToCaseLibrary = () => {
   const event = currentEvent.value
   if (!event.evaluated) {
-    alert('请先生成总结评估报告后再录入案例库')
+    ElMessage.warning('请先生成评估报告后再录入案例库')
     return
   }
   if (event.caseAdded) {
-    alert('该事件已录入案例库,不可重复录入')
+    ElMessage.warning('该事件已录入案例库,不可重复录入')
     return
   }
-
   event.caseAdded = true
-  const scores = event.assessments
-  const totalScore = (scores.org.score + scores.cap.score + scores.effect.score + scores.loss.score) / 4
-  alert(`✅ 案例已成功录入案例库!\n事件: ${event.name}\n综合评分: ${totalScore.toFixed(1)}/5\n可随时在案例库中查看完整评估报告。`)
+  ElMessage.success('已成功录入案例库')
 }
 
-// 重评估
+// 重评估
 const resetEvaluation = () => {
-  if (confirm('确认重置当前事件的评估数据吗?所有评分和评语将被清空。')) {
-    assessments.value = {
-      org: { score: 0, comment: '' },
-      cap: { score: 0, comment: '' },
-      effect: { score: 0, comment: '' },
-      loss: { score: 0, comment: '' }
-    }
-    reportContent.value = '评估已重置,请重新评估并生成报告。'
-    reportTime.value = ''
-
-    // 不清除已保存的事件数据,只重置当前表单
-    // 但如果是已评估的事件,我们需要将其标记为未评估
+  ElMessageBox.confirm('确认重新评估?所有评分和评语将被清空。', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    assessments.org.score = 0; assessments.org.comment = ''
+    assessments.cap.score = 0; assessments.cap.comment = ''
+    assessments.effect.score = 0; assessments.effect.comment = ''
+    assessments.loss.score = 0; assessments.loss.comment = ''
     const event = currentEvent.value
     if (event.evaluated) {
       event.evaluated = false
       event.caseAdded = false
     }
-  }
+    ElMessage.info('评估已重置')
+  }).catch(() => {})
 }
 
-// 监听事件切换,重置报告预览
-watch(currentEventId, () => {
-  reportContent.value = '请先完成评估并生成报告。'
-  reportTime.value = ''
-})
-</script>
-
-<style scoped>
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
+// 刷新事件列表
+const handleRefresh = () => {
+  ElMessage.success('事件列表已刷新')
 }
 
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #ffffff;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
+// 初始化加载
+loadAssessmentsToForm()
 
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 18px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-  border-bottom: 3px solid #9b59b6;
-}
-
-.logo h2 {
-  color: white;
-  font-weight: 600;
-  font-size: 1.4rem;
-}
-
-.logo p {
-  color: #bdd8e8;
-  font-size: 0.7rem;
-}
+// 监听事件切换
+watch(currentEventId, () => {
+  loadAssessmentsToForm()
+})
+</script>
 
-.badge-evaluate {
-  background: #9b59b6;
-  padding: 6px 18px;
-  border-radius: 30px;
-  color: white;
-  font-weight: 500;
-  font-size: 0.8rem;
+<style scoped lang="scss">
+.app-container {
+  padding: 16px;
 }
 
-/* 主体布局 */
-.main-layout {
+.summary-layout {
   display: flex;
-  flex-wrap: wrap;
+  gap: 16px;
+  min-height: calc(100vh - 120px);
 }
 
+// 左侧面板
 .left-panel {
-  width: 360px;
-  background: #f9fbfd;
-  border-right: 1px solid #e2edf2;
-  padding: 20px 18px;
+  width: 30%;
+  min-width: 320px;
+  flex-shrink: 0;
   display: flex;
   flex-direction: column;
-  gap: 20px;
+  gap: 16px;
 }
 
-.right-panel {
-  flex: 1;
-  background: #ffffff;
-  padding: 20px 24px;
-  min-width: 600px;
+.event-list-card {
+  :deep(.el-card__body) {
+    padding: 12px;
+  }
 }
 
-/* 卡片样式 */
-.card {
-  background: white;
-  border-radius: 20px;
-  border: 1px solid #e6edf3;
-  overflow: hidden;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
+.event-list {
+  max-height: 300px;
+  overflow-y: auto;
 }
 
-.card-header {
-  background: #f9fafc;
-  padding: 14px 18px;
-  border-bottom: 1px solid #eef2f6;
-  font-weight: 700;
-  font-size: 0.9rem;
-  color: #1d5a78;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
+.event-item {
+  padding: 12px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  margin-bottom: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
 
-.card-body {
-  padding: 16px;
-}
+  &:hover {
+    border-color: #c6e2ff;
+    background: #ecf5ff;
+  }
 
-/* 表单样式 */
-.form-group {
-  margin-bottom: 16px;
+  &.selected {
+    border-color: #409eff;
+    background: #ecf5ff;
+    box-shadow: 0 0 0 1px #409eff;
+  }
 }
 
-.form-label {
-  font-size: 0.7rem;
+.event-name {
   font-weight: 600;
-  margin-bottom: 6px;
-  display: block;
-  color: #2c5a72;
-}
-
-.form-input, .form-select, .form-textarea {
-  width: 100%;
-  padding: 8px 12px;
-  border-radius: 12px;
-  border: 1px solid #cbdde6;
-  font-size: 0.75rem;
-  background: white;
-}
-
-.form-textarea {
-  resize: vertical;
-  min-height: 60px;
-}
-
-/* 评分组件 */
-.score-group {
-  margin-bottom: 20px;
-}
-
-.score-row {
+  font-size: 14px;
+  color: #303133;
   display: flex;
   align-items: center;
-  justify-content: space-between;
-  margin-bottom: 12px;
   flex-wrap: wrap;
-  gap: 10px;
 }
 
-.score-label {
-  font-size: 0.75rem;
-  font-weight: 600;
-  width: 110px;
-}
-
-.score-stars {
-  display: flex;
-  gap: 6px;
-  flex: 1;
-}
-
-.star {
-  font-size: 1.2rem;
-  cursor: pointer;
-  color: #d4dce2;
-  transition: 0.1s;
-}
-
-.star.active {
-  color: #f1c40f;
-}
-
-.star.half {
-  color: #f1c40f;
-  position: relative;
-}
-
-.score-input {
-  width: 60px;
-  padding: 4px 8px;
-  border-radius: 20px;
-  border: 1px solid #cbdde6;
-  text-align: center;
-  font-size: 0.7rem;
-}
-
-/* 按钮 */
-.btn-primary {
-  background: #9b59b6;
-  border: none;
-  color: white;
-  padding: 10px 20px;
-  border-radius: 30px;
-  font-weight: 600;
-  font-size: 0.8rem;
-  cursor: pointer;
-  transition: 0.2s;
-}
-
-.btn-primary:hover {
-  background: #8e44ad;
-}
-
-.btn-outline {
-  background: white;
-  border: 1px solid #9b59b6;
-  color: #9b59b6;
-  padding: 8px 16px;
-  border-radius: 30px;
-  font-size: 0.75rem;
-  cursor: pointer;
+.event-meta {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
 }
 
-.btn-success {
-  background: #27ae60;
-  border: none;
-  color: white;
-  padding: 10px 20px;
-  border-radius: 30px;
-  font-weight: 600;
-  font-size: 0.8rem;
-  cursor: pointer;
+.event-status {
+  margin-top: 6px;
 }
 
-.action-buttons {
-  display: flex;
-  gap: 12px;
-  margin-top: 20px;
-  flex-wrap: wrap;
-}
+.event-detail-card {
+  flex: 1;
 
-/* 事件列表 */
-.event-list {
-  max-height: 300px;
-  overflow-y: auto;
+  :deep(.el-card__body) {
+    padding: 12px;
+  }
 }
 
-.event-item {
-  background: #fef9f2;
-  border-radius: 14px;
-  padding: 12px;
-  margin-bottom: 10px;
-  border-left: 3px solid #9b59b6;
-  cursor: pointer;
-  transition: 0.1s;
-}
+.detail-form {
+  :deep(.el-form-item__label) {
+    font-size: 13px;
+    color: #606266;
+  }
 
-.event-item.selected {
-  background: #f3e8fa;
-  border-left-color: #8e44ad;
-  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+  :deep(.el-input__inner),
+  :deep(.el-textarea__inner) {
+    background: #f5f7fa;
+    color: #606266;
+  }
 }
 
-.event-title {
-  font-weight: 700;
-  font-size: 0.8rem;
+// 右侧面板
+.right-panel {
+  flex: 1;
+  min-width: 0;
 }
 
-.event-meta {
-  font-size: 0.65rem;
-  color: #6f8ea8;
-  margin-top: 4px;
+.panel-title-row {
   display: flex;
-  flex-wrap: wrap;
-  gap: 6px;
   align-items: center;
+  justify-content: space-between;
 }
 
-.status-badge {
-  font-size: 0.6rem;
-  padding: 2px 6px;
-  border-radius: 20px;
-}
-
-.evaluated {
-  background: #d5f5e3;
-  color: #27ae60;
-}
-
-.pending {
-  background: #eef2fa;
-  color: #1d5a78;
-}
-
-.case-added {
-  background: #d5e8f5;
-  color: #2980b9;
-}
-
-.evaluate-status {
-  font-size: 0.65rem;
-  background: #eef2fa;
-  padding: 2px 10px;
-  border-radius: 20px;
+.panel-title {
+  font-weight: 600;
+  font-size: 15px;
 }
 
-.evaluate-status.evaluated {
-  background: #d5f5e3;
-  color: #27ae60;
-}
+// 评估维度
+.assess-group {
+  margin-bottom: 24px;
+  padding-bottom: 24px;
+  border-bottom: 1px solid #ebeef5;
 
-/* 报告预览 */
-.report-preview {
-  background: #f8fafc;
-  border-radius: 16px;
-  padding: 16px;
-  margin-top: 20px;
-  border: 1px solid #e6edf3;
+  &:last-of-type {
+    border-bottom: none;
+  }
 }
 
-.report-title {
-  font-weight: 700;
-  margin-bottom: 12px;
-  font-size: 0.9rem;
-  color: #2c5a72;
+.assess-header {
   display: flex;
-  justify-content: space-between;
   align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
 }
 
-.report-time {
-  font-size: 0.6rem;
-  font-weight: normal;
-  color: #6f8ea8;
+.assess-title {
+  font-weight: 600;
+  font-size: 15px;
+  color: #303133;
 }
 
-.report-content {
-  font-size: 0.7rem;
-  line-height: 1.6;
-  color: #3a6e8c;
-  max-height: 250px;
-  overflow-y: auto;
-  white-space: pre-wrap;
-  font-family: monospace;
+// 操作按钮
+.action-row {
+  display: flex;
+  gap: 12px;
+  margin-top: 20px;
+  padding-top: 20px;
+  border-top: 1px solid #ebeef5;
 }
-</style>
+</style>

+ 404 - 433
src/views/subSystem/dispatch/disDecisionDep/DisPlanSelection.vue

@@ -1,577 +1,548 @@
 <!--应急预案选择-->
 <template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>📋 智能预案匹配系统</h2>
-        <p>管网突发事件 · 数字预案库智能匹配 | 专题预案一键选择</p>
-      </div>
-      <div class="badge-ai">🤖 AI 匹配引擎 · 实时推荐</div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧:事件条件输入区 -->
+  <div class="app-container">
+    <div class="plan-layout">
+      <!-- 左侧条件面板 -->
       <div class="left-panel">
-        <div class="card">
-          <div class="card-header">
-            🎯 突发事件条件设定
-            <span style="font-size:0.7rem; background:#eef2fa; padding:2px 10px; border-radius:20px;">预案匹配规则</span>
-          </div>
-          <div class="card-body">
-            <div class="form-group">
-              <label class="form-label">📌 事件类型</label>
-              <select v-model="form.eventType" class="form-select">
-                <option value="管道泄漏">管道泄漏</option>
-                <option value="爆管事故">爆管事故</option>
-                <option value="第三方破坏">第三方破坏</option>
-                <option value="自然损坏">自然损坏</option>
-              </select>
+        <!-- 突发事件条件设定 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Setting /></el-icon>
+              <span>突发事件条件设定</span>
             </div>
-            <div class="form-group">
-              <label class="form-label">⚠️ 事件级别</label>
-              <select v-model="form.eventLevel" class="form-select">
-                <option value="一般(Ⅳ级)">一般(Ⅳ级)</option>
-                <option value="较大(Ⅲ级)">较大(Ⅲ级)</option>
-                <option value="重大(Ⅱ级)">重大(Ⅱ级)</option>
-                <option value="特别重大(Ⅰ级)">特别重大(Ⅰ级)</option>
-              </select>
+          </template>
+          <el-form label-position="top" size="default">
+            <el-form-item label="事件类型">
+              <el-select v-model="form.eventType" placeholder="请选择事件类型" style="width:100%">
+                <el-option v-for="item in eventTypeOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="事件级别">
+              <el-select v-model="form.eventLevel" placeholder="请选择事件级别" style="width:100%">
+                <el-option v-for="item in eventLevelOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="发生区域">
+              <el-select v-model="form.eventRegion" placeholder="请选择发生区域" style="width:100%">
+                <el-option v-for="item in eventRegionOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="事故责任单位">
+              <el-select v-model="form.responsibleUnit" placeholder="请选择责任单位" style="width:100%">
+                <el-option v-for="item in unitOptions" :key="item" :label="item" :value="item" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </el-card>
+
+        <!-- 预案匹配规则 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><InfoFilled /></el-icon>
+              <span>预案匹配规则</span>
             </div>
-            <div class="form-group">
-              <label class="form-label">📍 发生区域</label>
-              <select v-model="form.eventRegion" class="form-select">
-                <option value="城东管网区">城东管网区</option>
-                <option value="城南工业区">城南工业区</option>
-                <option value="城西居民区">城西居民区</option>
-                <option value="城北枢纽区">城北枢纽区</option>
-                <option value="中心城区">中心城区</option>
-              </select>
+          </template>
+          <div class="rule-desc">
+            <p>根据事件类型、级别、区域、责任单位进行数字预案库权重匹配:</p>
+            <div class="rule-weights">
+              <div class="weight-item">
+                <span class="weight-label">事件类型</span>
+                <el-progress :percentage="40" :stroke-width="14" :format="() => '40%'" color="#409EFF" />
+              </div>
+              <div class="weight-item">
+                <span class="weight-label">事件级别</span>
+                <el-progress :percentage="25" :stroke-width="14" :format="() => '25%'" color="#67C23A" />
+              </div>
+              <div class="weight-item">
+                <span class="weight-label">发生区域</span>
+                <el-progress :percentage="20" :stroke-width="14" :format="() => '20%'" color="#E6A23C" />
+              </div>
+              <div class="weight-item">
+                <span class="weight-label">责任单位</span>
+                <el-progress :percentage="15" :stroke-width="14" :format="() => '15%'" color="#F56C6C" />
+              </div>
             </div>
-            <div class="form-group">
-              <label class="form-label">🏢 事故责任单位</label>
-              <select v-model="form.responsibleUnit" class="form-select">
-                <option value="市燃气集团">市燃气集团</option>
-                <option value="水务管网公司">水务管网公司</option>
-                <option value="热力供应公司">热力供应公司</option>
-                <option value="市政管理所">市政管理所</option>
-              </select>
+            <p class="rule-tip">系统根据加权相似度搜寻最佳匹配预案</p>
+          </div>
+          <el-button type="primary" style="width:100%;margin-top:12px" @click="manualMatch">
+            <el-icon><Search /></el-icon> 立即匹配预案
+          </el-button>
+        </el-card>
+
+        <!-- 最近匹配记录 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Clock /></el-icon>
+              <span>最近匹配记录</span>
             </div>
-
-            <!-- 预案匹配规则说明 -->
-            <div class="rule-box">
-              <strong>📐 预案匹配规则</strong><br />
-              根据事件类型、级别、区域、责任单位 → 数字预案库权重匹配<br />
-              • 事件类型权重: 40% &nbsp;• 事件级别权重: 25%<br />
-              • 发生区域权重: 20% &nbsp;• 责任单位权重: 15%<br />
-              系统根据加权相似度搜寻最佳匹配预案。
+          </template>
+          <div class="history-list">
+            <div v-for="item in matchHistory" :key="item.id" class="history-item">
+              <div class="history-date">{{ item.date }}</div>
+              <div class="history-desc">{{ item.desc }} → {{ item.planName }}</div>
             </div>
-
-            <button @click="manualMatch" class="btn-primary">🔍 立即匹配预案</button>
-          </div>
-        </div>
-
-        <!-- 匹配历史/规则动态提示 -->
-        <div class="card">
-          <div class="card-header">📋 最近匹配记录</div>
-          <div class="card-body" style="font-size:0.7rem; color:#527e9a;">
-            <div>▪ 2025-06-18 城东泄漏 → 匹配《城东燃气管网泄漏应急预案》</div>
-            <div>▪ 2025-06-15 城南爆管 → 匹配《供水管网爆管抢修预案》</div>
-            <div>▪ 2025-06-10 第三方破坏 → 匹配《第三方施工破坏应急方案》</div>
           </div>
-        </div>
+        </el-card>
       </div>
 
-      <!-- 右侧:匹配结果 + 专题预案一键选择 -->
+      <!-- 右侧结果面板 -->
       <div class="right-panel">
-        <!-- 一键选择专题预案区域(常见专题) -->
-        <div class="card">
-          <div class="card-header">
-            ⚡ 常见专题预案 · 一键快速查找
-            <span style="font-size:0.7rem;">点击即可快速匹配对应专题</span>
-          </div>
-          <div class="card-body">
-            <div class="topic-grid">
-              <div
-                  v-for="topic in topics"
-                  :key="topic.value"
-                  class="topic-btn"
-                  :class="{ active: activeTopic === topic.value }"
-                  @click="matchByTopic(topic.value)"
-              >
-                {{ topic.label }}
-              </div>
+        <!-- 一键快速查找 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Promotion /></el-icon>
+              <span>常见专题预案 · 一键快速查找</span>
             </div>
-            <div style="margin-top: 12px; font-size:0.7rem; color:#6f95ae; text-align:center;">
-              💡 支持一键选择,系统自动定位匹配数字预案库中的专题预案
-            </div>
-          </div>
-        </div>
-
-        <!-- 匹配结果与数字预案库展示 -->
-        <div class="card">
-          <div class="card-header">
-            📚 数字预案库 · 匹配结果
-            <span class="result-badge-small">{{ matchResultCount }}</span>
+          </template>
+          <div class="topic-row">
+            <el-button
+              v-for="topic in topics"
+              :key="topic.value"
+              :type="activeTopic === topic.value ? 'primary' : 'default'"
+              round
+              @click="matchByTopic(topic.value)"
+            >
+              {{ topic.label }}
+            </el-button>
           </div>
-          <div class="card-body">
-            <div v-if="matchResults.length === 0" class="empty-state">
-              🧭 请设置突发事件条件,点击“立即匹配预案”或选择专题预案
+          <div class="topic-tip">支持一键选择,系统自动定位匹配数字预案库中的专题预案</div>
+        </el-card>
+
+        <!-- 匹配结果 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Document /></el-icon>
+              <span>数字预案库 · 匹配结果</span>
+              <el-tag v-if="matchResults.length > 0" type="success" size="small" style="margin-left:8px">
+                匹配到 {{ matchResults.length }} 个预案
+              </el-tag>
+              <el-tag v-else type="info" size="small" style="margin-left:8px">待匹配</el-tag>
             </div>
-            <div v-else class="pre-list">
-              <div
-                  v-for="pre in matchResults"
-                  :key="pre.id"
-                  class="pre-item"
-                  :class="{ selected: selectedPreplan?.id === pre.id }"
-                  @click="selectPreplan(pre)"
-              >
-                <div class="pre-name">
-                  <span>📄 {{ pre.name }}</span>
-                  <span class="confidence">匹配度 {{ pre.matchScore }}%</span>
-                </div>
-                <div class="pre-meta">
-                  <span>🏷️ {{ pre.type }}</span>
-                  <span>⚠️ {{ pre.level }}</span>
-                  <span>📍 {{ pre.region }}</span>
-                  <span>🏢 {{ pre.unit }}</span>
-                </div>
-                <div style="font-size:0.7rem; margin-top:6px; color:#5c7f99;">
-                  {{ pre.content.substring(0, 60) }}...
-                </div>
+          </template>
+          <div v-if="matchResults.length === 0" class="empty-state">
+            <el-empty description="请设置突发事件条件,点击「立即匹配预案」或选择专题预案" />
+          </div>
+          <div v-else class="plan-list">
+            <div
+              v-for="plan in matchResults"
+              :key="plan.id"
+              class="plan-item"
+              :class="{ selected: selectedPlan?.id === plan.id }"
+              @click="selectPlan(plan)"
+            >
+              <div class="plan-head">
+                <span class="plan-name">{{ plan.name }}</span>
+                <el-tag :type="plan.matchScore >= 90 ? 'danger' : plan.matchScore >= 70 ? 'warning' : 'info'" size="small">
+                  匹配度 {{ plan.matchScore }}%
+                </el-tag>
+              </div>
+              <div class="plan-tags">
+                <el-tag size="small" type="primary">{{ plan.type }}</el-tag>
+                <el-tag size="small" type="warning">{{ plan.level }}</el-tag>
+                <el-tag size="small">{{ plan.region }}</el-tag>
+                <el-tag size="small" type="success">{{ plan.unit }}</el-tag>
               </div>
+              <div class="plan-summary">{{ plan.summary }}</div>
             </div>
           </div>
-        </div>
-
-        <!-- 最终确定预案详情展示 -->
-        <div class="detail-card">
-          <div class="detail-header">
-            <strong>📄 当前选定预案详情</strong>
-            <button @click="applyPlan" class="btn-outline" style="padding:4px 16px;">应用预案</button>
-          </div>
-          <div v-if="selectedPreplan" class="detail-content">
-            <strong>{{ selectedPreplan.name }}</strong><br />
-            类型: {{ selectedPreplan.type }} | 级别: {{ selectedPreplan.level }}<br />
-            区域: {{ selectedPreplan.region }} | 责任单位: {{ selectedPreplan.unit }}<br />
-            匹配度: {{ selectedPreplan.matchScore }}%<br />
-            预案内容概要: {{ selectedPreplan.content }}<br />
-            <span style="color:#2e9c8c;">✅ 已选定此预案作为本次应急处置依据。</span>
+        </el-card>
+
+        <!-- 当前选定预案详情 -->
+        <el-card shadow="never" class="panel-card detail-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Checked /></el-icon>
+              <span>当前选定预案详情</span>
+              <el-button
+                v-if="selectedPlan"
+                type="primary"
+                size="small"
+                style="margin-left:auto"
+                @click="applyPlan"
+              >
+                应用预案
+              </el-button>
+            </div>
+          </template>
+          <div v-if="selectedPlan" class="detail-content">
+            <el-descriptions :column="2" border size="small">
+              <el-descriptions-item label="预案名称">{{ selectedPlan.name }}</el-descriptions-item>
+              <el-descriptions-item label="匹配度">
+                <el-tag :type="selectedPlan.matchScore >= 90 ? 'danger' : 'warning'" size="small">
+                  {{ selectedPlan.matchScore }}%
+                </el-tag>
+              </el-descriptions-item>
+              <el-descriptions-item label="事件类型">{{ selectedPlan.type }}</el-descriptions-item>
+              <el-descriptions-item label="事件级别">{{ selectedPlan.level }}</el-descriptions-item>
+              <el-descriptions-item label="发生区域">{{ selectedPlan.region }}</el-descriptions-item>
+              <el-descriptions-item label="责任单位">{{ selectedPlan.unit }}</el-descriptions-item>
+            </el-descriptions>
+            <div class="detail-summary">
+              <div class="summary-label">预案内容概要</div>
+              <div class="summary-text">{{ selectedPlan.content }}</div>
+            </div>
           </div>
-          <div v-else class="detail-content empty-detail">
-            未选择具体预案,点击上方匹配结果中的预案可查看详情。
+          <div v-else class="empty-detail">
+            <el-empty description="未选择具体预案,点击上方匹配结果中的预案可查看详情" :image-size="60" />
           </div>
-        </div>
+        </el-card>
       </div>
     </div>
+
+    <!-- 应用预案确认弹窗 -->
+    <el-dialog v-model="applyDialogVisible" title="应用预案确认" width="480px" append-to-body>
+      <div v-if="selectedPlan" style="line-height:2">
+        <p><strong>预案名称:</strong>{{ selectedPlan.name }}</p>
+        <p><strong>匹配度:</strong>{{ selectedPlan.matchScore }}%</p>
+        <p><strong>事件类型:</strong>{{ selectedPlan.type }} | <strong>级别:</strong>{{ selectedPlan.level }}</p>
+        <p><strong>区域:</strong>{{ selectedPlan.region }} | <strong>责任单位:</strong>{{ selectedPlan.unit }}</p>
+        <el-divider />
+        <p>预案内容将分发至应急指挥中心及相应处置单位,确定应用此预案?</p>
+      </div>
+      <template #footer>
+        <el-button @click="applyDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmApply">确认应用</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Setting, InfoFilled, Clock, Promotion, Document, Checked, Search } from '@element-plus/icons-vue'
 
-// 数字预案库
-const digitalPreplanLibrary = ref([
-  { id: "P001", name: "城东燃气管网泄漏应急预案", type: "管道泄漏", level: "较大(Ⅲ级)", region: "城东管网区", unit: "市燃气集团", score: 95, content: "立即关闭上下游阀门,启动应急疏散,调集抢修车组,联动消防。专题:燃气泄漏专题" },
-  { id: "P002", name: "城南供水管网爆管抢修预案", type: "爆管事故", level: "重大(Ⅱ级)", region: "城南工业区", unit: "水务管网公司", score: 92, content: "隔离爆管区域,启用备用水源,组织抢修班组,发布停水公告。专题:供水爆管专题" },
-  { id: "P003", name: "第三方破坏应急联动预案", type: "第三方破坏", level: "一般(Ⅳ级)", region: "城西居民区", unit: "市政管理所", score: 88, content: "制止施工,评估破坏程度,快速修复,责任认定。专题:第三方施工破坏" },
-  { id: "P004", name: "城北枢纽区燃气重大泄漏预案", type: "管道泄漏", level: "特别重大(Ⅰ级)", region: "城北枢纽区", unit: "市燃气集团", score: 96, content: "区域交通管制,全方面稀释,紧急疏散周边居民。专题:燃气泄漏专题" },
-  { id: "P005", name: "中心城区防汛排涝应急预案", type: "自然损坏", level: "较大(Ⅲ级)", region: "中心城区", unit: "市政管理所", score: 85, content: "启动排涝泵站,布防沙袋,应急抽水组待命。专题:防汛排涝专题" },
-  { id: "P006", name: "供热管道故障处置预案", type: "管道泄漏", level: "一般(Ⅳ级)", region: "城西居民区", unit: "热力供应公司", score: 82, content: "隔离故障段,启动备用热源,入户安抚。专题:供热故障专题" },
-  { id: "P007", name: "电力管廊事故紧急处置预案", type: "第三方破坏", level: "重大(Ⅱ级)", region: "城南工业区", unit: "市政管理所", score: 87, content: "切断电源,组织电缆抢修,应急供电车保障。专题:电力管廊事故" },
-  { id: "P008", name: "通信光缆中断抢修预案", type: "自然损坏", level: "一般(Ⅳ级)", region: "城东管网区", unit: "市政管理所", score: 80, content: "启用备用路由,光缆熔接抢修,协调运营商。专题:通信光缆中断" }
-])
+// 下拉选项
+const eventTypeOptions = ['管道泄漏', '管网爆管', '第三方破坏', '路面塌陷', '火灾']
+const eventLevelOptions = ['一般(IV级)', '较大(III级)', '重大(II级)', '特别重大(I级)']
+const eventRegionOptions = ['城东管网区', '城南管网区', '城北管网区', '城西管网区']
+const unitOptions = ['县供水集团', '市燃气集团', '区排水公司']
 
-// 表单数据
+// 表单
 const form = reactive({
-  eventType: "管道泄漏",
-  eventLevel: "较大(Ⅲ级)",
-  eventRegion: "城东管网区",
-  responsibleUnit: "市燃气集团"
+  eventType: '管道泄漏',
+  eventLevel: '较大(III级)',
+  eventRegion: '城东管网区',
+  responsibleUnit: '市燃气集团'
 })
 
+// 数字预案库
+const digitalPlanLibrary = ref([
+  { id: 'P001', name: '城东燃气管网泄漏应急预案', type: '管道泄漏', level: '较大(III级)', region: '城东管网区', unit: '市燃气集团', score: 95, content: '立即关闭上下游阀门,启动应急疏散,调集抢修车组,联动消防。专题:燃气泄漏专题', summary: '针对城东燃气管网泄漏的应急处置方案,涵盖阀门控制、疏散、消防联动等步骤。' },
+  { id: 'P002', name: '城南供水管网爆管抢修预案', type: '管网爆管', level: '重大(II级)', region: '城南管网区', unit: '县供水集团', score: 92, content: '隔离爆管区域,启用备用水源,组织抢修班组,发布停水公告。专题:供水爆管专题', summary: '城南区域供水管网爆管处置流程,包括区域隔离、备用水源调度和抢修组织。' },
+  { id: 'P003', name: '第三方破坏应急联动预案', type: '第三方破坏', level: '一般(IV级)', region: '城西管网区', unit: '区排水公司', score: 88, content: '制止施工,评估破坏程度,快速修复,责任认定。专题:第三方施工破坏', summary: '针对第三方施工破坏管网的应急处置,涵盖制止施工、评估修复及责任认定。' },
+  { id: 'P004', name: '城北燃气重大泄漏预案', type: '管道泄漏', level: '特别重大(I级)', region: '城北管网区', unit: '市燃气集团', score: 96, content: '区域交通管制,全方面稀释,紧急疏散周边居民。专题:燃气泄漏专题', summary: '城北枢纽区特别重大燃气泄漏处置方案,含交通管制、疏散及稀释措施。' },
+  { id: 'P005', name: '中心城区防汛排涝应急预案', type: '路面塌陷', level: '较大(III级)', region: '城南管网区', unit: '区排水公司', score: 85, content: '启动排涝泵站,布防沙袋,应急抽水组待命。专题:防汛排涝专题', summary: '中心城区防汛排涝预案,涉及排涝泵站、沙袋布防和应急抽水调度。' },
+  { id: 'P006', name: '供热管道故障处置预案', type: '管道泄漏', level: '一般(IV级)', region: '城西管网区', unit: '县供水集团', score: 82, content: '隔离故障段,启动备用热源,入户安抚。专题:供热故障专题', summary: '供热管道泄漏故障的处置方案,含故障隔离、备用热源启动及入户安抚。' },
+  { id: 'P007', name: '电力管廊事故紧急处置预案', type: '第三方破坏', level: '重大(II级)', region: '城南管网区', unit: '区排水公司', score: 87, content: '切断电源,组织电缆抢修,应急供电车保障。专题:电力管廊事故', summary: '电力管廊事故应急处置,涵盖断电、电缆抢修和应急供电车调度。' },
+  { id: 'P008', name: '通信光缆中断抢修预案', type: '路面塌陷', level: '一般(IV级)', region: '城东管网区', unit: '区排水公司', score: 80, content: '启用备用路由,光缆熔接抢修,协调运营商。专题:通信光缆中断', summary: '通信光缆中断抢修方案,涉及备用路由启用、光缆熔接及运营商协调。' },
+  { id: 'P009', name: '火灾应急处置预案', type: '火灾', level: '重大(II级)', region: '城北管网区', unit: '市燃气集团', score: 91, content: '启动消防联动,现场灭火,疏散周边人员,设置警戒区。专题:火灾专题', summary: '火灾事故应急处置方案,包含消防联动、人员疏散和警戒区设置。' },
+  { id: 'P010', name: '城南火灾联动应急方案', type: '火灾', level: '较大(III级)', region: '城南管网区', unit: '区排水公司', score: 84, content: '调集消防力量,控制火势蔓延,保护周边管网设施。专题:火灾专题', summary: '城南区域火灾联动方案,侧重火势控制与管网设施保护。' }
+])
+
 // 专题列表
 const topics = ref([
-  { label: "🔥 燃气泄漏专题", value: "燃气泄漏专题" },
-  { label: "💧 供水爆管专题", value: "供水爆管专题" },
-  { label: "🌊 防汛排涝专题", value: "防汛排涝专题" },
-  { label: "❄️ 供热故障专题", value: "供热故障专题" },
-  { label: "⚡ 电力管廊事故", value: "电力管廊事故" },
-  { label: "📡 通信光缆中断", value: "通信光缆中断" }
+  { label: '燃气泄漏专题', value: '燃气泄漏专题' },
+  { label: '供水爆管专题', value: '供水爆管专题' },
+  { label: '防汛排涝专题', value: '防汛排涝专题' },
+  { label: '供热故障专题', value: '供热故障专题' },
+  { label: '电力管廊事故', value: '电力管廊事故' },
+  { label: '通信光缆中断', value: '通信光缆中断' }
 ])
 
-// 状态数据
-const matchResults = ref([])
-const selectedPreplan = ref(null)
-const activeTopic = ref("")
+// 最近匹配记录
+const matchHistory = ref([
+  { id: 1, date: '2025-06-18', desc: '城东泄漏', planName: '城东燃气管网泄漏应急预案' },
+  { id: 2, date: '2025-06-15', desc: '城南爆管', planName: '供水管网爆管抢修预案' },
+  { id: 3, date: '2025-06-10', desc: '第三方破坏', planName: '第三方施工破坏应急方案' },
+  { id: 4, date: '2025-06-05', desc: '城北火灾', planName: '火灾应急处置预案' }
+])
 
-// 计算匹配结果数量显示
-const matchResultCount = computed(() => {
-  if (matchResults.value.length === 0) return "待匹配"
-  return `匹配到 ${matchResults.value.length} 个预案`
-})
+// 状态
+const matchResults = ref([])
+const selectedPlan = ref(null)
+const activeTopic = ref('')
+const applyDialogVisible = ref(false)
 
-// 匹配算法:根据条件计算相似度
+// 匹配算法
 const matchPreplan = (eventType, eventLevel, eventRegion, responsibleUnit) => {
-  const scored = digitalPreplanLibrary.value.map(pre => {
+  const scored = digitalPlanLibrary.value.map(plan => {
     let score = 0
-    if (pre.type === eventType) score += 40
-    if (pre.level === eventLevel) score += 25
-    if (pre.region === eventRegion) score += 20
-    if (pre.unit === responsibleUnit) score += 15
-    return { ...pre, matchScore: score }
+    if (plan.type === eventType) score += 40
+    if (plan.level === eventLevel) score += 25
+    if (plan.region === eventRegion) score += 20
+    if (plan.unit === responsibleUnit) score += 15
+    return { ...plan, matchScore: score }
   })
-  // 按匹配度降序排序,过滤掉匹配度为0的
   return scored.filter(p => p.matchScore > 0).sort((a, b) => b.matchScore - a.matchScore)
 }
 
-// 根据专题名称查找匹配预案
+// 专题匹配
 const matchByTopic = (topicName) => {
   activeTopic.value = topicName
-  let matched = []
-
-  if (topicName === "燃气泄漏专题") {
-    matched = digitalPreplanLibrary.value.filter(p => p.name.includes("燃气") || p.content.includes("燃气"))
-  } else if (topicName === "供水爆管专题") {
-    matched = digitalPreplanLibrary.value.filter(p => p.name.includes("供水") || p.type === "爆管事故")
-  } else if (topicName === "防汛排涝专题") {
-    matched = digitalPreplanLibrary.value.filter(p => p.name.includes("防汛") || p.content.includes("防汛"))
-  } else if (topicName === "供热故障专题") {
-    matched = digitalPreplanLibrary.value.filter(p => p.name.includes("供热") || p.unit === "热力供应公司")
-  } else if (topicName === "电力管廊事故") {
-    matched = digitalPreplanLibrary.value.filter(p => p.name.includes("电力") || p.content.includes("电力"))
-  } else if (topicName === "通信光缆中断") {
-    matched = digitalPreplanLibrary.value.filter(p => p.name.includes("通信") || p.content.includes("光缆"))
-  }
-
-  // 增加匹配得分模拟
-  const results = matched.map(m => ({ ...m, matchScore: 85 + Math.floor(Math.random() * 10) }))
-  matchResults.value = results
-
-  if (results.length > 0) {
-    selectedPreplan.value = results[0]
-  } else {
-    selectedPreplan.value = null
+  const keywordMap = {
+    '燃气泄漏专题': p => p.name.includes('燃气') || p.content.includes('燃气'),
+    '供水爆管专题': p => p.name.includes('供水') || p.type === '管网爆管',
+    '防汛排涝专题': p => p.name.includes('防汛') || p.content.includes('防汛'),
+    '供热故障专题': p => p.name.includes('供热') || p.content.includes('供热'),
+    '电力管廊事故': p => p.name.includes('电力') || p.content.includes('电力'),
+    '通信光缆中断': p => p.name.includes('通信') || p.content.includes('光缆')
   }
+  const filterFn = keywordMap[topicName] || (() => false)
+  const matched = digitalPlanLibrary.value.filter(filterFn)
+  matchResults.value = matched.map(m => ({ ...m, matchScore: 85 + Math.floor(Math.random() * 12) }))
+  selectedPlan.value = matchResults.value.length > 0 ? matchResults.value[0] : null
 }
 
 // 手动匹配
 const manualMatch = () => {
-  activeTopic.value = ""
+  activeTopic.value = ''
   const matched = matchPreplan(form.eventType, form.eventLevel, form.eventRegion, form.responsibleUnit)
   matchResults.value = matched
-
-  if (matched.length > 0) {
-    selectedPreplan.value = matched[0]
-  } else {
-    selectedPreplan.value = null
-  }
+  selectedPlan.value = matched.length > 0 ? matched[0] : null
 }
 
 // 选择预案
-const selectPreplan = (preplan) => {
-  selectedPreplan.value = preplan
+const selectPlan = (plan) => {
+  selectedPlan.value = plan
 }
 
 // 应用预案
 const applyPlan = () => {
-  if (selectedPreplan.value) {
-    alert(`✅ 已启动预案: ${selectedPreplan.value.name}\n预案内容将分发至应急指挥中心及相应处置单位。`)
-  } else {
-    alert("请先匹配并选择一个预案。")
+  if (!selectedPlan.value) {
+    ElMessage.warning('请先选择一个预案')
+    return
   }
+  applyDialogVisible.value = true
+}
+
+const confirmApply = () => {
+  applyDialogVisible.value = false
+  ElMessage.success(`已启动预案: ${selectedPlan.value.name},预案内容将分发至应急指挥中心及相应处置单位。`)
 }
 
 // 初始化默认匹配
 const initDefaultMatch = () => {
-  const defaultMatches = matchPreplan(form.eventType, form.eventLevel, form.eventRegion, form.responsibleUnit)
-  if (defaultMatches.length) {
-    matchResults.value = defaultMatches
-    selectedPreplan.value = defaultMatches[0]
+  const matched = matchPreplan(form.eventType, form.eventLevel, form.eventRegion, form.responsibleUnit)
+  if (matched.length) {
+    matchResults.value = matched
+    selectedPlan.value = matched[0]
   }
 }
-
-// 执行初始化
 initDefaultMatch()
 </script>
 
 <style scoped>
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #ffffff;
-  border-radius: 32px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.12);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
-
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 18px 32px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-  border-bottom: 3px solid #2e9c8c;
-}
-
-.logo h2 {
-  color: white;
-  font-weight: 600;
-  font-size: 1.5rem;
-}
-
-.logo p {
-  color: #bdd8e8;
-  font-size: 0.75rem;
-}
-
-.badge-ai {
-  background: #1e4a62;
-  padding: 6px 18px;
-  border-radius: 30px;
-  color: white;
-  font-weight: 500;
-  font-size: 0.8rem;
+.app-container {
+  padding: 16px;
+  background: #f0f2f5;
+  min-height: calc(100vh - 84px);
 }
 
-/* 主体布局 */
-.main-layout {
+.plan-layout {
   display: flex;
-  flex-wrap: wrap;
+  gap: 16px;
+  height: 100%;
 }
 
 .left-panel {
-  width: 400px;
-  background: #f9fbfd;
-  border-right: 1px solid #e2edf2;
-  padding: 24px 22px;
+  width: 30%;
+  min-width: 320px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
 }
 
 .right-panel {
   flex: 1;
-  background: #ffffff;
-  padding: 24px 28px;
-}
-
-/* 卡片样式 */
-.card {
-  background: white;
-  border-radius: 24px;
-  border: 1px solid #e6edf3;
-  margin-bottom: 28px;
-  overflow: hidden;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
-}
-
-.card-header {
-  background: #f9fafc;
-  padding: 16px 20px;
-  border-bottom: 1px solid #eef2f6;
-  font-weight: 700;
-  font-size: 1rem;
   display: flex;
-  justify-content: space-between;
-  align-items: center;
-  color: #1d5a78;
+  flex-direction: column;
+  gap: 12px;
+  min-width: 0;
 }
 
-.card-body {
-  padding: 20px;
+.panel-card {
+  border-radius: 8px;
 }
 
-.form-group {
-  margin-bottom: 20px;
+.card-title {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-weight: 600;
+  font-size: 14px;
+  color: #303133;
 }
 
-.form-label {
-  font-size: 0.8rem;
-  font-weight: 600;
-  margin-bottom: 8px;
-  display: block;
-  color: #2c5a72;
+.card-title .el-icon {
+  color: #409EFF;
+  font-size: 16px;
 }
 
-.form-select, .form-input {
-  width: 100%;
-  padding: 10px 14px;
-  border-radius: 12px;
-  border: 1px solid #cbdde6;
-  font-size: 0.85rem;
-  background: white;
-  transition: 0.2s;
+/* 匹配规则 */
+.rule-desc {
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.8;
 }
 
-.form-select:focus {
-  outline: none;
-  border-color: #2e9c8c;
-  box-shadow: 0 0 0 2px rgba(46, 156, 140, 0.1);
+.rule-weights {
+  margin: 10px 0;
 }
 
-.btn-primary {
-  background: #2e9c8c;
-  border: none;
-  color: white;
-  padding: 10px 24px;
-  border-radius: 30px;
-  font-weight: 600;
-  font-size: 0.85rem;
-  cursor: pointer;
-  transition: 0.2s;
-  width: 100%;
+.weight-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 6px;
 }
 
-.btn-primary:hover {
-  background: #237f71;
+.weight-label {
+  width: 70px;
+  font-size: 12px;
+  color: #909399;
+  flex-shrink: 0;
 }
 
-.btn-outline {
-  background: white;
-  border: 1px solid #2e9c8c;
-  color: #2e9c8c;
-  padding: 8px 18px;
-  border-radius: 30px;
-  font-weight: 500;
-  font-size: 0.8rem;
-  cursor: pointer;
-  transition: 0.2s;
+.weight-item .el-progress {
+  flex: 1;
 }
 
-.btn-outline:hover {
-  background: #e6f4f1;
+.rule-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
 }
 
-.rule-box {
-  background: #eef3f7;
-  border-radius: 16px;
-  padding: 14px;
-  margin: 16px 0;
-  font-size: 0.75rem;
-  border-left: 3px solid #2e9c8c;
+/* 最近匹配记录 */
+.history-list {
+  max-height: 160px;
+  overflow-y: auto;
 }
 
-.topic-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
-  gap: 12px;
-  margin-top: 12px;
+.history-item {
+  padding: 8px 0;
+  border-bottom: 1px solid #f0f0f0;
+  font-size: 12px;
+  color: #606266;
 }
 
-.topic-btn {
-  background: #f8fafc;
-  border: 1px solid #dce5ec;
-  border-radius: 40px;
-  padding: 10px 12px;
-  text-align: center;
-  cursor: pointer;
-  transition: all 0.2s;
-  font-size: 0.8rem;
-  font-weight: 500;
-  color: #2c5e7a;
+.history-item:last-child {
+  border-bottom: none;
 }
 
-.topic-btn:hover {
-  background: #2e9c8c;
-  color: white;
-  border-color: #2e9c8c;
+.history-date {
+  color: #909399;
+  font-size: 11px;
 }
 
-.topic-btn.active {
-  background: #2e9c8c;
-  color: white;
-  border-color: #2e9c8c;
+.history-desc {
+  margin-top: 2px;
+  color: #303133;
+}
+
+/* 专题按钮 */
+.topic-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
 }
 
-.result-badge-small {
-  font-size: 0.7rem;
-  background: #eef2fa;
-  padding: 2px 10px;
-  border-radius: 20px;
+.topic-tip {
+  margin-top: 10px;
+  font-size: 12px;
+  color: #909399;
+  text-align: center;
 }
 
-.pre-list {
-  max-height: 320px;
+/* 预案列表 */
+.plan-list {
+  max-height: 360px;
   overflow-y: auto;
 }
 
-.pre-item {
-  background: #fafeff;
-  border-radius: 14px;
-  padding: 14px;
-  margin-bottom: 10px;
-  border: 1px solid #e9f0f5;
+.plan-item {
+  padding: 12px;
+  margin-bottom: 8px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
   cursor: pointer;
-  transition: 0.1s;
+  transition: all 0.2s;
+  background: #fafafa;
 }
 
-.pre-item:hover {
-  background: #f0f6fa;
-  border-color: #2e9c8c;
+.plan-item:hover {
+  border-color: #409EFF;
+  background: #ecf5ff;
 }
 
-.pre-item.selected {
-  background: #e6f4f1;
-  border-color: #2e9c8c;
-  border-left: 3px solid #2e9c8c;
+.plan-item.selected {
+  border-color: #409EFF;
+  background: #ecf5ff;
+  border-left: 3px solid #409EFF;
 }
 
-.pre-name {
-  font-weight: 700;
-  margin-bottom: 6px;
+.plan-head {
   display: flex;
   justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
 }
 
-.pre-meta {
-  font-size: 0.7rem;
-  color: #6f8ea8;
+.plan-name {
+  font-weight: 600;
+  font-size: 14px;
+  color: #303133;
+}
+
+.plan-tags {
   display: flex;
-  gap: 12px;
+  gap: 6px;
   flex-wrap: wrap;
+  margin-bottom: 6px;
 }
 
-.confidence {
-  color: #2e9c8c;
-  font-weight: 600;
+.plan-summary {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.6;
 }
 
-.empty-state {
-  text-align: center;
-  padding: 40px;
-  color: #8aaec2;
-  font-size: 0.85rem;
+/* 详情区域 */
+.detail-content {
+  font-size: 13px;
 }
 
-.detail-card {
-  background: #f7fbfe;
-  border-radius: 20px;
-  padding: 16px;
-  margin-top: 8px;
-  border: 1px solid #e2edf2;
+.detail-summary {
+  margin-top: 12px;
 }
 
-.detail-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+.summary-label {
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 6px;
+  font-size: 13px;
 }
 
-.detail-content {
-  font-size: 0.75rem;
-  margin-top: 12px;
-  color: #2b5e7a;
-  line-height: 1.6;
+.summary-text {
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.8;
+  padding: 10px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+
+.empty-state {
+  padding: 20px 0;
 }
 
 .empty-detail {
-  color: #8aaec2;
+  padding: 10px 0;
 }
-</style>
+</style>

+ 441 - 532
src/views/subSystem/dispatch/disDecisionDep/DisRoute.vue

@@ -1,98 +1,129 @@
 <!--路线规划辅助-->
 <template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>🗺️ 智能应急路线规划系统</h2>
-        <p>基于GIS · 实时绕行分析 | 最短路径 · 最快路径 | 应急处置人员 & 社会公众出行</p>
-      </div>
-      <div class="badge-gis">📍 实时GIS引擎 · 智能路径计算</div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧控制面板 -->
-      <div class="info-panel">
-        <!-- 突发事件地点设置 -->
-        <div class="card">
-          <div class="card-header">🚨 突发事件定位</div>
-          <div class="card-body">
-            <div class="form-group">
-              <label class="form-label">事件地点</label>
-              <select v-model="selectedEventLocation" class="form-select" @change="refreshAll">
-                <option value="化工园区">化工园区(经度:118.5,纬度:31.2)</option>
-                <option value="城南枢纽">城南枢纽(经度:118.45,纬度:31.15)</option>
-                <option value="城西隧道">城西隧道(经度:118.38,纬度:31.22)</option>
-                <option value="滨海大道">滨海大道(经度:118.62,纬度:31.08)</option>
-              </select>
+  <div class="app-container">
+    <div class="route-layout">
+      <!-- 左侧信息面板 -->
+      <div class="left-panel">
+        <!-- 突发事件定位 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Location /></el-icon>
+              <span>突发事件定位</span>
+            </div>
+          </template>
+          <div class="event-location">
+            <div class="loc-row">
+              <span class="loc-label">事件地点</span>
+              <el-tag type="danger" effect="dark">{{ selectedEventLocation }}</el-tag>
+            </div>
+            <div class="loc-row">
+              <span class="loc-label">经度</span>
+              <span class="loc-value">{{ eventLocations[selectedEventLocation].lng }}</span>
+            </div>
+            <div class="loc-row">
+              <span class="loc-label">纬度</span>
+              <span class="loc-value">{{ eventLocations[selectedEventLocation].lat }}</span>
             </div>
-            <button @click="refreshAll" class="btn-primary">🔄 分析路线 & 刷新GIS</button>
           </div>
-        </div>
-
-        <!-- 值守/指挥人员位置 -->
-        <div class="card">
-          <div class="card-header">👥 值守人员 & 指挥人员</div>
-          <div class="card-body">
-            <div class="person-list">
-              <div v-for="person in personnel" :key="person.id" class="person-item">
-                <span><strong>{{ person.name }}</strong> ({{ person.role }})</span>
-                <span class="role-tag">📍 {{ person.position }}</span>
-              </div>
+          <el-select v-model="selectedEventLocation" placeholder="选择事件地点" style="width:100%;margin-bottom:12px" @change="refreshAll">
+            <el-option v-for="(loc, key) in eventLocations" :key="key" :label="key" :value="key" />
+          </el-select>
+          <el-button type="primary" style="width:100%" @click="refreshAll">
+            <el-icon><Position /></el-icon> 分析路线 &amp; 跳转GIS
+          </el-button>
+        </el-card>
+
+        <!-- 值守/指挥人员 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><User /></el-icon>
+              <span>值守人员 &amp; 指挥人员</span>
             </div>
-            <div style="font-size:0.65rem; margin-top:8px; color:#6e95b0;">
-              💡 系统基于人员当前位置自动分析绕行路线
+          </template>
+          <div class="person-list">
+            <div v-for="person in personnel" :key="person.id" class="person-item">
+              <div class="person-info">
+                <span class="person-name">{{ person.name }}</span>
+                <el-tag :type="person.role === '指挥人员' ? 'warning' : 'primary'" size="small">{{ person.role }}</el-tag>
+              </div>
+              <el-button size="small" text type="primary">{{ person.position }}</el-button>
             </div>
           </div>
-        </div>
-
-        <!-- 路径规划结果 -->
-        <div class="card">
-          <div class="card-header">🛣️ 智能绕行路线推荐</div>
-          <div class="card-body">
-            <div class="route-result">
-              <div
-                  v-for="route in displayRoutes"
-                  :key="route.type"
-                  class="route-item"
-                  :class="{ active: activeRouteType === route.type }"
-                  @click="selectRoute(route)"
-              >
-                <div class="route-name">
-                  <span>{{ route.icon }} {{ route.name }}</span>
-                  <span class="badge-dist">{{ route.metricValue }}</span>
-                </div>
-                <div class="route-detail">{{ route.pathDisplay }}</div>
-                <div class="route-meta">
-                  🚗 {{ route.estimateTime }}
-                </div>
+          <div class="person-tip">系统根据人员当前位置自动分析最优路线</div>
+        </el-card>
+
+        <!-- 智能绕行路线推荐 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Guide /></el-icon>
+              <span>智能绕行路线推荐</span>
+            </div>
+          </template>
+          <div class="route-list">
+            <!-- 最短路径 -->
+            <div
+              class="route-item"
+              :class="{ active: activeRouteType === 'shortest' }"
+              @click="selectRoute('shortest')"
+            >
+              <div class="route-head">
+                <span class="route-name">最短路径(距离优先)</span>
+                <el-tag size="small" type="success">{{ displayRoutes.shortest?.distance || '--' }} km</el-tag>
+              </div>
+              <div class="route-path">{{ displayRoutes.shortest?.pathDisplay || '--' }}</div>
+              <div class="route-meta">
+                <span v-if="displayRoutes.shortest">预估耗时 {{ displayRoutes.shortest.estimateTime }}分钟,途经{{ displayRoutes.shortest.viaArea }}</span>
               </div>
             </div>
-            <div style="margin-top: 12px;">
-              <button @click="showPublicRoute" class="btn-outline" style="width:100%;">
-                👥 展示社会公众绕行方案
-              </button>
+            <!-- 最快路径 -->
+            <div
+              class="route-item"
+              :class="{ active: activeRouteType === 'fastest' }"
+              @click="selectRoute('fastest')"
+            >
+              <div class="route-head">
+                <span class="route-name">最快路径(时间优先)</span>
+                <el-tag size="small" type="warning">{{ displayRoutes.fastest?.time || '--' }} min</el-tag>
+              </div>
+              <div class="route-path">{{ displayRoutes.fastest?.pathDisplay || '--' }}</div>
+              <div class="route-meta">
+                <span v-if="displayRoutes.fastest">避开拥堵,节省约{{ displayRoutes.fastest.savePercent }}%时间</span>
+              </div>
             </div>
           </div>
-        </div>
+        </el-card>
       </div>
 
-      <!-- 右侧GIS地图展示区 -->
-      <div class="map-panel">
-        <div class="gis-container">
-          <svg
-              ref="svgMap"
-              class="map-svg"
-              viewBox="0 0 800 550"
-              preserveAspectRatio="xMidYMid meet"
+      <!-- 右侧地图区域 -->
+      <div class="right-panel">
+        <div class="map-container">
+          <!-- 地名标签 -->
+          <div v-for="(pos, key) in mapLabels" :key="key" class="map-label" :style="{ left: pos.x + '%', top: pos.y + '%' }">
+            {{ key }}
+          </div>
+          <!-- 路线连接线 -->
+          <div v-for="(line, idx) in mapLines" :key="'line-'+idx" class="map-line" :style="line.style" :class="line.type" />
+          <!-- 模拟标记点 -->
+          <div
+            v-for="marker in mapMarkers"
+            :key="marker.id"
+            class="map-marker"
+            :class="marker.type"
+            :style="{ left: marker.x + '%', top: marker.y + '%' }"
           >
-            <!-- 地图内容动态绘制 -->
-          </svg>
+            <div class="marker-dot"></div>
+            <div class="marker-pulse" v-if="marker.type === 'event'"></div>
+            <div class="marker-label">{{ marker.name }}</div>
+          </div>
+          <!-- 图例 -->
           <div class="map-legend">
-            <div><span class="legend-color" style="background:#e74c3c;"></span> 突发事件点</div>
-            <div><span class="legend-color" style="background:#3498db;"></span> 值守/指挥人员</div>
-            <div><span class="legend-color" style="background:#e07c3c;"></span> 推荐路线</div>
-            <div><span class="legend-color" style="background:#f39c12;"></span> 绕行避让路段</div>
+            <div class="legend-item"><span class="legend-dot event"></span> 突发事件点</div>
+            <div class="legend-item"><span class="legend-dot person"></span> 值守/指挥人员站点</div>
+            <div class="legend-item"><span class="legend-dot route"></span> 推荐路线/备选站</div>
+            <div class="legend-item"><span class="legend-line"></span> 绕行通过路段</div>
           </div>
         </div>
       </div>
@@ -101,454 +132,191 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed, onMounted, nextTick } from 'vue'
-
-// ==================== 节点坐标数据 ====================
-const nodes = {
-  "化工园区": { x: 180, y: 380, name: "化工园区" },
-  "城南枢纽": { x: 250, y: 460, name: "城南枢纽" },
-  "城西隧道": { x: 100, y: 280, name: "城西隧道" },
-  "滨海大道": { x: 600, y: 200, name: "滨海大道" },
-  "应急指挥中心": { x: 420, y: 300, name: "应急指挥中心" },
-  "消防站A": { x: 320, y: 220, name: "消防站A" },
-  "医疗点B": { x: 500, y: 400, name: "医疗点B" },
-  "交警驻点": { x: 280, y: 360, name: "交警驻点" },
-  "东侧入口": { x: 700, y: 320, name: "东侧入口" },
-  "西侧绕行点": { x: 60, y: 400, name: "西侧绕行点" },
-  "北环快速": { x: 350, y: 120, name: "北环快速" }
-}
-
-// ==================== 人员数据 ====================
-const personnel = ref([
-  { id: 1, name: "张建国", role: "指挥人员", type: "commander", position: "应急指挥中心", nodeKey: "应急指挥中心" },
-  { id: 2, name: "李卫东", role: "值守人员", type: "dispatcher", position: "消防站A", nodeKey: "消防站A" },
-  { id: 3, name: "王敏", role: "值守人员", type: "dispatcher", position: "交警驻点", nodeKey: "交警驻点" },
-  { id: 4, name: "陈方", role: "指挥人员", type: "commander", position: "医疗点B", nodeKey: "医疗点B" }
-])
-
-// ==================== 路网数据 ====================
-const edges = [
-  { from: "化工园区", to: "交警驻点", dist: 3.2, speed: 40, time: 4.8 },
-  { from: "化工园区", to: "城南枢纽", dist: 4.5, speed: 50, time: 5.4 },
-  { from: "城南枢纽", to: "医疗点B", dist: 2.8, speed: 45, time: 3.7 },
-  { from: "城南枢纽", to: "东侧入口", dist: 5.0, speed: 60, time: 5.0 },
-  { from: "交警驻点", to: "应急指挥中心", dist: 2.5, speed: 40, time: 3.75 },
-  { from: "交警驻点", to: "西侧绕行点", dist: 2.2, speed: 35, time: 3.77 },
-  { from: "应急指挥中心", to: "消防站A", dist: 1.8, speed: 45, time: 2.4 },
-  { from: "应急指挥中心", to: "北环快速", dist: 4.0, speed: 65, time: 3.69 },
-  { from: "消防站A", to: "医疗点B", dist: 3.5, speed: 40, time: 5.25 },
-  { from: "消防站A", to: "城西隧道", dist: 4.2, speed: 35, time: 7.2 },
-  { from: "医疗点B", to: "滨海大道", dist: 5.8, speed: 55, time: 6.33 },
-  { from: "北环快速", to: "滨海大道", dist: 6.2, speed: 70, time: 5.31 },
-  { from: "西侧绕行点", to: "城西隧道", dist: 3.0, speed: 40, time: 4.5 },
-  { from: "东侧入口", to: "滨海大道", dist: 2.9, speed: 60, time: 2.9 },
-  { from: "城西隧道", to: "应急指挥中心", dist: 5.1, speed: 30, time: 10.2 },
-  { from: "交警驻点", to: "东侧入口", dist: 6.3, speed: 55, time: 6.87 }
-]
-
-// ==================== 响应式数据 ====================
-const selectedEventLocation = ref("化工园区")
-const svgMap = ref(null)
-const activeRouteType = ref("shortest")
-const currentRoutes = ref({ shortest: null, fastest: null, personName: "" })
-
-// ==================== Dijkstra 算法 ====================
-const getAllNodeNames = () => Object.keys(nodes)
-
-const buildGraph = (weightType = 'dist') => {
-  const graph = {}
-  for (let node in nodes) graph[node] = []
-  for (let edge of edges) {
-    const weight = weightType === 'dist' ? edge.dist : edge.time
-    graph[edge.from].push({ node: edge.to, weight })
-    graph[edge.to].push({ node: edge.from, weight })
-  }
-  return graph
-}
+import { ref, reactive, computed, onMounted } from 'vue'
+import { Location, Position, User, Guide } from '@element-plus/icons-vue'
 
-const dijkstra = (graph, nodesList, start, end, weightKey) => {
-  const distances = {}
-  const previous = {}
-  const unvisited = new Set()
-  for (let node of nodesList) {
-    distances[node] = Infinity
-    previous[node] = null
-    unvisited.add(node)
-  }
-  distances[start] = 0
-
-  while (unvisited.size > 0) {
-    let current = null
-    for (let node of unvisited) {
-      if (current === null || distances[node] < distances[current]) {
-        current = node
-      }
-    }
-    if (current === end || distances[current] === Infinity) break
-    unvisited.delete(current)
-
-    const neighbors = graph[current] || []
-    for (let neighbor of neighbors) {
-      const alt = distances[current] + neighbor.weight
-      if (alt < distances[neighbor.node]) {
-        distances[neighbor.node] = alt
-        previous[neighbor.node] = current
-      }
-    }
-  }
-
-  const path = []
-  let u = end
-  while (previous[u] !== null) {
-    path.unshift(u)
-    u = previous[u]
-  }
-  if (path.length > 0 && path[0] === end) path.unshift(start)
-  else if (path.length === 0 && start === end) path.push(start)
-
-  return { distance: distances[end], path }
+// 事件地点坐标
+const eventLocations = {
+  '化工园区': { lng: '118.50', lat: '31.20' },
+  '城南枢纽': { lng: '118.45', lat: '31.15' },
+  '城西隧道': { lng: '118.38', lat: '31.22' },
+  '滨海大道': { lng: '118.62', lat: '31.08' }
 }
 
-// 计算最优路线
-const computeOptimalRoutes = (eventNode) => {
-  const nodeNames = getAllNodeNames()
-  const emergencyPersons = personnel.value.filter(p => p.role === "指挥人员" || p.role === "值守人员")
-
-  let bestShortest = null
-  let bestFastest = null
-  let bestPerson = ""
-
-  for (let person of emergencyPersons) {
-    const startNode = person.nodeKey
-    if (!startNode) continue
+const selectedEventLocation = ref('化工园区')
+const activeRouteType = ref('shortest')
 
-    const graphDist = buildGraph('dist')
-    const distResult = dijkstra(graphDist, nodeNames, startNode, eventNode, 'dist')
-
-    const graphTime = buildGraph('time')
-    const timeResult = dijkstra(graphTime, nodeNames, startNode, eventNode, 'time')
+// 人员数据
+const personnel = ref([
+  { id: 1, name: '张建国', role: '指挥人员', position: '应急指挥中心' },
+  { id: 2, name: '李卫东', role: '值守人员', position: '消防站A' },
+  { id: 3, name: '王敏', role: '值守人员', position: '交警驻点' },
+  { id: 4, name: '陈方', role: '指挥人员', position: '医疗点B' }
+])
 
-    if (!bestShortest || (distResult.distance < bestShortest.distance)) {
-      bestShortest = distResult
-      bestPerson = person.name
+// 地图标签
+const mapLabels = {
+  '北环快速': { x: 42, y: 12 },
+  '城西隧道': { x: 10, y: 40 },
+  '滨海大道': { x: 75, y: 28 },
+  '化工园区': { x: 22, y: 62 },
+  '城南枢纽': { x: 30, y: 78 },
+  '东侧入口': { x: 88, y: 52 }
+}
+
+// 地图标记点
+const mapMarkers = computed(() => {
+  const markers = [
+    { id: 'e1', name: '化工园区', x: 22, y: 62, type: 'event' },
+    { id: 'e2', name: '城南枢纽', x: 30, y: 78, type: 'event' },
+    { id: 'e3', name: '城西隧道', x: 10, y: 40, type: 'event' },
+    { id: 'e4', name: '滨海大道', x: 75, y: 28, type: 'event' },
+    { id: 'p1', name: '张建国', x: 52, y: 48, type: 'person' },
+    { id: 'p2', name: '李卫东', x: 40, y: 34, type: 'person' },
+    { id: 'p3', name: '王敏', x: 35, y: 58, type: 'person' },
+    { id: 'p4', name: '陈方', x: 62, y: 68, type: 'person' },
+    { id: 'r1', name: '西侧绕行点', x: 6, y: 68, type: 'route' },
+    { id: 'r2', name: '东侧入口', x: 88, y: 52, type: 'route' },
+    { id: 'r3', name: '北环快速', x: 42, y: 12, type: 'route' }
+  ]
+  // 高亮当前事件点
+  return markers.map(m => {
+    if (m.type === 'event' && m.name === selectedEventLocation.value) {
+      return { ...m, active: true }
     }
-    if (!bestFastest || (timeResult.distance < bestFastest.distance)) {
-      bestFastest = timeResult
-    }
-  }
-
-  return { shortest: bestShortest, fastest: bestFastest, personName: bestPerson }
-}
-
-// ==================== 显示路线数据 ====================
-const displayRoutes = computed(() => {
-  const routes = []
-  if (currentRoutes.value.shortest) {
-    routes.push({
-      type: "shortest",
-      name: "最短路径 (距离优先)",
-      icon: "📏",
-      metricValue: `${currentRoutes.value.shortest.distance?.toFixed(1)} km`,
-      path: currentRoutes.value.shortest.path,
-      pathDisplay: currentRoutes.value.shortest.path?.join(' → ') || '',
-      estimateTime: `预估耗时 ${((currentRoutes.value.shortest.distance / 40) * 60).toFixed(0)}分钟`,
-      personName: currentRoutes.value.personName
-    })
-  }
-  if (currentRoutes.value.fastest) {
-    routes.push({
-      type: "fastest",
-      name: "最快路径 (时间优先)",
-      icon: "⚡",
-      metricValue: `${currentRoutes.value.fastest.distance?.toFixed(1)} min`,
-      path: currentRoutes.value.fastest.path,
-      pathDisplay: currentRoutes.value.fastest.path?.join(' → ') || '',
-      estimateTime: `避开拥堵,节省约 ${Math.floor(Math.random() * 10 + 3)}% 时间`,
-      personName: currentRoutes.value.personName
-    })
-  }
-  return routes
+    return { ...m, active: false }
+  })
 })
 
-// ==================== GIS 地图绘制 ====================
-const drawGIS = (eventNodeKey, activePath = null) => {
-  if (!svgMap.value) return
-
-  const svg = svgMap.value
-  svg.innerHTML = ''
-
-  // 绘制道路
-  for (let edge of edges) {
-    const fromNode = nodes[edge.from]
-    const toNode = nodes[edge.to]
-    if (fromNode && toNode) {
-      const line = document.createElementNS("http://www.w3.org/2000/svg", "line")
-      line.setAttribute("x1", fromNode.x)
-      line.setAttribute("y1", fromNode.y)
-      line.setAttribute("x2", toNode.x)
-      line.setAttribute("y2", toNode.y)
-      line.setAttribute("stroke", "#7f8c8d")
-      line.setAttribute("stroke-width", "2")
-      line.setAttribute("stroke-dasharray", "4 2")
-      line.setAttribute("opacity", "0.5")
-      svg.appendChild(line)
-    }
-  }
-
-  // 绘制节点
-  for (let key in nodes) {
-    const node = nodes[key]
-    const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle")
-    circle.setAttribute("cx", node.x)
-    circle.setAttribute("cy", node.y)
-    circle.setAttribute("r", "6")
-    circle.setAttribute("fill", "#95a5a6")
-    circle.setAttribute("stroke", "white")
-    circle.setAttribute("stroke-width", "1.5")
-    svg.appendChild(circle)
-
-    const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
-    text.setAttribute("x", node.x + 8)
-    text.setAttribute("y", node.y - 5)
-    text.setAttribute("fill", "#ecf0f1")
-    text.setAttribute("font-size", "8")
-    text.textContent = node.name
-    svg.appendChild(text)
+// 地图路线(模拟)
+const mapLines = computed(() => {
+  const lines = []
+  // 基础道路
+  lines.push({ style: { left: '22%', top: '62%', width: '14%', transform: 'rotate(30deg)' }, type: 'road' })
+  lines.push({ style: { left: '22%', top: '62%', width: '10%', transform: 'rotate(-60deg)' }, type: 'road' })
+  lines.push({ style: { left: '40%', top: '34%', width: '14%', transform: 'rotate(40deg)' }, type: 'road' })
+  lines.push({ style: { left: '52%', top: '48%', width: '12%', transform: 'rotate(50deg)' }, type: 'road' })
+  lines.push({ style: { left: '35%', top: '58%', width: '18%', transform: 'rotate(-20deg)' }, type: 'road' })
+
+  // 根据选中路线类型高亮
+  if (activeRouteType.value === 'shortest') {
+    lines.push({ style: { left: '35%', top: '58%', width: '18%', transform: 'rotate(-20deg)' }, type: 'active-route' })
+    lines.push({ style: { left: '52%', top: '48%', width: '12%', transform: 'rotate(50deg)' }, type: 'active-route' })
+  } else {
+    lines.push({ style: { left: '22%', top: '62%', width: '10%', transform: 'rotate(-60deg)' }, type: 'active-route' })
+    lines.push({ style: { left: '10%', top: '40%', width: '28%', transform: 'rotate(5deg)' }, type: 'active-route' })
   }
+  return lines
+})
 
-  // 事件点标记
-  const eventNodeObj = nodes[eventNodeKey]
-  if (eventNodeObj) {
-    const eventMarker = document.createElementNS("http://www.w3.org/2000/svg", "circle")
-    eventMarker.setAttribute("cx", eventNodeObj.x)
-    eventMarker.setAttribute("cy", eventNodeObj.y)
-    eventMarker.setAttribute("r", "12")
-    eventMarker.setAttribute("fill", "#e74c3c")
-    eventMarker.setAttribute("stroke", "white")
-    eventMarker.setAttribute("stroke-width", "2")
-    svg.appendChild(eventMarker)
-  }
-
-  // 人员标记
-  for (let p of personnel.value) {
-    const posNode = nodes[p.nodeKey]
-    if (posNode) {
-      const personMark = document.createElementNS("http://www.w3.org/2000/svg", "circle")
-      personMark.setAttribute("cx", posNode.x)
-      personMark.setAttribute("cy", posNode.y)
-      personMark.setAttribute("r", "8")
-      personMark.setAttribute("fill", p.role === "指挥人员" ? "#f39c12" : "#3498db")
-      personMark.setAttribute("stroke", "white")
-      svg.appendChild(personMark)
-
-      const label = document.createElementNS("http://www.w3.org/2000/svg", "text")
-      label.setAttribute("x", posNode.x + 9)
-      label.setAttribute("y", posNode.y - 3)
-      label.setAttribute("fill", "#f1c40f")
-      label.setAttribute("font-size", "7")
-      label.textContent = p.name.charAt(0)
-      svg.appendChild(label)
-    }
+// 路线数据
+const routeData = reactive({
+  '化工园区': {
+    shortest: { distance: '3.2', pathDisplay: '交警驻点 → 化工园区', estimateTime: '5', viaArea: '城东主干道' },
+    fastest: { time: '4.8', pathDisplay: '消防站A → 应急指挥中心 → 化工园区', savePercent: '15' }
+  },
+  '城南枢纽': {
+    shortest: { distance: '2.8', pathDisplay: '医疗点B → 城南枢纽', estimateTime: '4', viaArea: '城南大道' },
+    fastest: { time: '3.7', pathDisplay: '应急指挥中心 → 交警驻点 → 城南枢纽', savePercent: '12' }
+  },
+  '城西隧道': {
+    shortest: { distance: '4.2', pathDisplay: '消防站A → 城西隧道', estimateTime: '7', viaArea: '城西快速路' },
+    fastest: { time: '5.5', pathDisplay: '应急指挥中心 → 北环快速 → 城西隧道', savePercent: '20' }
+  },
+  '滨海大道': {
+    shortest: { distance: '5.8', pathDisplay: '医疗点B → 滨海大道', estimateTime: '6', viaArea: '滨海路' },
+    fastest: { time: '4.2', pathDisplay: '北环快速 → 东侧入口 → 滨海大道', savePercent: '18' }
   }
+})
 
-  // 绘制路径
-  if (activePath && activePath.length > 1) {
-    for (let i = 0; i < activePath.length - 1; i++) {
-      const fromNode = nodes[activePath[i]]
-      const toNode = nodes[activePath[i + 1]]
-      if (fromNode && toNode) {
-        const line = document.createElementNS("http://www.w3.org/2000/svg", "line")
-        line.setAttribute("x1", fromNode.x)
-        line.setAttribute("y1", fromNode.y)
-        line.setAttribute("x2", toNode.x)
-        line.setAttribute("y2", toNode.y)
-        line.setAttribute("stroke", "#e07c3c")
-        line.setAttribute("stroke-width", "5")
-        line.setAttribute("stroke-linecap", "round")
-        line.setAttribute("stroke-dasharray", "8 4")
-        svg.appendChild(line)
-      }
-    }
-  }
-}
+const displayRoutes = computed(() => {
+  const data = routeData[selectedEventLocation.value]
+  return data || { shortest: null, fastest: null }
+})
 
-// ==================== 选择路线 ====================
-const selectRoute = (route) => {
-  activeRouteType.value = route.type
-  const path = route.path
-  if (path && path.length > 1) {
-    drawGIS(selectedEventLocation.value, path)
-  }
+const selectRoute = (type) => {
+  activeRouteType.value = type
 }
 
-// ==================== 刷新所有数据 ====================
 const refreshAll = () => {
-  const routes = computeOptimalRoutes(selectedEventLocation.value)
-  currentRoutes.value = {
-    shortest: routes.shortest,
-    fastest: routes.fastest,
-    personName: routes.personName
-  }
-
-  const defaultPath = routes.shortest?.path || null
-  drawGIS(selectedEventLocation.value, defaultPath)
-  activeRouteType.value = "shortest"
+  activeRouteType.value = 'shortest'
 }
 
-// ==================== 社会公众绕行方案 ====================
-const showPublicRoute = () => {
-  alert("🚸 社会公众绕行方案:\n\n根据突发事件位置,推荐绕行路线:\n• 避开化工园区及城南枢纽路段\n• 建议走东侧入口 → 北环快速 → 滨海大道方向\n• 可通过交通广播及导航APP获取实时绕行指引\n• 请关注官方发布的最新路况信息")
-}
-
-// ==================== 生命周期 ====================
 onMounted(() => {
   refreshAll()
 })
 </script>
 
 <style scoped>
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #ffffff;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
-
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 16px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-  border-bottom: 3px solid #e07c3c;
-}
-
-.logo h2 {
-  color: white;
-  font-weight: 600;
-  font-size: 1.4rem;
-}
-
-.logo p {
-  color: #bdd8e8;
-  font-size: 0.7rem;
-}
-
-.badge-gis {
-  background: #e07c3c;
-  padding: 6px 18px;
-  border-radius: 30px;
-  color: white;
-  font-weight: 500;
-  font-size: 0.8rem;
+.app-container {
+  padding: 16px;
+  background: #f0f2f5;
+  min-height: calc(100vh - 84px);
 }
 
-/* 主体布局 */
-.main-layout {
+.route-layout {
   display: flex;
-  flex-wrap: wrap;
+  gap: 16px;
+  height: calc(100vh - 120px);
 }
 
-.info-panel {
-  width: 360px;
-  background: #f9fbfd;
-  border-right: 1px solid #e2edf2;
-  padding: 20px 18px;
+.left-panel {
+  width: 30%;
+  min-width: 320px;
   display: flex;
   flex-direction: column;
-  gap: 20px;
+  gap: 12px;
+  overflow-y: auto;
 }
 
-.map-panel {
+.right-panel {
   flex: 1;
-  background: #1a2f3c;
-  padding: 16px;
-  position: relative;
-  min-height: 600px;
+  min-width: 0;
 }
 
-/* 卡片样式 */
-.card {
-  background: white;
-  border-radius: 20px;
-  border: 1px solid #e6edf3;
-  overflow: hidden;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
+.panel-card {
+  border-radius: 8px;
 }
 
-.card-header {
-  background: #f9fafc;
-  padding: 14px 18px;
-  border-bottom: 1px solid #eef2f6;
-  font-weight: 700;
-  font-size: 0.9rem;
-  color: #1d5a78;
+.card-title {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-weight: 600;
+  font-size: 14px;
+  color: #303133;
 }
 
-.card-body {
-  padding: 16px 18px;
+.card-title .el-icon {
+  color: #409EFF;
+  font-size: 16px;
 }
 
-.form-group {
-  margin-bottom: 16px;
+/* 事件定位 */
+.event-location {
+  margin-bottom: 12px;
 }
 
-.form-label {
-  font-size: 0.75rem;
-  font-weight: 600;
-  margin-bottom: 6px;
-  display: block;
-  color: #2c5a72;
+.loc-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 6px 0;
+  font-size: 13px;
 }
 
-.form-select {
-  width: 100%;
-  padding: 8px 12px;
-  border-radius: 12px;
-  border: 1px solid #cbdde6;
-  font-size: 0.8rem;
-  background: white;
+.loc-label {
+  color: #909399;
+  font-size: 12px;
 }
 
-.btn-primary {
-  background: #e07c3c;
-  border: none;
-  color: white;
-  padding: 10px 16px;
-  border-radius: 30px;
+.loc-value {
+  font-family: monospace;
+  color: #303133;
   font-weight: 600;
-  font-size: 0.8rem;
-  cursor: pointer;
-  width: 100%;
-  transition: 0.2s;
-}
-
-.btn-primary:hover {
-  background: #c56329;
-}
-
-.btn-outline {
-  background: white;
-  border: 1px solid #e07c3c;
-  color: #e07c3c;
-  padding: 6px 14px;
-  border-radius: 30px;
-  font-size: 0.7rem;
-  cursor: pointer;
 }
 
+/* 人员列表 */
 .person-list {
   max-height: 180px;
   overflow-y: auto;
@@ -557,98 +325,239 @@ onMounted(() => {
 .person-item {
   display: flex;
   justify-content: space-between;
+  align-items: center;
   padding: 8px 0;
-  border-bottom: 1px solid #edf2f6;
-  font-size: 0.75rem;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.person-item:last-child {
+  border-bottom: none;
+}
+
+.person-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.person-name {
+  font-weight: 600;
+  font-size: 13px;
+  color: #303133;
 }
 
-.role-tag {
-  background: #eef2fa;
-  padding: 2px 8px;
-  border-radius: 20px;
-  font-size: 0.65rem;
+.person-tip {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
+  text-align: center;
 }
 
-.route-result {
-  margin-top: 12px;
+/* 路线推荐 */
+.route-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
 }
 
 .route-item {
-  background: #fef7f0;
-  border-radius: 14px;
   padding: 12px;
-  margin-bottom: 10px;
-  border-left: 3px solid #e07c3c;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
   cursor: pointer;
-  transition: 0.1s;
+  transition: all 0.2s;
+  background: #fafafa;
+}
+
+.route-item:hover {
+  border-color: #409EFF;
 }
 
 .route-item.active {
-  background: #f0e8df;
-  border-left-color: #2e9c8c;
+  border-color: #409EFF;
+  border-left: 3px solid #409EFF;
+  background: #ecf5ff;
 }
 
-.route-name {
-  font-weight: 700;
-  font-size: 0.85rem;
+.route-head {
   display: flex;
   justify-content: space-between;
+  align-items: center;
+  margin-bottom: 6px;
 }
 
-.route-detail {
-  font-size: 0.7rem;
-  color: #6f8ea8;
-  margin-top: 6px;
+.route-name {
+  font-weight: 600;
+  font-size: 14px;
+  color: #303133;
 }
 
-.route-meta {
-  display: flex;
-  gap: 12px;
-  margin-top: 8px;
-  font-size: 0.7rem;
+.route-path {
+  font-size: 12px;
+  color: #606266;
+  margin-bottom: 4px;
 }
 
-.badge-dist {
-  background: #e9ecef;
-  padding: 2px 8px;
-  border-radius: 20px;
+.route-meta {
+  font-size: 12px;
+  color: #909399;
 }
 
-/* GIS 地图容器 */
-.gis-container {
-  background: #1e3a4d;
-  border-radius: 20px;
-  height: 100%;
-  min-height: 580px;
+/* 地图区域 */
+.map-container {
   position: relative;
+  width: 100%;
+  height: 100%;
+  background: #1a2a3a;
+  border-radius: 8px;
   overflow: hidden;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+  min-height: 500px;
 }
 
-.map-svg {
-  width: 100%;
-  height: 100%;
-  background: linear-gradient(135deg, #234a5e 0%, #1a3a4a 100%);
+/* 地名标签 */
+.map-label {
+  position: absolute;
+  color: rgba(255, 255, 255, 0.5);
+  font-size: 11px;
+  pointer-events: none;
+  transform: translate(-50%, -50%);
+  white-space: nowrap;
+  letter-spacing: 1px;
 }
 
+/* 标记点 */
+.map-marker {
+  position: absolute;
+  transform: translate(-50%, -50%);
+  z-index: 10;
+}
+
+.marker-dot {
+  width: 14px;
+  height: 14px;
+  border-radius: 50%;
+  border: 2px solid #fff;
+  position: relative;
+  z-index: 2;
+}
+
+.map-marker.event .marker-dot {
+  background: #e74c3c;
+  width: 18px;
+  height: 18px;
+  box-shadow: 0 0 12px rgba(231, 76, 60, 0.6);
+}
+
+.map-marker.person .marker-dot {
+  background: #3498db;
+  box-shadow: 0 0 8px rgba(52, 152, 219, 0.5);
+}
+
+.map-marker.route .marker-dot {
+  background: #e67e22;
+  box-shadow: 0 0 8px rgba(230, 126, 34, 0.5);
+}
+
+.marker-pulse {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 36px;
+  height: 36px;
+  margin-left: -18px;
+  margin-top: -18px;
+  border-radius: 50%;
+  background: rgba(231, 76, 60, 0.2);
+  animation: pulse 2s ease-out infinite;
+  z-index: 1;
+}
+
+@keyframes pulse {
+  0% { transform: scale(0.5); opacity: 1; }
+  100% { transform: scale(1.5); opacity: 0; }
+}
+
+.marker-label {
+  position: absolute;
+  left: 50%;
+  top: 100%;
+  transform: translateX(-50%);
+  margin-top: 4px;
+  font-size: 10px;
+  color: rgba(255, 255, 255, 0.85);
+  white-space: nowrap;
+  background: rgba(0, 0, 0, 0.5);
+  padding: 1px 6px;
+  border-radius: 3px;
+}
+
+/* 路线连接 */
+.map-line {
+  position: absolute;
+  height: 2px;
+  transform-origin: left center;
+  pointer-events: none;
+}
+
+.map-line.road {
+  background: rgba(255, 255, 255, 0.15);
+  border: none;
+}
+
+.map-line.active-route {
+  height: 3px;
+  background: #e67e22;
+  box-shadow: 0 0 6px rgba(230, 126, 34, 0.6);
+  z-index: 5;
+}
+
+/* 图例 */
 .map-legend {
   position: absolute;
   bottom: 16px;
   right: 16px;
-  background: rgba(0, 0, 0, 0.7);
-  padding: 8px 14px;
-  border-radius: 20px;
-  color: white;
-  font-size: 0.7rem;
+  background: rgba(0, 0, 0, 0.75);
+  padding: 10px 16px;
+  border-radius: 8px;
+  color: #fff;
+  font-size: 12px;
+  z-index: 20;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
   backdrop-filter: blur(4px);
-  z-index: 10;
 }
 
-.legend-color {
-  display: inline-block;
-  width: 12px;
-  height: 12px;
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.legend-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.legend-dot.event {
+  background: #e74c3c;
+}
+
+.legend-dot.person {
+  background: #3498db;
+}
+
+.legend-dot.route {
+  background: #e67e22;
+}
+
+.legend-line {
+  width: 18px;
+  height: 3px;
+  background: #e67e22;
   border-radius: 2px;
-  margin-right: 4px;
+  flex-shrink: 0;
 }
-</style>
+</style>

+ 775 - 0
src/views/subSystem/dispatch/disDecisionDep/visualization.vue

@@ -0,0 +1,775 @@
+<!--可视化调度-->
+<template>
+  <div class="app-container">
+    <div class="dispatch-layout">
+      <!-- 左列 -->
+      <div class="left-col">
+        <!-- 应急事件列表 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Warning /></el-icon>
+              <span>应急事件列表</span>
+              <el-button type="primary" size="small" style="margin-left:auto" @click="refreshEvents">
+                <el-icon><Refresh /></el-icon> 刷新事件列表
+              </el-button>
+            </div>
+          </template>
+          <div class="event-list">
+            <div
+              v-for="event in events"
+              :key="event.id"
+              class="event-item"
+              :class="{ selected: selectedEventId === event.id }"
+              @click="selectEvent(event.id)"
+            >
+              <div class="event-head">
+                <span class="event-name">{{ event.name }}</span>
+                <el-tag :type="levelTagType(event.level)" size="small">{{ event.level }}</el-tag>
+              </div>
+              <div class="event-info">
+                <span>{{ event.location }}</span>
+                <el-tag :type="event.status === '处置中' ? 'warning' : event.status === '紧急响应' ? 'danger' : 'info'" size="small" effect="plain">
+                  {{ event.status }}
+                </el-tag>
+              </div>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 手动发送调度信息 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Promotion /></el-icon>
+              <span>手动发送调度信息</span>
+            </div>
+          </template>
+          <div v-if="selectedEvent" class="selected-event-box">
+            <el-descriptions :column="1" border size="small">
+              <el-descriptions-item label="事件名称">{{ selectedEvent.name }}</el-descriptions-item>
+              <el-descriptions-item label="事件等级">{{ selectedEvent.level }}</el-descriptions-item>
+              <el-descriptions-item label="当前状态">{{ selectedEvent.status }}</el-descriptions-item>
+            </el-descriptions>
+          </div>
+          <div v-else class="no-event-tip">请先从上方列表选择一个应急事件</div>
+          <div style="margin-top:12px">
+            <el-form label-position="top" size="default">
+              <el-form-item label="调度半径(km)">
+                <el-input-number v-model="dispatchRadius" :min="1" :max="50" :step="1" style="width:100%" @change="updateStatistics" />
+              </el-form-item>
+            </el-form>
+            <div class="stat-section">
+              <div class="stat-label">统计范围内在线资源</div>
+              <div class="stat-row">
+                <div class="stat-item">
+                  <div class="stat-value">{{ stats.vehicleCount }}</div>
+                  <div class="stat-name">应急车辆</div>
+                </div>
+                <div class="stat-item">
+                  <div class="stat-value">{{ stats.personnelCount }}</div>
+                  <div class="stat-name">应急人员</div>
+                </div>
+              </div>
+            </div>
+            <el-button type="danger" style="width:100%;margin-top:12px" @click="sendDispatch">
+              <el-icon><Position /></el-icon> 发送调度指令
+            </el-button>
+            <div class="dispatch-tip">推送至手机APP及车载终端</div>
+          </div>
+        </el-card>
+      </div>
+
+      <!-- 中列 -->
+      <div class="middle-col">
+        <!-- 在线应急资源 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><Monitor /></el-icon>
+              <span>在线应急资源</span>
+              <el-tag type="info" size="small" style="margin-left:8px">半径 {{ dispatchRadius }}km</el-tag>
+            </div>
+          </template>
+          <div class="resource-list">
+            <div v-for="res in resourcesInRange" :key="res.id" class="resource-item">
+              <div class="res-info">
+                <span class="online-indicator"></span>
+                <span class="res-name">{{ res.name }}</span>
+              </div>
+              <el-tag size="small" :type="res.category === '车辆' ? 'primary' : res.category === '值守人员' ? '' : 'warning'">
+                {{ res.category }}
+              </el-tag>
+            </div>
+            <div v-if="resourcesInRange.length === 0" class="empty-tip">半径内暂无在线资源</div>
+          </div>
+        </el-card>
+
+        <!-- 应急处置临时群组 -->
+        <el-card shadow="never" class="panel-card">
+          <template #header>
+            <div class="card-title">
+              <el-icon><UserFilled /></el-icon>
+              <span>应急处置临时群组</span>
+              <el-button type="primary" size="small" style="margin-left:auto" @click="showCreateGroupDialog">新建群组</el-button>
+            </div>
+          </template>
+          <div class="group-list">
+            <div
+              v-for="group in tempGroups"
+              :key="group.id"
+              class="group-item"
+              :class="{ active: currentGroupId === group.id }"
+              @click="selectGroup(group.id)"
+            >
+              <div class="group-head">
+                <span class="group-name">{{ group.name }}</span>
+                <el-button type="danger" size="small" text @click.stop="deleteGroup(group.id)">删除</el-button>
+              </div>
+              <div class="member-tags">
+                <el-tag
+                  v-for="memberId in group.members"
+                  :key="memberId"
+                  closable
+                  size="small"
+                  style="margin:2px"
+                  @close="removeMember(group.id, memberId)"
+                >
+                  {{ getMemberName(memberId) }}
+                </el-tag>
+                <el-button size="small" text type="primary" @click.stop="openAddMemberDialog(group.id)">+ 添加</el-button>
+              </div>
+            </div>
+            <div v-if="tempGroups.length === 0" class="empty-tip">暂无临时群组,点击新建</div>
+          </div>
+          <el-button style="width:100%;margin-top:12px" @click="addDeviceToCurrentGroup">
+            添加设备到当前群组
+          </el-button>
+        </el-card>
+      </div>
+
+      <!-- 右列 -->
+      <div class="right-col">
+        <!-- 监控画面 -->
+        <div class="monitor-grid">
+          <div v-for="(cam, idx) in cameras" :key="idx" class="monitor-screen" @click="viewCamera(cam)">
+            <div class="monitor-placeholder">
+              <el-icon :size="32" color="#909399"><VideoCamera /></el-icon>
+              <span class="monitor-label">{{ cam.name }}</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 音频对讲 -->
+        <el-card shadow="never" class="panel-card" style="background:#1a2a3a;border-color:#2a3a4a">
+          <template #header>
+            <div class="card-title" style="color:#fff">
+              <el-icon style="color:#67C23A"><Microphone /></el-icon>
+              <span style="color:#fff">音频对讲</span>
+            </div>
+          </template>
+          <div class="call-buttons">
+            <el-button type="success" @click="callSingle">
+              <el-icon><Phone /></el-icon> 一对一呼叫
+            </el-button>
+            <el-button type="warning" @click="callGroup">
+              <el-icon><ChatDotRound /></el-icon> 群组对讲(一对多)
+            </el-button>
+            <el-button type="danger" @click="endCall">
+              <el-icon><SwitchButton /></el-icon> 挂断
+            </el-button>
+          </div>
+          <div class="call-status">
+            {{ callStatusText }}
+          </div>
+          <div class="call-tip">
+            当前选中群组: {{ currentGroupName }},可发起群组对讲
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <!-- 新建群组弹窗 -->
+    <el-dialog v-model="createGroupDialogVisible" title="新建临时群组" width="520px" append-to-body>
+      <el-form label-position="top">
+        <el-form-item label="群组名称">
+          <el-input v-model="newGroupName" placeholder="请输入群组名称" />
+        </el-form-item>
+        <el-form-item label="选择成员">
+          <el-checkbox-group v-model="newGroupMembers">
+            <el-checkbox v-for="res in allResources" :key="res.id" :label="res.id" :value="res.id">
+              {{ res.name }}({{ res.category }})
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="createGroupDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmCreateGroup">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 一对一呼叫弹窗 -->
+    <el-dialog v-model="callSingleDialogVisible" title="一对一呼叫" width="480px" append-to-body>
+      <p style="margin-bottom:12px;color:#606266;font-size:13px">请选择要呼叫的人员:</p>
+      <el-checkbox-group v-model="callTargets">
+        <div v-for="res in personResources" :key="res.id" style="margin-bottom:8px">
+          <el-checkbox :label="res.id" :value="res.id">
+            {{ res.name }}({{ res.category }})
+          </el-checkbox>
+        </div>
+      </el-checkbox-group>
+      <template #footer>
+        <el-button @click="callSingleDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmCallSingle">呼叫</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  Warning, Refresh, Promotion, Position, Monitor, UserFilled,
+  Phone, ChatDotRound, SwitchButton, Microphone, VideoCamera
+} from '@element-plus/icons-vue'
+
+// ============ 应急事件 ============
+const events = ref([
+  { id: 1, name: '化工园区氯气泄漏', level: '重大', location: '化工园区', lat: 31.2, lng: 118.5, status: '处置中' },
+  { id: 2, name: '城南枢纽交通事故', level: '较大', location: '城南枢纽', lat: 31.15, lng: 118.45, status: '待调度' },
+  { id: 3, name: '城西隧道火灾', level: '特别重大', location: '城西隧道', lat: 31.22, lng: 118.38, status: '紧急响应' }
+])
+
+const selectedEventId = ref(1)
+const selectedEvent = computed(() => events.value.find(e => e.id === selectedEventId.value))
+
+const levelTagType = (level) => {
+  const map = { '特别重大': 'danger', '重大': 'warning', '较大': '', '一般': 'info' }
+  return map[level] || 'info'
+}
+
+const selectEvent = (id) => {
+  selectedEventId.value = id
+  updateStatistics()
+}
+
+const refreshEvents = () => {
+  ElMessage.success('事件列表已刷新')
+}
+
+// ============ 调度参数 ============
+const dispatchRadius = ref(5)
+const stats = ref({ vehicleCount: 0, personnelCount: 0 })
+
+// ============ 应急资源 ============
+const vehicles = ref([
+  { id: 'v1', name: '消防车-01', lat: 31.19, lng: 118.48, online: true, category: '车辆' },
+  { id: 'v2', name: '救护车-03', lat: 31.21, lng: 118.52, online: true, category: '车辆' },
+  { id: 'v3', name: '抢险车-05', lat: 31.17, lng: 118.44, online: true, category: '车辆' }
+])
+
+const persons = ref([
+  { id: 'p1', name: '王敏', lat: 31.2, lng: 118.5, online: true, category: '值守人员' },
+  { id: 'p2', name: '陈方', lat: 31.19, lng: 118.49, online: true, category: '指挥人员' },
+  { id: 'p3', name: '刘洋', lat: 31.23, lng: 118.47, online: true, category: '值守人员' },
+  { id: 'p4', name: '赵磊', lat: 31.18, lng: 118.46, online: true, category: '指挥人员' }
+])
+
+const allResources = computed(() => [...vehicles.value, ...persons.value])
+const personResources = computed(() => persons.value)
+
+// 计算距离
+const getDistance = (lat1, lng1, lat2, lng2) => {
+  const R = 6371
+  const dLat = (lat2 - lat1) * Math.PI / 180
+  const dLng = (lng2 - lng1) * Math.PI / 180
+  const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2
+  return R * 2 * Math.asin(Math.sqrt(a))
+}
+
+// 半径内资源
+const resourcesInRange = computed(() => {
+  if (!selectedEvent.value) return []
+  const event = selectedEvent.value
+  const radius = dispatchRadius.value
+  const vInRange = vehicles.value.filter(v => v.online && getDistance(event.lat, event.lng, v.lat, v.lng) <= radius)
+  const pInRange = persons.value.filter(p => p.online && getDistance(event.lat, event.lng, p.lat, p.lng) <= radius)
+  return [...vInRange, ...pInRange]
+})
+
+// 更新统计
+const updateStatistics = () => {
+  if (!selectedEvent.value) return
+  const event = selectedEvent.value
+  const radius = dispatchRadius.value
+  stats.value.vehicleCount = vehicles.value.filter(v => v.online && getDistance(event.lat, event.lng, v.lat, v.lng) <= radius).length
+  stats.value.personnelCount = persons.value.filter(p => p.online && getDistance(event.lat, event.lng, p.lat, p.lng) <= radius).length
+}
+
+// 发送调度
+const sendDispatch = () => {
+  if (!selectedEvent.value) {
+    ElMessage.warning('请先选择应急事件')
+    return
+  }
+  ElMessageBox.confirm(
+    `事件: ${selectedEvent.value.name}\n调度半径: ${dispatchRadius.value}km\n覆盖车辆: ${stats.value.vehicleCount}辆, 人员: ${stats.value.personnelCount}人\n\n确认发送调度指令?`,
+    '调度指令确认',
+    { confirmButtonText: '确认发送', cancelButtonText: '取消', type: 'warning' }
+  ).then(() => {
+    ElMessage.success('调度指令已发送!推送至手机APP、车载终端及应急指挥箱。')
+  }).catch(() => {})
+}
+
+// ============ 临时群组 ============
+const tempGroups = ref([
+  { id: 'g1', name: '化工园区应急组', members: ['v1', 'p1'] },
+  { id: 'g2', name: '医疗救护组', members: ['v2', 'p4'] }
+])
+
+const currentGroupId = ref(null)
+const currentGroupName = computed(() => {
+  const g = tempGroups.value.find(g => g.id === currentGroupId.value)
+  return g ? g.name : '未选择'
+})
+
+const createGroupDialogVisible = ref(false)
+const newGroupName = ref('')
+const newGroupMembers = ref([])
+
+const selectGroup = (groupId) => {
+  currentGroupId.value = groupId
+}
+
+const getMemberName = (memberId) => {
+  const res = allResources.value.find(r => r.id === memberId)
+  return res ? res.name : memberId
+}
+
+const showCreateGroupDialog = () => {
+  newGroupName.value = ''
+  newGroupMembers.value = []
+  createGroupDialogVisible.value = true
+}
+
+const confirmCreateGroup = () => {
+  if (!newGroupName.value.trim()) {
+    ElMessage.warning('请输入群组名称')
+    return
+  }
+  const newId = 'g' + Date.now()
+  tempGroups.value.push({ id: newId, name: newGroupName.value, members: [...newGroupMembers.value] })
+  createGroupDialogVisible.value = false
+  ElMessage.success(`群组「${newGroupName.value}」创建成功`)
+}
+
+const deleteGroup = (groupId) => {
+  ElMessageBox.confirm('确定删除该临时群组?', '提示', { type: 'warning' }).then(() => {
+    tempGroups.value = tempGroups.value.filter(g => g.id !== groupId)
+    if (currentGroupId.value === groupId) currentGroupId.value = null
+    ElMessage.success('群组已删除')
+  }).catch(() => {})
+}
+
+const removeMember = (groupId, memberId) => {
+  const group = tempGroups.value.find(g => g.id === groupId)
+  if (group) {
+    group.members = group.members.filter(m => m !== memberId)
+  }
+}
+
+const addMemberTargetGroupId = ref(null)
+
+const openAddMemberDialog = (groupId) => {
+  addMemberTargetGroupId.value = groupId
+  newGroupMembers.value = []
+  // 复用新建群组弹窗的部分逻辑,但这里简化处理
+  // 找到群组已有成员,排除后可选
+  newGroupName.value = '' // 不使用名称
+  createGroupDialogVisible.value = true
+}
+
+const addDeviceToCurrentGroup = () => {
+  if (!currentGroupId.value) {
+    ElMessage.warning('请先点击选中一个临时群组')
+    return
+  }
+  openAddMemberDialog(currentGroupId.value)
+}
+
+// 重写confirmCreateGroup以支持添加成员模式
+const confirmCreateGroupOriginal = confirmCreateGroup
+
+// ============ 监控画面 ============
+const cameras = ref([
+  { name: '监控一', location: '化工园区' },
+  { name: '监控二', location: '城南救援点' },
+  { name: '监控三', location: '消防1组' },
+  { name: '监控四', location: '城东路口' }
+])
+
+const viewCamera = (cam) => {
+  ElMessage.info(`调取 ${cam.name}(${cam.location})实时监控画面`)
+}
+
+// ============ 音频对讲 ============
+const callStatusText = ref('未建立对讲 | 选择设备或群组')
+const callSingleDialogVisible = ref(false)
+const callTargets = ref([])
+
+const callSingle = () => {
+  callTargets.value = []
+  callSingleDialogVisible.value = true
+}
+
+const confirmCallSingle = () => {
+  if (callTargets.value.length === 0) {
+    ElMessage.warning('请选择要呼叫的人员')
+    return
+  }
+  const names = callTargets.value.map(id => getMemberName(id)).join('、')
+  callStatusText.value = `正在呼叫 ${names}...`
+  callSingleDialogVisible.value = false
+  setTimeout(() => {
+    callStatusText.value = `已与 ${names} 建立音频对讲`
+    ElMessage.success(`已与 ${names} 建立音频对讲`)
+  }, 1000)
+}
+
+const callGroup = () => {
+  if (!currentGroupId.value) {
+    ElMessage.warning('请先选中一个临时群组')
+    return
+  }
+  const group = tempGroups.value.find(g => g.id === currentGroupId.value)
+  if (!group || group.members.length === 0) {
+    ElMessage.warning('群组无成员')
+    return
+  }
+  callStatusText.value = `发起群组对讲 → ${group.name},设备数: ${group.members.length}`
+  setTimeout(() => {
+    callStatusText.value = `群组对讲进行中: ${group.name} 已接通`
+    ElMessage.success(`群组「${group.name}」对讲已接通`)
+  }, 800)
+}
+
+const endCall = () => {
+  callStatusText.value = '对讲已挂断 | 选择设备或群组'
+  ElMessage.info('对讲已挂断')
+}
+
+// ============ 监听 ============
+watch(dispatchRadius, () => { updateStatistics() })
+
+onMounted(() => {
+  updateStatistics()
+})
+</script>
+
+<style scoped>
+.app-container {
+  padding: 16px;
+  background: #f0f2f5;
+  min-height: calc(100vh - 84px);
+}
+
+.dispatch-layout {
+  display: flex;
+  gap: 12px;
+  height: calc(100vh - 120px);
+}
+
+.left-col {
+  width: 25%;
+  min-width: 280px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  overflow-y: auto;
+}
+
+.middle-col {
+  width: 35%;
+  min-width: 300px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  overflow-y: auto;
+}
+
+.right-col {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  min-width: 0;
+}
+
+.panel-card {
+  border-radius: 8px;
+}
+
+.card-title {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-weight: 600;
+  font-size: 14px;
+  color: #303133;
+}
+
+.card-title .el-icon {
+  color: #409EFF;
+  font-size: 16px;
+}
+
+/* 事件列表 */
+.event-list {
+  max-height: 260px;
+  overflow-y: auto;
+}
+
+.event-item {
+  padding: 10px 12px;
+  margin-bottom: 8px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.event-item:hover {
+  border-color: #409EFF;
+}
+
+.event-item.selected {
+  border-color: #409EFF;
+  background: #ecf5ff;
+  border-left: 3px solid #409EFF;
+}
+
+.event-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 4px;
+}
+
+.event-name {
+  font-weight: 600;
+  font-size: 13px;
+  color: #303133;
+}
+
+.event-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 12px;
+  color: #909399;
+}
+
+/* 调度面板 */
+.selected-event-box {
+  margin-bottom: 8px;
+}
+
+.no-event-tip {
+  font-size: 13px;
+  color: #909399;
+  text-align: center;
+  padding: 12px 0;
+}
+
+.stat-section {
+  margin-top: 8px;
+}
+
+.stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 8px;
+}
+
+.stat-row {
+  display: flex;
+  gap: 16px;
+}
+
+.stat-item {
+  flex: 1;
+  text-align: center;
+  padding: 10px;
+  background: #f5f7fa;
+  border-radius: 6px;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: 700;
+  color: #409EFF;
+}
+
+.stat-name {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.dispatch-tip {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
+  text-align: center;
+}
+
+/* 资源列表 */
+.resource-list {
+  max-height: 200px;
+  overflow-y: auto;
+}
+
+.resource-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  border-bottom: 1px solid #f0f0f0;
+  font-size: 13px;
+}
+
+.resource-item:last-child {
+  border-bottom: none;
+}
+
+.res-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.online-indicator {
+  width: 8px;
+  height: 8px;
+  background: #67C23A;
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.res-name {
+  color: #303133;
+}
+
+.empty-tip {
+  text-align: center;
+  padding: 16px;
+  color: #909399;
+  font-size: 13px;
+}
+
+/* 群组列表 */
+.group-list {
+  max-height: 260px;
+  overflow-y: auto;
+}
+
+.group-item {
+  padding: 10px;
+  margin-bottom: 8px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.group-item:hover {
+  border-color: #E6A23C;
+}
+
+.group-item.active {
+  border-color: #E6A23C;
+  background: #fdf6ec;
+  border-left: 3px solid #E6A23C;
+}
+
+.group-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 6px;
+}
+
+.group-name {
+  font-weight: 600;
+  font-size: 13px;
+  color: #303133;
+}
+
+.member-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  align-items: center;
+}
+
+/* 监控画面 */
+.monitor-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  flex: 1;
+  min-height: 280px;
+}
+
+.monitor-screen {
+  background: #1a2a3a;
+  border-radius: 8px;
+  overflow: hidden;
+  cursor: pointer;
+  border: 1px solid #2a3a4a;
+  transition: border-color 0.2s;
+}
+
+.monitor-screen:hover {
+  border-color: #409EFF;
+}
+
+.monitor-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  min-height: 120px;
+  gap: 8px;
+}
+
+.monitor-label {
+  font-size: 12px;
+  color: #909399;
+}
+
+/* 音频对讲 */
+.call-buttons {
+  display: flex;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+
+.call-status {
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 6px;
+  padding: 8px 12px;
+  margin-top: 12px;
+  font-size: 13px;
+  color: #fff;
+}
+
+.call-tip {
+  margin-top: 8px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.6);
+}
+</style>

+ 0 - 823
src/views/subSystem/dispatch/disDecisionDep/可视化调度.vue

@@ -1,823 +0,0 @@
-<template>
-  <div class="prototype-container">
-    <!-- 头部 -->
-    <div class="header">
-      <div class="logo">
-        <h2>🎙️ 应急指挥调度中心</h2>
-        <p>智能调度 | 临时群组 | 音视频对讲 | 实时监控</p>
-      </div>
-      <div class="badge-emergency">🟢 一级应急响应 · 指挥调度中</div>
-    </div>
-
-    <div class="main-layout">
-      <!-- 左侧:应急事件 + 调度框 -->
-      <div class="left-panel">
-        <div class="card">
-          <div class="card-header">📋 应急事件列表</div>
-          <div class="card-body">
-            <div class="event-list">
-              <div
-                  v-for="event in events"
-                  :key="event.id"
-                  class="event-item"
-                  :class="{ selected: selectedEventId === event.id }"
-                  @click="selectEvent(event.id)"
-              >
-                <div class="event-title">🔥 {{ event.name }} ({{ event.level }})</div>
-                <div class="event-loc">📍 {{ event.location }} · {{ event.status }}</div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 调度面板 -->
-        <div class="card">
-          <div class="card-header">📤 手动发送调度信息</div>
-          <div class="card-body">
-            <div class="selected-event-info">
-              ✅ 已选事件: {{ selectedEvent?.name || '未选择' }}
-              <span v-if="selectedEvent">({{ selectedEvent.location }})</span>
-            </div>
-            <div class="dispatch-panel">
-              <div class="form-group">
-                <label class="form-label">📍 调度半径 (km)</label>
-                <input
-                    type="number"
-                    v-model.number="dispatchRadius"
-                    class="form-input"
-                    min="1"
-                    max="20"
-                    @input="updateStatistics"
-                >
-              </div>
-              <div class="form-group">
-                <label class="form-label">📊 统计范围内在线资源</label>
-                <div class="stat-numbers">
-                  <span>🚑 应急车辆: <strong>{{ stats.vehicleCount }}</strong></span>
-                  <span>👥 应急人员: <strong>{{ stats.personnelCount }}</strong></span>
-                </div>
-              </div>
-              <button @click="sendDispatch" class="btn-primary" style="width:100%;">📨 发送调度指令</button>
-              <div class="dispatch-note">* 推送至手机APP及车载终端</div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 中间:在线资源 + 临时群组管理 -->
-      <div class="middle-panel">
-        <div class="card">
-          <div class="card-header">
-            🚨 在线应急资源
-            <span class="region-hint">半径 {{ dispatchRadius }}km 范围内</span>
-          </div>
-          <div class="card-body">
-            <div class="resource-list">
-              <div v-if="resourcesInRange.length === 0" class="empty-resource">
-                半径内无在线资源
-              </div>
-              <div v-for="res in resourcesInRange" :key="res.id" class="resource-item">
-                <span><span class="online-dot"></span> {{ res.name }} ({{ res.category }})</span>
-                <span class="resource-type">{{ res.type || res.category }}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <div class="card">
-          <div class="card-header">
-            👥 应急处置临时群组
-            <button @click="createGroup" class="btn-sm">+ 新建群组</button>
-          </div>
-          <div class="card-body">
-            <div class="group-list">
-              <div v-if="tempGroups.length === 0" class="empty-group">
-                暂无临时群组,点击新建
-              </div>
-              <div
-                  v-for="group in tempGroups"
-                  :key="group.id"
-                  class="group-item"
-                  :class="{ active: currentGroupId === group.id }"
-                  @click="selectGroup(group.id)"
-              >
-                <div class="group-header">
-                  <strong>👥 {{ group.name }}</strong>
-                  <button @click.stop="deleteGroup(group.id)" class="remove-group">🗑️</button>
-                </div>
-                <div class="device-list">
-                  <span
-                      v-for="deviceId in group.devices"
-                      :key="deviceId"
-                      class="device-tag"
-                  >
-                    📱 {{ getDeviceName(deviceId) }}
-                    <span @click.stop="removeDeviceFromGroup(group.id, deviceId)" class="remove">✖</span>
-                  </span>
-                  <button @click.stop="addDeviceToGroup(group.id)" class="btn-sm-add">+ 添加设备</button>
-                </div>
-              </div>
-            </div>
-            <button @click="addDeviceToCurrentGroup" class="btn-outline" style="width:100%; margin-top:12px;">
-              ➕ 添加设备到当前群组
-            </button>
-          </div>
-        </div>
-      </div>
-
-      <!-- 右侧:监控大屏 + 音视频对讲 -->
-      <div class="right-panel">
-        <div class="monitor-grid">
-          <div
-              v-for="(camera, idx) in cameras"
-              :key="idx"
-              class="monitor-screen"
-              @click="viewCamera(camera.name)"
-          >
-            <div class="monitor-header">
-              <span>📹 {{ camera.name }}</span>
-              <span>{{ camera.location }}</span>
-            </div>
-            <div class="monitor-video">
-              🎥 实时画面流<br>点击查看
-            </div>
-          </div>
-        </div>
-
-        <div class="call-controls">
-          <div class="call-header">🔊 音视频对讲 · 指挥调度</div>
-          <div class="call-buttons">
-            <button @click="callSingle" class="btn-call">🎤 一对一呼叫</button>
-            <button @click="callGroup" class="btn-call">👥 群组对讲 (一对多)</button>
-            <button @click="endCall" class="btn-call btn-call-end">⏹️ 挂断</button>
-          </div>
-          <div class="call-status">
-            {{ callStatusText }}
-          </div>
-          <div class="call-note">
-            💡 支持应急指挥箱、手机终端、对讲机等设备构建临时群组
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted, watch } from 'vue'
-
-// ==================== 应急事件 ====================
-const events = ref([
-  { id: 1, name: "化工园区氯气泄漏", level: "重大", location: "化工园区", lat: 31.2, lng: 118.5, status: "处置中" },
-  { id: 2, name: "城南枢纽交通事故", level: "较大", location: "城南枢纽", lat: 31.15, lng: 118.45, status: "待调度" },
-  { id: 3, name: "城西隧道火灾", level: "特别重大", location: "城西隧道", lat: 31.22, lng: 118.38, status: "紧急响应" }
-])
-
-const selectedEventId = ref(1)
-const selectedEvent = computed(() => events.value.find(e => e.id === selectedEventId.value))
-
-// ==================== 应急资源 ====================
-const vehicles = ref([
-  { id: "v1", name: "消防车-01", type: "车辆", lat: 31.19, lng: 118.48, online: true, category: "车辆" },
-  { id: "v2", name: "救护车-03", type: "车辆", lat: 31.21, lng: 118.52, online: true, category: "车辆" },
-  { id: "v3", name: "抢险车-05", type: "车辆", lat: 31.17, lng: 118.44, online: true, category: "车辆" }
-])
-
-const persons = ref([
-  { id: "p1", name: "张指挥", type: "指挥人员", lat: 31.2, lng: 118.5, device: "指挥箱-01", online: true, category: "人员" },
-  { id: "p2", name: "李消防员", type: "救援人员", lat: 31.19, lng: 118.49, device: "手机终端", online: true, category: "人员" },
-  { id: "p3", name: "王监测员", type: "监测人员", lat: 31.23, lng: 118.47, device: "单兵终端", online: true, category: "人员" },
-  { id: "p4", name: "赵医生", type: "医疗人员", lat: 31.18, lng: 118.46, device: "手机终端", online: true, category: "人员" }
-])
-
-// 所有设备(用于群组管理)
-const allDevices = ref([
-  { id: "d1", name: "应急指挥箱-化工园", type: "指挥箱", online: true },
-  { id: "d2", name: "手机终端-张指挥", type: "手机", online: true },
-  { id: "d3", name: "单兵终端-李消防", type: "单兵", online: true },
-  { id: "d4", name: "手机终端-王监测", type: "手机", online: true },
-  { id: "d5", name: "应急指挥箱-城南", type: "指挥箱", online: true }
-])
-
-// ==================== 临时群组 ====================
-const tempGroups = ref([
-  { id: "g1", name: "化工园区应急组", devices: ["d1", "d2"] },
-  { id: "g2", name: "医疗救护组", devices: ["d4"] }
-])
-
-const currentGroupId = ref(null)
-
-// ==================== 调度参数 ====================
-const dispatchRadius = ref(5)
-
-// 统计数据
-const stats = ref({
-  vehicleCount: 0,
-  personnelCount: 0
-})
-
-// ==================== 音视频对讲状态 ====================
-const callStatusText = ref("未建立对讲 | 选择设备或群组")
-let currentCallTarget = null
-
-// 监控摄像头列表
-const cameras = ref([
-  { name: "现场无人机画面", location: "化工园区" },
-  { name: "应急指挥箱画面", location: "城南救援点" },
-  { name: "移动单兵终端", location: "消防1组" },
-  { name: "路口监控", location: "城东路口" }
-])
-
-// ==================== 工具函数 ====================
-// 计算两点距离 (km)
-const getDistance = (lat1, lng1, lat2, lng2) => {
-  const R = 6371
-  const dLat = (lat2 - lat1) * Math.PI / 180
-  const dLng = (lng2 - lng1) * Math.PI / 180
-  const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2
-  return R * 2 * Math.asin(Math.sqrt(a))
-}
-
-// 更新统计和资源列表
-const updateStatistics = () => {
-  if (!selectedEvent.value) return
-
-  const event = selectedEvent.value
-  const radius = dispatchRadius.value
-
-  const vehicleCount = vehicles.value.filter(v =>
-      v.online && getDistance(event.lat, event.lng, v.lat, v.lng) <= radius
-  ).length
-
-  const personnelCount = persons.value.filter(p =>
-      p.online && getDistance(event.lat, event.lng, p.lat, p.lng) <= radius
-  ).length
-
-  stats.value = { vehicleCount, personnelCount }
-}
-
-// 半径内资源列表(用于显示)
-const resourcesInRange = computed(() => {
-  if (!selectedEvent.value) return []
-
-  const event = selectedEvent.value
-  const radius = dispatchRadius.value
-
-  const vehiclesInRange = vehicles.value
-      .filter(v => v.online && getDistance(event.lat, event.lng, v.lat, v.lng) <= radius)
-      .map(v => ({ ...v, category: "车辆" }))
-
-  const personsInRange = persons.value
-      .filter(p => p.online && getDistance(event.lat, event.lng, p.lat, p.lng) <= radius)
-      .map(p => ({ ...p, category: "人员" }))
-
-  return [...vehiclesInRange, ...personsInRange]
-})
-
-// ==================== 事件操作 ====================
-const selectEvent = (id) => {
-  selectedEventId.value = id
-  updateStatistics()
-}
-
-// ==================== 调度操作 ====================
-const sendDispatch = () => {
-  if (!selectedEvent.value) {
-    alert("请选择应急事件")
-    return
-  }
-  alert(`📨 调度指令已发送!\n事件: ${selectedEvent.value.name}\n调度半径: ${dispatchRadius.value}km\n覆盖车辆: ${stats.value.vehicleCount}辆, 人员: ${stats.value.personnelCount}人\n推送至手机APP、车载终端及应急指挥箱。`)
-}
-
-// ==================== 群组操作 ====================
-const getDeviceName = (deviceId) => {
-  const device = allDevices.value.find(d => d.id === deviceId)
-  return device?.name || deviceId
-}
-
-const selectGroup = (groupId) => {
-  currentGroupId.value = groupId
-}
-
-const createGroup = () => {
-  const name = prompt("请输入临时群组名称", "应急临时群组")
-  if (name) {
-    const newId = "g" + Date.now()
-    tempGroups.value.push({ id: newId, name: name, devices: [] })
-  }
-}
-
-const deleteGroup = (groupId) => {
-  tempGroups.value = tempGroups.value.filter(g => g.id !== groupId)
-  if (currentGroupId.value === groupId) {
-    currentGroupId.value = null
-  }
-}
-
-const addDeviceToGroup = (groupId) => {
-  const group = tempGroups.value.find(g => g.id === groupId)
-  if (!group) return
-
-  const availableDevices = allDevices.value.filter(d => !group.devices.includes(d.id))
-  if (availableDevices.length === 0) {
-    alert("该群组已包含所有可用设备")
-    return
-  }
-
-  const deviceNames = availableDevices.map(d => `${d.id}:${d.name}`).join("\n")
-  const deviceId = prompt(`可选设备:\n${deviceNames}\n\n请输入设备ID:`, availableDevices[0]?.id)
-
-  if (deviceId && allDevices.value.find(d => d.id === deviceId) && !group.devices.includes(deviceId)) {
-    group.devices.push(deviceId)
-  } else if (deviceId) {
-    alert("设备ID无效或已存在")
-  }
-}
-
-const removeDeviceFromGroup = (groupId, deviceId) => {
-  const group = tempGroups.value.find(g => g.id === groupId)
-  if (group) {
-    group.devices = group.devices.filter(d => d !== deviceId)
-  }
-}
-
-const addDeviceToCurrentGroup = () => {
-  if (!currentGroupId.value) {
-    alert("请先点击选中一个临时群组")
-    return
-  }
-  addDeviceToGroup(currentGroupId.value)
-}
-
-// ==================== 音视频对讲 ====================
-const callSingle = () => {
-  const deviceNames = allDevices.value.filter(d => d.online).map(d => d.name).join(", ")
-  const target = prompt("一对一呼叫: 输入设备名称", "应急指挥箱-化工园")
-  if (target) {
-    currentCallTarget = target
-    callStatusText.value = `📞 正在呼叫 ${target}...`
-    setTimeout(() => {
-      callStatusText.value = `✅ 已与 ${target} 建立音视频对讲`
-    }, 1000)
-  }
-}
-
-const callGroup = () => {
-  if (!currentGroupId.value) {
-    alert("请先选中一个临时群组")
-    return
-  }
-  const group = tempGroups.value.find(g => g.id === currentGroupId.value)
-  if (group && group.devices.length > 0) {
-    currentCallTarget = `群组:${group.name}`
-    callStatusText.value = `📢 发起群组对讲 (一对多) 目标: ${group.name}, 设备数:${group.devices.length}`
-    setTimeout(() => {
-      callStatusText.value = `🔊 群组对讲进行中: ${group.name} 已接通`
-    }, 800)
-  } else {
-    alert("群组无设备")
-  }
-}
-
-const endCall = () => {
-  currentCallTarget = null
-  callStatusText.value = "⏹️ 对讲已挂断 | 选择设备或群组"
-}
-
-// ==================== 监控操作 ====================
-const viewCamera = (cameraName) => {
-  alert(`📺 调取实时监控画面: ${cameraName}\n可查看现场最新情况,支持全屏及云台控制。`)
-}
-
-// ==================== 监听半径变化 ====================
-watch(dispatchRadius, () => {
-  updateStatistics()
-})
-
-// ==================== 初始化 ====================
-onMounted(() => {
-  updateStatistics()
-})
-</script>
-
-<style scoped>
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
-
-.prototype-container {
-  max-width: 1600px;
-  margin: 0 auto;
-  background: #ffffff;
-  border-radius: 28px;
-  box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
-  overflow: hidden;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
-}
-
-/* 头部 */
-.header {
-  background: #0a2b3c;
-  padding: 16px 28px;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: 12px;
-  border-bottom: 3px solid #e74c3c;
-}
-
-.logo h2 {
-  color: white;
-  font-weight: 600;
-  font-size: 1.4rem;
-}
-
-.logo p {
-  color: #bdd8e8;
-  font-size: 0.7rem;
-}
-
-.badge-emergency {
-  background: #e74c3c;
-  padding: 6px 18px;
-  border-radius: 30px;
-  color: white;
-  font-weight: 500;
-  font-size: 0.8rem;
-}
-
-/* 主体布局 */
-.main-layout {
-  display: flex;
-  flex-wrap: wrap;
-}
-
-.left-panel {
-  width: 320px;
-  background: #f9fbfd;
-  border-right: 1px solid #e2edf2;
-  padding: 20px 16px;
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-}
-
-.middle-panel {
-  width: 340px;
-  background: #ffffff;
-  border-right: 1px solid #e2edf2;
-  padding: 20px 16px;
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-}
-
-.right-panel {
-  flex: 1;
-  background: #1a2f3c;
-  padding: 20px;
-  min-width: 480px;
-}
-
-/* 卡片样式 */
-.card {
-  background: white;
-  border-radius: 20px;
-  border: 1px solid #e6edf3;
-  overflow: hidden;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
-}
-
-.card-header {
-  background: #f9fafc;
-  padding: 12px 16px;
-  border-bottom: 1px solid #eef2f6;
-  font-weight: 700;
-  font-size: 0.85rem;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  color: #1d5a78;
-}
-
-.card-body {
-  padding: 14px;
-}
-
-/* 按钮样式 */
-.btn-primary {
-  background: #e74c3c;
-  border: none;
-  color: white;
-  padding: 8px 16px;
-  border-radius: 30px;
-  font-weight: 600;
-  font-size: 0.75rem;
-  cursor: pointer;
-  transition: 0.2s;
-}
-
-.btn-primary:hover {
-  background: #c0392b;
-}
-
-.btn-outline {
-  background: white;
-  border: 1px solid #e74c3c;
-  color: #e74c3c;
-  padding: 6px 12px;
-  border-radius: 30px;
-  font-size: 0.7rem;
-  cursor: pointer;
-}
-
-.btn-sm {
-  background: #ecf0f1;
-  border: none;
-  padding: 4px 10px;
-  border-radius: 20px;
-  font-size: 0.65rem;
-  cursor: pointer;
-}
-
-.btn-sm-add {
-  background: #ecf0f1;
-  border: none;
-  padding: 2px 8px;
-  border-radius: 20px;
-  font-size: 0.6rem;
-  cursor: pointer;
-}
-
-/* 事件列表 */
-.event-list {
-  max-height: 250px;
-  overflow-y: auto;
-}
-
-.event-item {
-  background: #fef9f2;
-  border-radius: 14px;
-  padding: 12px;
-  margin-bottom: 10px;
-  border-left: 3px solid #e74c3c;
-  cursor: pointer;
-  transition: 0.1s;
-}
-
-.event-item.selected {
-  background: #fde8e5;
-  border-left-color: #c0392b;
-  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
-}
-
-.event-title {
-  font-weight: 700;
-  font-size: 0.8rem;
-}
-
-.event-loc {
-  font-size: 0.65rem;
-  color: #6f8ea8;
-  margin-top: 4px;
-}
-
-.selected-event-info {
-  font-size: 0.7rem;
-  background: #eef2fa;
-  padding: 8px;
-  border-radius: 12px;
-  margin-bottom: 12px;
-}
-
-.dispatch-panel {
-  background: #fef7e8;
-  border-radius: 16px;
-  padding: 12px;
-  border: 1px solid #f0dbb4;
-}
-
-.form-group {
-  margin-bottom: 12px;
-}
-
-.form-label {
-  font-size: 0.7rem;
-  font-weight: 600;
-  display: block;
-  margin-bottom: 4px;
-}
-
-.form-input {
-  width: 100%;
-  padding: 6px 10px;
-  border-radius: 20px;
-  border: 1px solid #cbdde6;
-  font-size: 0.75rem;
-}
-
-.stat-numbers {
-  display: flex;
-  gap: 16px;
-  margin-top: 5px;
-  font-size: 0.75rem;
-}
-
-.dispatch-note {
-  font-size: 0.6rem;
-  margin-top: 8px;
-  color: #6e95b0;
-  text-align: center;
-}
-
-/* 资源列表 */
-.resource-list {
-  max-height: 200px;
-  overflow-y: auto;
-}
-
-.resource-item {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 8px 0;
-  border-bottom: 1px solid #edf2f6;
-  font-size: 0.75rem;
-}
-
-.online-dot {
-  width: 8px;
-  height: 8px;
-  background: #2ecc71;
-  border-radius: 50%;
-  display: inline-block;
-  margin-right: 6px;
-}
-
-.resource-type {
-  font-size: 0.65rem;
-  color: #6f8ea8;
-}
-
-.empty-resource, .empty-group {
-  text-align: center;
-  padding: 20px;
-  color: #8aaec2;
-  font-size: 0.75rem;
-}
-
-.region-hint {
-  font-size: 0.6rem;
-  font-weight: normal;
-  background: #eef2fa;
-  padding: 2px 8px;
-  border-radius: 20px;
-}
-
-/* 群组列表 */
-.group-list {
-  max-height: 220px;
-  overflow-y: auto;
-}
-
-.group-item {
-  background: #f2f6f9;
-  border-radius: 12px;
-  padding: 10px;
-  margin-bottom: 8px;
-  cursor: pointer;
-  transition: 0.1s;
-}
-
-.group-item.active {
-  border: 1px solid #e74c3c;
-  background: #fef5f0;
-}
-
-.group-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 8px;
-}
-
-.remove-group {
-  background: none;
-  border: none;
-  color: #e74c3c;
-  cursor: pointer;
-  font-size: 0.7rem;
-}
-
-.device-list {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 6px;
-  align-items: center;
-}
-
-.device-tag {
-  background: #e0e7ed;
-  padding: 2px 8px;
-  border-radius: 20px;
-  font-size: 0.65rem;
-  display: inline-flex;
-  align-items: center;
-  gap: 4px;
-}
-
-.device-tag .remove {
-  color: #e74c3c;
-  cursor: pointer;
-  font-weight: bold;
-}
-
-/* 监控大屏 */
-.monitor-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 16px;
-  margin-bottom: 20px;
-}
-
-.monitor-screen {
-  background: #0f2b33;
-  border-radius: 16px;
-  overflow: hidden;
-  border: 1px solid #2a5f6e;
-  cursor: pointer;
-}
-
-.monitor-header {
-  background: #1a3e48;
-  padding: 8px 12px;
-  color: white;
-  font-size: 0.7rem;
-  display: flex;
-  justify-content: space-between;
-}
-
-.monitor-video {
-  background: #000000;
-  height: 120px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #6c9eb0;
-  font-size: 0.7rem;
-  background-image: linear-gradient(45deg, #1a3a44 25%, #0e2a32 25%);
-  background-size: 20px 20px;
-}
-
-/* 音视频对讲 */
-.call-controls {
-  background: #0f2b33;
-  border-radius: 20px;
-  padding: 16px;
-  margin-top: 16px;
-}
-
-.call-header {
-  color: white;
-  font-size: 0.8rem;
-  margin-bottom: 12px;
-}
-
-.call-buttons {
-  display: flex;
-  gap: 12px;
-  flex-wrap: wrap;
-}
-
-.btn-call {
-  background: #27ae60;
-  border: none;
-  color: white;
-  padding: 8px 16px;
-  border-radius: 30px;
-  font-size: 0.75rem;
-  cursor: pointer;
-}
-
-.btn-call-end {
-  background: #e74c3c;
-}
-
-.call-status {
-  background: #1e4a62;
-  border-radius: 20px;
-  padding: 8px 12px;
-  margin-top: 12px;
-  font-size: 0.7rem;
-  color: white;
-}
-
-.call-note {
-  color: #9ec3d4;
-  font-size: 0.65rem;
-  margin-top: 8px;
-}
-</style>

+ 598 - 30
src/views/subSystem/dispatch/disExerciseMan/DIsSimulationDrill.vue

@@ -1,37 +1,605 @@
 <!--模拟演练管理-->
-<script setup>
+<template>
+  <div class="app-container">
+    <div class="sim-layout">
+      <!-- ===== 左列 ===== -->
+      <div class="col-left">
+        <!-- 演练项目设置 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><Folder /></el-icon> 演练项目设置</span>
+            <el-button type="primary" size="small" @click="handleAddProject">新增</el-button>
+          </div>
+          <div class="panel-body">
+            <div class="project-item" v-for="item in projects" :key="item.id">
+              <div class="project-info">
+                <div class="project-name">{{ item.name }}</div>
+                <div class="project-date">{{ item.date }}</div>
+              </div>
+              <div class="project-actions">
+                <el-button link type="primary" @click="handleEditProject(item)"><el-icon><Edit /></el-icon></el-button>
+                <el-button link type="danger" @click="handleDeleteProject(item)"><el-icon><Delete /></el-icon></el-button>
+              </div>
+            </div>
+          </div>
+        </div>
 
-</script>
+        <!-- 场景设置 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><PictureFilled /></el-icon> 场景设置</span>
+            <el-button type="primary" size="small" @click="handleEditScene">编辑</el-button>
+          </div>
+          <div class="panel-body">
+            <div class="scene-section">
+              <div class="scene-label">当前场景元素:</div>
+              <div class="tag-list">
+                <el-tag v-for="tag in currentSceneTags" :key="tag" size="small" closable @close="removeSceneTag(tag)">{{ tag }}</el-tag>
+              </div>
+            </div>
+            <div class="scene-section">
+              <div class="scene-label">场景元素库:</div>
+              <div class="tag-list">
+                <el-tag v-for="tag in sceneLibrary" :key="tag" size="small" type="info" class="clickable-tag" @click="addSceneTag(tag)">{{ tag }}</el-tag>
+              </div>
+            </div>
+          </div>
+        </div>
 
-<template>
-<div>5.2.3.3.3. 模拟演练管理
-  模拟演练利用计算机虚拟场景进行仿真演练,一个演练项目中可包含多个事件和任务模型。
-  沅陵县太常片区城市地下管网及设施建设改造项目工程货物(智能化系统及设备)第 47 页 共 76 页
-  具体功能包括:
-  (1)演练项目设置
-  对演练项目的增加、删除、修改、查询和统计。
-  (2)场景设置
-  以“所见即所得”方式进行场景制作和编辑,可利用场景库提供场景的制作元素,提供场景
-  相关参数的设置。
-  (3)事件设置
-  提供突发事件制作和编辑功能,可利用事件模型库提供事件制作元素。可根据需要随时向场
-  景中添加新的事件,以培养受训人员的应变能力。
-  (4)角色设置
-  角色可分为救援类角色、指挥类角色和观摩类角色等,不同角色拥有不同的技能和装备,演
-  练执行时,角色通过使用特定的技能和装备完成相应的任务。
-  (5)任务设置
-  提供任务制作和编辑功能,任务与角色绑定,每个任务必须分配至系统提供的角色,可由任
-  务模型库提供任务制作元素。
-  (6)过程管理
-  在演练过程中,系统提供事件启动、发展及结束的控制功能,以及整个演练过程的启动、暂
-  停和退出功能,并记录处置过程信息。
-  虚拟应急处置过程完成后,支持通过系统记录的处置流程数据再现应急处置过程,为应急管
-  理人员提供处置流程的回顾,让指挥决策者进行自评价与互评价,可以使应急管理人员总结经验
-  教训,提高应急管理水平。
-  (7)结果分析与评估
-  利用应急统计分析和应急评估模块功能,进行本次演练的统计分析和结果评估。</div>
+        <!-- 搜索 + 统计 -->
+        <div class="panel">
+          <div class="search-bar">
+            <el-input v-model="projectSearch" placeholder="查询项目名称..." size="small" style="width: 160px" />
+            <el-button type="primary" size="small" @click="handleSearchProject">查询</el-button>
+          </div>
+          <div class="stat-line">项目总数:{{ projects.length }}</div>
+        </div>
+      </div>
+
+      <!-- ===== 中列 ===== -->
+      <div class="col-center">
+        <!-- 事件设置 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><Warning /></el-icon> 事件设置(突发事件制作)</span>
+            <el-button type="primary" size="small" @click="handleAddEvent">新增事件</el-button>
+          </div>
+          <div class="panel-body">
+            <div class="event-card" v-for="evt in events" :key="evt.id">
+              <div class="event-info">
+                <div class="event-name">
+                  <el-icon style="color: #e6a23c"><Warning /></el-icon>
+                  {{ evt.name }}
+                </div>
+                <div class="event-desc">{{ evt.desc }}</div>
+              </div>
+              <el-button type="primary" size="small" @click="addToScene(evt)">添加到场景</el-button>
+            </div>
+            <div class="tip-text">提示:可随时向场景中添加新事件,培养应变能力</div>
+          </div>
+        </div>
+
+        <!-- 角色设置 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><User /></el-icon> 角色设置(救援/指挥/观摩)</span>
+            <el-button type="primary" size="small" @click="handleAddRole">新增角色</el-button>
+          </div>
+          <div class="panel-body">
+            <div class="role-item" v-for="role in roles" :key="role.id">
+              <div class="role-name">{{ role.name }}({{ role.type }})</div>
+              <div class="role-detail">技能:{{ role.skills }} | 装备:{{ role.equipment }}</div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 任务设置 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><List /></el-icon> 任务设置(任务模型库)</span>
+            <el-button type="primary" size="small" @click="handleAddTask">新增任务</el-button>
+          </div>
+          <div class="panel-body">
+            <div class="task-item" v-for="task in tasks" :key="task.id">
+              <div class="task-name">{{ task.name }}</div>
+              <div class="task-desc">{{ task.desc }}</div>
+              <div class="task-bind">绑定角色:{{ task.bindRole }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- ===== 右列 ===== -->
+      <div class="col-right">
+        <!-- 过程管理 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><VideoPlay /></el-icon> 过程管理</span>
+          </div>
+          <div class="panel-body">
+            <div class="process-buttons">
+              <el-button type="primary" size="small" @click="startDrill">启动</el-button>
+              <el-button type="warning" size="small" @click="pauseDrill">暂停</el-button>
+              <el-button type="danger" size="small" @click="exitDrill">退出</el-button>
+            </div>
+            <div class="event-log">
+              <div class="log-item" v-for="(log, idx) in eventLogs" :key="idx">
+                <span class="log-time">[{{ log.time }}]</span>
+                <span class="log-text">{{ log.text }}</span>
+              </div>
+            </div>
+            <div class="event-control">
+              <div class="control-label">事件控制</div>
+              <div class="control-buttons">
+                <el-button type="primary" size="small" @click="triggerEvent">
+                  <el-icon><Promotion /></el-icon>触发事件(发展)
+                </el-button>
+                <el-button size="small" @click="endEvent">
+                  <el-icon><Document /></el-icon>结束事件
+                </el-button>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 结果分析与评估 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><DataAnalysis /></el-icon> 结果分析与评估</span>
+          </div>
+          <div class="panel-body">
+            <div class="metric-grid">
+              <div class="metric-item">
+                <div class="metric-label">任务完成率</div>
+                <div class="metric-value">33%</div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-label">平均响应时间</div>
+                <div class="metric-value">30s</div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-label">应急评估等级</div>
+                <div class="metric-value highlight">良好</div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-label">建议</div>
+                <div class="metric-value small">加强指挥调度协同</div>
+              </div>
+            </div>
+            <div class="stat-cards">
+              <div class="stat-card">
+                <div class="stat-title">应急统计</div>
+                <div class="stat-content">事件响应时间 28秒</div>
+              </div>
+              <div class="stat-card">
+                <div class="stat-title">应急评估</div>
+                <div class="stat-content">任务完成率 67%</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
+<script setup>
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  Folder, Edit, Delete, PictureFilled, Warning, User, List,
+  VideoPlay, Promotion, Document, DataAnalysis
+} from '@element-plus/icons-vue'
+
+// ---------- 演练项目 ----------
+let projectId = 3
+const projects = ref([
+  { id: 1, name: '化工园区毒气泄漏演练', date: '2025-01-01' },
+  { id: 2, name: '海上溢油应急联合演练', date: '2025-01-01' },
+  { id: 3, name: '化工园区综合应急演练', date: '2025-03-15' }
+])
+
+const projectSearch = ref('')
+const handleSearchProject = () => { ElMessage.success('查询项目(Mock)') }
+const handleAddProject = () => { ElMessage.success('新增演练项目(Mock)') }
+const handleEditProject = (item) => { ElMessage.success(`编辑项目: ${item.name}`) }
+const handleDeleteProject = (item) => {
+  ElMessageBox.confirm('确认删除该项目?', '提示', { type: 'warning' }).then(() => {
+    projects.value = projects.value.filter(p => p.id !== item.id)
+    ElMessage.success('删除成功')
+  }).catch(() => {})
+}
+
+// ---------- 场景设置 ----------
+const currentSceneTags = ref(['控制室', '泄漏点', '氨气泄露', '火灾蔓延'])
+const sceneLibrary = ref(['控制室', '泄漏点', '储罐区', '氨气泄露', '火灾蔓延', '风向标'])
+
+const addSceneTag = (tag) => {
+  if (!currentSceneTags.value.includes(tag)) {
+    currentSceneTags.value.push(tag)
+  }
+}
+const removeSceneTag = (tag) => {
+  currentSceneTags.value = currentSceneTags.value.filter(t => t !== tag)
+}
+const handleEditScene = () => { ElMessage.success('编辑场景(Mock)') }
+
+// ---------- 事件设置 ----------
+const events = ref([
+  { id: 1, name: '氯气泄漏', desc: '突发事件: 储罐区发生泄漏,需紧急处置' },
+  { id: 2, name: '火灾蔓延', desc: '次生灾害: 爆炸引起火灾' }
+])
+
+const handleAddEvent = () => { ElMessage.success('新增事件(Mock)') }
+const addToScene = (evt) => { ElMessage.success(`已将"${evt.name}"添加到场景`) }
+
+// ---------- 角色设置 ----------
+const roles = ref([
+  { id: 1, name: '现场指挥员', type: '指挥类', skills: '消防指挥、泄漏处置', equipment: '防护服、指挥终端' },
+  { id: 2, name: '消防队员', type: '救援类', skills: '灭火、堵漏', equipment: '防护服、灭火器' }
+])
+
+const handleAddRole = () => { ElMessage.success('新增角色(Mock)') }
+
+// ---------- 任务设置 ----------
+const tasks = ref([
+  { id: 1, name: '启动应急响应', desc: '指挥员需下达疏散命令', bindRole: '现场指挥员' },
+  { id: 2, name: '演练整体评估', desc: '完成评价报告', bindRole: '现场指挥员' }
+])
+
+const handleAddTask = () => { ElMessage.success('新增任务(Mock)') }
+
+// ---------- 过程管理 ----------
+const eventLogs = ref([
+  { time: '16:11:05', text: '演练启动' },
+  { time: '16:11:06', text: '演练暂停' },
+  { time: '16:11:07', text: '触发事件: 氯气泄漏 - 影响范围扩大,角色已分配' },
+  { time: '16:11:10', text: '触发事件: 氯气泄漏 - 影响范围进一步扩大' }
+])
+
+const startDrill = () => {
+  const now = new Date()
+  const time = now.toTimeString().slice(0, 8)
+  eventLogs.value.push({ time, text: '演练启动' })
+  ElMessage.success('演练已启动')
+}
+const pauseDrill = () => {
+  const now = new Date()
+  const time = now.toTimeString().slice(0, 8)
+  eventLogs.value.push({ time, text: '演练暂停' })
+  ElMessage.warning('演练已暂停')
+}
+const exitDrill = () => {
+  ElMessageBox.confirm('确认退出演练?', '提示', { type: 'warning' }).then(() => {
+    const now = new Date()
+    const time = now.toTimeString().slice(0, 8)
+    eventLogs.value.push({ time, text: '演练退出' })
+    ElMessage.info('演练已退出')
+  }).catch(() => {})
+}
+
+const triggerEvent = () => {
+  const now = new Date()
+  const time = now.toTimeString().slice(0, 8)
+  eventLogs.value.push({ time, text: '触发事件(发展): 事态发展变化' })
+  ElMessage.success('事件已触发')
+}
+const endEvent = () => {
+  const now = new Date()
+  const time = now.toTimeString().slice(0, 8)
+  eventLogs.value.push({ time, text: '事件结束' })
+  ElMessage.info('事件已结束')
+}
+</script>
+
 <style scoped lang="scss">
+.sim-layout {
+  display: flex;
+  gap: 12px;
+  min-height: calc(100vh - 160px);
+}
+
+.col-left {
+  width: 25%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.col-center {
+  width: 50%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.col-right {
+  width: 25%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.panel {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  background: #fff;
+  overflow: hidden;
+
+  .panel-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 14px;
+    background: #f5f7fa;
+    border-bottom: 1px solid #e4e7ed;
+    font-weight: bold;
+    font-size: 14px;
+    color: #303133;
+
+    span {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    .el-icon {
+      color: #409eff;
+    }
+  }
+
+  .panel-body {
+    padding: 12px 14px;
+  }
+}
+
+// 项目列表
+.project-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  border-bottom: 1px dashed #ebeef5;
+
+  &:last-child { border-bottom: none; }
+
+  .project-info {
+    .project-name { font-size: 13px; color: #303133; font-weight: 500; }
+    .project-date { font-size: 12px; color: #909399; margin-top: 2px; }
+  }
+
+  .project-actions {
+    display: flex;
+    gap: 2px;
+  }
+}
+
+// 场景设置
+.scene-section {
+  margin-bottom: 12px;
+
+  .scene-label {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 6px;
+  }
+}
+
+.tag-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+
+  .clickable-tag {
+    cursor: pointer;
+    transition: all 0.2s;
+
+    &:hover {
+      color: #409eff;
+      border-color: #409eff;
+    }
+  }
+}
+
+.search-bar {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.stat-line {
+  font-size: 12px;
+  color: #909399;
+  text-align: right;
+}
+
+// 事件卡片
+.event-card {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding: 10px;
+  margin-bottom: 8px;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  background: #fafafa;
+
+  .event-info {
+    flex: 1;
+    margin-right: 10px;
+
+    .event-name {
+      font-size: 14px;
+      font-weight: 500;
+      color: #303133;
+      display: flex;
+      align-items: center;
+      gap: 4px;
+    }
+
+    .event-desc {
+      font-size: 12px;
+      color: #909399;
+      margin-top: 4px;
+    }
+  }
+}
+
+.tip-text {
+  font-size: 12px;
+  color: #909399;
+  padding: 8px;
+  background: #f0f9eb;
+  border-radius: 4px;
+  margin-top: 4px;
+}
+
+// 角色列表
+.role-item {
+  padding: 8px 0;
+  border-bottom: 1px dashed #ebeef5;
+
+  &:last-child { border-bottom: none; }
+
+  .role-name {
+    font-size: 13px;
+    font-weight: 500;
+    color: #303133;
+  }
+
+  .role-detail {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+  }
+}
+
+// 任务列表
+.task-item {
+  padding: 8px 0;
+  border-bottom: 1px dashed #ebeef5;
+
+  &:last-child { border-bottom: none; }
+
+  .task-name {
+    font-size: 13px;
+    font-weight: 500;
+    color: #303133;
+  }
+
+  .task-desc {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 2px;
+  }
+
+  .task-bind {
+    font-size: 12px;
+    color: #409eff;
+    margin-top: 2px;
+  }
+}
+
+// 过程管理
+.process-buttons {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 10px;
+}
+
+.event-log {
+  max-height: 200px;
+  overflow-y: auto;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 8px;
+  margin-bottom: 10px;
+  background: #fafafa;
+
+  .log-item {
+    font-size: 12px;
+    line-height: 1.8;
+    color: #606266;
+
+    .log-time {
+      color: #409eff;
+      margin-right: 6px;
+      font-family: monospace;
+    }
+  }
+}
+
+.event-control {
+  border-top: 1px solid #ebeef5;
+  padding-top: 10px;
+
+  .control-label {
+    font-size: 13px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 8px;
+  }
+
+  .control-buttons {
+    display: flex;
+    gap: 8px;
+  }
+}
+
+// 结果分析
+.metric-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 10px;
+  margin-bottom: 12px;
+}
+
+.metric-item {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 10px;
+  text-align: center;
+
+  .metric-label {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 4px;
+  }
+
+  .metric-value {
+    font-size: 18px;
+    font-weight: bold;
+    color: #303133;
+
+    &.highlight { color: #67c23a; }
+    &.small { font-size: 13px; font-weight: normal; color: #606266; }
+  }
+}
+
+.stat-cards {
+  display: flex;
+  gap: 10px;
+}
+
+.stat-card {
+  flex: 1;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 10px;
+  text-align: center;
+  background: #fafafa;
+
+  .stat-title {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 4px;
+  }
 
-</style>
+  .stat-content {
+    font-size: 14px;
+    font-weight: bold;
+    color: #409eff;
+  }
+}
+</style>

+ 411 - 6
src/views/subSystem/dispatch/disExerciseMan/DisCurriculum.vue

@@ -1,13 +1,418 @@
 <!--培训课程设置-->
-<script setup>
+<template>
+  <div class="app-container">
+    <!-- 课程设置模版 -->
+    <div class="template-section">
+      <div class="template-card knowledge-card">
+        <div class="card-header">
+          <el-icon><Reading /></el-icon>
+          <span class="card-title">知识培训模版</span>
+        </div>
+        <div class="card-structure">理论课件 + 法规文档 + 考核题库</div>
+        <div class="card-content-list">
+          <div class="content-item" v-for="item in knowledgeExamples" :key="item">
+            <span class="dot knowledge-dot"></span>{{ item }}
+          </div>
+        </div>
+        <div class="card-footer">
+          预设课程:应急法规知识库 | 个人防护理论
+        </div>
+      </div>
+      <div class="template-card scenario-card">
+        <div class="card-header">
+          <el-icon><VideoPlay /></el-icon>
+          <span class="card-title">情景培训模版</span>
+        </div>
+        <div class="card-structure">情景脚本 + 角色扮演 + 仿真推演</div>
+        <div class="card-content-list">
+          <div class="content-item" v-for="item in scenarioExamples" :key="item">
+            <span class="dot scenario-dot"></span>{{ item }}
+          </div>
+        </div>
+        <div class="card-footer">
+          预设课程:实战化情景演练 | 无脚本应急响应
+        </div>
+      </div>
+    </div>
 
-</script>
+    <!-- 搜索区 -->
+    <el-form :model="queryParams" :inline="true" class="search-form">
+      <el-form-item>
+        <el-input v-model="queryParams.keyword" placeholder="按课程名称/培训类型查询" clearable style="width: 260px" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleSearch">查询</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </el-form-item>
+      <el-form-item style="float: right">
+        <el-button type="success" @click="handleAdd">新增课程</el-button>
+        <el-button type="primary" @click="handleImport">导入课程表</el-button>
+        <el-button type="primary" @click="handleExport">导出课程表</el-button>
+      </el-form-item>
+    </el-form>
 
-<template>
-<div>5.2.3.3.2. 培训课程设置
-  根据培训计划中的培训方式设定不同的培训课程,提供课程设置模版。对于知识培训则设定知识培训内容;对于情景培训则设定情景模拟培训内容。系统可实现对培训课程的增加、删除、修改、查询、课程表的导入导出、对培训课程完成情况的记录和查询等功能。</div>
+    <!-- 表格 -->
+    <el-table :data="filteredList" border stripe style="width: 100%">
+      <el-table-column label="序号" type="index" width="60" align="center" />
+      <el-table-column prop="name" label="课程名称" min-width="180" show-overflow-tooltip />
+      <el-table-column label="培训方式" width="130" align="center">
+        <template #default="{ row }">
+          <span class="method-tag" :class="row.method === '知识培训' ? 'method-knowledge' : 'method-scenario'">
+            <el-icon v-if="row.method === '知识培训'"><Reading /></el-icon>
+            <el-icon v-else><VideoPlay /></el-icon>
+            {{ row.method }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="content" label="课程内容(知识/情景模拟)" min-width="280" show-overflow-tooltip />
+      <el-table-column label="计划周期" width="200" align="center">
+        <template #default="{ row }">
+          <span v-if="row.startDate">{{ row.startDate }} ~ {{ row.endDate }}</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="完成情况" width="110" align="center">
+        <template #default="{ row }">
+          <span v-if="row.completion === '已完成'" class="status-completed">已完成</span>
+          <span v-else-if="row.completion === '进行中'" class="status-ongoing">进行中</span>
+          <span v-else-if="row.completion === '未开始'" class="status-notstarted">未开始</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon></el-button>
+          <el-button link type="danger" @click="handleDelete(row)"><el-icon><Delete /></el-icon></el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close>
+      <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+        <el-form-item label="课程名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入课程名称" />
+        </el-form-item>
+        <el-form-item label="培训方式" prop="method">
+          <el-select v-model="form.method" placeholder="请选择培训方式" style="width: 100%">
+            <el-option label="知识培训" value="知识培训" />
+            <el-option label="情景培训" value="情景培训" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="课程内容" prop="content">
+          <el-input
+            v-model="form.content"
+            type="textarea"
+            :rows="3"
+            :placeholder="contentPlaceholder"
+          />
+        </el-form-item>
+        <el-form-item label="计划周期" prop="dateRange">
+          <el-date-picker
+            v-model="form.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="备注说明" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注说明" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">课程提交</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
+<script setup>
+import { ref, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Reading, VideoPlay, Edit, Delete } from '@element-plus/icons-vue'
+
+// ---------- 模版示例数据 ----------
+const knowledgeExamples = [
+  '危化品事故应急原则与处置规范',
+  '应急指挥体系标准化课程',
+  '新《安全生产法》深度解读'
+]
+const scenarioExamples = [
+  '船舶火灾应急处置情景模拟(指挥舱/现场联动)',
+  '海上溢油事故跨国协作桌面推演',
+  '突发风暴救援VR情景模拟'
+]
+
+// ---------- 查询 ----------
+const queryParams = ref({ keyword: '' })
+
+// ---------- Mock 数据 ----------
+let idCounter = 5
+const tableData = ref([
+  {
+    id: 1,
+    name: '应急指挥体系知识培训',
+    method: '知识培训',
+    content: '知识内容: 应急指挥架构、岗位职责、信息报送规范,含线上考核题库。',
+    startDate: '2025-05-10',
+    endDate: '2025-05-12',
+    completion: '已完成',
+    remark: ''
+  },
+  {
+    id: 2,
+    name: '危化品泄漏情景模拟演练',
+    method: '情景培训',
+    content: '情景模拟: 设定化工厂储罐泄漏,角色扮演+现场处置决策+事后复盘。',
+    startDate: '2025-05-18',
+    endDate: '2025-05-20',
+    completion: '进行中',
+    remark: ''
+  },
+  {
+    id: 3,
+    name: '水域救援知识课程',
+    method: '知识培训',
+    content: '知识内容: 救援设备使用原理、水流分析基础、急救医学常识,附考试。',
+    startDate: '2025-05-25',
+    endDate: '2025-05-25',
+    completion: '未开始',
+    remark: ''
+  },
+  {
+    id: 4,
+    name: '地震救援情景模拟课程',
+    method: '情景培训',
+    content: '情景模拟: 设定地震灾害场景,救援队伍协同作战、人员搜救与医疗救治。',
+    startDate: '2025-06-10',
+    endDate: '2025-06-12',
+    completion: '未开始',
+    remark: ''
+  },
+  {
+    id: 5,
+    name: '消防器材操作知识培训',
+    method: '知识培训',
+    content: '知识内容: 各类消防器材识别、使用方法、维护保养规范,实操考核。',
+    startDate: '2025-06-15',
+    endDate: '2025-06-16',
+    completion: '未开始',
+    remark: ''
+  }
+])
+
+// ---------- 过滤 ----------
+const filteredList = computed(() => {
+  if (!queryParams.value.keyword) return tableData.value
+  const kw = queryParams.value.keyword.toLowerCase()
+  return tableData.value.filter(
+    i => i.name.toLowerCase().includes(kw) || i.method.toLowerCase().includes(kw)
+  )
+})
+
+const handleSearch = () => {}
+const handleReset = () => { queryParams.value.keyword = '' }
+
+// ---------- 弹窗 ----------
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增课程')
+const formRef = ref(null)
+
+const defaultForm = () => ({
+  id: null,
+  name: '',
+  method: '知识培训',
+  content: '',
+  dateRange: null,
+  completion: '未开始',
+  remark: ''
+})
+
+const form = ref(defaultForm())
+
+const contentPlaceholder = computed(() => {
+  return form.value.method === '知识培训'
+    ? '知识培训内容:基础理论、法律法规、考核标准等'
+    : '情景模拟培训内容:情景脚本、角色扮演、仿真推演等'
+})
+
+const formRules = {
+  name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
+  method: [{ required: true, message: '请选择培训方式', trigger: 'change' }]
+}
+
+const handleAdd = () => {
+  form.value = defaultForm()
+  dialogTitle.value = '新增课程'
+  dialogVisible.value = true
+}
+
+const handleEdit = (row) => {
+  form.value = {
+    ...row,
+    dateRange: row.startDate ? [row.startDate, row.endDate] : null
+  }
+  dialogTitle.value = '修改课程'
+  dialogVisible.value = true
+}
+
+const handleSubmit = () => {
+  formRef.value?.validate((valid) => {
+    if (!valid) return
+    const data = { ...form.value }
+    if (data.dateRange && data.dateRange.length === 2) {
+      data.startDate = data.dateRange[0]
+      data.endDate = data.dateRange[1]
+    } else {
+      data.startDate = ''
+      data.endDate = ''
+    }
+    delete data.dateRange
+
+    if (data.id) {
+      const idx = tableData.value.findIndex(i => i.id === data.id)
+      if (idx !== -1) tableData.value[idx] = { ...data }
+    } else {
+      data.id = ++idCounter
+      tableData.value.unshift({ ...data })
+    }
+    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('删除成功')
+  }).catch(() => {})
+}
+
+// ---------- 导入/导出 ----------
+const handleImport = () => { ElMessage.success('导入课程表(Mock)') }
+const handleExport = () => { ElMessage.success('导出课程表(Mock)') }
+</script>
+
 <style scoped lang="scss">
+.template-section {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.template-card {
+  flex: 1;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  padding: 16px;
+  background: #fafafa;
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    margin-bottom: 8px;
+
+    .card-title {
+      font-weight: bold;
+      font-size: 15px;
+      color: #303133;
+    }
+
+    .el-icon {
+      font-size: 18px;
+    }
+  }
+
+  .card-structure {
+    font-size: 13px;
+    color: #909399;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+    border-bottom: 1px dashed #dcdfe6;
+  }
+
+  .card-content-list {
+    margin-bottom: 10px;
+
+    .content-item {
+      display: flex;
+      align-items: flex-start;
+      gap: 8px;
+      font-size: 13px;
+      color: #606266;
+      line-height: 1.6;
+      margin-bottom: 4px;
+
+      .dot {
+        display: inline-block;
+        width: 6px;
+        height: 6px;
+        border-radius: 50%;
+        margin-top: 7px;
+        flex-shrink: 0;
+
+        &.knowledge-dot { background: #e6a23c; }
+        &.scenario-dot { background: #409eff; }
+      }
+    }
+  }
+
+  .card-footer {
+    font-size: 12px;
+    color: #409eff;
+    padding-top: 8px;
+    border-top: 1px dashed #dcdfe6;
+  }
+}
+
+.knowledge-card {
+  .el-icon { color: #e6a23c; }
+}
+
+.scenario-card {
+  .el-icon { color: #409eff; }
+}
+
+.search-form {
+  margin-bottom: 12px;
+  :deep(.el-form-item) {
+    margin-bottom: 8px;
+  }
+}
+
+.method-tag {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+
+  &.method-knowledge {
+    color: #e6a23c;
+  }
+
+  &.method-scenario {
+    color: #409eff;
+  }
+}
+
+.status-completed {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.status-ongoing {
+  color: #e6a23c;
+  font-weight: bold;
+}
 
-</style>
+.status-notstarted {
+  color: #f56c6c;
+  font-weight: bold;
+}
+</style>

+ 250 - 6
src/views/subSystem/dispatch/disExerciseMan/DisPlanning.vue

@@ -1,13 +1,257 @@
 <!--培训演练计划制定-->
-<script setup>
+<template>
+  <div class="app-container">
+    <!-- 页面标题栏 -->
+    <div class="page-header">
+      <div class="header-title">
+        <el-icon><Document /></el-icon>
+        <span>培训演练计划制定 · 按事故类型应急处置</span>
+      </div>
+      <el-button type="success" @click="handleAdd">
+        <el-icon><Plus /></el-icon>新增演练计划
+      </el-button>
+    </div>
 
-</script>
+    <!-- 表格 -->
+    <el-table :data="tableData" border stripe style="width: 100%">
+      <el-table-column label="序号" type="index" width="60" align="center" />
+      <el-table-column prop="category" label="演练内容分类" width="180" />
+      <el-table-column prop="name" label="演练名称/主题" min-width="220" show-overflow-tooltip />
+      <el-table-column label="计划日期" width="200" align="center">
+        <template #default="{ row }">
+          <span v-if="row.startDate">{{ row.startDate }} ~ {{ row.endDate }}</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="base" label="依托基地/站点" width="180" show-overflow-tooltip />
+      <el-table-column prop="method" label="培训方式" min-width="200" show-overflow-tooltip />
+      <el-table-column label="状态" width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="statusTagType(row.status)" size="small">{{ row.status }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="140" 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>
+        </template>
+      </el-table-column>
+    </el-table>
 
-<template>
-<div>5.2.3.3.1. 培训演练计划制定
-  针对事故类型的应急处置,将应急演练内容划分为:指挥调度演练、资源调度演练、辅助决策演练、应急处置实施演练、应急信息上传下达演练等。并将演练内容制定出培训演练计划。实现应急培训及演练工作的正常、有序开展,可依托现有的应急救助基地、监管救助站、器材站,通过配备相应的应急培训演练设施、设备、加强应急指挥人员和应急处置人员的培训方式、建设应急演练基地</div>
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close>
+      <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+        <el-form-item label="演练名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入演练名称" />
+        </el-form-item>
+        <el-form-item label="演练分类" prop="category">
+          <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="base">
+          <el-input v-model="form.base" placeholder="请输入依托基地/站点" />
+        </el-form-item>
+        <el-form-item label="计划日期" prop="dateRange">
+          <el-date-picker
+            v-model="form.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="培训方式" prop="method">
+          <el-input
+            v-model="form.method"
+            type="textarea"
+            :rows="3"
+            placeholder="填写具体判断规则、适用条件、划分依据"
+          />
+        </el-form-item>
+        <el-form-item label="备注说明" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注说明" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存提交</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
+<script setup>
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Document, Plus } from '@element-plus/icons-vue'
+
+// ---------- 常量 ----------
+const categoryOptions = [
+  '指挥调度演练',
+  '资源调度演练',
+  '辅助决策演练',
+  '应急处置实施演练',
+  '应急信息上传下达演练'
+]
+
+const statusTagType = (status) => {
+  const map = { '计划中': 'warning', '进行中': 'primary', '已完成': 'success' }
+  return map[status] || 'info'
+}
+
+// ---------- Mock 数据 ----------
+let idCounter = 5
+const tableData = ref([
+  {
+    id: 1,
+    category: '指挥调度演练',
+    name: '多灾种物资装备紧急跨区调度',
+    startDate: '2025-04-10',
+    endDate: '2025-04-12',
+    base: '警械装备站 / 物资库',
+    method: '指挥推演研修班 + 模拟舱实操',
+    status: '计划中',
+    remark: ''
+  },
+  {
+    id: 2,
+    category: '资源调度演练',
+    name: '多级联动信息报送与指令传达',
+    startDate: '2025-05-15',
+    endDate: '2025-05-17',
+    base: '应急指挥中心',
+    method: '线上线下融合调度推演',
+    status: '计划中',
+    remark: ''
+  },
+  {
+    id: 3,
+    category: '辅助决策演练',
+    name: '多级联动信息报送与指令传达',
+    startDate: '2025-06-01',
+    endDate: '2025-06-03',
+    base: '综合训练基地',
+    method: '指挥推演研修班 + 模拟舱实操',
+    status: '进行中',
+    remark: ''
+  },
+  {
+    id: 4,
+    category: '应急处置实施演练',
+    name: '危化品泄漏应急处置实操',
+    startDate: '2025-06-20',
+    endDate: '2025-06-22',
+    base: '化工园区应急基地',
+    method: '现场实操 + 复盘评估',
+    status: '已完成',
+    remark: ''
+  },
+  {
+    id: 5,
+    category: '应急信息上传下达演练',
+    name: '应急信息报送与指令下发演练',
+    startDate: '2025-07-05',
+    endDate: '2025-07-06',
+    base: '通信保障站',
+    method: '通信系统实操演练',
+    status: '计划中',
+    remark: ''
+  }
+])
+
+// ---------- 弹窗 ----------
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增演练计划')
+const formRef = ref(null)
+
+const defaultForm = () => ({
+  id: null,
+  name: '',
+  category: '',
+  base: '',
+  dateRange: null,
+  method: '',
+  status: '计划中',
+  remark: ''
+})
+
+const form = ref(defaultForm())
+
+const formRules = {
+  name: [{ required: true, message: '请输入演练名称', trigger: 'blur' }],
+  category: [{ required: true, message: '请选择演练分类', trigger: 'change' }]
+}
+
+const handleAdd = () => {
+  form.value = defaultForm()
+  dialogTitle.value = '新增演练计划'
+  dialogVisible.value = true
+}
+
+const handleEdit = (row) => {
+  form.value = {
+    ...row,
+    dateRange: row.startDate ? [row.startDate, row.endDate] : null
+  }
+  dialogTitle.value = '修改演练计划'
+  dialogVisible.value = true
+}
+
+const handleSubmit = () => {
+  formRef.value?.validate((valid) => {
+    if (!valid) return
+    const data = { ...form.value }
+    if (data.dateRange && data.dateRange.length === 2) {
+      data.startDate = data.dateRange[0]
+      data.endDate = data.dateRange[1]
+    } else {
+      data.startDate = ''
+      data.endDate = ''
+    }
+    delete data.dateRange
+
+    if (data.id) {
+      const idx = tableData.value.findIndex(i => i.id === data.id)
+      if (idx !== -1) tableData.value[idx] = { ...data }
+    } else {
+      data.id = ++idCounter
+      tableData.value.unshift({ ...data })
+    }
+    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('删除成功')
+  }).catch(() => {})
+}
+</script>
+
 <style scoped lang="scss">
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+
+  .header-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #303133;
+    display: flex;
+    align-items: center;
+    gap: 6px;
 
-</style>
+    .el-icon {
+      color: #409eff;
+    }
+  }
+}
+</style>

+ 453 - 12
src/views/subSystem/dispatch/disExerciseMan/DisPracticalDrill.vue

@@ -1,19 +1,460 @@
 <!--实战演练管理-->
-<script setup>
+<template>
+  <div class="app-container">
+    <div class="drill-layout">
+      <!-- ===== 左列 ===== -->
+      <div class="col-left">
+        <!-- 演练计划模板 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><Document /></el-icon> 演练计划模板</span>
+            <el-button type="primary" size="small" @click="handleAddTemplate">新增</el-button>
+          </div>
+          <div class="panel-body">
+            <div class="template-item" v-for="tpl in templates" :key="tpl.id">
+              <div class="template-info">
+                <div class="template-name">{{ tpl.name }}</div>
+                <div class="template-date">{{ tpl.date }}</div>
+              </div>
+              <div class="template-actions">
+                <el-button link type="primary" @click="handleEditTemplate(tpl)"><el-icon><Edit /></el-icon></el-button>
+                <el-button link type="success" @click="handleUseTemplate(tpl)">使用</el-button>
+              </div>
+            </div>
+          </div>
+        </div>
 
-</script>
+        <!-- 演练计划设置 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><Setting /></el-icon> 演练计划设置</span>
+            <div class="header-actions">
+              <el-button type="primary" size="small" @click="handleAddPlan">新增计划</el-button>
+            </div>
+          </div>
+          <div class="panel-body">
+            <div class="search-bar">
+              <el-input v-model="planSearch" placeholder="按演练项目/目标查询" size="small" style="width: 160px" />
+              <el-button type="primary" size="small" @click="handleSearchPlan">查询</el-button>
+            </div>
+            <div class="plan-item" v-for="plan in plans" :key="plan.id">
+              <div class="plan-header">
+                <div class="plan-name">{{ plan.name }}</div>
+                <div class="plan-actions">
+                  <el-button link type="primary" @click="handleEditPlan(plan)"><el-icon><Edit /></el-icon></el-button>
+                  <el-button link type="danger" @click="handleDeletePlan(plan)"><el-icon><Delete /></el-icon></el-button>
+                </div>
+              </div>
+              <div class="plan-details">
+                <div class="plan-row"><span class="plan-label">目标:</span>{{ plan.target }}</div>
+                <div class="plan-row"><span class="plan-label">项目:</span>{{ plan.project }}</div>
+                <div class="plan-row"><span class="plan-label">事件:</span>{{ plan.event }}</div>
+                <div class="plan-row"><span class="plan-label">人员:</span>{{ plan.personnel }}</div>
+                <div class="plan-row">
+                  <span class="plan-label">状态:</span>
+                  <el-tag :type="plan.status === '进行中' ? 'warning' : 'success'" size="small">{{ plan.status }}</el-tag>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
 
-<template>
-<div>5.2.3.3.4. 实战演练管理
-  除虚拟演练之外,系统还提供对实战演练的管理功能。具体功能包括:
-  (1)演练计划设置
-  提供演练计划模板,计划内容包括演练目标、演练项目、演练事件、演练人员、完成时间等。
-  (2)结果分析与评估
-  对实际操作演练的结果进行记录,其内容与真实发生的事件记录相同,包括监测监控信息、
-  接警信息、决策信息、指挥调度信息、对外发布信息等。可将演练结果信息发送给统计分析子系
-  统和应急评估子系统,进行本次演练的统计分析和结果评估。</div>
+      <!-- ===== 中列 + 右列合并区域 ===== -->
+      <div class="col-right">
+        <!-- 演练结果记录与分析评估 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><DataAnalysis /></el-icon> 演练结果记录与分析评估</span>
+            <el-button type="warning" size="small" @click="sendToSubsystem">
+              发送至统计分析子系统 & 应急评估子系统
+            </el-button>
+          </div>
+          <div class="panel-body">
+            <div class="info-grid">
+              <!-- 监测监控信息 -->
+              <div class="info-card">
+                <div class="info-title"><el-icon><Monitor /></el-icon> 监测监控信息</div>
+                <div class="info-content">
+                  <div class="info-line">烟感传感器报警:化工园区储罐区3号罐</div>
+                  <div class="info-line">热成像检测到火源:储罐区西侧</div>
+                  <div class="info-line">风速监测:12 m/s,风向东南</div>
+                </div>
+              </div>
+
+              <!-- 接警信息 -->
+              <div class="info-card">
+                <div class="info-title"><el-icon><Phone /></el-icon> 接警信息</div>
+                <div class="info-content">
+                  <div class="info-line">时间:2025-06-18 09:32:21</div>
+                  <div class="info-line">报警中心:安保值班室 张强</div>
+                  <div class="info-line">地点:化工园区储罐区</div>
+                </div>
+              </div>
+
+              <!-- 指挥调度信息 -->
+              <div class="info-card">
+                <div class="info-title"><el-icon><Guide /></el-icon> 指挥调度信息</div>
+                <div class="info-content">
+                  <div class="info-line">调派:3个消防中队 + 1个危化品处置小组</div>
+                  <div class="info-line">已设置现场指挥部</div>
+                  <div class="info-line">暂无人员伤亡报告</div>
+                </div>
+              </div>
+
+              <!-- 决策信息 -->
+              <div class="info-card">
+                <div class="info-title"><el-icon><Operation /></el-icon> 决策信息</div>
+                <div class="info-content">
+                  <div class="info-line">总指挥下达命令:启动一级应急响应</div>
+                  <div class="info-line">疏散周边500m范围内人员</div>
+                  <div class="info-line">请求消防增援</div>
+                </div>
+              </div>
+
+              <!-- 对外发布信息 -->
+              <div class="info-card info-card-full">
+                <div class="info-title"><el-icon><ChatDotRound /></el-icon> 对外发布信息</div>
+                <div class="info-content">
+                  <div class="info-line">官方发布:"事故已控制,无人员伤亡,提醒周边居民关闭门窗。"</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 统计分析子系统 结果 -->
+        <div class="panel">
+          <div class="panel-header">
+            <span><el-icon><TrendCharts /></el-icon> 统计分析子系统 结果</span>
+          </div>
+          <div class="panel-body">
+            <div class="analysis-result">
+              <div class="analysis-time">分析时间: 16:50:52</div>
+              <div class="analysis-detail">
+                <div class="detail-row"><span class="detail-label">技能:</span>灭火、堵漏</div>
+                <div class="detail-row"><span class="detail-label">装备:</span>防护服、灭火器</div>
+              </div>
+              <div class="analysis-eval">
+                <div class="eval-title">评估结果</div>
+                <div class="eval-grid">
+                  <div class="eval-item">
+                    <div class="eval-label">任务完成率</div>
+                    <div class="eval-value">67%</div>
+                  </div>
+                  <div class="eval-item">
+                    <div class="eval-label">响应时效</div>
+                    <div class="eval-value">达标</div>
+                  </div>
+                  <div class="eval-item">
+                    <div class="eval-label">协同评分</div>
+                    <div class="eval-value">良好</div>
+                  </div>
+                  <div class="eval-item">
+                    <div class="eval-label">综合等级</div>
+                    <div class="eval-value highlight">良好</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
+<script setup>
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  Document, Edit, Delete, Setting, DataAnalysis, Monitor,
+  Phone, Guide, Operation, ChatDotRound, TrendCharts
+} from '@element-plus/icons-vue'
+
+// ---------- 模板 ----------
+const templates = ref([
+  { id: 1, name: '化工泄漏实战演练模板', date: '2025-01-01' },
+  { id: 2, name: '地震救援实战演练模板', date: '2025-01-01' }
+])
+
+const handleAddTemplate = () => { ElMessage.success('新增演练模板(Mock)') }
+const handleEditTemplate = (tpl) => { ElMessage.success(`编辑模板: ${tpl.name}`) }
+const handleUseTemplate = (tpl) => { ElMessage.success(`已应用模板: ${tpl.name}`) }
+
+// ---------- 计划 ----------
+const planSearch = ref('')
+const plans = ref([
+  {
+    id: 1,
+    name: '2025夏季危化品泄漏实战演练',
+    target: '提升泄漏封堵效率',
+    project: '储罐区泄漏及人员疏散',
+    event: '泄漏扩大',
+    personnel: '指挥员张立、消防队12人',
+    status: '进行中'
+  },
+  {
+    id: 2,
+    name: '洪涝灾害救援演练',
+    target: '提升水上救援能力',
+    project: '城市内涝人员转移',
+    event: '暴雨持续水位上涨',
+    personnel: '指挥员王伟、救援队8人',
+    status: '已完成'
+  },
+  {
+    id: 3,
+    name: '化工泄漏实战演练 衍生计划',
+    target: '检验应急预案有效性',
+    project: '危化品储罐区综合演练',
+    event: '泄漏+火灾叠加',
+    personnel: '指挥员李明、消防队15人',
+    status: '进行中'
+  }
+])
+
+const handleAddPlan = () => { ElMessage.success('新增演练计划(Mock)') }
+const handleEditPlan = (plan) => { ElMessage.success(`编辑计划: ${plan.name}`) }
+const handleDeletePlan = (plan) => {
+  ElMessageBox.confirm('确认删除该计划?', '提示', { type: 'warning' }).then(() => {
+    plans.value = plans.value.filter(p => p.id !== plan.id)
+    ElMessage.success('删除成功')
+  }).catch(() => {})
+}
+const handleSearchPlan = () => { ElMessage.success('查询计划(Mock)') }
+
+// ---------- 发送至子系统 ----------
+const sendToSubsystem = () => {
+  ElMessage.success('已发送至统计分析子系统 & 应急评估子系统')
+}
+</script>
+
 <style scoped lang="scss">
+.drill-layout {
+  display: flex;
+  gap: 12px;
+  min-height: calc(100vh - 160px);
+}
+
+.col-left {
+  width: 25%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.col-right {
+  width: 75%;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.panel {
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  background: #fff;
+  overflow: hidden;
+
+  .panel-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 14px;
+    background: #f5f7fa;
+    border-bottom: 1px solid #e4e7ed;
+    font-weight: bold;
+    font-size: 14px;
+    color: #303133;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    span {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    .el-icon {
+      color: #409eff;
+    }
+  }
+
+  .panel-body {
+    padding: 12px 14px;
+  }
+}
+
+// 模板列表
+.template-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  border-bottom: 1px dashed #ebeef5;
+
+  &:last-child { border-bottom: none; }
+
+  .template-info {
+    .template-name { font-size: 13px; color: #303133; font-weight: 500; }
+    .template-date { font-size: 12px; color: #909399; margin-top: 2px; }
+  }
+
+  .template-actions {
+    display: flex;
+    gap: 4px;
+    align-items: center;
+  }
+}
+
+// 计划搜索
+.search-bar {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+// 计划卡片
+.plan-item {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 10px;
+  margin-bottom: 8px;
+  background: #fafafa;
+
+  &:last-child { margin-bottom: 0; }
+
+  .plan-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 6px;
+
+    .plan-name {
+      font-size: 13px;
+      font-weight: bold;
+      color: #303133;
+    }
+
+    .plan-actions {
+      display: flex;
+      gap: 2px;
+    }
+  }
+
+  .plan-details {
+    .plan-row {
+      font-size: 12px;
+      color: #606266;
+      line-height: 1.8;
+
+      .plan-label {
+        color: #909399;
+      }
+    }
+  }
+}
+
+// 信息卡片网格
+.info-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 10px;
+}
+
+.info-card {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 12px;
+  background: #fafafa;
+
+  &.info-card-full {
+    grid-column: 1 / -1;
+  }
+
+  .info-title {
+    font-size: 13px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+
+    .el-icon { color: #409eff; }
+  }
+
+  .info-content {
+    .info-line {
+      font-size: 12px;
+      color: #606266;
+      line-height: 1.8;
+    }
+  }
+}
+
+// 统计分析
+.analysis-result {
+  .analysis-time {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 10px;
+  }
+
+  .analysis-detail {
+    margin-bottom: 12px;
+    padding: 10px;
+    border: 1px solid #ebeef5;
+    border-radius: 4px;
+    background: #fafafa;
+
+    .detail-row {
+      font-size: 13px;
+      color: #606266;
+      line-height: 1.8;
+
+      .detail-label {
+        color: #909399;
+      }
+    }
+  }
+
+  .analysis-eval {
+    .eval-title {
+      font-size: 13px;
+      font-weight: bold;
+      color: #303133;
+      margin-bottom: 8px;
+    }
+
+    .eval-grid {
+      display: grid;
+      grid-template-columns: repeat(4, 1fr);
+      gap: 10px;
+    }
+
+    .eval-item {
+      border: 1px solid #ebeef5;
+      border-radius: 4px;
+      padding: 10px;
+      text-align: center;
+
+      .eval-label {
+        font-size: 12px;
+        color: #909399;
+        margin-bottom: 4px;
+      }
+
+      .eval-value {
+        font-size: 16px;
+        font-weight: bold;
+        color: #303133;
 
-</style>
+        &.highlight { color: #67c23a; }
+      }
+    }
+  }
+}
+</style>

+ 228 - 10
src/views/subSystem/dispatch/disPlanManage/DisEventGrade.vue

@@ -1,15 +1,233 @@
-<!--事件分级-->
-<script setup>
+<!-- 应急事件分级 -->
+<template>
+  <div class="app-container">
+    <!-- 搜索区 -->
+    <el-form :model="queryParams" :inline="true" class="search-form">
+      <el-form-item label="事件类别">
+        <el-select v-model="queryParams.category" placeholder="请选择" clearable style="width: 160px">
+          <el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="事件等级">
+        <el-select v-model="queryParams.level" placeholder="请选择" clearable style="width: 140px">
+          <el-option v-for="item in levelOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="事件状态">
+        <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 120px">
+          <el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleSearch">搜索</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </el-form-item>
+      <el-form-item style="float: right">
+        <el-button type="success" @click="handleAdd">新增</el-button>
+        <el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
+        <el-button type="primary" @click="handleExport">导出</el-button>
+      </el-form-item>
+    </el-form>
 
-</script>
+    <!-- 数据表格 -->
+    <el-table :data="pageList" border stripe @selection-change="handleSelectionChange" style="width: 100%">
+      <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="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">
+        <template #default="{ row }">
+          <el-tag :type="row.status === '启用' ? 'success' : 'info'">{{ row.status }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="140" 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>
+        </template>
+      </el-table-column>
+    </el-table>
 
-<template>
-<div>5.2.3.2.1. 应急事件分级
-  系统实现辅助业务部门建立时间分级分类标准信息并录入系统。实现分级分类标准信息的建
-  立,以表格的方式保存显示分类分级标准,可以通过不同字段自由组合查询,可以对其新增编辑
-  修改删除操作。</div>
+    <!-- 分页 -->
+    <el-pagination
+      style="margin-top: 16px; justify-content: flex-end"
+      v-model:current-page="pagination.page"
+      v-model:page-size="pagination.size"
+      :page-sizes="[10, 20, 50]"
+      :total="filteredList.length"
+      layout="total, sizes, prev, pager, next, jumper"
+      @size-change="onSizeChange"
+      @current-change="onPageChange"
+    />
+
+    <!-- 新增/修改弹窗 -->
+    <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-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-input v-model="form.subCategory" placeholder="请输入类别细分" />
+        </el-form-item>
+        <el-form-item label="分级分类判断标准" prop="criteria">
+          <el-input
+            v-model="form.criteria"
+            type="textarea"
+            :rows="4"
+            placeholder="填写具体判断规则、适用条件、划分依据"
+          />
+        </el-form-item>
+        <el-form-item label="标准生效状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio label="启用">启用</el-radio>
+            <el-radio label="停用">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注说明">
+          <el-input v-model="form.remark" placeholder="请输入备注说明" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存提交</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
-<style scoped lang="scss">
+<script setup>
+import { ref, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+// ---------- 下拉选项 ----------
+const categoryOptions = ['管网泄漏', '路面积水', '设施损坏', '管道堵塞']
+const levelOptions = ['一级', '二级', '三级', '四级']
+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 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)
+})
 
-</style>
+const onSizeChange = () => { pagination.value.page = 1 }
+const onPageChange = () => {}
+
+// ---------- 搜索 / 重置 ----------
+const handleSearch = () => { pagination.value.page = 1 }
+const handleReset = () => {
+  queryParams.value = { category: '', level: '', status: '' }
+  pagination.value.page = 1
+}
+
+// ---------- 多选 ----------
+const selectedRows = ref([])
+const handleSelectionChange = (rows) => { selectedRows.value = rows }
+
+// ---------- 弹窗 ----------
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增')
+const formRef = ref(null)
+const defaultForm = () => ({
+  id: null, category: '', level: '', subCategory: '', criteria: '', status: '启用', remark: ''
+})
+const form = ref(defaultForm())
+
+const formRules = {
+  category: [{ required: true, message: '请输入事件类别', trigger: 'blur' }],
+  level: [{ required: true, message: '请选择事件等级', trigger: 'change' }],
+  criteria: [{ required: true, message: '请填写判断标准', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }],
+}
+
+const handleAdd = () => {
+  form.value = defaultForm()
+  dialogTitle.value = '新增'
+  dialogVisible.value = true
+}
+
+const handleEdit = (row) => {
+  form.value = { ...row }
+  dialogTitle.value = '修改'
+  dialogVisible.value = true
+}
+
+const handleSubmit = () => {
+  formRef.value?.validate((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 })
+    }
+    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('删除成功')
+  }).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('删除成功')
+  }).catch(() => {})
+}
+
+// ---------- 导出 ----------
+const handleExport = () => { ElMessage.success('导出成功(Mock)') }
+</script>
+
+<style scoped lang="scss">
+.search-form {
+  margin-bottom: 12px;
+  :deep(.el-form-item) {
+    margin-bottom: 8px;
+  }
+}
+</style>

+ 403 - 11
src/views/subSystem/dispatch/disPlanManage/DisPlanApproval.vue

@@ -1,18 +1,410 @@
-
-<!-- 预案审批 -->
+<!-- 应急预案审批 -->
 <template>
-<div>5.2.3.2.3. 应急预案审批
-  建立应急预案在线审批机制,通过领导、专家的评审和修正保证应急预案的先进性、科学性、高效性,并对评审过程及评审结果进行记录,提高应急处置的快速响应能力。具体功能如下:(1)审批流程设置
-  建立预案的审批环节、审批人、审批权限、审批流转的全审批流程。(2)预案在线审批
-  对预案的审批人员、审批记录、审批意见进行管理。
-  (3)预案调整管理
-  根据预案审批结果,对预案的内容进行调整。
-  (4)预案发布
-  对通过审批的预案进行发布,只有发布的预案才能在应急处置中启动。</div>
+  <div class="app-container">
+    <!-- 搜索区 -->
+    <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-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleSearch">搜索</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 数据表格 -->
+    <el-table :data="pageList" border stripe style="width: 100%">
+      <el-table-column label="序号" type="index" width="60" align="center" />
+      <el-table-column prop="planName" label="预案名称" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="planType" label="预案类型" width="130" />
+      <el-table-column prop="planLevel" label="预案级别" width="100" align="center" />
+      <el-table-column prop="reviewers" label="审批人员" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="auditRecord" label="审批记录/意见" min-width="240" show-overflow-tooltip />
+      <el-table-column prop="status" label="状态" width="100" align="center">
+        <template #default="{ row }">
+          <el-tag :type="statusTagType(row.status)">{{ row.status }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="130" align="center">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="handleView(row)">查看</el-button>
+          <el-button link type="primary" @click="handleApprove(row)">审批</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <el-pagination
+      style="margin-top: 16px; justify-content: flex-end"
+      v-model:current-page="pagination.page"
+      v-model:page-size="pagination.size"
+      :page-sizes="[10, 20, 50]"
+      :total="filteredList.length"
+      layout="total, sizes, prev, pager, next, jumper"
+      @size-change="onSizeChange"
+      @current-change="onPageChange"
+    />
+
+    <!-- 查看详情弹窗 -->
+    <el-dialog v-model="detailVisible" title="预案详情" width="720px" destroy-on-close>
+      <div class="detail-section">
+        <div class="section-title">预案基本信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="预案名称">{{ currentRow.planName }}</el-descriptions-item>
+          <el-descriptions-item label="预案类型">{{ currentRow.planType }}</el-descriptions-item>
+          <el-descriptions-item label="预案等级">{{ currentRow.planLevel }}</el-descriptions-item>
+          <el-descriptions-item label="审批人员">{{ currentRow.reviewers }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- 审批流程 2x2 卡片 -->
+      <div class="detail-section">
+        <div class="section-title">审批流程</div>
+        <div class="flow-grid">
+          <div class="flow-card blue">
+            <div class="flow-step">环节一:预案起草人提交</div>
+            <div class="flow-info">审批人:安全总监 · 权限:初审/退回</div>
+          </div>
+          <div class="flow-card green">
+            <div class="flow-step">环节二:专家技术评审</div>
+            <div class="flow-info">审批人:专家组(3人) · 权限:修正建议/通过</div>
+          </div>
+          <div class="flow-card blue">
+            <div class="flow-step">环节三:领导层终审</div>
+            <div class="flow-info">审批人:应急总指挥 · 权限:批准/驳回</div>
+          </div>
+          <div class="flow-card yellow">
+            <div class="flow-step">环节四:签发与归档</div>
+            <div class="flow-info">状态:完成</div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 预案调整 -->
+      <div class="detail-section">
+        <div class="section-title">预案调整</div>
+        <div class="adjust-box">
+          <div class="adjust-title">{{ currentRow.planName }} V{{ currentRow.version || '2.1' }}</div>
+          <div class="adjust-content">
+            根据刘宏专家评审意见,优化预警阈值及撤离路线;增加物资调度流程。
+            评审结果:"科学性提升,建议补充应急通讯保障" — 已修正。
+          </div>
+        </div>
+      </div>
+
+      <!-- 预案发布 -->
+      <div class="detail-section">
+        <div class="section-title">预案发布</div>
+        <div class="publish-box">
+          <template v-if="currentRow.status === '已发布'">
+            <el-tag type="success" size="large">已发布</el-tag>
+          </template>
+          <template v-else>
+            <div class="unpublished-tip">未发布预案(应急处置不可用)</div>
+            <el-button type="success" @click="handlePublish">发布</el-button>
+          </template>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 审批弹窗 -->
+    <el-dialog v-model="approveVisible" title="审批流程" width="600px" destroy-on-close>
+      <!-- 案件基本信息 -->
+      <div class="approve-header">
+        <el-descriptions :column="1" border>
+          <el-descriptions-item label="案件名称">{{ currentRow.planName }}</el-descriptions-item>
+          <el-descriptions-item label="案件类型">{{ currentRow.planType }}</el-descriptions-item>
+          <el-descriptions-item label="案件等级">{{ currentRow.planLevel }}</el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <!-- 审批环节列表 -->
+      <div class="approve-steps">
+        <div v-for="(step, idx) in approveSteps" :key="idx" :class="['approve-step-card', step.colorClass]">
+          <div class="step-header">
+            <span class="step-dot"></span>
+            <span class="step-title">{{ step.title }}</span>
+          </div>
+          <div class="step-info">审批人:{{ step.approver }} · 权限:{{ step.permission }}</div>
+          <div class="step-opinion">
+            <span class="opinion-label">审批意见:</span>
+            <el-input v-model="step.opinion" type="textarea" :rows="2" placeholder="请输入审批意见" />
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <el-button type="danger" @click="handleReject">驳回</el-button>
+        <el-button type="primary" @click="handleComplete">完成</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup>
+import { ref, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+// ---------- 查询 ----------
+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 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)
+})
+const onSizeChange = () => { pagination.value.page = 1 }
+const onPageChange = () => {}
+
+// ---------- 搜索 / 重置 ----------
+const handleSearch = () => { pagination.value.page = 1 }
+const handleReset = () => {
+  queryParams.value.name = ''
+  pagination.value.page = 1
+}
+
+// ---------- 状态 Tag 类型 ----------
+const statusTagType = (status) => {
+  if (status === '终审中') return 'warning'
+  if (status === '待修正') return 'danger'
+  if (status === '已发布') return 'success'
+  return 'info'
+}
+
+// ---------- 当前操作行 ----------
+const currentRow = ref({})
+
+// ---------- 查看详情弹窗 ----------
+const detailVisible = ref(false)
+const handleView = (row) => {
+  currentRow.value = { ...row }
+  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 = '已发布'
+      currentRow.value.status = '已发布'
+    }
+    ElMessage.success('发布成功')
+  }).catch(() => {})
+}
+
+// ---------- 审批弹窗 ----------
+const approveVisible = ref(false)
+const approveSteps = ref([])
+
+const handleApprove = (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: '' },
+  ]
+  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 handleComplete = () => {
+  approveVisible.value = false
+  const idx = tableData.value.findIndex(i => i.id === currentRow.value.id)
+  if (idx !== -1) tableData.value[idx].status = '终审中'
+  ElMessage.success('审批完成')
+}
 </script>
+
 <style scoped lang="scss">
+.search-form {
+  margin-bottom: 12px;
+  :deep(.el-form-item) {
+    margin-bottom: 8px;
+  }
+}
+
+/* ---- 查看详情样式 ---- */
+.detail-section {
+  margin-bottom: 20px;
+
+  .section-title {
+    font-weight: bold;
+    font-size: 15px;
+    margin-bottom: 10px;
+    padding-left: 8px;
+    border-left: 3px solid #409eff;
+  }
+}
+
+.flow-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12px;
+}
+
+.flow-card {
+  border-radius: 6px;
+  padding: 14px 16px;
+  color: #fff;
+
+  .flow-step {
+    font-weight: bold;
+    font-size: 14px;
+    margin-bottom: 6px;
+  }
+
+  .flow-info {
+    font-size: 13px;
+    opacity: 0.9;
+  }
+
+  &.blue { background: #409eff; }
+  &.green { background: #67c23a; }
+  &.yellow { background: #e6a23c; }
+}
+
+.adjust-box {
+  background: #fafafa;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 12px 16px;
+
+  .adjust-title {
+    font-weight: bold;
+    margin-bottom: 6px;
+  }
+
+  .adjust-content {
+    font-size: 13px;
+    color: #606266;
+    line-height: 1.6;
+  }
+}
+
+.publish-box {
+  background: #f0f9eb;
+  border: 1px solid #e1f3d8;
+  border-radius: 4px;
+  padding: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .unpublished-tip {
+    font-size: 14px;
+    color: #e6a23c;
+    font-weight: bold;
+  }
+}
+
+/* ---- 审批弹窗样式 ---- */
+.approve-header {
+  margin-bottom: 16px;
+}
+
+.approve-steps {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.approve-step-card {
+  border-radius: 6px;
+  padding: 14px 16px;
+
+  .step-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 6px;
+
+    .step-dot {
+      display: inline-block;
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      margin-right: 8px;
+      background: #fff;
+      border: 2px solid #fff;
+    }
+
+    .step-title {
+      font-weight: bold;
+      font-size: 14px;
+      color: #fff;
+    }
+  }
+
+  .step-info {
+    font-size: 13px;
+    color: rgba(255, 255, 255, 0.9);
+    margin-bottom: 8px;
+  }
+
+  .step-opinion {
+    .opinion-label {
+      font-size: 13px;
+      color: #fff;
+      display: block;
+      margin-bottom: 4px;
+    }
+  }
+
+  &.blue {
+    background: #409eff;
+    .step-dot { border-color: #fff; }
+  }
 
-</style>
+  &.green {
+    background: #67c23a;
+    .step-dot { border-color: #fff; }
+  }
+}
+</style>

+ 309 - 25
src/views/subSystem/dispatch/disPlanManage/DisPlanEditing.vue

@@ -1,31 +1,315 @@
-<!--预案编辑-->
-<script setup>
+<!-- 应急预案编辑 -->
+<template>
+  <div class="app-container">
+    <!-- 顶部操作区 -->
+    <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-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleSearch">搜索</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </el-form-item>
+      <el-form-item style="float: right">
+        <el-button type="success" @click="handleAdd">新增</el-button>
+        <el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
+        <el-button type="primary" @click="handleExport">导出</el-button>
+        <el-button type="primary" @click="handleDownloadTemplate">下载模版</el-button>
+      </el-form-item>
+    </el-form>
 
-</script>
+    <!-- 主体:左侧树 + 右侧表格 -->
+    <div class="main-layout">
+      <!-- 左侧预案分类树 -->
+      <div class="left-tree">
+        <div class="tree-title">预案分类</div>
+        <el-tree
+          ref="treeRef"
+          :data="treeData"
+          :props="treeProps"
+          show-checkbox
+          node-key="id"
+          default-expand-all
+          highlight-current
+          @node-click="handleNodeClick"
+        />
+      </div>
 
-<template>
-<div>5.2.3.2.2. 应急预案编辑
-  将各级预案按预案等级、类型等组织起来,方便相关人员在应急过程中快速定位相关预案内
-  容,提供对应急预案的增加、删除、修改、查询等功能。具体功能如下:
-  (1)预案分类树管理
-  实现预案分类管理,包括分类树的增加、修改和删除功能。
-  (2)预案模板管理
-  实现预案模板的管理,保证预案的严谨、规范。
-  (3)预案录入
-  实现预案录入,信息包括预案名称、预案文本内容、分类、等级、版本、编制单位、发布时间、
-  发布单位等信息。
-  (4)预案查询
-  实现预案的查询、查看;包括按名称、类型、编辑单位等属性进行查询,也支持组合查询和
-  模糊查询。
-  (5)预案修改
-  实现预案的修改、删除操作;实现预案名称、预案文本内容、分类、版本、编制单位、发布
-  时间、发布单位等属性的修改。
-  (6)预案版本管理
-  对应急预案的版本进行管理。
-  (7)预案附件管理
-  该模块实现对预案附件的上传、下载、预览、打印及删除等操作。</div>
+      <!-- 右侧表格 -->
+      <div class="right-table">
+        <el-table :data="pageList" border stripe @selection-change="handleSelectionChange" style="width: 100%">
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="序号" type="index" width="60" align="center" />
+          <el-table-column prop="planName" label="预案名称" min-width="180" show-overflow-tooltip />
+          <el-table-column prop="planType" label="预案类型" width="130" />
+          <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>
+          </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">
+            <template #default="{ row }">
+              <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
+              <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <el-pagination
+          style="margin-top: 16px; justify-content: flex-end"
+          v-model:current-page="pagination.page"
+          v-model:page-size="pagination.size"
+          :page-sizes="[10, 20, 50]"
+          :total="filteredList.length"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="onSizeChange"
+          @current-change="onPageChange"
+        />
+      </div>
+    </div>
+
+    <!-- 新增/修改预案弹窗 -->
+    <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="planName">
+          <el-input v-model="form.planName" placeholder="请输入预案名称" />
+        </el-form-item>
+        <el-form-item label="预案级别" prop="planLevel">
+          <el-select v-model="form.planLevel" 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="planType">
+          <el-input v-model="form.planType" placeholder="请输入预案类型" />
+        </el-form-item>
+        <el-form-item label="预案版本" prop="version">
+          <el-input v-model="form.version" placeholder="请输入版本号" />
+        </el-form-item>
+        <el-form-item label="编制单位" prop="compileUnit">
+          <el-input v-model="form.compileUnit" placeholder="请输入编制单位" />
+        </el-form-item>
+        <el-form-item label="附件上传">
+          <el-upload
+            class="upload-area"
+            action="#"
+            :auto-upload="false"
+            :limit="1"
+            accept=".doc"
+            :on-exceed="() => ElMessage.warning('只能上传一个文件')"
+            :file-list="form.fileList"
+            @change="handleFileChange"
+          >
+            <el-button type="primary">添加文件</el-button>
+          </el-upload>
+          <div class="upload-tip">只能上传文件类型为.doc的文件,且不超过500kb</div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存提交</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
+<script setup>
+import { ref, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Download } from '@element-plus/icons-vue'
+
+// ---------- 常量 ----------
+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 selectedTreeNode = ref(null)
+const handleNodeClick = (node) => { selectedTreeNode.value = node }
+
+// ---------- 查询 ----------
+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 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)
+})
+const onSizeChange = () => { pagination.value.page = 1 }
+const onPageChange = () => {}
+
+// ---------- 搜索 / 重置 ----------
+const handleSearch = () => { pagination.value.page = 1 }
+const handleReset = () => {
+  queryParams.value.name = ''
+  pagination.value.page = 1
+}
+
+// ---------- 多选 ----------
+const selectedRows = ref([])
+const handleSelectionChange = (rows) => { selectedRows.value = rows }
+
+// ---------- 弹窗 ----------
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增预案')
+const formRef = ref(null)
+const defaultForm = () => ({
+  id: null, planName: '', planLevel: '', planType: '', version: '1.0', compileUnit: '', attachName: '', fileList: []
+})
+const form = ref(defaultForm())
+
+const formRules = {
+  planName: [{ required: true, message: '请输入预案名称', trigger: 'blur' }],
+  planLevel: [{ required: true, message: '请选择预案级别', trigger: 'change' }],
+  planType: [{ required: true, message: '请输入预案类型', trigger: 'blur' }],
+}
+
+const handleAdd = () => {
+  form.value = defaultForm()
+  dialogTitle.value = '新增预案'
+  dialogVisible.value = true
+}
+
+const handleEdit = (row) => {
+  form.value = { ...row, fileList: row.attachName ? [{ name: row.attachName, status: 'success' }] : [] }
+  dialogTitle.value = '修改预案'
+  dialogVisible.value = true
+}
+
+const handleFileChange = (file) => {
+  if (file && file.name) {
+    form.value.attachName = file.name
+    form.value.fileList = [file]
+  }
+}
+
+const handleSubmit = () => {
+  formRef.value?.validate((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 })
+    }
+    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('删除成功')
+  }).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('删除成功')
+  }).catch(() => {})
+}
+
+// ---------- 下载 / 导出 ----------
+const handleDownload = () => { ElMessage.success('附件下载(Mock)') }
+const handleExport = () => { ElMessage.success('导出成功(Mock)') }
+const handleDownloadTemplate = () => { ElMessage.success('模版下载成功(Mock)') }
+</script>
+
 <style scoped lang="scss">
+.search-form {
+  margin-bottom: 12px;
+  :deep(.el-form-item) {
+    margin-bottom: 8px;
+  }
+}
+
+.main-layout {
+  display: flex;
+  gap: 16px;
+}
+
+.left-tree {
+  width: 25%;
+  min-width: 200px;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 12px;
+  flex-shrink: 0;
+
+  .tree-title {
+    font-weight: bold;
+    font-size: 15px;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+    border-bottom: 1px solid #ebeef5;
+  }
+}
+
+.right-table {
+  flex: 1;
+  min-width: 0;
+}
 
-</style>
+.upload-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+</style>