| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237 |
- <template>
- <div class="task-setting-container">
- <!-- 背景网格 -->
- <div class="grid-bg"></div>
- <!-- 顶部栏:标题 + 控制按钮 -->
- <div class="header">
- <div class="header-logo flex items-center">
- <i class="el-icon-shield text-red-500 mr-2 text-xl"></i>
- <h1>任务想定编辑</h1>
- <div class="info-value">{{ currentT0HMS }}</div>
- <small v-if="isRealtime">(实时)</small>
- </div>
- <div class="header-controls">
- <el-button class="btn军事" icon="el-icon-back" type="success" @click="goBack">返回</el-button>
- <el-button class="btn军事" icon="el-icon-save" type="primary" @click="saveTask">保存任务</el-button>
- <el-button class="btn军事" icon="el-icon-refresh" type="warning" @click="resetTask">重置</el-button>
- </div>
- </div>
- <!-- 三段式主布局 -->
- <el-container class="h-[calc(100vh-60px)] flex-row overflow-hidden">
- <!-- 左侧:装备树 -->
- <el-aside class="min-w-[240px] border-right border-[#0c4a6e] relative" width="10%">
- <div class="equipment-tree-container col-span-1">
- <div class="section-card h-full">
- <el-tree
- :data="equipmentTree"
- :expand-on-click-node="false"
- :indent="15"
- :props="defaultProps"
- class="equipment-tree"
- default-expand-all
- node-key="id"
- @node-click="handleEquipmentClick"
- />
- </div>
- </div>
- </el-aside>
- <!-- 中间:概览 + 甘特图 + 时序计划 -->
- <el-main class="p-0 h-full flex flex-col relative">
- <div class="main-border-decoration"></div>
- <div class="equipment-content-box flex-1 p-4 middle-layout">
- <!-- 1) 装备数量概览 -->
- <div class="section-card mb-4 overview-card">
- <div class="section-header">
- <h4 class="section-title">
- <i class="el-icon-dashboard mr-2"></i>
- 装备数量概览
- </h4>
- </div>
- <div class="equipment-status-grid p-4 grid grid-cols-4 gap-3">
- <div class="status-card p-3 border border-blue-500/30 rounded">
- <div class="status-title text-sm text-gray-400 mb-1">总装备数</div>
- <div class="status-value text-2xl font-bold">{{ stastic.sumTotal }}</div>
- </div>
- <div class="status-card p-3 border border-green-500/30 rounded">
- <div class="status-title text-sm text-gray-400 mb-1">测量装备数</div>
- <div class="status-value text-2xl font-bold text-green-400">{{ stastic.cc }}</div>
- </div>
- <div class="status-card p-3 border border-purple-500/30 rounded">
- <div class="status-title text-sm text-gray-400 mb-1">干扰装备数</div>
- <div class="status-value text-2xl font-bold text-purple-400">{{ stastic.gr }}</div>
- </div>
- <div class="status-card p-3 border border-red-500/30 rounded">
- <div class="status-title text-sm text-gray-400 mb-1">靶标装备数</div>
- <div class="status-value text-2xl font-bold text-red-400">{{ stastic.bb }}</div>
- </div>
- </div>
- </div>
- <!-- 2) 任务甘特图 -->
- <div class="section-card mb-4 gantt-card">
- <div class="section-header">
- <h4 class="section-title">
- <i class="el-icon-date mr-2"></i>
- 任务甘特图
- </h4>
- <div>
- <!--<el-input v-model="taskId" placeholder="任务ID(可选)" size="mini"-->
- <!-- style="width:220px;margin-right:8px;"/>-->
- <!--<el-button size="mini" @click="useDefaultGantt">默认数据</el-button>-->
- <!--<el-button :loading="loadingGantt" size="mini" type="primary" @click="fetchGanttFromApi">-->
- <!-- {{ loadingGantt ? '加载中...' : '接口拉取' }}-->
- <!--</el-button>-->
- <!--<span class="meta" style="margin-left:8px;">T0: {{ t0Display || '-' }}</span>-->
- <span class="meta" style="margin-left:8px;">事件数: {{ currentPreviewEvents.length }}</span>
- </div>
- </div>
- <div class="gantt-body">
- <!-- 子组件入参:时间线数据 + 类型配色 -->
- <GanttChart
- :default-duration="120"
- :time-margin="300"
- :timeline-data="currentPreviewEvents"
- :type-color-map="ganttTypeColorMap"
- />
- <div v-if="ganttError" class="meta" style="color:#ff6b6b;margin-top:6px;">{{ ganttError }}</div>
- </div>
- </div>
- <!-- 3) 时序计划(示例占位;只传入 map-data) -->
- <div class="timeline-wrapper">
- <TargetEquipmentMap
- :map-data="timingPlanData"
- :event-catalog="allKeyEvents"
- :plan="plan"
- @update="onSeqUpdate"
- @delete="onSeqDelete"
- @search-change="onSeqSearch"
- />
- </div>
- </div>
- </el-main>
- <!-- 右侧:任务预览与配置表单 -->
- <el-aside class="min-w-[240px] border-left border-[#0c4a6e] relative" width="20%">
- <div class="aside-border-decoration"></div>
- <div class="task-panel h-full">
- <div class="task-content">
- <!-- 任务基本信息 -->
- <task-basic-info
- :sub-task-id="plan.subTaskId"
- :task-id="plan.taskId"
- @update-targets-by-missile-type="updateTargetsByMissileType"
- @update-targets-by-missile-count="updateTargetsByMissileCount"
- />
- <!-- 理论抛洒点配置 -->
- <!--<runway-drop-setting ref="dropForm" v-model="runwayDrop"/>-->
- <!-- 气象环境配置 -->
- <weather-env-form ref="weatherRef" v-model="weather"/>
- <!-- 理论数据导入(触发弹窗) -->
- <Trajectory @do-import="doImport"/>
- <!-- 导入 Excel 弹窗 -->
- <ExcelImportDialog
- :max-files="10"
- :visible.sync="visible"
- title="导入理论数据"
- @confirm="onBindTargets"
- @error="onSingleError"
- @success="onSingleSuccess"
- />
- </div>
- </div>
- </el-aside>
- </el-container>
- <!-- 装备参数编辑对话框 -->
- <el-dialog
- :close-on-click-modal="false"
- :visible.sync="showEquipmentDialog"
- custom-class="equipment-dialog"
- title="编辑装备参试"
- width="520px"
- >
- <el-form :model="equipmentEditForm" label-width="96px" size="small">
- <el-form-item label="装备名称">
- <el-input v-model="equipmentEditForm.label" disabled/>
- </el-form-item>
- <el-form-item label="工作状态">
- <el-select v-model="equipmentEditForm.status" placeholder="选择工作状态">
- <el-option label="待命" value="standby"/>
- <el-option label="就绪" value="ready"/>
- <el-option label="维护中" value="maintenance"/>
- </el-select>
- </el-form-item>
- <el-form-item label="部署位置">
- <el-select v-model="equipmentEditForm.position" placeholder="选择部署位置">
- <el-option label="左翼" value="left"/>
- <el-option label="右翼" value="right"/>
- <el-option label="中央" value="center"/>
- <el-option label="前沿" value="front"/>
- <el-option label="后方" value="rear"/>
- </el-select>
- </el-form-item>
- <el-form-item label="备注信息">
- <el-input v-model="equipmentEditForm.notes" :rows="3" placeholder="请输入备注" type="textarea"/>
- </el-form-item>
- </el-form>
- <div slot="footer">
- <el-button @click="showEquipmentDialog = false">取消</el-button>
- <el-button type="primary" @click="confirmEquipmentEdit">保存</el-button>
- </div>
- </el-dialog>
- <!--总关键事件列表-->
- <el-drawer
- >
- </el-drawer>
- </div>
- </template>
- <script>
- /**
- * 任务想定编辑父组件(精简版)
- * - 左侧:装备树
- * - 中间:概览 + 甘特图 + 时序计划
- * - 右侧:任务表单、抛洒点、气象、理论数据导入
- * - 导入流程:Trajectory 发出 @do-import -> 打开 ExcelImportDialog -> @confirm 拿到映射关系
- */
- import axios from 'axios'
- import {mapGetters} from 'vuex'
- import TaskBasicInfo from './components/BasicInfo.vue'
- import RunwayDropSetting from './components/RunwayDropSetting.vue'
- import WeatherEnvForm from './components/WeatherEnvForm.vue'
- import Trajectory from './components/Trajectory.vue'
- import ExcelImportDialog from './components/ExcelImportDialog.vue'
- import TargetEquipmentMap from './components/TargetEquipmentMap.vue'
- import GanttChart from './components/GanttChartWithEndTime.vue'
- import {getEquTree} from "@/api/faultSimulation";
- import {getSubPlanZbStatistics, getSubPlanZbTsList} from "@/api/subPlanZb";
- import {battlefieldEnvironmentInsert, getBattlefieldEnvironment} from "@/api/battlefieldEnvironment";
- import {saveJson} from "@/api/deductionTask";
- /** 默认甘特数据(示例) */
- const DEFAULT_GANTT_PAYLOAD = {
- t0: '2025-09-16T03:00:00Z',
- legend_types: [
- {kind: '测量装备', color: '#5470c6'},
- {kind: '靶标装备', color: '#91cc75'},
- {kind: '干扰装备', color: '#fac858'}
- ],
- y_order: ['雷达A', '靶机B', '干扰车C'],
- devices: [
- {
- name: '雷达A', kind: '测量装备', events: [
- {start_sec: 120, duration_sec: 600, title: '上电', desc: '自检', trigger_type: '自动'},
- {start_sec: 900, duration_sec: 300, title: '工作', desc: '跟踪', trigger_type: '手动'}
- ]
- },
- {
- name: '靶机B', kind: '靶标装备', events: [
- {start_sec: 150, duration_sec: 1200, title: '起飞', desc: '', trigger_type: '计划'}
- ]
- }
- ]
- }
- export default {
- name: 'OrderEdit',
- components: {
- TaskBasicInfo,
- RunwayDropSetting,
- WeatherEnvForm,
- Trajectory,
- ExcelImportDialog,
- TargetEquipmentMap,
- GanttChart
- },
- data() {
- return {
- plan:JSON.parse(this.$route.query.plan),
- /** 表格数据:由父组件持有与更新 */
- timingPlanData: [],
- /** 关键事件总表:用于“新增抽屉”里多选 */
- allKeyEvents: [
- { id: 'E1', strikeDomain: '对陆', timing: 'T+30', keyEvent: '测量系统上电' },
- { id: 'E2', strikeDomain: '对陆', timing: 'T+60', keyEvent: '目标进入监视区' },
- { id: 'E3', strikeDomain: '对海', timing: 'T+90', keyEvent: '干扰装备启动' },
- { id: 'E4', strikeDomain: '对海', timing: 'T+120', keyEvent: '目标锁定' },
- { id: 'E5', strikeDomain: '对陆', timing: 'T+150', keyEvent: '导弹准备' },
- { id: 'E6', strikeDomain: '对海', timing: 'T+180', keyEvent: '导弹发射' },
- { id: 'E7', strikeDomain: '对陆', timing: 'T+210', keyEvent: '数据记录开始' },
- { id: 'E8', strikeDomain: '对海', timing: 'T+240', keyEvent: '拦截评估' }
- ],
- /** 导入弹窗显隐 */
- visible: false,
- /** 气象参数(作为 WeatherEnvForm 的 v-model) */
- weather: {
- temp: 20,
- rh: 55,
- pressure: 1013,
- wind: 5.5,
- windDir: 270,
- fallSpeed: 0.8,
- airDensity: 1200,
- visibility: 5000,
- rain: 0,
- weather: 'sunny'
- },
- /** 抛洒点配置(RunwayDropSetting 的 v-model) */
- runwayDrop: {
- runwayHead: {lon: 116.397, lat: 39.907, alt: 35},
- groups: [
- {hOffset: 120, vOffset: -50},
- {hOffset: 0, vOffset: 0},
- {hOffset: -60, vOffset: 80}
- ]
- },
- /** 甘特相关输入与状态 */
- taskId: '',
- loadingGantt: false,
- ganttError: '',
- ganttTimelineData: [],
- ganttTypeColorMap: {'测量装备': '#5470c6', '靶标装备': '#91cc75', '干扰装备': '#fac858'},
- t0Ms: null,
- /** 示例时序计划(传入 TargetEquipmentMap) */
- /** 任务基本信息(传入 TaskBasicInfo) */
- taskForm: {
- ts:"2025-10-15 14:30:00",
- name: '红旗-9B防空导弹拦截试验',
- category: 'combat',
- type: 'air-defense',
- missileType: 'surface-to-air',
- missileCount: 6,
- executeTime: '2025-10-15 14:30:00',
- description: '验证在强电子干扰环境下的作战效能。',
- targets: [
- {
- id: 1,
- name: '模拟指挥所靶标',
- type: '空中靶标',
- coordinates: '东经121°25′,北纬30°15′',
- threatLevel: 5,
- equipmentId: 'A1'
- },
- {
- id: 2,
- name: '模拟雷达站靶标',
- type: '空中靶标',
- coordinates: '东经121°30′,北纬30°20′',
- threatLevel: 4,
- equipmentId: 'A1'
- },
- {
- id: 3,
- name: '模拟机场跑道靶标',
- type: '空中靶标',
- coordinates: '东经121°20′,北纬30°25′',
- threatLevel: 5,
- equipmentId: 'B2'
- }
- ]
- },
- /** 装备树(左侧) */
- equipmentTree: [
- {
- id: 'category1',
- label: '测量装备',
- children: [
- {
- id: 'A1',
- label: '高空观测装置A1',
- indicatorClass: 'bg-blue-500',
- status: 'ready',
- position: 'front',
- notes: '',
- details: []
- },
- {
- id: 'B2',
- label: '地面观测装置B2',
- indicatorClass: 'bg-blue-500',
- status: 'ready',
- position: 'center',
- notes: '',
- details: []
- },
- {
- id: 'C3',
- label: '移动观测装置C3',
- indicatorClass: 'bg-blue-500',
- status: 'standby',
- position: 'rear',
- notes: '',
- details: []
- }
- ]
- },
- {
- id: 'category2',
- label: '干扰装备',
- children: [
- {
- id: 'D1',
- label: '雷达干扰器D1',
- indicatorClass: 'bg-purple-500',
- status: 'standby',
- position: 'left',
- notes: '',
- details: []
- },
- {
- id: 'E2',
- label: '光电干扰器E2',
- indicatorClass: 'bg-purple-500',
- status: 'ready',
- position: 'right',
- notes: '',
- details: []
- }
- ]
- },
- {
- id: 'category3',
- label: '靶标装备',
- children: [
- {
- id: 'redA',
- label: '空中靶标A集群',
- indicatorClass: 'bg-red-500',
- status: 'ready',
- position: 'front',
- notes: '',
- details: []
- },
- {
- id: 'redB',
- label: '海上靶标B',
- indicatorClass: 'bg-red-500',
- status: 'standby',
- position: 'front',
- notes: '',
- details: []
- }
- ]
- }
- ],
- defaultProps: {children: 'equList', label: 'name'},
- // 统计数据
- stastic:{},
- /** 装备参数对话框 */
- selectedEquipment: null,
- showEquipmentDialog: false,
- equipmentEditForm: {id: '', label: '', status: '', position: '', notes: ''},
- /** WebSocket 相关引用(保存/释放监听用) */
- wsOff: null,
- offMsg: null
- }
- },
- computed: {
- ...mapGetters('tTime', ['currentT0HMS', 'isRealtime']),
- // 时序图格式化数据
- currentPreviewEvents() {
- const list = [];
- this.timingPlanData.forEach(t => {
- if (!t.ts) return;
- list.push({
- id:t.id,
- time: `T0+${this.formatSeconds(t.ts.split('-')[0])}`,
- endTime: `T0+${this.formatSeconds(t.ts.split('-')[1])}`,
- name: t.zbName,
- rawTime: this.formatSeconds(t.seconds),
- title: t.seconds === '0' ? '初始化' : t.behavior,
- desc: `${t.zbName}`,
- zbType: this.$getDictNameByValue('zb_type', t.zbType),
- });
- });
- return list.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime));
- },
- /** T0 可读显示 */
- t0Display() {
- if (!this.t0Ms) return ''
- const d = new Date(this.t0Ms)
- const p = n => String(n).padStart(2, '0')
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
- }
- },
- watch: {
- /** 导弹类型变更时,按业务规则筛选靶标 */
- 'taskForm.missileType': function () {
- this.updateTargetsByMissileType()
- }
- },
- mounted() {
- this.fetchEqTree()
- this.fetchEquCount()
- this.fetchTsList()
- this.getEnvDate()
- // 默认加载一份甘特示例数据,避免空白
- this.useDefaultGantt()
- },
- methods: {
- formatSeconds(seconds) {
- // 确保输入是有效的数字,并取整数部分
- const totalSeconds = Math.max(0, Math.floor(seconds)); // 处理负数或无效输入
- // 计算小时、分钟和秒
- const hours = Math.floor(totalSeconds / 3600);
- const minutes = Math.floor((totalSeconds % 3600) / 60);
- const secs = totalSeconds % 60;
- // 使用 padStart 确保分钟和秒始终显示为两位数 (e.g., 05, 09, 12)
- const paddedMinutes = String(minutes).padStart(2, '0');
- const paddedSeconds = String(secs).padStart(2, '0');
- // 返回格式: "Hhmmss" 或 "HH:mm:ss"
- // 这里采用更标准的 HH:mm:ss 格式
- return `${hours}:${paddedMinutes}:${paddedSeconds}`;
- },
- hhmmssToSec(hms) {
- const [h = 0, m = 0, s = 0] = (hms || '00:00:00').split(':').map(n => parseInt(n, 10) || 0);
- return h * 3600 + m * 60 + s;
- },
- getEnvDate(){
- // 获取环境信息
- getBattlefieldEnvironment({simulationId:this.plan.id}).then((res)=>{
- if(res.data?.environmentJson){
- this.weather = JSON.parse(res.data?.environmentJson)
- }else {
- }
- })
- },
- fetchEqTree(){
- getEquTree({subTaskId:this.plan.subTaskId}).then((res)=>{
- this.equipmentTree = res.data
- })
- },
- fetchEquCount(){
- getSubPlanZbStatistics({schemeId:this.plan.schemeId}).then((res)=>{
- this.stastic = res.data;
- })
- },
- fetchTsList() {
- getSubPlanZbTsList({simulationId: this.plan.id}).then((res) => {
- this.timingPlanData = res.data
- })
- },
- /** 检索条件变更(如果要联动后端,在这里调接口;不需要就当日志看) */
- onSeqSearch({ strikeDomain, keyEvent }) {
- // 这里通常发请求:/api/xxx?domain=strikeDomain&kw=keyEvent
- // 本地表格筛选已在子组件里做了
- // console.log('检索条件:', strikeDomain, keyEvent)
- },
- /** 编辑保存 */
- onSeqUpdate() {
- this.fetchTsList();
- },
- /** 删除 */
- onSeqDelete(row) {
- // row:原表格行
- this.$confirm(`确定删除「${row.keyEvent}」吗?`, '提示', { type: 'warning' })
- .then(() => {
- this.timingPlanData = this.timingPlanData.filter(r => r.id !== row.id)
- this.$message.success('已删除')
- })
- .catch(() => {})
- },
- /** 去重(按 id 优先,其次按 领域+时序+事件 文本去重) */
- deDupRows(rows) {
- const seen = new Set()
- const out = []
- for (const r of rows) {
- const key = r.id || `${r.strikeDomain}|${r.timing}|${r.keyEvent}`
- if (seen.has(key)) continue
- seen.add(key)
- out.push(r)
- }
- return out
- },
- /** 打开导入弹窗(由 Trajectory 触发) */
- doImport() {
- this.visible = true
- },
- /** 单个文件上传成功(可选) */
- onSingleSuccess({file, upload}) {
- // 可在此处理单个文件返回
- },
- /** 单个文件上传失败(可选) */
- onSingleError({file}) {
- // 可在此处理失败场景
- },
- /** 点击“确定”时拿到全部映射:[{ fileName, targetCode, upload }] */
- onBindTargets(mappings) {
- // 将 upload 与 targetCode 绑定提交给后端
- // 例如:api.bindTheory(mappings.map(m => ({ code: m.targetCode, fileId: m.upload.data.id })))
- this.$message.success(`已提交 ${mappings.length} 个文件的靶标编号关联`)
- },
- /** 载入默认甘特数据 */
- useDefaultGantt() {
- this.consumeGanttPayload(DEFAULT_GANTT_PAYLOAD)
- },
- /** 从接口拉取甘特数据(返回结构参考 DEFAULT_GANTT_PAYLOAD) */
- async fetchGanttFromApi() {
- this.loadingGantt = true
- this.ganttError = ''
- try {
- const {data} = await axios.get('/api/gantt', {params: this.taskId ? {taskId: this.taskId} : {}})
- this.consumeGanttPayload(data)
- } catch (e) {
- this.ganttError = (e && e.message) || '甘特数据请求失败'
- this.ganttTimelineData = []
- } finally {
- this.loadingGantt = false
- }
- },
- /** 归一化 payload -> timelineData + 颜色映射 */
- consumeGanttPayload(payload) {
- if (!payload) {
- this.ganttTimelineData = [];
- return
- }
- // 1) 解析 T0(ISO / epoch 秒 / 毫秒)
- this.t0Ms = this.parseT0(payload.t0)
- // 2) 覆盖颜色映射(legend_types 优先)
- if (Array.isArray(payload.legend_types)) {
- const next = {...this.ganttTypeColorMap}
- payload.legend_types.forEach(it => {
- if (it && it.kind) next[it.kind] = it.color || next[it.kind] || '#bbb'
- })
- this.ganttTypeColorMap = next
- }
- // 3) 扁平化事件
- const yOrder = Array.isArray(payload.y_order) ? payload.y_order.slice() : null
- const flat = this.flattenEvents(payload, yOrder)
- // 4) 扁平 -> timelineData
- this.ganttTimelineData = flat.map(ev => {
- const startSec = this.resolveStartSec(ev)
- const duration = Number.isFinite(ev.duration_sec) ? ev.duration_sec : 120
- return {
- name: ev.name,
- rawTime: this.secToT0HMS(startSec),
- duration,
- title: ev.title || '无标题',
- desc: ev.desc || '',
- kindText: ev.kind || '未知类型',
- triggerTypeText: ev.trigger_type || ''
- }
- })
- },
- /** 扁平化 devices/events 两种结构 */
- flattenEvents(payload, yOrder) {
- const orderIdx = name => {
- if (!yOrder) return Number.MAX_SAFE_INTEGER
- const i = yOrder.indexOf(name)
- return i === -1 ? Number.MAX_SAFE_INTEGER : i
- }
- if (Array.isArray(payload.devices)) {
- const sorted = payload.devices.slice().sort((a, b) => {
- const ia = orderIdx(a.name), ib = orderIdx(b.name)
- if (ia !== ib) return ia - ib
- return String(a.name).localeCompare(String(b.name), 'zh')
- })
- const res = []
- sorted.forEach(dev => {
- (dev.events || []).forEach(ev => {
- res.push({
- name: dev.name,
- kind: dev.kind,
- start_sec: ev.start_sec,
- start_at: ev.start_at,
- duration_sec: ev.duration_sec,
- title: ev.title,
- desc: ev.desc,
- trigger_type: ev.trigger_type
- })
- })
- })
- return res
- }
- if (Array.isArray(payload.events)) {
- return payload.events.slice().sort((a, b) => {
- const ia = orderIdx(a.name), ib = orderIdx(b.name)
- if (ia !== ib) return ia - ib
- return this.resolveStartSec(a) - this.resolveStartSec(b)
- })
- }
- return []
- },
- /** start_sec / start_at -> 相对 T0 的秒 */
- resolveStartSec(ev) {
- if (Number.isFinite(ev.start_sec)) return ev.start_sec
- if (ev.start_at != null) {
- const abs = this.toEpochMs(ev.start_at)
- if (this.t0Ms != null && Number.isFinite(abs)) {
- return Math.max(0, Math.floor((abs - this.t0Ms) / 1000))
- }
- }
- return 0
- },
- /** 时间解析工具 */
- parseT0(t0) {
- if (t0 == null) return null
- if (typeof t0 === 'string') {
- const ms = Date.parse(t0)
- return Number.isFinite(ms) ? ms : null
- }
- if (typeof t0 === 'number') {
- return t0 < 2e10 ? t0 * 1000 : t0
- }
- return null
- },
- toEpochMs(x) {
- if (x == null) return NaN
- if (typeof x === 'number') return x < 2e10 ? x * 1000 : x
- const ms = Date.parse(x)
- return Number.isFinite(ms) ? ms : NaN
- },
- secToT0HMS(sec) {
- const s = Math.max(0, Math.floor(sec || 0))
- const h = String(Math.floor(s / 3600)).padStart(2, '0')
- const m = String(Math.floor((s % 3600) / 60)).padStart(2, '0')
- const ss = String(s % 60).padStart(2, '0')
- return `T0+${h}:${m}:${ss}`
- },
- /** 装备树点击 -> 打开编辑弹窗 */
- handleEquipmentClick(data, node) {
- if (node.level === 2 || (node.level > 2 && (!data.children || !data.children.length))) {
- this.selectedEquipment = {...data}
- this.openEquipmentDialog(data)
- }
- },
- openEquipmentDialog(equipment) {
- this.equipmentEditForm = {
- id: equipment.id || '',
- label: equipment.label || '',
- status: equipment.status || 'standby',
- position: equipment.position || 'center',
- notes: equipment.notes || ''
- }
- this.showEquipmentDialog = true
- },
- confirmEquipmentEdit() {
- const {id, status, position, notes} = this.equipmentEditForm
- const updateNode = (nodes) => {
- for (let i = 0; i < nodes.length; i++) {
- if (nodes[i].id === id) {
- nodes[i] = {...nodes[i], status, position, notes};
- return true
- }
- if (nodes[i].children && nodes[i].children.length) {
- if (updateNode(nodes[i].children)) return true
- }
- }
- return false
- }
- updateNode(this.equipmentTree)
- if (this.selectedEquipment && this.selectedEquipment.id === id) {
- this.selectedEquipment = {...this.selectedEquipment, status, position, notes}
- }
- this.$message.success('参试参数已保存')
- this.showEquipmentDialog = false
- },
- /** 任务保存(示例:初始化 T0、连接 WS) */
- async saveTask() {
- if (this.taskForm.targets.length === 0) {
- this.$message.error('靶标设置不能为空');
- return
- }
- try {
- // 保存环境配置
- await battlefieldEnvironmentInsert({simulationId:this.plan.id,environmentJson:JSON.stringify(this.weatherForm)})
- await saveJson({id:this.plan.id});
- this.$message.success('任务保存成功')
- } catch (e) {
- }
- // this.$store.dispatch('tTime/initFixedT0', 0)
- // this.$ws.connect('telemetry', 'ws://127.0.0.1:9000/telemetry')
- // if (this.wsOff) this.wsOff()
- // this.offMsg = this.$ws.onMessage('telemetry', () => {
- // })
- // if (this.wsOff) this.wsOff()
- // this.wsOff = this.$ws.onMessage('telemetry', (msg) => {
- // this.$store.dispatch('tTime/ingestFromWs', JSON.parse(msg))
- // })
- },
- /** 返回、重置 */
- goBack() {
- this.$confirm('确定要退出吗?', '提示', {confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'})
- .then(() => {
- this.$router.go(-1)
- }).catch(() => {
- })
- },
- resetTask() {
- this.$confirm('确定要重置任务配置吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
- .then(() => {
- this.$message.success('任务配置已重置')
- }).catch(() => {
- })
- },
- /** 任务基本信息里变更导弹类型/数量时,按规则调整靶标 */
- updateTargetsByMissileType() {
- if (!this.taskForm.missileType) return
- const compatible = []
- if (['surface-to-air', 'air-to-surface', 'cruise'].includes(this.taskForm.missileType)) compatible.push('空中靶标')
- if (['anti-ship', 'cruise'].includes(this.taskForm.missileType)) compatible.push('海上靶标')
- if (['air-to-surface', 'cruise'].includes(this.taskForm.missileType)) compatible.push('地面靶标')
- this.taskForm.targets = this.taskForm.targets.filter(t => compatible.includes(t.type))
- if (this.taskForm.targets.length === 0) {
- this.taskForm.targets.push({
- id: Date.now(), name: `靶标${compatible[0]}`, type: compatible[0],
- coordinates: '东经120°00′,北纬30°00′', threatLevel: 3, equipmentId: ''
- })
- }
- },
- updateTargetsByMissileCount() {
- const maxTargets = this.taskForm.missileCount * 2
- if (this.taskForm.targets.length > maxTargets) {
- this.taskForm.targets = this.taskForm.targets.slice(0, maxTargets)
- this.$message.info(`靶标数量已调整为${maxTargets}个(不超过导弹数量的2倍)`)
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- /* 布局与配色(军事风格) */
- .task-setting-container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background-color: #050c1a;
- color: #e0f2fe;
- font-family: "Microsoft YaHei", Arial, sans-serif;
- position: relative;
- overflow: hidden;
- }
- /* 背景网格 */
- .grid-bg {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-size: 40px 40px;
- background-image: linear-gradient(to right, rgba(14, 55, 107, 0.1) 1px, transparent 1px),
- linear-gradient(to bottom, rgba(14, 55, 107, 0.1) 1px, transparent 1px);
- pointer-events: none;
- z-index: 0;
- }
- /* 头部 */
- .header {
- flex: 0 0 60px;
- background-color: #0f172a;
- background-image: linear-gradient(to right, #0f172a, #1e3a8a);
- display: flex;
- align-items: center;
- padding: 0 20px;
- justify-content: space-between;
- border-bottom: 1px solid #0ea5e9;
- box-shadow: 0 2px 10px rgba(14, 165, 233, 0.2);
- position: relative;
- z-index: 10;
- .header-logo {
- display: flex;
- align-items: center;
- h1 {
- font-size: 1.5rem;
- color: #bae6fd;
- margin: 0;
- white-space: nowrap;
- text-shadow: 0 0 5px rgba(14, 165, 233, 0.5);
- }
- }
- .header-controls {
- display: flex;
- gap: 15px;
- }
- }
- /* 按钮特效 */
- .btn军事 {
- position: relative;
- overflow: hidden;
- transition: all .3s ease;
- border: 1px solid rgba(14, 165, 233, .5) !important;
- box-shadow: 0 0 5px rgba(14, 165, 233, .3);
- &:after {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent);
- transition: all .5s ease;
- }
- &:hover:after {
- left: 100%;
- }
- }
- /* 装饰线 */
- .aside-border-decoration, .main-border-decoration {
- position: absolute;
- top: 0;
- width: 3px;
- height: 100%;
- background: linear-gradient(to bottom, rgba(14, 165, 233, 0) 0%, rgba(14, 165, 233, .5) 50%, rgba(14, 165, 233, 0) 100%);
- z-index: 1;
- }
- .aside-border-decoration {
- right: 0;
- }
- .main-border-decoration {
- left: 0;
- }
- /* 右侧任务面板滚动 */
- .task-panel {
- width: 100%;
- background-color: rgba(15, 23, 42, 0.8);
- backdrop-filter: blur(5px);
- display: flex;
- flex-direction: column;
- min-width: 0;
- overflow: hidden;
- border-right: 1px solid rgba(14, 165, 233, 0.2);
- .task-content {
- flex: 1;
- overflow-y: auto;
- padding: 15px;
- min-height: 0;
- &::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
- &::-webkit-scrollbar-track {
- background: rgba(15, 23, 42, 0.5);
- }
- &::-webkit-scrollbar-thumb {
- background-color: rgba(59, 130, 246, 0.5);
- border-radius: 3px;
- }
- }
- }
- /* 中间分区 */
- .equipment-content-box {
- background-color: rgba(5, 12, 26, 0.9);
- min-height: 0;
- overflow-y: auto;
- }
- .middle-layout {
- height: 100%;
- grid-template-rows:20% 40% 40%;
- }
- .overview-card, .gantt-card, .timeline-wrapper {
- min-height: 0;
- }
- /* 甘特容器 */
- .gantt-body {
- //height: 300px;
- padding: 8px 12px 12px 12px;
- }
- /* 统计卡片 */
- .equipment-status-grid {
- display: grid;
- grid-template-columns:repeat(4, 1fr);
- gap: 10px;
- }
- .status-card {
- background-color: rgba(15, 23, 42, 0.6);
- border-radius: 3px;
- transition: all .2s;
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 3px 10px rgba(14, 165, 233, 0.1);
- }
- .status-title {
- color: #94a3b8;
- }
- .status-value {
- color: #60a5fa;
- text-shadow: 0 0 3px rgba(59, 130, 246, 0.2);
- }
- }
- /* 树形滚动条 */
- .equipment-tree {
- max-height: calc(100% - 20px);
- overflow-y: auto;
- &::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
- &::-webkit-scrollbar-track {
- background: rgba(15, 23, 42, 0.5);
- }
- &::-webkit-scrollbar-thumb {
- background-color: rgba(59, 130, 246, 0.5);
- border-radius: 3px;
- }
- }
- ::v-deep .el-tree-node[aria-level="2"] .el-tree-node__expand-icon {
- display: none !important;
- }
- ::v-deep .el-tree-node__expand-icon.is-leaf {
- display: none !important;
- }
- /* 模块头 */
- .section-header {
- background-color: rgba(30, 58, 138, 0.5);
- padding: 8px 15px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid rgba(14, 165, 233, 0.2);
- .section-title {
- color: #bae6fd;
- font-size: 14px;
- margin: 0;
- font-weight: 500;
- display: flex;
- align-items: center;
- }
- }
- /* 表单控件 */
- .el-input__inner, .el-textarea__inner, .el-select .el-input__inner {
- background-color: rgba(15, 23, 42, 0.8);
- border: 1px solid rgba(14, 165, 233, 0.3);
- color: #e0f2fe;
- width: 100%;
- border-radius: 3px;
- height: 32px;
- font-size: 14px;
- &:focus {
- border-color: #3b82f6;
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
- }
- &::placeholder {
- color: #64748b;
- }
- &:disabled {
- background-color: rgba(15, 23, 42, 0.5);
- color: #94a3b8;
- cursor: not-allowed;
- }
- }
- .el-textarea__inner {
- min-height: 80px !important;
- height: auto;
- resize: vertical;
- }
- ::v-deep .el-select-dropdown {
- background-color: rgba(15, 23, 42, 0.95);
- border: 1px solid rgba(14, 165, 233, 0.3);
- border-radius: 3px;
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
- }
- ::v-deep .el-table {
- background-color: transparent;
- color: #e0f2fe;
- .el-table__empty-text {
- color: #94a3b8;
- }
- .el-table__row:hover > td {
- background-color: rgba(30, 58, 138, 0.2);
- }
- }
- /* 弹窗皮肤 */
- ::v-deep .equipment-dialog {
- .el-dialog {
- background-color: rgba(15, 23, 42, 0.95);
- border: 1px solid rgba(14, 165, 233, 0.3);
- border-radius: 4px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, .5);
- }
- .el-dialog__header {
- background-color: rgba(30, 58, 138, 0.6);
- border-bottom: 1px solid rgba(14, 165, 233, 0.3);
- .el-dialog__title {
- color: #bae6fd;
- }
- }
- .el-dialog__body, .el-dialog__footer {
- background-color: rgba(15, 23, 42, 0.95);
- color: #e0f2fe;
- }
- }
- /* 按钮配色 */
- ::v-deep .el-button {
- border-radius: 3px;
- border: none;
- &.el-button--primary {
- background-color: #1e40af;
- color: #fff;
- &:hover {
- background-color: #3b82f6;
- }
- }
- &.el-button--success {
- background-color: #065f46;
- color: #fff;
- &:hover {
- background-color: #16a34a;
- }
- }
- &.el-button--warning {
- background-color: #92400e;
- color: #fff;
- &:hover {
- background-color: #d97706;
- }
- }
- &.el-button--text {
- color: #94a3b8;
- &:hover {
- color: #bae6fd;
- background-color: rgba(148, 163, 184, 0.1);
- }
- }
- }
- /* 其他 */
- ::v-deep .el-slider .el-slider__runway {
- background-color: rgba(51, 65, 85, 0.5);
- }
- ::v-deep .el-slider .el-slider__bar {
- background-color: #3b82f6;
- }
- ::v-deep .el-slider .el-slider__button {
- border-color: #3b82f6;
- }
- ::v-deep .el-message {
- background-color: rgba(30, 58, 138, 0.8);
- border-color: rgba(14, 165, 233, 0.3);
- color: #e0f2fe;
- }
- ::v-deep .el-container {
- background: transparent;
- }
- /* 响应式 */
- @media (max-width: 1600px) {
- .middle-layout {
- grid-template-rows: 34% 26% 40%;
- }
- }
- @media (max-width: 1200px) {
- .equipment-status-grid {
- grid-template-columns: repeat(2, 1fr);
- }
- .middle-layout {
- grid-template-rows: 38% 26% 36%;
- }
- }
- ::v-deep .el-tree {
- background: transparent;
- }
- .meta {
- font-size: 12px;
- color: #9aa0a6;
- }
- </style>
|