zhaoen 3 месяцев назад
Родитель
Сommit
b53d091104

+ 9 - 0
src/api/planningScheme/index.js

@@ -182,3 +182,12 @@ export function zbDel(id) {
     method: "get",
   });
 }
+
+// 接口测试
+export function testtest(data) {
+  return request({
+    url: `/api/v1/bcsimequipment/insert`,
+    method: "post",
+    data,
+  });
+}

+ 7 - 0
src/router/index.js

@@ -140,6 +140,13 @@ export const constantRoutes = [
     component: () => import("@/views/Deduction/taskSetting/dedOrderEdit.vue"),
     hidden: true,
   },
+  // 构建页面 想定编辑
+  {
+    path: "/decision/scenarioEdit",
+    name: "scenarioEdit",
+    component: () => import("@/views/decision/testBuild/scenarioEdit.vue"),
+    hidden: true,
+  },
   {
     path: "/Deduction/stratDeduction",
     name: "stratDeduction",

+ 4 - 5
src/views/decision/testBuild/components/ImportTaskCard.vue

@@ -3,9 +3,9 @@
     <!-- 头部插槽 -->
     <template #header>
       <div class="plan-header">
-        <div class="plan-secret-tag" :style="secretTagStyle">
-          {{ $getDictNameByValue('secret_level',plan.secretLevel+'') }}
-        </div>
+        <!--<div class="plan-secret-tag" :style="secretTagStyle">-->
+        <!--  {{ $getDictNameByValue('secret_level',plan.secretLevel+'') }}-->
+        <!--</div>-->
         <el-tooltip
           :content="plan.simulationName + '-' + plan.simulationType"
           placement="top"
@@ -105,9 +105,8 @@
       <el-button type="primary" icon="el-icon-document" @click="editTask">
         任务详情
       </el-button>
-      <!-- 想定编辑按钮:仅选择三个方案后显示 -->
+      <!-- 想定编辑按钮:仅选择三个方案后显示  v-if="hasThreePlans" -->
       <el-button
-        v-if="hasThreePlans"
         type="primary"
         icon="el-icon-edit"
         @click="editScenario"

+ 100 - 43
src/views/decision/testBuild/index.vue

@@ -12,14 +12,19 @@
         </el-form-item>
         <el-form-item label="试验任务类型">
           <el-select v-model="queryForm.testTaskType" clearable placeholder="请选择试验任务类型">
-            <el-option v-for="item in $getDictList('test_task_type')" :key="item.id" :value="item.dictValue"
-                       :label="item.dictName"></el-option>
+            <el-option
+              v-for="item in $getDictList('test_task_type')"
+              :key="item.id"
+              :value="item.dictValue"
+              :label="item.dictName"
+            ></el-option>
           </el-select>
         </el-form-item>
       </template>
 
       <!-- Header右侧操作按钮 -->
       <template #header-actions>
+        <el-button @click="splitTechStack(testCodeData)">测试接口(导入数据用)</el-button>
         <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
         <el-button class="blue-btn" icon="el-icon-delete" @click="resetQuery">重置</el-button>
         <el-button class="blue-btn" icon="el-icon-plus" @click="addTask">新增试验任务</el-button>
@@ -249,14 +254,14 @@
                     $getDictNameByValue('test_task_type', currentDetailPlan.testTaskType + '') || "-"
                   }}</span>
               </div>
-              <div class="info-item bg-info-item">
-                <span class="info-label">秘级:</span>
-                <span class="info-value">
-                  <span class="secret-tag" :style="{background: secretTagColor}">
-                     {{ $getDictNameByValue('secret_level', currentDetailPlan.secretLevel + '') }}
-                  </span>
-                </span>
-              </div>
+              <!--<div class="info-item bg-info-item">-->
+              <!--  <span class="info-label">秘级:</span>-->
+              <!--  <span class="info-value">-->
+              <!--    <span class="secret-tag" :style="{background: secretTagColor}">-->
+              <!--       {{ $getDictNameByValue('secret_level', currentDetailPlan.secretLevel + '') }}-->
+              <!--    </span>-->
+              <!--  </span>-->
+              <!--</div>-->
               <div class="info-item bg-info-item">
                 <span class="info-label">开始时间:</span>
                 <span class="info-value">{{ currentDetailPlan.startTime || '-' }}</span>
@@ -328,16 +333,16 @@
                 </el-select>
               </el-form-item>
 
-              <el-form-item label="秘级" prop="secretLevel">
-                <el-select
-                    v-model="createForm.secretLevel"
-                    placeholder="选择秘级"
-                    style="width: 100%;"
-                >
-                  <el-option v-for="item in $getDictList('secret_level')" key="item" :value="item.dictValue"
-                             :label="item.dictName"></el-option>
-                </el-select>
-              </el-form-item>
+              <!--<el-form-item label="秘级" prop="secretLevel">-->
+              <!--  <el-select-->
+              <!--      v-model="createForm.secretLevel"-->
+              <!--      placeholder="选择秘级"-->
+              <!--      style="width: 100%;"-->
+              <!--  >-->
+              <!--    <el-option v-for="item in $getDictList('secret_level')" key="item" :value="item.dictValue"-->
+              <!--               :label="item.dictName"></el-option>-->
+              <!--  </el-select>-->
+              <!--</el-form-item>-->
 
               <el-form-item label="开始时间" prop="startTime">
                 <el-date-picker
@@ -374,10 +379,10 @@
                 <i class="el-icon-info text-blue-400 mt-1 mr-2"></i>
                 <span>请根据实际任务性质选择任务类型</span>
               </li>
-              <li class="flex items-start mt-2">
-                <i class="el-icon-info text-blue-400 mt-1 mr-2"></i>
-                <span>秘级选择需符合任务敏感程度</span>
-              </li>
+              <!--<li class="flex items-start mt-2">-->
+              <!--  <i class="el-icon-info text-blue-400 mt-1 mr-2"></i>-->
+              <!--  <span>秘级选择需符合任务敏感程度</span>-->
+              <!--</li>-->
             </ul>
           </div>
         </div>
@@ -418,7 +423,7 @@ import InterferenceCard from "./components/InterferenceCard.vue"
 import ImportTaskCard from "./components/ImportTaskCard.vue"
 import {findPageTask, findSubTaskPage} from "@/api/taskMage/taskMage";
 import {getList, insertDeductionTask, updateDeductionTask} from "@/api/deductionTask";
-import {getListVer} from "@/api/planningScheme";
+import {getListVer, testtest} from "@/api/planningScheme";
 
 export default {
   components: {
@@ -468,7 +473,7 @@ export default {
           startTime: '2024-07-01 08:30:00',
           endTime: '2024-07-01 12:00:00',
           compileStatus: '已编制',
-          secretLevel: '机密',
+          // secretLevel: '机密',
           status: '有效'
         }
       ],
@@ -480,7 +485,7 @@ export default {
           startTime: '2024-07-01 09:00:00',
           endTime: '2024-07-01 11:30:00',
           compileStatus: '已编制',
-          secretLevel: '机密',
+          // secretLevel: '机密',
           status: '有效'
         }
       ],
@@ -492,7 +497,7 @@ export default {
           startTime: '2024-07-01 08:30:00',
           endTime: '2024-07-01 12:00:00',
           compileStatus: '已编制',
-          secretLevel: '机密',
+          // secretLevel: '机密',
           status: '有效'
         }
       ],
@@ -508,7 +513,7 @@ export default {
         testTaskType: undefined,
         simType: 1,
         simulationType: 1,
-        secretLevel: undefined,
+        // secretLevel: undefined,
         startTime: '',
         remarks: ''
       },
@@ -516,9 +521,24 @@ export default {
         simulationName: [{required: true, message: '请输入试验任务名称', trigger: 'blur'}],
         testTaskType: [{required: true, message: '请选择试验任务类型', trigger: 'change'}],
         startTime: [{required: true, message: '请选择开始时间', trigger: 'change'}],
-        secretLevel: [{required: true, message: '请选择秘级', trigger: 'change'}],
+        // secretLevel: [{required: true, message: '请选择秘级', trigger: 'change'}],
         remarks: [{required: true, message: '请输入试验任务描述', trigger: 'blur'}]
-      }
+      },
+      testCodeData: {
+        "常速摄像机1,常速摄像机2,常速摄像机3,常速摄像机4,常速摄像机5,常速摄像机6,高速相机,微波信道机靶载端,微波信道机岸站端": [
+          {
+            label: "设备状态",
+            prop: "deviceStatus",
+            dataType: "select",
+            type: "STATIC",
+            rules: [{ required: true, message: "请选择压力信息", trigger: "change" }],
+            options: [
+              { label: "正常", value: 0 },
+              { label: "异常", value: 1 },
+            ],
+          },
+        ],
+      },
     }
   },
   computed: {
@@ -536,6 +556,37 @@ export default {
     this.fetchData()
   },
   methods: {
+    splitTechStack(input) {
+      // 1. 获取唯一的 key(如 "js,css,html")
+      const key = Object.keys(input)[0];
+      if (!key) return [];
+
+      // 2. 拆分 key 得到 ['js', 'css', 'html']
+      const names = key.split(',');
+
+      // 3. 获取共用的 data 数组
+      const sharedData = input[key];
+
+      // 4. 构建结果数组
+      const result = names.map(name => ({
+        name: name.trim(),     // 去除可能的空格
+        data: JSON.stringify(sharedData)
+      }));
+
+      result.forEach(item =>{
+        this.testCode(item)
+      });
+    },
+    testCode(item) {
+      let data = {
+        equipmentName: item.name,
+        modelConfig: item.data,
+        equipmentCode: '1'
+      }
+      testtest(data).then(res => {
+        console.log(res)
+      })
+    },
     // 解析 schemeSubPlan 获取 docx 文件链接
     getDocxUrl(row, type) {
       if (!row.schemeSubPlan) return null;
@@ -703,19 +754,25 @@ export default {
 
     /* 想定编辑跳转 */
     handleEditScenario(plan) {
-      this.$alert(`正在编辑任务"${plan.planName}"的想定方案`, '提示', {
-        confirmButtonText: '确定',
-        callback: () => {
-          this.$router.push({
-            path: '/testBuild/orderEdit',
-            query: {
-              interferenceName: this.selectedInterference?.planName || '',
-              targetName: this.selectedTarget?.planName || '',
-              measName: this.selectedMeas?.planName || ''
-            }
-          });
+      this.$router.push({
+        path: '/decision/scenarioEdit',
+        query: {
+          plan: JSON.stringify(plan)
         }
       });
+      // this.$alert(`正在编辑任务"${plan.simulationName}"的想定方案`, '提示', {
+      //   confirmButtonText: '确定',
+      //   callback: () => {
+      //     this.$router.push({
+      //       path: '/testBuild/orderEdit',
+      //       query: {
+      //         interferenceName: this.selectedInterference?.planName || '',
+      //         targetName: this.selectedTarget?.planName || '',
+      //         measName: this.selectedMeas?.planName || ''
+      //       }
+      //     });
+      //   }
+      // });
     },
 
     /* 打开详情抽屉 */
@@ -878,7 +935,7 @@ export default {
         testTaskType: undefined,
         simType: 1,
         simulationType: 1,
-        secretLevel: undefined,
+        // secretLevel: undefined,
         startTime: '',
         remarks: ''
       };

+ 256 - 0
src/views/decision/testBuild/scenario/EquipmentSidebar.vue

@@ -0,0 +1,256 @@
+<template>
+  <div class="sidebar-section">
+    <h3>
+      装备信息
+      <el-button
+          class="collapse-btn"
+          :icon="isEquipmentExpanded ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
+          @click="toggleEquipmentExpand"
+          size="mini"
+          type="text"
+      >
+        {{ isEquipmentExpanded ? '收起' : '展开' }}
+      </el-button>
+    </h3>
+
+    <!-- 默认展开 -->
+    <div class="target-list" v-show="isEquipmentExpanded">
+      <div class="tree-container">
+        <el-tree
+            ref="tree"
+            :data="equipmentTree"
+            :props="defaultProps"
+            node-key="id"
+            @node-click="handleEquipmentClick"
+            class="equipment-tree"
+            :indent="16"
+            default-expand-all
+            highlight-current
+        />
+      </div>
+    </div>
+
+    <!-- 装备详情弹窗(可选,想要仅编辑可以不打开) -->
+    <el-dialog
+        title="装备详情"
+        :visible.sync="equipmentDialogVisible"
+        width="30%"
+        :modal="true"
+        :close-on-click-modal="true"
+        custom-class="equipment-dialog"
+    >
+      <div class="equipment-detail-modal">
+        <div class="modal-header">
+          <div class="modal-title">
+            <span
+                :class="[
+                'unit-indicator',
+                selectedEquipment.indicatorClass || getIndicatorClassByType(selectedEquipment.type)
+              ]"
+            />
+            {{ selectedEquipment.name || selectedEquipment.label }}
+          </div>
+          <div class="modal-side"></div>
+        </div>
+
+        <div class="modal-body">
+          <div class="detail-grid">
+            <div
+                class="detail-row"
+                v-for="(item, index) in getEquipmentDetails(selectedEquipment)"
+                :key="index"
+            >
+              <div class="detail-label">{{ item.label }}:</div>
+              <div class="detail-value">{{ item.value }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'EquipmentSidebar',
+  props: {
+    equipmentTree: {
+      type: Array,
+      required: true
+    },
+    /** 是否在挂载/数据就绪时自动选中第一个叶子节点 */
+    autoSelectFirst: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      isEquipmentExpanded: true, // 默认展开
+      defaultProps: {
+        children: 'children',
+        label: 'label'
+      },
+      equipmentDialogVisible: false,
+      selectedEquipment: {}
+    }
+  },
+  mounted() {
+    // 首次挂载后,尝试自动选中第一个叶子
+    if (this.autoSelectFirst) {
+      this.$nextTick(this.selectFirstLeaf)
+    }
+  },
+  watch: {
+    // 当树数据变更(例如异步加载完成)且当前没有已选节点时,再自动选一次
+    equipmentTree: {
+      deep: true,
+      handler() {
+        if (!this.autoSelectFirst) return
+        this.$nextTick(() => {
+          const curKey = this.$refs.tree && this.$refs.tree.getCurrentKey && this.$refs.tree.getCurrentKey()
+          if (!curKey) this.selectFirstLeaf()
+        })
+      }
+    }
+  },
+  methods: {
+    toggleEquipmentExpand() {
+      this.isEquipmentExpanded = !this.isEquipmentExpanded
+    },
+    handleEquipmentClick(data) {
+      // 仅叶子节点触发选择(保持你的原逻辑)
+      if (!data.children) {
+        this.$emit('select', data)
+
+        // 如需弹窗预览,可放开
+        // this.selectedEquipment = data
+        // this.equipmentDialogVisible = true
+      }
+    },
+    /** 选中并高亮第一个叶子节点,同时向父组件派发 select 事件 */
+    selectFirstLeaf() {
+      const leaf = this.findFirstLeaf(this.equipmentTree)
+      if (!leaf) return
+      this.$nextTick(() => {
+        if (this.$refs.tree && this.$refs.tree.setCurrentKey) {
+          this.$refs.tree.setCurrentKey(leaf.id)
+        }
+        this.$emit('select', leaf)
+      })
+    },
+    /** 深度优先找到第一个没有 children 的节点 */
+    findFirstLeaf(nodes = []) {
+      for (const n of nodes) {
+        if (Array.isArray(n.children) && n.children.length) {
+          const got = this.findFirstLeaf(n.children)
+          if (got) return got
+        } else {
+          return n
+        }
+      }
+      return null
+    },
+
+    // ——下方工具函数保持原样——
+    getIndicatorClassByType(type) {
+      const map = { measurement: 'blue', interference: 'red', target: 'green' }
+      return map[type] || 'gray'
+    },
+    getDetailLabel(key) {
+      const map = {
+        power: '电源',
+        coverage: '覆盖范围',
+        duration: '持续时间',
+        frequency: '频率',
+        range: '作用距离',
+        battery: '电量',
+        frequencyRange: '频率范围',
+        responseTime: '响应时间',
+        altitude: '高度',
+        speed: '速度',
+        resolution: '分辨率',
+        frameRate: '帧率',
+        storage: '存储容量',
+        triggerMode: '触发模式',
+        size: '尺寸',
+        material: '材料',
+        rcs: '雷达截面积',
+        heatSignature: '热特征',
+        model: '型号',
+        deploymentTime: '部署时间',
+        channels: '通道数'
+      }
+      return map[key] || key
+    },
+    getDeviceTypeName(type) {
+      const map = { measurement: '测量设备', interference: '干扰设备', target: '靶标设备' }
+      return map[type] || type
+    },
+    getEquipmentDetails(equipment) {
+      if (equipment.details && Array.isArray(equipment.details)) return equipment.details
+
+      if (equipment.details && typeof equipment.details === 'object') {
+        return Object.entries(equipment.details).map(([key, value]) => ({
+          label: this.getDetailLabel(key),
+          value
+        }))
+      }
+
+      return [
+        { label: '类型', value: this.getDeviceTypeName(equipment.type) },
+        { label: '子类型', value: equipment.subType || '未知' },
+        { label: '状态', value: equipment.status || '未知' }
+      ]
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+h3 { display: flex; justify-content: space-between; align-items: center; }
+
+.collapse-btn {
+  font-size: 12px; color: #95a5a6; padding: 2px 6px;
+  &:hover { color: #597A37; }
+}
+
+::v-deep .el-tree { background-color: transparent; }
+
+.tree-container {
+  background-color: #34495e; border-radius: 4px; padding: 10px;
+  max-height: 300px; overflow-y: auto; box-shadow: inset 0 0 5px rgba(0,0,0,.2);
+}
+.equipment-tree { color: #ecf0f1; }
+
+/* 节点 */
+::v-deep .el-tree-node__content {
+  height: 36px; line-height: 36px; border-radius: 3px; margin-bottom: 2px; transition: all .2s;
+}
+::v-deep .el-tree-node__label { font-size: .9rem; padding: 0 4px; }
+::v-deep .el-tree-node:focus > .el-tree-node__content { background-color: rgba(89,122,55,.3); color: #fff; }
+::v-deep .el-tree-node__content:hover { background-color: rgba(89,122,55,.15); color: #fff; }
+::v-deep .el-tree__expand-icon { color: #959e99; width: 18px; height: 18px; }
+::v-deep .el-tree__expand-icon.expanded { color: #597A37; }
+::v-deep .el-tree-node__children { padding-left: 10px !important; }
+
+/* 弹窗(保留) */
+.equipment-detail-modal { color: #333; }
+.modal-header {
+  display: flex; justify-content: space-between; align-items: center;
+  margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee;
+}
+.modal-title { display: flex; align-items: center; font-size: 16px; font-weight: bold; }
+.unit-indicator {
+  display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px;
+  &.blue { background-color: #3498db; }
+  &.red { background-color: #e74c3c; }
+  &.purple { background-color: #9b59b6; }
+  &.green { background-color: #2ecc71; }
+  &.gray { background-color: #95a5a6; }
+}
+.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+.detail-row { padding: 5px 0; border-bottom: 1px dashed #eee; &:last-child { border-bottom: none; } }
+.detail-label { color: #7f8c8d; font-size: 14px; }
+.detail-value { color: #2c3e50; font-weight: 500; }
+</style>

+ 361 - 0
src/views/decision/testBuild/scenario/GanttChart.vue

@@ -0,0 +1,361 @@
+<template>
+  <div class="gantt-container">
+    <div class="chart-wrapper">
+      <div ref="ganttChart" class="gantt-chart" :style="{width: '100%', height: '600px'}"></div>
+    </div>
+    <div class="legend-container">
+      <div class="legend-items">
+        <!-- 图例项添加点击事件,传递设备类型(item.name) -->
+        <div
+          class="legend-item"
+          v-for="(item, index) in legendData"
+          :key="index"
+          @click="handleLegendClick(item.name)"
+          :style="{ cursor: 'pointer', opacity: isLegendActive(item.name) ? 1 : 0.5 }"
+        >
+          <div class="legend-marker" :style="{ backgroundColor: item.color }"></div>
+          <div class="legend-text">{{ item.name }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import 'echarts/theme/macarons'
+
+export default {
+  name: 'GanttChart',
+  props: {
+    timelineData: {
+      type: Array,
+      required: true,
+      default: () => []
+    },
+    typeColorMap: {
+      type: Object,
+      default: () => ({
+        '测量装备': '#5470c6',
+        '靶标装备': '#91cc75',
+        '干扰装备': '#fac858'
+      })
+    },
+    defaultDuration: {
+      type: Number
+    },
+    timeMargin: {
+      type: Number,
+      default: 300 // 默认5分钟边距
+    }
+  },
+  data() {
+    return {
+      chart: null,
+      // 存储图例激活状态:key=设备类型(如“靶标装备”),value=是否显示(true/false)
+      activeLegends: {}
+    }
+  },
+  computed: {
+    legendData() {
+      const legends = Object.keys(this.typeColorMap).map(type => ({
+        name: type,
+        color: this.typeColorMap[type]
+      }));
+      // 初始化激活状态:默认全部显示
+      this.initActiveLegends(legends);
+      return legends;
+    },
+
+    timeRange() {
+      if (this.timelineData.length === 0) {
+        return { min: 0, max: 3600 }
+      }
+
+      const getTimeInSeconds = (timeStr) => {
+        const cleanTimeStr = timeStr.startsWith('T0+') ? timeStr.slice(3) : timeStr;
+        const [hours, minutes, seconds] = cleanTimeStr.split(':').map(Number)
+        return hours * 3600 + minutes * 60 + seconds
+      }
+
+      const times = this.timelineData.flatMap(item => {
+        const startTime = getTimeInSeconds(item.rawTime)
+        let endTime;
+        if (item.duration !== undefined) endTime = startTime + item.duration;
+        else if (this.defaultDuration !== undefined) endTime = startTime + this.defaultDuration;
+        else endTime = Math.min(startTime + 86399, 86399);
+        return [startTime, endTime];
+      });
+
+      const minTime = Math.min(...times) - this.timeMargin;
+      const maxTime = Math.max(...times) + this.timeMargin;
+      return {
+        min: Math.max(0, minTime),
+        max: Math.min(86399, maxTime)
+      }
+    },
+
+    processedData() {
+      const equipmentNames = [...new Set(this.timelineData.map(item => item.name))]
+      const getTimeInSeconds = (timeStr) => {
+        const cleanTimeStr = timeStr.startsWith('T0+') ? timeStr.slice(3) : timeStr;
+        const [hours, minutes, seconds] = cleanTimeStr.split(':').map(Number)
+        return hours * 3600 + minutes * 60 + seconds
+      }
+
+      // 1. 先处理原始数据(不过滤)
+      const rawProcessedData = this.timelineData.map(item => {
+        const startTime = getTimeInSeconds(item.rawTime)
+        let duration;
+        if (item.duration !== undefined) duration = item.duration;
+        else if (this.defaultDuration !== undefined) duration = this.defaultDuration;
+        else duration = 86399 - startTime;
+        const endTime = Math.min(startTime + duration, 86399)
+
+        const displayTime = item.rawTime.startsWith('T0+') ? item.rawTime : `T0+${item.rawTime}`;
+        const displayText = `${item.title || '无标题'}  ${displayTime}\n${item.desc || '无描述'}\n${item.kindText || ''}  ${item.triggerTypeText || ''}  ${item.name || ''}`
+
+        return {
+          name: item.name,
+          kindText: item.kindText, // 存储设备类型(用于过滤)
+          value: [
+            equipmentNames.indexOf(item.name),
+            startTime,
+            endTime,
+            endTime - startTime,
+            item.title
+          ],
+          itemStyle: {
+            color: this.typeColorMap[item.kindText] || '#ccc'
+          },
+          label: {
+            text: displayText,
+            color: this.getTextColor(this.typeColorMap[item.kindText] || '#ccc')
+          }
+        }
+      });
+
+      // 2. 根据图例激活状态过滤数据:只保留激活类型的设备
+      return rawProcessedData.filter(item => {
+        // 如果激活状态为空(初始化前),默认显示全部
+        if (Object.keys(this.activeLegends).length === 0) return true;
+        // 只显示激活状态为true的设备类型
+        return this.activeLegends[item.kindText] === true;
+      });
+    }
+  },
+  watch: {
+    timelineData: { deep: true, handler: 'updateChart' },
+    typeColorMap: { deep: true, handler: 'updateChart' },
+    defaultDuration: { handler: 'updateChart' },
+    timeMargin: { handler: 'updateChart' },
+    activeLegends: { deep: true, handler: 'updateChart' } // 监听激活状态变化,重新渲染
+  },
+  mounted() {
+    this.$nextTick(() => {
+      const container = this.$refs.ganttChart
+      if (!container.clientWidth || !container.clientHeight) {
+        container.style.width = '100%'
+        container.style.height = '600px'
+      }
+      this.initChart()
+    })
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    if (this.chart) this.chart.dispose()
+  },
+  methods: {
+    // 初始化图例激活状态:默认全部显示
+    initActiveLegends(legends) {
+      const newActiveLegends = {};
+      legends.forEach(item => {
+        // 如果已有激活状态,保留;否则默认true(显示)
+        newActiveLegends[item.name] = this.activeLegends[item.name] ?? true;
+      });
+      this.activeLegends = newActiveLegends;
+    },
+
+    // 判断图例是否激活(用于控制透明度)
+    isLegendActive(legendName) {
+      // 初始化前默认激活,否则取activeLegends的值
+      return Object.keys(this.activeLegends).length === 0
+        ? true
+        : this.activeLegends[legendName] === true;
+    },
+
+    // 图例点击事件:切换激活状态
+    handleLegendClick(legendName) {
+      this.activeLegends = {
+        ...this.activeLegends,
+        [legendName]: !this.activeLegends[legendName] // 取反当前状态
+      };
+    },
+
+    getTextColor(backgroundColor) {
+      if (!backgroundColor) return '#000'
+      let hex = backgroundColor.replace('#', '')
+      if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
+      const r = parseInt(hex.substring(0, 2), 16)
+      const g = parseInt(hex.substring(2, 4), 16)
+      const b = parseInt(hex.substring(4, 6), 16)
+      const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
+      return luminance > 0.5 ? '#000000' : '#ffffff'
+    },
+
+    initChart() {
+      if (!this.$refs.ganttChart) return
+      if (this.chart) this.chart.dispose()
+      this.chart = echarts.init(this.$refs.ganttChart, 'macarons')
+      this.setChartOption()
+    },
+
+    setChartOption() {
+      if (!this.chart) return
+      const equipmentNames = [...new Set(this.timelineData.map(item => item.name))]
+      const formatTime = (seconds) => {
+        const hours = Math.floor(seconds / 3600).toString().padStart(2, '0')
+        const minutes = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0')
+        const secs = (seconds % 60).toString().padStart(2, '0')
+        return `T0+${hours}:${minutes}:${secs}`
+      }
+
+      const option = {
+        tooltip: { show: false },
+        grid: { left: '5%', right: '5%', top: '10%', bottom: '15%' },
+        xAxis: {
+          type: 'value',
+          name: '时间',
+          nameLocation: 'middle',
+          nameGap: 30,
+          min: this.timeRange.min,
+          max: this.timeRange.max,
+          axisLabel: { formatter: value => formatTime(value) },
+          splitLine: { lineStyle: { type: 'dashed' } }
+        },
+        yAxis: {
+          type: 'category',
+          data: equipmentNames,
+          show: false,
+          splitLine: { show: false }
+        },
+        series: [
+          {
+            type: 'custom',
+            data: this.processedData, // 使用过滤后的data
+            renderItem: (params, api) => {
+              const categoryIndex = api.value(0)
+              const start = api.coord([api.value(1), categoryIndex])
+              const end = api.coord([api.value(2), categoryIndex])
+              const height = 60
+              const width = end[0] - start[0]
+              const minWidth = 200 // 增大最小宽度,适配靠左文本
+              const finalWidth = Math.max(width, minWidth)
+              const dataItem = this.processedData[params.dataIndex]
+
+              // 色块
+              const rect = {
+                type: 'rect',
+                shape: {
+                  x: start[0],
+                  y: start[1] - height / 2,
+                  width: finalWidth,
+                  height: height,
+                  r: 4
+                },
+                style: api.style(),
+                z2: 0
+              }
+
+              // 靠左文本(保持原配置)
+              const text = {
+                type: 'text',
+                x: start[0] + 10,
+                y: start[1],
+                style: {
+                  text: dataItem.label.text,
+                  textAlign: 'left',
+                  textVerticalAlign: 'middle',
+                  fill: dataItem.label.color,
+                  font: '12px sans-serif',
+                  lineHeight: 18
+                },
+                z: 1
+              }
+
+              return { type: 'group', children: [rect, text] }
+            },
+            encode: { x: [1, 2], y: 0 }
+          }
+        ]
+      }
+
+      this.chart.setOption(option)
+    },
+
+    updateChart() {
+      this.chart ? this.setChartOption() : this.initChart()
+    },
+
+    handleResize() {
+      if (this.chart) setTimeout(() => this.chart.resize(), 100)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.gantt-container {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  min-width: 800px;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+.chart-wrapper {
+  flex: 1;
+  min-height: 600px;
+}
+
+.gantt-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 600px;
+}
+
+.legend-container {
+  display: flex;
+  flex-direction: column;
+  background-color: transparent;
+  border-radius: 4px;
+  margin-top: 10px;
+}
+
+.legend-items {
+  display: flex;
+  justify-content: center;
+  gap: 20px;
+  flex-wrap: wrap;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  transition: opacity 0.3s ease;
+}
+
+.legend-marker {
+  width: 16px;
+  height: 16px;
+  border-radius: 3px;
+  margin-right: 6px;
+}
+
+.legend-text {
+  color: #ffffff;
+  font-size: 14px;
+}
+</style>

+ 67 - 0
src/views/decision/testBuild/scenario/TaskInfo.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="sidebar-section">
+    <h3>任务信息</h3>
+    <div class="info-list">
+      <div class="task-info">
+        <div class="task-info-label">任务名称:</div>
+        <div class="task-info-value">{{ taskData.schemeName }}</div>
+      </div>
+      <div class="task-info">
+        <div class="task-info-label">任务代号:</div>
+        <div class="task-info-value">{{ taskData.taskCode }}</div>
+      </div>
+      <div class="task-info">
+        <div class="task-info-label">任务要求:</div>
+        <div class="task-info-value">{{ taskData.taskRequirements }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TaskInfo',
+  props: {
+    taskData: {
+      type: Object,
+      required: true,
+      default: () => ({
+        schemeName: '',
+        taskCode: '',
+        taskRequirements: ''
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* 可复用原样式 */
+.sidebar-section {
+  margin-bottom: 20px;
+  padding: 10px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+h3 {
+  margin-top: 0;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #ddd;
+}
+
+.task-info {
+  margin-bottom: 12px;
+}
+
+.task-info-label {
+  margin-bottom: 4px;
+  color: #bdc3c7;
+}
+
+.task-info-value {
+  white-space: pre-line;
+  color: white;
+  font-weight: bold;
+}
+</style>

+ 239 - 0
src/views/decision/testBuild/scenario/dd-upload.vue

@@ -0,0 +1,239 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible.sync="dialogFormVisible"
+    :close-on-click-modal="false"
+    width="800px"
+    class="import-excel"
+    @close="close"
+  >
+    <el-upload
+      ref="excelImport"
+      drag
+      accept=".xls,.xlsx"
+      style="width: 100%"
+      :action="importExcelUrl"
+      :multiple="false"
+      :before-upload="beforeUpload"
+      :http-request="handleImport"
+      :on-success="onSuccess"
+      :on-error="onError"
+      :on-progress="onProcess"
+    >
+      <i class="el-icon-upload"></i>
+      <div class="el-upload__text">
+        将文件拖到此处,或
+        <em>点击导入</em>
+      </div>
+      <div slot="tip" class="el-upload__tip">
+        只能上传xls/xlsx文件,且不超过5MB
+      </div>
+    </el-upload>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="downloadExcelTemplate">
+        下载模版
+      </el-button>
+      <el-button @click="close">关 闭</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import {
+  doDownloadTemplate,
+  doImportExcel,
+} from "@/api/battlefieldEnvironment/index";
+import { isNull } from "@/utils/validate";
+import { random } from "@/utils";
+
+export default {
+  name: "dd-upload",
+  data() {
+    return {
+      title: "导入Excel",
+      importExcelUrl: "",
+      dialogFormVisible: false,
+      loadProgress: 0, // 动态显示进度条
+      progressFlag: false, // 关闭进度条,
+      progressMap: {},
+      row:{},
+    };
+  },
+  created() {},
+  mounted() {},
+  methods: {
+    show(row) {
+      this.row = row;
+      this.dialogFormVisible = true;
+    },
+    close() {
+      this.dialogFormVisible = false;
+      this.row = {};
+      this.$refs["excelImport"].clearFiles();
+    },
+    // 下载模版
+    downloadExcelTemplate() {
+      doDownloadTemplate();
+    },
+    // 上传成功
+    onSuccess(response, file, fileList) {
+      this.successProcess(file.uid);
+      this.$emit("fetchData");
+    },
+    // 上传失败
+    onError(err, file, fileList) {
+      this.errorProcess(file.uid);
+    },
+    // 进度条
+    onProcess(event, file, fileList) {
+      file.status = "uploading";
+      file.percentage = 0;
+      this.progressMap[file.uid] = {
+        file: file,
+      };
+      this.autoLoadingProcess(file.uid);
+    },
+
+    // 导入文件限制验证
+    beforeUpload(file) {
+      let testMsg = file.name.substring(file.name.lastIndexOf(".") + 1);
+      const extension = testMsg === "xls";
+      const extension2 = testMsg === "xlsx";
+      const isLt2M = file.size / 1024 / 1024 < 5;
+      if (!extension && !extension2) {
+        this.$baseMessage("上传文件只能是 xls、xlsx格式!", "warning");
+      }
+      if (!isLt2M) {
+        this.$baseMessage("上传文件大小不能超过 5MB!", "warning");
+      }
+      return (extension || extension2) && isLt2M;
+    },
+    // 自定义导入
+    handleImport(params) {
+      if (!isNull(params)) {
+        let blobObject = new Blob([params.file]);
+        let formData = new window.FormData();
+        formData.append("file", blobObject);
+        formData.append('simulationId',this.row.simulationId)
+        formData.append("missileId",this.row.missileId)
+        const ret = doImportExcel(formData);
+        ret
+          .then((v) => {
+            const { msg, data } = v;
+            this.$baseMessage(msg, "success");
+            // 成功
+            params.onSuccess();
+          })
+          .catch((e) => {
+            // 失败
+            params.onError();
+          });
+        // 上传进度
+        params.onProgress();
+      } else {
+        params.onError();
+      }
+    },
+
+    // ==============
+
+    successProcess(fileUid) {
+      let tmp = this.progressMap[fileUid];
+      if (tmp !== null && tmp !== undefined) {
+        try {
+          window.clearTimeout(tmp.timer);
+        } catch (e) {}
+        tmp.file.status = "success";
+        tmp.file.percentage = 100;
+        delete this.progressMap[fileUid];
+      }
+    },
+    errorProcess(fileUid) {
+      let tmp = this.progressMap[fileUid];
+      if (tmp !== null && tmp !== undefined) {
+        try {
+          window.clearTimeout(tmp.timer);
+        } catch (e) {}
+        tmp.file.status = "fail";
+        delete this.progressMap[fileUid];
+      }
+    },
+    autoLoadingProcess(fileUid) {
+      const that = this;
+      let tmp = this.progressMap[fileUid];
+      if (tmp !== null && tmp !== undefined) {
+        if (tmp.file.percentage >= 99) {
+          try {
+            window.clearTimeout(tmp.timer);
+          } catch (e) {}
+        } else {
+          // 如果大于 99 则 停止
+          if (tmp.file.percentage + random(1, 12) > 99) {
+            tmp.file.percentage = 99;
+          } else {
+            // 进度随机增长 1 - 12
+            tmp.file.percentage += random(1, 12);
+          }
+
+          // 递归增加百分比 递归时间为 随机 1-5秒
+          tmp.timer = window.setTimeout(function () {
+            that.autoLoadingProcess(fileUid);
+          }, random(1000, 5000));
+        }
+      }
+    },
+  },
+};
+</script>
+
+<style>
+
+/* 上传拖拽区域 */
+.el-upload-dragger {
+  background-color: #4a86e8; /* 浅蓝色背景 */
+  color: #003366; /* 深蓝色文字 */
+}
+
+/* 当鼠标悬停在上传区域上时 */
+.el-upload-dragger:hover {
+  border-color: #002b5c; /* 更深的蓝色虚线边框 */
+}
+
+/* 上传图标 */
+.el-upload-dragger .el-icon-upload {
+  font-size: 67px;
+  color: #003366; /* 深蓝色图标 */
+  margin: 40px 0 16px;
+  line-height: 50px;
+}
+
+/* 提示文字 */
+.el-upload-dragger .el-upload__text {
+  color: #003366; /* 深蓝色提示文字 */
+}
+
+/* 文件列表项 */
+.el-upload-list__item-name {
+  color: #003366; /* 深蓝色文件名 */
+}
+/* 当鼠标悬停在按钮上时改变颜色 */
+.el-upload:hover {
+  background-color: darken(#1890ff, 10%); /* 稍微加深的蓝色 */
+}
+
+/* 修改已上传文件列表中的文字颜色 */
+.el-upload-list__item-name {
+  color: #333; /* 深灰色文字 */
+}
+
+/* 成功状态下的图标颜色 */
+.el-icon-circle-check {
+  color: #67c23a; /* 成功绿色 */
+}
+
+/* 错误状态下的图标颜色 */
+.el-icon-error {
+  color: #f56c6c; /* 错误红色 */
+}
+</style>

+ 297 - 0
src/views/decision/testBuild/scenario/equipment/EquipmentListWithTimeline.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="equip-list-and-timeline">
+    <!-- 列表 -->
+    <div class="list-wrap fill-scroll">
+      <!-- 靶标 -->
+      <el-table
+        v-if="typeKey==='target'"
+        :data="items"
+        border
+        size="small"
+        stripe
+      >
+        <el-table-column label="名称" min-width="160" prop="name"/>
+        <el-table-column label="类型" prop="type" width="80"/>
+        <el-table-column label="经度" min-width="140" prop="lon"/>
+        <el-table-column label="纬度" min-width="140" prop="lat"/>
+        <el-table-column label="激活时间" prop="time" width="110"/>
+        <el-table-column align="center" label="操作" width="200">
+          <template slot-scope="scope">
+            <el-button
+              icon="el-icon-edit"
+              size="mini"
+              type="text"
+              @click="$emit('edit', scope.$index)"
+            >编辑</el-button>
+            <el-button
+              class="danger-btn"
+              icon="el-icon-delete"
+              size="mini"
+              type="text"
+              @click="$emit('remove', scope.$index)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 干扰 -->
+      <el-table
+        v-if="typeKey==='jammer'"
+        :data="items"
+        border
+        size="small"
+        stripe
+      >
+        <el-table-column label="名称" min-width="160" prop="name"/>
+        <el-table-column label="类型" prop="type" width="80"/>
+        <el-table-column label="经度" min-width="140" prop="lon"/>
+        <el-table-column label="纬度" min-width="140" prop="lat"/>
+        <el-table-column align="center" label="操作" width="220">
+          <template slot-scope="scope">
+            <el-button
+              icon="el-icon-edit"
+              size="mini"
+              type="text"
+              @click="$emit('edit', scope.$index)"
+            >编辑</el-button>
+            <el-button
+              class="danger-btn"
+              icon="el-icon-delete"
+              size="mini"
+              type="text"
+              @click="$emit('remove', scope.$index)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 测量 -->
+      <el-table
+        v-if="typeKey==='measurement'"
+        :data="items"
+        border
+        size="small"
+        stripe
+      >
+        <el-table-column label="名称" min-width="160" prop="name"/>
+        <el-table-column label="类型" prop="type" width="80"/>
+        <el-table-column label="经度" min-width="140" prop="lon"/>
+        <el-table-column label="纬度" min-width="140" prop="lat"/>
+        <el-table-column label="启动时间" prop="time" width="110"/>
+        <el-table-column align="center" label="操作" width="200">
+          <template slot-scope="scope">
+            <el-button
+              icon="el-icon-edit"
+              size="mini"
+              type="text"
+              @click="$emit('edit', scope.$index)"
+            >编辑</el-button>
+            <el-button
+              class="danger-btn"
+              icon="el-icon-delete"
+              size="mini"
+              type="text"
+              @click="$emit('remove', scope.$index)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 当前类事件时序 -->
+    <div class="timeline-section">
+      <div class="section-title">
+        <i class="el-icon-time"></i><span>事件时序</span>
+        <el-button size="mini" type="text" @click="$emit('refresh')">刷新</el-button>
+      </div>
+      <div class="timeline-wrap fill-scroll">
+        <el-timeline>
+          <el-timeline-item
+            v-for="(e,i) in currentPreviewEvents"
+            :key="i"
+            :timestamp="e.time"
+            :type="getTimelineType(e.typeClass)"
+            placement="top"
+          >
+            <el-card :shadow="false" class="ui-card soft tl-card">
+              <div class="tl-title">{{ e.title }}</div>
+              <div class="tl-desc">
+                {{ e.desc }}
+                <el-tag :type="getTagType(e.badgeClass)" class="m-l-6" size="mini">
+                  {{ e.kindText }}
+                </el-tag>
+                <el-tag v-if="e.triggerTypeText" class="m-l-6" size="mini" type="info">
+                  {{ e.triggerTypeText }}
+                </el-tag>
+              </div>
+            </el-card>
+          </el-timeline-item>
+        </el-timeline>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'EquipmentListWithTimeline',
+  props: {
+    /**
+     * 'target' | 'jammer' | 'measurement'
+     */
+    typeKey: {
+      type: String,
+      required: true
+    },
+    /**
+     * 列表数据(随 typeKey 结构不同):
+     * target: { name,type,lon,lat,time,statusText,statusClass,params }
+     * jammer: { name,type,lon,lat,params,schedules:[{time,triggerType,triggerDesc,delaySec}] }
+     * measurement: { name,type,lon,lat,time,params }
+     */
+    items: {
+      type: Array,
+      default: () => []
+    }
+  },
+  computed: {
+    currentPreviewEvents() {
+      const list = [];
+      if (this.typeKey === 'target') {
+        this.items.forEach(t => {
+          if (!t.time) return;
+          list.push({
+            time: `T0+${t.time}`,
+            rawTime: t.time,
+            title: t.type === '动态' ? '动态靶标运行' : '靶标激活',
+            desc: `${t.name} ${t.type === '动态' ? '按设定运动' : '激活'}`,
+            kindText: '靶标装备',
+            typeClass: t.type === '动态' ? 'tl-danger' : 'tl-warn',
+            badgeClass: t.statusText === '可用'
+              ? 'badge-success'
+              : (t.statusText === '维护中' ? 'badge-warn' : 'badge-accent'),
+            triggerTypeText: '定时触发'
+          });
+        });
+      } else if (this.typeKey === 'jammer') {
+        this.items.forEach(j => {
+          (j.schedules || []).forEach(s => {
+            if (!s.time) return;
+            list.push({
+              time: `T0+${s.time}`,
+              rawTime: s.time,
+              title: '干扰装备启动',
+              desc: `${j.name} ${s.triggerType === 'condition' ? '(条件触发)' : '(激活)'}`,
+              kindText: '干扰装备',
+              typeClass: s.triggerType === 'condition' ? 'tl-danger' : 'tl-muted',
+              badgeClass: s.triggerType === 'condition' ? 'badge-danger' : 'badge-accent',
+              triggerTypeText: s.triggerType === 'time'
+                ? '定时触发'
+                : (s.triggerType === 'manual' ? '手动触发' : '条件触发')
+            });
+          });
+        });
+      } else if (this.typeKey === 'measurement') {
+        this.items.forEach(m => {
+          if (!m.time) return;
+          list.push({
+            time: `T0+${m.time}`,
+            rawTime: m.time,
+            title: '测量装备启动',
+            desc: `${m.name} 开始工作`,
+            kindText: '测量装备',
+            typeClass: 'tl-secondary',
+            badgeClass: 'badge-secondary',
+            triggerTypeText: '定时触发'
+          });
+        });
+      }
+      return list.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime));
+    }
+  },
+  methods: {
+    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;
+    },
+    getTimelineType(c) {
+      return c === 'tl-secondary'
+        ? 'success'
+        : c === 'tl-warn'
+          ? 'warning'
+          : c === 'tl-danger'
+            ? 'danger'
+            : c === 'tl-muted'
+              ? 'info'
+              : 'primary';
+    },
+    getTagType(c) {
+      return c === 'badge-success'
+        ? 'success'
+        : c === 'badge-warn'
+          ? 'warning'
+          : c === 'badge-danger'
+            ? 'danger'
+            : c === 'badge-accent'
+              ? 'info'
+              : c === 'badge-secondary'
+                ? 'primary'
+                : 'default';
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* 仅此组件样式(不污染父页) */
+.equip-list-and-timeline{
+  display:grid;
+  grid-template-rows: 1fr 1fr;
+  gap:12px;
+  min-height:0;
+  height:100%;
+}
+
+/* 列表与时间轴容器复用父页命名,便于主题一致 */
+.list-wrap{ min-height:0; overflow:auto; }
+.timeline-section{ display:flex; flex-direction:column; min-height:0; }
+.section-title{ display:flex; align-items:center; gap:8px; margin:0 2px 6px; color:#EAF2FF; font-weight:700; }
+.timeline-wrap{ padding-right:4px; overflow:auto; }
+
+/* 质感与父页统一 */
+::v-deep .el-table{
+  background:var(--card-soft);
+  color:var(--text-2);
+  border-color:rgba(255,255,255,.08);
+}
+::v-deep .el-table th{
+  background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
+  color:var(--text); font-weight:700;
+  border-color:rgba(255,255,255,.08);
+}
+::v-deep .el-table td{ border-color:rgba(255,255,255,.06); }
+::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td{ background:rgba(255,255,255,.02); }
+::v-deep .el-table__row:hover td{ background:rgba(102,177,255,.06)!important; }
+
+.tl-card{ border-left:3px solid var(--accent); padding:10px 12px; background:rgba(255,255,255,.03)!important; }
+.tl-title{ font-weight:800; color:#EAF2FF; margin-bottom:4px; font-size:14px; }
+.tl-desc{ font-size:12px; color:var(--text-2); }
+.m-l-6{ margin-left:6px; }
+
+/* 时间轴节点颜色与父页一致 */
+.el-timeline-item__tail{ border-left-color:rgba(255,255,255,.08)!important; }
+.el-timeline-item.is-success .el-timeline-item__node{ background:#2BD99F!important; }
+.el-timeline-item.is-warning .el-timeline-item__node{ background:#FFC15A!important; }
+.el-timeline-item.is-danger  .el-timeline-item__node{ background:#FF7070!important; }
+.el-timeline-item.is-info    .el-timeline-item__node{ background:#69B1FF!important; }
+
+/* Tag 质感 */
+.el-tag--success{ background:rgba(43,217,159,.15); border-color:rgba(43,217,159,.35); color:#2BD99F; }
+.el-tag--warning{ background:rgba(255,193,90,.15); border-color:rgba(255,193,90,.35); color:#FFC15A; }
+.el-tag--danger { background:rgba(255,112,112,.15); border-color:rgba(255,112,112,.35); color:#FF7070; }
+.el-tag--info   { background:rgba(105,177,255,.12); border-color:rgba(105,177,255,.35); color:#9ED0FF; }
+.el-tag--primary{ background:rgba(105,177,255,.18); border-color:rgba(105,177,255,.4);  color:#E8F3FF; }
+
+/* 删除按钮色 */
+.danger-btn{ color: var(--danger); }
+</style>

+ 140 - 0
src/views/decision/testBuild/scenario/mission/MissileMissionCard.vue

@@ -0,0 +1,140 @@
+<template>
+    <el-form style="margin-top: 40px;padding: 0 40px" :model="local" disabled class="dense-form" label-width="120px" size="small">
+      <el-row :gutter="16">
+        <!--<el-col :span="12">-->
+        <!--  <el-form-item label="任务目标类型">-->
+        <!--    <el-select v-model="local.targetType" style="width:100%">-->
+        <!--      <el-option label="对陆目标" value="land"/>-->
+        <!--      <el-option label="对空目标" value="air"/>-->
+        <!--      <el-option label="对海目标" value="sea"/>-->
+        <!--    </el-select>-->
+        <!--  </el-form-item>-->
+        <!--</el-col>-->
+
+        <el-col :span="12">
+          <el-form-item label="总时间(s)">
+            <el-input v-model.number="local.totalTime" :min="1" type="number"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="攻击靶标">
+            <el-input v-model.number="local.attackTheTarget"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="射程/航程(km)">
+            <el-input v-model.number="local.range" :min="1" type="number"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="攻击目标方式">
+            <el-input v-model.number="local.attackTargetMethod"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="攻击目标经度">
+            <el-input v-model="local.attackTargetLongitude" placeholder="lon,lat,alt(可选)"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="攻击目标纬度">
+            <el-input v-model="local.attackTargetLatitude" placeholder="lon,lat,alt(可选)"/>
+          </el-form-item>
+        </el-col>
+
+        <!--<el-col :span="24">-->
+        <!--  <el-form-item label="任务目标特性">-->
+        <!--    <el-input v-model="local.targetDesc" :rows="3" placeholder="物理特性、防御能力等..." type="textarea"/>-->
+        <!--  </el-form-item>-->
+        <!--</el-col>-->
+      </el-row>
+
+      <div class="tool-row">
+        <el-button type="primary" :disabled="false" @click="$emit('import-trajectory', local)">导入弹道轨迹</el-button>
+        <!--<el-button type="text" @click="$emit('preview-trajectory', local.id)">-->
+        <!--  预览轨迹-->
+        <!--</el-button>-->
+      </div>
+
+      <!--<el-card body-style="padding:0;" class="ui-card panel-card trajectory-placeholder" shadow="never">-->
+      <!--  <div class="placeholder-content">-->
+      <!--    <i class="el-icon-picture-outline placeholder-icon"></i>-->
+      <!--    <p class="placeholder-text">轨迹预览</p>-->
+      <!--  </div>-->
+      <!--</el-card>-->
+    </el-form>
+</template>
+
+<script>
+export default {
+  name: 'MissileMissionCard',
+  props: {
+    /**
+     * 使用 .sync:<MissileMissionCard :missile.sync="m" />
+     * 结构示例:
+     * { id, targetType, targetDesc, traj: { start, end, timeSec } }
+     */
+    missile: { type: Object, required: true }
+  },
+  data() {
+    return {
+      // 本地副本,避免直接改父级引用导致脏写
+      local: this.clone(this.missile)
+    };
+  },
+  watch: {
+  },
+  methods: {
+    clone(o) { return JSON.parse(JSON.stringify(o || {})); }
+  }
+};
+</script>
+
+<style scoped>
+/* 复用你在父页的卡片质感与占位风格 */
+.panel-card{
+  background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02)) !important;
+  border: 1px solid rgba(255,255,255,.10) !important;
+  border-radius: 14px !important;
+  box-shadow: 0 8px 24px rgba(0,0,0,.28) !important;
+  transition: box-shadow .25s ease, transform .25s ease, border-color .25s ease;
+  overflow: hidden;
+}
+.panel-card:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 14px 36px rgba(0,0,0,.34) !important;
+  border-color: rgba(102,177,255,.28) !important;
+}
+.dense-form ::v-deep .el-form-item { margin-bottom: 8px; }
+.dense-form ::v-deep .el-form-item__label { color: var(--text-2); padding-right: 8px; }
+.dense-form ::v-deep .el-input__inner,
+.dense-form ::v-deep .el-textarea__inner,
+.dense-form ::v-deep .el-select .el-input__inner{
+  background: rgba(255,255,255,.04);
+  border: 1px solid rgba(255,255,255,.14);
+  color: var(--text);
+}
+.dense-form ::v-deep .el-input__inner:focus,
+.dense-form ::v-deep .el-textarea__inner:focus{
+  border-color: var(--accent);
+  box-shadow: 0 0 0 3px rgba(102,177,255,.18);
+}
+.tool-row{
+  text-align:right;
+  margin-top:-6px;
+  margin-bottom:8px;
+}
+.trajectory-placeholder{
+  margin-top:8px; height:240px; display:flex; align-items:center; justify-content:center;
+  border:1px dashed rgba(102,177,255,.28); border-radius:10px;
+  background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));
+}
+.placeholder-content{ text-align:center; }
+.placeholder-icon{ font-size:40px; opacity:.9; }
+.placeholder-text{ font-size:14px; color: var(--text-2); }
+</style>

+ 768 - 0
src/views/decision/testBuild/scenario/mission/MissileMissionPanel.vue

@@ -0,0 +1,768 @@
+<template>
+  <el-card class="ui-card fill-card" shadow="hover">
+    <!--<div slot="header" class="card-header">-->
+    <!--  <div class="title-left">-->
+    <!--    <i class="fa fa-crosshairs icon-primary"></i><span>试验任务规划(导弹打击目标与弹道)</span>-->
+    <!--  </div>-->
+    <!--  <div class="title-right">-->
+    <!--    <el-form inline size="small">-->
+    <!--      <el-form-item label="方案名称">-->
+    <!--        <el-input v-model="scenarioNameLocal" disabled style="width:220px;"/>-->
+    <!--      </el-form-item>-->
+    <!--      <el-form-item label="导弹数量">-->
+    <!--        <el-input-number v-model="missileCountLocal" :max="20" :min="1" :step="1" disabled/>-->
+    <!--      </el-form-item>-->
+    <!--      <el-form-item label="想定版本">-->
+    <!--        <el-input v-model="scenarioVersionLocal" style="width:120px;"/>-->
+    <!--      </el-form-item>-->
+    <!--    </el-form>-->
+    <!--  </div>-->
+    <!--</div>-->
+
+    <div class="fill-scroll">
+      <el-alert :closable="false" class="mb-16" show-icon title="每枚导弹的打击目标与弹道参数" type="info"/>
+      <el-collapse v-model="openPanels">
+        <el-collapse-item
+          v-for="(m,idx) in missilesLocal"
+          :key="m.id"
+          :name="m.id"
+          :title="`导弹 #${idx+1}`"
+        >
+          <MissileMissionCard
+            :missile="missilesLocal[idx]"
+            @import-trajectory="$emit('import-trajectory', $event)"
+            @preview-trajectory="$emit('preview-trajectory', $event)"
+          />
+        </el-collapse-item>
+      </el-collapse>
+    </div>
+
+    <el-card style="background: rgb(14 31 58)">
+      <div slot="header" class="clearfix">
+        <span>故障模拟</span>
+      </div>
+      <!-- 过滤与清空 -->
+      <div class="fault-toolbar" style="margin-bottom:8px; display:flex; align-items:center; gap:8px;">
+        <el-input
+          v-model="faultQuery"
+          placeholder="搜索故障/设备…"
+          size="mini"
+          clearable
+          class="fault-search"
+          style="width: 220px;"
+        />
+        <div style="flex:1"></div>
+        <!--<el-button type="text" class="gc-op" @click="clearAllFaults">-->
+        <!--  <i class="el-icon-circle-close"></i> 清空模拟-->
+        <!--</el-button>-->
+        <el-button
+            class="blue_btn"
+            style="margin-right: 4px"
+            @click="addFault(row)"
+        >新增
+        </el-button>
+      </div>
+
+      <!-- 列表表格 -->
+      <el-table
+        :data="filteredFaults"
+        border
+        stripe
+        size="mini"
+        height="360"
+        class="gc-table"
+        row-key="id"
+      >
+        <el-table-column type="index" label="#" width="48" align="center"/>
+        <el-table-column prop="faultName" label="故障名称" min-width="160" show-overflow-tooltip/>
+        <el-table-column prop="zbName" label="目标设备" min-width="160" show-overflow-tooltip/>
+        <el-table-column prop="executeTheScript" label="执行脚本" min-width="160" show-overflow-tooltip>
+
+        </el-table-column>
+        <el-table-column label="操作" width="250" align="center" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button
+              size="mini"
+              type="primary"
+              style="margin-right: 4px"
+              @click="jsonEdit(row)"
+            >JSON编辑器
+            </el-button>
+            <el-button
+                size="mini"
+                type="danger"
+                style="margin-right: 4px"
+                @click="deleteFault(row)"
+            >删除
+            </el-button>
+            <!--<el-button-->
+            <!--  size="mini"-->
+            <!--  type="primary"-->
+            <!--  style="padding: 2px 6px; margin-right: 4px"-->
+            <!--  :disabled="activeFaultIds.has(row.id)"-->
+            <!--  @click="triggerFault(row)"-->
+            <!--&gt;触发-->
+            <!--</el-button>-->
+            <!--<el-button-->
+            <!--  size="mini"-->
+            <!--  :disabled="!activeFaultIds.has(row.id)"-->
+            <!--  style="padding: 2px 6px"-->
+            <!--  @click="restoreFault(row.id)"-->
+            <!--&gt;恢复-->
+            <!--</el-button>-->
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 快速添加 -->
+      <!--<div class="fault-quick-add"-->
+      <!--     style="margin-top:10px; display:grid; grid-template-columns:1.2fr 1.4fr .9fr auto; gap:8px;">-->
+      <!--  <el-input v-model="quickFault.name" size="mini" placeholder="自定义故障名,如:相机离线"/>-->
+      <!--  <el-select v-model="quickFault.targetUnitId" size="mini" placeholder="选择目标设备">-->
+      <!--    <el-option-->
+      <!--      v-for="u in units"-->
+      <!--      :key="'opt-'+u.id"-->
+      <!--      :label="u.name"-->
+      <!--      :value="u.id"-->
+      <!--    />-->
+      <!--  </el-select>-->
+      <!--  &lt;!&ndash;        <el-select v-model="quickFault.severity" size="mini" placeholder="级别">&ndash;&gt;-->
+      <!--  &lt;!&ndash;          <el-option label="warning" value="warning" />&ndash;&gt;-->
+      <!--  &lt;!&ndash;          <el-option label="critical" value="critical" />&ndash;&gt;-->
+      <!--  &lt;!&ndash;        </el-select>&ndash;&gt;-->
+      <!--  <el-button size="mini" type="success" @click="addQuickFault">添加并触发</el-button>-->
+      <!--</div>-->
+    </el-card>
+    <el-dialog :title="dialogTitle" :visible.sync="showFaultEdit" width="40%">
+      <el-form ref="faultForm"
+               :rule="rules" :model="faultForm" label-width="120px">
+        <!-- 故障名称 -->
+        <el-form-item label="故障名称">
+          <el-input v-model="faultForm.faultName" placeholder="请输入故障名称" />
+        </el-form-item>
+        <el-form-item label="目标设备">
+          <el-cascader
+              v-model="faultForm.schemeEquId"
+              :options="cascaderOptions"
+              :props="{ emitPath: false, checkStrictly: true, expandTrigger: 'hover' }"
+              placeholder="请选择设备"
+              clearable
+              style="width: 300px"
+          />
+        </el-form-item>
+
+        <!-- 执行脚本(JSON 格式) -->
+        <el-form-item label="执行脚本">
+          <!--<JsonEditor :value="faultForm.executeTheScript"></JsonEditor>-->
+          <el-input
+              v-model="faultForm.executeTheScript"
+              type="textarea"
+              :rows="6"
+              placeholder='请输入 JSON 格式脚本,例如:{"cmd": "reboot", "timeout": 30}'
+          />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @click="submitForm">提交</el-button>
+        </el-form-item>
+      </el-form>
+    </el-dialog>
+
+    <el-dialog title="JSON编辑器" :visible.sync="showJsonEditor" width="40%">
+      <JsonEditor ref="JsonEditor" :value="scriptJson"></JsonEditor>
+      <el-button type="primary" @click="updeteJson">提交</el-button>
+    </el-dialog>
+  </el-card>
+</template>
+
+<script>
+import MissileMissionCard from './MissileMissionCard.vue';
+import {missileList} from "@/api/taskMage/taskMage";
+import JsonEditor from "@/components/JsonEditor/index.vue";
+import {
+  battlefieldEnvironmentInsert,
+  faultsimulationDelete,
+  faultsimulationInsert, faultsimulationUpdate,
+  findPageWrapper, getEquTree
+} from "@/api/faultSimulation";
+
+export default {
+  name: 'MissileMissionPanel',
+  components: {MissileMissionCard,JsonEditor},
+  props: {
+    planId:{type: String, default: ''},
+    taskId:{type: String, default: ''},
+    subTaskId:{type: String, default: ''},
+    missiles: {type: Array, required: true},
+    openMissilePanels: {type: Array, default: () => []},
+    scenarioName: {type: String, default: ''},
+    scenarioVersion: {type: String, default: ''},
+    missileCount: {type: Number, default: 1}
+  },
+  data() {
+    return {
+      missilesLocal: [],
+      openPanels: [...this.openMissilePanels],
+      scenarioNameLocal: this.scenarioName,
+      scenarioVersionLocal: this.scenarioVersion,
+      missileCountLocal: this.missileCount,
+      units: [
+        {
+          id: 'unit1',
+          name: '中波红外热像仪-01',
+          type: 'observation',
+          faction: 'blue',
+          status: '运行中',
+          position: 'N39°54\'05", E117°26\'10"',
+          value: '3500米',
+          statusClass: 'active',
+          icon: 'el-icon-monitor',
+          mapPosition: {top: '25%', left: '40%'},
+          params: [{label: '状态', value: '运行中'}, {label: '高度', value: '5000米'}, {
+            label: '观测范围',
+            value: '3500米'
+          }, {label: '观测精度', value: '92%'}, {label: '功率', value: '100%'}, {
+            label: '电量',
+            value: '86%'
+          }, {label: '发现目标', value: '3个'}, {label: '数据链路', value: '稳定'}]
+        },
+        {
+          id: 'unit2',
+          name: '可见光相机-03',
+          type: 'observation',
+          faction: 'blue',
+          status: '运行中',
+          position: 'N39°54\'18", E117°26\'42"',
+          value: '1200米',
+          statusClass: 'active',
+          icon: 'el-icon-camera',
+          mapPosition: {top: '32%', left: '32%'},
+          params: [{label: '状态', value: '运行中'}, {label: '高度', value: '地面'}, {
+            label: '观测范围',
+            value: '1200米'
+          }, {label: '观测精度', value: '95%'}, {label: '功率', value: '85%'}, {
+            label: '电量',
+            value: '92%'
+          }, {label: '发现目标', value: '2个'}, {label: '数据链路', value: '稳定'}]
+        },
+        {
+          id: 'unit3',
+          name: '激光测距仪-02',
+          type: 'observation',
+          faction: 'blue',
+          status: '待命',
+          position: 'N39°54\'25", E117°26\'38"',
+          value: '60%',
+          statusClass: 'standby',
+          icon: 'el-icon-aim',
+          mapPosition: {top: '16%', left: '32%'},
+          params: [{label: '状态', value: '待命'}, {label: '高度', value: '地面'}, {
+            label: '干扰范围',
+            value: '2500米'
+          }, {label: '干扰强度', value: '中等'}, {label: '功率', value: '60%'}, {
+            label: '电量',
+            value: '78%'
+          }, {label: '干扰目标', value: '无'}, {label: '数据链路', value: '稳定'}]
+        },
+        {
+          id: 'unit4',
+          name: '脱靶量复测测量设备',
+          type: 'observation',
+          faction: 'blue',
+          status: '待命',
+          position: 'N39°54\'25", E117°26\'38"',
+          value: '60%',
+          statusClass: 'standby',
+          icon: 'el-icon-data-line',
+          mapPosition: {top: '10%', left: '40%'},
+          params: [{label: '状态', value: '待命'}, {label: '高度', value: '地面'}, {
+            label: '干扰范围',
+            value: '2500米'
+          }, {label: '干扰强度', value: '中等'}, {label: '功率', value: '60%'}, {
+            label: '电量',
+            value: '78%'
+          }, {label: '干扰目标', value: '无'}, {label: '数据链路', value: '稳定'}]
+        },
+        {
+          id: 'unit5',
+          name: '指挥中心',
+          type: 'red',
+          faction: 'blue',
+          status: '完好',
+          position: 'N39°54\'15", E117°26\'38"',
+          value: 'A级',
+          statusClass: 'active',
+          icon: 'el-icon-s-home',
+          mapPosition: {top: '24%', left: '31%'},
+          params: [{label: '状态', value: '完好'}, {label: '高度', value: '地面'}, {
+            label: '防护等级',
+            value: 'A级'
+          }, {label: '人员数量', value: '25'}, {label: '通信状态', value: '良好'}, {
+            label: '电力供应',
+            value: '100%'
+          }, {label: '威胁评估', value: '低'}, {label: '防御系统', value: '激活'}]
+        },
+        {
+          id: 'unit10',
+          name: '充气式角反射体',
+          type: 'interference',
+          faction: 'blue',
+          status: '完好',
+          position: 'N39°54\'15", E117°26\'38"',
+          value: 'A级',
+          statusClass: 'active',
+          icon: 'el-icon-connection',
+          mapPosition: {top: '43%', left: '42%'},
+          params: [{label: '状态', value: '完好'}, {label: '高度', value: '地面'}, {
+            label: '防护等级',
+            value: 'A级'
+          }, {label: '人员数量', value: '25'}, {label: '通信状态', value: '良好'}, {
+            label: '电力供应',
+            value: '100%'
+          }, {label: '威胁评估', value: '低'}, {label: '防御系统', value: '激活'}]
+        },
+        {
+          id: 'unit12',
+          name: '模拟机库',
+          type: 'target',
+          faction: 'blue',
+          status: '完好',
+          position: 'N39°54\'15", E117°26\'38"',
+          value: 'A级',
+          statusClass: 'active',
+          icon: 'el-icon-school',
+          mapPosition: {top: '37%', left: '46%'},
+          params: [{label: '状态', value: '完好'}, {label: '高度', value: '地面'}, {
+            label: '防护等级',
+            value: 'A级'
+          }, {label: '人员数量', value: '25'}, {label: '通信状态', value: '良好'}, {
+            label: '电力供应',
+            value: '100%'
+          }, {label: '威胁评估', value: '低'}, {label: '防御系统', value: '激活'}]
+        },
+        {
+          id: 'unit13',
+          name: '仿爱国者-3导弹发射车',
+          type: 'target',
+          faction: 'blue',
+          status: '完好',
+          position: 'N39°54\'15", E117°26\'38"',
+          value: 'A级',
+          statusClass: 'active',
+          icon: 'el-icon-ship',
+          mapPosition: {top: '27%', left: '48%'},
+          params: [{label: '状态', value: '完好'}, {label: '高度', value: '地面'}, {
+            label: '防护等级',
+            value: 'A级'
+          }, {label: '人员数量', value: '25'}, {label: '通信状态', value: '良好'}, {
+            label: '电力供应',
+            value: '100%'
+          }, {label: '威胁评估', value: '低'}, {label: '防御系统', value: '激活'}]
+        },
+        {
+          id: 'unit14',
+          name: '机动式防御雷达车',
+          type: 'target',
+          faction: 'blue',
+          status: '完好',
+          position: 'N39°54\'15", E117°26\'38"',
+          value: 'A级',
+          statusClass: 'active',
+          icon: 'el-icon-location-information',
+          mapPosition: {top: '34%', left: '41%'},
+          params: [{label: '状态', value: '完好'}, {label: '高度', value: '地面'}, {
+            label: '防护等级',
+            value: 'A级'
+          }, {label: '人员数量', value: '25'}, {label: '通信状态', value: '良好'}, {
+            label: '电力供应',
+            value: '100%'
+          }, {label: '威胁评估', value: '低'}, {label: '防御系统', value: '激活'}]
+        },
+        {
+          id: 'unit15',
+          name: '仿海马斯火箭炮',
+          type: 'target',
+          faction: 'blue',
+          status: '完好',
+          position: 'N39°54\'15", E117°26\'38"',
+          value: 'A级',
+          statusClass: 'active',
+          icon: 'el-icon-chicken',
+          mapPosition: {top: '15%', left: '48%'},
+          params: [{label: '状态', value: '完好'}, {label: '高度', value: '地面'}, {
+            label: '防护等级',
+            value: 'A级'
+          }, {label: '人员数量', value: '25'}, {label: '通信状态', value: '良好'}, {
+            label: '电力供应',
+            value: '100%'
+          }, {label: '威胁评估', value: '低'}, {label: '防御系统', value: '激活'}]
+        },
+      ],
+      /* ===== 故障模拟:新增字段 ===== */
+      faultQuery: '',
+      dialogTitle:'新增故障模拟',
+      showFaultEdit:false,
+      faultForm: {
+        faultName: '',
+        executeTheScript: '', // 前端保存为字符串
+        equipmentId:'1',
+        schemeEquId:'1',
+      },
+      equTree:[],
+      rules: {
+        faultName: [
+          { required: true, message: '请输入故障名称', trigger: 'blur' }
+        ],
+        schemeEquId: [
+          { required: true, message: '请选中设备', trigger: 'change' }
+        ],
+        executeTheScript: [
+          { required: true, message: '请输入执行脚本', trigger: 'blur' },
+          {
+            validator: this.validateJson,
+            trigger: 'blur'
+          }
+        ]
+      },
+      showJsonEditor:false,
+      selectRow:null,
+      scriptJson:{},
+      faultScenarios: [],
+      activeFaultIds: new Set(),
+      _unitBackups: {},
+      faultOverlays: [],
+      quickFault: {name: '', targetUnitId: '', severity: 'warning'}
+    };
+  },
+  watch: {
+    missiles: {
+      deep: true,
+      handler(v) {
+        this.missilesLocal = this.clone(v);
+      }
+    },
+    openMissilePanels: {
+      immediate: true,
+      handler(v) {
+        this.openPanels = [...v];
+      }
+    },
+    scenarioVersionLocal(v) {
+      this.$emit('update:scenarioVersion', v);
+    },
+    scenarioVersion(v) {
+      this.scenarioVersionLocal = v;
+    }
+  },
+  computed:{
+    cascaderOptions() {
+      return this.equTree.map(group => ({
+        value: group.name,
+        label: group.name,
+        disabled: true,
+        children: group.equList?.map(item => ({
+          value: item.id,
+          label: item.name,
+          disabled: false
+        })) || []
+      }));
+    },
+    // 故障列表过滤
+    filteredFaults() {
+      const q = (this.faultQuery || '').trim().toLowerCase()
+      if (!q) return this.faultScenarios
+      return this.faultScenarios.filter(f =>
+        String(f.faultName).toLowerCase().includes(q) ||
+        String(f.schemeEquId).toLowerCase().includes(q)
+      )
+    }
+  },
+  mounted() {
+    this.fetchData()
+  },
+  methods: {
+    validateJson(rule, value, callback) {
+      if (!value) {
+        callback(); // 空值由 required 控制
+        return;
+      }
+
+      try {
+        JSON.parse(value);
+        callback(); // 合法 JSON
+      } catch (e) {
+        callback(new Error('执行脚本必须是有效的 JSON 格式'));
+      }
+    },
+     fetchData(){
+       // 导弹列表
+       missileList({taskId:this.taskId,subTaskId:this.subTaskId}).then((res)=>{
+        this.missilesLocal = res.data
+      })
+    //    故障列表
+       findPageWrapper({simulationId:this.planId,pageNo:1,pageSize:9999}).then((res)=>{
+         this.faultScenarios = res.data.records
+       })
+       // 获取设备信息
+       getEquTree({subTaskId:this.subTaskId}).then((res)=>{
+         this.equTree = res.data
+       })
+    },
+    clone(o) {
+      return JSON.parse(JSON.stringify(o || {}));
+    },
+    clearAllFaults() {
+      Array.from(this.activeFaultIds).forEach(fid => this.restoreFault(fid))
+      this.faultOverlays = []
+      this.$message.success('已清空所有故障模拟')
+    },
+    /* ===== 故障模拟:核心方法 ===== */
+    addFault(){
+
+      this.dialogTitle='新增故障模拟'
+      this.showFaultEdit = true
+    },
+    deleteFault(row){
+      this.$confirm(`确定要删除故障【${row.faultName}】吗?`, '删除确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 用户点击“确定”
+        faultsimulationDelete({id:row.id}).then(() => {
+          this.$message.success('删除成功');
+          findPageWrapper({simulationId:this.planId,pageNo:1,pageSize:9999}).then((res)=>{
+            this.faultScenarios = res.data.records
+          })
+        }).catch(() => {
+          this.$message.error('删除失败');
+        });
+      }).catch(() => {
+        // 用户点击“取消”或关闭弹窗
+        this.$message.info('已取消删除');
+      });
+    },
+    submitForm(){
+      this.$refs.faultForm.validate(valid => {
+        if (valid) {
+          // 提交到后端
+          faultsimulationInsert({simulationId:this.planId,...this.faultForm}).then(res => {
+            this.$message.success('提交成功');
+            findPageWrapper({simulationId:this.planId,pageNo:1,pageSize:9999}).then((res)=>{
+              this.faultScenarios = res.data.records
+            })
+            this.showFaultEdit = false
+          }).catch(err => {
+            this.$message.error('提交失败');
+          });
+        } else {
+          this.$message.warning('请完善表单信息');
+          return false;
+        }
+      });
+
+    },
+    jsonEdit(row){
+      this.selectRow = row
+      this.scriptJson = JSON.parse(row.executeTheScript)
+      this.showJsonEditor = true
+    },
+    updeteJson(){
+      if(!this.$refs.JsonEditor.isJsonString(this.$refs.JsonEditor.getValue())){
+        this.$message.warning("JSON格式出错!")
+        return
+      }
+      faultsimulationUpdate({id:this.selectRow.id,executeTheScript:this.$refs.JsonEditor.getValue()}).then((res)=>{
+        if(res.code === 0){
+          this.$message.success("修改成功")
+          findPageWrapper({simulationId:this.planId,pageNo:1,pageSize:9999}).then((res)=>{
+            this.faultScenarios = res.data.records
+          })
+          this.showJsonEditor = false
+        }else{
+          // this.$message.success("修改成功")
+        }
+      })
+    },
+    triggerFault(fault) {
+      const unit = this.units.find(u => u.id === fault.targetUnitId)
+      if (!unit) {
+        this.$message.error('未找到目标设备')
+        return
+      }
+      // 首次备份
+      if (!this._unitBackups[unit.id]) {
+        this._unitBackups[unit.id] = JSON.parse(JSON.stringify(unit))
+      }
+
+      // 地图联动
+      this.selectUnit(unit)
+
+      // 改视觉状态
+      if (fault.effect && fault.effect.statusClass) {
+        this.$set(unit, 'statusClass', fault.effect.statusClass)
+      }
+      if (!unit._faultTagged) {
+        unit._faultTagged = true
+        unit.name = unit.name.replace(/【故障】/g, '') + '【故障】'
+      }
+
+      // 覆盖层效果
+      const ovId = `${fault.id}-${Date.now()}`
+      this._addOverlayAtUnit(unit, (fault.effect && fault.effect.overlay) || 'ripple', ovId)
+
+      this.activeFaultIds.add(fault.id)
+      this.$message.warning(`${fault.name} 已触发:${fault.effect && fault.effect.note ? fault.effect.note : ''}`)
+    },
+
+    restoreFault(faultId) {
+      const fault = this.faultScenarios.find(f => f.id === faultId)
+      if (!fault) return
+      const unit = this.units.find(u => u.id === fault.targetUnitId)
+      if (!unit) return
+
+      const backup = this._unitBackups[unit.id]
+      if (backup) {
+        this.$set(unit, 'statusClass', backup.statusClass)
+        unit.name = backup.name
+        delete unit._faultTagged
+        delete this._unitBackups[unit.id]
+      }
+
+      this._removeOverlayByPrefix(`${fault.id}-`)
+      this.activeFaultIds.delete(faultId)
+      this.$message.success('已恢复:' + fault.name)
+    },
+    addQuickFault() {
+      const {name, targetUnitId, severity} = this.quickFault
+      if (!name || !targetUnitId) {
+        this.$message.info('请填写故障名并选择设备')
+        return
+      }
+      const unit = this.units.find(u => u.id === targetUnitId)
+      if (!unit) {
+        this.$message.error('设备不存在')
+        return
+      }
+      const id = 'F' + Math.random().toString(16).slice(2, 8).toUpperCase()
+      const fault = {
+        id,
+        name,
+        targetUnitId,
+        targetName: unit.name,
+        severity: severity || 'warning',
+        effect: {
+          statusClass: severity === 'critical' ? 'inactive' : 'standby',
+          overlay: severity === 'critical' ? 'cross' : 'ripple',
+          blink: true,
+          note: '自定义模拟'
+        }
+      }
+      this.faultScenarios.unshift(fault)
+      this.quickFault = {name: '', targetUnitId: '', severity: 'warning'}
+      this.$nextTick(() => this.triggerFault(fault))
+    }
+  }
+};
+</script>
+
+<style scoped>
+.mb-16 {
+  margin-bottom: 16px;
+}
+
+.fill-card {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.fill-card ::v-deep .el-card__body {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  padding: 12px;
+}
+
+.fill-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+}
+
+/* 折叠项头部与父页风格一致 */
+::v-deep .el-collapse-item__header {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
+  border: 1px solid rgba(255, 255, 255, .10);
+  border-bottom: none;
+  border-radius: 12px 12px 0 0;
+  color: var(--text);
+  font-weight: 700;
+  padding: 0 14px;
+  min-height: 44px;
+}
+
+::v-deep .el-collapse-item__arrow {
+  color: #9ED0FF;
+}
+
+::v-deep .el-collapse-item__wrap {
+  background: transparent;
+  border: 1px solid rgba(255,255,255,.10);
+  border-top: none;
+  border-radius: 0px 0px 12px 12px;
+  margin-top: -1px;
+}
+
+::v-deep .el-collapse {
+  border: none;
+}
+
+::v-deep .el-collapse-item {
+  margin-bottom: 12px;
+}
+
+/* ===== 故障模拟面板风格(与侧栏统一) ===== */
+.fault-panel .panel-content {
+  padding: 8px 10px;
+}
+
+.fault-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+}
+
+.fault-search {
+  width: 160px;
+}
+
+.fault-badge {
+  display: inline-block;
+  padding: 2px 6px;
+  border-radius: 6px;
+  font-size: 12px;
+  border: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+.fault-badge.sev-warning {
+  color: #f3e8c0;
+  background: rgba(184, 130, 48, .18);
+  border-color: rgba(184, 130, 48, .35);
+}
+
+.fault-badge.sev-critical {
+  color: #ffd1d1;
+  background: rgba(196, 86, 86, .18);
+  border-color: rgba(196, 86, 86, .35);
+}
+
+.fault-quick-add {
+  display: grid;
+  grid-template-columns:1.2fr 1.4fr .9fr auto;
+  gap: 8px;
+  margin-top: 10px;
+}
+</style>

+ 66 - 0
src/views/decision/testBuild/scenario/scheme/JammerSchemeInfo.vue

@@ -0,0 +1,66 @@
+<template>
+  <div class="scheme-panel">
+    <el-card body-style="padding:16px;" class="ui-card panel-card" shadow="never">
+      <div class="sub-title">
+        <i class="el-icon-document"></i> 干扰方案信息
+      </div>
+      <el-descriptions :column="2" border class="desc-table" size="small">
+        <el-descriptions-item label="方案名称">{{ scheme.name }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ scheme.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="方案状态">{{ scheme.status }}</el-descriptions-item>
+        <el-descriptions-item label="编制人员">{{ scheme.creator }}</el-descriptions-item>
+        <el-descriptions-item label="干扰布设算法">{{ scheme.algorithm }}</el-descriptions-item>
+        <el-descriptions-item label="干扰装备数量">{{ scheme.count }}</el-descriptions-item>
+        <el-descriptions-item label="装备类型">
+          {{ (scheme.types || []).join('、') || '—' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="干扰范围">{{ scheme.range }}</el-descriptions-item>
+        <el-descriptions-item :span="2" label="包含装备">
+          {{ (scheme.includes || []).join('、') || '—' }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'JammerSchemeInfo',
+  props: {
+    scheme: { type: Object, required: true }
+  }
+};
+</script>
+
+<style scoped>
+.scheme-panel{ width:100%; }
+.ui-card{
+  background: var(--card, #0D2346) !important;
+  border: 1px solid rgba(255,255,255,.08) !important;
+  border-radius: 16px !important;
+  box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 12px 24px rgba(5,24,54,.45) !important;
+}
+.panel-card{ background: var(--panel, #0B2143) !important; }
+
+.sub-title{
+  position: relative;
+  padding-left: 10px;
+  font-weight: 800;
+  color: #EAF2FF;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.sub-title:before{
+  content: "";
+  position: absolute;
+  left: 0; top: 2px; bottom: 2px; width: 3px;
+  background: linear-gradient(180deg, var(--accent, #66B1FF), rgba(102,177,255,.3));
+  border-radius: 2px;
+}
+
+.desc-table ::v-deep .el-descriptions__body{ background: rgba(255,255,255,.02); }
+.desc-table ::v-deep .el-descriptions__label{ color: #CFE2FF; }
+.desc-table ::v-deep .el-descriptions__content{ color: #EAF2FF; }
+</style>

+ 65 - 0
src/views/decision/testBuild/scenario/scheme/MeasurementSchemeInfo.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="scheme-panel">
+    <el-card body-style="padding:16px;" class="ui-card panel-card" shadow="never">
+      <div class="sub-title">
+        <i class="el-icon-document"></i> 测量方案信息
+      </div>
+      <el-descriptions :column="2" border class="desc-table" size="small">
+        <el-descriptions-item label="方案名称">{{ scheme.name }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ scheme.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="方案状态">{{ scheme.status }}</el-descriptions-item>
+        <el-descriptions-item label="编制人员">{{ scheme.creator }}</el-descriptions-item>
+        <el-descriptions-item label="测量布设算法">{{ scheme.algorithm }}</el-descriptions-item>
+        <el-descriptions-item label="测量装备数量">{{ scheme.count }}</el-descriptions-item>
+        <el-descriptions-item label="装备类型">
+          {{ (scheme.types || []).join('、') || '—' }}
+        </el-descriptions-item>
+        <el-descriptions-item :span="2" label="包含装备">
+          {{ (scheme.includes || []).join('、') || '—' }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MeasurementSchemeInfo',
+  props: {
+    scheme: { type: Object, required: true }
+  }
+};
+</script>
+
+<style scoped>
+.scheme-panel{ width:100%; }
+.ui-card{
+  background: var(--card, #0D2346) !important;
+  border: 1px solid rgba(255,255,255,.08) !important;
+  border-radius: 16px !important;
+  box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 12px 24px rgba(5,24,54,.45) !important;
+}
+.panel-card{ background: var(--panel, #0B2143) !important; }
+
+.sub-title{
+  position: relative;
+  padding-left: 10px;
+  font-weight: 800;
+  color: #EAF2FF;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.sub-title:before{
+  content: "";
+  position: absolute;
+  left: 0; top: 2px; bottom: 2px; width: 3px;
+  background: linear-gradient(180deg, var(--accent, #66B1FF), rgba(102,177,255,.3));
+  border-radius: 2px;
+}
+
+.desc-table ::v-deep .el-descriptions__body{ background: rgba(255,255,255,.02); }
+.desc-table ::v-deep .el-descriptions__label{ color: #CFE2FF; }
+.desc-table ::v-deep .el-descriptions__content{ color: #EAF2FF; }
+</style>

+ 66 - 0
src/views/decision/testBuild/scenario/scheme/TargetSchemeInfo.vue

@@ -0,0 +1,66 @@
+<template>
+  <div class="scheme-panel">
+    <el-card body-style="padding:16px;" class="ui-card panel-card" shadow="never">
+      <div class="sub-title">
+        <i class="el-icon-document"></i> 靶标布设方案信息
+      </div>
+      <el-descriptions :column="2" border class="desc-table" size="small">
+        <el-descriptions-item label="方案名称">{{ scheme.name }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ scheme.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="方案状态">{{ scheme.status }}</el-descriptions-item>
+        <el-descriptions-item label="编制人员">{{ scheme.creator }}</el-descriptions-item>
+        <el-descriptions-item label="靶标布设算法">{{ scheme.algorithm }}</el-descriptions-item>
+        <el-descriptions-item label="靶标装备数量">{{ scheme.count }}</el-descriptions-item>
+        <el-descriptions-item label="装备类型">
+          {{ (scheme.types || []).join('、') || '—' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="布设密度">{{ scheme.density }}</el-descriptions-item>
+        <el-descriptions-item label="覆盖面积">{{ scheme.coveragePct }}%</el-descriptions-item>
+      </el-descriptions>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TargetSchemeInfo',
+  props: {
+    scheme: { type: Object, required: true }
+  }
+};
+</script>
+
+<style scoped>
+/* 独立样式:不依赖父页,使用 CSS 变量时均提供兜底色 */
+.scheme-panel{ width:100%; }
+.ui-card{
+  background: var(--card, #0D2346) !important;
+  border: 1px solid rgba(255,255,255,.08) !important;
+  border-radius: 16px !important;
+  box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 12px 24px rgba(5,24,54,.45) !important;
+}
+.panel-card{ background: var(--panel, #0B2143) !important; }
+
+.sub-title{
+  position: relative;
+  padding-left: 10px;
+  font-weight: 800;
+  color: #EAF2FF;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+.sub-title:before{
+  content: "";
+  position: absolute;
+  left: 0; top: 2px; bottom: 2px; width: 3px;
+  background: linear-gradient(180deg, var(--accent, #66B1FF), rgba(102,177,255,.3));
+  border-radius: 2px;
+}
+
+/* ElementUI Descriptions 局部皮肤 */
+.desc-table ::v-deep .el-descriptions__body{ background: rgba(255,255,255,.02); }
+.desc-table ::v-deep .el-descriptions__label{ color: #CFE2FF; }
+.desc-table ::v-deep .el-descriptions__content{ color: #EAF2FF; }
+</style>

+ 2123 - 0
src/views/decision/testBuild/scenarioEdit.vue

@@ -0,0 +1,2123 @@
+<template>
+  <div class="page">
+    <!-- 选择靶区弹窗 -->
+    <el-dialog
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :visible.sync="rangeDialogVisible"
+      center
+      title="选择靶区场景"
+      width="560px"
+    >
+      <el-form :model="rangeForm" label-width="100px">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="靶区场景" prop="rangeId">
+              <el-select v-model="rangeForm.rangeId" placeholder="请选择靶区">
+                <el-option
+                  v-for="r in rangeForm.ranges"
+                  :key="r.id"
+                  :label="r.name"
+                  :value="r.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="编辑方式" prop="rangeId">
+              <el-select v-model="rangeForm.editType" placeholder="请选择编辑方式">
+                <el-option label="手动编辑" value="手动编辑"/>
+                <el-option label="导入XML文件" value="导入XML文件"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 预览所选靶区 -->
+        <!--        <el-card-->
+        <!--          v-if="currentRangeInfo && rangeForm.editType !== '导入XML文件'"-->
+        <!--          body-style="padding:12px;"-->
+        <!--          class="ui-card panel-card"-->
+        <!--          shadow="never"-->
+        <!--        >-->
+        <!--          <div class="sub-title">-->
+        <!--            <i class="el-icon-map-location"></i> 场景详情(预览)-->
+        <!--          </div>-->
+        <!--          <el-form-->
+        <!--            :model="currentRangeInfo"-->
+        <!--            class="range-preview-form"-->
+        <!--            label-width="90px"-->
+        <!--            size="small"-->
+        <!--          >-->
+        <!--            <el-row :gutter="12">-->
+        <!--              <el-col :span="12">-->
+        <!--                <el-form-item label="名称">-->
+        <!--                  <el-input v-model="currentRangeInfo.name" disabled/>-->
+        <!--                </el-form-item>-->
+        <!--              </el-col>-->
+        <!--              <el-col v-if="currentRangeInfo.area" :span="12">-->
+        <!--                <el-form-item label="面积">-->
+        <!--                  <el-input v-model="currentRangeInfo.area" disabled/>-->
+        <!--                </el-form-item>-->
+        <!--              </el-col>-->
+        <!--              <el-col v-if="currentRangeInfo.description" :span="24">-->
+        <!--                <el-form-item label="描述">-->
+        <!--                  <el-input-->
+        <!--                    v-model="currentRangeInfo.description"-->
+        <!--                    :rows="3"-->
+        <!--                    disabled-->
+        <!--                    type="textarea"-->
+        <!--                  />-->
+        <!--                </el-form-item>-->
+        <!--              </el-col>-->
+        <!--            </el-row>-->
+        <!--            <div class="form-hint">-->
+        <!--              以上为所选场景其他配置信息,点击「进入配置」应用到该想定。-->
+        <!--            </div>-->
+        <!--          </el-form>-->
+        <!--        </el-card>-->
+
+        <!-- 导入 XML -->
+        <el-form-item
+          v-if="rangeForm.editType === '导入XML文件'"
+          label="导入XML文件"
+        >
+          <el-upload
+            :auto-upload="false"
+            :before-upload="beforeUpload"
+            :file-list="fileList"
+            :limit="1"
+            :on-change="handleFileChange"
+            :on-remove="handleRemove"
+            accept=".xml"
+            action=""
+            class="upload-demo"
+          >
+            <el-button icon="el-icon-upload" type="primary">选择 XML 文件</el-button>
+            <div slot="tip" class="el-upload__tip">
+              仅支持 .xml,点击「进入配置」后将解析导入。
+            </div>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+
+      <span slot="footer">
+        <el-button plain @click="goBack">取消</el-button>
+        <el-button :disabled="!rangeForm.rangeId" type="primary" @click="confirmRange">进入配置</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 主体:选择场景后再渲染 -->
+    <template v-if="showMain">
+      <el-container class="root" direction="vertical">
+        <!-- 顶部 -->
+        <el-header class="header">
+          <div class="header-inner">
+            <el-steps
+              :active="step"
+              :space="260"
+              align-center
+              class="steps-light"
+              finish-status="success"
+              simple
+            >
+              <el-step v-for="(s,i) in stepMeta" :key="i" :icon="s.icon" :title="s.title"/>
+            </el-steps>
+            <div class="header-actions">
+              <el-button icon="el-icon-back" type="danger" @click="goBack">返回</el-button>
+            </div>
+            <div class="step-index">
+              步骤 {{ step }} / {{ stepMeta.length }}
+              <span class="version-badge">想定版本:{{ scenarioVersion }}</span>
+            </div>
+            <div class="header-actions">
+              <el-button icon="el-icon-download" plain @click="exportPlan">导出</el-button>
+            </div>
+          </div>
+        </el-header>
+
+        <!-- 工作区 -->
+        <el-container class="workspace">
+          <!-- Step 1 装备配置 -->
+          <template v-if="step===1">
+            <!-- 左侧 -->
+            <el-aside :width="leftWidth" class="aside aside-left">
+              <el-card class="ui-card fill-card" shadow="hover">
+                <div slot="header" class="card-header">
+                  <div class="title-left">
+                    <i class="fa fa-cogs icon-primary"></i><span>装备配置</span>
+                  </div>
+                  <div class="title-right">
+                    <el-select
+                      v-model="activeTab"
+                      :popper-append-to-body="false"
+                      class="switcher"
+                      size="mini"
+                    >
+                      <el-option
+                        v-for="t in equipmentTabs"
+                        :key="t.key"
+                        :label="t.label"
+                        :value="t.key"
+                      />
+                    </el-select>
+
+                    <el-button
+                      v-if="activeTab==='target'"
+                      icon="el-icon-plus"
+                      size="mini"
+                      type="primary"
+                      @click="openAddTargetDialog"
+                    >添加靶标
+                    </el-button>
+
+                    <el-button
+                      v-if="activeTab==='jammer'"
+                      icon="el-icon-plus"
+                      size="mini"
+                      type="primary"
+                      @click="openAddJammerDialog"
+                    >添加干扰
+                    </el-button>
+
+                    <el-button
+                      v-if="activeTab==='measurement'"
+                      icon="el-icon-plus"
+                      size="mini"
+                      type="primary"
+                      @click="openAddMeasurementDialog"
+                    >添加测量
+                    </el-button>
+                  </div>
+                </div>
+
+                <!-- 列表 + 时序 合体(交由子组件渲染) -->
+                <EquipmentListWithTimeline
+                  :type-key="activeTab"
+                  :items="activeTab==='target' ? targets : (activeTab==='jammer' ? jammers : measurements)"
+                  @edit="(i) => activeTab==='target' ? editTarget(i) : activeTab==='jammer' ? editJammer(i) : editMeasurement(i)"
+                  @remove="(i) => activeTab==='target' ? removeTarget(i) : activeTab==='jammer' ? removeJammer(i) : removeMeasurement(i)"
+                  @refresh="rebuildPreview"
+                />
+              </el-card>
+            </el-aside>
+
+            <!-- 右侧 -->
+            <el-aside :width="rightWidth" class="aside aside-right">
+              <el-card class="ui-card fill-card" shadow="hover">
+                <div slot="header" class="card-header">
+                  <div class="title-left">
+                    <i class="fa fa-file-text-o icon-primary"></i>
+                    <span>{{ getPreviewTitle() }}</span>
+                  </div>
+                </div>
+
+                <div class="preview-scroll fill-scroll">
+                  <!-- KPI -->
+                  <div class="kpi-grid">
+                    <div class="kpi-card">
+                      <div class="kpi-value">{{ stat.total }}</div>
+                      <div class="kpi-label">装备总数</div>
+                    </div>
+                    <div class="kpi-card">
+                      <div class="kpi-value">{{ stat.target }}</div>
+                      <div class="kpi-label">靶标装备</div>
+                    </div>
+                    <div class="kpi-card">
+                      <div class="kpi-value">{{ stat.jammer }}</div>
+                      <div class="kpi-label">干扰装备</div>
+                    </div>
+                    <div class="kpi-card">
+                      <div class="kpi-value">{{ stat.measure }}</div>
+                      <div class="kpi-label">测量装备</div>
+                    </div>
+                  </div>
+
+                  <template v-if="activeTab==='target'">
+                    <TargetSchemeInfo :scheme="targetSchemeView" class="mb-16"/>
+                  </template>
+
+                  <template v-if="activeTab==='jammer'">
+                    <JammerSchemeInfo :scheme="jammerSchemeView" class="mb-16"/>
+                  </template>
+
+                  <template v-if="activeTab==='measurement'">
+                    <MeasurementSchemeInfo :scheme="measureSchemeView" class="mb-16"/>
+                  </template>
+                </div>
+              </el-card>
+            </el-aside>
+          </template>
+
+          <!-- Step 2 任务规划(整块交由面板组件) -->
+          <template v-if="step===2">
+            <el-main class="main-full">
+              <MissileMissionPanel
+                :planId="plan.id"
+                :taskId="plan.taskId"
+                :subTaskId="plan.subTaskId"
+                :missiles.sync="missiles"
+                :open-missile-panels.sync="openMissilePanels"
+                :scenario-name="scenarioName"
+                :scenario-version.sync="scenarioVersion"
+                :missile-count="missionMissileCount"
+                @import-trajectory="handleTrajectory"
+                @preview-trajectory="handleTrajectory"
+              />
+            </el-main>
+          </template>
+
+          <!-- Step 3 环境设置(保留) -->
+          <template v-if="step===3">
+            <el-main class="main-full">
+              <el-card class="ui-card fill-card" shadow="hover">
+                <div slot="header" class="card-header">
+                  <div class="title-left">
+                    <i class="fa fa-cloud icon-primary"></i><span>战场环境规划</span>
+                  </div>
+                </div>
+                <div class="fill-scroll">
+                  <el-collapse v-model="activeEnvTab">
+                    <el-collapse-item name="meteorology" title="气象条件">
+                      <el-form :model="weatherForm" class="dense-form" label-width="120px" size="small" style="margin-top: 20px">
+                        <el-row :gutter="16">
+                          <el-col :span="12">
+                            <el-form-item label="风速(m/s)">
+                              <el-input v-model.number="weatherForm.wind_speed" :min="0" step="0.1" type="number"/>
+                            </el-form-item>
+                          </el-col>
+                          <el-col :span="12">
+                            <el-form-item label="风向(°)">
+                              <el-input v-model.number="weatherForm.wind_direction" :max="360" :min="0" step="1" type="number"/>
+                            </el-form-item>
+                          </el-col>
+                          <el-col :span="12">
+                            <el-form-item label="温度(°C)">
+                              <el-input v-model.number="weatherForm.temperature" step="0.5" type="number"/>
+                            </el-form-item>
+                          </el-col>
+                          <el-col :span="12">
+                            <el-form-item label="能见度(km)">
+                              <el-input v-model.number="weatherForm.visibility" :min="0.1" step="0.5" type="number"/>
+                            </el-form-item>
+                          </el-col>
+                        </el-row>
+                      </el-form>
+                    </el-collapse-item>
+                  </el-collapse>
+
+                </div>
+              </el-card>
+            </el-main>
+          </template>
+
+          <!-- Step 4 想定预览 -->
+          <template v-if="step===4">
+            <el-main class="main-full">
+              <el-card class="ui-card fill-card" shadow="hover">
+                <div slot="header" class="card-header">
+                  <div class="title-left">
+                    <i class="fa fa-file-text icon-primary"></i><span>想定预览(版本:{{ scenarioVersion }})</span>
+                  </div>
+                </div>
+
+                <div class="fill-scroll">
+                  <!-- 装备数量 -->
+                  <el-card body-style="padding:16px;" class="ui-card panel-card mb-16" shadow="never">
+                    <div class="sub-title"><i class="el-icon-odometer"></i> 装备数量预览</div>
+                    <div class="kpi-grid">
+                      <div class="kpi-card">
+                        <div class="kpi-value">{{ stat.total }}</div>
+                        <div class="kpi-label">装备总数</div>
+                      </div>
+                      <div class="kpi-card">
+                        <div class="kpi-value">{{ stat.target }}</div>
+                        <div class="kpi-label">靶标装备</div>
+                      </div>
+                      <div class="kpi-card">
+                        <div class="kpi-value">{{ stat.jammer }}</div>
+                        <div class="kpi-label">干扰装备</div>
+                      </div>
+                      <div class="kpi-card">
+                        <div class="kpi-value">{{ stat.measure }}</div>
+                        <div class="kpi-label">测量装备</div>
+                      </div>
+                    </div>
+                  </el-card>
+
+                  <!-- 装备信息总览(三类) -->
+                  <el-card body-style="padding:16px;" class="ui-card panel-card mb-16" shadow="never">
+                    <div class="sub-title"><i class="el-icon-tickets"></i> 装备信息总览</div>
+                    <el-row :gutter="12">
+                      <el-col :span="8" v-if="equTree.find(item=>item.name==='靶标方案')">
+                        <el-card body-style="padding:10px;" class="ui-card soft" shadow="never">
+                          <div class="module-title"><i class="el-icon-location-information"></i>
+                            靶标装备({{ equTree.find(item=>item.name==='靶标方案').equList.length }})
+                          </div>
+                          <el-table :data="equTree.find(item=>item.name==='靶标方案').equList" border height="220" size="mini">
+                            <el-table-column label="名称" min-width="120" prop="name"/>
+                            <el-table-column label="类型" prop="type" width="70"/>
+                            <el-table-column label="状态" width="90">
+                              <template slot-scope="scope">
+                                <el-tag
+                                  :type="scope.row.statusClass==='badge-success'?'success':(scope.row.statusClass==='badge-warn'?'warning':'info')"
+                                  size="mini">
+                                  {{ scope.row.statusText }}
+                                </el-tag>
+                              </template>
+                            </el-table-column>
+                            <el-table-column label="激活" prop="time" width="90"/>
+                          </el-table>
+                        </el-card>
+                      </el-col>
+
+                      <el-col :span="8" v-if="equTree.find(item=>item.name==='干扰方案')">
+                        <el-card body-style="padding:10px;" class="ui-card soft" shadow="never">
+                          <div class="module-title"><i class="el-icon-microphone"></i> 干扰装备({{ equTree.find(item=>item.name==='干扰方案').equList.length }})
+                          </div>
+                          <el-table :data="equTree.find(item=>item.name==='干扰方案').equList" border height="220" size="mini">
+                            <el-table-column label="名称" min-width="120" prop="name"/>
+                            <el-table-column label="类型" prop="type" width="70"/>
+                            <el-table-column label="频率(MHz)" width="100">
+                              <template slot-scope="scope">{{ scope.row.params && scope.row.params.freq }}</template>
+                            </el-table-column>
+                          </el-table>
+                        </el-card>
+                      </el-col>
+
+                      <el-col :span="8" v-if="equTree.find(item=>item.name==='测量方案')">
+                        <el-card body-style="padding:10px;" class="ui-card soft" shadow="never">
+                          <div class="module-title"><i class="el-icon-view"></i> 测量装备({{ equTree.find(item=>item.name==='测量方案').equList.length }})
+                          </div>
+                          <el-table :data="equTree.find(item=>item.name==='测量方案').equList" border height="220" size="mini">
+                            <el-table-column label="名称" min-width="120" prop="name"/>
+                            <el-table-column label="类型" prop="type" width="70"/>
+                            <el-table-column label="启动" prop="time" width="90"/>
+                            <el-table-column label="范围(km)" width="90">
+                              <template slot-scope="scope">{{ scope.row.params && scope.row.params.range }}</template>
+                            </el-table-column>
+                          </el-table>
+                        </el-card>
+                      </el-col>
+                    </el-row>
+                  </el-card>
+
+                  <!-- 完整事件时序(仍使用父级聚合) -->
+                  <el-card body-style="padding:16px;" class="ui-card panel-card" shadow="never">
+                    <div class="module-title module-title-row">
+                      <div><i class="el-icon-time"></i> 完整事件时序</div>
+                    </div>
+                    <div class="timeline-wrap fill-scroll">
+                      <GanttChart :timelineData="filteredAllEvents" :default-duration="600"></GanttChart>
+                    </div>
+                  </el-card>
+                </div>
+              </el-card>
+            </el-main>
+          </template>
+        </el-container>
+
+        <!-- 底部操作 -->
+        <div class="footer-actions">
+          <div class="actions-inner">
+            <el-button :disabled="step<=1" @click="goPrev"><i class="el-icon-arrow-left el-icon--left"></i> 上一步
+            </el-button>
+            <div style="flex:1;"></div>
+            <el-button v-if="step<4" type="primary" @click="goNext">下一步 <i
+              class="el-icon-arrow-right el-icon--right"></i></el-button>
+            <el-button v-else icon="el-icon-check" type="success" @click="saveFinal">保存想定</el-button>
+          </div>
+        </div>
+      </el-container>
+    </template>
+    <!-- 新增/编辑:靶标 -->
+    <el-dialog :close-on-click-modal="false" :title="editModeTarget ? '编辑靶标' : '添加靶标'"
+               :visible.sync="addTargetDialog" width="900px">
+      <el-form ref="targetFormRef" :model="newTargetForm" :rules="targetRules" class="dense-form" label-width="110px"
+               size="small">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="靶标名称" prop="name">
+              <el-input v-model="newTargetForm.name" placeholder="请输入"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="靶标类型" prop="type">
+              <el-select v-model="newTargetForm.type" style="width:100%" @change="onTargetTypeChange">
+                <el-option label="静态" value="静态"/>
+                <el-option label="动态" value="动态"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <template v-if="newTargetForm.type==='静态'">
+            <el-col :span="12">
+              <el-form-item label="固定点位来源">
+                <el-switch v-model="newTargetForm.fixedFromPlan" active-text="使用方案固定点位"
+                           inactive-text="手动/地图选点"/>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="状态" prop="statusText">
+                <el-select v-model="newTargetForm.statusText" style="width:100%">
+                  <el-option label="可用" value="可用"/>
+                  <el-option label="维护中" value="维护中"/>
+                  <el-option label="停用" value="停用"/>
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </template>
+
+          <el-col :span="12">
+            <el-form-item :prop="newTargetForm.fixedFromPlan ? '' : 'lon'" label="经度">
+              <el-input v-model="newTargetForm.lon" placeholder="116.320000"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :prop="newTargetForm.fixedFromPlan ? '' : 'lat'" label="纬度">
+              <el-input v-model="newTargetForm.lat" placeholder="39.950000"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="激活时间" prop="time">
+              <el-time-picker v-model="newTargetForm.time" format="HH:mm:ss" placeholder="选择时间"
+                              style="width:100%;" value-format="HH:mm:ss"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12"></el-col>
+
+          <template v-if="newTargetForm.type==='动态'">
+            <el-col :span="12">
+              <el-form-item label="运动模式">
+                <el-select v-model="targetForm.motion">
+                  <el-option label="直线" value="linear"/>
+                  <el-option label="圆周" value="circular"/>
+                  <el-option label="随机" value="random"/>
+                  <el-option label="自定义" value="custom"/>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="速度(km/h)">
+                <el-input v-model.number="targetForm.speed" :min="0" step="0.1" type="number"/>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="持续时间(s)">
+                <el-input v-model.number="targetForm.duration" :min="1" type="number"/>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="工作模式">
+                <el-select v-model="targetForm.mode">
+                  <el-option label="正常" value="normal"/>
+                  <el-option label="模拟" value="simulated"/>
+                  <el-option label="增强" value="enhanced"/>
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </template>
+
+          <el-col v-if="!newTargetForm.fixedFromPlan" :span="24">
+            <!-- 地图选点 -->
+            <el-form-item label="地图选点">
+              <div class="map-container">
+                <div class="map-placeholder" @click.stop="selectMapPosition">
+                  <img alt="map" class="map-image" src="/img/banner.jpg"/>
+                  <div
+                    v-if="mapMarkerPosition"
+                    class="map-marker"
+                    :style="{left: mapMarkerPosition.x + 'px', top: mapMarkerPosition.y + 'px'}"
+                  >
+                    <i class="el-icon-location icon-marker"></i>
+                  </div>
+                </div>
+                <p class="map-hint">点击地图可快速带出经纬度</p>
+              </div>
+            </el-form-item>
+
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="addTargetDialog=false">取消</el-button>
+        <el-button type="primary" @click="confirmAddTarget">{{ editModeTarget ? '保存' : '确认添加' }}</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增/编辑:干扰 -->
+    <el-dialog :close-on-click-modal="false" :title="editModeJammer ? '编辑干扰装备' : '添加干扰装备'"
+               :visible.sync="addJammerDialog" size="40%">
+      <el-form ref="jammerFormRef" :model="newJammerForm" :rules="jammerRules" class="dense-form" label-width="110px"
+               size="small">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="装备名称" prop="name">
+              <el-input v-model="newJammerForm.name"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="装备类型" prop="type">
+              <el-select v-model="newJammerForm.type" style="width:100%">
+                <el-option label="电磁" value="电磁"/>
+                <el-option label="雷达" value="雷达"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="经度" prop="lon">
+              <el-input v-model="newJammerForm.lon"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="纬度" prop="lat">
+              <el-input v-model="newJammerForm.lat"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="干扰频率(MHz)">
+              <el-input v-model.number="jammerForm.freq" type="number"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="干扰功率(W)">
+              <el-input v-model.number="jammerForm.power" type="number"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="持续时间(s)">
+              <el-input v-model.number="jammerForm.duration" :min="1" type="number"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <div class="subsection-header">
+              <span>激活计划</span>
+              <el-button icon="el-icon-plus" size="mini" type="primary" @click="addJammerSchedule">新增计划</el-button>
+            </div>
+            <el-table :data="newJammerForm.schedules" border size="small">
+              <el-table-column label="激活时间" width="140">
+                <template slot-scope="scope">
+                  <el-time-picker v-model="scope.row.time" format="HH:mm:ss" placeholder="HH:mm:ss" size="mini"
+                                  value-format="HH:mm:ss"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="触发类型" width="150">
+                <template slot-scope="scope">
+                  <el-select v-model="scope.row.triggerType" size="mini" style="width:140px;">
+                    <el-option label="导弹接近" value="导弹接近"/>
+                    <el-option label="雷达信号异常" value="雷达信号异常"/>
+                    <el-option label="目标激活" value="目标激活"/>
+                    <el-option label="手动触发" value="手动触发"/>
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="阈值参数" width="140">
+                <template slot-scope="scope">
+                  <el-input v-model="scope.row.threshold" placeholder="如:距离<5km" size="mini"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="触发动作" width="160">
+                <template slot-scope="scope">
+                  <el-select v-model="scope.row.action" size="mini" style="width:150px;">
+                    <el-option label="立即启动干扰" value="立即启动干扰"/>
+                    <el-option label="进入准备状态" value="进入准备状态"/>
+                    <el-option label="发出警报" value="发出警报"/>
+                    <el-option label="执行预设序列" value="执行预设序列"/>
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="说明">
+                <template slot-scope="scope">
+                  <el-input v-model="scope.row.triggerDesc" placeholder="补充说明(可选)" size="mini"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="延迟(s)" width="100">
+                <template slot-scope="scope">
+                  <el-input v-model.number="scope.row.delaySec" :min="0" size="mini" type="number"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="90">
+                <template slot-scope="scope">
+                  <el-button class="danger-btn" size="mini" type="text" @click="removeJammerSchedule(scope.$index)">
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="addJammerDialog=false">取消</el-button>
+        <el-button type="primary" @click="confirmAddJammer">{{ editModeJammer ? '保存' : '确认添加' }}</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增/编辑:测量 -->
+    <el-dialog :close-on-click-modal="false" :title="editModeMeasurement ? '编辑测量装备' : '添加测量装备'"
+               :visible.sync="addMeasurementDialog" size="40%">
+      <el-form ref="measureFormRef" :model="newMeasurementForm" :rules="measureRules" class="dense-form"
+               label-width="110px"
+               size="small">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="装备名称" prop="name">
+              <el-input v-model="newMeasurementForm.name"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="装备类型" prop="type">
+              <el-select v-model="newMeasurementForm.type" style="width:100%">
+                <el-option label="雷达" value="雷达"/>
+                <el-option label="光学" value="光学"/>
+              </el-select>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="经度" prop="lon">
+              <el-input v-model="newMeasurementForm.lon"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="纬度" prop="lat">
+              <el-input v-model="newMeasurementForm.lat"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12">
+            <el-form-item label="启动时间" prop="time">
+              <el-time-picker v-model="newMeasurementForm.time" format="HH:mm:ss" placeholder="选择时间"
+                              style="width:100%;" value-format="HH:mm:ss"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12"></el-col>
+
+          <el-col :span="12">
+            <el-form-item label="测量范围(km)">
+              <el-input v-model.number="measureForm.range" type="number"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="测量精度(m)">
+              <el-input v-model.number="measureForm.precision" :min="0.1" step="0.1" type="number"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="俯仰角(°)">
+              <el-input v-model.number="measureForm.deg" type="number"/>
+            </el-form-item>
+          </el-col>
+          <el-col :spn="12">
+            <el-form-item label="离地高度(m)">
+              <el-input v-model.number="measureForm.height" type="number"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <el-form-item label="相对位置备注">
+              <el-input v-model="newMeasurementForm.posNote" placeholder="如:指挥所东南角 200m 高度20m"/>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24">
+            <div class="subsection-header">
+              <span>激活计划</span>
+              <el-button icon="el-icon-plus" size="mini" type="primary" @click="addMeasurementSchedule">新增计划
+              </el-button>
+            </div>
+            <el-table :data="newMeasurementForm.schedules" border size="small">
+              <el-table-column label="激活时间" width="140">
+                <template slot-scope="scope">
+                  <el-time-picker v-model="scope.row.time" format="HH:mm:ss" placeholder="HH:mm:ss" size="mini"
+                                  value-format="HH:mm:ss"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="触发类型" width="150">
+                <template slot-scope="scope">
+                  <el-select v-model="scope.row.triggerType" size="mini" style="width:140px;">
+                    <el-option label="导弹接近" value="导弹接近"/>
+                    <el-option label="雷达信号异常" value="雷达信号异常"/>
+                    <el-option label="目标激活" value="目标激活"/>
+                    <el-option label="手动触发" value="手动触发"/>
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="阈值参数" width="140">
+                <template slot-scope="scope">
+                  <el-input v-model="scope.row.threshold" placeholder="如:距离<5km" size="mini"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="触发动作" width="160">
+                <template slot-scope="scope">
+                  <el-select v-model="scope.row.action" size="mini" style="width:150px;">
+                    <el-option label="立即启动干扰" value="立即启动干扰"/>
+                    <el-option label="进入准备状态" value="进入准备状态"/>
+                    <el-option label="发出警报" value="发出警报"/>
+                    <el-option label="执行预设序列" value="执行预设序列"/>
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="说明">
+                <template slot-scope="scope">
+                  <el-input v-model="scope.row.triggerDesc" placeholder="补充说明(可选)" size="mini"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="延迟(s)" width="100">
+                <template slot-scope="scope">
+                  <el-input v-model.number="scope.row.delaySec" :min="0" size="mini" type="number"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="90">
+                <template slot-scope="scope">
+                  <el-button class="danger-btn" size="mini" type="text"
+                             @click="removeMeasurementSchedule(scope.$index)">删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="addMeasurementDialog=false">取消</el-button>
+        <el-button type="primary" @click="confirmAddMeasurement">{{
+            editModeMeasurement ? '保存' : '确认添加'
+          }}
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <dd-upload ref="uploadDd" />
+  </div>
+</template>
+
+<script>
+import TargetSchemeInfo from './scenario/scheme/TargetSchemeInfo.vue';
+import JammerSchemeInfo from './scenario/scheme/JammerSchemeInfo.vue';
+import MeasurementSchemeInfo from './scenario/scheme/MeasurementSchemeInfo.vue';
+import EquipmentListWithTimeline from './scenario/equipment/EquipmentListWithTimeline.vue';
+import MissileMissionPanel from './scenario/mission/MissileMissionPanel.vue';
+import GanttChart from './scenario/GanttChart.vue';
+import MissileMissionCard from "@/views/decision/testBuild/scenario/mission/MissileMissionCard.vue";
+import {battlefieldEnvironmentInsert, getBattlefieldEnvironment} from "@/api/battlefieldEnvironment";
+import {getEquTree} from "@/api/faultSimulation";
+import DdUpload from "@/views/decision/testBuild/scenario/dd-upload.vue";
+
+export default {
+  components: {
+    DdUpload,
+    MissileMissionCard,
+    TargetSchemeInfo,
+    JammerSchemeInfo,
+    MeasurementSchemeInfo,
+    EquipmentListWithTimeline,
+    MissileMissionPanel,
+    GanttChart
+  },
+  data() {
+    return {
+      //路由参数
+      plan: JSON.parse(this.$route.query.plan),
+      saveTimer: null,
+      saveLoad: false,
+      fileList: [],
+      xmlImport: {rawFile: null},
+      rangeDialogVisible: true,
+      showMain: false,
+      rangeForm: {
+        rangeId: 'A',
+        editType: '手动编辑',
+        ranges: [
+          {
+            id: 'A',
+            name: 'A靶区',
+            description: '主要用于基础训练',
+            area: '15.5 km²',
+            status: 'active',
+            capacity: '50 units'
+          },
+          {
+            id: 'B',
+            name: 'B靶区',
+            description: '主要用于综合训练',
+            area: '30.5 km²',
+            status: 'maintenance',
+            capacity: '50 units'
+          },
+          {id: 'C', name: 'C靶区'},
+          {id: 'all', name: '综合靶区'}
+        ]
+      },
+      selectedRange: '',
+      scenarioName: 'XXXXxx',
+      scenarioVersion: 'v1.0.0',
+      missionType: '对陆目标打击',
+      createTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
+      missionMissileCount: 3,
+
+      // 方案基座(用于右侧方案视图)
+      targetPlanBase: {
+        name: '靶标布设方案-01', creator: '张三', status: '已编制',
+        algorithm: '随机布设', density: '中等', coveragePct: 72,
+        createdAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
+        points: [
+          {name: '静态靶标-1', type: '固定式靶标', lon: '116.320000', lat: '39.950000', time: '00:10:00'},
+          {name: '动态靶标-1', type: '机动式靶标', lon: '116.450000', lat: '40.120000', time: '00:12:30'}
+        ]
+      },
+      jammerPlanBase: {
+        name: '干扰布设方案-01', creator: '李四', status: '已编制',
+        algorithm: '组合干扰', range: '5 km 半径',
+        includesPreset: ['箔条', '发烟罐', '电磁干扰-A'],
+        createdAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
+        points: [
+          {
+            name: '电磁干扰器-A',
+            type: '组合式干扰装备',
+            lon: '116.350000',
+            lat: '39.980000',
+            freq: 350,
+            power: 500,
+            duration: 180,
+            scheduleTime: '00:10:55'
+          },
+          {
+            name: '雷达干扰器-B',
+            type: '组合式干扰装备',
+            lon: '116.420000',
+            lat: '40.050000',
+            freq: 420,
+            power: 600,
+            duration: 160,
+            scheduleTime: '00:13:20'
+          }
+        ]
+      },
+      measurePlanBase: {
+        name: '测量布设方案-01', creator: '王五', status: '已编制',
+        algorithm: '多传感器融合布设算法示',
+        includesPreset: ['高速相机-Alpha', '红外相机-Beta', '常速相机-Gamma'],
+        createdAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
+        points: [
+          {
+            name: '雷达测量站-1',
+            type: '雷达',
+            posNote: '指挥所东南角 200m 高度20m',
+            height: 20,
+            lon: '116.300000',
+            lat: '39.900000',
+            time: '00:09:30',
+            range: 50,
+            precision: 0.5,
+            freq: 100
+          },
+          {
+            name: '光学测量仪-2',
+            type: '光学',
+            posNote: '指挥所东南角 300m 高度15m',
+            height: 15,
+            lon: '116.500000',
+            lat: '40.180000',
+            time: '00:11:15',
+            range: 50,
+            precision: 0.5,
+            freq: 100
+          }
+        ]
+      },
+
+      step: 1,
+      stepMeta: [
+        {title: '装备配置', icon: 'el-icon-s-tools'},
+        {title: '试验任务规划', icon: 'el-icon-s-flag'},
+        {title: '战场环境规划', icon: 'el-icon-cloudy'},
+        {title: '想定预览', icon: 'el-icon-document'}
+      ],
+      equipmentTabs: [
+        {key: 'target', label: '靶标装备'},
+        {key: 'jammer', label: '干扰装备'},
+        {key: 'measurement', label: '测量装备'}
+      ],
+      activeTab: 'target',
+
+      targets: [
+        {
+          id: 'T-1',
+          name: '静态靶标-1',
+          type: '静态',
+          fixedFromPlan: true,
+          lon: '116.320000',
+          lat: '39.950000',
+          time: '00:10:00',
+          statusText: '可用',
+          statusClass: 'badge-success',
+          params: {mode: 'normal', duration: 300, motion: 'fixed', speed: 0}
+        },
+        {
+          id: 'T-2',
+          name: '动态靶标-1',
+          type: '动态',
+          fixedFromPlan: false,
+          lon: '116.450000',
+          lat: '40.120000',
+          time: '00:12:30',
+          statusText: '可用',
+          statusClass: 'badge-success',
+          params: {mode: 'normal', duration: 240, motion: 'linear', speed: 30}
+        }
+      ],
+      jammers: [
+        {
+          id: 'J-1',
+          name: '电磁干扰器-A',
+          type: '电磁',
+          lon: '116.350000',
+          lat: '39.980000',
+          params: {freq: 350, power: 500, duration: 180},
+          schedules: [{time: '00:10:55', triggerType: 'time', triggerDesc: '原计划', delaySec: 0}]
+        },
+        {
+          id: 'J-2',
+          name: '雷达干扰器-B',
+          type: '雷达',
+          lon: '116.420000',
+          lat: '40.050000',
+          params: {freq: 420, power: 600, duration: 160},
+          schedules: [{time: '00:13:20', triggerType: 'time', triggerDesc: '', delaySec: 0}]
+        }
+      ],
+      measurements: [
+        {
+          id: 'M-1',
+          name: '雷达测量站-1',
+          type: '雷达',
+          lon: '116.300000',
+          lat: '39.900000',
+          time: '00:09:30',
+          params: {range: 50, freq: 100, precision: 0.5},
+          schedules: []
+        },
+        {
+          id: 'M-2',
+          name: '光学测量仪-2',
+          type: '光学',
+          lon: '116.500000',
+          lat: '40.180000',
+          time: '00:11:15',
+          params: {range: 50, freq: 100, precision: 0.5},
+          schedules: []
+        }
+      ],
+
+      missiles: [],
+      openMissilePanels: [],
+
+      activeEnvTab:['meteorology'],
+      weatherForm: {wind_speed: 3.5, wind_direction: 90, temperature: 25, visibility: 10},
+      geoForm: {terrain: 'hilly', alt: 350, desc: ''},
+
+      legend: {showMeasurement: true, showTarget: true, showJammer: true},
+
+      addTargetDialog: false,
+      addJammerDialog: false,
+      addMeasurementDialog: false,
+      editModeTarget: false,
+      editIndexTarget: -1,
+      editModeJammer: false,
+      editIndexJammer: -1,
+      editModeMeasurement: false,
+      editIndexMeasurement: -1,
+
+      newTargetForm: {
+        name: '',
+        type: '静态',
+        fixedFromPlan: false,
+        lon: '',
+        lat: '',
+        time: '',
+        statusText: '可用',
+        schemePointName: ''
+      },
+      newJammerForm: {name: '', type: '电磁', lon: '', lat: '', schedules: []},
+      newMeasurementForm: {name: '', type: '雷达', lon: '', lat: '', time: '', schedules: [], posNote: ''},
+
+      targetForm: {mode: 'normal', duration: 300, motion: 'fixed', speed: 0},
+      jammerForm: {freq: 350, power: 500, duration: 180},
+      measureForm: {range: 50, freq: 100, precision: 0.5, deg: 10, height: 20},
+
+      mapMarkerPosition: null,
+
+      targetRules: {
+        name: [{required: true, message: '填写名称', trigger: 'blur'}],
+        type: [{required: true, message: '选择类型', trigger: 'change'}],
+        lon: [{required: true, message: '填写经度', trigger: 'blur'}],
+        lat: [{required: true, message: '填写纬度', trigger: 'blur'}],
+        time: [{required: true, message: '选择时间', trigger: 'change'}]
+      },
+      jammerRules: {
+        name: [{required: true, message: '填写名称', trigger: 'blur'}],
+        type: [{required: true, message: '选择类型', trigger: 'change'}],
+        lon: [{required: true, message: '填写经度', trigger: 'blur'}],
+        lat: [{required: true, message: '填写纬度', trigger: 'blur'}]
+      },
+      measureRules: {
+        name: [{required: true, message: '填写名称', trigger: 'blur'}],
+        type: [{required: true, message: '选择类型', trigger: 'change'}],
+        lon: [{required: true, message: '填写经度', trigger: 'blur'}],
+        lat: [{required: true, message: '填写纬度', trigger: 'blur'}],
+        time: [{required: true, message: '选择时间', trigger: 'change'}]
+      },
+
+      leftWidth: '58%',
+      rightWidth: '42%',
+    };
+  },
+  computed: {
+    currentRangeInfo() {
+      if (!this.rangeForm.rangeId) return null;
+      return this.rangeForm.ranges.find(r => r.id === this.rangeForm.rangeId) || null;
+    },
+    targetSchemeView() {
+      const types = Array.from(new Set(this.targets.map(t => t.type))).filter(Boolean);
+      return {
+        ...this.targetPlanBase,
+        count: this.targets.length,
+        types,
+        points: this.targets,
+        coveragePct: this.targetPlanBase.coveragePct
+      };
+    },
+    jammerSchemeView() {
+      const types = Array.from(new Set(this.jammers.map(j => j.type))).filter(Boolean);
+      const includes = Array.from(new Set([...(this.jammerPlanBase.includesPreset || []), ...this.jammers.map(j => j.name)]));
+      return {...this.jammerPlanBase, count: this.jammers.length, types, points: this.jammers, includes};
+    },
+    measureSchemeView() {
+      const types = Array.from(new Set(this.measurements.map(m => m.type))).filter(Boolean);
+      const includes = Array.from(new Set([...(this.measurePlanBase.includesPreset || []), ...this.measurements.map(m => m.name)]));
+      return {...this.measurePlanBase, count: this.measurements.length, types, includes, points: this.measurements};
+    },
+    stat() {
+      return {
+        total: this.targets.length + this.jammers.length + this.measurements.length,
+        target: this.targets.length,
+        jammer: this.jammers.length,
+        measure: this.measurements.length
+      };
+    },
+    // 父级仍负责汇总 Step4 的完整时序
+    allEvents() {
+      const events = [];
+      this.measurements.forEach((m) => {
+        if (m.time) events.push({
+          time: `T0+${m.time}`, rawTime: m.time, name: m.name,
+          title: '测量装备启动', desc: `${m.name} 开始工作`,
+          kindText: '测量装备', typeClass: 'tl-secondary', badgeClass: 'badge-secondary', triggerTypeText: '定时触发'
+        });
+      });
+      this.targets.forEach((t) => {
+        if (t.time) events.push({
+          time: `T0+${t.time}`, rawTime: t.time, name: t.name,
+          title: t.type === '动态' ? '动态靶标运行' : '靶标激活',
+          desc: `${t.name} ${t.type === '动态' ? '按设定运动' : '激活'}`,
+          kindText: '靶标装备',
+          typeClass: t.type === '动态' ? 'tl-danger' : 'tl-warn',
+          badgeClass: t.statusText === '可用' ? 'badge-success' : (t.statusText === '维护中' ? 'badge-warn' : 'badge-accent'),
+          triggerTypeText: '定时触发'
+        });
+      });
+      this.jammers.forEach((j) => {
+        (j.schedules || []).forEach((s) => {
+          if (!s.time) return;
+          events.push({
+            time: `T0+${s.time}`, rawTime: s.time, name: j.name,
+            title: '干扰装备启动',
+            desc: `${j.name} ${s.triggerType === 'condition' ? '(条件触发)' : '(激活)'}`,
+            kindText: s.triggerType === 'condition' ? '干扰装备(触发)' : '干扰装备',
+            typeClass: s.triggerType === 'condition' ? 'tl-danger' : 'tl-muted',
+            badgeClass: s.triggerType === 'condition' ? 'badge-danger' : 'badge-accent',
+            triggerTypeText: s.triggerType === 'time' ? '定时触发' : (s.triggerType === 'manual' ? '手动触发' : '条件触发')
+          });
+        });
+      });
+      console.log('events', events.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime)));
+      return events.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime));
+    },
+    filteredAllEvents() {
+      return this.allEvents.filter(e => {
+        if (e.kindText === '测量装备' && !this.legend.showMeasurement) return false;
+        if (e.kindText.indexOf('靶标') > -1 && !this.legend.showTarget) return false;
+        if (e.kindText.indexOf('干扰') > -1 && !this.legend.showJammer) return false;
+        return true;
+      });
+    }
+  },
+  watch: {
+    missionMissileCount: {
+      immediate: true,
+      handler(n) {
+        const count = Number(n) || 1;
+        const arr = this.missiles.slice();
+        if (arr.length < count) {
+          for (let i = arr.length; i < count; i++) {
+            arr.push({
+              id: `MSL-${i + 1}`,
+              targetType: 'land',
+              targetName: '',
+              targetCoord: '',
+              targetSpeed: 0,
+              targetDesc: '',
+              traj: {start: '116.3,39.9,0', end: '116.5,40.1,0.5', timeSec: 90}
+            });
+          }
+        } else if (arr.length > count) {
+          arr.length = count;
+        }
+        this.missiles = arr;
+        this.openMissilePanels = this.missiles.map(m => m.id);
+      }
+    }
+  },
+  mounted() {
+    // 获取环境信息
+    getBattlefieldEnvironment({simulationId:this.plan.id}).then((res)=>{
+      this.weatherForm = JSON.parse(res.data.environmentJson)
+    })
+    getEquTree({subTaskId:this.plan.subTaskId}).then((res)=>{
+      this.equTree = res.data
+    })
+  },
+  methods: {
+    beforeUpload(file) {
+      const okExt = /\.xml$/i.test(file.name);
+      const okSize = file.size <= 2 * 1024 * 1024;
+      if (!okExt) this.$message.error('仅支持 .xml 文件');
+      if (!okSize) this.$message.error('文件不能超过 2MB');
+      return okExt && okSize;
+    },
+    handleFileChange(file, fileList) {
+      this.fileList = fileList.slice(-1);
+      this.xmlImport.rawFile = file.raw;
+    },
+    handleRemove() {
+      this.fileList = [];
+      this.xmlImport = {rawFile: null};
+    },
+    confirmRange() {
+      const selected = this.currentRangeInfo;
+      if (!selected) {
+        this.$message.warning('请先选择靶区');
+        return;
+      }
+      this.selectedRange = selected.name || '';
+      this.rangeDialogVisible = false;
+      this.showMain = true;
+      this.$message.success(`已选择靶区:${this.selectedRange}`);
+    },
+    exportPlan() {
+    },
+    goBack() {
+      this.$router && this.$router.go(-1);
+    },
+    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;
+    },
+    getPreviewTitle() {
+      return this.activeTab === 'target'
+        ? '靶标布设方案信息'
+        : this.activeTab === 'jammer'
+          ? '干扰装备布设方案信息'
+          : '测量装备布设方案信息';
+    },
+    getTimelineType(c) {
+      return c === 'tl-secondary' ? 'success'
+        : c === 'tl-warn' ? 'warning'
+          : c === 'tl-danger' ? 'danger'
+            : c === 'tl-muted' ? 'info'
+              : 'primary';
+    },
+    getTagType(c) {
+      return c === 'badge-success' ? 'success'
+        : c === 'badge-warn' ? 'warning'
+          : c === 'badge-danger' ? 'danger'
+            : c === 'badge-accent' ? 'info'
+              : c === 'badge-secondary' ? 'primary'
+                : 'default';
+    },
+    goPrev() {
+      this.step = Math.max(1, this.step - 1);
+    },
+    goNext() {
+      this.step = Math.min(4, this.step + 1);
+    },
+
+    // —— 新增/编辑弹窗相关(保留) ——
+    openAddTargetDialog() {
+      this.editModeTarget = false;
+      this.editIndexTarget = -1;
+      this.newTargetForm = {
+        name: '',
+        type: '静态',
+        fixedFromPlan: false,
+        lon: '',
+        lat: '',
+        time: '',
+        statusText: '可用',
+        schemePointName: ''
+      };
+      this.targetForm = {mode: 'normal', duration: 300, motion: 'fixed', speed: 0};
+      this.mapMarkerPosition = null;
+      this.addTargetDialog = true;
+    },
+    openAddJammerDialog() {
+      this.editModeJammer = false;
+      this.editIndexJammer = -1;
+      this.newJammerForm = {name: '', type: '电磁', lon: '', lat: '', schedules: []};
+      this.jammerForm = {freq: 350, power: 500, duration: 180};
+      this.addJammerDialog = true;
+    },
+    openAddMeasurementDialog() {
+      this.editModeMeasurement = false;
+      this.editIndexMeasurement = -1;
+      this.newMeasurementForm = {name: '', type: '雷达', lon: '', lat: '', time: '', schedules: [], posNote: ''};
+      this.measureForm = {range: 50, freq: 100, precision: 0.5, deg: 10, height: 20};
+      this.addMeasurementDialog = true;
+    },
+    selectMapPosition(e) {
+      const x = Math.max(0, Math.min(e.offsetX, e.currentTarget.clientWidth));
+      const y = Math.max(0, Math.min(e.offsetY, e.currentTarget.clientHeight));
+      this.mapMarkerPosition = {x, y};
+      const baseLon = 116.3, baseLat = 39.9;
+      this.newTargetForm.lon = (baseLon + x / 200).toFixed(6);
+      this.newTargetForm.lat = (baseLat + y / 200).toFixed(6);
+    },
+    onTargetTypeChange() {
+      if (this.newTargetForm.type === '动态') this.newTargetForm.fixedFromPlan = false;
+    },
+    confirmAddTarget() {
+      this.$refs.targetFormRef.validate(valid => {
+        if (!valid) return;
+        const statusMap = {'可用': 'badge-success', '维护中': 'badge-warn', '停用': 'badge-accent'};
+        const params = {
+          mode: this.targetForm.mode,
+          duration: this.targetForm.duration,
+          motion: this.newTargetForm.type === '动态' ? this.targetForm.motion : 'fixed',
+          speed: this.newTargetForm.type === '动态' ? this.targetForm.speed : 0
+        };
+        if (this.editModeTarget && this.editIndexTarget > -1) {
+          const t = this.targets[this.editIndexTarget];
+          Object.assign(t, {
+            name: this.newTargetForm.name, type: this.newTargetForm.type,
+            fixedFromPlan: !!this.newTargetForm.fixedFromPlan, lon: this.newTargetForm.lon, lat: this.newTargetForm.lat,
+            time: this.newTargetForm.time, statusText: this.newTargetForm.statusText,
+            statusClass: statusMap[this.newTargetForm.statusText] || 'badge-success',
+            params: {...params}
+          });
+          this.$message.success('已保存靶标修改');
+        } else {
+          const id = `T-${Date.now()}`;
+          this.targets.push({
+            id, name: this.newTargetForm.name, type: this.newTargetForm.type,
+            fixedFromPlan: !!this.newTargetForm.fixedFromPlan, lon: this.newTargetForm.lon, lat: this.newTargetForm.lat,
+            time: this.newTargetForm.time, statusText: this.newTargetForm.statusText,
+            statusClass: statusMap[this.newTargetForm.statusText] || 'badge-success',
+            params
+          });
+          this.$message.success('已添加靶标');
+        }
+        this.addTargetDialog = false;
+      });
+    },
+    editTarget(i) {
+      const t = this.targets[i];
+      this.editModeTarget = true;
+      this.editIndexTarget = i;
+      this.newTargetForm = {
+        name: t.name,
+        type: t.type,
+        fixedFromPlan: !!t.fixedFromPlan,
+        lon: t.lon,
+        lat: t.lat,
+        time: t.time,
+        statusText: t.statusText,
+        schemePointName: t.name
+      };
+      this.targetForm = {
+        ...(t.params || {}),
+        motion: (t.params && t.params.motion) || (t.type === '动态' ? 'linear' : 'fixed')
+      };
+      this.mapMarkerPosition = null;
+      this.addTargetDialog = true;
+    },
+    removeTarget(i) {
+      this.$confirm(`确定删除 "${this.targets[i].name}"?`, '提示', {type: 'warning'})
+        .then(() => {
+          this.targets.splice(i, 1);
+          this.$message.success('已删除');
+        })
+        .catch(() => {
+        });
+    },
+
+    addJammerSchedule() {
+      this.newJammerForm.schedules.push({
+        time: '',
+        triggerType: '导弹接近',
+        threshold: '距离<5km',
+        action: '立即启动干扰',
+        triggerDesc: '',
+        delaySec: 0
+      });
+    },
+    removeJammerSchedule(i) {
+      this.newJammerForm.schedules.splice(i, 1);
+    },
+    confirmAddJammer() {
+      if ((this.newJammerForm.schedules || []).length === 0) return this.$message.warning('至少添加一条激活计划');
+      if (this.newJammerForm.schedules.some(s => !s.time || !s.triggerType)) return this.$message.warning('存在未填写完整的计划');
+      this.$refs.jammerFormRef.validate(valid => {
+        if (!valid) return;
+        if (this.editModeJammer && this.editIndexJammer > -1) {
+          const j = this.jammers[this.editIndexJammer];
+          j.name = this.newJammerForm.name;
+          j.type = this.newJammerForm.type;
+          j.lon = this.newJammerForm.lon;
+          j.lat = this.newJammerForm.lat;
+          j.params = {...this.jammerForm};
+          j.schedules = JSON.parse(JSON.stringify(this.newJammerForm.schedules));
+          this.$message.success('已保存干扰装备修改');
+        } else {
+          const id = `J-${Date.now()}`;
+          this.jammers.push({
+            id,
+            name: this.newJammerForm.name,
+            type: this.newJammerForm.type,
+            lon: this.newJammerForm.lon,
+            lat: this.newJammerForm.lat,
+            params: {...this.jammerForm},
+            schedules: JSON.parse(JSON.stringify(this.newJammerForm.schedules))
+          });
+          this.$message.success('已添加干扰装备');
+        }
+        this.addJammerDialog = false;
+      });
+    },
+    editJammer(i) {
+      const j = this.jammers[i];
+      this.editModeJammer = true;
+      this.editIndexJammer = i;
+      this.newJammerForm = {
+        name: j.name,
+        type: j.type,
+        lon: j.lon,
+        lat: j.lat,
+        schedules: JSON.parse(JSON.stringify(j.schedules || []))
+      };
+      this.jammerForm = {...(j.params || {freq: 350, power: 500, duration: 180})};
+      this.addJammerDialog = true;
+    },
+    removeJammer(i) {
+      this.$confirm(`确定删除 "${this.jammers[i].name}"?`, '提示', {type: 'warning'})
+        .then(() => {
+          this.jammers.splice(i, 1);
+          this.$message.success('已删除');
+        })
+        .catch(() => {
+        });
+    },
+
+    addMeasurementSchedule() {
+      this.newMeasurementForm.schedules.push({
+        time: '',
+        triggerType: '导弹接近',
+        threshold: '距离<5km',
+        action: '发出警报',
+        triggerDesc: '',
+        delaySec: 0
+      });
+    },
+    removeMeasurementSchedule(i) {
+      this.newMeasurementForm.schedules.splice(i, 1);
+    },
+    confirmAddMeasurement() {
+      this.$refs.measureFormRef.validate(valid => {
+        if (!valid) return;
+        if ((this.newMeasurementForm.schedules || []).some(s => !s.time || !s.triggerType || !s.action)) {
+          return this.$message.warning('测量装备存在未填写完整的激活计划');
+        }
+        if (this.editModeMeasurement && this.editIndexMeasurement > -1) {
+          const m = this.measurements[this.editIndexMeasurement];
+          m.name = this.newMeasurementForm.name;
+          m.type = this.newMeasurementForm.type;
+          m.lon = this.newMeasurementForm.lon;
+          m.lat = this.newMeasurementForm.lat;
+          m.time = this.newMeasurementForm.time;
+          m.posNote = this.newMeasurementForm.posNote || '';
+          m.params = {...this.measureForm};
+          m.schedules = JSON.parse(JSON.stringify(this.newMeasurementForm.schedules || []));
+          this.$message.success('已保存测量装备修改');
+        } else {
+          const id = `M-${Date.now()}`;
+          this.measurements.push({
+            id, name: this.newMeasurementForm.name, type: this.newMeasurementForm.type,
+            lon: this.newMeasurementForm.lon, lat: this.newMeasurementForm.lat, time: this.newMeasurementForm.time,
+            posNote: this.newMeasurementForm.posNote || '',
+            params: {...this.measureForm},
+            schedules: JSON.parse(JSON.stringify(this.newMeasurementForm.schedules || []))
+          });
+          this.$message.success('已添加测量装备');
+        }
+        this.addMeasurementDialog = false;
+      });
+    },
+    editMeasurement(i) {
+      const m = this.measurements[i];
+      this.editModeMeasurement = true;
+      this.editIndexMeasurement = i;
+      this.newMeasurementForm = {
+        name: m.name,
+        type: m.type,
+        lon: m.lon,
+        lat: m.lat,
+        time: m.time,
+        schedules: JSON.parse(JSON.stringify(m.schedules || [])),
+        posNote: m.posNote || ''
+      };
+      this.measureForm = {...(m.params || {range: 50, freq: 100, precision: 0.5, deg: 10, height: 20})};
+      this.addMeasurementDialog = true;
+    },
+    removeMeasurement(i) {
+      this.$confirm(`确定删除 "${this.measurements[i].name}"?`, '提示', {type: 'warning'})
+        .then(() => {
+          this.measurements.splice(i, 1);
+          this.$message.success('已删除');
+        })
+        .catch(() => {
+        });
+    },
+
+    rebuildPreview() {
+      this.$message.success('时序已刷新');
+    },
+    handleTrajectory(plan) {
+      this.$refs.uploadDd.show({
+        missileId:plan.id,
+        simulationId:this.plan.id,
+      })
+    },
+    async saveFinal() {
+      this.saveLoad = true;
+
+      try {
+        // 保存环境配置
+        await battlefieldEnvironmentInsert({simulationId:this.plan.id,environmentJson:JSON.stringify(this.weatherForm)})
+
+      } catch (e) {
+
+      }
+
+      this.$message.success('方案已保存(示例)');
+      const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(this.scenarioVersion || '');
+      if (m) {
+        const [maj, min, pat] = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10) + 1];
+        this.scenarioVersion = `v${maj}.${min}.${pat}`;
+      }
+
+      this.saveLoad = false;
+      this.$confirm("是否开始推演?", "提示", {type: "warning"}).then(() => {
+        this.$router.push("/Deduction/stratDeduction")
+      }).catch(() => {
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* ===== 入口页同色系主题(深蓝大屏) ===== */
+:root {
+  --brand: #0B2A59; /* 顶部导航深蓝 */
+  --bg: #071A35; /* 页面底色 */
+  --card: #0D2346; /* 卡片主体 */
+  --card-soft: #0F2B57; /* 次级卡片/表格底 */
+  --panel: #0B2143; /* 面板型卡片 */
+  --border: #1C3C72;
+  --text: #F4F8FF;
+  --text-2: #C9D7F0;
+  --muted: #9AB0D1;
+  --accent: #66B1FF; /* 入口页主高光蓝 */
+  --accent-2: #2BD99F; /* 入口页“选择方案/成功”绿 */
+  --warn: #FFC15A;
+  --danger: #FF7070;
+  --radius: 14px;
+  --radius-lg: 16px;
+  --header-h: 84px;
+  --footer-h: 60px;
+  --shadow: 0 10px 30px rgba(0, 0, 0, .35);
+  --glow: 0 0 0 1px rgba(255, 255, 255, .06), 0 12px 24px rgba(5, 24, 54, .45);
+}
+
+html, body, #app, .page {
+  height: 100%;
+}
+
+.page {
+  min-height: 100%;
+  background: radial-gradient(900px 500px at 85% -10%, rgba(102, 177, 255, .12), transparent 60%),
+  radial-gradient(800px 480px at -10% 25%, rgba(43, 217, 159, .10), transparent 60%),
+  var(--bg);
+  color: var(--text);
+  overflow: hidden;
+  font-family: "PingFang SC", "Microsoft YaHei", Segoe UI, system-ui, -apple-system, sans-serif;
+  letter-spacing: .2px;
+}
+
+.version-badge {
+  margin-left: 10px;
+  padding: 2px 8px;
+  border: 1px solid rgba(255, 255, 255, .28);
+  border-radius: 999px;
+  font-size: 12px;
+  color: #EAF2FF;
+}
+
+/* ===== 卡片与头部 ===== */
+.ui-card {
+  background: var(--card) !important;
+  border: 1px solid rgba(255, 255, 255, .08) !important;
+  border-radius: var(--radius-lg) !important;
+  box-shadow: var(--glow) !important;
+}
+
+.ui-card.soft {
+  background: var(--card-soft) !important;
+}
+
+.panel-card {
+  background: var(--panel) !important;
+}
+
+.header {
+  height: var(--header-h) !important;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(180deg, var(--brand), #0A244B);
+  border-bottom: 1px solid rgba(255, 255, 255, .06);
+  padding: 0 16px;
+}
+
+.header-inner {
+  width: 100%;
+  max-width: 1760px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.header-actions {
+  margin-left: 12px;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 14px;
+  font-weight: 700;
+  color: #EAF2FF;
+  background: linear-gradient(180deg, rgba(255, 255, 255, .07), rgba(255, 255, 255, .02));
+  border-bottom: 1px solid rgba(255, 255, 255, .08);
+}
+
+/* ===== Steps 对齐入口页 ===== */
+.steps-light {
+  flex: 1;
+}
+
+.steps-light ::v-deep .el-steps--simple {
+  background: transparent;
+  padding: 6px 10px;
+  border-radius: var(--radius);
+}
+
+.steps-light ::v-deep .el-step__title {
+  color: #EAF2FF;
+  font-weight: 700;
+  font-size: 14px;
+}
+
+.steps-light ::v-deep .is-process .el-step__title {
+  color: #fff;
+  text-shadow: 0 1px 0 rgba(0, 0, 0, .2);
+}
+
+.steps-light ::v-deep .el-step__icon-inner {
+  color: #EAF2FF;
+}
+
+.step-index {
+  color: #EAF2FF;
+  font-weight: 800;
+}
+
+/* ===== 布局与滚动 ===== */
+.workspace {
+  height: calc(100% - var(--header-h) - var(--footer-h));
+}
+
+.aside {
+  padding: 12px 12px 8px;
+  overflow: hidden;
+}
+
+.aside-left {
+  border-right: 1px solid rgba(255, 255, 255, .08);
+}
+
+.main-full {
+  padding: 12px;
+  overflow: hidden;
+}
+
+.fill-card {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.fill-card ::v-deep .el-card__body {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  padding: 12px;
+}
+
+.fill-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+}
+
+/* ===== 列表区 ===== */
+.switcher {
+  min-width: 160px;
+}
+
+.left-body-grid {
+  display: grid;
+  grid-template-rows:1fr 1fr;
+  gap: 12px;
+  flex: 1;
+  min-height: 0;
+}
+
+::v-deep .el-table {
+  background: var(--card-soft);
+  color: var(--text-2);
+  border-color: rgba(255, 255, 255, .08);
+}
+
+::v-deep .el-table th {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .03));
+  color: var(--text);
+  font-weight: 700;
+  border-color: rgba(255, 255, 255, .08);
+}
+
+::v-deep .el-table td {
+  border-color: rgba(255, 255, 255, .06);
+}
+
+::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
+  background: rgba(255, 255, 255, .02);
+}
+
+::v-deep .el-table__row:hover td {
+  background: rgba(102, 177, 255, .06) !important;
+}
+
+/* ===== 时间轴(与入口页卡片层次一致) ===== */
+.timeline-section {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin: 0 2px 6px;
+  color: #EAF2FF;
+  font-weight: 700;
+}
+
+.timeline-wrap {
+  padding-right: 4px;
+}
+
+.tl-card {
+  border-left: 3px solid var(--accent);
+  padding: 10px 12px;
+  background: rgba(255, 255, 255, .03) !important;
+}
+
+.tl-title {
+  font-weight: 800;
+  color: #EAF2FF;
+  margin-bottom: 4px;
+  font-size: 14px;
+}
+
+.tl-desc {
+  font-size: 12px;
+  color: var(--text-2);
+}
+
+.m-l-6 {
+  margin-left: 6px;
+}
+
+.el-timeline-item__tail {
+  border-left-color: rgba(255, 255, 255, .08) !important;
+}
+
+.el-timeline-item.is-success .el-timeline-item__node {
+  background: #2BD99F !important;
+}
+
+.el-timeline-item.is-warning .el-timeline-item__node {
+  background: #FFC15A !important;
+}
+
+.el-timeline-item.is-danger .el-timeline-item__node {
+  background: #FF7070 !important;
+}
+
+.el-timeline-item.is-info .el-timeline-item__node {
+  background: #69B1FF !important;
+}
+
+/* ===== KPI 卡片 ===== */
+.kpi-grid {
+  display: grid;
+  grid-template-columns:repeat(4, 1fr);
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.kpi-card {
+  border: 1px solid rgba(102, 177, 255, .28);
+  border-radius: 10px;
+  text-align: center;
+  padding: 12px 8px;
+  background: radial-gradient(240px 140px at 85% -30%, rgba(102, 177, 255, .16), transparent 60%),
+  radial-gradient(220px 140px at 0% 140%, rgba(43, 217, 159, .12), transparent 60%),
+  linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);
+}
+
+.kpi-value {
+  font-size: 26px;
+  font-weight: 900;
+  color: #E2EEFF;
+  letter-spacing: .5px;
+}
+
+.kpi-label {
+  font-size: 12px;
+  color: var(--muted);
+}
+
+/* ===== 子标题左色条(入口页常用) ===== */
+.sub-title {
+  position: relative;
+  padding-left: 10px;
+  font-weight: 800;
+  color: #EAF2FF;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.sub-title:before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 2px;
+  bottom: 2px;
+  width: 3px;
+  background: linear-gradient(180deg, var(--accent), rgba(102, 177, 255, .3));
+  border-radius: 2px;
+}
+
+/* Descriptions 表格底色 */
+.desc-table ::v-deep .el-descriptions__body {
+  background: rgba(255, 255, 255, .02);
+}
+
+.desc-table ::v-deep .el-descriptions__label {
+  color: #CFE2FF;
+}
+
+.desc-table ::v-deep .el-descriptions__content {
+  color: #EAF2FF;
+}
+
+/* ===== 弹窗(入口页玻璃感) ===== */
+::v-deep .el-dialog {
+  background: linear-gradient(180deg, rgba(13, 35, 70, .96), rgba(11, 33, 67, .96));
+  border: 1px solid rgba(255, 255, 255, .08);
+  box-shadow: var(--shadow);
+  border-radius: 16px;
+  backdrop-filter: blur(6px);
+}
+
+::v-deep .el-dialog__header {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
+  border-bottom: 1px solid rgba(255, 255, 255, .08);
+  color: var(--text);
+}
+
+/* ===== 底部操作条 ===== */
+.footer-actions {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: var(--footer-h);
+  background: rgba(8, 26, 52, .96);
+  border-top: 1px solid rgba(255, 255, 255, .06);
+  backdrop-filter: blur(6px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 20;
+}
+
+.actions-inner {
+  width: 100%;
+  max-width: 1760px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 10px 16px;
+}
+
+/* ===== 自适应 ===== */
+@media (max-width: 1480px) {
+  .kpi-grid {
+    grid-template-columns:repeat(2, 1fr);
+  }
+}
+
+@media (max-width: 1280px) {
+  .workspace {
+    flex-direction: column;
+  }
+
+  .aside-left, .aside-right {
+    width: 100% !important;
+  }
+}
+
+/* ===== 试验任务规划 · 折叠面板内的卡片优化 ===== */
+
+/* 折叠项标题行(“导弹 #1 …”那一行) */
+::v-deep .el-collapse-item__header {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
+  border: 1px solid rgba(255, 255, 255, .10);
+  border-bottom: none;
+  border-radius: 12px 12px 0 0;
+  color: var(--text);
+  font-weight: 700;
+  padding: 0 14px;
+  min-height: 44px;
+}
+
+::v-deep .el-collapse-item__arrow {
+  color: #9ED0FF;
+}
+
+/* 折叠项内容面板包裹层 */
+::v-deep .el-collapse-item__wrap {
+  background: transparent;
+  border: 1px solid rgba(255, 255, 255, .10);
+  border-top: none;
+  border-radius: 0px 0px 12px 12px;
+  margin-top: -1px;
+}
+
+/* 目标卡片:就在折叠项里那张 .ui-card.panel-card */
+.main-full .panel-card {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .04), rgba(255, 255, 255, .02)) !important;
+  border: 1px solid rgba(255, 255, 255, .10) !important;
+  border-radius: 0 0 14px 14px !important; /* 与header圆角匹配 */
+  box-shadow: 0 8px 24px rgba(0, 0, 0, .28) !important;
+  transition: box-shadow .25s ease, transform .25s ease, border-color .25s ease;
+  overflow: hidden;
+}
+
+/* 轻微悬浮反馈 */
+.main-full .panel-card:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 14px 36px rgba(0, 0, 0, .34) !important;
+  border-color: rgba(102, 177, 255, .28) !important;
+}
+
+/* 卡片内部:标题栏与分组(你这张卡片用的是 el-form + el-divider) */
+.main-full .panel-card .sub-title,
+.main-full .panel-card .card-header {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
+  border: 1px solid rgba(255, 255, 255, .08);
+  border-left: 3px solid var(--accent);
+  border-radius: 10px;
+  padding: 8px 10px;
+  margin-bottom: 10px;
+}
+
+/* 分隔线更细、更亮一点 */
+.main-full .panel-card ::v-deep .el-divider {
+  background-color: transparent;
+  margin: 10px 0 12px;
+}
+
+.main-full .panel-card ::v-deep .el-divider__text {
+  color: #CFE2FF;
+  font-weight: 700;
+  letter-spacing: .2px;
+  background: rgba(255, 255, 255, .02);
+  padding: 2px 10px;
+  border: 1px solid rgba(255, 255, 255, .08);
+  border-radius: 999px;
+}
+
+/* 表单致密化 + 对齐 */
+.main-full .panel-card .dense-form ::v-deep .el-form-item {
+  margin-bottom: 8px;
+}
+
+.main-full .panel-card .dense-form ::v-deep .el-form-item__label {
+  color: var(--text-2);
+  padding-right: 8px;
+}
+
+.main-full .panel-card .dense-form ::v-deep .el-input__inner,
+.main-full .panel-card .dense-form ::v-deep .el-textarea__inner,
+.main-full .panel-card .dense-form ::v-deep .el-select .el-input__inner {
+  background: rgba(255, 255, 255, .04);
+  border: 1px solid rgba(255, 255, 255, .14);
+  color: var(--text);
+}
+
+.main-full .panel-card .dense-form ::v-deep .el-input__inner:focus,
+.main-full .panel-card .dense-form ::v-deep .el-textarea__inner:focus {
+  border-color: var(--accent);
+  box-shadow: 0 0 0 3px rgba(102, 177, 255, .18);
+}
+
+/* 右上角小工具区(你在“弹道轨迹”分隔线下方有一个右侧按钮容器) */
+.main-full .panel-card .dense-form .el-row + div[style*="text-align:right"] {
+  margin-top: -6px;
+  margin-bottom: 8px;
+}
+
+.main-full .panel-card .dense-form .el-button--info {
+  border-radius: 999px;
+  border-color: rgba(255, 255, 255, .22);
+  background: rgba(255, 255, 255, .04);
+  color: var(--text);
+}
+
+/* 轨迹预览占位卡片再提亮一点边框与背景 */
+.main-full .panel-card .trajectory-placeholder {
+  background: linear-gradient(180deg, rgba(255, 255, 255, .03), rgba(255, 255, 255, .01));
+  border: 1px dashed rgba(102, 177, 255, .28);
+}
+
+.main-full .panel-card .placeholder-icon {
+  opacity: .9;
+}
+
+.main-full .panel-card .placeholder-text {
+  color: var(--text-2);
+}
+
+/* 折叠项间距 & 组与组之间留白 */
+.main-full ::v-deep .el-collapse {
+  border: none;
+}
+
+.main-full ::v-deep .el-collapse-item {
+  margin-bottom: 12px;
+}
+
+/* 小号徽章:给“导弹 #1/#2”做编号(可选)——标题里已有文本时也会加底纹 */
+::v-deep .el-collapse-item__header:before {
+  content: "";
+  position: absolute;
+  left: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--accent);
+  box-shadow: 0 0 0 4px rgba(102, 177, 255, .18);
+}
+
+/* 自适应:窄屏时让表单行距更松一些 */
+@media (max-width: 1280px) {
+  .main-full .panel-card .dense-form ::v-deep .el-form-item {
+    margin-bottom: 10px;
+  }
+}
+
+.map-container { /* 可选的外层包装,主要用于文字等 */
+}
+
+.map-placeholder {
+  position: relative; /* 作为定位上下文 */
+  width: 100%;
+  height: 240px; /* 你也可换成想要的高度 */
+  overflow: hidden;
+  border: 1px dashed rgba(102, 177, 255, .28);
+  border-radius: 10px;
+}
+
+.map-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover; /* 铺满容器 */
+  display: block;
+}
+
+.map-marker {
+  position: absolute; /* 让 left/top 生效 —— 核心修复 */
+  transform: translate(-50%, -100%); /* 锚点在图标尖端,看起来更准 */
+  pointer-events: none; /* 不挡住后续点击 */
+}
+
+.icon-marker {
+  font-size: 24px; /* 提高“高度很小”的可见感受 */
+  color: #ff5252;
+  text-shadow: 0 2px 6px rgba(0, 0, 0, .35);
+}
+
+.map-hint {
+  margin-top: 6px;
+  font-size: 12px;
+  color: var(--text-2);
+}
+
+</style>
+