learshaw преди 4 месеца
родител
ревизия
d9ba95664c
променени са 3 файла, в които са добавени 558 реда и са изтрити 677 реда
  1. 0 37
      ems-ui-cloud/src/api/mgr/energyStrategy.js
  2. 542 629
      ems-ui-cloud/src/views/mgr/strategy/components/StepConfig.vue
  3. 16 11
      ems-ui-cloud/src/views/mgr/strategy/log.vue

+ 0 - 37
ems-ui-cloud/src/api/mgr/energyStrategy.js

@@ -183,43 +183,6 @@ export function saveStrategyStepBatch(steps) {
 }
 
 // ===========================
-// 上下文变量管理
-// 注意:需要后端新增这些接口
-// ===========================
-
-/**
- * 获取策略上下文变量
- */
-export function getStrategyContext(strategyCode) {
-  return request({
-    url: '/ems/energyStrategy/context',
-    method: 'get',
-    params: { strategyCode }
-  })
-}
-
-/**
- * 保存上下文变量
- */
-export function saveStrategyContext(data) {
-  return request({
-    url: '/ems/energyStrategy/context',
-    method: 'post',
-    data: data
-  })
-}
-
-/**
- * 删除上下文变量
- */
-export function deleteStrategyContext(id) {
-  return request({
-    url: '/ems/energyStrategy/context/' + id,
-    method: 'delete'
-  })
-}
-
-// ===========================
 // 策略参数管理
 // ===========================
 

+ 542 - 629
ems-ui-cloud/src/views/mgr/strategy/components/StepConfig.vue

@@ -1,40 +1,37 @@
 <template>
-  <div class="step-config">
-    <el-form :model="form" label-width="90px" size="small">
-      <el-form-item label="步骤名称">
+  <el-dialog
+    :title="dialogTitle"
+    :visible.sync="visible"
+    width="900px"
+    @close="handleClose"
+    :close-on-click-modal="false"
+  >
+    <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+      <!-- 步骤名称 -->
+      <el-form-item label="步骤名称" prop="stepName">
         <el-input v-model="form.stepName" placeholder="请输入步骤名称" />
       </el-form-item>
 
-      <el-form-item label="步骤代码">
-        <el-input v-model="form.stepCode" placeholder="自动生成" disabled />
-      </el-form-item>
-
-      <el-form-item label="步骤类型">
-        <el-tag :type="getStepTypeTag(form.stepType)" size="medium">
-          <i :class="getStepTypeIcon(form.stepType)" style="margin-right: 4px"></i>
-          {{ getStepTypeName(form.stepType) }}
-        </el-tag>
+      <!-- 步骤类型 -->
+      <el-form-item label="步骤类型" prop="stepType">
+        <el-select v-model="form.stepType" placeholder="请选择步骤类型" @change="handleStepTypeChange">
+          <el-option label="能力调用" value="ABILITY" />
+          <el-option label="延时等待" value="DELAY" />
+          <el-option label="条件判断" value="CONDITION" />
+          <el-option label="循环执行" value="LOOP" />
+          <el-option label="属性查询" value="ATTR_QUERY" />
+        </el-select>
       </el-form-item>
 
-      <!-- 能力调用配置 -->
+      <!-- ==================== 能力调用配置 ==================== -->
       <template v-if="form.stepType === 'ABILITY'">
-        <el-divider content-position="left">目标配置</el-divider>
-
-        <el-form-item label="对象类型">
-          <el-radio-group v-model="form.targetObjType" @change="handleObjTypeChange">
-            <el-radio :label="2">设备</el-radio>
-            <el-radio :label="3">系统</el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <!-- 设备选择 -->
-        <el-form-item label="选择设备" v-if="form.targetObjType === 2">
+        <!-- 目标设备 -->
+        <el-form-item label="目标设备" prop="targetObjCode">
           <el-select
             v-model="form.targetObjCode"
+            placeholder="请选择目标设备"
             filterable
-            placeholder="搜索选择设备"
-            style="width: 100%"
-            @change="handleObjChange"
+            @change="handleDeviceChange"
           >
             <el-option
               v-for="device in deviceList"
@@ -43,45 +40,18 @@
               :value="device.deviceCode"
             >
               <span>{{ device.deviceName }}</span>
-              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ device.deviceCode }}</span>
+              <span style="float: right; color: #8492a6; font-size: 13px">
+                {{ device.deviceCode }}
+              </span>
             </el-option>
           </el-select>
         </el-form-item>
 
-        <!-- 系统选择 -->
-        <el-form-item label="选择系统" v-else-if="form.targetObjType === 3">
-          <el-select
-            v-model="form.targetObjCode"
-            filterable
-            placeholder="搜索选择系统"
-            style="width: 100%"
-            @change="handleObjChange"
-          >
-            <el-option
-              v-for="system in systemList"
-              :key="system.systemCode"
-              :label="system.systemName"
-              :value="system.systemCode"
-            >
-              <span>{{ system.systemName }}</span>
-              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ system.systemCode }}</span>
-            </el-option>
-          </el-select>
-        </el-form-item>
-
-        <el-form-item label="模型代码">
-          <el-input v-model="form.targetModelCode" placeholder="自动关联" disabled />
-        </el-form-item>
-
-        <!-- 能力选择 - 从物模型加载 -->
-        <el-form-item label="选择能力">
+        <!-- 设备能力 -->
+        <el-form-item label="设备能力" prop="abilityKey">
           <el-select
             v-model="form.abilityKey"
-            filterable
-            placeholder="请选择要调用的能力"
-            style="width: 100%"
-            :loading="loadingModel"
-            :disabled="!form.targetObjCode"
+            placeholder="请选择设备能力"
             @change="handleAbilityChange"
           >
             <el-option
@@ -90,690 +60,633 @@
               :label="ability.abilityName"
               :value="ability.abilityKey"
             >
-              <div style="display: flex; justify-content: space-between; align-items: center;">
-                <span>
-                  <i class="el-icon-magic-stick" style="color: #f5a623; margin-right: 4px"></i>
-                  {{ ability.abilityName }}
-                </span>
-                <el-tag size="mini" type="info">{{ ability.abilityKey }}</el-tag>
-              </div>
+              <span>{{ ability.abilityName }}</span>
+              <span style="float: right; color: #8492a6; font-size: 13px">
+                {{ ability.abilityDesc }}
+              </span>
             </el-option>
           </el-select>
-          <div class="form-tip" v-if="currentAbility">
-            {{ currentAbility.abilityDesc || '暂无描述' }}
-          </div>
-          <div class="form-tip" v-else-if="!loadingModel && abilityList.length === 0 && form.targetObjCode">
-            ⚠️ 该对象暂无能力定义
-          </div>
         </el-form-item>
 
-        <el-divider content-position="left">参数配置</el-divider>
-
-        <!-- 参数定义预览 -->
-        <el-form-item label="参数定义" v-if="currentAbility && currentAbility.paramDefinition">
-          <el-alert
-            type="info"
-            :closable="false"
-            style="margin-bottom: 12px"
-          >
-            <template slot="title">
-              <div style="display: flex; align-items: center; justify-content: space-between;">
-                <span>参数结构</span>
-                <el-button type="text" size="mini" @click="showParamHelp">
-                  <i class="el-icon-question"></i> 帮助
-                </el-button>
-              </div>
-            </template>
-            <pre class="param-definition-preview">{{ formatParamDefinition }}</pre>
-          </el-alert>
-        </el-form-item>
-
-        <el-form-item label="参数来源">
+        <!-- 参数来源 -->
+        <el-form-item label="参数来源" prop="paramSource">
           <el-radio-group v-model="form.paramSource" @change="handleParamSourceChange">
-            <el-radio label="STATIC">静态</el-radio>
-            <el-radio label="CONTEXT">上下文变量</el-radio>
-            <el-radio label="EXPR">表达式</el-radio>
+            <el-radio label="STATIC">静态参数</el-radio>
+            <el-radio label="CONTEXT">上下文参数</el-radio>
+            <el-radio label="ATTR">设备属性</el-radio>
           </el-radio-group>
         </el-form-item>
 
-        <!-- 静态参数 - 动态渲染 -->
-        <template v-if="form.paramSource === 'STATIC' && currentAbility">
-          <div class="static-params-container">
-            <!-- 根据paramDefinition动态渲染 -->
-            <component
-              :is="getParamComponent(paramDef)"
-              v-model="staticParamValue"
-              :definition="paramDef"
-              @change="handleStaticParamChange"
-            />
+        <!-- 静态参数配置 -->
+        <el-form-item
+          v-if="form.paramSource === 'STATIC' && currentAbility"
+          label="参数值"
+        >
+          <!-- 场景1: paramDefinition 为 null - 无参数 -->
+          <div v-if="!currentAbility.paramDefinition">
+            <el-tag type="info">该能力无需参数</el-tag>
           </div>
 
-          <el-form-item label="参数预览">
-            <el-input
-              type="textarea"
-              :value="JSON.stringify(staticParams, null, 2)"
-              :rows="3"
-              readonly
-            />
-          </el-form-item>
-        </template>
+          <!-- 场景2: Options 类型 - 枚举值 -->
+          <div v-else-if="getParamType(currentAbility.paramDefinition) === 'Options'">
+            <el-select
+              v-model="form.abilityParam"
+              placeholder="请选择参数"
+            >
+              <el-option
+                v-for="option in parseOptions(currentAbility.paramDefinition)"
+                :key="option.value"
+                :label="option.key"
+                :value="option.value"
+              />
+            </el-select>
+            <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+              当前选择: {{ getSelectedOptionLabel() }}
+            </div>
+          </div>
 
-        <!-- 上下文变量映射 -->
-        <template v-if="form.paramSource === 'CONTEXT'">
-          <el-form-item label="变量映射">
-            <el-input
-              type="textarea"
-              v-model="paramMappingStr"
-              :rows="4"
-              placeholder='如: {"param1": "${context.varKey1}", "param2": "${context.varKey2}"}'
+          <!-- 场景3: Slider 类型 - 滑块 -->
+          <div v-else-if="getParamType(currentAbility.paramDefinition) === 'Slider'">
+            <el-slider
+              v-model="sliderValue"
+              :min="getSliderMin(currentAbility.paramDefinition)"
+              :max="getSliderMax(currentAbility.paramDefinition)"
+              :format-tooltip="formatSliderTooltip"
+              @change="handleSliderChange"
             />
-            <div class="form-tip">格式: {"参数名": "${context.变量键}"}</div>
-          </el-form-item>
-
-          <el-form-item label="可用变量">
-            <div class="variable-list">
-              <el-tag
-                v-for="variable in contextVariables"
-                :key="variable.varKey"
-                size="small"
-                @click="insertVariable(variable.varKey)"
-                class="variable-tag"
-              >
-                {{ variable.varName }} (${context.{{ variable.varKey }}})
-              </el-tag>
+            <div style="margin-top: 8px;">
+              <el-input-number
+                v-model="sliderValue"
+                :min="getSliderMin(currentAbility.paramDefinition)"
+                :max="getSliderMax(currentAbility.paramDefinition)"
+                @change="handleSliderChange"
+              />
+              <span style="margin-left: 10px; color: #909399;">
+                {{ getSliderUnit(currentAbility.paramDefinition) }}
+              </span>
             </div>
-          </el-form-item>
-        </template>
+          </div>
 
-        <!-- 表达式 -->
-        <template v-if="form.paramSource === 'EXPR'">
-          <el-form-item label="参数表达式">
+          <!-- 场景4: Input 类型 - 文本输入 -->
+          <div v-else-if="getParamType(currentAbility.paramDefinition) === 'Input'">
             <el-input
-              type="textarea"
               v-model="form.abilityParam"
-              :rows="4"
-              placeholder='支持JS表达式和变量引用'
-            />
-            <div class="form-tip">
-              支持:变量引用 ${varKey}、运算符、函数等
-            </div>
-          </el-form-item>
-        </template>
-      </template>
-
-      <!-- 延时等待配置 -->
-      <template v-if="form.stepType === 'DELAY'">
-        <el-divider content-position="left">延时配置</el-divider>
-
-        <el-form-item label="延时时间">
-          <el-input-number
-            v-model="form.delaySeconds"
-            :min="0"
-            :max="86400"
-            style="width: 180px"
-          />
-          <span style="margin-left: 8px; color: #909399">秒</span>
-        </el-form-item>
+              placeholder="请输入参数值"
+            >
+              <template #append v-if="getInputUnit(currentAbility.paramDefinition)">
+                {{ getInputUnit(currentAbility.paramDefinition) }}
+              </template>
+            </el-input>
+          </div>
 
-        <el-form-item label="快捷设置">
-          <div class="delay-shortcuts">
-            <el-tag @click="form.delaySeconds = 5" class="delay-tag">5秒</el-tag>
-            <el-tag @click="form.delaySeconds = 10" class="delay-tag">10秒</el-tag>
-            <el-tag @click="form.delaySeconds = 30" class="delay-tag">30秒</el-tag>
-            <el-tag @click="form.delaySeconds = 60" class="delay-tag">1分钟</el-tag>
-            <el-tag @click="form.delaySeconds = 300" class="delay-tag">5分钟</el-tag>
-            <el-tag @click="form.delaySeconds = 600" class="delay-tag">10分钟</el-tag>
+          <!-- 未知类型 - 兜底 -->
+          <div v-else>
+            <el-input
+              v-model="form.abilityParam"
+              placeholder="请输入参数值"
+            />
           </div>
         </el-form-item>
-      </template>
-
-      <!-- 条件判断配置 -->
-      <template v-if="form.stepType === 'CONDITION'">
-        <el-divider content-position="left">条件配置</el-divider>
 
-        <el-form-item label="条件表达式">
-          <condition-builder
-            v-model="form.conditionExpr"
-            :variables="allVariables"
-            :device-list="deviceList"
-            :show-attr-select="true"
+        <!-- 上下文参数配置 -->
+        <el-form-item v-if="form.paramSource === 'CONTEXT'" label="参数映射">
+          <el-input
+            v-model="form.paramMapping"
+            type="textarea"
+            :rows="3"
+            placeholder='示例: {"value": "context.switchState"}'
           />
+          <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+            <i class="el-icon-info"></i>
+            从执行上下文中动态获取参数值
+          </div>
         </el-form-item>
 
-        <el-form-item label="满足时">
-          <el-input v-model="form.trueBranch" placeholder="跳转到步骤代码或继续" />
-        </el-form-item>
-
-        <el-form-item label="不满足时">
-          <el-input v-model="form.falseBranch" placeholder="跳转到步骤代码或跳过" />
-        </el-form-item>
-      </template>
-
-      <!-- 并行执行配置 -->
-      <template v-if="form.stepType === 'PARALLEL'">
-        <el-divider content-position="left">并行配置</el-divider>
-
-        <el-form-item label="并行步骤">
+        <!-- 设备属性配置 -->
+        <el-form-item v-if="form.paramSource === 'ATTR'" label="属性映射">
           <el-input
+            v-model="form.paramMapping"
             type="textarea"
-            v-model="form.parallelSteps"
             :rows="3"
-            placeholder="输入步骤代码,逗号分隔"
+            placeholder='示例: {"value": "D-B-QS-10000001.Switch"}'
           />
-        </el-form-item>
-
-        <el-form-item label="等待策略">
-          <el-radio-group v-model="form.waitStrategy">
-            <el-radio label="ALL">全部完成</el-radio>
-            <el-radio label="ANY">任一完成</el-radio>
-          </el-radio-group>
+          <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+            <i class="el-icon-info"></i>
+            从设备属性中动态获取参数值
+          </div>
         </el-form-item>
       </template>
 
-      <!-- 循环执行配置 -->
+      <!-- ==================== 延时等待配置 ==================== -->
+      <el-form-item v-if="form.stepType === 'DELAY'" label="延时时长" prop="delaySeconds">
+        <el-input-number
+          v-model="form.delaySeconds"
+          :min="1"
+          :max="3600"
+          placeholder="秒"
+        />
+        <span style="margin-left: 10px;">秒</span>
+      </el-form-item>
+
+      <!-- ==================== 条件判断配置 ==================== -->
+      <el-form-item v-if="form.stepType === 'CONDITION'" label="条件表达式">
+        <el-input
+          v-model="form.conditionExpr"
+          type="textarea"
+          :rows="4"
+          placeholder='简单格式: {"left":"Switch","op":"==","right":"1"}
+复杂格式: {"logic":"AND","conditions":[{"left":"Switch","op":"==","right":"1"}]}'
+        />
+        <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+          <i class="el-icon-info"></i>
+          支持简单条件和复杂逻辑组合(AND/OR)
+        </div>
+      </el-form-item>
+
+      <!-- ==================== 循环执行配置 ==================== -->
       <template v-if="form.stepType === 'LOOP'">
-        <el-divider content-position="left">循环配置</el-divider>
+        <el-card shadow="never" style="margin-bottom: 20px; background: #f5f7fa;">
+          <div slot="header" class="clearfix">
+            <span style="font-weight: 600;">循环配置</span>
+          </div>
 
-        <el-form-item label="循环类型">
-          <el-radio-group v-model="form.loopType">
-            <el-radio label="COUNT">固定次数</el-radio>
-            <el-radio label="CONDITION">条件循环</el-radio>
-          </el-radio-group>
-        </el-form-item>
+          <!-- 最大循环次数 -->
+          <el-form-item label="最大次数" label-width="120px">
+            <el-input-number
+              v-model="form.loopMaxCount"
+              :min="0"
+              :max="1000"
+              placeholder="0表示无限循环"
+            />
+            <span style="margin-left: 10px; color: #909399;">
+              0表示无限循环(需配置跳出条件)
+            </span>
+          </el-form-item>
 
-        <el-form-item label="循环次数" v-if="form.loopType === 'COUNT'">
-          <el-input-number v-model="form.loopCount" :min="1" :max="100" />
-        </el-form-item>
+          <!-- 循环间隔 -->
+          <el-form-item label="循环间隔" label-width="120px">
+            <el-input-number
+              v-model="form.loopInterval"
+              :min="100"
+              :max="60000"
+              :step="100"
+            />
+            <span style="margin-left: 10px;">毫秒</span>
+          </el-form-item>
 
-        <el-form-item label="循环条件" v-if="form.loopType === 'CONDITION'">
-          <condition-builder v-model="form.loopCondition" :variables="allVariables" />
+          <!-- 跳出条件 -->
+          <el-form-item label="跳出条件" label-width="120px">
+            <el-input
+              v-model="form.loopCondition"
+              type="textarea"
+              :rows="3"
+              placeholder='示例: {"left":"current_Switch","op":"==","right":"1"}'
+            />
+            <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+              <i class="el-icon-info"></i>
+              满足条件时跳出循环,可引用上下文变量(如 current_Switch)
+            </div>
+          </el-form-item>
+        </el-card>
+
+        <!-- 子步骤配置提示 -->
+        <el-alert
+          title="子步骤配置"
+          type="info"
+          :closable="false"
+          style="margin-bottom: 20px;"
+        >
+          循环步骤需要配置子步骤,子步骤将在每次循环中依次执行。
+          保存当前步骤后,请在编排器中为此循环步骤添加子步骤。
+        </el-alert>
+      </template>
+
+      <!-- ==================== 属性查询配置 ==================== -->
+      <template v-if="form.stepType === 'ATTR_QUERY'">
+        <!-- 目标设备 -->
+        <el-form-item label="目标设备" prop="targetObjCode">
+          <el-select
+            v-model="form.targetObjCode"
+            placeholder="请选择设备"
+            filterable
+            @change="handleDeviceChange"
+          >
+            <el-option
+              v-for="device in deviceList"
+              :key="device.deviceCode"
+              :label="device.deviceName"
+              :value="device.deviceCode"
+            >
+              <span>{{ device.deviceName }}</span>
+              <span style="float: right; color: #8492a6; font-size: 13px">
+                {{ device.deviceCode }}
+              </span>
+            </el-option>
+          </el-select>
         </el-form-item>
 
-        <el-form-item label="循环间隔">
-          <el-input-number v-model="form.loopInterval" :min="0" style="width: 180px" />
-          <span style="margin-left: 8px; color: #909399">秒</span>
+        <!-- 属性键 -->
+        <el-form-item label="属性键" prop="abilityKey">
+          <el-input
+            v-model="form.abilityKey"
+            placeholder="例如: Switch"
+          />
+          <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+            <i class="el-icon-info"></i>
+            查询结果将保存到上下文: current_[属性键名]
+          </div>
         </el-form-item>
       </template>
 
-      <el-divider content-position="left">执行控制</el-divider>
-
-      <el-form-item label="失败重试">
-        <el-switch v-model="form.retryOnFail" :active-value="1" :inactive-value="0" />
-      </el-form-item>
-
-      <el-form-item label="重试次数" v-if="form.retryOnFail === 1">
-        <el-input-number v-model="form.retryTimes" :min="1" :max="5" />
-      </el-form-item>
-
-      <el-form-item label="超时时间">
-        <el-input-number v-model="form.timeout" :min="0" style="width: 180px" />
-        <span style="margin-left: 8px; color: #909399">秒</span>
-      </el-form-item>
-
-      <el-form-item label="是否启用">
-        <el-switch v-model="form.enable" :active-value="1" :inactive-value="0" />
+      <!-- ==================== 通用配置 ==================== -->
+      <!-- 执行条件 -->
+      <el-form-item label="执行条件" v-if="form.stepType !== 'LOOP'">
+        <el-input
+          v-model="form.conditionExpr"
+          type="textarea"
+          :rows="2"
+          placeholder='留空表示无条件执行,示例: {"left":"status","op":"==","right":"ready"}'
+        />
+        <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+          <i class="el-icon-question"></i>
+          此步骤执行前需满足的条件(可选)
+        </div>
       </el-form-item>
 
-      <el-form-item label="备注">
-        <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选" />
+      <!-- 失败后继续 -->
+      <el-form-item label="失败后继续">
+        <el-switch v-model="continueOnFailSwitch" />
+        <span style="margin-left: 10px; color: #909399;">
+          开启后,即使此步骤失败也会继续执行后续步骤
+        </span>
       </el-form-item>
     </el-form>
-  </div>
+
+    <div slot="footer">
+      <el-button @click="visible = false">取消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
+    </div>
+  </el-dialog>
 </template>
 
 <script>
-import { getModelByCode } from '@/api/basecfg/objModel';
-import { listSubsystemAll } from '@/api/adapter/subsystem';
-import ConditionBuilder from './ConditionBuilder.vue';
+import { getModelByCode } from '@/api/basecfg/objModel'
 
 export default {
   name: 'StepConfig',
-  components: { ConditionBuilder },
-
   props: {
-    step: {
-      type: Object,
-      default: () => ({})
-    },
     deviceList: {
       type: Array,
       default: () => []
-    },
-    contextVariables: {
-      type: Array,
-      default: () => []
     }
   },
-
   data() {
     return {
-      form: {},
-      systemList: [],
+      visible: false,
+      dialogTitle: '配置步骤',
+      submitting: false,
+      form: {
+        id: null,
+        stepCode: '',
+        stepName: '',
+        stepType: 'ABILITY',
+        stepIndex: 1,
+        targetObjCode: '',
+        targetObjType: 2,
+        targetModelCode: '',
+        abilityKey: '',
+        abilityParam: '',
+        paramSource: 'STATIC',
+        paramMapping: '',
+        delaySeconds: 10,
+        conditionExpr: '',
+        continueOnFail: 0,
+        // 循环配置
+        loopMaxCount: 10,
+        loopInterval: 1000,
+        loopCondition: '',
+        parentStepCode: null
+      },
+      rules: {
+        stepName: [
+          { required: true, message: '请输入步骤名称', trigger: 'blur' }
+        ],
+        stepType: [
+          { required: true, message: '请选择步骤类型', trigger: 'change' }
+        ],
+        targetObjCode: [
+          { required: true, message: '请选择目标设备', trigger: 'change' }
+        ],
+        abilityKey: [
+          { required: true, message: '请选择设备能力或属性键', trigger: 'blur' }
+        ]
+      },
       abilityList: [],
       currentAbility: null,
-      loadingModel: false,
-
-      staticParams: {},
-      staticParamValue: null,
-      paramMappingStr: '',
-      paramDef: null
-    };
-  },
-
-  computed: {
-    allVariables() {
-      return this.contextVariables.map(v => ({
-        key: 'context.' + v.varKey,
-        name: v.varName,
-        type: 'context'
-      }));
-    },
-
-    formatParamDefinition() {
-      if (!this.currentAbility || !this.currentAbility.paramDefinition) {
-        return '无参数定义';
-      }
-      try {
-        return JSON.stringify(JSON.parse(this.currentAbility.paramDefinition), null, 2);
-      } catch {
-        return this.currentAbility.paramDefinition;
-      }
+      sliderValue: 50,
+      continueOnFailSwitch: false
     }
   },
-
   watch: {
-    step: {
-      immediate: true,
-      handler(val) {
-        this.form = { ...val };
-
-        // 解析已有参数
-        if (val.abilityParam) {
-          try {
-            this.staticParams = JSON.parse(val.abilityParam);
-          } catch (e) {
-            this.staticParams = {};
-          }
-        }
-
-        if (val.paramMapping) {
-          this.paramMappingStr = val.paramMapping;
-        }
+    continueOnFailSwitch(val) {
+      this.form.continueOnFail = val ? 1 : 0
+    }
+  },
+  methods: {
+    /**
+     * 打开对话框
+     */
+    open(step, parentStepCode) {
+      this.visible = true
+      this.resetForm()
+
+      if (step) {
+        this.dialogTitle = '编辑步骤'
+        this.form = { ...step }
+        this.continueOnFailSwitch = step.continueOnFail === 1
 
         // 加载能力列表
-        if (val.targetObjCode && val.targetModelCode) {
-          this.loadObjectModel(val.targetModelCode);
+        if (step.targetModelCode && step.stepType === 'ABILITY') {
+          this.loadAbilities(step.targetModelCode)
+        }
+      } else {
+        this.dialogTitle = '新增步骤'
+        if (parentStepCode) {
+          this.form.parentStepCode = parentStepCode
         }
       }
     },
-    form: {
-      deep: true,
-      handler(val) {
-        this.$emit('change', val);
-      }
-    }
-  },
 
-  created() {
-    this.loadSystems();
-  },
-
-  methods: {
-    // 加载系统列表
-    async loadSystems() {
-      try {
-        const response = await listSubsystemAll();
-        this.systemList = response.data || [];
-      } catch (error) {
-        console.error('加载系统列表失败', error);
-        this.systemList = [];
+    /**
+     * 重置表单
+     */
+    resetForm() {
+      this.form = {
+        id: null,
+        stepCode: '',
+        stepName: '',
+        stepType: 'ABILITY',
+        stepIndex: 1,
+        targetObjCode: '',
+        targetObjType: 2,
+        targetModelCode: '',
+        abilityKey: '',
+        abilityParam: '',
+        paramSource: 'STATIC',
+        paramMapping: '',
+        delaySeconds: 10,
+        conditionExpr: '',
+        continueOnFail: 0,
+        loopMaxCount: 10,
+        loopInterval: 1000,
+        loopCondition: '',
+        parentStepCode: null
       }
+      this.abilityList = []
+      this.currentAbility = null
+      this.sliderValue = 50
+      this.continueOnFailSwitch = false
     },
 
-    // 对象类型改变
-    handleObjTypeChange() {
-      this.form.targetObjCode = null;
-      this.form.targetModelCode = null;
-      this.form.abilityKey = null;
-      this.abilityList = [];
-      this.currentAbility = null;
-    },
-
-    // 对象改变
-    async handleObjChange(objCode) {
-      let modelCode = null;
-
-      if (this.form.targetObjType === 2) {
-        // 设备 - 使用 deviceModel
-        const device = this.deviceList.find(d => d.deviceCode === objCode);
-        if (device) {
-          modelCode = device.modelCode || device.deviceModel;
-        }
-      } else if (this.form.targetObjType === 3) {
-        // 系统
-        const system = this.systemList.find(s => s.systemCode === objCode);
-        if (system) {
-          modelCode = system.modelCode;
+    /**
+     * 设备改变事件
+     */
+    handleDeviceChange(deviceCode) {
+      const device = this.deviceList.find(d => d.deviceCode === deviceCode)
+      if (device) {
+        this.form.targetModelCode = device.deviceModel || device.modelCode
+        this.form.targetObjType = 2
+
+        // 加载能力列表(仅能力调用类型需要)
+        if (this.form.stepType === 'ABILITY') {
+          this.loadAbilities(this.form.targetModelCode)
         }
       }
 
-      this.form.targetModelCode = modelCode;
-
-      if (modelCode) {
-        await this.loadObjectModel(modelCode);
-      } else {
-        this.$message.warning('该对象未配置物模型');
-        this.abilityList = [];
-      }
+      // 重置能力和参数
+      this.form.abilityKey = ''
+      this.form.abilityParam = ''
+      this.currentAbility = null
     },
 
-    // 加载物模型(核心方法)
-    async loadObjectModel(modelCode) {
-      if (!modelCode) return;
-
-      this.loadingModel = true;
+    /**
+     * 加载设备能力列表
+     */
+    async loadAbilities(modelCode) {
       try {
-        const response = await getModelByCode(modelCode);
-        const modelData = response.data;
-
-        if (!modelData) {
-          throw new Error('物模型数据为空');
-        }
-
-        // 加载能力列表
-        this.abilityList = modelData.abilityList || [];
-
-        console.log('物模型加载成功:', {
-          modelCode,
-          abilityCount: this.abilityList.length
-        });
-
-        // 如果已选择能力,重新加载能力详情
-        if (this.form.abilityKey) {
-          this.handleAbilityChange(this.form.abilityKey);
-        }
-
+        const res = await getModelByCode(modelCode)
+        this.abilityList = res.data?.abilityList?.filter(a => a.hiddenFlag === 1) || []
       } catch (error) {
-        console.error('加载物模型失败', error);
-        this.$message.error('加载物模型失败: ' + (error.message || '未知错误'));
-        this.abilityList = [];
-      } finally {
-        this.loadingModel = false;
+        console.error('加载能力失败:', error)
+        this.$message.error('加载能力列表失败')
       }
     },
 
-    // 能力改变
+    /**
+     * 能力改变事件
+     */
     handleAbilityChange(abilityKey) {
-      const ability = this.abilityList.find(a => a.abilityKey === abilityKey);
-      this.currentAbility = ability;
-      this.form.abilityName = ability?.abilityName;
-
-      // 解析参数定义
-      if (ability && ability.paramDefinition) {
-        try {
-          this.paramDef = JSON.parse(ability.paramDefinition);
-          console.log('参数定义:', this.paramDef);
-
-          // 初始化参数值
-          this.initStaticParams();
-        } catch (e) {
-          console.error('解析参数定义失败', e);
-          this.paramDef = null;
-        }
-      } else {
-        this.paramDef = null;
+      this.currentAbility = this.abilityList.find(a => a.abilityKey === abilityKey)
+
+      // 重置参数
+      this.form.abilityParam = ''
+      this.form.paramMapping = ''
+
+      // 如果是 Slider 类型,设置默认值
+      if (this.currentAbility && this.getParamType(this.currentAbility.paramDefinition) === 'Slider') {
+        const min = this.getSliderMin(this.currentAbility.paramDefinition)
+        const max = this.getSliderMax(this.currentAbility.paramDefinition)
+        this.sliderValue = Math.floor((min + max) / 2)
+        this.form.abilityParam = String(this.sliderValue)
       }
     },
 
-    // 初始化静态参数
-    initStaticParams() {
-      if (!this.paramDef) return;
+    /**
+     * 参数来源改变事件
+     */
+    handleParamSourceChange() {
+      this.form.abilityParam = ''
+      this.form.paramMapping = ''
+    },
+
+    /**
+     * Slider 值改变事件
+     */
+    handleSliderChange(value) {
+      this.form.abilityParam = String(value)
+    },
 
-      // 根据参数定义类型初始化默认值
-      if (this.paramDef.type === 'Options') {
-        // 下拉选项,取第一个值
-        if (this.paramDef.list && this.paramDef.list.length > 0) {
-          this.staticParamValue = this.paramDef.list[0].value;
-        }
-      } else if (this.paramDef.type === 'Slider') {
-        // 滑块,取中间值
-        const min = this.paramDef.min || 0;
-        const max = this.paramDef.max || 100;
-        this.staticParamValue = Math.floor((min + max) / 2);
+    /**
+     * 步骤类型改变事件
+     */
+    handleStepTypeChange(newType) {
+      // 切换类型时重置相关字段
+      if (newType === 'ABILITY') {
+        // 能力调用
+        this.form.targetObjCode = ''
+        this.form.abilityKey = ''
+        this.form.abilityParam = ''
+        this.form.paramSource = 'STATIC'
+      } else if (newType === 'DELAY') {
+        // 延时等待
+        this.form.delaySeconds = 10
+      } else if (newType === 'CONDITION') {
+        // 条件判断
+        this.form.conditionExpr = ''
+      } else if (newType === 'LOOP') {
+        // 循环执行
+        this.form.loopMaxCount = 10
+        this.form.loopInterval = 1000
+        this.form.loopCondition = ''
+      } else if (newType === 'ATTR_QUERY') {
+        // 属性查询
+        this.form.targetObjCode = ''
+        this.form.abilityKey = '' // 用于存储属性键
       }
+    },
 
-      // 如果已有参数值,使用已有值
-      if (this.staticParams && Object.keys(this.staticParams).length > 0) {
-        this.staticParamValue = Object.values(this.staticParams)[0];
+    /**
+     * 获取参数类型
+     */
+    getParamType(paramDefinition) {
+      if (!paramDefinition) return null
+      try {
+        const def = JSON.parse(paramDefinition)
+        return def.type
+      } catch (e) {
+        return null
       }
     },
 
-    // 静态参数改变
-    handleStaticParamChange(value) {
-      // 根据能力键名构建参数对象
-      this.staticParams = { value: value };
-      this.form.abilityParam = JSON.stringify(this.staticParams);
+    /**
+     * 解析 Options 列表
+     */
+    parseOptions(paramDefinition) {
+      try {
+        const def = JSON.parse(paramDefinition)
+        if (def.type === 'Options' && def.list) {
+          return def.list
+        }
+      } catch (e) {
+        console.error('解析Options失败:', e)
+      }
+      return []
     },
 
-    // 参数来源改变
-    handleParamSourceChange() {
-      this.staticParams = {};
-      this.paramMappingStr = '';
+    /**
+     * 获取选中的选项标签
+     */
+    getSelectedOptionLabel() {
+      if (!this.currentAbility || !this.form.abilityParam) return ''
+      const options = this.parseOptions(this.currentAbility.paramDefinition)
+      const option = options.find(opt => opt.value === this.form.abilityParam)
+      return option ? option.key : this.form.abilityParam
     },
 
-    // 插入变量
-    insertVariable(varKey) {
-      this.paramMappingStr += `\${context.${varKey}}`;
+    /**
+     * 获取 Slider 最小值
+     */
+    getSliderMin(paramDefinition) {
+      try {
+        const def = JSON.parse(paramDefinition)
+        return def.min || 0
+      } catch (e) {
+        return 0
+      }
     },
 
-    // 显示参数帮助
-    showParamHelp() {
-      this.$alert(`
-        <div style="line-height: 1.8; font-size: 13px;">
-          <p><b>参数定义格式:</b></p>
-          <p><b>1. Options(下拉选择):</b></p>
-          <pre>{
-  "type": "Options",
-  "list": [
-    {"key": "开灯", "value": "1"},
-    {"key": "关灯", "value": "0"}
-  ]
-}</pre>
-          <p><b>2. Slider(滑块):</b></p>
-          <pre>{
-  "type": "Slider",
-  "min": 0,
-  "max": 100
-}</pre>
-        </div>
-      `, '参数定义说明', {
-        dangerouslyUseHTMLString: true,
-        customClass: 'param-help-dialog'
-      });
+    /**
+     * 获取 Slider 最大值
+     */
+    getSliderMax(paramDefinition) {
+      try {
+        const def = JSON.parse(paramDefinition)
+        return def.max || 100
+      } catch (e) {
+        return 100
+      }
     },
 
-    // 根据参数定义获取组件
-    getParamComponent(def) {
-      if (!def) return 'ParamInput';
-
-      if (def.type === 'Options') return 'ParamOptions';
-      if (def.type === 'Slider') return 'ParamSlider';
-      return 'ParamInput';
+    /**
+     * 获取 Slider 单位
+     */
+    getSliderUnit(paramDefinition) {
+      try {
+        const def = JSON.parse(paramDefinition)
+        return def.unit || ''
+      } catch (e) {
+        return ''
+      }
     },
 
-    // 辅助方法
-    getStepTypeName(type) {
-      const map = {
-        'ABILITY': '能力调用',
-        'DELAY': '延时等待',
-        'CONDITION': '条件判断',
-        'PARALLEL': '并行执行',
-        'LOOP': '循环执行'
-      };
-      return map[type] || type;
+    /**
+     * 格式化 Slider 提示
+     */
+    formatSliderTooltip(value) {
+      const unit = this.getSliderUnit(this.currentAbility?.paramDefinition)
+      return unit ? `${value}${unit}` : String(value)
     },
 
-    getStepTypeTag(type) {
-      const map = {
-        'ABILITY': '',
-        'DELAY': 'success',
-        'CONDITION': 'warning',
-        'PARALLEL': 'info',
-        'LOOP': 'danger'
-      };
-      return map[type] || '';
+    /**
+     * 获取 Input 单位
+     */
+    getInputUnit(paramDefinition) {
+      try {
+        const def = JSON.parse(paramDefinition)
+        return def.unit || ''
+      } catch (e) {
+        return ''
+      }
     },
 
-    getStepTypeIcon(type) {
-      const map = {
-        'ABILITY': 'el-icon-s-operation',
-        'DELAY': 'el-icon-time',
-        'CONDITION': 'el-icon-question',
-        'PARALLEL': 'el-icon-sort',
-        'LOOP': 'el-icon-refresh'
-      };
-      return map[type] || 'el-icon-tickets';
-    }
-  },
+    /**
+     * 提交表单
+     */
+    handleSubmit() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          // 特殊验证
+          if (this.form.stepType === 'LOOP') {
+            if (this.form.loopMaxCount === 0 && !this.form.loopCondition) {
+              this.$message.warning('无限循环必须配置跳出条件')
+              return
+            }
+          }
 
-  // 动态参数组件
-  components: {
-    ConditionBuilder,
+          this.submitting = true
+          this.$emit('submit', this.form)
 
-    ParamInput: {
-      props: ['value', 'definition'],
-      template: '<el-input v-model="inputValue" @input="handleInput" placeholder="请输入参数值" />',
-      data() {
-        return { inputValue: this.value };
-      },
-      watch: {
-        value(val) { this.inputValue = val; }
-      },
-      methods: {
-        handleInput(val) {
-          this.$emit('input', val);
-          this.$emit('change', val);
+          // 延迟关闭,等待外部处理
+          setTimeout(() => {
+            this.submitting = false
+            this.visible = false
+          }, 500)
         }
-      }
+      })
     },
 
-    ParamOptions: {
-      props: ['value', 'definition'],
-      template: `
-        <el-select v-model="selectValue" @change="handleChange" style="width: 100%">
-          <el-option
-            v-for="opt in definition.list"
-            :key="opt.value"
-            :label="opt.key"
-            :value="opt.value"
-          >
-            <span>{{ opt.key }}</span>
-            <span style="float: right; color: #909399; font-size: 12px">{{ opt.value }}</span>
-          </el-option>
-        </el-select>
-      `,
-      data() {
-        return { selectValue: this.value };
-      },
-      watch: {
-        value(val) { this.selectValue = val; }
-      },
-      methods: {
-        handleChange(val) {
-          this.$emit('input', val);
-          this.$emit('change', val);
-        }
-      }
-    },
-
-    ParamSlider: {
-      props: ['value', 'definition'],
-      template: `
-        <div>
-          <el-slider
-            v-model="sliderValue"
-            @change="handleChange"
-            :min="definition.min || 0"
-            :max="definition.max || 100"
-            show-input
-          />
-          <div style="font-size: 12px; color: #909399; margin-top: 4px;">
-            范围: {{ definition.min || 0 }} - {{ definition.max || 100 }}
-          </div>
-        </div>
-      `,
-      data() {
-        return { sliderValue: this.value || 0 };
-      },
-      watch: {
-        value(val) { this.sliderValue = val; }
-      },
-      methods: {
-        handleChange(val) {
-          this.$emit('input', val);
-          this.$emit('change', val);
-        }
-      }
+    /**
+     * 关闭对话框
+     */
+    handleClose() {
+      this.$refs.form.resetFields()
     }
   }
-};
+}
 </script>
 
-<style lang="scss" scoped>
-.step-config {
-  .delay-shortcuts {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-
-    .delay-tag {
-      cursor: pointer;
-      transition: all 0.2s;
-
-      &:hover {
-        background: #ecf5ff;
-        color: #409eff;
-        transform: scale(1.05);
-      }
-    }
-  }
-
-  .param-definition-preview {
-    background: #f5f7fa;
-    padding: 8px;
-    border-radius: 4px;
-    font-size: 12px;
-    line-height: 1.5;
-    max-height: 150px;
-    overflow-y: auto;
-    margin: 0;
-  }
-
-  .static-params-container {
-    margin-bottom: 12px;
-  }
-
-  .variable-list {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-
-    .variable-tag {
-      cursor: pointer;
-      transition: all 0.2s;
+<style scoped>
+.el-select {
+  width: 100%;
+}
 
-      &:hover {
-        background: #ecf5ff;
-        color: #409eff;
-        transform: scale(1.05);
-      }
-    }
-  }
+.clearfix:before,
+.clearfix:after {
+  display: table;
+  content: "";
+}
 
-  .form-tip {
-    font-size: 12px;
-    color: #909399;
-    margin-top: 4px;
-  }
+.clearfix:after {
+  clear: both;
 }
 </style>

+ 16 - 11
ems-ui-cloud/src/views/mgr/strategy/log.vue

@@ -67,6 +67,7 @@
             <el-radio label="TIME">定时</el-radio>
             <el-radio label="MANUAL">手动</el-radio>
             <el-radio label="CONDITION">条件</el-radio>
+            <el-radio label="ATTR">属性</el-radio>
           </el-radio-group>
         </div>
 
@@ -254,15 +255,7 @@
           <el-empty v-else description="暂无步骤执行记录" />
         </div>
 
-        <!-- 上下文数据 -->
-        <div class="detail-section" v-if="currentLog.contextData">
-          <h3 class="section-title">执行上下文</h3>
-          <el-collapse>
-            <el-collapse-item title="上下文数据">
-              <pre>{{ formatJson(currentLog.contextData) }}</pre>
-            </el-collapse-item>
-          </el-collapse>
-        </div>
+        <!-- ✅ 已删除"上下文数据"区块 -->
       </div>
     </el-drawer>
   </div>
@@ -412,12 +405,24 @@ export default {
 
     // 辅助方法
     getTriggerTypeName(type) {
-      const map = { 'EVENT': '事件', 'TIME': '定时', 'MANUAL': '手动', 'CONDITION': '条件', 'ATTR': '属性' };
+      const map = {
+        'EVENT': '事件',
+        'TIME': '定时',
+        'MANUAL': '手动',
+        'CONDITION': '条件',
+        'ATTR': '属性'  // ✅ 新增属性触发类型
+      };
       return map[type] || type || '-';
     },
 
     getTriggerTagType(type) {
-      const map = { 'EVENT': 'danger', 'TIME': '', 'MANUAL': 'info', 'CONDITION': 'success', 'ATTR': 'warning' };
+      const map = {
+        'EVENT': 'danger',
+        'TIME': '',
+        'MANUAL': 'info',
+        'CONDITION': 'success',
+        'ATTR': 'warning'  // ✅ 新增属性触发类型
+      };
       return map[type] || '';
     },