DisPracticalDrill.vue 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600
  1. <!--实战演练管理 - 基于业务场景的优化版-->
  2. <template>
  3. <div class="practical-drill-container">
  4. <!-- 顶部操作栏 -->
  5. <div class="drill-top-bar">
  6. <div class="top-left">
  7. <h2 class="page-title"><el-icon><TrendCharts /></el-icon>实战演练管理</h2>
  8. <el-tag type="warning" size="large">5.2.3.3.4 实战演练管理</el-tag>
  9. </div>
  10. <div class="top-right">
  11. <el-button type="primary" size="large" @click="handleCreatePlan">
  12. <el-icon><Plus /></el-icon>创建实战演练
  13. </el-button>
  14. <el-button size="large" @click="showAnalysisReport">
  15. <el-icon><Document /></el-icon>分析报表
  16. </el-button>
  17. </div>
  18. </div>
  19. <div class="drill-main-layout">
  20. <!-- ===== 左列 ===== -->
  21. <div class="left-column">
  22. <!-- 演练计划模板 -->
  23. <div class="panel template-panel">
  24. <div class="panel-header">
  25. <span><el-icon><Document /></el-icon> 演练计划模板</span>
  26. <el-button type="primary" size="small" @click="openTemplateDialog">
  27. <el-icon><Plus /></el-icon>新增模板
  28. </el-button>
  29. </div>
  30. <div class="panel-body">
  31. <div class="template-list">
  32. <el-empty v-if="templates.length === 0" description="暂无演练模板" :image-size="80" />
  33. <div v-for="tpl in templates" :key="tpl.id" class="template-card">
  34. <div class="template-card-header">
  35. <div class="template-name">{{ tpl.name }}</div>
  36. <el-tag :type="getCategoryTagType(tpl.category)" size="small">{{ getCategoryLabel(tpl.category) }}</el-tag>
  37. </div>
  38. <div class="template-meta">
  39. <span class="meta-item"><el-icon><Calendar /></el-icon>{{ tpl.createDate || '未知日期' }}</span>
  40. <span class="meta-item"><el-icon><User /></el-icon>{{ tpl.userCount || 0 }}人</span>
  41. </div>
  42. <div class="template-actions">
  43. <el-button link type="primary" size="small" @click.stop="openEditTemplate(tpl)">
  44. <el-icon><Edit /></el-icon>编辑
  45. </el-button>
  46. <el-button link type="success" size="small" @click.stop="applyTemplate(tpl)">
  47. <el-icon><SwitchButton /></el-icon>应用
  48. </el-button>
  49. <el-button link type="danger" size="small" @click.stop="confirmDeleteTemplate(tpl)">
  50. <el-icon><Delete /></el-icon>删除
  51. </el-button>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- 演练计划设置 -->
  58. <div class="panel plan-panel">
  59. <div class="panel-header">
  60. <span><el-icon><Setting /></el-icon> 演练计划设置</span>
  61. <div class="header-actions">
  62. <el-select v-model="planFilterStatus" placeholder="筛选状态" size="small" style="width: 90px; margin-right: 5px" clearable>
  63. <el-option label="全部" value="" />
  64. <el-option label="计划中" value="计划中" />
  65. <el-option label="进行中" value="进行中" />
  66. <el-option label="已完成" value="已完成" />
  67. </el-select>
  68. <el-button type="primary" size="small" @click="openPlanDialog">
  69. <el-icon><Plus /></el-icon>新建计划
  70. </el-button>
  71. </div>
  72. </div>
  73. <div class="panel-body">
  74. <div class="search-filter">
  75. <el-input v-model="planSearchText" placeholder="搜索演练名称/目标/项目" size="small" style="width: 200px" clearable>
  76. <template #prefix><el-icon><Search /></el-icon></template>
  77. </el-input>
  78. <el-button type="primary" size="small" @click="filterPlans">
  79. <el-icon><Search /></el-icon>查询
  80. </el-button>
  81. </div>
  82. <div class="plan-list">
  83. <el-empty v-if="filteredPlans.length === 0" description="暂无演练计划" :image-size="80" />
  84. <div
  85. v-for="plan in filteredPlans"
  86. :key="plan.id"
  87. :class="['plan-card', { active: selectedPlanId === plan.id }]"
  88. @click="selectPlan(plan)"
  89. >
  90. <div class="plan-status-badge" :class="plan.status">{{ plan.status }}</div>
  91. <div class="plan-card-content">
  92. <h4 class="plan-title">{{ plan.name }}</h4>
  93. <div class="plan-category">
  94. <el-tag :type="getCategoryTagType(plan.category)" size="small">{{ getCategoryLabel(plan.category) }}</el-tag>
  95. <span class="plan-date">{{ plan.endTime || plan.updateTime || '—' }}</span>
  96. </div>
  97. <div class="plan-info-list">
  98. <div class="info-item" v-if="plan.target">
  99. <el-icon><Flag /></el-icon>
  100. <span>目标:</span>
  101. <span class="info-text">{{ plan.target }}</span>
  102. </div>
  103. <div class="info-item" v-if="plan.project">
  104. <el-icon><List /></el-icon>
  105. <span>项目:</span>
  106. <span class="info-text">{{ plan.project }}</span>
  107. </div>
  108. <div class="info-item" v-if="plan.eventDesc">
  109. <el-icon><Bell /></el-icon>
  110. <span>事件:</span>
  111. <span class="info-text">{{ plan.eventDesc }}</span>
  112. </div>
  113. <div class="info-item" v-if="plan.personnel">
  114. <el-icon><User /></el-icon>
  115. <span>人员:</span>
  116. <span class="info-text">{{ plan.personnel }}</span>
  117. </div>
  118. <div class="info-item" v-if="plan.participantCount">
  119. <el-icon><UserFilled /></el-icon>
  120. <span>参与人数:</span>
  121. <span class="info-value">{{ plan.participantCount }}人</span>
  122. </div>
  123. <div class="info-item" v-if="plan.progress !== null && plan.progress !== undefined">
  124. <el-icon><CircleCheckFilled /></el-icon>
  125. <span>进度:</span>
  126. <el-progress :percentage="plan.progress" :status="plan.progress === 100 ? 'success' : ''" :stroke-width="6" />
  127. </div>
  128. </div>
  129. <div class="plan-actions">
  130. <el-button link type="primary" size="small" @click.stop="editPlan(plan)">
  131. <el-icon><Edit /></el-icon>编辑
  132. </el-button>
  133. <el-button link type="warning" size="small" @click.stop="startDrill(plan)">
  134. <el-icon><VideoPlay /></el-icon>开始演练
  135. </el-button>
  136. <el-button link type="success" size="small" @click.stop="viewDrillRecord(plan)">
  137. <el-icon><Document /></el-icon>查看记录
  138. </el-button>
  139. <el-button link type="danger" size="small" @click.stop="confirmDeletePlan(plan)">
  140. <el-icon><Delete /></el-icon>删除
  141. </el-button>
  142. </div>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. <!-- ===== 右列 ===== -->
  150. <div class="right-column">
  151. <!-- 当前演练状态 -->
  152. <div class="panel current-drill-panel" v-if="currentDrillInfo">
  153. <div class="panel-header">
  154. <span><el-icon><Monitor /></el-icon> 当前演练状态</span>
  155. <el-button v-if="isDrilling" type="danger" size="small" @click="stopDrill">
  156. <el-icon><VideoPause /></el-icon>结束演练
  157. </el-button>
  158. </div>
  159. <div class="panel-body">
  160. <div class="drill-status-summary">
  161. <div class="status-badge large" :class="currentDrillStatus">
  162. <el-icon><CircleCheckFilled /></el-icon>
  163. {{ currentDrillStatus }}
  164. </div>
  165. <div class="drill-timeline">
  166. <el-timeline>
  167. <el-timeline-item
  168. v-for="(action, index) in drillActions"
  169. :key="index"
  170. :timestamp="action.time"
  171. placement="top"
  172. :type="action.type"
  173. >
  174. {{ action.message }}
  175. </el-timeline-item>
  176. </el-timeline>
  177. </div>
  178. </div>
  179. </div>
  180. </div>
  181. <!-- 演练结果记录 -->
  182. <div class="panel record-panel">
  183. <div class="panel-header">
  184. <span><el-icon><DataAnalysis /></el-icon> 演练结果记录</span>
  185. <el-button type="primary" size="small" @click="saveAllRecord">
  186. <el-icon><CircleCheckFilled /></el-icon>保存记录
  187. </el-button>
  188. </div>
  189. <div class="panel-body">
  190. <el-descriptions :column="2" border v-if="currentDrillRecord">
  191. <el-descriptions-item label="演练项目">{{ currentDrillRecord.drillProjectName }}</el-descriptions-item>
  192. <el-descriptions-item label="记录时间">{{ currentDrillRecord.recordTime }}</el-descriptions-item>
  193. <el-descriptions-item label="演练类型">{{ currentDrillRecord.drillType }}</el-descriptions-item>
  194. <el-descriptions-item label="参演部门">{{ currentDrillRecord.participatingDept }}</el-descriptions-item>
  195. <el-descriptions-item label="演练负责人" :span="2">{{ currentDrillRecord.responsiblePerson }}</el-descriptions-item>
  196. </el-descriptions>
  197. <!-- 监测监控信息 -->
  198. <div class="record-section">
  199. <div class="section-title-row">
  200. <h4 class="section-title"><el-icon><Monitor /></el-icon> 监测监控信息</h4>
  201. <el-button type="primary" size="small" @click="openAddMonitoring">
  202. <el-icon><Plus /></el-icon>添加监测数据
  203. </el-button>
  204. </div>
  205. <el-table :data="monitoringInfo" stripe size="small" style="width: 100%" max-height="200">
  206. <el-table-column prop="sensorType" label="传感器类型" min-width="120" />
  207. <el-table-column prop="location" label="监测位置" min-width="140" show-overflow-tooltip />
  208. <el-table-column prop="value" label="监测数值" width="100" />
  209. <el-table-column prop="unit" label="单位" width="70" />
  210. <el-table-column prop="alarmLevel" label="报警等级" width="90">
  211. <template #default="{ row }">
  212. <el-tag :type="getAlarmLevelType(row.alarmLevel)" size="small">{{ row.alarmLevel }}</el-tag>
  213. </template>
  214. </el-table-column>
  215. <el-table-column prop="time" label="监测时间" width="150" />
  216. <el-table-column label="操作" width="120" fixed="right">
  217. <template #default="{ row }">
  218. <el-button link type="primary" size="small" @click="openEditMonitoring(row)">编辑</el-button>
  219. <el-button link type="danger" size="small" @click="confirmDeleteMonitoring(row)">删除</el-button>
  220. </template>
  221. </el-table-column>
  222. </el-table>
  223. <el-empty v-if="monitoringInfo.length === 0" description="暂无监测数据" :image-size="60" />
  224. </div>
  225. <!-- 接警信息 -->
  226. <div class="record-section" v-if="currentDrillRecord">
  227. <h4 class="section-title"><el-icon><Phone /></el-icon> 接警信息</h4>
  228. <el-descriptions :column="2" border>
  229. <el-descriptions-item label="接警时间">{{ currentDrillRecord.reportTime || '—' }}</el-descriptions-item>
  230. <el-descriptions-item label="报警方式">{{ currentDrillRecord.reportMethod || '电话' }}</el-descriptions-item>
  231. <el-descriptions-item label="报警人" :span="2">{{ currentDrillRecord.reporter || '—' }}</el-descriptions-item>
  232. <el-descriptions-item label="联系电话" :span="2">{{ currentDrillRecord.contactPhone || '—' }}</el-descriptions-item>
  233. <el-descriptions-item label="事件地点" :span="2">{{ currentDrillRecord.eventLocation || '—' }}</el-descriptions-item>
  234. <el-descriptions-item label="事件描述" :span="2">
  235. <el-input v-model="currentDrillRecord.eventDescription" type="textarea" :rows="2" placeholder="请输入事件描述" size="small" />
  236. </el-descriptions-item>
  237. </el-descriptions>
  238. </div>
  239. <!-- 指挥调度信息 -->
  240. <div class="record-section">
  241. <h4 class="section-title"><el-icon><Guide /></el-icon> 指挥调度信息</h4>
  242. <el-form :model="dispatchForm" label-width="80px" size="small">
  243. <el-form-item label="调度指令">
  244. <el-input v-model="dispatchForm.instruction" type="textarea" :rows="2" placeholder="请输入调度指令" />
  245. </el-form-item>
  246. <el-form-item label="调派力量">
  247. <el-select v-model="dispatchForm.forces" multiple placeholder="请选择调派力量" style="width: 100%">
  248. <el-option label="消防中队" value="消防中队" />
  249. <el-option label="危化品处置小组" value="危化品处置小组" />
  250. <el-option label="医疗救援队" value="医疗救援队" />
  251. <el-option label="应急抢险队" value="应急抢险队" />
  252. <el-option label="公安警力" value="公安警力" />
  253. </el-select>
  254. </el-form-item>
  255. <el-form-item label="现场指挥部">
  256. <el-switch v-model="dispatchForm.commandCenter" />
  257. </el-form-item>
  258. <el-form-item label="人员伤亡">
  259. <el-input v-model="dispatchForm.casualties" placeholder="暂无人员伤亡报告" />
  260. </el-form-item>
  261. </el-form>
  262. </div>
  263. <!-- 决策信息 -->
  264. <div class="record-section">
  265. <h4 class="section-title"><el-icon><Operation /></el-icon> 决策信息</h4>
  266. <el-form :model="decisionForm" label-width="80px" size="small">
  267. <el-form-item label="响应级别">
  268. <el-select v-model="decisionForm.responseLevel" style="width: 100%">
  269. <el-option label="一级响应" value="一级响应" />
  270. <el-option label="二级响应" value="二级响应" />
  271. <el-option label="三级响应" value="三级响应" />
  272. <el-option label="四级响应" value="四级响应" />
  273. </el-select>
  274. </el-form-item>
  275. <el-form-item label="总指挥命令">
  276. <el-input v-model="decisionForm.command" type="textarea" :rows="2" placeholder="请输入总指挥命令" />
  277. </el-form-item>
  278. <el-form-item label="疏散范围">
  279. <el-input v-model="decisionForm.evacuationRange" placeholder="请输入疏散范围" />
  280. </el-form-item>
  281. <el-form-item label="增援请求">
  282. <el-switch v-model="decisionForm.reinforcement" />
  283. </el-form-item>
  284. <el-form-item label="备注说明">
  285. <el-input v-model="decisionForm.remark" type="textarea" :rows="2" placeholder="请输入备注说明" />
  286. </el-form-item>
  287. </el-form>
  288. </div>
  289. <!-- 对外发布信息 -->
  290. <div class="record-section">
  291. <h4 class="section-title"><el-icon><ChatDotRound /></el-icon> 对外发布信息</h4>
  292. <el-form label-width="80px" size="small">
  293. <el-form-item label="发布渠道">
  294. <el-checkbox-group v-model="releaseChannels">
  295. <el-checkbox label="官方微博">官方微博</el-checkbox>
  296. <el-checkbox label="微信公众号">微信公众号</el-checkbox>
  297. <el-checkbox label="门户网站">门户网站</el-checkbox>
  298. <el-checkbox label="媒体发布">媒体发布</el-checkbox>
  299. <el-checkbox label="短信通知">短信通知</el-checkbox>
  300. </el-checkbox-group>
  301. </el-form-item>
  302. <el-form-item label="发布内容">
  303. <el-input v-model="releaseContent" type="textarea" :rows="4" placeholder="请输入对外发布信息内容" />
  304. </el-form-item>
  305. <el-form-item>
  306. <el-button type="primary" size="small" @click="saveReleaseInfo">保存并发布</el-button>
  307. <el-button size="small" @click="previewRelease">预览</el-button>
  308. </el-form-item>
  309. </el-form>
  310. </div>
  311. </div>
  312. </div>
  313. <!-- 统计分析子系统结果 -->
  314. <div class="panel analysis-panel">
  315. <div class="panel-header">
  316. <span><el-icon><TrendCharts /></el-icon> 统计分析子系统结果</span>
  317. <div class="header-actions">
  318. <el-button type="warning" size="small" @click="sendToAnalysisSubsystems" :disabled="!canSendToSubsystems">
  319. <el-icon><Share /></el-icon>发送至子系统和评估系统
  320. </el-button>
  321. <el-button type="success" size="small" @click="exportAnalysisReport">
  322. <el-icon><Download /></el-icon>导出分析报告
  323. </el-button>
  324. </div>
  325. </div>
  326. <div class="panel-body">
  327. <div class="analysis-summary" v-if="analysisResult">
  328. <div class="analysis-time">
  329. <el-icon><Clock /></el-icon>
  330. 分析时间:{{ analysisResult.analyzeTime || '--:--:--' }}
  331. </div>
  332. <div class="analysis-basic">
  333. <h4 class="basic-title">演练能力评估</h4>
  334. <div class="capability-grid">
  335. <div class="capability-item">
  336. <div class="cap-label">技能掌握</div>
  337. <div class="cap-score">{{ analysisResult.skillScore }}/100</div>
  338. <el-progress :percentage="analysisResult.skillScore" :stroke-width="8" />
  339. </div>
  340. <div class="capability-item">
  341. <div class="cap-label">装备使用</div>
  342. <div class="cap-score">{{ analysisResult.equipmentScore }}/100</div>
  343. <el-progress :percentage="analysisResult.equipmentScore" :stroke-width="8" />
  344. </div>
  345. <div class="capability-item">
  346. <div class="cap-label">响应时效</div>
  347. <div class="cap-score">{{ analysisResult.responseScore }}/100</div>
  348. <el-progress :percentage="analysisResult.responseScore" :stroke-width="8" />
  349. </div>
  350. <div class="capability-item">
  351. <div class="cap-label">协同配合</div>
  352. <div class="cap-score">{{ analysisResult.cooperationScore }}/100</div>
  353. <el-progress :percentage="analysisResult.cooperationScore" :stroke-width="8" />
  354. </div>
  355. </div>
  356. </div>
  357. <div class="analysis-task">
  358. <h4 class="task-title">任务执行情况</h4>
  359. <div class="task-stats">
  360. <div class="task-stat-item">
  361. <div class="stat-label">任务总数</div>
  362. <div class="stat-value highlight-blue">{{ analysisResult.totalTasks }}</div>
  363. </div>
  364. <div class="task-stat-item">
  365. <div class="stat-label">完成任务</div>
  366. <div class="stat-value highlight-green">{{ analysisResult.completedTasks }}</div>
  367. </div>
  368. <div class="task-stat-item">
  369. <div class="stat-label">完成率</div>
  370. <div class="stat-value highlight-orange">{{ analysisResult.completionRate }}%</div>
  371. </div>
  372. <div class="task-stat-item">
  373. <div class="stat-label">超时任务</div>
  374. <div class="stat-value highlight-red">{{ analysisResult.overtimeTasks }}</div>
  375. </div>
  376. </div>
  377. </div>
  378. <div class="analysis-comprehensive">
  379. <h4 class="comprehensive-title">综合评估结果</h4>
  380. <div class="eval-grid">
  381. <div class="eval-item">
  382. <div class="eval-label">综合等级</div>
  383. <div class="eval-value" :class="{ success: analysisResult.overallLevel === '优秀', warning: analysisResult.overallLevel === '良好', info: analysisResult.overallLevel === '合格', danger: analysisResult.overallLevel === '待改进' }">
  384. {{ analysisResult.overallLevel }}
  385. </div>
  386. </div>
  387. </div>
  388. <div class="eval-description">
  389. <strong>评估结论:</strong>
  390. <p>{{ analysisResult.evalConclusion || '无评估结论' }}</p>
  391. </div>
  392. <div class="suggestion-section">
  393. <strong>改进建议:</strong>
  394. <ul>
  395. <li v-for="(sug, idx) in analysisResult.improvementSuggestions" :key="idx">{{ sug }}</li>
  396. </ul>
  397. </div>
  398. </div>
  399. </div>
  400. <el-empty v-else description="暂无统计分析结果,请点击“分析报表”加载" :image-size="100" />
  401. </div>
  402. </div>
  403. </div>
  404. </div>
  405. <!-- 弹窗:模板管理 -->
  406. <el-dialog
  407. v-model="templateDialogVisible"
  408. :title="templateDialogTitle"
  409. width="600px"
  410. destroy-on-close
  411. >
  412. <el-form :model="templateForm" label-width="90px">
  413. <el-form-item label="模板名称">
  414. <el-input v-model="templateForm.name" placeholder="请输入演练模板名称" />
  415. </el-form-item>
  416. <el-form-item label="所属系统">
  417. <el-select v-model="templateForm.category" style="width: 100%">
  418. <el-option label="燃气管网" value="燃气" />
  419. <el-option label="排水管网" value="排水" />
  420. <el-option label="供水管网" value="供水" />
  421. </el-select>
  422. </el-form-item>
  423. <el-form-item label="演练目标">
  424. <el-input
  425. v-model="templateForm.target"
  426. type="textarea"
  427. :rows="2"
  428. placeholder="请输入演练目标"
  429. />
  430. </el-form-item>
  431. <el-form-item label="演练项目">
  432. <el-input
  433. v-model="templateForm.project"
  434. type="textarea"
  435. :rows="3"
  436. placeholder="请输入演练项目详情"
  437. />
  438. </el-form-item>
  439. <el-form-item label="涉及事件">
  440. <el-input
  441. v-model="templateForm.eventDesc"
  442. type="textarea"
  443. :rows="2"
  444. placeholder="请输入可能涉及的事件类型"
  445. />
  446. </el-form-item>
  447. <el-form-item label="参与人员">
  448. <el-input
  449. v-model="templateForm.personnel"
  450. type="textarea"
  451. :rows="2"
  452. placeholder="例如:指挥中心、消防队、医疗组等"
  453. />
  454. </el-form-item>
  455. <el-form-item label="完成时限">
  456. <el-date-picker
  457. v-model="templateForm.dueDate"
  458. type="date"
  459. placeholder="选择截止日期"
  460. format="YYYY-MM-DD"
  461. date-format="YYYY-MM-DD"
  462. style="width: 100%"
  463. />
  464. </el-form-item>
  465. <el-form-item label="预案内容">
  466. <el-input
  467. v-model="templateForm.content"
  468. type="textarea"
  469. :rows="4"
  470. placeholder="请输入详细的预案内容和操作流程"
  471. />
  472. </el-form-item>
  473. </el-form>
  474. <template #footer>
  475. <el-button @click="closeTemplateDialog">取消</el-button>
  476. <el-button type="primary" @click="submitTemplateForm">确定</el-button>
  477. </template>
  478. </el-dialog>
  479. <!-- 弹窗:计划管理 -->
  480. <el-dialog
  481. v-model="planDialogVisible"
  482. :title="planDialogTitle"
  483. width="600px"
  484. destroy-on-close
  485. >
  486. <el-form :model="planForm" ref="planFormRef" label-width="90px">
  487. <el-form-item label="演练名称" required>
  488. <el-input v-model="planForm.name" placeholder="请输入演练名称" />
  489. </el-form-item>
  490. <el-form-item label="所属系统" required>
  491. <el-select v-model="planForm.category" style="width: 100%">
  492. <el-option label="燃气管网" value="燃气" />
  493. <el-option label="排水管网" value="排水" />
  494. <el-option label="供水管网" value="供水" />
  495. </el-select>
  496. </el-form-item>
  497. <el-form-item label="演练目标" required>
  498. <el-input
  499. v-model="planForm.target"
  500. type="textarea"
  501. :rows="2"
  502. placeholder="请输入演练目标"
  503. />
  504. </el-form-item>
  505. <el-form-item label="演练项目" required>
  506. <el-input
  507. v-model="planForm.project"
  508. type="textarea"
  509. :rows="3"
  510. placeholder="请输入演练项目详情"
  511. />
  512. </el-form-item>
  513. <el-form-item label="事件描述">
  514. <el-input
  515. v-model="planForm.eventDesc"
  516. type="textarea"
  517. :rows="2"
  518. placeholder="请输入模拟事件的描述"
  519. />
  520. </el-form-item>
  521. <el-form-item label="参与人员" required>
  522. <el-input
  523. v-model="planForm.personnel"
  524. type="textarea"
  525. :rows="2"
  526. placeholder="请输入参与演练的人员或部门"
  527. />
  528. </el-form-item>
  529. <el-form-item label="完成时间">
  530. <el-date-picker
  531. v-model="planForm.endTime"
  532. type="date"
  533. placeholder="选择完成时间"
  534. format="YYYY-MM-DD"
  535. date-format="YYYY-MM-DD"
  536. style="width: 100%"
  537. />
  538. </el-form-item>
  539. <el-form-item label="参与人数">
  540. <el-input-number
  541. v-model="planForm.participantCount"
  542. :min="1"
  543. :max="1000"
  544. style="width: 100%"
  545. />
  546. </el-form-item>
  547. <el-form-item label="演练进度">
  548. <el-slider v-model="planForm.progress" :min="0" :max="100" :step="10" />
  549. </el-form-item>
  550. <el-form-item label="状态" required>
  551. <el-select v-model="planForm.status" style="width: 100%">
  552. <el-option label="计划中" value="计划中" />
  553. <el-option label="进行中" value="进行中" />
  554. <el-option label="已完成" value="已完成" />
  555. </el-select>
  556. </el-form-item>
  557. </el-form>
  558. <template #footer>
  559. <el-button @click="planDialogVisible = false">取消</el-button>
  560. <el-button type="primary" @click="submitPlanForm">确定</el-button>
  561. </template>
  562. </el-dialog>
  563. <!-- 弹窗:监测数据 -->
  564. <el-dialog v-model="monitoringDialogVisible" :title="monitoringDialogTitle" width="500px" destroy-on-close>
  565. <el-form :model="monitoringForm" label-width="90px">
  566. <el-form-item label="传感器类型" required>
  567. <el-input v-model="monitoringForm.sensorType" placeholder="如:燃气浓度传感器" />
  568. </el-form-item>
  569. <el-form-item label="监测位置" required>
  570. <el-input v-model="monitoringForm.location" placeholder="如:XX路XX号管点" />
  571. </el-form-item>
  572. <el-form-item label="监测数值">
  573. <el-input v-model="monitoringForm.value" placeholder="如:50" />
  574. </el-form-item>
  575. <el-form-item label="单位">
  576. <el-input v-model="monitoringForm.unit" placeholder="如:%LEL" />
  577. </el-form-item>
  578. <el-form-item label="报警等级">
  579. <el-select v-model="monitoringForm.alarmLevel" style="width: 100%">
  580. <el-option label="一般" value="一般" />
  581. <el-option label="较大" value="较大" />
  582. <el-option label="重大" value="重大" />
  583. <el-option label="特别重大" value="特别重大" />
  584. </el-select>
  585. </el-form-item>
  586. <el-form-item label="监测时间">
  587. <el-input v-model="monitoringForm.time" placeholder="如:2024-01-01 12:00:00" />
  588. </el-form-item>
  589. </el-form>
  590. <template #footer>
  591. <el-button @click="monitoringDialogVisible = false">取消</el-button>
  592. <el-button type="primary" @click="submitMonitoringForm">确定</el-button>
  593. </template>
  594. </el-dialog>
  595. </div>
  596. </template>
  597. <script setup>
  598. import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
  599. import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
  600. import {
  601. Document, Edit, Delete, Setting, DataAnalysis, Monitor,
  602. Phone, Guide, Operation, ChatDotRound, TrendCharts, Plus, Calendar,
  603. User, SwitchButton, Search, Flag, Bell, List, Share, Download,
  604. Clock, VideoPlay, VideoPause, UserFilled, CircleCheckFilled
  605. } from '@element-plus/icons-vue'
  606. import {
  607. getPracticalTemplates, savePracticalTemplate, updatePracticalTemplate, deletePracticalTemplate,
  608. getPracticalPlans, savePracticalPlan, updatePracticalPlan, deletePracticalPlan,
  609. sendDrillAnalysis,
  610. getDrillRecord, saveDrillRecord, updateDrillRecord,
  611. getDrillMonitoring, saveDrillMonitoring, deleteDrillMonitoring,
  612. getDrillAnalysis, exportDrillReport, updatePlanStatus
  613. } from '@/api/pipeNetwork/basic'
  614. // ========== 卸载保护 ==========
  615. let isUnmounted = false
  616. // ========== 演练模板相关 ==========
  617. const templates = ref([])
  618. const templateDialogVisible = ref(false)
  619. const templateDialogTitle = ref('新增演练模板')
  620. const templateForm = ref({
  621. id: null, name: '', category: '燃气', target: '', project: '', eventDesc: '',
  622. personnel: '', dueDate: null, content: ''
  623. })
  624. const openTemplateDialog = () => {
  625. templateForm.value = {
  626. id: null, name: '', category: '燃气', target: '', project: '',
  627. eventDesc: '', personnel: '', dueDate: null, content: ''
  628. }
  629. templateDialogTitle.value = '新增演练模板'
  630. templateDialogVisible.value = true
  631. }
  632. const closeTemplateDialog = () => { templateDialogVisible.value = false }
  633. const openEditTemplate = (tpl) => {
  634. templateForm.value = { ...tpl, dueDate: tpl.dueDate || null }
  635. templateDialogTitle.value = '编辑演练模板'
  636. templateDialogVisible.value = true
  637. }
  638. const submitTemplateForm = async () => {
  639. if (!templateForm.value.name) {
  640. ElMessage.warning('请输入模板名称')
  641. return
  642. }
  643. try {
  644. const d = templateForm.value
  645. if (d.id) await updatePracticalTemplate(d)
  646. else await savePracticalTemplate(d)
  647. if (isUnmounted) return
  648. ElMessage.success('保存成功')
  649. templateDialogVisible.value = false
  650. loadTemplates()
  651. } catch (e) {
  652. ElMessage.error('保存失败')
  653. }
  654. }
  655. // 应用模板 → 打开计划弹窗并预填模板数据
  656. const applyTemplate = (tpl) => {
  657. planForm.value = {
  658. id: null, name: tpl.name, category: tpl.category || '燃气',
  659. target: tpl.target || '', project: tpl.project || '',
  660. eventDesc: tpl.eventDesc || '', personnel: tpl.personnel || '',
  661. endTime: tpl.dueDate || null, participantCount: 1, progress: 0, status: '计划中'
  662. }
  663. planDialogTitle.value = '基于模板创建演练计划'
  664. planDialogVisible.value = true
  665. ElMessage.info(`已应用模板「${tpl.name}」,请完善计划信息`)
  666. }
  667. const confirmDeleteTemplate = (tpl) => {
  668. ElMessageBox.confirm('确认删除此演练模板?', '提示', { type: 'warning' })
  669. .then(async () => {
  670. await deletePracticalTemplate(tpl.id)
  671. ElMessage.success('删除成功')
  672. loadTemplates()
  673. })
  674. .catch(() => {})
  675. }
  676. async function loadTemplates() {
  677. try {
  678. const res = await getPracticalTemplates()
  679. if (isUnmounted) return
  680. const rows = res.data || []
  681. templates.value = rows.map(d => ({
  682. id: d.id, name: d.name, category: d.category || '燃气',
  683. target: d.target, project: d.project, eventDesc: d.eventDesc,
  684. personnel: d.personnel, dueDate: d.dueDate,
  685. content: d.content, createDate: d.createDate || d.updateTime,
  686. userCount: d.userCount || 0
  687. }))
  688. } catch (e) {
  689. console.error('加载模板失败', e)
  690. }
  691. }
  692. const getCategoryTagType = (category) => {
  693. const types = { 燃气: 'danger', 排水: 'warning', 供水: 'primary' }
  694. return types[category] || 'info'
  695. }
  696. const getCategoryLabel = (category) => {
  697. const labels = { 燃气: '燃气管网', 排水: '排水管网', 供水: '供水管网' }
  698. return labels[category] || category
  699. }
  700. // ========== 演练计划相关 ==========
  701. const planSearchText = ref('')
  702. const planFilterStatus = ref('')
  703. const plans = ref([])
  704. const filteredPlans = computed(() => {
  705. let result = plans.value
  706. if (planFilterStatus.value) {
  707. result = result.filter(p => p.status === planFilterStatus.value)
  708. }
  709. if (planSearchText.value) {
  710. const keyword = planSearchText.value.toLowerCase()
  711. result = result.filter(p =>
  712. p.name?.toLowerCase().includes(keyword) ||
  713. p.target?.toLowerCase().includes(keyword) ||
  714. p.project?.toLowerCase().includes(keyword)
  715. )
  716. }
  717. return result
  718. })
  719. const selectedPlanId = ref(null)
  720. const currentPlan = ref(null)
  721. // 选中计划 → 加载关联的演练记录
  722. const selectPlan = async (plan) => {
  723. selectedPlanId.value = plan.id
  724. currentPlan.value = plan
  725. await loadDrillRecord(plan.id)
  726. }
  727. const planDialogVisible = ref(false)
  728. const planDialogTitle = ref('新建演练计划')
  729. const planForm = ref({
  730. id: null, name: '', category: '燃气', target: '', project: '',
  731. eventDesc: '', personnel: '', endTime: null, participantCount: 1, progress: 0, status: '计划中'
  732. })
  733. const planFormRef = ref(null)
  734. const openPlanDialog = () => {
  735. planForm.value = {
  736. id: null, name: '', category: '燃气', target: '', project: '',
  737. eventDesc: '', personnel: '', endTime: null, participantCount: 1, progress: 0, status: '计划中'
  738. }
  739. planDialogTitle.value = '新建演练计划'
  740. planDialogVisible.value = true
  741. }
  742. const editPlan = (plan) => {
  743. planForm.value = { ...plan, endTime: plan.endTime || null }
  744. planDialogTitle.value = '编辑演练计划'
  745. planDialogVisible.value = true
  746. }
  747. const submitPlanForm = async () => {
  748. if (!planForm.value.name) {
  749. ElMessage.warning('请输入演练名称')
  750. return
  751. }
  752. try {
  753. const d = planForm.value
  754. if (d.id) await updatePracticalPlan(d)
  755. else await savePracticalPlan(d)
  756. if (isUnmounted) return
  757. ElMessage.success('保存成功')
  758. planDialogVisible.value = false
  759. loadData()
  760. } catch (e) {
  761. ElMessage.error('保存失败')
  762. }
  763. }
  764. const confirmDeletePlan = (plan) => {
  765. ElMessageBox.confirm('确认删除此演练计划?', '提示', { type: 'warning' })
  766. .then(async () => {
  767. await deletePracticalPlan(plan.id)
  768. ElMessage.success('删除成功')
  769. if (selectedPlanId.value === plan.id) {
  770. selectedPlanId.value = null
  771. currentPlan.value = null
  772. currentDrillRecord.value = null
  773. currentDrillInfo.value = null
  774. }
  775. loadData()
  776. })
  777. .catch(() => {})
  778. }
  779. const handleCreatePlan = () => { openPlanDialog() }
  780. const filterPlans = () => { /* 过滤由 computed 自动处理 */ }
  781. async function loadData() {
  782. try {
  783. const res = await getPracticalPlans()
  784. if (isUnmounted) return
  785. const rows = res.data || []
  786. plans.value = rows.map(d => ({
  787. id: d.id, name: d.name, category: d.category || '燃气',
  788. target: d.target, project: d.project, eventDesc: d.eventDesc || d.event,
  789. personnel: d.personnel, status: d.status || '计划中',
  790. endTime: d.endTime || d.updateTime, participantCount: d.participantCount || 0,
  791. progress: d.progress !== undefined ? d.progress : 0
  792. }))
  793. } catch (e) {
  794. console.error('加载计划失败', e)
  795. }
  796. }
  797. // ========== 演练记录相关 ==========
  798. const currentDrillInfo = ref(null)
  799. const currentDrillRecord = ref(null)
  800. const isDrilling = ref(false)
  801. const currentDrillStatus = ref('未开始')
  802. const drillActions = ref([])
  803. const monitoringInfo = ref([])
  804. const dispatchForm = ref({ instruction: '', forces: [], commandCenter: false, casualties: '' })
  805. const decisionForm = ref({ responseLevel: '三级响应', command: '', evacuationRange: '', reinforcement: false, remark: '' })
  806. const releaseChannels = ref([])
  807. const releaseContent = ref('')
  808. // 加载演练记录
  809. async function loadDrillRecord(planId) {
  810. try {
  811. const res = await getDrillRecord(planId)
  812. if (isUnmounted) return
  813. const data = res.data
  814. if (data) {
  815. currentDrillRecord.value = data
  816. currentDrillInfo.value = { planId, status: data.status || '已记录' }
  817. currentDrillStatus.value = data.status || '已记录'
  818. if (data.dispatchInfo) dispatchForm.value = { ...dispatchForm.value, ...data.dispatchInfo }
  819. if (data.decisionInfo) decisionForm.value = { ...decisionForm.value, ...data.decisionInfo }
  820. if (data.releaseChannels) releaseChannels.value = data.releaseChannels
  821. if (data.releaseContent) releaseContent.value = data.releaseContent
  822. if (data.id) await loadMonitoring(data.id)
  823. if (data.actions) drillActions.value = data.actions
  824. } else {
  825. currentDrillRecord.value = null
  826. currentDrillInfo.value = null
  827. monitoringInfo.value = []
  828. drillActions.value = []
  829. }
  830. } catch (e) {
  831. currentDrillRecord.value = null
  832. currentDrillInfo.value = null
  833. monitoringInfo.value = []
  834. drillActions.value = []
  835. }
  836. }
  837. // 加载监测信息
  838. async function loadMonitoring(recordId) {
  839. if (!recordId) {
  840. monitoringInfo.value = []
  841. return
  842. }
  843. try {
  844. const res = await getDrillMonitoring(recordId)
  845. if (isUnmounted) return
  846. monitoringInfo.value = res.data || []
  847. } catch (e) {
  848. monitoringInfo.value = []
  849. }
  850. }
  851. // 保存全部演练记录
  852. const saveAllRecord = async () => {
  853. if (!currentDrillRecord.value) {
  854. ElMessage.warning('请先开始演练或选择有记录的计划')
  855. return
  856. }
  857. try {
  858. const recordData = {
  859. ...currentDrillRecord.value,
  860. planId: selectedPlanId.value,
  861. dispatchInfo: { ...dispatchForm.value },
  862. decisionInfo: { ...decisionForm.value },
  863. releaseChannels: releaseChannels.value,
  864. releaseContent: releaseContent.value,
  865. actions: drillActions.value
  866. }
  867. if (currentDrillRecord.value.id) {
  868. await updateDrillRecord(recordData)
  869. } else {
  870. const res = await saveDrillRecord(recordData)
  871. if (res.data) {
  872. const saved = typeof res.data === 'object' ? res.data : { id: res.data }
  873. currentDrillRecord.value.id = saved.id
  874. }
  875. }
  876. if (isUnmounted) return
  877. ElMessage.success('演练记录已保存')
  878. } catch (e) {
  879. ElMessage.error('保存失败')
  880. }
  881. }
  882. // ========== 监测信息弹窗 ==========
  883. const monitoringDialogVisible = ref(false)
  884. const monitoringDialogTitle = ref('添加监测数据')
  885. const monitoringForm = ref({
  886. id: null, sensorType: '', location: '', value: '', unit: '', alarmLevel: '一般', time: ''
  887. })
  888. const openAddMonitoring = () => {
  889. if (!currentDrillRecord.value) {
  890. ElMessage.warning('请先开始演练或加载演练记录')
  891. return
  892. }
  893. monitoringForm.value = {
  894. id: null, sensorType: '', location: '', value: '', unit: '', alarmLevel: '一般',
  895. time: new Date().toLocaleString()
  896. }
  897. monitoringDialogTitle.value = '添加监测数据'
  898. monitoringDialogVisible.value = true
  899. }
  900. const openEditMonitoring = (row) => {
  901. monitoringForm.value = { ...row }
  902. monitoringDialogTitle.value = '编辑监测数据'
  903. monitoringDialogVisible.value = true
  904. }
  905. const submitMonitoringForm = async () => {
  906. if (!monitoringForm.value.sensorType || !monitoringForm.value.location) {
  907. ElMessage.warning('请填写传感器类型和监测位置')
  908. return
  909. }
  910. try {
  911. const data = { ...monitoringForm.value, recordId: currentDrillRecord.value.id }
  912. await saveDrillMonitoring(data)
  913. if (isUnmounted) return
  914. ElMessage.success('保存成功')
  915. monitoringDialogVisible.value = false
  916. if (currentDrillRecord.value.id) await loadMonitoring(currentDrillRecord.value.id)
  917. } catch (e) {
  918. ElMessage.error('保存失败')
  919. }
  920. }
  921. const confirmDeleteMonitoring = (row) => {
  922. ElMessageBox.confirm('确认删除此监测数据?', '提示', { type: 'warning' })
  923. .then(async () => {
  924. await deleteDrillMonitoring(row.id)
  925. ElMessage.success('删除成功')
  926. if (currentDrillRecord.value?.id) await loadMonitoring(currentDrillRecord.value.id)
  927. })
  928. .catch(() => {})
  929. }
  930. const getAlarmLevelType = (level) => {
  931. const types = { 一般: 'info', 较大: 'warning', 重大: 'danger', 特别重大: 'danger' }
  932. return types[level] || 'info'
  933. }
  934. // ========== 对外发布 ==========
  935. const saveReleaseInfo = () => {
  936. if (!releaseChannels.value.length && !releaseContent.value) {
  937. ElMessage.warning('请填写发布渠道和内容')
  938. return
  939. }
  940. saveAllRecord()
  941. }
  942. const previewRelease = () => {
  943. if (!releaseContent.value) {
  944. ElMessage.warning('请先输入发布内容')
  945. return
  946. }
  947. ElMessageBox.alert(releaseContent.value, '发布内容预览', { confirmButtonText: '确定' })
  948. }
  949. // ========== 统计分析子系统 ==========
  950. const analysisResult = ref(null)
  951. const canSendToSubsystems = computed(() => !!currentDrillRecord.value)
  952. const showAnalysisReport = async () => {
  953. if (!selectedPlanId.value) {
  954. ElMessage.warning('请先选择演练计划')
  955. return
  956. }
  957. try {
  958. const res = await getDrillAnalysis(selectedPlanId.value)
  959. if (isUnmounted) return
  960. if (res.data) {
  961. analysisResult.value = res.data
  962. ElMessage.success('分析报表已加载')
  963. } else {
  964. ElMessage.info('暂无分析数据,请先完成演练并保存记录')
  965. }
  966. } catch (e) {
  967. ElMessage.error('加载分析数据失败')
  968. }
  969. }
  970. const exportAnalysisReport = async () => {
  971. if (!selectedPlanId.value) {
  972. ElMessage.warning('请先选择演练计划')
  973. return
  974. }
  975. try {
  976. const data = await exportDrillReport(selectedPlanId.value)
  977. const blob = new Blob(['\ufeff' + data], { type: 'text/plain;charset=utf-8' })
  978. const url = window.URL.createObjectURL(blob)
  979. const a = document.createElement('a')
  980. a.href = url
  981. a.download = `演练分析报告_${currentPlan.value?.name || ''}.txt`
  982. a.click()
  983. window.URL.revokeObjectURL(url)
  984. ElMessage.success('报告已导出')
  985. } catch (e) {
  986. ElMessage.error('导出失败')
  987. }
  988. }
  989. const sendToAnalysisSubsystems = async () => {
  990. if (!currentDrillRecord.value) {
  991. ElMessage.warning('请先录入演练结果信息')
  992. return
  993. }
  994. try {
  995. const sendData = {
  996. ...currentDrillRecord.value,
  997. planId: selectedPlanId.value,
  998. planName: currentPlan.value?.name,
  999. dispatchInfo: { ...dispatchForm.value },
  1000. decisionInfo: { ...decisionForm.value },
  1001. releaseChannels: releaseChannels.value,
  1002. releaseContent: releaseContent.value,
  1003. monitoringInfo: monitoringInfo.value,
  1004. actions: drillActions.value
  1005. }
  1006. await sendDrillAnalysis(sendData)
  1007. if (isUnmounted) return
  1008. ElNotification({
  1009. title: '发送成功',
  1010. message: '演练分析数据已成功发送至统计分析和应急评估子系统',
  1011. type: 'success',
  1012. duration: 3000
  1013. })
  1014. } catch (e) {
  1015. ElMessage.error('发送失败')
  1016. }
  1017. }
  1018. // ========== 演练流程控制 ==========
  1019. const startDrill = async (plan) => {
  1020. try {
  1021. await updatePlanStatus(plan.id, '进行中')
  1022. if (isUnmounted) return
  1023. plan.status = '进行中'
  1024. plan.progress = Math.max(plan.progress, 10)
  1025. selectedPlanId.value = plan.id
  1026. currentPlan.value = plan
  1027. isDrilling.value = true
  1028. currentDrillStatus.value = '进行中'
  1029. currentDrillInfo.value = { planId: plan.id, status: '进行中' }
  1030. drillActions.value = [
  1031. { time: new Date().toLocaleString(), type: 'primary', message: `开始演练:${plan.name}` }
  1032. ]
  1033. currentDrillRecord.value = {
  1034. drillProjectName: plan.name,
  1035. recordTime: new Date().toLocaleString(),
  1036. drillType: '实战演练',
  1037. participatingDept: plan.personnel || '—',
  1038. responsiblePerson: '—',
  1039. planId: plan.id,
  1040. status: '进行中',
  1041. reportTime: new Date().toLocaleString(),
  1042. reportMethod: '电话',
  1043. reporter: '',
  1044. contactPhone: '',
  1045. eventLocation: '',
  1046. eventDescription: plan.eventDesc || ''
  1047. }
  1048. dispatchForm.value = { instruction: '', forces: [], commandCenter: false, casualties: '' }
  1049. decisionForm.value = { responseLevel: '三级响应', command: '', evacuationRange: '', reinforcement: false, remark: '' }
  1050. releaseChannels.value = []
  1051. releaseContent.value = ''
  1052. monitoringInfo.value = []
  1053. ElMessage.success('演练已开始,请录入演练过程信息')
  1054. } catch (e) {
  1055. ElMessage.error('启动演练失败')
  1056. }
  1057. }
  1058. const stopDrill = async () => {
  1059. if (!currentDrillRecord.value) return
  1060. try {
  1061. await saveAllRecord()
  1062. if (selectedPlanId.value) {
  1063. await updatePlanStatus(selectedPlanId.value, '已完成')
  1064. }
  1065. if (isUnmounted) return
  1066. isDrilling.value = false
  1067. currentDrillStatus.value = '已完成'
  1068. currentDrillRecord.value.status = '已完成'
  1069. drillActions.value.push({ time: new Date().toLocaleString(), type: 'success', message: '演练结束,记录已保存' })
  1070. const plan = plans.value.find(p => p.id === selectedPlanId.value)
  1071. if (plan) {
  1072. plan.status = '已完成'
  1073. plan.progress = 100
  1074. }
  1075. ElMessage.success('演练已结束,记录已保存')
  1076. } catch (e) {
  1077. ElMessage.error('结束演练失败')
  1078. }
  1079. }
  1080. const viewDrillRecord = async (plan) => {
  1081. selectedPlanId.value = plan.id
  1082. currentPlan.value = plan
  1083. await loadDrillRecord(plan.id)
  1084. if (currentDrillRecord.value) {
  1085. currentDrillInfo.value = { planId: plan.id, status: currentDrillRecord.value.status || '已记录' }
  1086. currentDrillStatus.value = currentDrillRecord.value.status || '已记录'
  1087. if (!drillActions.value.length) {
  1088. drillActions.value = [
  1089. { time: currentDrillRecord.value.recordTime, type: 'info', message: `查看记录:${plan.name}` }
  1090. ]
  1091. }
  1092. ElMessage.success('已加载演练记录')
  1093. } else {
  1094. ElMessage.info('该计划暂无演练记录,请先开始演练')
  1095. }
  1096. }
  1097. // ========== 生命周期 ==========
  1098. onMounted(async () => {
  1099. try {
  1100. await Promise.all([loadData(), loadTemplates()])
  1101. } catch (e) {
  1102. console.error(e)
  1103. }
  1104. })
  1105. onBeforeUnmount(() => {
  1106. isUnmounted = true
  1107. })
  1108. </script>
  1109. <style scoped lang="scss">
  1110. /* ========== 实战演练管理容器 ==========
  1111. .practical-drill-container {
  1112. padding: 20px;
  1113. min-height: calc(100vh - 160px);
  1114. }
  1115. /* 顶部操作栏 */
  1116. .drill-top-bar {
  1117. display: flex;
  1118. justify-content: space-between;
  1119. align-items: center;
  1120. margin-bottom: 20px;
  1121. padding: 20px;
  1122. background: white;
  1123. border-radius: 8px;
  1124. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1125. .top-left {
  1126. display: flex;
  1127. align-items: center;
  1128. gap: 16px;
  1129. .page-title {
  1130. font-size: 24px;
  1131. font-weight: bold;
  1132. color: #303133;
  1133. margin: 0;
  1134. }
  1135. }
  1136. }
  1137. /* 主布局 */
  1138. .drill-main-layout {
  1139. display: grid;
  1140. grid-template-columns: 350px 1fr;
  1141. gap: 20px;
  1142. min-height: calc(100vh - 220px);
  1143. }
  1144. .left-column {
  1145. display: flex;
  1146. flex-direction: column;
  1147. gap: 20px;
  1148. }
  1149. .right-column {
  1150. display: flex;
  1151. flex-direction: column;
  1152. gap: 20px;
  1153. }
  1154. /* 通用面板样式 */
  1155. .panel {
  1156. background: white;
  1157. border-radius: 8px;
  1158. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1159. overflow: hidden;
  1160. .panel-header {
  1161. display: flex;
  1162. justify-content: space-between;
  1163. align-items: center;
  1164. padding: 14px 16px;
  1165. background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%);
  1166. border-bottom: 2px solid #e4e7ed;
  1167. font-weight: bold;
  1168. font-size: 15px;
  1169. color: #303133;
  1170. gap: 8px;
  1171. span {
  1172. display: flex;
  1173. align-items: center;
  1174. gap: 8px;
  1175. }
  1176. .el-icon {
  1177. color: #409eff;
  1178. font-size: 18px;
  1179. }
  1180. }
  1181. .panel-body {
  1182. padding: 16px;
  1183. max-height: 600px;
  1184. overflow-y: auto;
  1185. }
  1186. }
  1187. /* 模板卡片 */
  1188. .template-panel, .plan-panel {
  1189. .panel-body {
  1190. max-height: none;
  1191. }
  1192. }
  1193. .template-list, .plan-list {
  1194. .template-card,
  1195. .plan-card {
  1196. border: 1px solid #ebeef5;
  1197. border-radius: 6px;
  1198. padding: 12px;
  1199. margin-bottom: 10px;
  1200. background: #fafafa;
  1201. transition: all 0.3s;
  1202. cursor: pointer;
  1203. position: relative;
  1204. &:hover {
  1205. transform: translateY(-2px);
  1206. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1207. border-color: #409eff;
  1208. }
  1209. &.active {
  1210. border-left: 4px solid #409eff;
  1211. background: #ecf5ff;
  1212. }
  1213. .template-card-header,
  1214. .plan-card-content {
  1215. margin-bottom: 10px;
  1216. h4 {
  1217. margin: 0 0 8px 0;
  1218. font-size: 16px;
  1219. font-weight: bold;
  1220. color: #303133;
  1221. }
  1222. }
  1223. .template-name,
  1224. .plan-title {
  1225. font-size: 15px;
  1226. font-weight: bold;
  1227. color: #303133;
  1228. margin-bottom: 6px;
  1229. }
  1230. .template-meta,
  1231. .plan-category {
  1232. display: flex;
  1233. gap: 12px;
  1234. margin-bottom: 8px;
  1235. font-size: 12px;
  1236. color: #909399;
  1237. .meta-item,
  1238. .plan-date {
  1239. display: flex;
  1240. align-items: center;
  1241. gap: 4px;
  1242. }
  1243. }
  1244. .plan-info-list {
  1245. .info-item {
  1246. display: flex;
  1247. align-items: center;
  1248. gap: 6px;
  1249. font-size: 12px;
  1250. margin-bottom: 4px;
  1251. .info-text {
  1252. flex: 1;
  1253. color: #606266;
  1254. }
  1255. .info-value {
  1256. font-weight: bold;
  1257. color: #409eff;
  1258. }
  1259. }
  1260. }
  1261. .template-actions,
  1262. .plan-actions {
  1263. display: flex;
  1264. gap: 8px;
  1265. margin-top: 10px;
  1266. padding-top: 10px;
  1267. border-top: 1px dashed #ebeef5;
  1268. .el-button {
  1269. font-size: 12px;
  1270. }
  1271. }
  1272. }
  1273. }
  1274. .plan-status-badge {
  1275. position: absolute;
  1276. top: 10px;
  1277. right: 10px;
  1278. padding: 4px 10px;
  1279. border-radius: 12px;
  1280. font-size: 12px;
  1281. font-weight: bold;
  1282. &.计划中 { background: #f0f0f0; color: #909399; }
  1283. &.进行中 { background: #fff4e6; color: #e6a23c; }
  1284. &.已完成 { background: #f0f9ff; color: #409eff; }
  1285. }
  1286. .search-filter {
  1287. display: flex;
  1288. gap: 8px;
  1289. margin-bottom: 12px;
  1290. }
  1291. /* 当前演练状态 */
  1292. .current-drill-panel {
  1293. .drill-status-summary {
  1294. .status-badge.large {
  1295. display: inline-flex;
  1296. align-items: center;
  1297. gap: 8px;
  1298. padding: 12px 24px;
  1299. border-radius: 20px;
  1300. font-size: 16px;
  1301. font-weight: bold;
  1302. background: #f0f9ff;
  1303. color: #409eff;
  1304. margin-bottom: 16px;
  1305. }
  1306. .drill-timeline {
  1307. margin-top: 16px;
  1308. }
  1309. }
  1310. }
  1311. /* 记录面板 */
  1312. .record-panel {
  1313. .record-section {
  1314. margin-top: 20px;
  1315. padding-top: 16px;
  1316. border-top: 1px dashed #ebeef5;
  1317. &:first-child {
  1318. margin-top: 0;
  1319. padding-top: 0;
  1320. border-top: none;
  1321. }
  1322. .section-title-row {
  1323. display: flex;
  1324. justify-content: space-between;
  1325. align-items: center;
  1326. margin-bottom: 12px;
  1327. .section-title {
  1328. margin-bottom: 0;
  1329. }
  1330. }
  1331. .section-title {
  1332. display: flex;
  1333. align-items: center;
  1334. gap: 8px;
  1335. font-size: 14px;
  1336. font-weight: bold;
  1337. color: #303133;
  1338. margin-bottom: 12px;
  1339. .el-icon {
  1340. color: #409eff;
  1341. }
  1342. }
  1343. }
  1344. }
  1345. /* 分析面板 */
  1346. .analysis-panel {
  1347. .analysis-summary {
  1348. .analysis-time {
  1349. display: flex;
  1350. align-items: center;
  1351. gap: 8px;
  1352. font-size: 13px;
  1353. color: #909399;
  1354. margin-bottom: 16px;
  1355. }
  1356. .analysis-basic, .analysis-task, .analysis-comprehensive {
  1357. margin-bottom: 20px;
  1358. padding: 16px;
  1359. border: 1px solid #ebeef5;
  1360. border-radius: 8px;
  1361. background: #fafafa;
  1362. &:last-child { margin-bottom: 0; }
  1363. }
  1364. .basic-title,
  1365. .task-title,
  1366. .comprehensive-title {
  1367. font-size: 15px;
  1368. font-weight: bold;
  1369. color: #303133;
  1370. margin-bottom: 12px;
  1371. }
  1372. /* 能力评估网格 */
  1373. .capability-grid {
  1374. display: grid;
  1375. grid-template-columns: repeat(2, 1fr);
  1376. gap: 16px;
  1377. .capability-item {
  1378. display: flex;
  1379. flex-direction: column;
  1380. gap: 6px;
  1381. .cap-label {
  1382. font-size: 12px;
  1383. color: #606266;
  1384. }
  1385. .cap-score {
  1386. font-size: 18px;
  1387. font-weight: bold;
  1388. color: #409eff;
  1389. }
  1390. }
  1391. }
  1392. /* 任务统计 */
  1393. .task-stats {
  1394. display: grid;
  1395. grid-template-columns: repeat(4, 1fr);
  1396. gap: 12px;
  1397. .task-stat-item {
  1398. display: flex;
  1399. flex-direction: column;
  1400. gap: 4px;
  1401. text-align: center;
  1402. padding: 12px;
  1403. background: white;
  1404. border-radius: 6px;
  1405. .stat-label {
  1406. font-size: 12px;
  1407. color: #909399;
  1408. }
  1409. .stat-value {
  1410. font-size: 20px;
  1411. font-weight: bold;
  1412. &.highlight-blue { color: #409eff; }
  1413. &.highlight-green { color: #67c23a; }
  1414. &.highlight-orange { color: #e6a23c; }
  1415. &.highlight-red { color: #f56c6c; }
  1416. }
  1417. }
  1418. }
  1419. /* 综合评估 */
  1420. .eval-grid {
  1421. display: grid;
  1422. grid-template-columns: repeat(4, 1fr);
  1423. gap: 12px;
  1424. margin-bottom: 16px;
  1425. .eval-item {
  1426. display: flex;
  1427. flex-direction: column;
  1428. align-items: center;
  1429. padding: 16px;
  1430. background: white;
  1431. border-radius: 6px;
  1432. .eval-label {
  1433. font-size: 12px;
  1434. color: #909399;
  1435. margin-bottom: 8px;
  1436. }
  1437. .eval-value {
  1438. font-size: 18px;
  1439. font-weight: bold;
  1440. &.success { color: #67c23a; }
  1441. &.warning { color: #e6a23c; }
  1442. &.info { color: #409eff; }
  1443. &.danger { color: #f56c6c; }
  1444. }
  1445. &.level-high { border-left: 4px solid #67c23a; }
  1446. &.level-middle { border-left: 4px solid #e6a23c; }
  1447. &.level-medium { border-left: 4px solid #409eff; }
  1448. &.level-low { border-left: 4px solid #f56c6c; }
  1449. }
  1450. }
  1451. .eval-description,
  1452. .suggestion-section {
  1453. margin-top: 12px;
  1454. font-size: 13px;
  1455. line-height: 1.6;
  1456. color: #606266;
  1457. strong {
  1458. color: #303133;
  1459. display: block;
  1460. margin-bottom: 4px;
  1461. }
  1462. p, ul {
  1463. margin: 4px 0;
  1464. padding-left: 20px;
  1465. }
  1466. li {
  1467. margin-bottom: 4px;
  1468. }
  1469. }
  1470. }
  1471. }
  1472. /* 响应式布局 */
  1473. @media (max-width: 1200px) {
  1474. .drill-main-layout {
  1475. grid-template-columns: 1fr;
  1476. }
  1477. .left-column,
  1478. .right-column {
  1479. min-height: auto;
  1480. }
  1481. }
  1482. </style>