learshaw пре 4 месеци
родитељ
комит
dfd0e6072d

+ 18 - 29
ems-ui-cloud/src/api/mgr/energyStrategy.js

@@ -219,17 +219,6 @@ export function deleteStrategyContext(id) {
   })
 }
 
-/**
- * 批量保存上下文变量
- */
-export function saveStrategyContextBatch(strategyCode, variables) {
-  return request({
-    url: '/ems/energyStrategy/context/batch',
-    method: 'post',
-    data: { strategyCode, variables }
-  })
-}
-
 // ===========================
 // 策略参数管理
 // ===========================
@@ -288,28 +277,18 @@ export function executeStrategy(strategyCode, params) {
 // ===========================
 
 /**
- * 获取执行日志列表
- * 修复:匹配后端路径参数格式
+ * 获取执行日志列表(通用接口,支持多条件查询)
  */
-export function getExecLogList(strategyCode, query) {
-  if (strategyCode) {
-    return request({
-      url: '/ems/energyStrategy/execLog/list/' + strategyCode,
-      method: 'get',
-      params: query
-    })
-  } else {
-    // 需要后端支持无策略码的全量查询
-    return request({
-      url: '/ems/energyStrategy/execLog/list',
-      method: 'get',
-      params: query
-    })
-  }
+export function getExecLogList(query) {
+  return request({
+    url: '/ems/energyStrategy/execLog/list',
+    method: 'get',
+    params: query
+  })
 }
 
 /**
- * 获取执行日志详情(包含步骤日志)
+ * 获取执行日志详情
  */
 export function getExecLog(execId) {
   return request({
@@ -318,6 +297,16 @@ export function getExecLog(execId) {
   })
 }
 
+/**
+ * 获取步骤执行日志
+ */
+export function getStepExecLog(execId) {
+  return request({
+    url: '/ems/energyStrategy/execLog/steps/' + execId,
+    method: 'get'
+  })
+}
+
 // ===========================
 // 策略模板
 // 注意:需要后端新增这些接口

+ 4 - 9
ems-ui-cloud/src/router/index.js

@@ -55,7 +55,7 @@ export const constantRoutes = [
         name: 'StrategyEditor',
         meta: {
           title: '策略编排',
-          activeMenu: '/mgr/strategy'
+          activeMenu: '/mgr/strategy/index'
         }
       }
     ]
@@ -192,9 +192,8 @@ export const dynamicRoutes = [
       icon: 'el-icon-s-operation'
     },
     children: [
-      // ✅ 策略中心(入口页面)
       {
-        path: 'index',
+        path: 'strategy-index',
         component: () => import('@/views/mgr/strategy/index'),
         name: 'StrategyIndex',
         meta: {
@@ -202,10 +201,8 @@ export const dynamicRoutes = [
           icon: 'el-icon-menu'
         }
       },
-
-      // ✅ 策略日志
       {
-        path: 'log',
+        path: 'strategy-log',
         component: () => import('@/views/mgr/strategy/log'),
         name: 'StrategyLog',
         meta: {
@@ -213,10 +210,8 @@ export const dynamicRoutes = [
           icon: 'el-icon-document'
         }
       },
-
-      // ✅ 策略模板
       {
-        path: 'template',
+        path: 'strategy-template',
         component: () => import('@/views/mgr/strategy/template'),
         name: 'StrategyTemplate',
         meta: {

+ 121 - 33
ems-ui-cloud/src/views/mgr/strategy/components/ConditionBuilder.vue

@@ -12,21 +12,22 @@
             size="small"
             style="width: 100%"
           >
-            <el-option-group label="上下文变量">
+            <el-option-group label="上下文变量" v-if="variables.length > 0">
               <el-option
                 v-for="v in variables"
                 :key="v.key"
                 :label="v.name"
                 :value="v.key"
-              />
+              >
+                <i class="el-icon-coin" style="color: #409eff; margin-right: 4px"></i>
+                {{ v.name }}
+              </el-option>
             </el-option-group>
-            <el-option-group label="设备属性" v-if="deviceAttrs.length">
-              <el-option
-                v-for="attr in deviceAttrs"
-                :key="attr.key"
-                :label="attr.name"
-                :value="attr.key"
-              />
+            <el-option-group label="当前属性" v-if="attrKey">
+              <el-option :label="attrKey" :value="attrKey">
+                <i class="el-icon-document" style="color: #67c23a; margin-right: 4px"></i>
+                {{ attrKey }}
+              </el-option>
             </el-option-group>
           </el-select>
         </el-col>
@@ -45,7 +46,27 @@
           </el-select>
         </el-col>
         <el-col :span="8">
+          <!-- 枚举类型属性,显示下拉选择 -->
+          <el-select
+            v-if="attrInfo && attrInfo.attrValueType === 'Enum' && attrInfo.valueEnums"
+            v-model="condition.right"
+            size="small"
+            style="width: 100%"
+            :disabled="['isEmpty', 'isNotEmpty'].includes(condition.op)"
+          >
+            <el-option
+              v-for="enumItem in attrInfo.valueEnums"
+              :key="enumItem.attrValue"
+              :label="enumItem.attrValueName"
+              :value="enumItem.attrValue"
+            >
+              <span>{{ enumItem.attrValueName }}</span>
+              <span style="float: right; color: #909399; font-size: 12px">{{ enumItem.attrValue }}</span>
+            </el-option>
+          </el-select>
+          <!-- 其他类型,输入框 -->
           <el-input
+            v-else
             v-model="condition.right"
             placeholder="比较值"
             size="small"
@@ -56,11 +77,22 @@
           <el-button
             type="text"
             icon="el-icon-s-tools"
+            size="small"
             @click="switchMode('advanced')"
             title="高级模式"
           />
         </el-col>
       </el-row>
+
+      <!-- 属性信息提示 -->
+      <div class="attr-info-tip" v-if="attrInfo">
+        <el-tag size="mini" :type="getAttrTypeTag(attrInfo.attrValueType)">
+          {{ getAttrTypeName(attrInfo.attrValueType) }}
+        </el-tag>
+        <span v-if="attrInfo.attrUnit" style="margin-left: 8px; color: #909399; font-size: 12px;">
+          单位: {{ attrInfo.attrUnit }}
+        </span>
+      </div>
     </div>
 
     <!-- 高级模式 - 多条件组合 -->
@@ -71,7 +103,7 @@
           <el-radio-button label="OR">或 (OR)</el-radio-button>
         </el-radio-group>
         <el-button type="text" size="mini" @click="switchMode('simple')">
-          简单模式
+          <i class="el-icon-back"></i> 简单模式
         </el-button>
       </div>
 
@@ -104,6 +136,8 @@
             <el-option label=">=" value=">=" />
             <el-option label="<" value="<" />
             <el-option label="<=" value="<=" />
+            <el-option label="包含" value="contains" />
+            <el-option label="不包含" value="notContains" />
           </el-select>
 
           <el-input
@@ -142,7 +176,9 @@
 
     <!-- 设备属性快捷选择 -->
     <div class="attr-quick-select" v-if="showAttrSelect">
-      <el-divider content-position="left">快捷选择设备属性</el-divider>
+      <el-divider content-position="left">
+        <i class="el-icon-position"></i> 快捷选择设备属性
+      </el-divider>
       <el-cascader
         v-model="selectedDeviceAttr"
         :options="deviceAttrOptions"
@@ -159,8 +195,7 @@
 </template>
 
 <script>
-import { listDevice } from '@/api/device/device';
-import { getObjAttr } from '@/api/basecfg/objAttribute';
+import { getModelByCode } from '@/api/basecfg/objModel';
 
 export default {
   name: 'ConditionBuilder',
@@ -178,6 +213,10 @@ export default {
       type: String,
       default: ''
     },
+    attrInfo: {
+      type: Object,
+      default: null
+    },
     deviceList: {
       type: Array,
       default: () => []
@@ -202,7 +241,6 @@ export default {
           { left: '', op: '==', right: '' }
         ]
       },
-      deviceAttrs: [],
       deviceAttrOptions: [],
       selectedDeviceAttr: []
     };
@@ -210,10 +248,15 @@ export default {
 
   computed: {
     allVariables() {
-      return [
-        ...this.variables,
-        ...this.deviceAttrs
-      ];
+      const vars = [...this.variables];
+      if (this.attrKey) {
+        vars.push({
+          key: this.attrKey,
+          name: this.attrKey,
+          type: 'attr'
+        });
+      }
+      return vars;
     },
 
     expressionPreview() {
@@ -254,9 +297,12 @@ export default {
         this.emitChange();
       }
     },
-    attrKey(val) {
-      if (val) {
-        this.condition.left = val;
+    attrKey: {
+      immediate: true,
+      handler(val) {
+        if (val && !this.condition.left) {
+          this.condition.left = val;
+        }
       }
     }
   },
@@ -334,13 +380,18 @@ export default {
     },
 
     async loadDeviceAttrOptions() {
-      // 构建设备-属性级联选项
       const options = [];
 
-      for (const device of this.deviceList.slice(0, 50)) { // 限制数量避免性能问题
+      // 限制数量避免性能问题
+      const deviceSubset = this.deviceList.slice(0, 30);
+
+      for (const device of deviceSubset) {
+        if (!device.modelCode) continue;
+
         try {
-          const response = await getObjAttr(2, device.deviceCode);
-          const attrs = response.data || [];
+          const response = await getModelByCode(device.modelCode);
+          const modelData = response.data;
+          const attrs = modelData.attrList || [];
 
           if (attrs.length > 0) {
             options.push({
@@ -348,12 +399,12 @@ export default {
               name: device.deviceName,
               children: attrs.map(attr => ({
                 code: attr.attrKey,
-                name: attr.attrName
+                name: `${attr.attrName} (${attr.attrKey})`
               }))
             });
           }
         } catch (e) {
-          // 忽略加载失败的设备
+          console.error('加载设备物模型失败', device.deviceCode, e);
         }
       }
 
@@ -375,12 +426,29 @@ export default {
             this.conditionGroup.conditions[lastIndex].left = `${deviceCode}.${attrKey}`;
           }
         }
-
-        this.deviceAttrs.push({
-          key: `${deviceCode}.${attrKey}`,
-          name: `${deviceCode}.${attrKey}`
-        });
       }
+    },
+
+    getAttrTypeName(type) {
+      const typeMap = {
+        'Value': '数值',
+        'String': '字符串',
+        'Enum': '枚举',
+        'Boolean': '布尔',
+        'WebPic': '图片',
+        'Object': '对象'
+      };
+      return typeMap[type] || type;
+    },
+
+    getAttrTypeTag(type) {
+      const tagMap = {
+        'Value': '',
+        'String': 'info',
+        'Enum': 'warning',
+        'Boolean': 'success'
+      };
+      return tagMap[type] || '';
     }
   }
 };
@@ -392,6 +460,16 @@ export default {
     .el-row {
       align-items: center;
     }
+
+    .attr-info-tip {
+      margin-top: 8px;
+      padding: 6px 10px;
+      background: #f5f7fa;
+      border-radius: 4px;
+      font-size: 12px;
+      display: flex;
+      align-items: center;
+    }
   }
 
   .advanced-mode {
@@ -411,6 +489,11 @@ export default {
         padding: 8px;
         background: #f5f7fa;
         border-radius: 6px;
+        transition: all 0.2s;
+
+        &:hover {
+          background: #ecf5ff;
+        }
       }
     }
 
@@ -431,12 +514,17 @@ export default {
         font-size: 13px;
         color: #409eff;
         word-break: break-all;
+        display: block;
+        line-height: 1.5;
       }
     }
   }
 
   .attr-quick-select {
-    margin-top: 12px;
+    margin-top: 16px;
+    padding: 12px;
+    background: #f5f7fa;
+    border-radius: 6px;
   }
 }
 </style>

+ 443 - 192
ems-ui-cloud/src/views/mgr/strategy/components/StepConfig.vue

@@ -10,13 +10,10 @@
       </el-form-item>
 
       <el-form-item label="步骤类型">
-        <el-select v-model="form.stepType" style="width: 100%" disabled>
-          <el-option label="能力调用" value="ABILITY" />
-          <el-option label="延时等待" value="DELAY" />
-          <el-option label="条件判断" value="CONDITION" />
-          <el-option label="并行执行" value="PARALLEL" />
-          <el-option label="循环执行" value="LOOP" />
-        </el-select>
+        <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>
 
       <!-- 能力调用配置 -->
@@ -24,19 +21,20 @@
         <el-divider content-position="left">目标配置</el-divider>
 
         <el-form-item label="对象类型">
-          <el-radio-group v-model="form.targetObjType">
+          <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-select
             v-model="form.targetObjCode"
             filterable
             placeholder="搜索选择设备"
             style="width: 100%"
-            @change="handleDeviceChange"
+            @change="handleObjChange"
           >
             <el-option
               v-for="device in deviceList"
@@ -50,16 +48,40 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="系统代码" v-else>
-          <el-input v-model="form.targetObjCode" placeholder="请输入系统代码" />
+        <!-- 系统选择 -->
+        <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-select
             v-model="form.abilityKey"
             filterable
             placeholder="请选择要调用的能力"
             style="width: 100%"
+            :loading="loadingModel"
+            :disabled="!form.targetObjCode"
             @change="handleAbilityChange"
           >
             <el-option
@@ -68,61 +90,69 @@
               :label="ability.abilityName"
               :value="ability.abilityKey"
             >
-              <span>{{ ability.abilityName }}</span>
-              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ ability.abilityKey }}</span>
+              <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>
             </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-radio-group v-model="form.paramSource" @change="handleParamSourceChange">
             <el-radio label="STATIC">静态值</el-radio>
             <el-radio label="CONTEXT">上下文变量</el-radio>
-            <el-radio label="ATTR">设备属性</el-radio>
+            <el-radio label="EXPR">表达式</el-radio>
           </el-radio-group>
         </el-form-item>
 
-        <!-- 静态参数 -->
-        <template v-if="form.paramSource === 'STATIC'">
-          <el-form-item
-            v-for="param in currentAbilityParams"
-            :key="param.paramKey"
-            :label="param.paramName"
-          >
-            <template v-if="param.paramType === 'NUMBER'">
-              <el-input-number
-                v-model="staticParams[param.paramKey]"
-                :min="param.min"
-                :max="param.max"
-                style="width: 100%"
-              />
-            </template>
-            <template v-else-if="param.paramType === 'BOOLEAN'">
-              <el-switch v-model="staticParams[param.paramKey]" />
-            </template>
-            <template v-else-if="param.paramType === 'SELECT'">
-              <el-select v-model="staticParams[param.paramKey]" style="width: 100%">
-                <el-option
-                  v-for="opt in param.options"
-                  :key="opt.value"
-                  :label="opt.label"
-                  :value="opt.value"
-                />
-              </el-select>
-            </template>
-            <template v-else>
-              <el-input v-model="staticParams[param.paramKey]" :placeholder="param.placeholder" />
-            </template>
-            <div v-if="param.description" class="param-desc">{{ param.description }}</div>
-          </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"
+            />
+          </div>
 
-          <el-form-item label="JSON预览">
+          <el-form-item label="参数预览">
             <el-input
               type="textarea"
               :value="JSON.stringify(staticParams, null, 2)"
-              :rows="4"
+              :rows="3"
               readonly
             />
           </el-form-item>
@@ -130,43 +160,43 @@
 
         <!-- 上下文变量映射 -->
         <template v-if="form.paramSource === 'CONTEXT'">
-          <el-form-item
-            v-for="param in currentAbilityParams"
-            :key="param.paramKey"
-            :label="param.paramName"
-          >
-            <el-select
-              v-model="paramMapping[param.paramKey]"
-              filterable
-              allow-create
-              placeholder="选择上下文变量"
-              style="width: 100%"
-            >
-              <el-option
+          <el-form-item label="变量映射">
+            <el-input
+              type="textarea"
+              v-model="paramMappingStr"
+              :rows="4"
+              placeholder='如: {"param1": "${context.varKey1}", "param2": "${context.varKey2}"}'
+            />
+            <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"
-                :label="variable.varName"
-                :value="'context.' + variable.varKey"
-              />
-            </el-select>
+                size="small"
+                @click="insertVariable(variable.varKey)"
+                class="variable-tag"
+              >
+                {{ variable.varName }} (${context.{{ variable.varKey }}})
+              </el-tag>
+            </div>
           </el-form-item>
         </template>
 
-        <!-- 属性值映射 -->
-        <template v-if="form.paramSource === 'ATTR'">
-          <el-form-item
-            v-for="param in currentAbilityParams"
-            :key="param.paramKey"
-            :label="param.paramName"
-          >
-            <el-cascader
-              v-model="paramMapping[param.paramKey]"
-              :options="deviceAttrOptions"
-              :props="{ value: 'code', label: 'name' }"
-              filterable
-              placeholder="选择设备.属性"
-              style="width: 100%"
+        <!-- 表达式 -->
+        <template v-if="form.paramSource === 'EXPR'">
+          <el-form-item label="参数表达式">
+            <el-input
+              type="textarea"
+              v-model="form.abilityParam"
+              :rows="4"
+              placeholder='支持JS表达式和变量引用'
             />
+            <div class="form-tip">
+              支持:变量引用 ${varKey}、运算符、函数等
+            </div>
           </el-form-item>
         </template>
       </template>
@@ -206,6 +236,7 @@
             v-model="form.conditionExpr"
             :variables="allVariables"
             :device-list="deviceList"
+            :show-attr-select="true"
           />
         </el-form-item>
 
@@ -223,14 +254,12 @@
         <el-divider content-position="left">并行配置</el-divider>
 
         <el-form-item label="并行步骤">
-          <el-select
+          <el-input
+            type="textarea"
             v-model="form.parallelSteps"
-            multiple
-            placeholder="选择要并行执行的步骤"
-            style="width: 100%"
-          >
-            <!-- 从父组件获取所有步骤 -->
-          </el-select>
+            :rows="3"
+            placeholder="输入步骤代码,逗号分隔"
+          />
         </el-form-item>
 
         <el-form-item label="等待策略">
@@ -268,15 +297,6 @@
 
       <el-divider content-position="left">执行控制</el-divider>
 
-      <el-form-item label="执行条件">
-        <el-input
-          v-model="form.conditionExpr"
-          type="textarea"
-          :rows="2"
-          placeholder="可选,满足条件才执行此步骤"
-        />
-      </el-form-item>
-
       <el-form-item label="失败重试">
         <el-switch v-model="form.retryOnFail" :active-value="1" :inactive-value="0" />
       </el-form-item>
@@ -285,16 +305,6 @@
         <el-input-number v-model="form.retryTimes" :min="1" :max="5" />
       </el-form-item>
 
-      <el-form-item label="重试间隔" v-if="form.retryOnFail === 1">
-        <el-input-number v-model="form.retryInterval" :min="1" style="width: 180px" />
-        <span style="margin-left: 8px; color: #909399">秒</span>
-      </el-form-item>
-
-      <el-form-item label="失败继续">
-        <el-switch v-model="form.continueOnFail" :active-value="1" :inactive-value="0" />
-        <span class="form-tip">失败后是否继续执行后续步骤</span>
-      </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>
@@ -312,8 +322,8 @@
 </template>
 
 <script>
-import { listAbility } from '@/api/basecfg/objAbility';
-import { getObjAttr } from '@/api/basecfg/objAttribute';
+import { getModelByCode } from '@/api/basecfg/objModel';
+import { listSubsystemAll } from '@/api/adapter/subsystem';
 import ConditionBuilder from './ConditionBuilder.vue';
 
 export default {
@@ -338,25 +348,36 @@ export default {
   data() {
     return {
       form: {},
+      systemList: [],
       abilityList: [],
       currentAbility: null,
-      currentAbilityParams: [],
+      loadingModel: false,
+
       staticParams: {},
-      paramMapping: {},
-      deviceAttrOptions: []
+      staticParamValue: null,
+      paramMappingStr: '',
+      paramDef: null
     };
   },
 
   computed: {
     allVariables() {
-      // 合并上下文变量和设备属性
-      return [
-        ...this.contextVariables.map(v => ({
-          key: 'context.' + v.varKey,
-          name: v.varName,
-          type: 'context'
-        }))
-      ];
+      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;
+      }
     }
   },
 
@@ -376,121 +397,323 @@ export default {
         }
 
         if (val.paramMapping) {
-          try {
-            this.paramMapping = JSON.parse(val.paramMapping);
-          } catch (e) {
-            this.paramMapping = {};
-          }
+          this.paramMappingStr = val.paramMapping;
         }
 
-        // 加载设备能力
-        if (val.targetObjCode) {
-          this.loadDeviceAbilities(val.targetObjCode);
+        // 加载能力列表
+        if (val.targetObjCode && val.targetModelCode) {
+          this.loadObjectModel(val.targetModelCode);
         }
       }
     },
     form: {
       deep: true,
       handler(val) {
-        // 同步参数到form
-        if (val.paramSource === 'STATIC') {
-          val.abilityParam = JSON.stringify(this.staticParams);
-        } else {
-          val.paramMapping = JSON.stringify(this.paramMapping);
-        }
-
         this.$emit('change', val);
       }
-    },
-    staticParams: {
-      deep: true,
-      handler() {
-        if (this.form.paramSource === 'STATIC') {
-          this.form.abilityParam = JSON.stringify(this.staticParams);
-        }
-      }
-    },
-    paramMapping: {
-      deep: true,
-      handler() {
-        if (this.form.paramSource !== 'STATIC') {
-          this.form.paramMapping = JSON.stringify(this.paramMapping);
-        }
-      }
     }
   },
 
   created() {
-    this.buildDeviceAttrOptions();
+    this.loadSystems();
   },
 
   methods: {
-    async handleDeviceChange(deviceCode) {
-      const device = this.deviceList.find(d => d.deviceCode === deviceCode);
-      if (device) {
-        this.form.targetModelCode = device.modelCode;
-        await this.loadDeviceAbilities(deviceCode);
+    // 加载系统列表
+    async loadSystems() {
+      try {
+        const response = await listSubsystemAll();
+        this.systemList = response.data || [];
+      } catch (error) {
+        console.error('加载系统列表失败', error);
+        this.systemList = [];
+      }
+    },
+
+    // 对象类型改变
+    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;
+        }
+      }
+
+      this.form.targetModelCode = modelCode;
+
+      if (modelCode) {
+        await this.loadObjectModel(modelCode);
+      } else {
+        this.$message.warning('该对象未配置物模型');
+        this.abilityList = [];
       }
     },
 
-    async loadDeviceAbilities(deviceCode) {
-      const device = this.deviceList.find(d => d.deviceCode === deviceCode);
-      if (!device) return;
+    // 加载物模型(核心方法)
+    async loadObjectModel(modelCode) {
+      if (!modelCode) return;
 
+      this.loadingModel = true;
       try {
-        const response = await listAbility({ modelCode: device.modelCode });
-        this.abilityList = response.rows || [];
+        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);
         }
+
       } catch (error) {
-        console.error('加载能力列表失败', error);
+        console.error('加载物模型失败', error);
+        this.$message.error('加载物模型失败: ' + (error.message || '未知错误'));
+        this.abilityList = [];
+      } finally {
+        this.loadingModel = false;
       }
     },
 
+    // 能力改变
     handleAbilityChange(abilityKey) {
       const ability = this.abilityList.find(a => a.abilityKey === abilityKey);
       this.currentAbility = ability;
+      this.form.abilityName = ability?.abilityName;
 
-      // 解析能力参数定义
-      if (ability && ability.inputParam) {
+      // 解析参数定义
+      if (ability && ability.paramDefinition) {
         try {
-          this.currentAbilityParams = JSON.parse(ability.inputParam);
+          this.paramDef = JSON.parse(ability.paramDefinition);
+          console.log('参数定义:', this.paramDef);
+
+          // 初始化参数值
+          this.initStaticParams();
         } catch (e) {
-          this.currentAbilityParams = [];
+          console.error('解析参数定义失败', e);
+          this.paramDef = null;
         }
       } else {
-        // 提供默认参数示例
-        this.currentAbilityParams = [
-          { paramKey: 'value', paramName: '参数值', paramType: 'STRING', placeholder: '请输入参数值' }
-        ];
+        this.paramDef = null;
       }
+    },
 
-      // 初始化参数值
-      this.currentAbilityParams.forEach(param => {
-        if (!(param.paramKey in this.staticParams)) {
-          this.$set(this.staticParams, param.paramKey, param.defaultValue || null);
-        }
-        if (!(param.paramKey in this.paramMapping)) {
-          this.$set(this.paramMapping, param.paramKey, null);
+    // 初始化静态参数
+    initStaticParams() {
+      if (!this.paramDef) return;
+
+      // 根据参数定义类型初始化默认值
+      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);
+      }
+
+      // 如果已有参数值,使用已有值
+      if (this.staticParams && Object.keys(this.staticParams).length > 0) {
+        this.staticParamValue = Object.values(this.staticParams)[0];
+      }
     },
 
+    // 静态参数改变
+    handleStaticParamChange(value) {
+      // 根据能力键名构建参数对象
+      this.staticParams = { value: value };
+      this.form.abilityParam = JSON.stringify(this.staticParams);
+    },
+
+    // 参数来源改变
     handleParamSourceChange() {
-      // 切换参数来源时清空映射
-      this.paramMapping = {};
+      this.staticParams = {};
+      this.paramMappingStr = '';
     },
 
-    buildDeviceAttrOptions() {
-      // 构建设备-属性级联选项
-      this.deviceAttrOptions = this.deviceList.map(device => ({
-        code: device.deviceCode,
-        name: device.deviceName,
-        children: [] // 延迟加载
-      }));
+    // 插入变量
+    insertVariable(varKey) {
+      this.paramMappingStr += `\${context.${varKey}}`;
+    },
+
+    // 显示参数帮助
+    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'
+      });
+    },
+
+    // 根据参数定义获取组件
+    getParamComponent(def) {
+      if (!def) return 'ParamInput';
+
+      if (def.type === 'Options') return 'ParamOptions';
+      if (def.type === 'Slider') return 'ParamSlider';
+      return 'ParamInput';
+    },
+
+    // 辅助方法
+    getStepTypeName(type) {
+      const map = {
+        'ABILITY': '能力调用',
+        'DELAY': '延时等待',
+        'CONDITION': '条件判断',
+        'PARALLEL': '并行执行',
+        'LOOP': '循环执行'
+      };
+      return map[type] || type;
+    },
+
+    getStepTypeTag(type) {
+      const map = {
+        'ABILITY': '',
+        'DELAY': 'success',
+        'CONDITION': 'warning',
+        'PARALLEL': 'info',
+        'LOOP': 'danger'
+      };
+      return map[type] || '';
+    },
+
+    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';
+    }
+  },
+
+  // 动态参数组件
+  components: {
+    ConditionBuilder,
+
+    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);
+        }
+      }
+    },
+
+    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);
+        }
+      }
     }
   }
 };
@@ -505,24 +728,52 @@ export default {
 
     .delay-tag {
       cursor: pointer;
+      transition: all 0.2s;
 
       &:hover {
         background: #ecf5ff;
         color: #409eff;
+        transform: scale(1.05);
       }
     }
   }
 
-  .param-desc {
+  .param-definition-preview {
+    background: #f5f7fa;
+    padding: 8px;
+    border-radius: 4px;
     font-size: 12px;
-    color: #909399;
-    margin-top: 4px;
+    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;
+
+      &:hover {
+        background: #ecf5ff;
+        color: #409eff;
+        transform: scale(1.05);
+      }
+    }
   }
 
   .form-tip {
     font-size: 12px;
     color: #909399;
-    margin-left: 12px;
+    margin-top: 4px;
   }
 }
 </style>

+ 328 - 71
ems-ui-cloud/src/views/mgr/strategy/components/TriggerConfig.vue

@@ -27,19 +27,20 @@
         <el-divider content-position="left">事件源配置</el-divider>
 
         <el-form-item label="对象类型">
-          <el-radio-group v-model="form.sourceObjType">
+          <el-radio-group v-model="form.sourceObjType" @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.sourceObjType === 2">
           <el-select
             v-model="form.sourceObjCode"
             filterable
             placeholder="搜索选择设备"
             style="width: 100%"
-            @change="handleDeviceChange"
+            @change="handleObjChange"
           >
             <el-option
               v-for="device in deviceList"
@@ -53,21 +54,40 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="系统代码" v-else>
-          <el-input v-model="form.sourceObjCode" placeholder="请输入系统代码" />
+        <!-- 系统选择 -->
+        <el-form-item label="选择系统" v-else-if="form.sourceObjType === 3">
+          <el-select
+            v-model="form.sourceObjCode"
+            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.sourceModelCode" placeholder="自动关联或手动输入" />
+          <el-input v-model="form.sourceModelCode" placeholder="自动关联" disabled />
         </el-form-item>
 
-        <el-form-item label="事件标识">
+        <!-- 事件选择 - 从物模型加载 -->
+        <el-form-item label="监听事件">
           <el-select
             v-model="form.eventKey"
             filterable
-            allow-create
-            placeholder="选择或输入事件标识"
+            placeholder="请选择事件"
             style="width: 100%"
+            :loading="loadingModel"
+            :disabled="!form.sourceObjCode"
           >
             <el-option
               v-for="event in eventList"
@@ -75,10 +95,21 @@
               :label="event.eventName"
               :value="event.eventKey"
             >
-              <span>{{ event.eventName }}</span>
-              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ event.eventKey }}</span>
+              <div style="display: flex; justify-content: space-between; align-items: center;">
+                <span>
+                  <i class="el-icon-bell" style="color: #f56c6c; margin-right: 4px"></i>
+                  {{ event.eventName }}
+                </span>
+                <el-tag size="mini" type="info">{{ event.eventKey }}</el-tag>
+              </div>
             </el-option>
           </el-select>
+          <div class="form-tip" v-if="selectedEvent">
+            {{ selectedEvent.eventDesc || '暂无描述' }}
+          </div>
+          <div class="form-tip" v-else-if="!loadingModel && eventList.length === 0 && form.sourceObjCode">
+            ⚠️ 该对象暂无事件定义
+          </div>
         </el-form-item>
       </template>
 
@@ -87,19 +118,20 @@
         <el-divider content-position="left">属性监控配置</el-divider>
 
         <el-form-item label="对象类型">
-          <el-radio-group v-model="form.sourceObjType">
+          <el-radio-group v-model="form.sourceObjType" @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.sourceObjType === 2">
           <el-select
             v-model="form.sourceObjCode"
             filterable
             placeholder="搜索选择设备"
             style="width: 100%"
-            @change="handleDeviceChange"
+            @change="handleObjChange"
           >
             <el-option
               v-for="device in deviceList"
@@ -110,28 +142,85 @@
           </el-select>
         </el-form-item>
 
+        <!-- 系统选择 -->
+        <el-form-item label="选择系统" v-else-if="form.sourceObjType === 3">
+          <el-select
+            v-model="form.sourceObjCode"
+            filterable
+            placeholder="搜索选择系统"
+            style="width: 100%"
+            @change="handleObjChange"
+          >
+            <el-option
+              v-for="system in systemList"
+              :key="system.systemCode"
+              :label="system.systemName"
+              :value="system.systemCode"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="模型代码">
+          <el-input v-model="form.sourceModelCode" placeholder="自动关联" disabled />
+        </el-form-item>
+
+        <!-- 属性选择 - 从物模型加载 -->
         <el-form-item label="监控属性">
           <el-select
             v-model="form.attrKey"
             filterable
-            allow-create
-            placeholder="选择或输入属性标识"
+            placeholder="请选择属性"
             style="width: 100%"
+            :loading="loadingModel"
+            :disabled="!form.sourceObjCode"
+            @change="handleAttrChange"
           >
-            <el-option
-              v-for="attr in attrList"
-              :key="attr.attrKey"
-              :label="attr.attrName"
-              :value="attr.attrKey"
+            <el-option-group
+              v-for="group in attrGroups"
+              :key="group.name"
+              :label="group.label"
             >
-              <span>{{ attr.attrName }}</span>
-              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ attr.attrKey }}</span>
-            </el-option>
+              <el-option
+                v-for="attr in group.attrs"
+                :key="attr.attrKey"
+                :label="attr.attrName"
+                :value="attr.attrKey"
+              >
+                <div style="display: flex; justify-content: space-between; align-items: center;">
+                  <span>
+                    <i class="el-icon-document" style="color: #409eff; margin-right: 4px"></i>
+                    {{ attr.attrName }}
+                  </span>
+                  <div>
+                    <el-tag size="mini" type="info" style="margin-right: 4px">{{ attr.attrKey }}</el-tag>
+                    <el-tag size="mini" v-if="attr.attrUnit">{{ attr.attrUnit }}</el-tag>
+                  </div>
+                </div>
+              </el-option>
+            </el-option-group>
           </el-select>
+          <div class="form-tip" v-if="!loadingModel && attrList.length === 0 && form.sourceObjCode">
+            ⚠️ 该对象暂无属性定义
+          </div>
+        </el-form-item>
+
+        <!-- 属性类型提示 -->
+        <el-form-item label="属性类型" v-if="selectedAttr">
+          <el-tag :type="getAttrTypeTag(selectedAttr.attrValueType)">
+            {{ getAttrTypeName(selectedAttr.attrValueType) }}
+          </el-tag>
+          <span style="margin-left: 8px; color: #909399; font-size: 12px">
+            {{ selectedAttr.attrUnit || '无单位' }}
+          </span>
         </el-form-item>
 
-        <el-form-item label="触发条件">
-          <condition-builder v-model="form.conditionExpr" :attr-key="form.attrKey" />
+        <!-- 触发条件 -->
+        <el-form-item label="触发条件" v-if="form.attrKey">
+          <condition-builder
+            v-model="form.conditionExpr"
+            :attr-key="form.attrKey"
+            :attr-info="selectedAttr"
+          />
         </el-form-item>
       </template>
 
@@ -159,12 +248,6 @@
             </el-tag>
           </div>
         </el-form-item>
-
-        <el-form-item label="执行预览">
-          <div class="cron-preview">
-            <p v-for="(time, idx) in cronPreview" :key="idx">{{ time }}</p>
-          </div>
-        </el-form-item>
       </template>
 
       <!-- 条件触发配置 -->
@@ -172,7 +255,12 @@
         <el-divider content-position="left">条件配置</el-divider>
 
         <el-form-item label="条件表达式">
-          <condition-builder v-model="form.conditionExpr" :variables="availableVariables" />
+          <condition-builder
+            v-model="form.conditionExpr"
+            :variables="availableVariables"
+            :device-list="deviceList"
+            :show-attr-select="true"
+          />
         </el-form-item>
       </template>
 
@@ -190,7 +278,8 @@
 </template>
 
 <script>
-import { getObjAttr } from '@/api/basecfg/objAttribute';
+import { getModelByCode } from '@/api/basecfg/objModel';
+import { listSubsystemAll } from '@/api/adapter/subsystem';
 import ConditionBuilder from './ConditionBuilder.vue';
 
 export default {
@@ -210,11 +299,25 @@ export default {
 
   data() {
     return {
-      form: {},
-      eventList: [],
-      attrList: [],
-      cronExpression: '',
+      form: {
+        triggerName: '',
+        triggerType: 'EVENT',
+        sourceObjType: 2,
+        sourceObjCode: '',
+        sourceModelCode: '',
+        eventKey: '',
+        attrKey: '',
+        conditionExpr: '',
+        priority: 50,
+        enable: 1
+      },
+
+      systemList: [], // 系统列表
+      eventList: [],  // 物模型事件列表
+      attrList: [],   // 物模型属性列表
+      loadingModel: false,
 
+      cronExpression: '',
       cronShortcuts: [
         { label: '每天8点', value: '0 0 8 * * ?' },
         { label: '每天0点', value: '0 0 0 * * ?' },
@@ -229,9 +332,32 @@ export default {
   },
 
   computed: {
-    cronPreview() {
-      // TODO: 解析CRON表达式,返回接下来几次执行时间
-      return ['计算中...'];
+    // 选中的事件
+    selectedEvent() {
+      return this.eventList.find(e => e.eventKey === this.form.eventKey);
+    },
+
+    // 选中的属性
+    selectedAttr() {
+      return this.attrList.find(a => a.attrKey === this.form.attrKey);
+    },
+
+    // 属性分组
+    attrGroups() {
+      const groups = {};
+      this.attrList.forEach(attr => {
+        const groupName = attr.attrGroup || 'Default';
+        if (!groups[groupName]) {
+          groups[groupName] = [];
+        }
+        groups[groupName].push(attr);
+      });
+
+      return Object.entries(groups).map(([name, attrs]) => ({
+        name,
+        label: this.getGroupLabel(name),
+        attrs
+      }));
     }
   },
 
@@ -239,9 +365,23 @@ export default {
     trigger: {
       immediate: true,
       handler(val) {
-        this.form = { ...val };
-        if (val.sourceObjCode) {
-          this.loadDeviceAttrs(val.sourceObjCode);
+        this.form = {
+          triggerName: '',
+          triggerType: 'EVENT',
+          sourceObjType: 2,
+          sourceObjCode: '',
+          sourceModelCode: '',
+          eventKey: '',
+          attrKey: '',
+          conditionExpr: '',
+          priority: 50,
+          enable: 1,
+          ...val
+        };
+
+        // 如果已有对象代码,加载物模型
+        if (val.sourceObjCode && val.sourceModelCode) {
+          this.loadObjectModel(val.sourceModelCode);
         }
       }
     },
@@ -253,40 +393,166 @@ export default {
     }
   },
 
+  created() {
+    this.loadSystems();
+  },
+
   methods: {
+    // 加载系统列表
+    async loadSystems() {
+      try {
+        const response = await listSubsystemAll();
+        this.systemList = response.data || [];
+      } catch (error) {
+        console.error('加载系统列表失败', error);
+        this.systemList = [];
+      }
+    },
+
+    // 触发类型改变
     handleTypeChange() {
-      // 切换类型时清空相关字段
       this.form.eventKey = null;
       this.form.attrKey = null;
       this.form.conditionExpr = null;
+      this.eventList = [];
+      this.attrList = [];
     },
 
-    async handleDeviceChange(deviceCode) {
-      const device = this.deviceList.find(d => d.deviceCode === deviceCode);
-      if (device) {
-        this.form.sourceModelCode = device.modelCode;
-        await this.loadDeviceAttrs(deviceCode);
+    // 对象类型改变
+    handleObjTypeChange() {
+      this.form.sourceObjCode = null;
+      this.form.sourceModelCode = null;
+      this.form.eventKey = null;
+      this.form.attrKey = null;
+      this.eventList = [];
+      this.attrList = [];
+    },
+
+    // 对象改变(设备或系统)
+    async handleObjChange(objCode) {
+      let modelCode = null;
+
+      if (this.form.sourceObjType === 2) {
+        // 设备 - 使用 deviceModel
+        const device = this.deviceList.find(d => d.deviceCode === objCode);
+        if (device) {
+          modelCode = device.modelCode || device.deviceModel;
+        }
+      } else if (this.form.sourceObjType === 3) {
+        // 系统
+        const system = this.systemList.find(s => s.systemCode === objCode);
+        if (system) {
+          modelCode = system.modelCode;
+        }
+      }
+
+      this.form.sourceModelCode = modelCode;
+
+      if (modelCode) {
+        await this.loadObjectModel(modelCode);
+      } else {
+        this.$message.warning('该对象未配置物模型');
+        this.eventList = [];
+        this.attrList = [];
       }
     },
 
-    async loadDeviceAttrs(deviceCode) {
+    // 加载物模型(核心方法)
+    async loadObjectModel(modelCode) {
+      if (!modelCode) return;
+
+      this.loadingModel = true;
       try {
-        const response = await getObjAttr(2, deviceCode);
-        this.attrList = response.data || [];
+        const response = await getModelByCode(modelCode);
+        const modelData = response.data;
+
+        if (!modelData) {
+          throw new Error('物模型数据为空');
+        }
+
+        // 加载属性列表
+        this.attrList = modelData.attrList || [];
+
+        // 加载事件列表
+        this.eventList = modelData.eventList || [];
+
+        console.log('物模型加载成功:', {
+          modelCode,
+          attrCount: this.attrList.length,
+          eventCount: this.eventList.length
+        });
+
       } catch (error) {
-        console.error('加载属性失败', error);
+        console.error('加载物模型失败', error);
+        this.$message.error('加载物模型失败: ' + (error.message || '未知错误'));
+        this.attrList = [];
+        this.eventList = [];
+      } finally {
+        this.loadingModel = false;
       }
     },
 
+    // 属性改变
+    handleAttrChange(attrKey) {
+      const attr = this.attrList.find(a => a.attrKey === attrKey);
+      console.log('选中属性:', attr);
+    },
+
+    // 设置CRON表达式
     setCron(value) {
       this.cronExpression = value;
-      // 将CRON表达式存入条件表达式字段
       this.form.conditionExpr = JSON.stringify({ cron: value });
     },
 
+    // 显示CRON帮助
     showCronHelper() {
-      // TODO: 显示CRON表达式帮助
-      this.$message.info('CRON帮助文档开发中');
+      this.$alert(`
+        <div style="line-height: 1.8">
+          <p><b>CRON表达式格式:</b>秒 分 时 日 月 周</p>
+          <p><b>常用示例:</b></p>
+          <p>• 0 0 8 * * ? - 每天8点执行</p>
+          <p>• 0 0/30 * * * ? - 每30分钟执行</p>
+          <p>• 0 0 9-18 * * ? - 每天9-18点整点执行</p>
+          <p>• 0 0 8 ? * MON-FRI - 工作日8点执行</p>
+        </div>
+      `, 'CRON表达式帮助', {
+        dangerouslyUseHTMLString: true
+      });
+    },
+
+    // 辅助方法
+    getGroupLabel(groupName) {
+      const labelMap = {
+        'Base': '基础信息',
+        'State': '状态信息',
+        'Protocol': '协议信息',
+        'Measure': '测量数据',
+        'Control': '控制参数',
+        'Default': '其他'
+      };
+      return labelMap[groupName] || groupName;
+    },
+
+    getAttrTypeName(type) {
+      const typeMap = {
+        'Value': '数值',
+        'String': '字符串',
+        'Enum': '枚举',
+        'Boolean': '布尔',
+        'WebPic': '图片',
+        'Object': '对象'
+      };
+      return typeMap[type] || type;
+    },
+
+    getAttrTypeTag(type) {
+      const tagMap = {
+        'Value': '',
+        'String': 'info',
+        'Enum': 'warning',
+        'Boolean': 'success'
+      };
+      return tagMap[type] || '';
     }
   }
 };
@@ -301,30 +567,21 @@ export default {
 
     .cron-tag {
       cursor: pointer;
+      transition: all 0.2s;
 
       &:hover {
         background: #ecf5ff;
         color: #409eff;
+        transform: scale(1.05);
       }
     }
   }
 
-  .cron-preview {
-    background: #f5f7fa;
-    border-radius: 6px;
-    padding: 10px;
-    max-height: 120px;
-    overflow-y: auto;
-
-    p {
-      margin: 0 0 4px;
-      font-size: 12px;
-      color: #606266;
-
-      &:last-child {
-        margin-bottom: 0;
-      }
-    }
+  .form-tip {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+    line-height: 1.5;
   }
 }
 </style>

+ 247 - 46
ems-ui-cloud/src/views/mgr/strategy/editor.vue

@@ -69,6 +69,26 @@
 
           <!-- 设备能力 -->
           <el-collapse-item title="设备能力" name="devices">
+            <!-- 新增:设备分类筛选 -->
+            <div class="device-category-filter">
+              <el-radio-group v-model="deviceCategoryFilter" size="mini" @change="filterDevicesByCategory">
+                <el-radio-button label="">全部</el-radio-button>
+                <el-radio-button label="E">
+                  <i class="el-icon-sunny"></i>
+                </el-radio-button>
+                <el-radio-button label="C">
+                  <i class="el-icon-coin"></i>
+                </el-radio-button>
+                <el-radio-button label="W">
+                  <i class="el-icon-connection"></i>
+                </el-radio-button>
+                <el-radio-button label="Z">
+                  <i class="el-icon-office-building"></i>
+                </el-radio-button>
+              </el-radio-group>
+
+            </div>
+
             <div class="device-search">
               <el-input
                 v-model="deviceKeyword"
@@ -78,12 +98,14 @@
                 clearable
               />
             </div>
+
             <div class="device-tree">
               <el-tree
                 :data="deviceTreeData"
                 :props="{ children: 'children', label: 'label' }"
-                node-key="id"
                 :filter-node-method="filterDeviceNode"
+                node-key="id"
+                highlight-current
                 :expand-on-click-node="false"
                 @node-click="handleDeviceNodeClick"
                 ref="deviceTree"
@@ -92,6 +114,8 @@
                   <i :class="getDeviceTreeIcon(data)"></i>
                   <span>{{ node.label }}</span>
                   <el-tag v-if="data.type === 'ability'" size="mini" type="success">能力</el-tag>
+                  <el-tag v-if="data.type === 'attribute'" size="mini" type="info">属性</el-tag>
+                  <el-tag v-if="data.type === 'event'" size="mini" type="warning">事件</el-tag>
                 </span>
               </el-tree>
             </div>
@@ -358,7 +382,7 @@ import {
   saveStrategyContext
 } from '@/api/mgr/energyStrategy';
 import { listDevRecursionByArea } from '@/api/device/device';
-import { listAbility } from '@/api/basecfg/objAbility';
+import { getModelByCode } from '@/api/basecfg/objModel';
 import TriggerConfig from './components/TriggerConfig.vue';
 import StepConfig from './components/StepConfig.vue';
 
@@ -393,7 +417,8 @@ export default {
       addStepPosition: -1,
       addVarDialogVisible: false,
       newVariable: {},
-
+      deviceCategoryFilter: '',
+      deviceSearchKeyword: '',
       triggerComponents: [
         { type: 'EVENT', name: '事件触发', icon: 'el-icon-lightning', color: '#f56c6c' },
         { type: 'ATTR', name: '属性变化', icon: 'el-icon-data-line', color: '#e6a23c' },
@@ -434,21 +459,6 @@ export default {
       if (this.selectedNodeType === 'trigger') return '触发器配置';
       if (this.selectedNodeType === 'step') return '步骤配置';
       return '属性';
-    },
-    // 获取返回的基础路径(去掉 /editor/:strategyCode 部分)
-    parentPath() {
-      const path = this.$route.path;
-      // 移除 /editor/xxx 部分
-      const idx = path.indexOf('/editor/');
-      if (idx > -1) {
-        return path.substring(0, idx);
-      }
-      // 兜底:使用 matched 路由
-      const matched = this.$route.matched;
-      if (matched.length >= 2) {
-        return matched[matched.length - 2].path;
-      }
-      return '/mgr/strategy';
     }
   },
 
@@ -459,19 +469,13 @@ export default {
   },
 
   created() {
-    // 调试:打印路由信息
-    console.log('=== 编排页面路由调试 ===');
-    console.log('完整路径:', this.$route.path);
-    console.log('fullPath:', this.$route.fullPath);
-    console.log('params:', JSON.stringify(this.$route.params));
-    console.log('query:', JSON.stringify(this.$route.query));
-
     // 同时支持 params 和 query
     this.strategyCode = this.$route.params.strategyCode || this.$route.query.strategyCode;
     console.log('解析到的 strategyCode:', this.strategyCode);
 
     if (this.strategyCode) {
       this.loadStrategy();
+      this.loadDevices();
       this.loadTriggers();
       this.loadSteps();
     } else {
@@ -538,38 +542,82 @@ export default {
 
     async loadDevices() {
       try {
-        const response = await listDevRecursionByArea({ pageNum: 1, pageSize: 1000 });
-        this.deviceList = response.rows || [];
+        const params = {
+          pageNum: 1,
+          pageSize: 1000
+        };
+
+        // ✅ 如果策略有区域代码,则按区域筛选设备
+        if (this.strategyData.areaCode) {
+          params.areaCode = this.strategyData.areaCode;
+          console.log('按区域加载设备:', this.strategyData.areaCode);
+        }
+
+        const response = await listDevRecursionByArea(params);
+        this.deviceList = (response.rows || []).map(device => ({
+          ...device,
+          modelCode: device.deviceModel
+        }));
+
+        console.log('设备加载成功:', {
+          areaCode: this.strategyData.areaCode,
+          deviceCount: this.deviceList.length
+        });
+
         this.buildDeviceTree();
       } catch (error) {
         console.error('加载设备失败', error);
+        this.deviceList = [];
       }
     },
 
     buildDeviceTree() {
       const categoryMap = {
-        E: { label: '能源生产系统', icon: 'el-icon-sunny', children: [] },
-        C: { label: '存储系统', icon: 'el-icon-coin', children: [] },
-        W: { label: '传输系统', icon: 'el-icon-connection', children: [] },
-        Z: { label: '用能系统', icon: 'el-icon-office-building', children: [] }
+        E: { label: '能源生产系统', icon: 'el-icon-sunny', children: [], gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
+        C: { label: '存储系统', icon: 'el-icon-coin', children: [], gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
+        W: { label: '传输系统', icon: 'el-icon-connection', children: [], gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
+        Z: { label: '用能系统', icon: 'el-icon-office-building', children: [], gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
+        SYS: { label: '子系统', icon: 'el-icon-set-up', children: [], gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }
       };
 
+      // 处理设备节点
       this.deviceList.forEach(device => {
-        const cat = device.deviceCategory || 'Z';
+        // 从 deviceCategory 提取大类字母(如 E5 -> E)
+        const mainCategory = device.deviceCategory ? device.deviceCategory.charAt(0) : 'Z';
+
         const node = {
           id: device.deviceCode,
           label: device.deviceName,
           type: 'device',
+          objType: 2, // 设备类型
+          modelCode: device.modelCode,
           data: device,
           children: []
         };
-        if (categoryMap[cat]) {
-          categoryMap[cat].children.push(node);
+
+        if (categoryMap[mainCategory]) {
+          categoryMap[mainCategory].children.push(node);
         } else {
           categoryMap['Z'].children.push(node);
         }
       });
 
+      // 处理系统节点
+      if (this.subsystemList && this.subsystemList.length > 0) {
+        this.subsystemList.forEach(system => {
+          const node = {
+            id: system.systemCode,
+            label: system.systemName,
+            type: 'system',
+            objType: 3, // 系统类型
+            modelCode: system.modelCode,
+            data: system,
+            children: []
+          };
+          categoryMap['SYS'].children.push(node);
+        });
+      }
+
       this.deviceTreeData = Object.entries(categoryMap)
         .filter(([_, v]) => v.children.length > 0)
         .map(([code, cat]) => ({
@@ -577,33 +625,154 @@ export default {
           label: cat.label,
           icon: cat.icon,
           type: 'category',
+          gradient: cat.gradient,
           children: cat.children
         }));
     },
 
+    async filterDevicesByCategory(category) {
+      this.deviceCategoryFilter = category;
+      try {
+        const params = {
+          pageNum: 1,
+          pageSize: 1000
+        };
+
+        if (category) {
+          params.deviceCategory = category;
+        }
+
+        const response = await listDevRecursionByArea(params);
+        this.deviceList = (response.rows || []).map(device => ({
+          ...device,
+          modelCode: device.deviceModel
+        }));
+        this.buildDeviceTree();
+      } catch (error) {
+        console.error('筛选设备失败', error);
+      }
+    },
+
     filterDeviceNode(value, data) {
       if (!value) return true;
       return data.label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
     },
 
     async handleDeviceNodeClick(data, node) {
-      if (data.type === 'device' && data.children.length === 0) {
+      // 点击分类节点,不做处理
+      if (data.type === 'category') return;
+
+      // 点击设备或系统节点,加载物模型
+      if ((data.type === 'device' || data.type === 'system') && data.children.length === 0) {
+        if (!data.modelCode) {
+          this.$message.warning(`该${data.type === 'device' ? '设备' : '系统'}未配置物模型`);
+          return;
+        }
+
         try {
-          const response = await listAbility({ modelCode: data.data.modelCode });
-          const abilities = response.data || response.rows || [];
-          data.children = abilities.map(ab => ({
-            id: data.id + '-' + ab.abilityKey,
-            label: ab.abilityName,
-            type: 'ability',
-            data: { ...ab, deviceCode: data.id, deviceName: data.label }
-          }));
+          // ✅ 直接使用现有接口
+          const response = await getModelByCode(data.modelCode);
+          const modelData = response.data;
+
+          if (!modelData) {
+            this.$message.error('获取物模型失败');
+            return;
+          }
+
+          // 构建子节点:属性、能力、事件
+          const children = [];
+
+          // 1. 属性节点组
+          if (modelData.attrList && modelData.attrList.length > 0) {
+            const attrGroup = {
+              id: `${data.id}-attrs`,
+              label: `属性 (${modelData.attrList.length})`,
+              type: 'attr-group',
+              icon: 'el-icon-data-line',
+              children: modelData.attrList.map(attr => ({
+                id: `${data.id}-attr-${attr.attrKey}`,
+                label: `${attr.attrName} (${attr.attrKey})`,
+                type: 'attribute',
+                icon: 'el-icon-document',
+                data: {
+                  ...attr,
+                  objCode: data.id,
+                  objType: data.objType,
+                  modelCode: data.modelCode
+                }
+              }))
+            };
+            children.push(attrGroup);
+          }
+
+          // 2. 能力节点组
+          if (modelData.abilityList && modelData.abilityList.length > 0) {
+            const abilityGroup = {
+              id: `${data.id}-abilities`,
+              label: `能力 (${modelData.abilityList.length})`,
+              type: 'ability-group',
+              icon: 'el-icon-s-operation',
+              children: modelData.abilityList.map(ability => ({
+                id: `${data.id}-ability-${ability.abilityKey}`,
+                label: `${ability.abilityName} (${ability.abilityKey})`,
+                type: 'ability',
+                icon: 'el-icon-magic-stick',
+                data: {
+                  ...ability,
+                  objCode: data.id,
+                  objType: data.objType,
+                  modelCode: data.modelCode,
+                  objName: data.label
+                }
+              }))
+            };
+            children.push(abilityGroup);
+          }
+
+          // 3. 事件节点组
+          if (modelData.eventList && modelData.eventList.length > 0) {
+            const eventGroup = {
+              id: `${data.id}-events`,
+              label: `事件 (${modelData.eventList.length})`,
+              type: 'event-group',
+              icon: 'el-icon-bell',
+              children: modelData.eventList.map(event => ({
+                id: `${data.id}-event-${event.eventKey}`,
+                label: `${event.eventName} (${event.eventKey})`,
+                type: 'event',
+                icon: 'el-icon-warning-outline',
+                data: {
+                  ...event,
+                  objCode: data.id,
+                  objType: data.objType,
+                  modelCode: data.modelCode
+                }
+              }))
+            };
+            children.push(eventGroup);
+          }
+
+          // 更新节点子树
+          this.$set(data, 'children', children);
           this.$set(node, 'expanded', true);
+
         } catch (error) {
-          console.error('加载能力失败', error);
+          console.error('加载物模型失败', error);
+          this.$message.error('加载物模型失败: ' + (error.message || '未知错误'));
         }
-      } else if (data.type === 'ability') {
+      }
+      // 点击能力节点,添加能力步骤
+      else if (data.type === 'ability') {
         this.addAbilityStep(data.data);
       }
+      // 点击属性节点,可用于条件设置
+      else if (data.type === 'attribute') {
+        this.$message.info(`属性:${data.data.attrName},可用于条件配置`);
+      }
+      // 点击事件节点,可用于触发器设置
+      else if (data.type === 'event') {
+        this.$message.info(`事件:${data.data.eventName},可用于触发器配置`);
+      }
     },
 
     addAbilityStep(abilityData) {
@@ -877,7 +1046,7 @@ export default {
 
     // 返回上级页面
     goBack() {
-      this.$router.push(this.parentPath);
+      this.$router.push('/power-mgr/strategy-mgr/strategy-index');
     },
 
     // 辅助方法
@@ -1143,4 +1312,36 @@ export default {
 }
 
 .form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
+.device-category-filter {
+  padding: 8px;
+  border-bottom: 1px solid #e4e7ed;
+
+  .el-radio-group {
+    width: 100%;
+    display: flex;
+
+    .el-radio-button {
+      flex: 1;
+
+      ::v-deep .el-radio-button__inner {
+        width: 100%;
+      }
+    }
+  }
+
+  .category-tips {
+    display: flex;
+    gap: 4px;
+    margin-top: 8px;
+    font-size: 11px;
+
+    .el-tag {
+      cursor: help;
+    }
+  }
+}
+
+.device-search {
+  padding: 8px;
+}
 </style>

+ 26 - 8
ems-ui-cloud/src/views/mgr/strategy/log.vue

@@ -183,6 +183,7 @@
           <el-descriptions :column="2" border size="small">
             <el-descriptions-item label="执行ID">{{ currentLog.execId }}</el-descriptions-item>
             <el-descriptions-item label="策略代码">{{ currentLog.strategyCode }}</el-descriptions-item>
+            <el-descriptions-item label="策略名称">{{ currentLog.strategyName || '-' }}</el-descriptions-item>
             <el-descriptions-item label="触发类型">{{ getTriggerTypeName(currentLog.triggerType) }}</el-descriptions-item>
             <el-descriptions-item label="触发源">{{ currentLog.triggerSource || '-' }}</el-descriptions-item>
             <el-descriptions-item label="执行状态">
@@ -287,8 +288,8 @@ export default {
         strategyCode: '',
         execStatus: null,
         triggerType: '',
-        startTime: '',
-        endTime: ''
+        startTimeBegin: '',
+        startTimeEnd: ''
       },
 
       stats: {
@@ -313,7 +314,6 @@ export default {
   },
 
   created() {
-
     this.loadStrategies();
     this.getList();
   },
@@ -326,6 +326,7 @@ export default {
         this.strategyList = response.rows || [];
       } catch (error) {
         console.error('加载策略列表失败', error);
+        this.$message.error('加载策略列表失败');
       }
     },
 
@@ -334,19 +335,34 @@ export default {
       this.loading = true;
       try {
         const params = { ...this.queryParams };
+
+        // 处理日期范围
         if (this.dateRange && this.dateRange.length === 2) {
-          params.startTime = this.dateRange[0] + ' 00:00:00';
-          params.endTime = this.dateRange[1] + ' 23:59:59';
+          params.startTimeBegin = this.dateRange[0] + ' 00:00:00';
+          params.startTimeEnd = this.dateRange[1] + ' 23:59:59';
+        } else {
+          params.startTimeBegin = '';
+          params.startTimeEnd = '';
         }
 
-        const response = await getExecLogList(params.strategyCode, params);
-        this.logList = response.rows || response.data || [];
+        // 清理空值参数
+        Object.keys(params).forEach(key => {
+          if (params[key] === '' || params[key] === null) {
+            delete params[key];
+          }
+        });
+
+        const response = await getExecLogList(params);
+        this.logList = response.rows || [];
         this.total = response.total || 0;
 
         // 计算统计数据
         this.calculateStats();
       } catch (error) {
         console.error('获取日志列表失败', error);
+        this.$message.error('获取日志列表失败');
+        this.logList = [];
+        this.total = 0;
       } finally {
         this.loading = false;
       }
@@ -362,6 +378,7 @@ export default {
     },
 
     refreshList() {
+      this.queryParams.pageNum = 1;
       this.getList();
       this.$message.success('刷新成功');
     },
@@ -381,6 +398,7 @@ export default {
     async viewLogDetail(log) {
       this.currentLog = log;
       this.detailDrawerVisible = true;
+      this.stepLogs = [];
 
       // 加载步骤执行日志
       try {
@@ -388,7 +406,7 @@ export default {
         this.stepLogs = (response.data || []).sort((a, b) => a.stepIndex - b.stepIndex);
       } catch (error) {
         console.error('加载步骤日志失败', error);
-        this.stepLogs = [];
+        this.$message.warning('步骤日志加载失败');
       }
     },