|
@@ -1,60 +1,147 @@
|
|
|
-<!--实战演练管理-->
|
|
|
|
|
|
|
+<!--实战演练管理 - 基于业务场景的优化版-->
|
|
|
<template>
|
|
<template>
|
|
|
- <div>
|
|
|
|
|
- <div class="app-container">
|
|
|
|
|
- <div class="drill-layout">
|
|
|
|
|
|
|
+ <div class="practical-drill-container">
|
|
|
|
|
+ <!-- 顶部操作栏 -->
|
|
|
|
|
+ <div class="drill-top-bar">
|
|
|
|
|
+ <div class="top-left">
|
|
|
|
|
+ <h2 class="page-title"><el-icon><TrendCharts /></el-icon>实战演练管理</h2>
|
|
|
|
|
+ <el-tag type="warning" size="large">5.2.3.3.4 实战演练管理</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="top-right">
|
|
|
|
|
+ <el-button type="primary" size="large" @click="handleCreatePlan">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>创建实战演练
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button size="large" @click="showAnalysisReport">
|
|
|
|
|
+ <el-icon><Document /></el-icon>分析报表
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="drill-main-layout">
|
|
|
<!-- ===== 左列 ===== -->
|
|
<!-- ===== 左列 ===== -->
|
|
|
- <div class="col-left">
|
|
|
|
|
|
|
+ <div class="left-column">
|
|
|
<!-- 演练计划模板 -->
|
|
<!-- 演练计划模板 -->
|
|
|
- <div class="panel">
|
|
|
|
|
|
|
+ <div class="panel template-panel">
|
|
|
<div class="panel-header">
|
|
<div class="panel-header">
|
|
|
<span><el-icon><Document /></el-icon> 演练计划模板</span>
|
|
<span><el-icon><Document /></el-icon> 演练计划模板</span>
|
|
|
- <el-button type="primary" size="small" @click="handleAddTemplate">新增</el-button>
|
|
|
|
|
|
|
+ <el-button type="primary" size="small" @click="openTemplateDialog">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>新增模板
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="panel-body">
|
|
<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>
|
|
|
|
|
- <el-button link type="danger" @click="handleDeleteTemplate(tpl)"><el-icon><Delete /></el-icon></el-button>
|
|
|
|
|
|
|
+ <div class="template-list">
|
|
|
|
|
+ <el-empty v-if="templates.length === 0" description="暂无演练模板" :image-size="80" />
|
|
|
|
|
+ <div v-for="tpl in templates" :key="tpl.id" class="template-card">
|
|
|
|
|
+ <div class="template-card-header">
|
|
|
|
|
+ <div class="template-name">{{ tpl.name }}</div>
|
|
|
|
|
+ <el-tag :type="getCategoryTagType(tpl.category)" size="small">{{ getCategoryLabel(tpl.category) }}</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="template-meta">
|
|
|
|
|
+ <span class="meta-item"><el-icon><Calendar /></el-icon>{{ tpl.createDate || '未知日期' }}</span>
|
|
|
|
|
+ <span class="meta-item"><el-icon><User /></el-icon>{{ tpl.userCount || 0 }}人</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="template-actions">
|
|
|
|
|
+ <el-button link type="primary" size="small" @click.stop="openEditTemplate(tpl)">
|
|
|
|
|
+ <el-icon><Edit /></el-icon>编辑
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button link type="success" size="small" @click.stop="applyTemplate(tpl)">
|
|
|
|
|
+ <el-icon><SwitchButton /></el-icon>应用
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button link type="danger" size="small" @click.stop="confirmDeleteTemplate(tpl)">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>删除
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 演练计划设置 -->
|
|
<!-- 演练计划设置 -->
|
|
|
- <div class="panel">
|
|
|
|
|
|
|
+ <div class="panel plan-panel">
|
|
|
<div class="panel-header">
|
|
<div class="panel-header">
|
|
|
<span><el-icon><Setting /></el-icon> 演练计划设置</span>
|
|
<span><el-icon><Setting /></el-icon> 演练计划设置</span>
|
|
|
<div class="header-actions">
|
|
<div class="header-actions">
|
|
|
- <el-button type="primary" size="small" @click="handleAddPlan">新增计划</el-button>
|
|
|
|
|
|
|
+ <el-select v-model="planFilterStatus" placeholder="筛选状态" size="small" style="width: 90px; margin-right: 5px" clearable>
|
|
|
|
|
+ <el-option label="全部" value="" />
|
|
|
|
|
+ <el-option label="计划中" value="计划中" />
|
|
|
|
|
+ <el-option label="进行中" value="进行中" />
|
|
|
|
|
+ <el-option label="已完成" value="已完成" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-button type="primary" size="small" @click="openPlanDialog">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>新建计划
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="panel-body">
|
|
<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 class="search-filter">
|
|
|
|
|
+ <el-input v-model="planSearchText" placeholder="搜索演练名称/目标/项目" size="small" style="width: 200px" clearable>
|
|
|
|
|
+ <template #prefix><el-icon><Search /></el-icon></template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ <el-button type="primary" size="small" @click="filterPlans">
|
|
|
|
|
+ <el-icon><Search /></el-icon>查询
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</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 class="plan-list">
|
|
|
|
|
+ <el-empty v-if="filteredPlans.length === 0" description="暂无演练计划" :image-size="80" />
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="plan in filteredPlans"
|
|
|
|
|
+ :key="plan.id"
|
|
|
|
|
+ :class="['plan-card', { active: selectedPlanId === plan.id }]"
|
|
|
|
|
+ @click="selectPlan(plan)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="plan-status-badge" :class="plan.status">{{ plan.status }}</div>
|
|
|
|
|
+ <div class="plan-card-content">
|
|
|
|
|
+ <h4 class="plan-title">{{ plan.name }}</h4>
|
|
|
|
|
+ <div class="plan-category">
|
|
|
|
|
+ <el-tag :type="getCategoryTagType(plan.category)" size="small">{{ getCategoryLabel(plan.category) }}</el-tag>
|
|
|
|
|
+ <span class="plan-date">{{ plan.endTime || plan.updateTime || '—' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="plan-info-list">
|
|
|
|
|
+ <div class="info-item" v-if="plan.target">
|
|
|
|
|
+ <el-icon><Flag /></el-icon>
|
|
|
|
|
+ <span>目标:</span>
|
|
|
|
|
+ <span class="info-text">{{ plan.target }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item" v-if="plan.project">
|
|
|
|
|
+ <el-icon><List /></el-icon>
|
|
|
|
|
+ <span>项目:</span>
|
|
|
|
|
+ <span class="info-text">{{ plan.project }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item" v-if="plan.eventDesc">
|
|
|
|
|
+ <el-icon><Bell /></el-icon>
|
|
|
|
|
+ <span>事件:</span>
|
|
|
|
|
+ <span class="info-text">{{ plan.eventDesc }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item" v-if="plan.personnel">
|
|
|
|
|
+ <el-icon><User /></el-icon>
|
|
|
|
|
+ <span>人员:</span>
|
|
|
|
|
+ <span class="info-text">{{ plan.personnel }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item" v-if="plan.participantCount">
|
|
|
|
|
+ <el-icon><UserFilled /></el-icon>
|
|
|
|
|
+ <span>参与人数:</span>
|
|
|
|
|
+ <span class="info-value">{{ plan.participantCount }}人</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item" v-if="plan.progress !== null && plan.progress !== undefined">
|
|
|
|
|
+ <el-icon><CircleCheckFilled /></el-icon>
|
|
|
|
|
+ <span>进度:</span>
|
|
|
|
|
+ <el-progress :percentage="plan.progress" :status="plan.progress === 100 ? 'success' : ''" :stroke-width="6" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="plan-actions">
|
|
|
|
|
+ <el-button link type="primary" size="small" @click.stop="editPlan(plan)">
|
|
|
|
|
+ <el-icon><Edit /></el-icon>编辑
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button link type="warning" size="small" @click.stop="startDrill(plan)">
|
|
|
|
|
+ <el-icon><VideoPlay /></el-icon>开始演练
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button link type="success" size="small" @click.stop="viewDrillRecord(plan)">
|
|
|
|
|
+ <el-icon><Document /></el-icon>查看记录
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button link type="danger" size="small" @click.stop="confirmDeletePlan(plan)">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>删除
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -62,420 +149,1452 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- ===== 中列 + 右列合并区域 ===== -->
|
|
|
|
|
- <div class="col-right">
|
|
|
|
|
- <!-- 演练结果记录与分析评估 -->
|
|
|
|
|
- <div class="panel">
|
|
|
|
|
|
|
+ <!-- ===== 右列 ===== -->
|
|
|
|
|
+ <div class="right-column">
|
|
|
|
|
+ <!-- 当前演练状态 -->
|
|
|
|
|
+ <div class="panel current-drill-panel" v-if="currentDrillInfo">
|
|
|
<div class="panel-header">
|
|
<div class="panel-header">
|
|
|
- <span><el-icon><DataAnalysis /></el-icon> 演练结果记录与分析评估</span>
|
|
|
|
|
- <el-button type="warning" size="small" @click="sendToSubsystem">
|
|
|
|
|
- 发送至统计分析子系统 & 应急评估子系统
|
|
|
|
|
|
|
+ <span><el-icon><Monitor /></el-icon> 当前演练状态</span>
|
|
|
|
|
+ <el-button v-if="isDrilling" type="danger" size="small" @click="stopDrill">
|
|
|
|
|
+ <el-icon><VideoPause /></el-icon>结束演练
|
|
|
</el-button>
|
|
</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="panel-body">
|
|
<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 class="drill-status-summary">
|
|
|
|
|
+ <div class="status-badge large" :class="currentDrillStatus">
|
|
|
|
|
+ <el-icon><CircleCheckFilled /></el-icon>
|
|
|
|
|
+ {{ currentDrillStatus }}
|
|
|
</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 class="drill-timeline">
|
|
|
|
|
+ <el-timeline>
|
|
|
|
|
+ <el-timeline-item
|
|
|
|
|
+ v-for="(action, index) in drillActions"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ :timestamp="action.time"
|
|
|
|
|
+ placement="top"
|
|
|
|
|
+ :type="action.type"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ action.message }}
|
|
|
|
|
+ </el-timeline-item>
|
|
|
|
|
+ </el-timeline>
|
|
|
</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 class="panel record-panel">
|
|
|
|
|
+ <div class="panel-header">
|
|
|
|
|
+ <span><el-icon><DataAnalysis /></el-icon> 演练结果记录</span>
|
|
|
|
|
+ <el-button type="primary" size="small" @click="saveAllRecord">
|
|
|
|
|
+ <el-icon><CircleCheckFilled /></el-icon>保存记录
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="panel-body">
|
|
|
|
|
+ <el-descriptions :column="2" border v-if="currentDrillRecord">
|
|
|
|
|
+ <el-descriptions-item label="演练项目">{{ currentDrillRecord.drillProjectName }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="记录时间">{{ currentDrillRecord.recordTime }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="演练类型">{{ currentDrillRecord.drillType }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="参演部门">{{ currentDrillRecord.participatingDept }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="演练负责人" :span="2">{{ currentDrillRecord.responsiblePerson }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 监测监控信息 -->
|
|
|
|
|
+ <div class="record-section">
|
|
|
|
|
+ <div class="section-title-row">
|
|
|
|
|
+ <h4 class="section-title"><el-icon><Monitor /></el-icon> 监测监控信息</h4>
|
|
|
|
|
+ <el-button type="primary" size="small" @click="openAddMonitoring">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>添加监测数据
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-table :data="monitoringInfo" stripe size="small" style="width: 100%" max-height="200">
|
|
|
|
|
+ <el-table-column prop="sensorType" label="传感器类型" min-width="120" />
|
|
|
|
|
+ <el-table-column prop="location" label="监测位置" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="value" label="监测数值" width="100" />
|
|
|
|
|
+ <el-table-column prop="unit" label="单位" width="70" />
|
|
|
|
|
+ <el-table-column prop="alarmLevel" label="报警等级" width="90">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="getAlarmLevelType(row.alarmLevel)" size="small">{{ row.alarmLevel }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="time" label="监测时间" width="150" />
|
|
|
|
|
+ <el-table-column label="操作" width="120" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button link type="primary" size="small" @click="openEditMonitoring(row)">编辑</el-button>
|
|
|
|
|
+ <el-button link type="danger" size="small" @click="confirmDeleteMonitoring(row)">删除</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-empty v-if="monitoringInfo.length === 0" description="暂无监测数据" :image-size="60" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 接警信息 -->
|
|
|
|
|
+ <div class="record-section" v-if="currentDrillRecord">
|
|
|
|
|
+ <h4 class="section-title"><el-icon><Phone /></el-icon> 接警信息</h4>
|
|
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
|
|
+ <el-descriptions-item label="接警时间">{{ currentDrillRecord.reportTime || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="报警方式">{{ currentDrillRecord.reportMethod || '电话' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="报警人" :span="2">{{ currentDrillRecord.reporter || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="联系电话" :span="2">{{ currentDrillRecord.contactPhone || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="事件地点" :span="2">{{ currentDrillRecord.eventLocation || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="事件描述" :span="2">
|
|
|
|
|
+ <el-input v-model="currentDrillRecord.eventDescription" type="textarea" :rows="2" placeholder="请输入事件描述" size="small" />
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 指挥调度信息 -->
|
|
|
|
|
+ <div class="record-section">
|
|
|
|
|
+ <h4 class="section-title"><el-icon><Guide /></el-icon> 指挥调度信息</h4>
|
|
|
|
|
+ <el-form :model="dispatchForm" label-width="80px" size="small">
|
|
|
|
|
+ <el-form-item label="调度指令">
|
|
|
|
|
+ <el-input v-model="dispatchForm.instruction" type="textarea" :rows="2" placeholder="请输入调度指令" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="调派力量">
|
|
|
|
|
+ <el-select v-model="dispatchForm.forces" multiple placeholder="请选择调派力量" style="width: 100%">
|
|
|
|
|
+ <el-option label="消防中队" value="消防中队" />
|
|
|
|
|
+ <el-option label="危化品处置小组" value="危化品处置小组" />
|
|
|
|
|
+ <el-option label="医疗救援队" value="医疗救援队" />
|
|
|
|
|
+ <el-option label="应急抢险队" value="应急抢险队" />
|
|
|
|
|
+ <el-option label="公安警力" value="公安警力" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="现场指挥部">
|
|
|
|
|
+ <el-switch v-model="dispatchForm.commandCenter" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="人员伤亡">
|
|
|
|
|
+ <el-input v-model="dispatchForm.casualties" placeholder="暂无人员伤亡报告" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 决策信息 -->
|
|
|
|
|
+ <div class="record-section">
|
|
|
|
|
+ <h4 class="section-title"><el-icon><Operation /></el-icon> 决策信息</h4>
|
|
|
|
|
+ <el-form :model="decisionForm" label-width="80px" size="small">
|
|
|
|
|
+ <el-form-item label="响应级别">
|
|
|
|
|
+ <el-select v-model="decisionForm.responseLevel" style="width: 100%">
|
|
|
|
|
+ <el-option label="一级响应" value="一级响应" />
|
|
|
|
|
+ <el-option label="二级响应" value="二级响应" />
|
|
|
|
|
+ <el-option label="三级响应" value="三级响应" />
|
|
|
|
|
+ <el-option label="四级响应" value="四级响应" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="总指挥命令">
|
|
|
|
|
+ <el-input v-model="decisionForm.command" type="textarea" :rows="2" placeholder="请输入总指挥命令" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="疏散范围">
|
|
|
|
|
+ <el-input v-model="decisionForm.evacuationRange" placeholder="请输入疏散范围" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="增援请求">
|
|
|
|
|
+ <el-switch v-model="decisionForm.reinforcement" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="备注说明">
|
|
|
|
|
+ <el-input v-model="decisionForm.remark" type="textarea" :rows="2" placeholder="请输入备注说明" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 对外发布信息 -->
|
|
|
|
|
+ <div class="record-section">
|
|
|
|
|
+ <h4 class="section-title"><el-icon><ChatDotRound /></el-icon> 对外发布信息</h4>
|
|
|
|
|
+ <el-form label-width="80px" size="small">
|
|
|
|
|
+ <el-form-item label="发布渠道">
|
|
|
|
|
+ <el-checkbox-group v-model="releaseChannels">
|
|
|
|
|
+ <el-checkbox label="官方微博">官方微博</el-checkbox>
|
|
|
|
|
+ <el-checkbox label="微信公众号">微信公众号</el-checkbox>
|
|
|
|
|
+ <el-checkbox label="门户网站">门户网站</el-checkbox>
|
|
|
|
|
+ <el-checkbox label="媒体发布">媒体发布</el-checkbox>
|
|
|
|
|
+ <el-checkbox label="短信通知">短信通知</el-checkbox>
|
|
|
|
|
+ </el-checkbox-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="发布内容">
|
|
|
|
|
+ <el-input v-model="releaseContent" type="textarea" :rows="4" placeholder="请输入对外发布信息内容" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" size="small" @click="saveReleaseInfo">保存并发布</el-button>
|
|
|
|
|
+ <el-button size="small" @click="previewRelease">预览</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- 统计分析子系统 结果 -->
|
|
|
|
|
- <div class="panel">
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 统计分析子系统结果 -->
|
|
|
|
|
+ <div class="panel analysis-panel">
|
|
|
<div class="panel-header">
|
|
<div class="panel-header">
|
|
|
- <span><el-icon><TrendCharts /></el-icon> 统计分析子系统 结果</span>
|
|
|
|
|
|
|
+ <span><el-icon><TrendCharts /></el-icon> 统计分析子系统结果</span>
|
|
|
|
|
+ <div class="header-actions">
|
|
|
|
|
+ <el-button type="warning" size="small" @click="sendToAnalysisSubsystems" :disabled="!canSendToSubsystems">
|
|
|
|
|
+ <el-icon><Share /></el-icon>发送至子系统和评估系统
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button type="success" size="small" @click="exportAnalysisReport">
|
|
|
|
|
+ <el-icon><Download /></el-icon>导出分析报告
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="panel-body">
|
|
<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 class="analysis-summary" v-if="analysisResult">
|
|
|
|
|
+ <div class="analysis-time">
|
|
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
|
|
+ 分析时间:{{ analysisResult.analyzeTime || '--:--:--' }}
|
|
|
</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 class="analysis-basic">
|
|
|
|
|
+ <h4 class="basic-title">演练能力评估</h4>
|
|
|
|
|
+ <div class="capability-grid">
|
|
|
|
|
+ <div class="capability-item">
|
|
|
|
|
+ <div class="cap-label">技能掌握</div>
|
|
|
|
|
+ <div class="cap-score">{{ analysisResult.skillScore }}/100</div>
|
|
|
|
|
+ <el-progress :percentage="analysisResult.skillScore" :stroke-width="8" />
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="eval-item">
|
|
|
|
|
- <div class="eval-label">响应时效</div>
|
|
|
|
|
- <div class="eval-value">达标</div>
|
|
|
|
|
|
|
+ <div class="capability-item">
|
|
|
|
|
+ <div class="cap-label">装备使用</div>
|
|
|
|
|
+ <div class="cap-score">{{ analysisResult.equipmentScore }}/100</div>
|
|
|
|
|
+ <el-progress :percentage="analysisResult.equipmentScore" :stroke-width="8" />
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="eval-item">
|
|
|
|
|
- <div class="eval-label">协同评分</div>
|
|
|
|
|
- <div class="eval-value">良好</div>
|
|
|
|
|
|
|
+ <div class="capability-item">
|
|
|
|
|
+ <div class="cap-label">响应时效</div>
|
|
|
|
|
+ <div class="cap-score">{{ analysisResult.responseScore }}/100</div>
|
|
|
|
|
+ <el-progress :percentage="analysisResult.responseScore" :stroke-width="8" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="capability-item">
|
|
|
|
|
+ <div class="cap-label">协同配合</div>
|
|
|
|
|
+ <div class="cap-score">{{ analysisResult.cooperationScore }}/100</div>
|
|
|
|
|
+ <el-progress :percentage="analysisResult.cooperationScore" :stroke-width="8" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="analysis-task">
|
|
|
|
|
+ <h4 class="task-title">任务执行情况</h4>
|
|
|
|
|
+ <div class="task-stats">
|
|
|
|
|
+ <div class="task-stat-item">
|
|
|
|
|
+ <div class="stat-label">任务总数</div>
|
|
|
|
|
+ <div class="stat-value highlight-blue">{{ analysisResult.totalTasks }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="task-stat-item">
|
|
|
|
|
+ <div class="stat-label">完成任务</div>
|
|
|
|
|
+ <div class="stat-value highlight-green">{{ analysisResult.completedTasks }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="task-stat-item">
|
|
|
|
|
+ <div class="stat-label">完成率</div>
|
|
|
|
|
+ <div class="stat-value highlight-orange">{{ analysisResult.completionRate }}%</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="task-stat-item">
|
|
|
|
|
+ <div class="stat-label">超时任务</div>
|
|
|
|
|
+ <div class="stat-value highlight-red">{{ analysisResult.overtimeTasks }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="analysis-comprehensive">
|
|
|
|
|
+ <h4 class="comprehensive-title">综合评估结果</h4>
|
|
|
|
|
+ <div class="eval-grid">
|
|
|
<div class="eval-item">
|
|
<div class="eval-item">
|
|
|
<div class="eval-label">综合等级</div>
|
|
<div class="eval-label">综合等级</div>
|
|
|
- <div class="eval-value highlight">良好</div>
|
|
|
|
|
|
|
+ <div class="eval-value" :class="{ success: analysisResult.overallLevel === '优秀', warning: analysisResult.overallLevel === '良好', info: analysisResult.overallLevel === '合格', danger: analysisResult.overallLevel === '待改进' }">
|
|
|
|
|
+ {{ analysisResult.overallLevel }}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="eval-description">
|
|
|
|
|
+ <strong>评估结论:</strong>
|
|
|
|
|
+ <p>{{ analysisResult.evalConclusion || '无评估结论' }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="suggestion-section">
|
|
|
|
|
+ <strong>改进建议:</strong>
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ <li v-for="(sug, idx) in analysisResult.improvementSuggestions" :key="idx">{{ sug }}</li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-empty v-else description="暂无统计分析结果,请点击“分析报表”加载" :image-size="100" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <!-- 模板弹窗 -->
|
|
|
|
|
- <el-dialog v-model="tplDialogVisible" title="演练模板" width="500px" destroy-on-close>
|
|
|
|
|
- <el-form :model="tplForm" label-width="80px">
|
|
|
|
|
- <el-form-item label="模板名称"><el-input v-model="tplForm.name" /></el-form-item>
|
|
|
|
|
- <el-form-item label="所属系统">
|
|
|
|
|
- <el-select v-model="tplForm.category" style="width:100%"><el-option label="燃气" value="燃气" /><el-option label="排水" value="排水" /><el-option label="供水" value="供水" /></el-select>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item label="模板内容"><el-input v-model="tplForm.content" type="textarea" :rows="4" /></el-form-item>
|
|
|
|
|
- </el-form>
|
|
|
|
|
- <template #footer><el-button @click="tplDialogVisible=false">取消</el-button><el-button type="primary" @click="handleTplSubmit">确定</el-button></template>
|
|
|
|
|
- </el-dialog>
|
|
|
|
|
- <!-- 计划表单弹窗 -->
|
|
|
|
|
- <el-dialog v-model="planDialogVisible" :title="planFormTitle" width="550px" destroy-on-close>
|
|
|
|
|
- <el-form :model="planForm" ref="planFormRef" label-width="90px">
|
|
|
|
|
- <el-form-item label="演练名称"><el-input v-model="planForm.name" /></el-form-item>
|
|
|
|
|
|
|
+ <!-- 弹窗:模板管理 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="templateDialogVisible"
|
|
|
|
|
+ :title="templateDialogTitle"
|
|
|
|
|
+ width="600px"
|
|
|
|
|
+ destroy-on-close
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form :model="templateForm" label-width="90px">
|
|
|
|
|
+ <el-form-item label="模板名称">
|
|
|
|
|
+ <el-input v-model="templateForm.name" placeholder="请输入演练模板名称" />
|
|
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="所属系统">
|
|
<el-form-item label="所属系统">
|
|
|
- <el-select v-model="planForm.category" style="width:100%">
|
|
|
|
|
- <el-option label="燃气" value="燃气" /><el-option label="排水" value="排水" /><el-option label="供水" value="供水" />
|
|
|
|
|
|
|
+ <el-select v-model="templateForm.category" style="width: 100%">
|
|
|
|
|
+ <el-option label="燃气管网" value="燃气" />
|
|
|
|
|
+ <el-option label="排水管网" value="排水" />
|
|
|
|
|
+ <el-option label="供水管网" value="供水" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="演练目标">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="templateForm.target"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ placeholder="请输入演练目标"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="演练项目">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="templateForm.project"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="3"
|
|
|
|
|
+ placeholder="请输入演练项目详情"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="涉及事件">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="templateForm.eventDesc"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ placeholder="请输入可能涉及的事件类型"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="参与人员">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="templateForm.personnel"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ placeholder="例如:指挥中心、消防队、医疗组等"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="完成时限">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="templateForm.dueDate"
|
|
|
|
|
+ type="date"
|
|
|
|
|
+ placeholder="选择截止日期"
|
|
|
|
|
+ format="YYYY-MM-DD"
|
|
|
|
|
+ date-format="YYYY-MM-DD"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="预案内容">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="templateForm.content"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="4"
|
|
|
|
|
+ placeholder="请输入详细的预案内容和操作流程"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="closeTemplateDialog">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="submitTemplateForm">确定</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 弹窗:计划管理 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="planDialogVisible"
|
|
|
|
|
+ :title="planDialogTitle"
|
|
|
|
|
+ width="600px"
|
|
|
|
|
+ destroy-on-close
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form :model="planForm" ref="planFormRef" label-width="90px">
|
|
|
|
|
+ <el-form-item label="演练名称" required>
|
|
|
|
|
+ <el-input v-model="planForm.name" placeholder="请输入演练名称" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="所属系统" required>
|
|
|
|
|
+ <el-select v-model="planForm.category" style="width: 100%">
|
|
|
|
|
+ <el-option label="燃气管网" value="燃气" />
|
|
|
|
|
+ <el-option label="排水管网" value="排水" />
|
|
|
|
|
+ <el-option label="供水管网" value="供水" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="演练目标" required>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="planForm.target"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ placeholder="请输入演练目标"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="演练项目" required>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="planForm.project"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="3"
|
|
|
|
|
+ placeholder="请输入演练项目详情"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="事件描述">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="planForm.eventDesc"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ placeholder="请输入模拟事件的描述"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="参与人员" required>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="planForm.personnel"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="2"
|
|
|
|
|
+ placeholder="请输入参与演练的人员或部门"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="完成时间">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="planForm.endTime"
|
|
|
|
|
+ type="date"
|
|
|
|
|
+ placeholder="选择完成时间"
|
|
|
|
|
+ format="YYYY-MM-DD"
|
|
|
|
|
+ date-format="YYYY-MM-DD"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="参与人数">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="planForm.participantCount"
|
|
|
|
|
+ :min="1"
|
|
|
|
|
+ :max="1000"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="演练进度">
|
|
|
|
|
+ <el-slider v-model="planForm.progress" :min="0" :max="100" :step="10" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="状态" required>
|
|
|
|
|
+ <el-select v-model="planForm.status" style="width: 100%">
|
|
|
|
|
+ <el-option label="计划中" value="计划中" />
|
|
|
|
|
+ <el-option label="进行中" value="进行中" />
|
|
|
|
|
+ <el-option label="已完成" value="已完成" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="planDialogVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="submitPlanForm">确定</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 弹窗:监测数据 -->
|
|
|
|
|
+ <el-dialog v-model="monitoringDialogVisible" :title="monitoringDialogTitle" width="500px" destroy-on-close>
|
|
|
|
|
+ <el-form :model="monitoringForm" label-width="90px">
|
|
|
|
|
+ <el-form-item label="传感器类型" required>
|
|
|
|
|
+ <el-input v-model="monitoringForm.sensorType" placeholder="如:燃气浓度传感器" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="监测位置" required>
|
|
|
|
|
+ <el-input v-model="monitoringForm.location" placeholder="如:XX路XX号管点" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="监测数值">
|
|
|
|
|
+ <el-input v-model="monitoringForm.value" placeholder="如:50" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="单位">
|
|
|
|
|
+ <el-input v-model="monitoringForm.unit" placeholder="如:%LEL" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="报警等级">
|
|
|
|
|
+ <el-select v-model="monitoringForm.alarmLevel" style="width: 100%">
|
|
|
|
|
+ <el-option label="一般" value="一般" />
|
|
|
|
|
+ <el-option label="较大" value="较大" />
|
|
|
|
|
+ <el-option label="重大" value="重大" />
|
|
|
|
|
+ <el-option label="特别重大" value="特别重大" />
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <el-form-item label="演练目标"><el-input v-model="planForm.target" type="textarea" :rows="2" /></el-form-item>
|
|
|
|
|
- <el-form-item label="演练项目"><el-input v-model="planForm.project" type="textarea" :rows="2" /></el-form-item>
|
|
|
|
|
- <el-form-item label="事件描述"><el-input v-model="planForm.eventDesc" /></el-form-item>
|
|
|
|
|
- <el-form-item label="参与人员"><el-input v-model="planForm.personnel" /></el-form-item>
|
|
|
|
|
- <el-form-item label="状态"><el-select v-model="planForm.status" style="width:100%"><el-option label="计划中" value="计划中" /><el-option label="进行中" value="进行中" /><el-option label="已完成" value="已完成" /></el-select></el-form-item>
|
|
|
|
|
|
|
+ <el-form-item label="监测时间">
|
|
|
|
|
+ <el-input v-model="monitoringForm.time" placeholder="如:2024-01-01 12:00:00" />
|
|
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
- <template #footer><el-button @click="planDialogVisible=false">取消</el-button><el-button type="primary" @click="handlePlanSubmit">确定</el-button></template>
|
|
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="monitoringDialogVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="submitMonitoringForm">确定</el-button>
|
|
|
|
|
+ </template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
|
|
-import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
|
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
|
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
|
|
import {
|
|
import {
|
|
|
Document, Edit, Delete, Setting, DataAnalysis, Monitor,
|
|
Document, Edit, Delete, Setting, DataAnalysis, Monitor,
|
|
|
- Phone, Guide, Operation, ChatDotRound, TrendCharts
|
|
|
|
|
|
|
+ Phone, Guide, Operation, ChatDotRound, TrendCharts, Plus, Calendar,
|
|
|
|
|
+ User, SwitchButton, Search, Flag, Bell, List, Share, Download,
|
|
|
|
|
+ Clock, VideoPlay, VideoPause, UserFilled, CircleCheckFilled
|
|
|
} from '@element-plus/icons-vue'
|
|
} from '@element-plus/icons-vue'
|
|
|
-import { getPracticalPlans, savePracticalPlan, updatePracticalPlan, deletePracticalPlan, getPracticalTemplates, savePracticalTemplate, updatePracticalTemplate, deletePracticalTemplate } from '@/api/pipeNetwork/basic'
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ getPracticalTemplates, savePracticalTemplate, updatePracticalTemplate, deletePracticalTemplate,
|
|
|
|
|
+ getPracticalPlans, savePracticalPlan, updatePracticalPlan, deletePracticalPlan,
|
|
|
|
|
+ sendDrillAnalysis,
|
|
|
|
|
+ getDrillRecord, saveDrillRecord, updateDrillRecord,
|
|
|
|
|
+ getDrillMonitoring, saveDrillMonitoring, deleteDrillMonitoring,
|
|
|
|
|
+ getDrillAnalysis, exportDrillReport, updatePlanStatus
|
|
|
|
|
+} from '@/api/pipeNetwork/basic'
|
|
|
|
|
+
|
|
|
|
|
+// ========== 卸载保护 ==========
|
|
|
|
|
+let isUnmounted = false
|
|
|
|
|
|
|
|
|
|
+// ========== 演练模板相关 ==========
|
|
|
const templates = ref([])
|
|
const templates = ref([])
|
|
|
-// 模板弹窗
|
|
|
|
|
-const tplDialogVisible = ref(false)
|
|
|
|
|
-const tplForm = ref({ id: null, name: '', category: '燃气', content: '' })
|
|
|
|
|
-const handleAddTemplate = () => { tplForm.value = { id: null, name: '', category: '燃气', content: '' }; tplDialogVisible.value = true }
|
|
|
|
|
-const handleEditTemplate = (tpl) => { tplForm.value = { ...tpl }; tplDialogVisible.value = true }
|
|
|
|
|
-const handleUseTemplate = (tpl) => { ElMessage.success(`已应用模板: ${tpl.name}`) }
|
|
|
|
|
-const handleTplSubmit = async () => {
|
|
|
|
|
- const d = tplForm.value
|
|
|
|
|
|
|
+const templateDialogVisible = ref(false)
|
|
|
|
|
+const templateDialogTitle = ref('新增演练模板')
|
|
|
|
|
+const templateForm = ref({
|
|
|
|
|
+ id: null, name: '', category: '燃气', target: '', project: '', eventDesc: '',
|
|
|
|
|
+ personnel: '', dueDate: null, content: ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const openTemplateDialog = () => {
|
|
|
|
|
+ templateForm.value = {
|
|
|
|
|
+ id: null, name: '', category: '燃气', target: '', project: '',
|
|
|
|
|
+ eventDesc: '', personnel: '', dueDate: null, content: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ templateDialogTitle.value = '新增演练模板'
|
|
|
|
|
+ templateDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const closeTemplateDialog = () => { templateDialogVisible.value = false }
|
|
|
|
|
+
|
|
|
|
|
+const openEditTemplate = (tpl) => {
|
|
|
|
|
+ templateForm.value = { ...tpl, dueDate: tpl.dueDate || null }
|
|
|
|
|
+ templateDialogTitle.value = '编辑演练模板'
|
|
|
|
|
+ templateDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const submitTemplateForm = async () => {
|
|
|
|
|
+ if (!templateForm.value.name) {
|
|
|
|
|
+ ElMessage.warning('请输入模板名称')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
- d.id ? await updatePracticalTemplate(d) : await savePracticalTemplate(d)
|
|
|
|
|
- ElMessage.success('保存成功'); tplDialogVisible.value = false; loadTemplates()
|
|
|
|
|
- } catch (e) { ElMessage.error('保存失败') }
|
|
|
|
|
|
|
+ const d = templateForm.value
|
|
|
|
|
+ if (d.id) await updatePracticalTemplate(d)
|
|
|
|
|
+ else await savePracticalTemplate(d)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ ElMessage.success('保存成功')
|
|
|
|
|
+ templateDialogVisible.value = false
|
|
|
|
|
+ loadTemplates()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('保存失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 应用模板 → 打开计划弹窗并预填模板数据
|
|
|
|
|
+const applyTemplate = (tpl) => {
|
|
|
|
|
+ planForm.value = {
|
|
|
|
|
+ id: null, name: tpl.name, category: tpl.category || '燃气',
|
|
|
|
|
+ target: tpl.target || '', project: tpl.project || '',
|
|
|
|
|
+ eventDesc: tpl.eventDesc || '', personnel: tpl.personnel || '',
|
|
|
|
|
+ endTime: tpl.dueDate || null, participantCount: 1, progress: 0, status: '计划中'
|
|
|
|
|
+ }
|
|
|
|
|
+ planDialogTitle.value = '基于模板创建演练计划'
|
|
|
|
|
+ planDialogVisible.value = true
|
|
|
|
|
+ ElMessage.info(`已应用模板「${tpl.name}」,请完善计划信息`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const confirmDeleteTemplate = (tpl) => {
|
|
|
|
|
+ ElMessageBox.confirm('确认删除此演练模板?', '提示', { type: 'warning' })
|
|
|
|
|
+ .then(async () => {
|
|
|
|
|
+ await deletePracticalTemplate(tpl.id)
|
|
|
|
|
+ ElMessage.success('删除成功')
|
|
|
|
|
+ loadTemplates()
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadTemplates() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getPracticalTemplates()
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ const rows = res.data || []
|
|
|
|
|
+ templates.value = rows.map(d => ({
|
|
|
|
|
+ id: d.id, name: d.name, category: d.category || '燃气',
|
|
|
|
|
+ target: d.target, project: d.project, eventDesc: d.eventDesc,
|
|
|
|
|
+ personnel: d.personnel, dueDate: d.dueDate,
|
|
|
|
|
+ content: d.content, createDate: d.createDate || d.updateTime,
|
|
|
|
|
+ userCount: d.userCount || 0
|
|
|
|
|
+ }))
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('加载模板失败', e)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getCategoryTagType = (category) => {
|
|
|
|
|
+ const types = { 燃气: 'danger', 排水: 'warning', 供水: 'primary' }
|
|
|
|
|
+ return types[category] || 'info'
|
|
|
}
|
|
}
|
|
|
-const handleDeleteTemplate = (tpl) => { ElMessageBox.confirm('确认删除?', '提示', { type: 'warning' }).then(async () => { await deletePracticalTemplate(tpl.id); ElMessage.success('删除成功'); loadTemplates() }).catch(() => {}) }
|
|
|
|
|
-async function loadTemplates() { try { const res = await getPracticalTemplates(); const rows = res.data || []; templates.value = rows.map(d => ({ id: d.id, name: d.name, content: d.content, date: d.createDate })) } catch (e) {} }
|
|
|
|
|
|
|
|
|
|
-const planSearch = ref('')
|
|
|
|
|
|
|
+const getCategoryLabel = (category) => {
|
|
|
|
|
+ const labels = { 燃气: '燃气管网', 排水: '排水管网', 供水: '供水管网' }
|
|
|
|
|
+ return labels[category] || category
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 演练计划相关 ==========
|
|
|
|
|
+const planSearchText = ref('')
|
|
|
|
|
+const planFilterStatus = ref('')
|
|
|
const plans = ref([])
|
|
const plans = ref([])
|
|
|
|
|
+const filteredPlans = computed(() => {
|
|
|
|
|
+ let result = plans.value
|
|
|
|
|
+ if (planFilterStatus.value) {
|
|
|
|
|
+ result = result.filter(p => p.status === planFilterStatus.value)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (planSearchText.value) {
|
|
|
|
|
+ const keyword = planSearchText.value.toLowerCase()
|
|
|
|
|
+ result = result.filter(p =>
|
|
|
|
|
+ p.name?.toLowerCase().includes(keyword) ||
|
|
|
|
|
+ p.target?.toLowerCase().includes(keyword) ||
|
|
|
|
|
+ p.project?.toLowerCase().includes(keyword)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return result
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const selectedPlanId = ref(null)
|
|
|
|
|
+const currentPlan = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+// 选中计划 → 加载关联的演练记录
|
|
|
|
|
+const selectPlan = async (plan) => {
|
|
|
|
|
+ selectedPlanId.value = plan.id
|
|
|
|
|
+ currentPlan.value = plan
|
|
|
|
|
+ await loadDrillRecord(plan.id)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-// 计划弹窗
|
|
|
|
|
const planDialogVisible = ref(false)
|
|
const planDialogVisible = ref(false)
|
|
|
-const planFormTitle = ref('新增计划')
|
|
|
|
|
-const planForm = ref({ id: null, name: '', category: '燃气', target: '', project: '', eventDesc: '', personnel: '', status: '计划中' })
|
|
|
|
|
|
|
+const planDialogTitle = ref('新建演练计划')
|
|
|
|
|
+const planForm = ref({
|
|
|
|
|
+ id: null, name: '', category: '燃气', target: '', project: '',
|
|
|
|
|
+ eventDesc: '', personnel: '', endTime: null, participantCount: 1, progress: 0, status: '计划中'
|
|
|
|
|
+})
|
|
|
const planFormRef = ref(null)
|
|
const planFormRef = ref(null)
|
|
|
-const handleAddPlan = () => { planForm.value = { id: null, name: '', category: '燃气', target: '', project: '', eventDesc: '', personnel: '', status: '计划中' }; planFormTitle.value = '新增计划'; planDialogVisible.value = true }
|
|
|
|
|
-const handleEditPlan = (plan) => { planForm.value = { id: plan.id, name: plan.name, category: plan.category || '燃气', target: plan.target || '', project: plan.project || '', eventDesc: plan.eventDesc || plan.event || '', personnel: plan.personnel || '', status: plan.status || '计划中' }; planFormTitle.value = '编辑计划'; planDialogVisible.value = true }
|
|
|
|
|
-const handleDeletePlan = (plan) => { ElMessageBox.confirm('确认删除?', '提示', { type: 'warning' }).then(async () => { await deletePracticalPlan(plan.id); ElMessage.success('删除成功'); loadData() }).catch(() => {}) }
|
|
|
|
|
-const handlePlanSubmit = async () => {
|
|
|
|
|
- const d = planForm.value
|
|
|
|
|
|
|
+
|
|
|
|
|
+const openPlanDialog = () => {
|
|
|
|
|
+ planForm.value = {
|
|
|
|
|
+ id: null, name: '', category: '燃气', target: '', project: '',
|
|
|
|
|
+ eventDesc: '', personnel: '', endTime: null, participantCount: 1, progress: 0, status: '计划中'
|
|
|
|
|
+ }
|
|
|
|
|
+ planDialogTitle.value = '新建演练计划'
|
|
|
|
|
+ planDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const editPlan = (plan) => {
|
|
|
|
|
+ planForm.value = { ...plan, endTime: plan.endTime || null }
|
|
|
|
|
+ planDialogTitle.value = '编辑演练计划'
|
|
|
|
|
+ planDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const submitPlanForm = async () => {
|
|
|
|
|
+ if (!planForm.value.name) {
|
|
|
|
|
+ ElMessage.warning('请输入演练名称')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const d = planForm.value
|
|
|
|
|
+ if (d.id) await updatePracticalPlan(d)
|
|
|
|
|
+ else await savePracticalPlan(d)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ ElMessage.success('保存成功')
|
|
|
|
|
+ planDialogVisible.value = false
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('保存失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const confirmDeletePlan = (plan) => {
|
|
|
|
|
+ ElMessageBox.confirm('确认删除此演练计划?', '提示', { type: 'warning' })
|
|
|
|
|
+ .then(async () => {
|
|
|
|
|
+ await deletePracticalPlan(plan.id)
|
|
|
|
|
+ ElMessage.success('删除成功')
|
|
|
|
|
+ if (selectedPlanId.value === plan.id) {
|
|
|
|
|
+ selectedPlanId.value = null
|
|
|
|
|
+ currentPlan.value = null
|
|
|
|
|
+ currentDrillRecord.value = null
|
|
|
|
|
+ currentDrillInfo.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleCreatePlan = () => { openPlanDialog() }
|
|
|
|
|
+const filterPlans = () => { /* 过滤由 computed 自动处理 */ }
|
|
|
|
|
+
|
|
|
|
|
+async function loadData() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getPracticalPlans()
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ const rows = res.data || []
|
|
|
|
|
+ plans.value = rows.map(d => ({
|
|
|
|
|
+ id: d.id, name: d.name, category: d.category || '燃气',
|
|
|
|
|
+ target: d.target, project: d.project, eventDesc: d.eventDesc || d.event,
|
|
|
|
|
+ personnel: d.personnel, status: d.status || '计划中',
|
|
|
|
|
+ endTime: d.endTime || d.updateTime, participantCount: d.participantCount || 0,
|
|
|
|
|
+ progress: d.progress !== undefined ? d.progress : 0
|
|
|
|
|
+ }))
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('加载计划失败', e)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 演练记录相关 ==========
|
|
|
|
|
+const currentDrillInfo = ref(null)
|
|
|
|
|
+const currentDrillRecord = ref(null)
|
|
|
|
|
+const isDrilling = ref(false)
|
|
|
|
|
+const currentDrillStatus = ref('未开始')
|
|
|
|
|
+const drillActions = ref([])
|
|
|
|
|
+const monitoringInfo = ref([])
|
|
|
|
|
+const dispatchForm = ref({ instruction: '', forces: [], commandCenter: false, casualties: '' })
|
|
|
|
|
+const decisionForm = ref({ responseLevel: '三级响应', command: '', evacuationRange: '', reinforcement: false, remark: '' })
|
|
|
|
|
+const releaseChannels = ref([])
|
|
|
|
|
+const releaseContent = ref('')
|
|
|
|
|
+
|
|
|
|
|
+// 加载演练记录
|
|
|
|
|
+async function loadDrillRecord(planId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getDrillRecord(planId)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ const data = res.data
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ currentDrillRecord.value = data
|
|
|
|
|
+ currentDrillInfo.value = { planId, status: data.status || '已记录' }
|
|
|
|
|
+ currentDrillStatus.value = data.status || '已记录'
|
|
|
|
|
+ if (data.dispatchInfo) dispatchForm.value = { ...dispatchForm.value, ...data.dispatchInfo }
|
|
|
|
|
+ if (data.decisionInfo) decisionForm.value = { ...decisionForm.value, ...data.decisionInfo }
|
|
|
|
|
+ if (data.releaseChannels) releaseChannels.value = data.releaseChannels
|
|
|
|
|
+ if (data.releaseContent) releaseContent.value = data.releaseContent
|
|
|
|
|
+ if (data.id) await loadMonitoring(data.id)
|
|
|
|
|
+ if (data.actions) drillActions.value = data.actions
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentDrillRecord.value = null
|
|
|
|
|
+ currentDrillInfo.value = null
|
|
|
|
|
+ monitoringInfo.value = []
|
|
|
|
|
+ drillActions.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ currentDrillRecord.value = null
|
|
|
|
|
+ currentDrillInfo.value = null
|
|
|
|
|
+ monitoringInfo.value = []
|
|
|
|
|
+ drillActions.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 加载监测信息
|
|
|
|
|
+async function loadMonitoring(recordId) {
|
|
|
|
|
+ if (!recordId) {
|
|
|
|
|
+ monitoringInfo.value = []
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getDrillMonitoring(recordId)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ monitoringInfo.value = res.data || []
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ monitoringInfo.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存全部演练记录
|
|
|
|
|
+const saveAllRecord = async () => {
|
|
|
|
|
+ if (!currentDrillRecord.value) {
|
|
|
|
|
+ ElMessage.warning('请先开始演练或选择有记录的计划')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const recordData = {
|
|
|
|
|
+ ...currentDrillRecord.value,
|
|
|
|
|
+ planId: selectedPlanId.value,
|
|
|
|
|
+ dispatchInfo: { ...dispatchForm.value },
|
|
|
|
|
+ decisionInfo: { ...decisionForm.value },
|
|
|
|
|
+ releaseChannels: releaseChannels.value,
|
|
|
|
|
+ releaseContent: releaseContent.value,
|
|
|
|
|
+ actions: drillActions.value
|
|
|
|
|
+ }
|
|
|
|
|
+ if (currentDrillRecord.value.id) {
|
|
|
|
|
+ await updateDrillRecord(recordData)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const res = await saveDrillRecord(recordData)
|
|
|
|
|
+ if (res.data) {
|
|
|
|
|
+ const saved = typeof res.data === 'object' ? res.data : { id: res.data }
|
|
|
|
|
+ currentDrillRecord.value.id = saved.id
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ ElMessage.success('演练记录已保存')
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('保存失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 监测信息弹窗 ==========
|
|
|
|
|
+const monitoringDialogVisible = ref(false)
|
|
|
|
|
+const monitoringDialogTitle = ref('添加监测数据')
|
|
|
|
|
+const monitoringForm = ref({
|
|
|
|
|
+ id: null, sensorType: '', location: '', value: '', unit: '', alarmLevel: '一般', time: ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const openAddMonitoring = () => {
|
|
|
|
|
+ if (!currentDrillRecord.value) {
|
|
|
|
|
+ ElMessage.warning('请先开始演练或加载演练记录')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ monitoringForm.value = {
|
|
|
|
|
+ id: null, sensorType: '', location: '', value: '', unit: '', alarmLevel: '一般',
|
|
|
|
|
+ time: new Date().toLocaleString()
|
|
|
|
|
+ }
|
|
|
|
|
+ monitoringDialogTitle.value = '添加监测数据'
|
|
|
|
|
+ monitoringDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openEditMonitoring = (row) => {
|
|
|
|
|
+ monitoringForm.value = { ...row }
|
|
|
|
|
+ monitoringDialogTitle.value = '编辑监测数据'
|
|
|
|
|
+ monitoringDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const submitMonitoringForm = async () => {
|
|
|
|
|
+ if (!monitoringForm.value.sensorType || !monitoringForm.value.location) {
|
|
|
|
|
+ ElMessage.warning('请填写传感器类型和监测位置')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = { ...monitoringForm.value, recordId: currentDrillRecord.value.id }
|
|
|
|
|
+ await saveDrillMonitoring(data)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ ElMessage.success('保存成功')
|
|
|
|
|
+ monitoringDialogVisible.value = false
|
|
|
|
|
+ if (currentDrillRecord.value.id) await loadMonitoring(currentDrillRecord.value.id)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('保存失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const confirmDeleteMonitoring = (row) => {
|
|
|
|
|
+ ElMessageBox.confirm('确认删除此监测数据?', '提示', { type: 'warning' })
|
|
|
|
|
+ .then(async () => {
|
|
|
|
|
+ await deleteDrillMonitoring(row.id)
|
|
|
|
|
+ ElMessage.success('删除成功')
|
|
|
|
|
+ if (currentDrillRecord.value?.id) await loadMonitoring(currentDrillRecord.value.id)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {})
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getAlarmLevelType = (level) => {
|
|
|
|
|
+ const types = { 一般: 'info', 较大: 'warning', 重大: 'danger', 特别重大: 'danger' }
|
|
|
|
|
+ return types[level] || 'info'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 对外发布 ==========
|
|
|
|
|
+const saveReleaseInfo = () => {
|
|
|
|
|
+ if (!releaseChannels.value.length && !releaseContent.value) {
|
|
|
|
|
+ ElMessage.warning('请填写发布渠道和内容')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ saveAllRecord()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const previewRelease = () => {
|
|
|
|
|
+ if (!releaseContent.value) {
|
|
|
|
|
+ ElMessage.warning('请先输入发布内容')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ ElMessageBox.alert(releaseContent.value, '发布内容预览', { confirmButtonText: '确定' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 统计分析子系统 ==========
|
|
|
|
|
+const analysisResult = ref(null)
|
|
|
|
|
+const canSendToSubsystems = computed(() => !!currentDrillRecord.value)
|
|
|
|
|
+
|
|
|
|
|
+const showAnalysisReport = async () => {
|
|
|
|
|
+ if (!selectedPlanId.value) {
|
|
|
|
|
+ ElMessage.warning('请先选择演练计划')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
- d.id ? await updatePracticalPlan(d) : await savePracticalPlan(d)
|
|
|
|
|
- ElMessage.success('保存成功'); planDialogVisible.value = false; loadData()
|
|
|
|
|
- } catch (e) { ElMessage.error('保存失败') }
|
|
|
|
|
|
|
+ const res = await getDrillAnalysis(selectedPlanId.value)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ if (res.data) {
|
|
|
|
|
+ analysisResult.value = res.data
|
|
|
|
|
+ ElMessage.success('分析报表已加载')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.info('暂无分析数据,请先完成演练并保存记录')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('加载分析数据失败')
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-const handleSearchPlan = () => { loadData() }
|
|
|
|
|
|
|
|
|
|
-// ---------- 发送至子系统 ----------
|
|
|
|
|
-async function loadData() { try { const res = await getPracticalPlans(); const rows = res.data || []; if (rows.length) plans.value = rows.map(d => ({ id: d.id, name: d.name, category: d.category, target: d.target, project: d.project, eventDesc: d.eventDesc, personnel: d.personnel, status: d.status })) } catch (e) {} }
|
|
|
|
|
-onMounted(() => { loadData(); loadTemplates() })
|
|
|
|
|
|
|
+const exportAnalysisReport = async () => {
|
|
|
|
|
+ if (!selectedPlanId.value) {
|
|
|
|
|
+ ElMessage.warning('请先选择演练计划')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await exportDrillReport(selectedPlanId.value)
|
|
|
|
|
+ const blob = new Blob(['\ufeff' + data], { type: 'text/plain;charset=utf-8' })
|
|
|
|
|
+ const url = window.URL.createObjectURL(blob)
|
|
|
|
|
+ const a = document.createElement('a')
|
|
|
|
|
+ a.href = url
|
|
|
|
|
+ a.download = `演练分析报告_${currentPlan.value?.name || ''}.txt`
|
|
|
|
|
+ a.click()
|
|
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
|
|
+ ElMessage.success('报告已导出')
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('导出失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-const sendToSubsystem = () => {
|
|
|
|
|
- ElMessage.success('已发送至统计分析子系统 & 应急评估子系统(伪)')
|
|
|
|
|
|
|
+const sendToAnalysisSubsystems = async () => {
|
|
|
|
|
+ if (!currentDrillRecord.value) {
|
|
|
|
|
+ ElMessage.warning('请先录入演练结果信息')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sendData = {
|
|
|
|
|
+ ...currentDrillRecord.value,
|
|
|
|
|
+ planId: selectedPlanId.value,
|
|
|
|
|
+ planName: currentPlan.value?.name,
|
|
|
|
|
+ dispatchInfo: { ...dispatchForm.value },
|
|
|
|
|
+ decisionInfo: { ...decisionForm.value },
|
|
|
|
|
+ releaseChannels: releaseChannels.value,
|
|
|
|
|
+ releaseContent: releaseContent.value,
|
|
|
|
|
+ monitoringInfo: monitoringInfo.value,
|
|
|
|
|
+ actions: drillActions.value
|
|
|
|
|
+ }
|
|
|
|
|
+ await sendDrillAnalysis(sendData)
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ ElNotification({
|
|
|
|
|
+ title: '发送成功',
|
|
|
|
|
+ message: '演练分析数据已成功发送至统计分析和应急评估子系统',
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ duration: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('发送失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 演练流程控制 ==========
|
|
|
|
|
+const startDrill = async (plan) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await updatePlanStatus(plan.id, '进行中')
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ plan.status = '进行中'
|
|
|
|
|
+ plan.progress = Math.max(plan.progress, 10)
|
|
|
|
|
+ selectedPlanId.value = plan.id
|
|
|
|
|
+ currentPlan.value = plan
|
|
|
|
|
+ isDrilling.value = true
|
|
|
|
|
+ currentDrillStatus.value = '进行中'
|
|
|
|
|
+ currentDrillInfo.value = { planId: plan.id, status: '进行中' }
|
|
|
|
|
+ drillActions.value = [
|
|
|
|
|
+ { time: new Date().toLocaleString(), type: 'primary', message: `开始演练:${plan.name}` }
|
|
|
|
|
+ ]
|
|
|
|
|
+ currentDrillRecord.value = {
|
|
|
|
|
+ drillProjectName: plan.name,
|
|
|
|
|
+ recordTime: new Date().toLocaleString(),
|
|
|
|
|
+ drillType: '实战演练',
|
|
|
|
|
+ participatingDept: plan.personnel || '—',
|
|
|
|
|
+ responsiblePerson: '—',
|
|
|
|
|
+ planId: plan.id,
|
|
|
|
|
+ status: '进行中',
|
|
|
|
|
+ reportTime: new Date().toLocaleString(),
|
|
|
|
|
+ reportMethod: '电话',
|
|
|
|
|
+ reporter: '',
|
|
|
|
|
+ contactPhone: '',
|
|
|
|
|
+ eventLocation: '',
|
|
|
|
|
+ eventDescription: plan.eventDesc || ''
|
|
|
|
|
+ }
|
|
|
|
|
+ dispatchForm.value = { instruction: '', forces: [], commandCenter: false, casualties: '' }
|
|
|
|
|
+ decisionForm.value = { responseLevel: '三级响应', command: '', evacuationRange: '', reinforcement: false, remark: '' }
|
|
|
|
|
+ releaseChannels.value = []
|
|
|
|
|
+ releaseContent.value = ''
|
|
|
|
|
+ monitoringInfo.value = []
|
|
|
|
|
+ ElMessage.success('演练已开始,请录入演练过程信息')
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('启动演练失败')
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+const stopDrill = async () => {
|
|
|
|
|
+ if (!currentDrillRecord.value) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ await saveAllRecord()
|
|
|
|
|
+ if (selectedPlanId.value) {
|
|
|
|
|
+ await updatePlanStatus(selectedPlanId.value, '已完成')
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isUnmounted) return
|
|
|
|
|
+ isDrilling.value = false
|
|
|
|
|
+ currentDrillStatus.value = '已完成'
|
|
|
|
|
+ currentDrillRecord.value.status = '已完成'
|
|
|
|
|
+ drillActions.value.push({ time: new Date().toLocaleString(), type: 'success', message: '演练结束,记录已保存' })
|
|
|
|
|
+ const plan = plans.value.find(p => p.id === selectedPlanId.value)
|
|
|
|
|
+ if (plan) {
|
|
|
|
|
+ plan.status = '已完成'
|
|
|
|
|
+ plan.progress = 100
|
|
|
|
|
+ }
|
|
|
|
|
+ ElMessage.success('演练已结束,记录已保存')
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ ElMessage.error('结束演练失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const viewDrillRecord = async (plan) => {
|
|
|
|
|
+ selectedPlanId.value = plan.id
|
|
|
|
|
+ currentPlan.value = plan
|
|
|
|
|
+ await loadDrillRecord(plan.id)
|
|
|
|
|
+ if (currentDrillRecord.value) {
|
|
|
|
|
+ currentDrillInfo.value = { planId: plan.id, status: currentDrillRecord.value.status || '已记录' }
|
|
|
|
|
+ currentDrillStatus.value = currentDrillRecord.value.status || '已记录'
|
|
|
|
|
+ if (!drillActions.value.length) {
|
|
|
|
|
+ drillActions.value = [
|
|
|
|
|
+ { time: currentDrillRecord.value.recordTime, type: 'info', message: `查看记录:${plan.name}` }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ElMessage.success('已加载演练记录')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.info('该计划暂无演练记录,请先开始演练')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 生命周期 ==========
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await Promise.all([loadData(), loadTemplates()])
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ isUnmounted = true
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
-.drill-layout {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
|
|
+/* ========== 实战演练管理容器 ==========
|
|
|
|
|
+.practical-drill-container {
|
|
|
|
|
+ padding: 20px;
|
|
|
min-height: calc(100vh - 160px);
|
|
min-height: calc(100vh - 160px);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.col-left {
|
|
|
|
|
- width: 25%;
|
|
|
|
|
|
|
+/* 顶部操作栏 */
|
|
|
|
|
+.drill-top-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+
|
|
|
|
|
+ .top-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .page-title {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 主布局 */
|
|
|
|
|
+.drill-main-layout {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 350px 1fr;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ min-height: calc(100vh - 220px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.left-column {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
|
|
|
|
+ gap: 20px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.col-right {
|
|
|
|
|
- width: 75%;
|
|
|
|
|
|
|
+.right-column {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
|
|
|
|
+ gap: 20px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 通用面板样式 */
|
|
|
.panel {
|
|
.panel {
|
|
|
- border: 1px solid #e4e7ed;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
.panel-header {
|
|
.panel-header {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 10px 14px;
|
|
|
|
|
- background: #f5f7fa;
|
|
|
|
|
- border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%);
|
|
|
|
|
+ border-bottom: 2px solid #e4e7ed;
|
|
|
font-weight: bold;
|
|
font-weight: bold;
|
|
|
- font-size: 14px;
|
|
|
|
|
|
|
+ font-size: 15px;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
|
|
|
|
|
span {
|
|
span {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- gap: 6px;
|
|
|
|
|
|
|
+ gap: 8px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.el-icon {
|
|
.el-icon {
|
|
|
color: #409eff;
|
|
color: #409eff;
|
|
|
|
|
+ font-size: 18px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.panel-body {
|
|
.panel-body {
|
|
|
- padding: 12px 14px;
|
|
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ max-height: 600px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 模板列表
|
|
|
|
|
-.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;
|
|
|
|
|
|
|
+/* 模板卡片 */
|
|
|
|
|
+.template-panel, .plan-panel {
|
|
|
|
|
+ .panel-body {
|
|
|
|
|
+ max-height: none;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 计划搜索
|
|
|
|
|
-.search-bar {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 10px;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.template-list, .plan-list {
|
|
|
|
|
+ .template-card,
|
|
|
|
|
+ .plan-card {
|
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-// 计划卡片
|
|
|
|
|
-.plan-item {
|
|
|
|
|
- border: 1px solid #ebeef5;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
- background: #fafafa;
|
|
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- &:last-child { margin-bottom: 0; }
|
|
|
|
|
|
|
+ .template-card-header,
|
|
|
|
|
+ .plan-card-content {
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
|
|
|
- .plan-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 6px;
|
|
|
|
|
|
|
+ h4 {
|
|
|
|
|
+ margin: 0 0 8px 0;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .plan-name {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
|
|
+ .template-name,
|
|
|
|
|
+ .plan-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
font-weight: bold;
|
|
font-weight: bold;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .plan-actions {
|
|
|
|
|
|
|
+ .template-meta,
|
|
|
|
|
+ .plan-category {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 2px;
|
|
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+
|
|
|
|
|
+ .meta-item,
|
|
|
|
|
+ .plan-date {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- .plan-details {
|
|
|
|
|
- .plan-row {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #606266;
|
|
|
|
|
- line-height: 1.8;
|
|
|
|
|
|
|
+ .plan-info-list {
|
|
|
|
|
+ .info-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .info-text {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .plan-label {
|
|
|
|
|
- color: #909399;
|
|
|
|
|
|
|
+ .info-value {
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .template-actions,
|
|
|
|
|
+ .plan-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ padding-top: 10px;
|
|
|
|
|
+ border-top: 1px dashed #ebeef5;
|
|
|
|
|
+
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 信息卡片网格
|
|
|
|
|
-.info-grid {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: 1fr 1fr;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
|
|
+.plan-status-badge {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ right: 10px;
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+
|
|
|
|
|
+ &.计划中 { background: #f0f0f0; color: #909399; }
|
|
|
|
|
+ &.进行中 { background: #fff4e6; color: #e6a23c; }
|
|
|
|
|
+ &.已完成 { background: #f0f9ff; color: #409eff; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.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;
|
|
|
|
|
|
|
+.search-filter {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- .el-icon { color: #409eff; }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+/* 当前演练状态 */
|
|
|
|
|
+.current-drill-panel {
|
|
|
|
|
+ .drill-status-summary {
|
|
|
|
|
+ .status-badge.large {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ background: #f0f9ff;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .info-content {
|
|
|
|
|
- .info-line {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #606266;
|
|
|
|
|
- line-height: 1.8;
|
|
|
|
|
|
|
+ .drill-timeline {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 统计分析
|
|
|
|
|
-.analysis-result {
|
|
|
|
|
- .analysis-time {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
- margin-bottom: 10px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+/* 记录面板 */
|
|
|
|
|
+.record-panel {
|
|
|
|
|
+ .record-section {
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ padding-top: 16px;
|
|
|
|
|
+ border-top: 1px dashed #ebeef5;
|
|
|
|
|
|
|
|
- .analysis-detail {
|
|
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- border: 1px solid #ebeef5;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- background: #fafafa;
|
|
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ padding-top: 0;
|
|
|
|
|
+ border-top: none;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- .detail-row {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: #606266;
|
|
|
|
|
- line-height: 1.8;
|
|
|
|
|
|
|
+ .section-title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
|
|
|
- .detail-label {
|
|
|
|
|
- color: #909399;
|
|
|
|
|
|
|
+ .section-title {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .section-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ color: #409eff;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- .analysis-eval {
|
|
|
|
|
- .eval-title {
|
|
|
|
|
|
|
+/* 分析面板 */
|
|
|
|
|
+.analysis-panel {
|
|
|
|
|
+ .analysis-summary {
|
|
|
|
|
+ .analysis-time {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .analysis-basic, .analysis-task, .analysis-comprehensive {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child { margin-bottom: 0; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .basic-title,
|
|
|
|
|
+ .task-title,
|
|
|
|
|
+ .comprehensive-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
font-weight: bold;
|
|
font-weight: bold;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 能力评估网格 */
|
|
|
|
|
+ .capability-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .capability-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .cap-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .cap-score {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 任务统计 */
|
|
|
|
|
+ .task-stats {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .task-stat-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .stat-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .stat-value {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+
|
|
|
|
|
+ &.highlight-blue { color: #409eff; }
|
|
|
|
|
+ &.highlight-green { color: #67c23a; }
|
|
|
|
|
+ &.highlight-orange { color: #e6a23c; }
|
|
|
|
|
+ &.highlight-red { color: #f56c6c; }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /* 综合评估 */
|
|
|
.eval-grid {
|
|
.eval-grid {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
- gap: 10px;
|
|
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .eval-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .eval-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .eval-value {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+
|
|
|
|
|
+ &.success { color: #67c23a; }
|
|
|
|
|
+ &.warning { color: #e6a23c; }
|
|
|
|
|
+ &.info { color: #409eff; }
|
|
|
|
|
+ &.danger { color: #f56c6c; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.level-high { border-left: 4px solid #67c23a; }
|
|
|
|
|
+ &.level-middle { border-left: 4px solid #e6a23c; }
|
|
|
|
|
+ &.level-medium { border-left: 4px solid #409eff; }
|
|
|
|
|
+ &.level-low { border-left: 4px solid #f56c6c; }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .eval-item {
|
|
|
|
|
- border: 1px solid #ebeef5;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+ .eval-description,
|
|
|
|
|
+ .suggestion-section {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
|
|
|
- .eval-label {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
|
|
+ strong {
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ display: block;
|
|
|
margin-bottom: 4px;
|
|
margin-bottom: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .eval-value {
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- color: #303133;
|
|
|
|
|
|
|
+ p, ul {
|
|
|
|
|
+ margin: 4px 0;
|
|
|
|
|
+ padding-left: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- &.highlight { color: #67c23a; }
|
|
|
|
|
|
|
+ li {
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 响应式布局 */
|
|
|
|
|
+@media (max-width: 1200px) {
|
|
|
|
|
+ .drill-main-layout {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .left-column,
|
|
|
|
|
+ .right-column {
|
|
|
|
|
+ min-height: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|