orderEdit.vue 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. <template>
  2. <div class="task-setting-container">
  3. <!-- 背景网格 -->
  4. <div class="grid-bg"></div>
  5. <!-- 顶部栏:标题 + 控制按钮 -->
  6. <div class="header">
  7. <div class="header-logo flex items-center">
  8. <i class="el-icon-shield text-red-500 mr-2 text-xl"></i>
  9. <h1>任务想定编辑</h1>
  10. <div class="info-value">{{ currentT0HMS }}</div>
  11. <small v-if="isRealtime">(实时)</small>
  12. </div>
  13. <div class="header-controls">
  14. <el-button class="btn军事" icon="el-icon-back" type="success" @click="goBack">返回</el-button>
  15. <el-button class="btn军事" icon="el-icon-save" type="primary" @click="saveTask">保存任务</el-button>
  16. <el-button class="btn军事" icon="el-icon-refresh" type="warning" @click="resetTask">重置</el-button>
  17. </div>
  18. </div>
  19. <!-- 三段式主布局 -->
  20. <el-container class="h-[calc(100vh-60px)] flex-row overflow-hidden">
  21. <!-- 左侧:装备树 -->
  22. <el-aside class="min-w-[240px] border-right border-[#0c4a6e] relative" width="10%">
  23. <div class="equipment-tree-container col-span-1">
  24. <div class="section-card h-full">
  25. <el-tree
  26. :data="equipmentTree"
  27. :expand-on-click-node="false"
  28. :indent="15"
  29. :props="defaultProps"
  30. class="equipment-tree"
  31. default-expand-all
  32. node-key="id"
  33. @node-click="handleEquipmentClick"
  34. />
  35. </div>
  36. </div>
  37. </el-aside>
  38. <!-- 中间:概览 + 甘特图 + 时序计划 -->
  39. <el-main class="p-0 h-full flex flex-col relative">
  40. <div class="main-border-decoration"></div>
  41. <div class="equipment-content-box flex-1 p-4 middle-layout">
  42. <!-- 1) 装备数量概览 -->
  43. <div class="section-card mb-4 overview-card">
  44. <div class="section-header">
  45. <h4 class="section-title">
  46. <i class="el-icon-dashboard mr-2"></i>
  47. 装备数量概览
  48. </h4>
  49. </div>
  50. <div class="equipment-status-grid p-4 grid grid-cols-4 gap-3">
  51. <div class="status-card p-3 border border-blue-500/30 rounded">
  52. <div class="status-title text-sm text-gray-400 mb-1">总装备数</div>
  53. <div class="status-value text-2xl font-bold">{{ stastic.sumTotal }}</div>
  54. </div>
  55. <div class="status-card p-3 border border-green-500/30 rounded">
  56. <div class="status-title text-sm text-gray-400 mb-1">测量装备数</div>
  57. <div class="status-value text-2xl font-bold text-green-400">{{ stastic.cc }}</div>
  58. </div>
  59. <div class="status-card p-3 border border-purple-500/30 rounded">
  60. <div class="status-title text-sm text-gray-400 mb-1">干扰装备数</div>
  61. <div class="status-value text-2xl font-bold text-purple-400">{{ stastic.gr }}</div>
  62. </div>
  63. <div class="status-card p-3 border border-red-500/30 rounded">
  64. <div class="status-title text-sm text-gray-400 mb-1">靶标装备数</div>
  65. <div class="status-value text-2xl font-bold text-red-400">{{ stastic.bb }}</div>
  66. </div>
  67. </div>
  68. </div>
  69. <!-- 2) 任务甘特图 -->
  70. <div class="section-card mb-4 gantt-card">
  71. <div class="section-header">
  72. <h4 class="section-title">
  73. <i class="el-icon-date mr-2"></i>
  74. 任务甘特图
  75. </h4>
  76. <div>
  77. <!--<el-input v-model="taskId" placeholder="任务ID(可选)" size="mini"-->
  78. <!-- style="width:220px;margin-right:8px;"/>-->
  79. <!--<el-button size="mini" @click="useDefaultGantt">默认数据</el-button>-->
  80. <!--<el-button :loading="loadingGantt" size="mini" type="primary" @click="fetchGanttFromApi">-->
  81. <!-- {{ loadingGantt ? '加载中...' : '接口拉取' }}-->
  82. <!--</el-button>-->
  83. <!--<span class="meta" style="margin-left:8px;">T0: {{ t0Display || '-' }}</span>-->
  84. <span class="meta" style="margin-left:8px;">事件数: {{ currentPreviewEvents.length }}</span>
  85. </div>
  86. </div>
  87. <div class="gantt-body">
  88. <!-- 子组件入参:时间线数据 + 类型配色 -->
  89. <GanttChart
  90. :default-duration="120"
  91. :time-margin="300"
  92. :timeline-data="currentPreviewEvents"
  93. :type-color-map="ganttTypeColorMap"
  94. />
  95. <div v-if="ganttError" class="meta" style="color:#ff6b6b;margin-top:6px;">{{ ganttError }}</div>
  96. </div>
  97. </div>
  98. <!-- 3) 时序计划(示例占位;只传入 map-data) -->
  99. <div class="timeline-wrapper">
  100. <TargetEquipmentMap
  101. :map-data="timingPlanData"
  102. :event-catalog="allKeyEvents"
  103. :plan="plan"
  104. @update="onSeqUpdate"
  105. @delete="onSeqDelete"
  106. @search-change="onSeqSearch"
  107. />
  108. </div>
  109. </div>
  110. </el-main>
  111. <!-- 右侧:任务预览与配置表单 -->
  112. <el-aside class="min-w-[240px] border-left border-[#0c4a6e] relative" width="20%">
  113. <div class="aside-border-decoration"></div>
  114. <div class="task-panel h-full">
  115. <div class="task-content">
  116. <!-- 任务基本信息 -->
  117. <task-basic-info
  118. :sub-task-id="plan.subTaskId"
  119. :task-id="plan.taskId"
  120. @update-targets-by-missile-type="updateTargetsByMissileType"
  121. @update-targets-by-missile-count="updateTargetsByMissileCount"
  122. />
  123. <!-- 理论抛洒点配置 -->
  124. <!--<runway-drop-setting ref="dropForm" v-model="runwayDrop"/>-->
  125. <!-- 气象环境配置 -->
  126. <weather-env-form ref="weatherRef" v-model="weather"/>
  127. <!-- 理论数据导入(触发弹窗) -->
  128. <Trajectory @do-import="doImport"/>
  129. <!-- 导入 Excel 弹窗 -->
  130. <ExcelImportDialog
  131. :max-files="10"
  132. :visible.sync="visible"
  133. title="导入理论数据"
  134. @confirm="onBindTargets"
  135. @error="onSingleError"
  136. @success="onSingleSuccess"
  137. />
  138. </div>
  139. </div>
  140. </el-aside>
  141. </el-container>
  142. <!-- 装备参数编辑对话框 -->
  143. <el-dialog
  144. :close-on-click-modal="false"
  145. :visible.sync="showEquipmentDialog"
  146. custom-class="equipment-dialog"
  147. title="编辑装备参试"
  148. width="520px"
  149. >
  150. <el-form :model="equipmentEditForm" label-width="96px" size="small">
  151. <el-form-item label="装备名称">
  152. <el-input v-model="equipmentEditForm.label" disabled/>
  153. </el-form-item>
  154. <el-form-item label="工作状态">
  155. <el-select v-model="equipmentEditForm.status" placeholder="选择工作状态">
  156. <el-option label="待命" value="standby"/>
  157. <el-option label="就绪" value="ready"/>
  158. <el-option label="维护中" value="maintenance"/>
  159. </el-select>
  160. </el-form-item>
  161. <el-form-item label="部署位置">
  162. <el-select v-model="equipmentEditForm.position" placeholder="选择部署位置">
  163. <el-option label="左翼" value="left"/>
  164. <el-option label="右翼" value="right"/>
  165. <el-option label="中央" value="center"/>
  166. <el-option label="前沿" value="front"/>
  167. <el-option label="后方" value="rear"/>
  168. </el-select>
  169. </el-form-item>
  170. <el-form-item label="备注信息">
  171. <el-input v-model="equipmentEditForm.notes" :rows="3" placeholder="请输入备注" type="textarea"/>
  172. </el-form-item>
  173. </el-form>
  174. <div slot="footer">
  175. <el-button @click="showEquipmentDialog = false">取消</el-button>
  176. <el-button type="primary" @click="confirmEquipmentEdit">保存</el-button>
  177. </div>
  178. </el-dialog>
  179. <!--总关键事件列表-->
  180. <el-drawer
  181. >
  182. </el-drawer>
  183. </div>
  184. </template>
  185. <script>
  186. /**
  187. * 任务想定编辑父组件(精简版)
  188. * - 左侧:装备树
  189. * - 中间:概览 + 甘特图 + 时序计划
  190. * - 右侧:任务表单、抛洒点、气象、理论数据导入
  191. * - 导入流程:Trajectory 发出 @do-import -> 打开 ExcelImportDialog -> @confirm 拿到映射关系
  192. */
  193. import axios from 'axios'
  194. import {mapGetters} from 'vuex'
  195. import TaskBasicInfo from './components/BasicInfo.vue'
  196. import RunwayDropSetting from './components/RunwayDropSetting.vue'
  197. import WeatherEnvForm from './components/WeatherEnvForm.vue'
  198. import Trajectory from './components/Trajectory.vue'
  199. import ExcelImportDialog from './components/ExcelImportDialog.vue'
  200. import TargetEquipmentMap from './components/TargetEquipmentMap.vue'
  201. import GanttChart from './components/GanttChartWithEndTime.vue'
  202. import {getEquTree} from "@/api/faultSimulation";
  203. import {getSubPlanZbStatistics, getSubPlanZbTsList} from "@/api/subPlanZb";
  204. import {battlefieldEnvironmentInsert, getBattlefieldEnvironment} from "@/api/battlefieldEnvironment";
  205. import {saveJson} from "@/api/deductionTask";
  206. /** 默认甘特数据(示例) */
  207. const DEFAULT_GANTT_PAYLOAD = {
  208. t0: '2025-09-16T03:00:00Z',
  209. legend_types: [
  210. {kind: '测量装备', color: '#5470c6'},
  211. {kind: '靶标装备', color: '#91cc75'},
  212. {kind: '干扰装备', color: '#fac858'}
  213. ],
  214. y_order: ['雷达A', '靶机B', '干扰车C'],
  215. devices: [
  216. {
  217. name: '雷达A', kind: '测量装备', events: [
  218. {start_sec: 120, duration_sec: 600, title: '上电', desc: '自检', trigger_type: '自动'},
  219. {start_sec: 900, duration_sec: 300, title: '工作', desc: '跟踪', trigger_type: '手动'}
  220. ]
  221. },
  222. {
  223. name: '靶机B', kind: '靶标装备', events: [
  224. {start_sec: 150, duration_sec: 1200, title: '起飞', desc: '', trigger_type: '计划'}
  225. ]
  226. }
  227. ]
  228. }
  229. export default {
  230. name: 'OrderEdit',
  231. components: {
  232. TaskBasicInfo,
  233. RunwayDropSetting,
  234. WeatherEnvForm,
  235. Trajectory,
  236. ExcelImportDialog,
  237. TargetEquipmentMap,
  238. GanttChart
  239. },
  240. data() {
  241. return {
  242. plan:JSON.parse(this.$route.query.plan),
  243. /** 表格数据:由父组件持有与更新 */
  244. timingPlanData: [],
  245. /** 关键事件总表:用于“新增抽屉”里多选 */
  246. allKeyEvents: [
  247. { id: 'E1', strikeDomain: '对陆', timing: 'T+30', keyEvent: '测量系统上电' },
  248. { id: 'E2', strikeDomain: '对陆', timing: 'T+60', keyEvent: '目标进入监视区' },
  249. { id: 'E3', strikeDomain: '对海', timing: 'T+90', keyEvent: '干扰装备启动' },
  250. { id: 'E4', strikeDomain: '对海', timing: 'T+120', keyEvent: '目标锁定' },
  251. { id: 'E5', strikeDomain: '对陆', timing: 'T+150', keyEvent: '导弹准备' },
  252. { id: 'E6', strikeDomain: '对海', timing: 'T+180', keyEvent: '导弹发射' },
  253. { id: 'E7', strikeDomain: '对陆', timing: 'T+210', keyEvent: '数据记录开始' },
  254. { id: 'E8', strikeDomain: '对海', timing: 'T+240', keyEvent: '拦截评估' }
  255. ],
  256. /** 导入弹窗显隐 */
  257. visible: false,
  258. /** 气象参数(作为 WeatherEnvForm 的 v-model) */
  259. weather: {
  260. temp: 20,
  261. rh: 55,
  262. pressure: 1013,
  263. wind: 5.5,
  264. windDir: 270,
  265. fallSpeed: 0.8,
  266. airDensity: 1200,
  267. visibility: 5000,
  268. rain: 0,
  269. weather: 'sunny'
  270. },
  271. /** 抛洒点配置(RunwayDropSetting 的 v-model) */
  272. runwayDrop: {
  273. runwayHead: {lon: 116.397, lat: 39.907, alt: 35},
  274. groups: [
  275. {hOffset: 120, vOffset: -50},
  276. {hOffset: 0, vOffset: 0},
  277. {hOffset: -60, vOffset: 80}
  278. ]
  279. },
  280. /** 甘特相关输入与状态 */
  281. taskId: '',
  282. loadingGantt: false,
  283. ganttError: '',
  284. ganttTimelineData: [],
  285. ganttTypeColorMap: {'测量装备': '#5470c6', '靶标装备': '#91cc75', '干扰装备': '#fac858'},
  286. t0Ms: null,
  287. /** 示例时序计划(传入 TargetEquipmentMap) */
  288. /** 任务基本信息(传入 TaskBasicInfo) */
  289. taskForm: {
  290. ts:"2025-10-15 14:30:00",
  291. name: '红旗-9B防空导弹拦截试验',
  292. category: 'combat',
  293. type: 'air-defense',
  294. missileType: 'surface-to-air',
  295. missileCount: 6,
  296. executeTime: '2025-10-15 14:30:00',
  297. description: '验证在强电子干扰环境下的作战效能。',
  298. targets: [
  299. {
  300. id: 1,
  301. name: '模拟指挥所靶标',
  302. type: '空中靶标',
  303. coordinates: '东经121°25′,北纬30°15′',
  304. threatLevel: 5,
  305. equipmentId: 'A1'
  306. },
  307. {
  308. id: 2,
  309. name: '模拟雷达站靶标',
  310. type: '空中靶标',
  311. coordinates: '东经121°30′,北纬30°20′',
  312. threatLevel: 4,
  313. equipmentId: 'A1'
  314. },
  315. {
  316. id: 3,
  317. name: '模拟机场跑道靶标',
  318. type: '空中靶标',
  319. coordinates: '东经121°20′,北纬30°25′',
  320. threatLevel: 5,
  321. equipmentId: 'B2'
  322. }
  323. ]
  324. },
  325. /** 装备树(左侧) */
  326. equipmentTree: [
  327. {
  328. id: 'category1',
  329. label: '测量装备',
  330. children: [
  331. {
  332. id: 'A1',
  333. label: '高空观测装置A1',
  334. indicatorClass: 'bg-blue-500',
  335. status: 'ready',
  336. position: 'front',
  337. notes: '',
  338. details: []
  339. },
  340. {
  341. id: 'B2',
  342. label: '地面观测装置B2',
  343. indicatorClass: 'bg-blue-500',
  344. status: 'ready',
  345. position: 'center',
  346. notes: '',
  347. details: []
  348. },
  349. {
  350. id: 'C3',
  351. label: '移动观测装置C3',
  352. indicatorClass: 'bg-blue-500',
  353. status: 'standby',
  354. position: 'rear',
  355. notes: '',
  356. details: []
  357. }
  358. ]
  359. },
  360. {
  361. id: 'category2',
  362. label: '干扰装备',
  363. children: [
  364. {
  365. id: 'D1',
  366. label: '雷达干扰器D1',
  367. indicatorClass: 'bg-purple-500',
  368. status: 'standby',
  369. position: 'left',
  370. notes: '',
  371. details: []
  372. },
  373. {
  374. id: 'E2',
  375. label: '光电干扰器E2',
  376. indicatorClass: 'bg-purple-500',
  377. status: 'ready',
  378. position: 'right',
  379. notes: '',
  380. details: []
  381. }
  382. ]
  383. },
  384. {
  385. id: 'category3',
  386. label: '靶标装备',
  387. children: [
  388. {
  389. id: 'redA',
  390. label: '空中靶标A集群',
  391. indicatorClass: 'bg-red-500',
  392. status: 'ready',
  393. position: 'front',
  394. notes: '',
  395. details: []
  396. },
  397. {
  398. id: 'redB',
  399. label: '海上靶标B',
  400. indicatorClass: 'bg-red-500',
  401. status: 'standby',
  402. position: 'front',
  403. notes: '',
  404. details: []
  405. }
  406. ]
  407. }
  408. ],
  409. defaultProps: {children: 'equList', label: 'name'},
  410. // 统计数据
  411. stastic:{},
  412. /** 装备参数对话框 */
  413. selectedEquipment: null,
  414. showEquipmentDialog: false,
  415. equipmentEditForm: {id: '', label: '', status: '', position: '', notes: ''},
  416. /** WebSocket 相关引用(保存/释放监听用) */
  417. wsOff: null,
  418. offMsg: null
  419. }
  420. },
  421. computed: {
  422. ...mapGetters('tTime', ['currentT0HMS', 'isRealtime']),
  423. // 时序图格式化数据
  424. currentPreviewEvents() {
  425. const list = [];
  426. this.timingPlanData.forEach(t => {
  427. if (!t.ts) return;
  428. list.push({
  429. id:t.id,
  430. time: `T0+${this.formatSeconds(t.ts.split('-')[0])}`,
  431. endTime: `T0+${this.formatSeconds(t.ts.split('-')[1])}`,
  432. name: t.zbName,
  433. rawTime: this.formatSeconds(t.seconds),
  434. title: t.seconds === '0' ? '初始化' : t.behavior,
  435. desc: `${t.zbName}`,
  436. zbType: this.$getDictNameByValue('zb_type', t.zbType),
  437. });
  438. });
  439. return list.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime));
  440. },
  441. /** T0 可读显示 */
  442. t0Display() {
  443. if (!this.t0Ms) return ''
  444. const d = new Date(this.t0Ms)
  445. const p = n => String(n).padStart(2, '0')
  446. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
  447. }
  448. },
  449. watch: {
  450. /** 导弹类型变更时,按业务规则筛选靶标 */
  451. 'taskForm.missileType': function () {
  452. this.updateTargetsByMissileType()
  453. }
  454. },
  455. mounted() {
  456. this.fetchEqTree()
  457. this.fetchEquCount()
  458. this.fetchTsList()
  459. this.getEnvDate()
  460. // 默认加载一份甘特示例数据,避免空白
  461. this.useDefaultGantt()
  462. },
  463. methods: {
  464. formatSeconds(seconds) {
  465. // 确保输入是有效的数字,并取整数部分
  466. const totalSeconds = Math.max(0, Math.floor(seconds)); // 处理负数或无效输入
  467. // 计算小时、分钟和秒
  468. const hours = Math.floor(totalSeconds / 3600);
  469. const minutes = Math.floor((totalSeconds % 3600) / 60);
  470. const secs = totalSeconds % 60;
  471. // 使用 padStart 确保分钟和秒始终显示为两位数 (e.g., 05, 09, 12)
  472. const paddedMinutes = String(minutes).padStart(2, '0');
  473. const paddedSeconds = String(secs).padStart(2, '0');
  474. // 返回格式: "Hhmmss" 或 "HH:mm:ss"
  475. // 这里采用更标准的 HH:mm:ss 格式
  476. return `${hours}:${paddedMinutes}:${paddedSeconds}`;
  477. },
  478. hhmmssToSec(hms) {
  479. const [h = 0, m = 0, s = 0] = (hms || '00:00:00').split(':').map(n => parseInt(n, 10) || 0);
  480. return h * 3600 + m * 60 + s;
  481. },
  482. getEnvDate(){
  483. // 获取环境信息
  484. getBattlefieldEnvironment({simulationId:this.plan.id}).then((res)=>{
  485. if(res.data?.environmentJson){
  486. this.weather = JSON.parse(res.data?.environmentJson)
  487. }else {
  488. }
  489. })
  490. },
  491. fetchEqTree(){
  492. getEquTree({subTaskId:this.plan.subTaskId}).then((res)=>{
  493. this.equipmentTree = res.data
  494. })
  495. },
  496. fetchEquCount(){
  497. getSubPlanZbStatistics({schemeId:this.plan.schemeId}).then((res)=>{
  498. this.stastic = res.data;
  499. })
  500. },
  501. fetchTsList() {
  502. getSubPlanZbTsList({simulationId: this.plan.id}).then((res) => {
  503. this.timingPlanData = res.data
  504. })
  505. },
  506. /** 检索条件变更(如果要联动后端,在这里调接口;不需要就当日志看) */
  507. onSeqSearch({ strikeDomain, keyEvent }) {
  508. // 这里通常发请求:/api/xxx?domain=strikeDomain&kw=keyEvent
  509. // 本地表格筛选已在子组件里做了
  510. // console.log('检索条件:', strikeDomain, keyEvent)
  511. },
  512. /** 编辑保存 */
  513. onSeqUpdate() {
  514. this.fetchTsList();
  515. },
  516. /** 删除 */
  517. onSeqDelete(row) {
  518. // row:原表格行
  519. this.$confirm(`确定删除「${row.keyEvent}」吗?`, '提示', { type: 'warning' })
  520. .then(() => {
  521. this.timingPlanData = this.timingPlanData.filter(r => r.id !== row.id)
  522. this.$message.success('已删除')
  523. })
  524. .catch(() => {})
  525. },
  526. /** 去重(按 id 优先,其次按 领域+时序+事件 文本去重) */
  527. deDupRows(rows) {
  528. const seen = new Set()
  529. const out = []
  530. for (const r of rows) {
  531. const key = r.id || `${r.strikeDomain}|${r.timing}|${r.keyEvent}`
  532. if (seen.has(key)) continue
  533. seen.add(key)
  534. out.push(r)
  535. }
  536. return out
  537. },
  538. /** 打开导入弹窗(由 Trajectory 触发) */
  539. doImport() {
  540. this.visible = true
  541. },
  542. /** 单个文件上传成功(可选) */
  543. onSingleSuccess({file, upload}) {
  544. // 可在此处理单个文件返回
  545. },
  546. /** 单个文件上传失败(可选) */
  547. onSingleError({file}) {
  548. // 可在此处理失败场景
  549. },
  550. /** 点击“确定”时拿到全部映射:[{ fileName, targetCode, upload }] */
  551. onBindTargets(mappings) {
  552. // 将 upload 与 targetCode 绑定提交给后端
  553. // 例如:api.bindTheory(mappings.map(m => ({ code: m.targetCode, fileId: m.upload.data.id })))
  554. this.$message.success(`已提交 ${mappings.length} 个文件的靶标编号关联`)
  555. },
  556. /** 载入默认甘特数据 */
  557. useDefaultGantt() {
  558. this.consumeGanttPayload(DEFAULT_GANTT_PAYLOAD)
  559. },
  560. /** 从接口拉取甘特数据(返回结构参考 DEFAULT_GANTT_PAYLOAD) */
  561. async fetchGanttFromApi() {
  562. this.loadingGantt = true
  563. this.ganttError = ''
  564. try {
  565. const {data} = await axios.get('/api/gantt', {params: this.taskId ? {taskId: this.taskId} : {}})
  566. this.consumeGanttPayload(data)
  567. } catch (e) {
  568. this.ganttError = (e && e.message) || '甘特数据请求失败'
  569. this.ganttTimelineData = []
  570. } finally {
  571. this.loadingGantt = false
  572. }
  573. },
  574. /** 归一化 payload -> timelineData + 颜色映射 */
  575. consumeGanttPayload(payload) {
  576. if (!payload) {
  577. this.ganttTimelineData = [];
  578. return
  579. }
  580. // 1) 解析 T0(ISO / epoch 秒 / 毫秒)
  581. this.t0Ms = this.parseT0(payload.t0)
  582. // 2) 覆盖颜色映射(legend_types 优先)
  583. if (Array.isArray(payload.legend_types)) {
  584. const next = {...this.ganttTypeColorMap}
  585. payload.legend_types.forEach(it => {
  586. if (it && it.kind) next[it.kind] = it.color || next[it.kind] || '#bbb'
  587. })
  588. this.ganttTypeColorMap = next
  589. }
  590. // 3) 扁平化事件
  591. const yOrder = Array.isArray(payload.y_order) ? payload.y_order.slice() : null
  592. const flat = this.flattenEvents(payload, yOrder)
  593. // 4) 扁平 -> timelineData
  594. this.ganttTimelineData = flat.map(ev => {
  595. const startSec = this.resolveStartSec(ev)
  596. const duration = Number.isFinite(ev.duration_sec) ? ev.duration_sec : 120
  597. return {
  598. name: ev.name,
  599. rawTime: this.secToT0HMS(startSec),
  600. duration,
  601. title: ev.title || '无标题',
  602. desc: ev.desc || '',
  603. kindText: ev.kind || '未知类型',
  604. triggerTypeText: ev.trigger_type || ''
  605. }
  606. })
  607. },
  608. /** 扁平化 devices/events 两种结构 */
  609. flattenEvents(payload, yOrder) {
  610. const orderIdx = name => {
  611. if (!yOrder) return Number.MAX_SAFE_INTEGER
  612. const i = yOrder.indexOf(name)
  613. return i === -1 ? Number.MAX_SAFE_INTEGER : i
  614. }
  615. if (Array.isArray(payload.devices)) {
  616. const sorted = payload.devices.slice().sort((a, b) => {
  617. const ia = orderIdx(a.name), ib = orderIdx(b.name)
  618. if (ia !== ib) return ia - ib
  619. return String(a.name).localeCompare(String(b.name), 'zh')
  620. })
  621. const res = []
  622. sorted.forEach(dev => {
  623. (dev.events || []).forEach(ev => {
  624. res.push({
  625. name: dev.name,
  626. kind: dev.kind,
  627. start_sec: ev.start_sec,
  628. start_at: ev.start_at,
  629. duration_sec: ev.duration_sec,
  630. title: ev.title,
  631. desc: ev.desc,
  632. trigger_type: ev.trigger_type
  633. })
  634. })
  635. })
  636. return res
  637. }
  638. if (Array.isArray(payload.events)) {
  639. return payload.events.slice().sort((a, b) => {
  640. const ia = orderIdx(a.name), ib = orderIdx(b.name)
  641. if (ia !== ib) return ia - ib
  642. return this.resolveStartSec(a) - this.resolveStartSec(b)
  643. })
  644. }
  645. return []
  646. },
  647. /** start_sec / start_at -> 相对 T0 的秒 */
  648. resolveStartSec(ev) {
  649. if (Number.isFinite(ev.start_sec)) return ev.start_sec
  650. if (ev.start_at != null) {
  651. const abs = this.toEpochMs(ev.start_at)
  652. if (this.t0Ms != null && Number.isFinite(abs)) {
  653. return Math.max(0, Math.floor((abs - this.t0Ms) / 1000))
  654. }
  655. }
  656. return 0
  657. },
  658. /** 时间解析工具 */
  659. parseT0(t0) {
  660. if (t0 == null) return null
  661. if (typeof t0 === 'string') {
  662. const ms = Date.parse(t0)
  663. return Number.isFinite(ms) ? ms : null
  664. }
  665. if (typeof t0 === 'number') {
  666. return t0 < 2e10 ? t0 * 1000 : t0
  667. }
  668. return null
  669. },
  670. toEpochMs(x) {
  671. if (x == null) return NaN
  672. if (typeof x === 'number') return x < 2e10 ? x * 1000 : x
  673. const ms = Date.parse(x)
  674. return Number.isFinite(ms) ? ms : NaN
  675. },
  676. secToT0HMS(sec) {
  677. const s = Math.max(0, Math.floor(sec || 0))
  678. const h = String(Math.floor(s / 3600)).padStart(2, '0')
  679. const m = String(Math.floor((s % 3600) / 60)).padStart(2, '0')
  680. const ss = String(s % 60).padStart(2, '0')
  681. return `T0+${h}:${m}:${ss}`
  682. },
  683. /** 装备树点击 -> 打开编辑弹窗 */
  684. handleEquipmentClick(data, node) {
  685. if (node.level === 2 || (node.level > 2 && (!data.children || !data.children.length))) {
  686. this.selectedEquipment = {...data}
  687. this.openEquipmentDialog(data)
  688. }
  689. },
  690. openEquipmentDialog(equipment) {
  691. this.equipmentEditForm = {
  692. id: equipment.id || '',
  693. label: equipment.label || '',
  694. status: equipment.status || 'standby',
  695. position: equipment.position || 'center',
  696. notes: equipment.notes || ''
  697. }
  698. this.showEquipmentDialog = true
  699. },
  700. confirmEquipmentEdit() {
  701. const {id, status, position, notes} = this.equipmentEditForm
  702. const updateNode = (nodes) => {
  703. for (let i = 0; i < nodes.length; i++) {
  704. if (nodes[i].id === id) {
  705. nodes[i] = {...nodes[i], status, position, notes};
  706. return true
  707. }
  708. if (nodes[i].children && nodes[i].children.length) {
  709. if (updateNode(nodes[i].children)) return true
  710. }
  711. }
  712. return false
  713. }
  714. updateNode(this.equipmentTree)
  715. if (this.selectedEquipment && this.selectedEquipment.id === id) {
  716. this.selectedEquipment = {...this.selectedEquipment, status, position, notes}
  717. }
  718. this.$message.success('参试参数已保存')
  719. this.showEquipmentDialog = false
  720. },
  721. /** 任务保存(示例:初始化 T0、连接 WS) */
  722. async saveTask() {
  723. if (this.taskForm.targets.length === 0) {
  724. this.$message.error('靶标设置不能为空');
  725. return
  726. }
  727. try {
  728. // 保存环境配置
  729. await battlefieldEnvironmentInsert({simulationId:this.plan.id,environmentJson:JSON.stringify(this.weatherForm)})
  730. await saveJson({id:this.plan.id});
  731. this.$message.success('任务保存成功')
  732. } catch (e) {
  733. }
  734. // this.$store.dispatch('tTime/initFixedT0', 0)
  735. // this.$ws.connect('telemetry', 'ws://127.0.0.1:9000/telemetry')
  736. // if (this.wsOff) this.wsOff()
  737. // this.offMsg = this.$ws.onMessage('telemetry', () => {
  738. // })
  739. // if (this.wsOff) this.wsOff()
  740. // this.wsOff = this.$ws.onMessage('telemetry', (msg) => {
  741. // this.$store.dispatch('tTime/ingestFromWs', JSON.parse(msg))
  742. // })
  743. },
  744. /** 返回、重置 */
  745. goBack() {
  746. this.$confirm('确定要退出吗?', '提示', {confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'})
  747. .then(() => {
  748. this.$router.go(-1)
  749. }).catch(() => {
  750. })
  751. },
  752. resetTask() {
  753. this.$confirm('确定要重置任务配置吗?', '提示', {
  754. confirmButtonText: '确定',
  755. cancelButtonText: '取消',
  756. type: 'warning'
  757. })
  758. .then(() => {
  759. this.$message.success('任务配置已重置')
  760. }).catch(() => {
  761. })
  762. },
  763. /** 任务基本信息里变更导弹类型/数量时,按规则调整靶标 */
  764. updateTargetsByMissileType() {
  765. if (!this.taskForm.missileType) return
  766. const compatible = []
  767. if (['surface-to-air', 'air-to-surface', 'cruise'].includes(this.taskForm.missileType)) compatible.push('空中靶标')
  768. if (['anti-ship', 'cruise'].includes(this.taskForm.missileType)) compatible.push('海上靶标')
  769. if (['air-to-surface', 'cruise'].includes(this.taskForm.missileType)) compatible.push('地面靶标')
  770. this.taskForm.targets = this.taskForm.targets.filter(t => compatible.includes(t.type))
  771. if (this.taskForm.targets.length === 0) {
  772. this.taskForm.targets.push({
  773. id: Date.now(), name: `靶标${compatible[0]}`, type: compatible[0],
  774. coordinates: '东经120°00′,北纬30°00′', threatLevel: 3, equipmentId: ''
  775. })
  776. }
  777. },
  778. updateTargetsByMissileCount() {
  779. const maxTargets = this.taskForm.missileCount * 2
  780. if (this.taskForm.targets.length > maxTargets) {
  781. this.taskForm.targets = this.taskForm.targets.slice(0, maxTargets)
  782. this.$message.info(`靶标数量已调整为${maxTargets}个(不超过导弹数量的2倍)`)
  783. }
  784. }
  785. }
  786. }
  787. </script>
  788. <style lang="scss" scoped>
  789. /* 布局与配色(军事风格) */
  790. .task-setting-container {
  791. display: flex;
  792. flex-direction: column;
  793. height: 100vh;
  794. background-color: #050c1a;
  795. color: #e0f2fe;
  796. font-family: "Microsoft YaHei", Arial, sans-serif;
  797. position: relative;
  798. overflow: hidden;
  799. }
  800. /* 背景网格 */
  801. .grid-bg {
  802. position: absolute;
  803. top: 0;
  804. left: 0;
  805. width: 100%;
  806. height: 100%;
  807. background-size: 40px 40px;
  808. background-image: linear-gradient(to right, rgba(14, 55, 107, 0.1) 1px, transparent 1px),
  809. linear-gradient(to bottom, rgba(14, 55, 107, 0.1) 1px, transparent 1px);
  810. pointer-events: none;
  811. z-index: 0;
  812. }
  813. /* 头部 */
  814. .header {
  815. flex: 0 0 60px;
  816. background-color: #0f172a;
  817. background-image: linear-gradient(to right, #0f172a, #1e3a8a);
  818. display: flex;
  819. align-items: center;
  820. padding: 0 20px;
  821. justify-content: space-between;
  822. border-bottom: 1px solid #0ea5e9;
  823. box-shadow: 0 2px 10px rgba(14, 165, 233, 0.2);
  824. position: relative;
  825. z-index: 10;
  826. .header-logo {
  827. display: flex;
  828. align-items: center;
  829. h1 {
  830. font-size: 1.5rem;
  831. color: #bae6fd;
  832. margin: 0;
  833. white-space: nowrap;
  834. text-shadow: 0 0 5px rgba(14, 165, 233, 0.5);
  835. }
  836. }
  837. .header-controls {
  838. display: flex;
  839. gap: 15px;
  840. }
  841. }
  842. /* 按钮特效 */
  843. .btn军事 {
  844. position: relative;
  845. overflow: hidden;
  846. transition: all .3s ease;
  847. border: 1px solid rgba(14, 165, 233, .5) !important;
  848. box-shadow: 0 0 5px rgba(14, 165, 233, .3);
  849. &:after {
  850. content: '';
  851. position: absolute;
  852. top: 0;
  853. left: -100%;
  854. width: 100%;
  855. height: 100%;
  856. background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent);
  857. transition: all .5s ease;
  858. }
  859. &:hover:after {
  860. left: 100%;
  861. }
  862. }
  863. /* 装饰线 */
  864. .aside-border-decoration, .main-border-decoration {
  865. position: absolute;
  866. top: 0;
  867. width: 3px;
  868. height: 100%;
  869. background: linear-gradient(to bottom, rgba(14, 165, 233, 0) 0%, rgba(14, 165, 233, .5) 50%, rgba(14, 165, 233, 0) 100%);
  870. z-index: 1;
  871. }
  872. .aside-border-decoration {
  873. right: 0;
  874. }
  875. .main-border-decoration {
  876. left: 0;
  877. }
  878. /* 右侧任务面板滚动 */
  879. .task-panel {
  880. width: 100%;
  881. background-color: rgba(15, 23, 42, 0.8);
  882. backdrop-filter: blur(5px);
  883. display: flex;
  884. flex-direction: column;
  885. min-width: 0;
  886. overflow: hidden;
  887. border-right: 1px solid rgba(14, 165, 233, 0.2);
  888. .task-content {
  889. flex: 1;
  890. overflow-y: auto;
  891. padding: 15px;
  892. min-height: 0;
  893. &::-webkit-scrollbar {
  894. width: 6px;
  895. height: 6px;
  896. }
  897. &::-webkit-scrollbar-track {
  898. background: rgba(15, 23, 42, 0.5);
  899. }
  900. &::-webkit-scrollbar-thumb {
  901. background-color: rgba(59, 130, 246, 0.5);
  902. border-radius: 3px;
  903. }
  904. }
  905. }
  906. /* 中间分区 */
  907. .equipment-content-box {
  908. background-color: rgba(5, 12, 26, 0.9);
  909. min-height: 0;
  910. overflow-y: auto;
  911. }
  912. .middle-layout {
  913. height: 100%;
  914. grid-template-rows:20% 40% 40%;
  915. }
  916. .overview-card, .gantt-card, .timeline-wrapper {
  917. min-height: 0;
  918. }
  919. /* 甘特容器 */
  920. .gantt-body {
  921. //height: 300px;
  922. padding: 8px 12px 12px 12px;
  923. }
  924. /* 统计卡片 */
  925. .equipment-status-grid {
  926. display: grid;
  927. grid-template-columns:repeat(4, 1fr);
  928. gap: 10px;
  929. }
  930. .status-card {
  931. background-color: rgba(15, 23, 42, 0.6);
  932. border-radius: 3px;
  933. transition: all .2s;
  934. &:hover {
  935. transform: translateY(-2px);
  936. box-shadow: 0 3px 10px rgba(14, 165, 233, 0.1);
  937. }
  938. .status-title {
  939. color: #94a3b8;
  940. }
  941. .status-value {
  942. color: #60a5fa;
  943. text-shadow: 0 0 3px rgba(59, 130, 246, 0.2);
  944. }
  945. }
  946. /* 树形滚动条 */
  947. .equipment-tree {
  948. max-height: calc(100% - 20px);
  949. overflow-y: auto;
  950. &::-webkit-scrollbar {
  951. width: 6px;
  952. height: 6px;
  953. }
  954. &::-webkit-scrollbar-track {
  955. background: rgba(15, 23, 42, 0.5);
  956. }
  957. &::-webkit-scrollbar-thumb {
  958. background-color: rgba(59, 130, 246, 0.5);
  959. border-radius: 3px;
  960. }
  961. }
  962. ::v-deep .el-tree-node[aria-level="2"] .el-tree-node__expand-icon {
  963. display: none !important;
  964. }
  965. ::v-deep .el-tree-node__expand-icon.is-leaf {
  966. display: none !important;
  967. }
  968. /* 模块头 */
  969. .section-header {
  970. background-color: rgba(30, 58, 138, 0.5);
  971. padding: 8px 15px;
  972. display: flex;
  973. justify-content: space-between;
  974. align-items: center;
  975. border-bottom: 1px solid rgba(14, 165, 233, 0.2);
  976. .section-title {
  977. color: #bae6fd;
  978. font-size: 14px;
  979. margin: 0;
  980. font-weight: 500;
  981. display: flex;
  982. align-items: center;
  983. }
  984. }
  985. /* 表单控件 */
  986. .el-input__inner, .el-textarea__inner, .el-select .el-input__inner {
  987. background-color: rgba(15, 23, 42, 0.8);
  988. border: 1px solid rgba(14, 165, 233, 0.3);
  989. color: #e0f2fe;
  990. width: 100%;
  991. border-radius: 3px;
  992. height: 32px;
  993. font-size: 14px;
  994. &:focus {
  995. border-color: #3b82f6;
  996. box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
  997. }
  998. &::placeholder {
  999. color: #64748b;
  1000. }
  1001. &:disabled {
  1002. background-color: rgba(15, 23, 42, 0.5);
  1003. color: #94a3b8;
  1004. cursor: not-allowed;
  1005. }
  1006. }
  1007. .el-textarea__inner {
  1008. min-height: 80px !important;
  1009. height: auto;
  1010. resize: vertical;
  1011. }
  1012. ::v-deep .el-select-dropdown {
  1013. background-color: rgba(15, 23, 42, 0.95);
  1014. border: 1px solid rgba(14, 165, 233, 0.3);
  1015. border-radius: 3px;
  1016. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
  1017. }
  1018. ::v-deep .el-table {
  1019. background-color: transparent;
  1020. color: #e0f2fe;
  1021. .el-table__empty-text {
  1022. color: #94a3b8;
  1023. }
  1024. .el-table__row:hover > td {
  1025. background-color: rgba(30, 58, 138, 0.2);
  1026. }
  1027. }
  1028. /* 弹窗皮肤 */
  1029. ::v-deep .equipment-dialog {
  1030. .el-dialog {
  1031. background-color: rgba(15, 23, 42, 0.95);
  1032. border: 1px solid rgba(14, 165, 233, 0.3);
  1033. border-radius: 4px;
  1034. box-shadow: 0 10px 30px rgba(0, 0, 0, .5);
  1035. }
  1036. .el-dialog__header {
  1037. background-color: rgba(30, 58, 138, 0.6);
  1038. border-bottom: 1px solid rgba(14, 165, 233, 0.3);
  1039. .el-dialog__title {
  1040. color: #bae6fd;
  1041. }
  1042. }
  1043. .el-dialog__body, .el-dialog__footer {
  1044. background-color: rgba(15, 23, 42, 0.95);
  1045. color: #e0f2fe;
  1046. }
  1047. }
  1048. /* 按钮配色 */
  1049. ::v-deep .el-button {
  1050. border-radius: 3px;
  1051. border: none;
  1052. &.el-button--primary {
  1053. background-color: #1e40af;
  1054. color: #fff;
  1055. &:hover {
  1056. background-color: #3b82f6;
  1057. }
  1058. }
  1059. &.el-button--success {
  1060. background-color: #065f46;
  1061. color: #fff;
  1062. &:hover {
  1063. background-color: #16a34a;
  1064. }
  1065. }
  1066. &.el-button--warning {
  1067. background-color: #92400e;
  1068. color: #fff;
  1069. &:hover {
  1070. background-color: #d97706;
  1071. }
  1072. }
  1073. &.el-button--text {
  1074. color: #94a3b8;
  1075. &:hover {
  1076. color: #bae6fd;
  1077. background-color: rgba(148, 163, 184, 0.1);
  1078. }
  1079. }
  1080. }
  1081. /* 其他 */
  1082. ::v-deep .el-slider .el-slider__runway {
  1083. background-color: rgba(51, 65, 85, 0.5);
  1084. }
  1085. ::v-deep .el-slider .el-slider__bar {
  1086. background-color: #3b82f6;
  1087. }
  1088. ::v-deep .el-slider .el-slider__button {
  1089. border-color: #3b82f6;
  1090. }
  1091. ::v-deep .el-message {
  1092. background-color: rgba(30, 58, 138, 0.8);
  1093. border-color: rgba(14, 165, 233, 0.3);
  1094. color: #e0f2fe;
  1095. }
  1096. ::v-deep .el-container {
  1097. background: transparent;
  1098. }
  1099. /* 响应式 */
  1100. @media (max-width: 1600px) {
  1101. .middle-layout {
  1102. grid-template-rows: 34% 26% 40%;
  1103. }
  1104. }
  1105. @media (max-width: 1200px) {
  1106. .equipment-status-grid {
  1107. grid-template-columns: repeat(2, 1fr);
  1108. }
  1109. .middle-layout {
  1110. grid-template-rows: 38% 26% 36%;
  1111. }
  1112. }
  1113. ::v-deep .el-tree {
  1114. background: transparent;
  1115. }
  1116. .meta {
  1117. font-size: 12px;
  1118. color: #9aa0a6;
  1119. }
  1120. </style>