zhaoen пре 3 месеци
родитељ
комит
244d6046f7

+ 1 - 1
src/plugins/mqttPlugins.js

@@ -10,7 +10,7 @@ const OPTIONS = {
   password: "your-password", // 如果需要认证
   clean: true,
   connectTimeout: 30000,
-  reconnectPeriod: 5000, // 断线重连间隔
+  reconnectPeriod: 500000, // 断线重连间隔
 };
 
 let client = null;

+ 74 - 18
src/views/Deduction/taskSetting/components/GanttChart.vue

@@ -48,6 +48,15 @@ export default {
     timeMargin: {
       type: Number,
       default: 600 // 10分钟的时间边距
+    },
+    // 新增:每项高度和间隔的配置
+    itemHeight: {
+      type: Number,
+      default: 60 // 每项高度
+    },
+    itemSpacing: {
+      type: Number,
+      default: 20 // 项目间隔
     }
   },
   data() {
@@ -58,8 +67,15 @@ export default {
     }
   },
   computed: {
+    // 修改:根据项目数量、每项高度和间隔计算总高度
     chartHeight() {
-      return this.timeOrderedItems.length * 60 + 400
+      if (this.timeOrderedItems.length === 0) {
+        return 600; // 默认高度
+      }
+      // 总高度 = 所有项目高度 + 所有间隔 + 顶部和底部边距
+      return this.timeOrderedItems.length * this.itemHeight +
+          (this.timeOrderedItems.length - 1) * this.itemSpacing +
+          100; // 额外边距
     },
     legendData() {
       const legends = Object.keys(this.typeColorMap).map(type => ({
@@ -83,7 +99,13 @@ export default {
 
       const times = this.timelineData.flatMap(item => {
         const startTime = getTimeInSeconds(item.rawTime)
-        const endTime = startTime + (item.duration || this.defaultDuration);
+        // 如果有endTime,使用endTime计算结束时间,否则使用开始时间+默认持续时间
+        let endTime;
+        if (item.endTime) {
+          endTime = getTimeInSeconds(item.endTime)
+        } else {
+          endTime = startTime + (item.duration || this.defaultDuration);
+        }
         return [startTime, endTime];
       });
 
@@ -106,8 +128,15 @@ export default {
 
       return this.timelineData.map(item => {
         const startTime = getTimeInSeconds(item.rawTime)
-        const duration = item.duration || this.defaultDuration;
-        const endTime = startTime + duration;
+        // 计算持续时间:如果有endTime,使用endTime - startTime,否则使用duration或默认值
+        let duration, endTime;
+        if (item.endTime) {
+          endTime = getTimeInSeconds(item.endTime)
+          duration = endTime - startTime
+        } else {
+          duration = item.duration || this.defaultDuration;
+          endTime = startTime + duration;
+        }
 
         // 根据id查找索引,确保唯一性
         const yAxisIndex = this.timeOrderedItems.findIndex(
@@ -115,7 +144,12 @@ export default {
         );
 
         const displayTime = item.rawTime.startsWith('T0+') ? item.rawTime : `T0+${item.rawTime}`;
-        const displayText = `${displayTime}\n${item.title || '无标题'} \n${item.desc || '无描述'}`
+        // 显示结束时间(如果有)
+        const endTimeDisplay = item.endTime ?
+            (item.endTime.startsWith('T0+') ? item.endTime : `T0+${item.endTime}`) :
+            `T0+${this.formatTime(endTime)}`;
+
+        const displayText = `${displayTime} - ${endTimeDisplay}\n${item.title || '无标题'} \n${item.desc || '无描述'}`
 
         return {
           id: item.id, // 保留id用于标识
@@ -157,6 +191,14 @@ export default {
       return `${hours}h${minutes}m${secs}s`;
     },
 
+    // 将秒数格式化为时间字符串
+    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 `${hours}:${minutes}:${secs}`;
+    },
+
     // 根据id获取唯一的时间线项目
     getOrderedTimelineItems() {
       const getTimeInSeconds = (timeStr) => {
@@ -257,7 +299,7 @@ export default {
         yAxis: {
           type: 'category',
           data: yAxisData,
-          show: false,
+          show: false, // 不显示Y轴标签
           splitLine: { show: false }
         },
         series: [
@@ -267,7 +309,6 @@ export default {
             renderItem: (params, api) => {
               const startTime = api.value(1);
               const duration = api.value(3);
-              const endTime = startTime + duration;
               const categoryIndex = api.value(0);
 
               const xAxisScale = this.timeRange.max - this.timeRange.min;
@@ -278,23 +319,39 @@ export default {
               const minWidth = Math.max(50, (this.defaultDuration / xAxisScale) * chartWidth * 0.5);
               width = Math.max(width, minWidth);
 
-              const yPos = api.coord([0, categoryIndex])[1];
-              const height = 60;
+              // ✅ 关键修改:计算“平均分布”的 Y 位置,并添加顶部内边距
+              const totalItems = this.timeOrderedItems.length;
+              if (totalItems === 0) return;
+
+              // 获取图表的绘图区域高度(减去上下边距)
+              const gridHeight = this.chart.getHeight() * 0.9; // 假设上下各5%
 
-              const dataItem = this.processedData[params.dataIndex]
+              // --- 新增:定义顶部内边距 ---
+              // 例如,设置为 itemHeight 的 0.5 倍,或一个固定像素值
+              const topPadding = this.itemHeight * 0.5; // 也可以用固定值,如 30
+              // --- 新增结束 ---
+
+              // 计算每个项目的“中心位置”:等分减去顶部内边距后的高度
+              // 可用于放置项目的有效高度
+              const effectiveHeight = gridHeight - topPadding;
+              const slotHeight = effectiveHeight / totalItems;
+              // 中心位置 = 顶部内边距 + (槽位高度 * (索引 + 0.5))
+              const yPos = topPadding + (slotHeight * (categoryIndex + 0.5)); // +0.5 使项目居中于槽位
+
+              const dataItem = this.processedData[params.dataIndex];
 
               const rect = {
                 type: 'rect',
                 shape: {
                   x: startX + (this.chart.getWidth() * 0.05),
-                  y: yPos - height / 2,
+                  y: yPos - this.itemHeight / 2, // 居中对齐
                   width: width,
-                  height: height,
+                  height: this.itemHeight,
                   r: 4
                 },
                 style: api.style(),
                 z2: 0
-              }
+              };
 
               const text = {
                 type: 'text',
@@ -309,11 +366,10 @@ export default {
                   lineHeight: 18
                 },
                 z: 1
-              }
+              };
 
-              return { type: 'group', children: [rect, text] }
-            },
-            encode: { x: [1, 2], y: 0 }
+              return {type: 'group', children: [rect, text]};
+            }
           }
         ]
       }
@@ -339,7 +395,6 @@ export default {
       const container = this.$refs.ganttChart
       if (!container.clientWidth || !container.clientHeight) {
         container.style.width = '100%'
-        // container.style.height = '600px'
       }
       this.initChart()
     })
@@ -407,3 +462,4 @@ export default {
   font-size: 14px;
 }
 </style>
+

+ 167 - 193
src/views/Deduction/taskSetting/components/equipment/DynamicFormModal.vue

@@ -13,54 +13,50 @@
         label-width="140px"
         class="dynamic-form"
     >
-      <!-- 表单字段内容,添加showWhen条件判断 -->
+      <!-- 时序字段:拆分为分钟和秒输入框 -->
       <el-form-item
           v-for="(field, index) in formFields"
           :key="index"
           :label="field.label"
-          :prop="getFieldProp(field)"
+          :prop="field.prop"
           :required="isFieldRequired(field)"
           v-if="shouldShowField(field)"
       >
-        <!-- 处理ts特殊字段:拆分为分钟和秒两个输入框 -->
-        <div v-if="field.prop === 'ts'" class="ts-input-group">
+        <div v-if="field.dataType === 'ts'" class="ts-input-group">
           <el-input
-              class="custom-input"
-              style="width:48%; display: inline-block; margin-right:4%"
-              v-model="tsMinutes"
-              placeholder="请输入分钟数(无上限)"
-              type="number"
-              @input="handleTsChange"
+              v-model.number="tsValues[field.prop].minutes"
+          placeholder="请输入分钟数"
+          type="number"
+          min="0"
+          @input="handleTsChange(field.prop)"
+          @blur="handleTsBlur(field.prop, 'minutes')"
           ></el-input>分
           <el-input
-              class="custom-input"
-              style="width:48%; display: inline-block"
-              v-model="tsSeconds"
-              placeholder="请输入秒数(1-59)"
-              type="number"
-              max="59"
-              @input="handleTsChange"
+              v-model.number="tsValues[field.prop].seconds"
+          placeholder="请输入秒数"
+          type="number"
+          min="0"
+          max="59"
+          @input="handleTsChange(field.prop)"
+          @blur="handleTsBlur(field.prop, 'seconds')"
           ></el-input>秒
         </div>
 
         <!-- 其他表单字段... -->
         <el-input
-            style="width:100%"
+            style="width: 100%"
             v-else-if="['string', 'number', 'decimal', 'time'].includes(field.dataType)"
             v-model="formData[field.prop]"
             :placeholder="getFieldPlaceholder(field)"
             :type="getInputType(field.dataType)"
-            :step="field.dataType === 'decimal' ? 0.1 : undefined"
-            @input="handleFieldChange(field.prop)"
         ></el-input>
 
         <el-select
-            style="width:100%"
+            style="width: 100%"
             v-else-if="field.dataType === 'select' && field.options"
             v-model="formData[field.prop]"
-            :placeholder="field.placeholder || `请选择${field.label}`"
+            :placeholder="`请选择${field.label}`"
             clearable
-            @change="handleFieldChange(field.prop)"
         >
           <el-option
               v-for="option in field.options"
@@ -80,7 +76,7 @@
 </template>
 
 <script>
-import {getSimEquipmentDeploymentBySimulation} from "@/api/subPlanZb";
+import { getSimEquipmentDeploymentBySimulation } from "@/api/subPlanZb";
 
 export default {
   name: 'DynamicFormModal',
@@ -98,8 +94,7 @@ export default {
       formData: {},
       formRules: {},
       loading: false,
-      tsMinutes: '',
-      tsSeconds: ''
+      tsValues: {} // 存储时序字段的分秒值 { prop: { minutes: 0, seconds: 0 } }
     };
   },
   watch: {
@@ -111,44 +106,56 @@ export default {
       immediate: true,
       deep: true
     },
+    // 监听formData中时序字段变化,同步到分秒输入框
     formData: {
-      handler() {
-        if (this.formData.ts !== undefined && !isNaN(this.formData.ts)) {
-          this.tsMinutes = Math.floor(this.formData.ts / 60);
-          this.tsSeconds = this.formData.ts % 60;
-        }
+      handler(newVal) {
+        this.formFields.forEach(field => {
+          if (field.dataType === 'ts' && newVal[field.prop] !== undefined) {
+            const totalSeconds = Number(newVal[field.prop]);
+            // 仅在值变化时更新,避免循环触发
+            if (
+                this.tsValues[field.prop]?.minutes !== Math.floor(totalSeconds / 60) ||
+                this.tsValues[field.prop]?.seconds !== totalSeconds % 60
+            ) {
+              this.$set(this.tsValues, field.prop, {
+                minutes: Math.floor(totalSeconds / 60),
+                seconds: totalSeconds % 60
+              });
+            }
+          }
+        });
       },
       deep: true
     }
   },
-  mounted() {
-    this.initFormData(this.formFields);
-  },
   methods: {
     initFormData(fields) {
       const newFormData = {};
+      const newTsValues = {};
+
       fields.forEach(field => {
+        // 初始化表单数据
         if (this.formData[field.prop] !== undefined) {
           newFormData[field.prop] = this.formData[field.prop];
         } else {
-          if (field.prop === 'ts') {
-            newFormData[field.prop] = 0;
-            this.tsMinutes = '';  // 初始化为空而不是0,强化非空约束
-            this.tsSeconds = '';
-          } else if (field.dataType === 'select' && field.options && field.options.length) {
-            newFormData[field.prop] = '';
-          } else if (field.dataType === 'number' || field.dataType === 'decimal') {
-            newFormData[field.prop] = 0;
-          } else {
-            newFormData[field.prop] = '';
-          }
+          newFormData[field.prop] = field.dataType === 'ts' ? 0 : '';
+        }
+
+        // 初始化时序字段的分秒值
+        if (field.dataType === 'ts') {
+          const totalSeconds = Number(newFormData[field.prop]) || 0;
+          newTsValues[field.prop] = {
+            minutes: Math.floor(totalSeconds / 60),
+            seconds: totalSeconds % 60
+          };
         }
       });
+
       this.formData = newFormData;
+      this.tsValues = newTsValues;
     },
 
     async loadFormData() {
-      // 保持原有逻辑不变
       if (!this.zbId) return;
       try {
         this.loading = true;
@@ -158,13 +165,7 @@ export default {
         });
         if (response.code === 0 && response.data) {
           const modelConfig = JSON.parse(response.data.modelConfig);
-          this.formData = {...this.formData, ...modelConfig};
-          if (modelConfig.ts !== undefined) {
-            this.tsMinutes = Math.floor(modelConfig.ts / 60);
-            this.tsSeconds = modelConfig.ts % 60;
-          }
-        } else {
-          this.$message.warning('未找到相关数据,将使用默认值');
+          this.formData = { ...this.formData, ...modelConfig };
         }
       } catch (error) {
         console.error('加载表单数据失败:', error);
@@ -174,168 +175,93 @@ export default {
       }
     },
 
-    getFieldPlaceholder(field) {
-      if (field.placeholder) return field.placeholder;
-      if (field.dataType === 'time') return `请输入时间 (如 01:30:00 或 25:15:45)`;
-      return `请输入${field.label}`;
+    // 处理时序字段输入变化(核心同步逻辑)
+    handleTsChange(prop) {
+      const ts = this.tsValues[prop];
+      if (!ts) return;
+
+      // 强制转换为数字,避免字符串计算
+      const minutes = Number(ts.minutes) || 0;
+      const seconds = Number(ts.seconds) || 0;
+
+      // 秒数自动进位处理(关键修复)
+      const validSeconds = seconds % 60;
+      const carryMinutes = Math.floor(seconds / 60);
+      const validMinutes = minutes + carryMinutes;
+
+      // 实时更新分秒值(确保响应式)
+      if (validMinutes !== minutes || validSeconds !== seconds) {
+        this.$set(this.tsValues, prop, {
+          minutes: validMinutes,
+          seconds: validSeconds
+        });
+      }
+
+      // 同步更新到表单数据
+      this.formData[prop] = validMinutes * 60 + validSeconds;
+    },
+
+    // 处理失焦事件,确保值有效
+    handleTsBlur(prop, type) {
+      const ts = this.tsValues[prop];
+      if (!ts) return;
+
+      // 失焦时如果为空,自动填充0
+      if (ts[type] === '' || ts[type] === null || isNaN(ts[type])) {
+        this.$set(ts, type, 0);
+        this.handleTsChange(prop); // 触发重新计算
+      }
     },
 
+    // 初始化验证规则
     initFormRules(fields) {
       this.formRules = {};
       fields.forEach(field => {
-        // 强化ts字段的非空验证规则
-        if (field.prop === 'ts') {
-          this.formRules['ts'] = [{
+        if (field.dataType === 'ts') {
+          this.formRules[field.prop] = [{
             validator: (rule, value, callback) => {
-              // 检查是否两个输入框都为空
-              if (this.tsMinutes === '' && this.tsSeconds === '') {
-                callback(new Error(`请至少输入分钟或秒数`));
-                return;
-              }
+              const ts = this.tsValues[field.prop];
+              const totalSeconds = (Number(ts.minutes) || 0) * 60 + (Number(ts.seconds) || 0);
 
-              // 验证分钟数有效性
-              if (this.tsMinutes !== '' && (isNaN(Number(this.tsMinutes)) || Number(this.tsMinutes) < 0)) {
-                callback(new Error(`请输入有效的分钟数(非负整数)`));
-                return;
-              }
-
-              // 验证秒数有效性
-              if (this.tsSeconds !== '' && (isNaN(Number(this.tsSeconds)) || Number(this.tsSeconds) < 0 || Number(this.tsSeconds) > 59)) {
-                callback(new Error(`请输入有效的秒数(0-59)`));
-                return;
-              }
-
-              // 验证总时长不能为0
-              const totalSeconds = (this.tsMinutes === '' ? 0 : Number(this.tsMinutes)) * 60 +
-                  (this.tsSeconds === '' ? 0 : Number(this.tsSeconds));
               if (totalSeconds <= 0) {
-                callback(new Error(`总时长必须大于0`));
-                return;
+                callback(new Error(`${field.label}总时长必须大于0`));
+              } else {
+                callback();
               }
-
-              callback();
             },
-            trigger: ['change', 'blur']  // 增加blur触发,提升用户体验
+            trigger: ['change', 'blur']
           }];
         }
-        // 其他字段验证规则保持不变
-        else if (field.dataType === 'time') {
-          // ... 保持原有逻辑
-        } else if (field.rules && field.rules.length) {
-          // ... 保持原有逻辑
-        }
       });
     },
 
-    getFieldProp(field) {
-      return field.prop === 'ts' ? 'ts' : field.prop;
-    },
-
+    // 其他方法保持不变...
     isFieldRequired(field) {
-      if (field.prop === 'ts') return true;  // 强制ts字段为必填
-      if (!field.rules || !field.rules.length) return false;
-      return field.rules.some(rule => rule.required);
+      return field.dataType === 'ts' || (field.rules?.some(rule => rule.required) || false);
     },
 
-    // 处理ts字段的分钟和秒变化
-    handleTsChange() {
-      // 确保值为有效数字
-      const minutes = this.tsMinutes === '' ? 0 : Number(this.tsMinutes);
-      const seconds = this.tsSeconds === '' ? 0 : Number(this.tsSeconds);
-
-      // 秒数边界处理
-      const validSeconds = seconds < 0 ? 0 : (seconds > 59 ? 59 : seconds);
-      if (validSeconds !== seconds) {
-        this.tsSeconds = validSeconds;
-      }
-
-      // 分钟数边界处理(确保非负)
-      const validMinutes = Math.max(0, minutes);
-      if (validMinutes !== minutes) {
-        this.tsMinutes = validMinutes;
-      }
-
-      // 计算总秒数并更新
-      this.formData.ts = validMinutes * 60 + validSeconds;
-      this.handleFieldChange('ts');
+    getFieldPlaceholder(field) {
+      return field.placeholder || `请输入${field.label}`;
     },
 
-    // 其他方法保持不变...
     getInputType(dataType) {
-      switch (dataType) {
-        case 'number':
-        case 'decimal':
-          return 'number';
-        case 'time':
-          return 'text';
-        default:
-          return 'text';
-      }
+      return ['number', 'decimal'].includes(dataType) ? 'number' : 'text';
     },
 
     shouldShowField(field) {
-      // 保持原有逻辑
-      if (!field.showWhen || !Array.isArray(field.showWhen) || field.showWhen.length === 0) {
-        return true;
-      }
-      return field.showWhen.every(condition => {
-        const {field: conditionField, operator, value} = condition;
-        const currentValue = this.formData[conditionField];
+      if (!field.showWhen || !field.showWhen.length) return true;
+      return field.showWhen.every(({ field: condField, operator, value }) => {
+        const currentValue = this.formData[condField];
         switch (operator) {
           case 'eq': return currentValue === value;
           case 'neq': return currentValue !== value;
           case 'gt': return Number(currentValue) > Number(value);
           case 'lt': return Number(currentValue) < Number(value);
-          case 'gte': return Number(currentValue) >= Number(value);
-          case 'lte': return Number(currentValue) <= Number(value);
-          case 'in': return Array.isArray(value) && value.includes(currentValue);
-          case 'nin': return Array.isArray(value) && !value.includes(currentValue);
-          case 'has': return typeof currentValue === 'string' && currentValue.includes(value);
           default: return true;
         }
       });
     },
 
-    handleFieldChange(fieldProp) {
-      // 保持原有逻辑
-      let dependentFields = this.formFields.filter(
-          field => field.showWhen && field.showWhen.some(cond => cond.field === fieldProp)
-      );
-      const selectFieldsWithDependentOptions = this.formFields.filter(field => {
-        if (field.dataType !== 'select' || !field.options) return false;
-        return field.options.some(option =>
-            option.showWhen && option.showWhen.some(cond => cond.field === fieldProp)
-        );
-      });
-      if (selectFieldsWithDependentOptions.length > 0) {
-        selectFieldsWithDependentOptions.forEach(selectField => {
-          this.$set(this.formData, selectField.prop, '');
-        });
-      }
-      dependentFields = [...new Set([...dependentFields, ...selectFieldsWithDependentOptions])];
-      if (dependentFields.length > 0 && this.$refs.dynamicForm) {
-        dependentFields.forEach(field => this.$refs.dynamicForm.clearValidate(field.prop));
-      }
-    },
-
-    show() {
-      this.visible = true;
-      this.$nextTick(() => {
-        if (this.$refs.dynamicForm) this.$refs.dynamicForm.clearValidate();
-        if (this.type === 'init') this.loadFormData();
-      });
-    },
-
-    handleClose() {
-      this.visible = false;
-      this.$emit('close');
-      this.resetForm();
-    },
-
-    handleCancel() {
-      this.handleClose();
-    },
-
     handleSubmit() {
       this.$refs.dynamicForm.validate(valid => {
         if (valid) {
@@ -344,27 +270,75 @@ export default {
             behavior = this.formFields.find(field => field.prop === 'behaviorName')?.options
                 ?.find(item => item.value === this.formData['behaviorName'])?.label;
           }
-          if (this.formData.ts !== undefined) {
-            this.formData.ts = Number(this.formData.ts);
+          if (this.formData.startTs !== undefined) {
+            if(this.formData.startTs > this.formData.endTs){
+              this.$message.warning('时序时间范围有误!')
+              return
+            }
           }
-          this.$emit('submit', this.formData, behavior);
+          this.$emit('submit', { ...this.formData }, behavior);
           this.handleClose();
-        } else {
-          this.$message.warning('请完善表单信息');
-          return false;
+        }
+      });
+    },
+    // 关键补充:show方法用于显示弹窗并初始化
+    show(initialData = {}) {
+      // 显示弹窗
+      this.visible = true;
+
+      // 重置表单状态
+      this.resetForm();
+
+      // 等待DOM更新后执行初始化
+      this.$nextTick(() => {
+        // 填充初始数据(如果有)
+        // if (Object.keys(initialData).length > 0) {
+        //   this.formData = { ...this.formData, ...initialData };
+        //
+        //   // 同步时序字段的分秒值
+        //   this.formFields.forEach(field => {
+        //     if (field.dataType === 'ts' && initialData[field.prop] !== undefined) {
+        //       const totalSeconds = Number(initialData[field.prop]);
+        //       this.$set(this.tsValues, field.prop, {
+        //         minutes: Math.floor(totalSeconds / 60),
+        //         seconds: totalSeconds % 60
+        //       });
+        //     }
+        //   });
+        // }
+        // 如果是初始化类型,加载远程数据
+        if (this.type === 'init') {
+          this.loadFormData();
+        }
+
+        // 清除之前的验证状态
+        if (this.$refs.dynamicForm) {
+          this.$refs.dynamicForm.clearValidate();
         }
       });
     },
 
     resetForm() {
+      // 1. 重置 formData(根据 formFields 重新初始化)
+      this.initFormData(this.formFields);
+
+      // 2. 重置 tsValues(分秒值)
+      this.tsValues = {};
+
+      // 3. 清除验证状态(如果表单已挂载)
       if (this.$refs.dynamicForm) {
-        this.$refs.dynamicForm.resetFields();
         this.$refs.dynamicForm.clearValidate();
-        this.formData = {};
-        this.formRules = {};
-        this.tsMinutes = '';
-        this.tsSeconds = '';
       }
+    },
+
+    handleClose() {
+      this.resetForm();
+      this.visible = false;
+      this.$emit('close');
+    },
+
+    handleCancel() {
+      this.handleClose();
     }
   }
 };
@@ -400,7 +374,7 @@ export default {
   margin: 0;
 }
 
-:v-deep input[type="number"] {
+::v-deep input[type="number"] {
   -moz-appearance: textfield;
 }
 

+ 3 - 3
src/views/Deduction/taskSetting/components/equipment/EquipmentListWithTimeline.vue

@@ -147,7 +147,7 @@ export default {
           id:t.id,
           time: `T0+${this.formatSeconds(t.seconds)}`,
           rawTime: t.ts,
-          title: t.ts === '00:00:00' ? '初始化' : t.behavior,
+          title: t.ts === '0' ? '初始化' : t.behavior,
           desc: `${t.zbName}`,
           zbType: t.zbType,
           typeClass: t.type === '动态' ? 'tl-danger' : 'tl-warn',
@@ -255,7 +255,7 @@ export default {
           const timingField = {
             "label": "时序时间",
             "prop": "ts",
-            "dataType": "time",  // 改为time类型,对应时间选择器
+            "dataType": "ts",  // 改为time类型,对应时间选择器
             "type": "DYNAMIC",
             "placeholder": "请选择时序时间",
             "required": true,
@@ -297,7 +297,7 @@ export default {
       } else {
         submitTs = formData.ts
 
-        subPlanZbZbSaveData({id: this.selectRow.id,isInit:1, simulationId: this.planId,ts:submitTs,behavior:behavior, interactionConfigData: formData}).then((res) => {
+        subPlanZbZbSaveData({id: this.selectRow.id,isInit:1, simulationId: this.planId,seconds:submitTs,behavior:behavior, interactionConfigData: formData}).then((res) => {
           if (res.code === 0) {
             this.fetchTsList()
             this.$message.success('配置已保存');

+ 8 - 7
src/views/Deduction/taskSetting/components/mission/MissileMissionCard.vue

@@ -1,5 +1,5 @@
 <template>
-    <el-form style="margin-top: 40px;padding: 0 40px" :model="local" disabled class="dense-form" label-width="120px" size="small">
+    <el-form style="margin-top: 40px;padding: 0 40px" :model="local" disabled class="dense-form" label-width="110px" size="small">
       <el-row :gutter="16">
         <!--<el-col :span="12">-->
         <!--  <el-form-item label="任务目标类型">-->
@@ -11,37 +11,37 @@
         <!--  </el-form-item>-->
         <!--</el-col>-->
 
-        <el-col :span="12">
+        <el-col :span="span">
           <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-col :span="span">
           <el-form-item label="攻击靶标">
             <el-input v-model.number="local.attackTheTarget"/>
           </el-form-item>
         </el-col>
 
-        <el-col :span="12">
+        <el-col :span="span">
           <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-col :span="span">
           <el-form-item label="攻击目标方式">
             <el-input v-model.number="local.attackTargetMethod"/>
           </el-form-item>
         </el-col>
 
-        <el-col :span="12">
+        <el-col :span="span">
           <el-form-item label="攻击目标经度">
             <el-input v-model="local.attackTargetLongitude" placeholder="lon,lat,alt(可选)"/>
           </el-form-item>
         </el-col>
 
-        <el-col :span="12">
+        <el-col :span="span">
           <el-form-item label="攻击目标纬度">
             <el-input v-model="local.attackTargetLatitude" placeholder="lon,lat,alt(可选)"/>
           </el-form-item>
@@ -74,6 +74,7 @@
 export default {
   name: 'MissileMissionCard',
   props: {
+    span:{type:Number,default:12},
     /**
      * 使用 .sync:<MissileMissionCard :missile.sync="m" />
      * 结构示例:

+ 20 - 308
src/views/Deduction/taskSetting/dedOrderEdit.vue

@@ -201,32 +201,8 @@
                 </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>
+                    <!-- 气象环境配置 -->
+                    <weather-env-form ref="weatherRef" v-model="weatherForm"/>
                   </el-collapse>
 
                 </div>
@@ -350,7 +326,7 @@
                       <div><i class="el-icon-time"></i> 完整事件时序</div>
                     </div>
                     <div class="timeline-wrap fill-scroll">
-                      <GanttChart :timelineData="filteredAllEvents" :default-duration="7200"></GanttChart>
+                      <GanttChart :timelineData="currentPreviewEvents" :default-duration="7200"></GanttChart>
                     </div>
                   </el-card>
                 </div>
@@ -390,9 +366,11 @@ import {getEquTree} from "@/api/faultSimulation";
 import DdUpload from "@/views/Deduction/taskSetting/components/dd-upload.vue";
 import {getSubPlanZbStatistics, getSubPlanZbTsList, getSubPlanZbZbList} from "@/api/subPlanZb";
 import {saveJson} from "@/api/deductionTask";
+import WeatherEnvForm from "@/views/decision/testBuild/components/WeatherEnvForm.vue";
 
 export default {
   components: {
+    WeatherEnvForm,
     DdUpload,
     MissileMissionCard,
     TargetSchemeInfo,
@@ -467,7 +445,13 @@ export default {
       openMissilePanels: [],
 
       activeEnvTab:['meteorology'],
-      weatherForm: {wind_speed: 3.5, wind_direction: 90, temperature: 25, visibility: 10},
+      /** 气象参数(作为 WeatherEnvForm 的 v-model) */
+      weatherForm: {
+        temp: 20, rh: 55, pressure: 1013,
+        wind: 5.5, windDir: 270, fallSpeed: 0.8,
+        airDensity: 1200, visibility: 5000, rain: 0,
+        weather: 'sunny'
+      },
       geoForm: {terrain: 'hilly', alt: 350, desc: ''},
 
       legend: {showMeasurement: true, showTarget: true, showJammer: true},
@@ -514,7 +498,6 @@ export default {
     },
     currentPreviewEvents() {
       const list = [];
-
       this.tsList.forEach(t => {
         if (!t.ts) return;
         list.push({
@@ -530,14 +513,14 @@ export default {
 
       return list.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime));
     },
-    filteredAllEvents() {
-      return this.currentPreviewEvents.filter(e => {
-        if (e.zbType === '2' && !this.legend.showMeasurement) return false;
-        if (e.zbType === '1' && !this.legend.showTarget) return false;
-        if (e.zbType === '3' && !this.legend.showJammer) return false;
-        return true;
-      });
-    }
+    // filteredAllEvents() {
+    //   return this.currentPreviewEvents.filter(e => {
+    //     if (e.zbType === '2' && !this.legend.showMeasurement) return false;
+    //     if (e.zbType === '1' && !this.legend.showTarget) return false;
+    //     if (e.zbType === '3' && !this.legend.showJammer) return false;
+    //     return true;
+    //   });
+    // }
   },
   watch: {
     missionMissileCount: {
@@ -632,28 +615,6 @@ export default {
       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);
     },
@@ -666,255 +627,6 @@ export default {
       }
       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,

+ 99 - 54
src/views/decision/testBuild/components/BasicInfo.vue

@@ -12,83 +12,96 @@
         ></i>
       </h4>
     </div>
+
     <el-form
-      :model="taskForm"
-      label-width="100px"
-      size="small"
-      class="p-4"
-      v-show="!isCollapsed"
+        :model="taskForm"
+        label-width="100px"
+        size="small"
+        class="p-4"
+        v-show="!isCollapsed"
     >
       <el-form-item label="任务名称" class="mb-3">
-        <el-input v-model="taskForm.name" placeholder="请输入任务名称" :disabled="true" />
+        <el-input v-model="taskForm.subTaskName" placeholder="请输入任务名称" :disabled="true" />
       </el-form-item>
       <el-form-item label="任务类型" class="mb-3">
-        <el-select v-model="taskForm.category" placeholder="请选择任务类别" :disabled="true">
-          <el-option label="保障任务" value="support" />
-          <el-option label="实战任务" value="combat" />
-        </el-select>
+        <vab-dict-select placeholder="请选择任务类型" v-model="taskForm.subTaskType" clearable :disabled="true" dict-code="task_type" />
       </el-form-item>
-      <el-form-item label="打击纬度" class="mb-3">
-        <el-select v-model="taskForm.type" placeholder="请选择任务类型" :disabled="true">
-          <el-option label="防空作战" value="air-defense" />
-          <el-option label="对海作战" value="sea-combat" />
-          <el-option label="对地打击" value="ground-strike" />
-          <el-option label="电子对抗" value="electronic-warfare" />
-        </el-select>
+      <el-form-item label="打击维度" class="fightAway">
+        <el-input v-model="taskForm.fightAway" placeholder="" :disabled="true" />
       </el-form-item>
-      <el-form-item label="导弹类型" class="mb-3">
-        <el-select
-          v-model="taskForm.missileType"
-          placeholder="请选择导弹类型"
-          @change="$emit('update-targets-by-missile-type', $event)"
-          :disabled="true"
+    </el-form>
+
+    <div style="padding: 10px">
+      <el-collapse v-model="openPanels">
+        <el-collapse-item
+            v-for="(m,idx) in missilesLocal"
+            :key="m.id"
+            :name="m.id"
+            :title="`导弹 #${idx+1}`"
         >
-          <el-option label="XXX子母弹" value="surface-to-air" />
-          <el-option label="空地导弹" value="air-to-surface" />
-          <el-option label="反舰导弹" value="anti-ship" />
-        </el-select>
-      </el-form-item>
+          <MissileMissionCard
+              :missile="missilesLocal[idx]"
+              :span="24"
+              @import-trajectory="$emit('import-trajectory', $event)"
+              @preview-trajectory="$emit('preview-trajectory', $event)"
+          />
+        </el-collapse-item>
+      </el-collapse>
+    </div>
+    <!--<el-alert :closable="false" class="mb-16" show-icon title="每枚导弹的打击目标与弹道参数" type="info"/>-->
 
 
-      <el-form-item :disabled="true" label="零点时刻" class="mb-3">
-        <el-date-picker
-          v-model="taskForm.TO"
-          type="datetime"
-          placeholder="请选择零点时刻"
-          format="yyyy-MM-dd HH:mm:ss"
-          value-format="yyyy-MM-dd HH:mm:ss"
-        >
-        </el-date-picker>
-      </el-form-item>
-    </el-form>
   </div>
 </template>
 
 <script>
+import MissileMissionCard from "./MissileMissionCard.vue";
+import {findSubTaskPage, missileList} from "@/api/taskMage/taskMage";
+import {findPageWrapper, getEquTree} from "@/api/faultSimulation";
+import VabDictSelect from "@/components/VabDictSelect/VabDictSelect.vue";
+
 export default {
   name: 'BasicInfo',
+  components: {VabDictSelect, MissileMissionCard},
   props: {
-    taskForm: {
-      type: Object,
-      required: true,
-      default: () => ({
-        TO:'',
-        name: '',
-        category: '',
-        type: '',
-        missileType: '',
-        missileCount: 0,
-        executeTime: '',
-        description: ''
-      })
-    }
+    taskId:{type: String, default: ''},
+    subTaskId:{type: String, default: ''},
+    // taskForm: {
+    //   type: Object,
+    //   required: true,
+    //   default: () => ({
+    //     TO:'',
+    //     name: '',
+    //     category: '',
+    //     type: '',
+    //     missileType: '',
+    //     missileCount: 0,
+    //     executeTime: '',
+    //     description: ''
+    //   })
+    // }
   },
   data() {
     return {
-      isCollapsed: false
+      isCollapsed: false,
+      missilesLocal: [],
+      openPanels: [],
+      taskForm: {},
     }
   },
+  mounted() {
+    this.fetchData()
+  },
   methods: {
+    fetchData(){
+      // 导弹列表
+      missileList({taskId:this.taskId,subTaskId:this.subTaskId}).then((res)=>{
+        this.missilesLocal = res.data
+      })
+      findSubTaskPage({id_EQ:this.subTaskId,pageNo:1,pageSize:1}).then((res)=>{
+        this.taskForm = res.data.rows[0]
+      })
+    },
     toggleCollapse() {
       this.isCollapsed = !this.isCollapsed;
     }
@@ -131,4 +144,36 @@ export default {
 .el-icon-arrow-right {
   color: #94a3b8;
 }
+
+/* 折叠项头部与父页风格一致 */
+::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;
+}
 </style>

+ 488 - 0
src/views/decision/testBuild/components/GanttChartWithEndTime.vue

@@ -0,0 +1,488 @@
+<template>
+  <div class="gantt-container">
+    <div class="chart-wrapper">
+      <div ref="ganttChart" class="gantt-chart" :style="{width: '100%', height: chartHeight + 'px'}"></div>
+    </div>
+    <div class="legend-container">
+      <div class="legend-items">
+        <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,
+      required: true,
+      default: 3600 // 默认1小时
+    },
+    timeMargin: {
+      type: Number,
+      default: 600 // 10分钟的时间边距
+    },
+    // 新增:每项高度和间隔的配置
+    itemHeight: {
+      type: Number,
+      default: 60 // 每项高度
+    },
+    itemSpacing: {
+      type: Number,
+      default: 20 // 项目间隔
+    }
+  },
+  data() {
+    return {
+      chart: null,
+      activeLegends: {},
+      timeOrderedItems: []
+    }
+  },
+  computed: {
+    // 修改:根据项目数量、每项高度和间隔计算总高度
+    chartHeight() {
+      if (this.timeOrderedItems.length === 0) {
+        return 300; // 默认高度
+      }
+      // 总高度 = 所有项目高度 + 所有间隔 + 顶部和底部边距
+      return this.timeOrderedItems.length * this.itemHeight +
+          (this.timeOrderedItems.length - 1) * this.itemSpacing +
+          100; // 额外边距
+    },
+    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: this.defaultDuration }
+      }
+
+      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)
+        // 如果有endTime,使用endTime计算结束时间,否则使用开始时间+默认持续时间
+        let endTime;
+        if (item.endTime) {
+          endTime = getTimeInSeconds(item.endTime)
+        } else {
+          endTime = startTime + (item.duration || this.defaultDuration);
+        }
+        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.max(this.defaultDuration, maxTime)
+      }
+    },
+
+    processedData() {
+      this.timeOrderedItems = this.getOrderedTimelineItems();
+
+      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
+      }
+
+      return this.timelineData.map(item => {
+        const startTime = getTimeInSeconds(item.rawTime)
+        // 计算持续时间:如果有endTime,使用endTime - startTime,否则使用duration或默认值
+        let duration, endTime;
+        if (item.endTime) {
+          endTime = getTimeInSeconds(item.endTime)
+          duration = endTime - startTime
+        } else {
+          duration = item.duration || this.defaultDuration;
+          endTime = startTime + duration;
+        }
+
+        // 根据id查找索引,确保唯一性
+        const yAxisIndex = this.timeOrderedItems.findIndex(
+            orderedItem => orderedItem.id === item.id
+        );
+
+        const displayTime = item.rawTime.startsWith('T0+') ? item.rawTime : `T0+${item.rawTime}`;
+        // 显示结束时间(如果有)
+        const endTimeDisplay = item.endTime ?
+            (item.endTime.startsWith('T0+') ? item.endTime : `T0+${item.endTime}`) :
+            `T0+${this.formatTime(endTime)}`;
+
+        const displayText = `${displayTime} - ${endTimeDisplay}\n${item.title || '无标题'} \n${item.desc || '无描述'}`
+
+        return {
+          id: item.id, // 保留id用于标识
+          name: item.name,
+          zbType: item.zbType,
+          value: [
+            yAxisIndex,
+            startTime,
+            endTime,
+            duration,
+            item.title
+          ],
+          itemStyle: {
+            color: this.typeColorMap[item.zbType] || '#ccc'
+          },
+          label: {
+            text: displayText,
+            color: this.getTextColor(this.typeColorMap[item.zbType] || '#ccc')
+          }
+        }
+      }).filter(item => {
+        if (Object.keys(this.activeLegends).length === 0) return true;
+        return this.activeLegends[item.zbType] === true;
+      });
+    }
+  },
+  watch: {
+    timelineData: { deep: true, handler: 'updateChart' },
+    typeColorMap: { deep: true, handler: 'updateChart' },
+    defaultDuration: { handler: 'updateChart' },
+    timeMargin: { handler: 'updateChart' },
+    activeLegends: { deep: true, handler: 'updateChart' },
+    // 👇 新增:监听 timeOrderedItems 变化
+    timeOrderedItems: {
+      handler(newVal, oldVal) {
+        // 如果内容变了(长度或引用变了),重新渲染
+        if (newVal !== oldVal || newVal.length !== oldVal.length) {
+          this.updateChart();
+        }
+      },
+      deep: true // 如果数组内对象变化也需要监听,可以加 deep
+    },
+    chartHeight: {
+      handler(newHeight, oldHeight) {
+        if (newHeight !== oldHeight && this.chart) {
+          this.chart.resize(); // 👈 关键!通知 ECharts 容器尺寸变了
+        }
+      }
+    }
+  },
+  methods: {
+    formatDuration(seconds) {
+      const hours = Math.floor(seconds / 3600);
+      const minutes = Math.floor((seconds % 3600) / 60);
+      const secs = seconds % 60;
+      return `${hours}h${minutes}m${secs}s`;
+    },
+
+    // 将秒数格式化为时间字符串
+    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 `${hours}:${minutes}:${secs}`;
+    },
+
+    // 根据id获取唯一的时间线项目
+    getOrderedTimelineItems() {
+      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
+      }
+
+      // 使用id作为唯一标识去重
+      const uniqueItems = Array.from(
+          new Map(this.timelineData.map(item =>
+              [item.id, item] // 用id作为map的key确保唯一性
+          )).values()
+      );
+
+      // 按时间和名称排序
+      return uniqueItems.sort((a, b) => {
+        const timeA = getTimeInSeconds(a.rawTime);
+        const timeB = getTimeInSeconds(b.rawTime);
+        if (timeA !== timeB) {
+          return timeA - timeB;
+        }
+        return a.name.localeCompare(b.name);
+      });
+    },
+
+    initActiveLegends(legends) {
+      const newActiveLegends = {};
+      legends.forEach(item => {
+        newActiveLegends[item.name] = this.activeLegends[item.name] ?? true;
+      });
+      this.activeLegends = newActiveLegends;
+    },
+
+    isLegendActive(legendName) {
+      return Object.keys(this.activeLegends).length === 0
+          ? true
+          : this.activeLegends[legendName] === true;
+    },
+
+    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
+
+      // 使用id对应的项目作为Y轴数据
+      const yAxisData = this.timeOrderedItems.map(item =>
+          `${item.name} (${item.rawTime})`
+      );
+
+      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: true,
+          formatter: (params) => {
+            const data = params.data;
+            return `
+              <div style="font-weight:bold">${data.name}</div>
+              <div>开始时间: ${formatTime(data.value[1])}</div>
+              <div>结束时间: ${formatTime(data.value[2])}</div>
+              <div>持续时间: ${this.formatDuration(data.value[3])}</div>
+            `;
+          }
+        },
+        grid: { left: '5%', right: '5%', top: '5%', bottom: '20px' },
+        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: yAxisData,
+          show: false, // 不显示Y轴标签
+          splitLine: { show: false }
+        },
+        series: [
+          {
+            type: 'custom',
+            data: this.processedData,
+            renderItem: (params, api) => {
+              const startTime = api.value(1);
+              const duration = api.value(3);
+              const categoryIndex = api.value(0);
+
+              const xAxisScale = this.timeRange.max - this.timeRange.min;
+              const chartWidth = this.chart.getWidth() * 0.85;
+              const startX = ((startTime - this.timeRange.min) / xAxisScale) * chartWidth;
+
+              let width = (duration / xAxisScale) * chartWidth;
+              const minWidth = Math.max(50, (this.defaultDuration / xAxisScale) * chartWidth * 0.5);
+              width = Math.max(width, minWidth);
+
+              // ✅ 关键修改:计算“平均分布”的 Y 位置,并添加顶部内边距
+              const totalItems = this.timeOrderedItems.length;
+              if (totalItems === 0) return;
+
+              // 获取图表的绘图区域高度(减去上下边距)
+              const gridHeight = this.chart.getHeight() * 0.9; // 假设上下各5%
+
+              // --- 新增:定义顶部内边距 ---
+              // 例如,设置为 itemHeight 的 0.5 倍,或一个固定像素值
+              const topPadding = this.itemHeight * 0.5; // 也可以用固定值,如 30
+              // --- 新增结束 ---
+
+              // 计算每个项目的“中心位置”:等分减去顶部内边距后的高度
+              // 可用于放置项目的有效高度
+              const effectiveHeight = gridHeight - topPadding;
+              const slotHeight = effectiveHeight / totalItems;
+              // 中心位置 = 顶部内边距 + (槽位高度 * (索引 + 0.5))
+              const yPos = topPadding + (slotHeight * (categoryIndex + 0.5)); // +0.5 使项目居中于槽位
+
+              const dataItem = this.processedData[params.dataIndex];
+
+              const rect = {
+                type: 'rect',
+                shape: {
+                  x: startX + (this.chart.getWidth() * 0.05),
+                  y: yPos - this.itemHeight / 2, // 居中对齐
+                  width: width,
+                  height: this.itemHeight,
+                  r: 4
+                },
+                style: api.style(),
+                z2: 0
+              };
+
+              const text = {
+                type: 'text',
+                x: startX + (this.chart.getWidth() * 0.05) + 10,
+                y: yPos,
+                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]};
+            }
+          }
+        ]
+      }
+
+      this.chart.setOption(option)
+    },
+
+    updateChart() {
+      this.$nextTick(() => {
+        if (this.chart) {
+          this.setChartOption();
+          this.chart.resize(); // 👈 强制重置尺寸,确保响应容器高度变化
+        } else {
+          this.initChart();
+        }
+      });
+    },
+
+    handleResize() {
+      if (this.chart) setTimeout(() => this.chart.resize(), 100)
+    },
+
+    handleLegendClick(name) {
+      this.activeLegends[name] = !this.activeLegends[name];
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      const container = this.$refs.ganttChart
+      if (!container.clientWidth || !container.clientHeight) {
+        container.style.width = '100%'
+      }
+      this.initChart()
+    })
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    if (this.chart) this.chart.dispose()
+  }
+}
+</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;
+  //overflow-x: auto;
+}
+
+.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: #FFF;
+  font-size: 14px;
+}
+</style>
+

+ 93 - 0
src/views/decision/testBuild/components/MissileMissionCard.vue

@@ -0,0 +1,93 @@
+<template>
+    <el-form style="margin-top: 40px;padding: 0 40px" :model="local" disabled class="dense-form" label-width="70px" size="small">
+      <el-row :gutter="16">
+        <el-col :span="span">
+          <el-form-item label="导弹类型">
+            <vab-dict-select placeholder="请选择任务类型" v-model="local.bulletType" clearable :disabled="true" dict-code="missile_type" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="span">
+          <el-form-item label="零点时间">
+            <el-input v-model="local.attackPeriod" placeholder="lon,lat,alt(可选)" :disabled="true"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="tool-row">
+        <el-button type="primary" :disabled="false" @click="$emit('import-trajectory', local)">导入弹道轨迹</el-button>
+      </div>
+    </el-form>
+</template>
+
+<script>
+import VabDictSelect from "@/components/VabDictSelect/VabDictSelect.vue";
+
+export default {
+  name: 'MissileMissionCard',
+  components: {VabDictSelect},
+  props: {
+    span:{type:Number,default:12},
+    /**
+     * 使用 .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>

+ 259 - 104
src/views/decision/testBuild/components/TargetEquipmentMap.vue

@@ -22,10 +22,9 @@
           <el-select
             v-model="filterDomain"
             clearable
-            placeholder="打击域"
+            placeholder="筛选打击域"
             size="small"
             class="mr8"
-            @change="onDomainChange"
           >
             <el-option label="对陆" value="对陆" />
             <el-option label="对海" value="对海" />
@@ -34,12 +33,11 @@
           <el-input
             v-model="filterKeyword"
             clearable
-            placeholder="关键事件"
+            placeholder="筛选关键事件"
             size="small"
+            icon="el-icon-search"
             class="mr8 w220"
-            @change="onKeywordChange"
           >
-            <i slot="prefix" class="el-icon-search" />
           </el-input>
 
           <el-button size="small" @click="resetFilters">重置</el-button>
@@ -65,28 +63,36 @@
         :header-cell-style="{background: 'rgba(30, 58, 138, 0.3)', color: '#bae6fd', borderColor: 'rgba(14, 165, 233, 0.2)'}"
         :row-style="{background: 'rgba(15, 23, 42, 0.5)', color: '#e0f2fe', borderColor: 'rgba(14, 165, 233, 0.1)'}"
       >
-        <el-table-column type="index" label="序号" align="center" width="80"/>
-        <el-table-column prop="strikeDomain" label="打击域"  align="center" min-width="100"/>
-        <el-table-column prop="timing" label="时序"  align="center" min-width="160"/>
-        <el-table-column prop="keyEvent" label="关键事件"  align="center" min-width="220" show-overflow-tooltip/>
-        <el-table-column label="操作" width="180" align="center" fixed="right">
+        <el-table-column type="index" label="序号" align="center" width="70"/>
+        <el-table-column prop="strikeDomain" label="打击域"  align="center">
           <template slot-scope="scope">
-            <el-tooltip content="查看" placement="top" :open-delay="200">
-              <el-button
-                type="text"
-                icon="el-icon-view"
-                class="p-1 text-blue-400 action-btn"
-                @click="openForm('view', scope.row)"
-              />
-            </el-tooltip>
-            <el-tooltip content="编辑" placement="top" :open-delay="200">
-              <el-button
-                type="text"
-                icon="el-icon-edit"
-                class="p-1 text-blue-400 action-btn"
-                @click="openForm('edit', scope.row)"
-              />
-            </el-tooltip>
+            {{ JSON.parse(scope.row.interactionConfig).strikeDomain }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="ts" label="时序"  align="center" min-width="160">
+          <template slot-scope="scope">
+            T0+{{ formatSeconds(scope.row.ts.split('-')[0])}} - T0+{{ formatSeconds(scope.row.ts.split('-')[1]) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="behavior" label="关键事件"  align="center" show-overflow-tooltip/>
+        <el-table-column label="操作" width="120" align="center">
+          <template slot-scope="scope">
+            <!--<el-tooltip content="查看" placement="top" :open-delay="200">-->
+            <!--  <el-button-->
+            <!--    type="text"-->
+            <!--    icon="el-icon-view"-->
+            <!--    class="p-1 text-blue-400 action-btn"-->
+            <!--    @click="openForm('view', scope.row)"-->
+            <!--  />-->
+            <!--</el-tooltip>-->
+            <!--<el-tooltip content="编辑" placement="top" :open-delay="200">-->
+            <!--  <el-button-->
+            <!--    type="text"-->
+            <!--    icon="el-icon-edit"-->
+            <!--    class="p-1 text-blue-400 action-btn"-->
+            <!--    @click="openForm('edit', scope.row)"-->
+            <!--  />-->
+            <!--</el-tooltip>-->
             <el-tooltip content="删除" placement="top" :open-delay="200">
               <el-button
                 type="text"
@@ -109,57 +115,30 @@
     </div>
 
     <!-- 新增抽屉:多选关键事件 -->
-    <el-drawer
-      title="新增时序(多选)"
+    <el-dialog
+      title="新增时序"
       :visible.sync="dlgAddVisible"
-      size="60%"
+      size="40%"
       :close-on-click-modal="false"
       :append-to-body="true"
       destroy-on-close
+      @close="handleDialogClose"
     >
       <div class="drawer-body">
-        <div class="drawer-toolbar">
-          <el-input
-            v-model.trim="addSearch"
-            size="small"
-            clearable
-            placeholder="搜索关键事件/时序/打击域"
-            class="w260 mr8"
-          >
-            <i slot="prefix" class="el-icon-search" />
-          </el-input>
-          <el-select v-model="addDomain" size="small" clearable placeholder="按打击域筛选" class="mr8">
-            <el-option label="对陆" value="对陆" />
-            <el-option label="对海" value="对海" />
-          </el-select>
-          <el-button size="small" @click="resetAddFilters">重置</el-button>
-        </div>
-
-        <el-table
-          ref="addTableRef"
-          :data="addFiltered"
-          height="420"
-          border
-          size="small"
-          @selection-change="onAddSelectionChange"
-          :header-cell-style="{background: 'rgba(30, 58, 138, 0.3)', color: '#bae6fd', borderColor: 'rgba(14, 165, 233, 0.2)'}"
-          :row-style="{background: 'rgba(15, 23, 42, 0.5)', color: '#e0f2fe', borderColor: 'rgba(14, 165, 233, 0.1)'}"
-        >
-          <el-table-column type="selection" width="50" />
-          <el-table-column prop="strikeDomain" label="打击域" width="80" align="center" />
-          <el-table-column prop="timing" label="时序" width="130" align="center" />
-          <el-table-column prop="keyEvent" label="关键事件" min-width="280" show-overflow-tooltip />
-        </el-table>
-
-        <div class="drawer-footer">
-          <span class="hint">已选择:{{ addSelection.length }} 条</span>
-          <div>
-            <el-button @click="dlgAddVisible=false">取 消</el-button>
-            <el-button type="primary" @click="submitAddBatch">确 定</el-button>
-          </div>
+        <div>
+          <el-cascader
+              ref="cascaderRef"
+              v-model="schemeEquId"
+              :options="cascaderOptions"
+              :props="{ emitPath: false, checkStrictly: true, expandTrigger: 'hover' }"
+              placeholder="请选择设备"
+              clearable
+              style="width: 100%;margin-bottom: 20px"
+              @change="handleCascaderChange"
+          />
         </div>
       </div>
-    </el-drawer>
+    </el-dialog>
 
     <!-- 统一表单弹窗(查看 / 编辑共用) -->
     <el-dialog
@@ -248,13 +227,30 @@
         <el-button v-if="!isViewMode" type="primary" @click="submitForm">保 存</el-button>
       </span>
     </el-dialog>
+
+    <dynamic-form-modal
+        ref="myFormModal"
+        title="装备初始化配置"
+        :form-fields="formFields"
+        :zbId="schemeEquId"
+        :planId="plan.id"
+        :type="selectType"
+        @submit="handleFormSubmit"
+        @close="handleFormClose"
+    ></dynamic-form-modal>
   </div>
 </template>
 
 <script>
+import {getEquTree} from "@/api/faultSimulation";
+import {deleteSubPlanZbTsList, getModelData, subPlanZbZbSaveData} from "@/api/subPlanZb";
+import DynamicFormModal from "@/views/Deduction/taskSetting/components/equipment/DynamicFormModal.vue";
+
 export default {
   name: 'TargetEquipmentMap',
+  components: {DynamicFormModal},
   props: {
+    plan:{type:Object,default:()=>{}},
     // 表格数据由父组件持有
     mapData: { type: Array, default: () => [] },
     // 关键事件总表(新增抽屉多选)
@@ -277,6 +273,11 @@ export default {
     return {
       isCollapsed: false,
 
+      equTree:[],
+      schemeEquId:'',
+      selectType:'',
+      formFields:[],
+
       // 检索条件
       filterDomain: '',
       filterKeyword: '',
@@ -318,6 +319,18 @@ export default {
     };
   },
   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
+        })) || []
+      }));
+    },
     isViewMode() {
       return this.dlgMode === 'view';
     },
@@ -328,8 +341,8 @@ export default {
       if (!this.hasAnyData) return [];
       const kw = (this.filterKeyword || '').trim();
       return this.mapData.filter(row => {
-        const okDomain = this.filterDomain ? String(row.strikeDomain) === this.filterDomain : true;
-        const okKey = kw ? String(row.keyEvent || '').includes(kw) : true;
+        const okDomain = this.filterDomain ? String(JSON.parse(row.interactionConfig).strikeDomain) === this.filterDomain : true;
+        const okKey = kw ? String(row.behavior || '').includes(kw) : true;
         return okDomain && okKey;
       });
     },
@@ -366,6 +379,7 @@ export default {
     }
   },
   mounted() {
+    this.fetchEqTree()
     console.log("eventCatalog",this.eventCatalog)
     this.$nextTick(this.calcTableHeight);
     if (window.ResizeObserver) {
@@ -384,43 +398,164 @@ export default {
     window.removeEventListener('resize', this.calcTableHeight);
   },
   methods: {
-    /* ========== 基础 ========== */
-    initForm() {
-      return {id: '', strikeDomain: '', timing: 'T', keyEvent: ''};
+    formatSeconds(seconds) {
+      // 确保输入是有效的数字,并取整数部分
+      const totalSeconds = Math.max(0, Math.floor(seconds)); // 处理负数或无效输入
+
+      const minutes = Math.floor(totalSeconds / 60);
+      const secs = totalSeconds % 60;
+
+      // 使用 padStart 确保秒数始终显示为两位数 (e.g., 05, 09, 12)
+      const paddedSeconds = String(secs).padStart(2, '0');
+
+      // 返回
+      return `${minutes}分${paddedSeconds}秒`;
     },
-    toggleCollapse() {
-      this.isCollapsed = !this.isCollapsed;
+    fetchEqTree(){
+      getEquTree({subTaskId:this.plan.subTaskId}).then((res)=>{
+        this.equTree = res.data
+      })
     },
-
-    /* ========== 表格检索 ========== */
-    onDomainChange(val) {
-      this.filterDomain = val || '';
-      this.$emit('search-change', {strikeDomain: this.filterDomain, keyEvent: (this.filterKeyword || '').trim()});
-      this.$nextTick(this.calcTableHeight);
+    // 弹窗关闭时触发(无论何种关闭方式)
+    handleDialogClose() {
+      // 1. 清空绑定值(核心)
+      this.schemeEquId = '';
+      // 2. (可选)清空级联选择器内部状态,避免残留选中样式
+      const cascader = this.$refs.cascaderRef;
+      if (cascader) {
+        cascader.clearCheckedNodes(); // 清除级联选中节点
+      }
     },
-    onKeywordChange(val) {
-      this.filterKeyword = (val || '').trim();
-      this.$emit('search-change', {strikeDomain: this.filterDomain, keyEvent: this.filterKeyword});
-      this.$nextTick(this.calcTableHeight);
+    // 添加这个方法来处理级联选择器变化事件
+    handleCascaderChange() {
+      this.addTiming()
+      // 获取级联选择器实例
+      const cascader = this.$refs.cascaderRef;
+      if (cascader && cascader.dropDownVisible) {
+        // 关闭下拉面板
+        cascader.dropDownVisible = false;
+      }
     },
-    resetFilters() {
-      this.filterDomain = '';
-      this.filterKeyword = '';
-      this.$emit('search-change', {strikeDomain: '', keyEvent: ''});
-      this.$nextTick(this.calcTableHeight);
+    addTiming() {
+      this.selectType = 'add'
+      getModelData({id: this.schemeEquId,isInit:1}).then(res => {
+        if (res.code === 0 && res.data ) {
+          // 筛选出动态类型的字段
+          let dynamicFields = res.data.interactionConfigForm.filter(item => item.prop !== 'ts');
+
+          // 创建额外字段
+          const timingField = [{
+            "label": "时序开始时间",
+            "prop": "startTs",
+            "dataType": "ts",  // 改为time类型,对应时间选择器
+            "placeholder": "请选择时序时间",
+            "required": true,
+            "options": null,
+            "attrs": null,
+            "rules": [
+              {
+                "required": true,
+                "message": "请选择时序时间",
+                "trigger": "change"  // 时间选择器使用change触发验证
+              }
+            ]
+          },
+            {
+              "label": "时序结束时间",
+              "prop": "endTs",
+              "dataType": "ts",  // 改为time类型,对应时间选择器
+              "placeholder": "请选择时序时间",
+              "required": true,
+              "options": null,
+              "attrs": null,
+              "rules": [
+                {
+                  "required": true,
+                  "message": "请选择时序时间",
+                  "trigger": "change"  // 时间选择器使用change触发验证
+                }
+              ]
+            },
+            {
+              "label": "打击域",
+              "prop": "strikeDomain",
+              "dataType": "select",
+              "placeholder": null,
+              "required": true,
+              "options": [
+                {
+                  "label": "对海",
+                  "value": "对海",
+                },
+                {
+                  "label": "对陆",
+                  "value": "对陆",
+                },
+              ],
+              "showWhen": null,
+              "attrs": null,
+              "rules": [
+                {
+                  "required": true,
+                  "message": "请选择触发动作",
+                  "trigger": "change"
+                }
+              ]
+            }
+          ];
+
+          // 在最前面插入额外字段
+          this.formFields = [...timingField, ...dynamicFields];
+
+          // 显示弹窗
+          this.$refs.myFormModal.show();
+        } else {
+          this.$message.error('获取表单配置失败');
+        }
+      }).catch(err => {
+        console.error('获取表单数据失败', err);
+      });
     },
+    // 处理表单提交
+    handleFormSubmit(formData,behavior) {
+      console.log('表单提交数据:', formData);
+      let submitTs = ''
+      if (this.selectType === 'init') {
+        submitTs = '00:00:00'
+        subPlanZbZbSaveData({id: this.schemeEquId, isInit:0, simulationId: this.plan.id,ts:submitTs, params: formData}).then((res) => {
+          if (res.code === 0) {
+            this.$emit('update');
+            this.$message.success('配置已保存');
+          }
+        })
+      } else {
+        submitTs = formData.startTs + '-' + formData.endTs
+        subPlanZbZbSaveData({id: this.schemeEquId,isInit:1, simulationId: this.plan.id,ts:submitTs,behavior:behavior, interactionConfigData: formData}).then((res) => {
+          if (res.code === 0) {
+            this.$emit('update');
+            this.$message.success('配置已保存');
+          }
+        })
+      }
 
-    /* ========== 表格高度计算 ========== */
-    calcTableHeight() {
-      const bodyEl = this.$refs.bodyRef;
-      if (!bodyEl || this.isCollapsed) return;
-      const bodyH = bodyEl.clientHeight || 0;
-      const toolbarH = this.$refs.toolbarRef ? this.$refs.toolbarRef.offsetHeight : 0;
-      const usable = Math.max(0, bodyH - toolbarH);
-      const h = Math.floor(usable * 0.6);
-      this.tableHeight = Math.max(h, 240);
+      this.dlgAddVisible = false
     },
 
+    // 处理表单关闭
+    handleFormClose() {
+      console.log('表单已关闭');
+    },
+    /* ========== 基础 ========== */
+    initForm() {
+      return {id: '', strikeDomain: '', timing: 'T', keyEvent: ''};
+    },
+    toggleCollapse() {
+      this.isCollapsed = !this.isCollapsed;
+    },
+    resetFilters(){
+      this.filterDomain = null
+      this.filterKeyword =null
+    },
     /* ========== 新增(抽屉) ========== */
     openAdd() {
       this.addSearch = '';
@@ -543,10 +678,30 @@ export default {
 
     /* ========== 删除 ========== */
     confirmDelete(row) {
-      this.$confirm('确定删除该条时序吗?', '提示', {type: 'warning'})
-        .then(() => this.$emit('delete', row))
-        .catch(() => {
+      // 使用 Element UI 的 $confirm 方法显示确认弹窗
+      this.$confirm(`确定要删除该项吗?`, '删除确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning' // 设置弹窗类型为警告
+      }).then(() => {
+        // 用户点击了“确定”
+        deleteSubPlanZbTsList({ id: row.id }).then((res) => {
+          if (res.code === 0) {
+            this.$message.success('删除成功');
+            // 刷新列表
+            this.$emit('update');
+          } else {
+            // 处理删除失败的情况
+            this.$message.error(res.msg || '删除失败');
+          }
+        }).catch((err) => {
+          // 处理请求异常
+          console.error('删除请求出错:', err);
         });
+      }).catch(() => {
+        // 用户点击了“取消”或关闭了弹窗
+        this.$message.info('已取消删除');
+      });
     },
 
     /* ========== 工具 ========== */
@@ -634,7 +789,7 @@ export default {
 .drawer-body {
   display: flex;
   flex-direction: column;
-  height: 100%;
+  //height: 100%;
 }
 
 .drawer-toolbar {

+ 12 - 0
src/views/decision/testBuild/components/WeatherEnvForm.vue

@@ -294,4 +294,16 @@ export default {
 .target-list { padding: 12px; }
 .block-title { color:#93c5fd; font-weight:600; margin: 6px 0 12px; }
 .dense-form ::v-deep .el-form-item { margin-bottom: 10px; }
+
+/* 隐藏数字输入框的默认箭头 */
+::v-deep input::-webkit-inner-spin-button,
+::v-deep input::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+  appearance: none;
+  margin: 0;
+}
+
+::v-deep input[type="number"] {
+  -moz-appearance: textfield;
+}
 </style>

+ 1 - 1
src/views/decision/testBuild/index.vue

@@ -498,7 +498,7 @@ export default {
     handleEditScenario(plan) {
       this.$router.push({
         path: '/testBuild/orderEdit',
-        query: {planId: plan.id}
+        query: {plan: JSON.stringify(plan)}
       })
     },
 

+ 127 - 89
src/views/decision/testBuild/orderEdit.vue

@@ -54,19 +54,19 @@
             <div class="equipment-status-grid p-4 grid grid-cols-4 gap-3">
               <div class="status-card p-3 border border-blue-500/30 rounded">
                 <div class="status-title text-sm text-gray-400 mb-1">总装备数</div>
-                <div class="status-value text-2xl font-bold">{{ totalEquipment }}</div>
+                <div class="status-value text-2xl font-bold">{{ stastic.sumTotal }}</div>
               </div>
               <div class="status-card p-3 border border-green-500/30 rounded">
                 <div class="status-title text-sm text-gray-400 mb-1">测量装备数</div>
-                <div class="status-value text-2xl font-bold text-green-400">{{ measurementEquipmentCount }}</div>
+                <div class="status-value text-2xl font-bold text-green-400">{{ stastic.cc }}</div>
               </div>
               <div class="status-card p-3 border border-purple-500/30 rounded">
                 <div class="status-title text-sm text-gray-400 mb-1">干扰装备数</div>
-                <div class="status-value text-2xl font-bold text-purple-400">{{ jammingEquipmentCount }}</div>
+                <div class="status-value text-2xl font-bold text-purple-400">{{ stastic.gr }}</div>
               </div>
               <div class="status-card p-3 border border-red-500/30 rounded">
                 <div class="status-title text-sm text-gray-400 mb-1">靶标装备数</div>
-                <div class="status-value text-2xl font-bold text-red-400">{{ targetEquipmentCount }}</div>
+                <div class="status-value text-2xl font-bold text-red-400">{{ stastic.bb }}</div>
               </div>
             </div>
           </div>
@@ -79,14 +79,14 @@
                 任务甘特图
               </h4>
               <div>
-                <el-input v-model="taskId" placeholder="任务ID(可选)" size="mini"
-                          style="width:220px;margin-right:8px;"/>
-                <el-button size="mini" @click="useDefaultGantt">默认数据</el-button>
-                <el-button :loading="loadingGantt" size="mini" type="primary" @click="fetchGanttFromApi">
-                  {{ loadingGantt ? '加载中...' : '接口拉取' }}
-                </el-button>
-                <span class="meta" style="margin-left:8px;">T0: {{ t0Display || '-' }}</span>
-                <span class="meta" style="margin-left:8px;">事件数: {{ ganttTimelineData.length }}</span>
+                <!--<el-input v-model="taskId" placeholder="任务ID(可选)" size="mini"-->
+                <!--          style="width:220px;margin-right:8px;"/>-->
+                <!--<el-button size="mini" @click="useDefaultGantt">默认数据</el-button>-->
+                <!--<el-button :loading="loadingGantt" size="mini" type="primary" @click="fetchGanttFromApi">-->
+                <!--  {{ loadingGantt ? '加载中...' : '接口拉取' }}-->
+                <!--</el-button>-->
+                <!--<span class="meta" style="margin-left:8px;">T0: {{ t0Display || '-' }}</span>-->
+                <span class="meta" style="margin-left:8px;">事件数: {{ currentPreviewEvents.length }}</span>
               </div>
             </div>
             <div class="gantt-body">
@@ -94,7 +94,7 @@
               <GanttChart
                 :default-duration="120"
                 :time-margin="300"
-                :timeline-data="ganttTimelineData"
+                :timeline-data="currentPreviewEvents"
                 :type-color-map="ganttTypeColorMap"
               />
               <div v-if="ganttError" class="meta" style="color:#ff6b6b;margin-top:6px;">{{ ganttError }}</div>
@@ -106,8 +106,7 @@
             <TargetEquipmentMap
               :map-data="timingPlanData"
               :event-catalog="allKeyEvents"
-              @create-batch="onSeqCreateBatch"
-              @create="onSeqCreate"
+              :plan="plan"
               @update="onSeqUpdate"
               @delete="onSeqDelete"
               @search-change="onSeqSearch"
@@ -123,12 +122,13 @@
           <div class="task-content">
             <!-- 任务基本信息 -->
             <task-basic-info
-              :task-form="taskForm"
+              :sub-task-id="plan.subTaskId"
+              :task-id="plan.taskId"
               @update-targets-by-missile-type="updateTargetsByMissileType"
               @update-targets-by-missile-count="updateTargetsByMissileCount"
             />
             <!-- 理论抛洒点配置 -->
-            <runway-drop-setting ref="dropForm" v-model="runwayDrop"/>
+            <!--<runway-drop-setting ref="dropForm" v-model="runwayDrop"/>-->
             <!-- 气象环境配置 -->
             <weather-env-form ref="weatherRef" v-model="weather"/>
             <!-- 理论数据导入(触发弹窗) -->
@@ -210,7 +210,11 @@ import WeatherEnvForm from './components/WeatherEnvForm.vue'
 import Trajectory from './components/Trajectory.vue'
 import ExcelImportDialog from './components/ExcelImportDialog.vue'
 import TargetEquipmentMap from './components/TargetEquipmentMap.vue'
-import GanttChart from './components/GanttChart.vue'
+import GanttChart from './components/GanttChartWithEndTime.vue'
+import {getEquTree} from "@/api/faultSimulation";
+import {getSubPlanZbStatistics, getSubPlanZbTsList} from "@/api/subPlanZb";
+import {battlefieldEnvironmentInsert, getBattlefieldEnvironment} from "@/api/battlefieldEnvironment";
+import {saveJson} from "@/api/deductionTask";
 
 /** 默认甘特数据(示例) */
 const DEFAULT_GANTT_PAYLOAD = {
@@ -249,12 +253,9 @@ export default {
   },
   data() {
     return {
+      plan:JSON.parse(this.$route.query.plan),
       /** 表格数据:由父组件持有与更新 */
-      timingPlanData: [
-        { id: 'E1', strikeDomain: '对陆', timing: 'T+30',  keyEvent: '测量系统上电' },
-        { id: 'E3', strikeDomain: '对海', timing: 'T+90',  keyEvent: '干扰装备启动' },
-        { id: 'E8', strikeDomain: '对海', timing: 'T+240', keyEvent: '拦截评估' }
-      ],
+      timingPlanData: [],
       /** 关键事件总表:用于“新增抽屉”里多选 */
       allKeyEvents: [
         { id: 'E1', strikeDomain: '对陆', timing: 'T+30',  keyEvent: '测量系统上电' },
@@ -271,9 +272,15 @@ export default {
 
       /** 气象参数(作为 WeatherEnvForm 的 v-model) */
       weather: {
-        temp: 20, rh: 55, pressure: 1013,
-        wind: 5.5, windDir: 270, fallSpeed: 0.8,
-        airDensity: 1200, visibility: 5000, rain: 0,
+        temp: 20,
+        rh: 55,
+        pressure: 1013,
+        wind: 5.5,
+        windDir: 270,
+        fallSpeed: 0.8,
+        airDensity: 1200,
+        visibility: 5000,
+        rain: 0,
         weather: 'sunny'
       },
 
@@ -420,7 +427,9 @@ export default {
           ]
         }
       ],
-      defaultProps: {children: 'children', label: 'label'},
+      defaultProps: {children: 'equList', label: 'name'},
+      // 统计数据
+      stastic:{},
 
       /** 装备参数对话框 */
       selectedEquipment: null,
@@ -434,37 +443,25 @@ export default {
   },
   computed: {
     ...mapGetters('tTime', ['currentT0HMS', 'isRealtime']),
-
-    /** 数量统计 */
-    measurementEquipmentCount() {
-      let c = 0;
-      this.equipmentTree.forEach(k => {
-        if (k.id === 'category1' && k.children) c += k.children.length
-      })
-      return c
-    },
-    jammingEquipmentCount() {
-      let c = 0;
-      this.equipmentTree.forEach(k => {
-        if (k.id === 'category2' && k.children) c += k.children.length
-      })
-      return c
-    },
-    targetEquipmentCount() {
-      let c = 0;
-      this.equipmentTree.forEach(k => {
-        if (k.id === 'category3' && k.children) c += k.children.length
-      })
-      return c
+    // 时序图格式化数据
+    currentPreviewEvents() {
+      const list = [];
+      this.timingPlanData.forEach(t => {
+        if (!t.ts) return;
+        list.push({
+          id:t.id,
+          time: `T0+${this.formatSeconds(t.ts.split('-')[0])}`,
+          endTime: `T0+${this.formatSeconds(t.ts.split('-')[1])}`,
+          name: t.zbName,
+          rawTime: this.formatSeconds(t.seconds),
+          title: t.seconds === '0' ? '初始化' : t.behavior,
+          desc: `${t.zbName}`,
+          zbType: this.$getDictNameByValue('zb_type', t.zbType),
+        });
+      });
+
+      return list.sort((a, b) => this.hhmmssToSec(a.rawTime) - this.hhmmssToSec(b.rawTime));
     },
-    totalEquipment() {
-      let c = 0;
-      this.equipmentTree.forEach(k => {
-        if (k.children && k.children.length) c += k.children.length
-      })
-      return c
-    },
-
     /** T0 可读显示 */
     t0Display() {
       if (!this.t0Ms) return ''
@@ -480,10 +477,60 @@ export default {
     }
   },
   mounted() {
+    this.fetchEqTree()
+    this.fetchEquCount()
+    this.fetchTsList()
+    this.getEnvDate()
     // 默认加载一份甘特示例数据,避免空白
     this.useDefaultGantt()
   },
   methods: {
+    formatSeconds(seconds) {
+      // 确保输入是有效的数字,并取整数部分
+      const totalSeconds = Math.max(0, Math.floor(seconds)); // 处理负数或无效输入
+
+      // 计算小时、分钟和秒
+      const hours = Math.floor(totalSeconds / 3600);
+      const minutes = Math.floor((totalSeconds % 3600) / 60);
+      const secs = totalSeconds % 60;
+
+      // 使用 padStart 确保分钟和秒始终显示为两位数 (e.g., 05, 09, 12)
+      const paddedMinutes = String(minutes).padStart(2, '0');
+      const paddedSeconds = String(secs).padStart(2, '0');
+
+      // 返回格式: "Hhmmss" 或 "HH:mm:ss"
+      // 这里采用更标准的 HH:mm:ss 格式
+      return `${hours}:${paddedMinutes}:${paddedSeconds}`;
+    },
+    hhmmssToSec(hms) {
+      const [h = 0, m = 0, s = 0] = (hms || '00:00:00').split(':').map(n => parseInt(n, 10) || 0);
+      return h * 3600 + m * 60 + s;
+    },
+    getEnvDate(){
+      // 获取环境信息
+      getBattlefieldEnvironment({simulationId:this.plan.id}).then((res)=>{
+        if(res.data?.environmentJson){
+          this.weather = JSON.parse(res.data?.environmentJson)
+        }else {
+
+        }
+      })
+    },
+    fetchEqTree(){
+      getEquTree({subTaskId:this.plan.subTaskId}).then((res)=>{
+        this.equipmentTree = res.data
+      })
+    },
+    fetchEquCount(){
+      getSubPlanZbStatistics({schemeId:this.plan.schemeId}).then((res)=>{
+        this.stastic = res.data;
+      })
+    },
+    fetchTsList() {
+      getSubPlanZbTsList({simulationId: this.plan.id}).then((res) => {
+        this.timingPlanData = res.data
+      })
+    },
     /** 检索条件变更(如果要联动后端,在这里调接口;不需要就当日志看) */
     onSeqSearch({ strikeDomain, keyEvent }) {
       // 这里通常发请求:/api/xxx?domain=strikeDomain&kw=keyEvent
@@ -491,25 +538,9 @@ export default {
       // console.log('检索条件:', strikeDomain, keyEvent)
     },
 
-    /** 抽屉“确定”批量新增 */
-    onSeqCreateBatch(rows) {
-      // rows 形如:[{ id, strikeDomain, timing, keyEvent }, ...]
-      this.timingPlanData = this.deDupRows([...this.timingPlanData, ...rows])
-      this.$message.success(`已新增 ${rows.length} 条时序`)
-    },
-
-    /** 单条新增(子组件也会逐条向后兼容触发) */
-    onSeqCreate(row) {
-      this.timingPlanData = this.deDupRows([...this.timingPlanData, row])
-    },
-
     /** 编辑保存 */
-    onSeqUpdate(payload) {
-      // payload:{ id, strikeDomain, timing, keyEvent }
-      this.timingPlanData = this.timingPlanData.map(r =>
-        r.id === payload.id ? { ...r, ...payload } : r
-      )
-      this.$message.success('已保存修改')
+    onSeqUpdate() {
+      this.fetchTsList();
     },
 
     /** 删除 */
@@ -736,21 +767,29 @@ export default {
     },
 
     /** 任务保存(示例:初始化 T0、连接 WS) */
-    saveTask() {
+    async saveTask() {
       if (this.taskForm.targets.length === 0) {
         this.$message.error('靶标设置不能为空');
         return
       }
-      this.$store.dispatch('tTime/initFixedT0', 0)
-      this.$ws.connect('telemetry', 'ws://127.0.0.1:9000/telemetry')
-      if (this.wsOff) this.wsOff()
-      this.offMsg = this.$ws.onMessage('telemetry', () => {
-      })
-      if (this.wsOff) this.wsOff()
-      this.wsOff = this.$ws.onMessage('telemetry', (msg) => {
-        this.$store.dispatch('tTime/ingestFromWs', JSON.parse(msg))
-      })
-      this.$message.success('任务保存成功')
+      try {
+        // 保存环境配置
+        await battlefieldEnvironmentInsert({simulationId:this.plan.id,environmentJson:JSON.stringify(this.weatherForm)})
+        await saveJson({id:this.plan.id});
+        this.$message.success('任务保存成功')
+      } catch (e) {
+
+      }
+      // this.$store.dispatch('tTime/initFixedT0', 0)
+      // this.$ws.connect('telemetry', 'ws://127.0.0.1:9000/telemetry')
+      // if (this.wsOff) this.wsOff()
+      // this.offMsg = this.$ws.onMessage('telemetry', () => {
+      // })
+      // if (this.wsOff) this.wsOff()
+      // this.wsOff = this.$ws.onMessage('telemetry', (msg) => {
+      //   this.$store.dispatch('tTime/ingestFromWs', JSON.parse(msg))
+      // })
+
     },
 
     /** 返回、重置 */
@@ -938,11 +977,10 @@ export default {
 .equipment-content-box {
   background-color: rgba(5, 12, 26, 0.9);
   min-height: 0;
-  overflow: hidden;
+  overflow-y: auto;
 }
 
 .middle-layout {
-  display: grid;
   height: 100%;
   grid-template-rows:20% 40% 40%;
 }
@@ -953,7 +991,7 @@ export default {
 
 /* 甘特容器 */
 .gantt-body {
-  height: 100%;
+  //height: 300px;
   padding: 8px 12px 12px 12px;
 }
 

+ 1 - 1
src/views/planAndDeduction/childsTask/index.vue

@@ -158,7 +158,7 @@
             </el-col>
             <el-col :span="12">
               <el-form-item label="弹种">
-                <el-input v-model="taskForm.bulletType" placeholder="请输入弹种"></el-input>
+                <vab-dict-select placeholder="请选择" v-model="taskForm.bulletType" clearable dict-code="missile_type" />
               </el-form-item>
             </el-col>
             <el-col :span="12">

+ 1 - 1
src/views/planAndDeduction/taskMage/taskCreate.vue

@@ -382,7 +382,7 @@
                 </el-col>
                 <el-col :span="12">
                   <el-form-item label="弹种">
-                    <el-input v-model="childsForm.bulletType" placeholder="请输入弹种"></el-input>
+                    <vab-dict-select placeholder="请选择" v-model="taskForm.bulletType" clearable dict-code="missile_type" />
                   </el-form-item>
                 </el-col>
                 <el-col :span="12">

+ 1 - 1
src/views/planAndDeduction/taskMage/taskMage.vue

@@ -296,7 +296,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="弹种">
-              <el-input v-model="childsForm.bulletType" placeholder="请输入弹种"></el-input>
+              <vab-dict-select placeholder="请选择" v-model="childsForm.bulletType" clearable dict-code="missile_type" />
             </el-form-item>
           </el-col>
           <el-col :span="12">