Quellcode durchsuchen

策略管理初版

learshaw vor 4 Monaten
Ursprung
Commit
00897d16c0

+ 319 - 5
ems-ui-cloud/src/api/mgr/energyStrategy.js

@@ -1,6 +1,12 @@
 import request from '@/utils/request'
 
-// 查询能源策略列表
+// ===========================
+// 策略基础管理
+// ===========================
+
+/**
+ * 查询策略列表
+ */
 export function listEnergyStrategy(query) {
   return request({
     url: '/ems/energyStrategy/list',
@@ -9,7 +15,9 @@ export function listEnergyStrategy(query) {
   })
 }
 
-// 查询能源策略详细
+/**
+ * 查询策略详细(通过ID)
+ */
 export function getEnergyStrategy(id) {
   return request({
     url: '/ems/energyStrategy/' + id,
@@ -17,7 +25,20 @@ export function getEnergyStrategy(id) {
   })
 }
 
-// 新增能源策略
+/**
+ * 查询策略详细(通过代码)
+ * 注意:需要后端新增此接口
+ */
+export function getEnergyStrategyByCode(strategyCode) {
+  return request({
+    url: '/ems/energyStrategy/code/' + strategyCode,
+    method: 'get'
+  })
+}
+
+/**
+ * 新增策略
+ */
 export function addEnergyStrategy(data) {
   return request({
     url: '/ems/energyStrategy',
@@ -26,7 +47,9 @@ export function addEnergyStrategy(data) {
   })
 }
 
-// 修改能源策略
+/**
+ * 修改策略
+ */
 export function updateEnergyStrategy(data) {
   return request({
     url: '/ems/energyStrategy',
@@ -35,10 +58,301 @@ export function updateEnergyStrategy(data) {
   })
 }
 
-// 删除能源策略
+/**
+ * 删除策略
+ */
 export function delEnergyStrategy(id) {
   return request({
     url: '/ems/energyStrategy/' + id,
     method: 'delete'
   })
 }
+
+/**
+ * 启用/停用策略
+ * 修复:匹配后端路径参数格式
+ */
+export function changeStrategyState(strategyCode, state) {
+  return request({
+    url: '/ems/energyStrategy/state/' + strategyCode + '/' + state,
+    method: 'put'
+  })
+}
+
+// ===========================
+// 触发器管理
+// ===========================
+
+/**
+ * 获取策略触发器列表
+ * 修复:匹配后端路径参数格式
+ */
+export function getTriggers(strategyCode) {
+  return request({
+    url: '/ems/energyStrategy/trigger/' + strategyCode,
+    method: 'get'
+  })
+}
+
+/**
+ * 保存触发器(新增或更新)
+ */
+export function saveTrigger(data) {
+  return request({
+    url: '/ems/energyStrategy/trigger',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 删除触发器
+ */
+export function deleteTrigger(id) {
+  return request({
+    url: '/ems/energyStrategy/trigger/' + id,
+    method: 'delete'
+  })
+}
+
+// ===========================
+// 步骤管理
+// ===========================
+
+/**
+ * 获取策略步骤列表
+ * 修复:匹配后端路径
+ */
+export function getStrategySteps(strategyCode) {
+  return request({
+    url: '/ems/energyStrategy/step',
+    method: 'get',
+    params: { strategyCode }
+  })
+}
+
+/**
+ * 新增步骤
+ */
+export function addStrategyStep(data) {
+  return request({
+    url: '/ems/energyStrategy/step',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 修改步骤
+ */
+export function updateStrategyStep(data) {
+  return request({
+    url: '/ems/energyStrategy/step',
+    method: 'put',
+    data: data
+  })
+}
+
+/**
+ * 删除步骤
+ */
+export function deleteStrategyStep(id) {
+  return request({
+    url: '/ems/energyStrategy/step/' + id,
+    method: 'delete'
+  })
+}
+
+/**
+ * 批量保存步骤
+ */
+export function saveStrategyStepBatch(steps) {
+  return request({
+    url: '/ems/energyStrategy/step/batch',
+    method: 'put',
+    data: 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'
+  })
+}
+
+/**
+ * 批量保存上下文变量
+ */
+export function saveStrategyContextBatch(strategyCode, variables) {
+  return request({
+    url: '/ems/energyStrategy/context/batch',
+    method: 'post',
+    data: { strategyCode, variables }
+  })
+}
+
+// ===========================
+// 策略参数管理
+// ===========================
+
+/**
+ * 获取策略参数
+ */
+export function getStrategyParam(strategyCode, paramGroup) {
+  return request({
+    url: '/ems/energyStrategy/param',
+    method: 'get',
+    params: { strategyCode, paramGroup }
+  })
+}
+
+/**
+ * 更新策略参数
+ */
+export function updateStrategyParam(data) {
+  return request({
+    url: '/ems/energyStrategy/param',
+    method: 'put',
+    data: data
+  })
+}
+
+/**
+ * 获取参数选项(下拉列表)
+ */
+export function getStrategyParamOption(strategyType, paramKey) {
+  return request({
+    url: '/ems/energyStrategy/param/option',
+    method: 'get',
+    params: { strategyType, paramKey }
+  })
+}
+
+// ===========================
+// 策略执行
+// ===========================
+
+/**
+ * 手动执行策略
+ * 修复:匹配后端路径参数格式
+ */
+export function executeStrategy(strategyCode, params) {
+  return request({
+    url: '/ems/energyStrategy/execute/' + strategyCode,
+    method: 'post',
+    data: 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 getExecLog(execId) {
+  return request({
+    url: '/ems/energyStrategy/execLog/' + execId,
+    method: 'get'
+  })
+}
+
+// ===========================
+// 策略模板
+// 注意:需要后端新增这些接口
+// ===========================
+
+/**
+ * 获取策略模板列表
+ */
+export function listStrategyTemplate(query) {
+  return request({
+    url: '/ems/energyStrategy/template/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 获取模板详情
+ */
+export function getStrategyTemplate(templateCode) {
+  return request({
+    url: '/ems/energyStrategy/template/' + templateCode,
+    method: 'get'
+  })
+}
+
+/**
+ * 从模板创建策略
+ */
+export function createFromTemplate(templateCode, strategyData) {
+  return request({
+    url: '/ems/energyStrategy/template/create',
+    method: 'post',
+    data: { templateCode, ...strategyData }
+  })
+}
+
+/**
+ * 删除模板
+ */
+export function deleteStrategyTemplate(templateCode) {
+  return request({
+    url: '/ems/energyStrategy/template/' + templateCode,
+    method: 'delete'
+  })
+}

+ 0 - 982
ems-ui-cloud/src/views/mgr/strategy.vue

@@ -1,982 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-row :gutter="20">
-      <!-- 左侧树形区域 - 优化版 -->
-      <el-col :span="5" :xs="24">
-        <div class="head-container">
-          <el-input
-            v-model="areaName"
-            placeholder="请输入区块名称"
-            clearable
-            size="small"
-            prefix-icon="el-icon-search"
-            style="margin-bottom: 20px"
-            @input="filterTree"
-          />
-        </div>
-        <div class="head-container tree-container">
-          <el-tree
-            ref="tree"
-            :data="areaOptions"
-            :props="defaultProps"
-            :expand-on-click-node="false"
-            :filter-node-method="filterNode"
-            node-key="id"
-            :default-expanded-keys="defaultExpandedKeys"
-            highlight-current
-            @node-click="handleNodeClick"
-          >
-            <span class="custom-tree-node" slot-scope="{ node, data }">
-              <span class="tree-label">
-                <i :class="getTreeIcon(data)" class="tree-icon"></i>
-                {{ node.label }}
-              </span>
-            </span>
-          </el-tree>
-        </div>
-      </el-col>
-
-      <!-- 右侧内容区域 -->
-      <el-col :span="19" :xs="24">
-        <div class="content-wrapper">
-          <!-- 策略类型标签页 -->
-          <el-tabs v-model="strategyType" @tab-click="strategyTypeChange" class="strategy-tabs">
-            <el-tab-pane label="源网策略" name="1"></el-tab-pane>
-            <el-tab-pane label="源荷策略" name="2"></el-tab-pane>
-            <el-tab-pane label="网储策略" name="3"></el-tab-pane>
-            <el-tab-pane label="其他策略" name="4"></el-tab-pane>
-          </el-tabs>
-
-          <!-- 查询表单 -->
-          <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px" class="search-form">
-            <el-form-item label="策略名称" prop="strategyName">
-              <el-input
-                v-model="queryParams.strategyName"
-                placeholder="请输入策略名称"
-                clearable
-                @keyup.enter.native="handleQuery"
-              />
-            </el-form-item>
-            <el-form-item label="执行模式" prop="execMode">
-              <el-select v-model="queryParams.execMode" placeholder="请选择执行模式" clearable>
-                <el-option
-                  v-for="dict in dict.type.exec_mode"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
-              </el-select>
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-              <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-
-          <!-- 操作按钮行 -->
-          <el-row :gutter="10" class="mb8">
-            <el-col :span="1.5">
-              <el-button
-                type="primary"
-                plain
-                icon="el-icon-plus"
-                size="mini"
-                @click="handleAdd"
-                v-hasPermi="['ems:energyStrategy:add']"
-              >新增
-              </el-button>
-            </el-col>
-            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-          </el-row>
-
-          <!-- 数据表格 -->
-          <el-table v-loading="loading" :data="energyStrategyList" @selection-change="handleSelectionChange" class="data-table">
-            <el-table-column label="策略代码" align="center" prop="strategyCode" />
-            <el-table-column label="策略名称" align="center" prop="strategyName" />
-            <el-table-column label="策略类型" align="center" prop="strategyType">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.strategy_type" :value="scope.row.strategyType" />
-              </template>
-            </el-table-column>
-            <el-table-column label="执行模式" align="center" prop="execMode">
-              <template slot-scope="scope">
-                <dict-tag :options="dict.type.exec_mode" :value="scope.row.execMode" />
-              </template>
-            </el-table-column>
-            <el-table-column label="状态" align="center" prop="strategyState">
-              <template slot-scope="scope">
-                {{ getStateName(scope.row.strategyState) }}
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-              <template slot-scope="scope">
-                <el-button
-                  size="mini"
-                  type="text"
-                  icon="el-icon-message"
-                  @click="handleParameter(scope.row)"
-                  v-hasPermi="['basecfg:energyStrategy:edit']"
-                >参数
-                </el-button>
-                <el-button
-                  size="mini"
-                  type="text"
-                  icon="el-icon-info"
-                  @click="handleStep(scope.row)"
-                  v-hasPermi="['basecfg:energyStrategy:edit']"
-                >步骤
-                </el-button>
-                <el-button
-                  size="mini"
-                  type="text"
-                  icon="el-icon-edit"
-                  @click="handleUpdate(scope.row)"
-                  v-hasPermi="['ems:energyStrategy:edit']"
-                >修改
-                </el-button>
-                <el-button
-                  size="mini"
-                  type="text"
-                  icon="el-icon-delete"
-                  class="deleteBtn"
-                  @click="handleDelete(scope.row)"
-                  v-hasPermi="['ems:energyStrategy:remove']"
-                >删除
-                </el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-
-          <!-- 分页 -->
-          <pagination
-            v-show="total > 0"
-            :total="total"
-            :page-size.sync="queryParams.pageSize"
-            :page-sizes="[10, 20, 50]"
-            :page.sync="queryParams.pageNum"
-            @pagination="getList"
-          />
-        </div>
-      </el-col>
-    </el-row>
-
-    <!-- 参数弹框 -->
-    <el-dialog title="策略参数维护" :visible.sync="openParameterDialog" width="50%" append-to-body>
-      <div>
-        <div class="container-block">
-          <SubTitle title="默认参数" />
-        </div>
-        <div v-for="param in defaultParams" :key="param.paramName">
-          <label class="param-label">{{ param.paramName }}</label>
-          <el-input v-if="param.paramValueFormat === 'text'" v-model="param.paramValue" placeholder="请输入" @change="handleParamChange"></el-input>
-          <el-select v-if="param.paramValueFormat === 'enum'" v-model="param.paramValue" placeholder="请选择" @change="handleParamChange">
-            <el-option
-              v-for="item in paramOptions"
-              :key="item.key"
-              :label="item.name"
-              :value="item.key">
-              <el-tooltip :content="item.desc" placement="top">
-                <div>{{ item.name }}</div>
-              </el-tooltip>
-            </el-option>
-          </el-select>
-        </div>
-      </div>
-      <!-- 模型参数输入框/下拉框 -->
-      <div class="param-spacer"></div>
-      <div class="container-block">
-        <SubTitle title="模式参数" />
-      </div>
-      <div v-if="selectedParamValue === 'maxPowerTrack'">
-        <!-- 当 selectedParamValue 为 'maxPowerTrack' 时,不显示任何模式参数 -->
-      </div>
-      <div v-if="selectedParamValue === 'inverterControl'">
-        <el-select v-model="selectedParamKey" placeholder="请选择">
-          <el-option
-            v-for="item in modelParamOptions"
-            :key="item.key"
-            :label="item.name"
-            :value="item.key">
-            <el-tooltip :content="item.desc" placement="top">
-              <div>{{ item.name }}</div>
-            </el-tooltip>
-          </el-option>
-        </el-select>
-      </div>
-      <div v-if="selectedParamValue === 'powerAndVoltage'">
-        <!-- 当 selectedParamValue 为 'powerAndVoltage' 时,显示文本框 -->
-        <div v-for="item in defaultParamsOption" :key="item.paramName" class="param-container">
-          <label>{{ item.paramName }}</label>
-          <el-input v-if="item.paramValueFormat === 'text'" v-model="item.paramValue" class="param-containerInput"></el-input>
-        </div>
-      </div>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="updateOpEnergyStrategy">确认</el-button>
-        <el-button @click="openParameterDialog = false">关闭</el-button>
-      </div>
-    </el-dialog>
-
-    <!-- 步骤流程图弹框 -->
-    <el-dialog title="步骤流程图" :visible.sync="stepDialogVisible" width="80%" :style="{ height: '100vh' }">
-      <div style="display: flex;">
-        <div style="width: 50%;">
-          <el-steps direction="vertical" :active="currentStepDetail.stepIndex">
-            <el-step
-              v-for="(step, index) in strategyStepOption"
-              :key="step.id"
-              :title="step.stepName"
-              :index="index + 1"
-              @click.native="handleStepClick(step)">
-            </el-step>
-          </el-steps>
-          <el-button type="primary" icon="el-icon-plus" @click="addStep" style="position: absolute; left: 16px;  width: 30px; height: 30px; padding: 0; border-radius: 50%;"></el-button>
-        </div>
-        <div style="width: 50%; padding-left: 20px;">
-          <div v-if="currentStepDetail">
-            <el-form v-if="editMode" label-width="120px">
-              <el-form-item label="步骤名称">
-                <el-input v-model="currentStepDetail.stepName"></el-input>
-              </el-form-item>
-              <el-form-item label="步骤代码">
-                <el-input v-model="currentStepDetail.stepCode"></el-input>
-              </el-form-item>
-              <el-form-item label="步骤处理">
-                <el-input v-model="currentStepDetail.stepHandler"></el-input>
-              </el-form-item>
-              <el-form-item label="步骤参数">
-                <el-input v-model="currentStepDetail.stepParam"></el-input>
-              </el-form-item>
-              <el-form-item label="目标设施">
-                <el-input v-model="currentStepDetail.targetFacs"></el-input>
-              </el-form-item>
-              <el-form-item label="目标设备">
-                <el-input v-model="currentStepDetail.targetDevice"></el-input>
-              </el-form-item>
-              <el-form-item>
-                <el-button type="primary" @click="saveStep">保存</el-button>
-                <el-button type="primary" @click="deleteStep">删除步骤</el-button>
-                <el-button @click="cancelEdit">退出编辑</el-button>
-              </el-form-item>
-            </el-form>
-            <div v-else>
-              <h2>步骤详情:</h2>
-              <ul>
-                <li><p><strong>步骤名称:</strong> {{ currentStepDetail.stepName }}</p></li>
-                <li><p><strong>策略代码:</strong> {{ currentStepDetail.strategyCode }}</p></li>
-                <li><p><strong>步骤代码:</strong> {{ currentStepDetail.stepCode }}</p></li>
-                <li><p><strong>步骤顺序:</strong> {{ currentStepDetail.stepIndex }}</p></li>
-                <li><p><strong>步骤处理:</strong> {{ currentStepDetail.stepHandler }}</p></li>
-                <li><p><strong>步骤参数:</strong> {{ currentStepDetail.stepParam }}</p></li>
-                <li><p><strong>目标设施:</strong> {{ currentStepDetail.targetFacs }}</p></li>
-                <li><p><strong>目标设备:</strong> {{ currentStepDetail.targetDevice }}</p></li>
-              </ul>
-              <el-button type="primary" @click="editMode = true">编辑</el-button>
-              <el-button @click="stepDialogVisible = false">关闭</el-button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </el-dialog>
-
-    <!-- 添加或修改能源策略对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="地块" prop="areaCode">
-          <treeselect v-model="form.areaCode" :options="areaOptions" :show-count="true" placeholder="请选择策略区域" />
-        </el-form-item>
-        <el-form-item label="策略代码" prop="strategyCode">
-          <el-input v-model="form.strategyCode" placeholder="请输入策略代码" />
-        </el-form-item>
-        <el-form-item label="策略名称" prop="strategyName">
-          <el-input v-model="form.strategyName" placeholder="请输入策略名称" />
-        </el-form-item>
-        <el-form-item label="执行模式" prop="execMode">
-          <el-select v-model="form.execMode" placeholder="请选择执行模式" @change="handleExecModeChange">
-            <el-option
-              v-for="dict in dict.type.exec_mode"
-              :key="dict.value"
-              :label="dict.label"
-              :value="parseInt(dict.value)"
-            ></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item v-if="showExecRule" label="执行规则" prop="execRule">
-          <el-input v-model="form.execRule" placeholder="请输入执行规则" />
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { addEnergyStrategy, delEnergyStrategy, getEnergyStrategy, listEnergyStrategy, updateEnergyStrategy} from '@/api/mgr/energyStrategy';
-import { areaTreeSelect } from '@/api/basecfg/area'
-import Treeselect from '@riophae/vue-treeselect'
-import '@riophae/vue-treeselect/dist/vue-treeselect.css'
-import {
-  addStrategyStep,
-  deleteStrategyStep,
-  getStrategyParam,
-  getStrategyParamOption,
-  getStrategyStep,
-  updateOpEnergyStrategy, updateStrategyStep
-} from '@/api/mgr/OpEnergyStrategy'
-import { Tooltip } from 'element-ui';
-import SubTitle from "@/components/SubTitle/index.vue";
-import BaseChart from "@/components/BaseChart/index.vue";
-
-export default {
-  name: 'EnergyStrategy',
-  dicts: ['strategy_type', 'allow_modify', 'exec_mode'],
-  components: {BaseChart, SubTitle, Treeselect},
-  data() {
-    return {
-      // 遮罩层
-      loading: true,
-      // 选中数组
-      ids: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 能源策略表格数据
-      energyStrategyList: [],
-      // 弹出层标题
-      title: '',
-      // 是否显示弹出层
-      open: false,
-      areaCode: undefined,
-      // 区域名称
-      areaName: undefined,
-      areaOptions: [],
-      strategyType: "1",
-      showExecRule: false,
-      defaultProps: {
-        children: 'children',
-        label: 'label'
-      },
-      defaultExpandedKeys: ['-1'],
-
-      // 控制参数弹框的显示
-      openParameterDialog: false,
-      paramOptions: [], // 存储参数选项
-      selectedParamValue: '', // 存储选中的参数值
-      selectedParamKey:'',
-      defaultControlMode:'',
-      defaultParams: [], // 存储default下的参数
-      selectedParamDesc:'',
-      modelParamOptions: [], // 存储模型参数选项
-      modelParamValue: '', // 存储模型参数值
-      modeParams: [], // 存储模式参数的数据
-      currentStrategyCode:'',
-      defaultParamsOption:[],
-      paramKeys:'',//获取"controlMode": "reactivePower"、 "voltage":
-      paramValues:'',
-      savedParams: {}, // 用于存储用户保存的参数
-
-      stepDialogVisible:false,
-      strategyStepOption:[],
-      strategyStepName:'',
-      activeStep:'',
-      currentStepDetail:[],
-      editMode:true,
-      currentRow:null,
-      nextStepId: 1, // 初始ID值
-
-      // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        areaCode: null,
-        strategyType: null,
-        execMode: null
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        areaCode: [
-          {
-            required: true,
-            message: '地块不能为空',
-            trigger: 'blur',
-          },
-        ],
-        strategyCode: [
-          {
-            required: true,
-            message: '策略代码不能为空',
-            trigger: 'blur',
-          },
-        ],
-        strategyName: [
-          {
-            required: true,
-            message: '策略名称不能为空',
-            trigger: 'blur',
-          },
-        ],
-        strategyType: [
-          {
-            required: true,
-            message: '策略类型不能为空',
-            trigger: 'change',
-          },
-        ],
-        execMode: [
-          {
-            required: true,
-            message: '执行模式不能为空',
-            trigger: 'change',
-          },
-        ],
-        allowModify: [
-          {
-            required: true,
-            message: '允许修改不能为空',
-            trigger: 'change',
-          },
-        ],
-      },
-    };
-  },
-  created() {
-    this.queryParams.strategyType = 1;
-    this.getAreaTree('0', 1)
-    this.getList();
-  },
-  methods: {
-    // 获取树节点图标
-    getTreeIcon(data) {
-      if (data.id === '-1') {
-        return 'el-icon-s-home'
-      }
-      return 'el-icon-office-building'
-    },
-
-    // 过滤树
-    filterTree() {
-      this.$refs.tree.filter(this.areaName)
-    },
-
-    /** 查询能源策略列表 */
-    getList() {
-      this.loading = true;
-      listEnergyStrategy(this.queryParams).then(response => {
-        this.energyStrategyList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-      });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: null,
-        areaCode: null,
-        strategyCode: null,
-        strategyName: null,
-        strategyDesc: null,
-        strategyType: null,
-        strategyState: null,
-        execMode: null,
-        execRule: null
-      };
-      this.resetForm('form');
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNum = 1;
-      this.getList();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.resetForm('queryForm');
-      this.handleQuery();
-    },
-    // 多选框选中数据
-    handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.id);
-      this.single = selection.length !== 1;
-      this.multiple = !selection.length;
-
-    },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = '添加能源策略';
-      this.form.execMode = 99;
-      this.form.strategyType = this.strategyType;
-    },
-
-    /** 参数按钮操作 */
-    handleParameter(row) {
-      console.log("参数按钮操作!!!!!",row)
-      this.currentStrategyCode=row.strategyCode
-      this.selectedParamValue = ''; // 重置选中的参数值
-      this.openParameterDialog = true;
-      this.getStrategyParam(row.strategyCode);
-      this.getStrategyParamOption(row.strategyType, 'default.controlMode');
-      console.log("打开参数弹框时的 selectedParamValue:", this.selectedParamValue);
-    },
-
-    /** 获取默认参数 */
-    getStrategyParam(strategyCode) {
-      getStrategyParam(strategyCode).then(response => {
-        const params = response.data.default.controlMode;
-        this.defaultParams = [
-          { paramName: params.paramName, paramValue: params.paramValue, paramValueFormat: params.paramValueFormat },
-        ];
-        console.log(" 获取默认参数", this.defaultParams)
-      })
-    },
-
-    /** 获取参数选项 */
-    getStrategyParamOption(strategyType, paramKey) {
-      getStrategyParamOption({strategyType, paramKey}).then(response => {
-        // 更新控制模式下拉框的选项
-        this.paramOptions = response.data;
-      })
-    },
-
-    getStrategyParamOptions(strategyType, paramKey) {
-      getStrategyParamOption({strategyType, paramKey}).then(response => {
-        // 更新模式参数下拉框的选项
-        this.modelParamOptions = response.data;
-        console.log('更新下拉框的选项',this.modelParamOptions);
-      })
-    },
-
-    handleParamChange(value) {
-      this.selectedParamValue = value;
-      console.log("选中的参数值:", this.selectedParamValue);
-      switch (this.selectedParamValue) {
-        case 'maxPowerTrack':
-          // 无模式参数
-          this.modelParamOptions = [];
-          break;
-        case 'inverterControl':
-          getStrategyParam(this.currentStrategyCode).then(response => {
-            const inverterControlParams = response.data.inverterControl;
-            console.log("inverterControlParams",inverterControlParams)
-            this.selectedParamKey=inverterControlParams.controlMode.paramValue
-            console.log("selectedParamKey", this.selectedParamKey);
-            this.paramKeys = Object.keys(inverterControlParams);
-            console.log("Param Keys:", this.paramKeys);
-          });
-          this.getStrategyParamOptions(1, 'inverterControl.controlMode');
-          break;
-        case 'powerAndVoltage':
-          // 直接展示文本框,不需要调用接口
-          getStrategyParam(this.currentStrategyCode).then(response => {
-            const powerAndVoltageParams = response.data.powerAndVoltage;
-            this.paramKeys = Object.keys(powerAndVoltageParams);
-            this.defaultParamsOption = Object.values(powerAndVoltageParams);
-          });
-          break;
-        default:
-          console.error('未知的参数值');
-      }
-    },
-
-    updateOpEnergyStrategy() {
-      // 根据 selectedParamValue 确定参数结构
-      if (this.selectedParamValue === 'powerAndVoltage') {
-        this.paramKeys.forEach((key, index) => {
-          const requestBody = {
-            strategyCode: this.currentStrategyCode,
-            paramGroup: this.selectedParamValue,
-            paramKey: key,
-            paramValue: this.defaultParamsOption[index].paramValue,
-          };
-          console.log("requestBody",requestBody)
-          updateOpEnergyStrategy(requestBody).then(response => {
-            if (response || response.data || response.data.success) {
-              this.$message.success('参数修改成功');
-              this.openParameterDialog = false; // 关闭弹框
-            } else {
-              this.$message.error('参数修改失败');
-            }
-          });
-        });
-      }else if(this.selectedParamValue === 'inverterControl'){
-        this.paramKeys.forEach((key, index) => {
-          const requestBody = {
-            strategyCode: this.currentStrategyCode,
-            paramGroup: this.selectedParamValue,
-            paramKey: key,
-            paramValue: this.selectedParamKey,
-          };
-          console.log("requestBody",requestBody)
-          updateOpEnergyStrategy(requestBody).then(response => {
-            if (response || response.data || response.data.success) {
-              this.$message.success('参数修改成功');
-              this.openParameterDialog = false; // 关闭弹框
-            } else {
-              this.$message.error('参数修改失败');
-            }
-          });
-        });
-      }
-    },
-
-    /**步骤流程图弹框*/
-    handleStep(row){
-      this.stepDialogVisible=true;
-      this.editMode=false;
-      this.currentRow = row; // 保存当前行的数据
-      getStrategyStep(row.strategyCode).then(response => {
-        this.strategyStepOption=response.data
-        this.activeStep = 1;
-        // 默认展示第一个步骤的详情
-        this.currentStepDetail = this.strategyStepOption[0] || {};
-        this.editMode = false;
-      })
-    },
-
-    handleStepClick(step) {
-      this.currentStepDetail = step;
-      this.editMode = false;
-    },
-
-    addStep() {
-      const newStep = {
-        id:'',
-        stepName: '',
-        stepCode: '',
-        stepIndex: this.strategyStepOption.length + 1 || 1,
-        stepHandler: '',
-        strategyCode:this.currentRow.strategyCode,
-        stepParam: '',
-        targetFacs: '',
-        targetDevice: '',
-      };
-      // 将新步骤添加到策略步骤选项中
-      this.strategyStepOption.push(newStep);
-      // 更新当前步骤详情为新步骤
-      this.currentStepDetail = newStep;
-      this.editMode = true;
-    },
-
-    saveStep() {
-      // 克隆当前步骤详情,以避免直接修改原始数据
-      const stepDetailToSave = { ...this.currentStepDetail };
-      console.log("保存步骤信息:", stepDetailToSave);
-      if (stepDetailToSave.id != null && stepDetailToSave.id !== '') {
-        // 更新操作
-        updateStrategyStep(stepDetailToSave).then(response => {
-          this.$message.success('修改成功');
-          this.handleStep(this.currentRow);
-        }).catch(error => {
-          console.error('保存失败', error);
-        });
-      } else {
-        addStrategyStep(stepDetailToSave).then(response => {
-          this.$message.success('新增成功');
-          // 重新加载步骤信息
-          this.handleStep(this.currentRow);
-        }).catch(error => {
-          console.error('保存失败', error);
-        });
-      }
-    },
-
-    cancelEdit() {
-      this.editMode = false;
-    },
-
-    deleteStep() {
-      deleteStrategyStep(this.currentStepDetail.strategyCode).then(response => {
-        this.$message.success('删除成功');
-        this.handleStep(this.currentRow);
-        this.stepDialogVisible=false
-      }).catch(error => {
-        console.error('删除失败', error);
-      });
-    },
-
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      const id = row.id || this.ids;
-      getEnergyStrategy(id).then(response => {
-        this.form = response.data;
-        this.open = true;
-        this.title = '修改能源策略';
-      });
-    },
-
-    /** 提交按钮 */
-    submitForm() {
-      this.$refs['form'].validate(valid => {
-        if (valid) {
-          if (this.form.id != null) {
-            updateEnergyStrategy(this.form).then(response => {
-              this.$modal.msgSuccess('修改成功');
-              this.open = false;
-              this.getList();
-            });
-          } else {
-            addEnergyStrategy(this.form).then(response => {
-              this.$modal.msgSuccess('新增成功');
-              this.open = false;
-              this.getList();
-            });
-          }
-        }
-      });
-    },
-
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$modal.confirm('是否确认删除能源策略编号为"' + ids + '"的数据项?').then(function () {
-        return delEnergyStrategy(ids);
-      }).then(() => {
-        this.getList();
-        this.$modal.msgSuccess('删除成功');
-      }).catch(() => {});
-    },
-
-    // 筛选节点
-    filterNode(value, data) {
-      if (!value) return true
-      return data.label.indexOf(value) !== -1
-    },
-
-    // 节点单击事件
-    handleNodeClick(data) {
-      this.queryParams.areaCode = data.id
-      this.handleQuery()
-    },
-
-    /** 查询区域树结构 */
-    getAreaTree(areaCode, layer) {
-      areaTreeSelect(areaCode, layer).then(response => {
-        this.areaOptions = [{
-          id: '-1',
-          label: '全部',
-          children: response.data
-        }]
-      })
-    },
-
-    strategyTypeChange() {
-      this.queryParams.strategyType = this.strategyType
-      this.getList()
-    },
-
-    handleExecModeChange(value) {
-      this.showExecRule = value === 1;
-    },
-
-    getStateName(key) {
-      if (key === null || key === 0) {
-        return "停用"
-      } else {
-        return "启用"
-      }
-    }
-  },
-};
-</script>
-
-<style lang="scss" scoped>
-.app-container {
-  padding: 20px;
-  background: #f5f7fa;
-  min-height: calc(100vh - 84px);
-
-  // 左侧树容器优化样式
-  .head-container {
-    background: #fff;
-    padding: 15px;
-    border-radius: 8px;
-    margin-bottom: 15px;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-
-    &.tree-container {
-      max-height: calc(100vh - 280px);
-      overflow-y: auto;
-
-      &::-webkit-scrollbar {
-        width: 6px;
-      }
-
-      &::-webkit-scrollbar-track {
-        background: #f1f1f1;
-        border-radius: 3px;
-      }
-
-      &::-webkit-scrollbar-thumb {
-        background: #c1c1c1;
-        border-radius: 3px;
-      }
-
-      &::-webkit-scrollbar-thumb:hover {
-        background: #a8a8a8;
-      }
-
-      ::v-deep .el-tree {
-        background: transparent;
-
-        .el-tree-node__content {
-          height: 40px;
-          padding: 0 8px;
-          transition: all 0.3s;
-
-          &:hover {
-            background-color: #f5f7fa;
-          }
-        }
-
-        .el-tree-node.is-current > .el-tree-node__content {
-          background-color: #ecf5ff;
-          color: #409eff;
-
-          .tree-icon {
-            color: #409eff !important;
-          }
-        }
-
-        .custom-tree-node {
-          flex: 1;
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          font-size: 14px;
-
-          .tree-label {
-            display: flex;
-            align-items: center;
-
-            .tree-icon {
-              margin-right: 8px;
-              font-size: 16px;
-              color: #606266;
-              transition: color 0.3s;
-            }
-          }
-        }
-      }
-    }
-  }
-
-  // 右侧内容区域
-  .content-wrapper {
-    background: #fff;
-    border-radius: 12px;
-    padding: 20px;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
-
-    // 策略标签页样式
-    .strategy-tabs {
-      ::v-deep .el-tabs__header {
-        margin: 0 0 20px;
-        border-bottom: 2px solid #e4e7ed;
-      }
-
-      ::v-deep .el-tabs__item {
-        height: 45px;
-        line-height: 45px;
-        font-size: 14px;
-        font-weight: 500;
-
-        &:hover {
-          color: #409eff;
-        }
-
-        &.is-active {
-          color: #409eff;
-          font-weight: 600;
-        }
-      }
-
-      ::v-deep .el-tabs__active-bar {
-        height: 3px;
-        background: linear-gradient(90deg, #409eff 0%, #53a8ff 100%);
-      }
-    }
-
-    // 搜索表单样式
-    .search-form {
-      padding: 15px;
-      background: #f5f7fa;
-      border-radius: 8px;
-      margin-bottom: 15px;
-    }
-
-    // 数据表格样式
-    .data-table {
-      ::v-deep .el-table__header {
-        th {
-          background-color: #f5f7fa;
-          color: #606266;
-          font-weight: 600;
-        }
-      }
-
-      .deleteBtn {
-        color: #f56c6c;
-
-        &:hover {
-          color: #f23030;
-        }
-      }
-    }
-  }
-}
-
-// 参数弹框样式
-.param-spacer {
-  margin: 50px 0;
-}
-
-.container-block {
-  margin-bottom: 15px;
-}
-
-.param-label {
-  display: inline-block;
-  margin-right: 10px;
-}
-
-.param-container {
-  display: flex;
-  align-items: center;
-  margin-bottom: 10px;
-}
-
-.param-containerInput {
-  width: 40%;
-}
-
-.param-container label {
-  margin-right: 10px;
-  white-space: nowrap;
-}
-
-// 响应式布局
-@media (max-width: 768px) {
-  .app-container {
-    padding: 10px;
-
-    .el-col {
-      margin-bottom: 20px;
-    }
-  }
-}
-</style>

+ 442 - 0
ems-ui-cloud/src/views/mgr/strategy/components/ConditionBuilder.vue

@@ -0,0 +1,442 @@
+<template>
+  <div class="condition-builder">
+    <!-- 简单模式 -->
+    <div v-if="mode === 'simple'" class="simple-mode">
+      <el-row :gutter="8">
+        <el-col :span="8">
+          <el-select
+            v-model="condition.left"
+            filterable
+            allow-create
+            placeholder="选择变量"
+            size="small"
+            style="width: 100%"
+          >
+            <el-option-group label="上下文变量">
+              <el-option
+                v-for="v in variables"
+                :key="v.key"
+                :label="v.name"
+                :value="v.key"
+              />
+            </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>
+          </el-select>
+        </el-col>
+        <el-col :span="6">
+          <el-select v-model="condition.op" size="small" style="width: 100%">
+            <el-option label="等于 (==)" value="==" />
+            <el-option label="不等于 (!=)" value="!=" />
+            <el-option label="大于 (>)" value=">" />
+            <el-option label="大于等于 (>=)" value=">=" />
+            <el-option label="小于 (<)" value="<" />
+            <el-option label="小于等于 (<=)" value="<=" />
+            <el-option label="包含" value="contains" />
+            <el-option label="不包含" value="notContains" />
+            <el-option label="为空" value="isEmpty" />
+            <el-option label="不为空" value="isNotEmpty" />
+          </el-select>
+        </el-col>
+        <el-col :span="8">
+          <el-input
+            v-model="condition.right"
+            placeholder="比较值"
+            size="small"
+            :disabled="['isEmpty', 'isNotEmpty'].includes(condition.op)"
+          />
+        </el-col>
+        <el-col :span="2">
+          <el-button
+            type="text"
+            icon="el-icon-s-tools"
+            @click="switchMode('advanced')"
+            title="高级模式"
+          />
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 高级模式 - 多条件组合 -->
+    <div v-else class="advanced-mode">
+      <div class="mode-header">
+        <el-radio-group v-model="conditionGroup.logic" size="mini">
+          <el-radio-button label="AND">且 (AND)</el-radio-button>
+          <el-radio-button label="OR">或 (OR)</el-radio-button>
+        </el-radio-group>
+        <el-button type="text" size="mini" @click="switchMode('simple')">
+          简单模式
+        </el-button>
+      </div>
+
+      <div class="condition-list">
+        <div
+          v-for="(cond, index) in conditionGroup.conditions"
+          :key="index"
+          class="condition-row"
+        >
+          <el-select
+            v-model="cond.left"
+            filterable
+            allow-create
+            placeholder="变量"
+            size="mini"
+            style="width: 140px"
+          >
+            <el-option
+              v-for="v in allVariables"
+              :key="v.key"
+              :label="v.name"
+              :value="v.key"
+            />
+          </el-select>
+
+          <el-select v-model="cond.op" size="mini" style="width: 100px">
+            <el-option label="==" value="==" />
+            <el-option label="!=" value="!=" />
+            <el-option label=">" value=">" />
+            <el-option label=">=" value=">=" />
+            <el-option label="<" value="<" />
+            <el-option label="<=" value="<=" />
+          </el-select>
+
+          <el-input
+            v-model="cond.right"
+            placeholder="值"
+            size="mini"
+            style="width: 120px"
+          />
+
+          <el-button
+            type="text"
+            icon="el-icon-delete"
+            size="mini"
+            @click="removeCondition(index)"
+            :disabled="conditionGroup.conditions.length <= 1"
+          />
+        </div>
+      </div>
+
+      <el-button
+        type="text"
+        icon="el-icon-plus"
+        size="mini"
+        @click="addCondition"
+      >
+        添加条件
+      </el-button>
+
+      <el-divider />
+
+      <div class="expression-preview">
+        <span class="preview-label">表达式预览:</span>
+        <code class="preview-code">{{ expressionPreview }}</code>
+      </div>
+    </div>
+
+    <!-- 设备属性快捷选择 -->
+    <div class="attr-quick-select" v-if="showAttrSelect">
+      <el-divider content-position="left">快捷选择设备属性</el-divider>
+      <el-cascader
+        v-model="selectedDeviceAttr"
+        :options="deviceAttrOptions"
+        :props="{ value: 'code', label: 'name', children: 'children' }"
+        placeholder="选择设备和属性"
+        size="small"
+        filterable
+        clearable
+        style="width: 100%"
+        @change="handleAttrSelect"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import { listDevice } from '@/api/device/device';
+import { getObjAttr } from '@/api/basecfg/objAttribute';
+
+export default {
+  name: 'ConditionBuilder',
+
+  props: {
+    value: {
+      type: [String, Object],
+      default: ''
+    },
+    variables: {
+      type: Array,
+      default: () => []
+    },
+    attrKey: {
+      type: String,
+      default: ''
+    },
+    deviceList: {
+      type: Array,
+      default: () => []
+    },
+    showAttrSelect: {
+      type: Boolean,
+      default: false
+    }
+  },
+
+  data() {
+    return {
+      mode: 'simple',
+      condition: {
+        left: '',
+        op: '==',
+        right: ''
+      },
+      conditionGroup: {
+        logic: 'AND',
+        conditions: [
+          { left: '', op: '==', right: '' }
+        ]
+      },
+      deviceAttrs: [],
+      deviceAttrOptions: [],
+      selectedDeviceAttr: []
+    };
+  },
+
+  computed: {
+    allVariables() {
+      return [
+        ...this.variables,
+        ...this.deviceAttrs
+      ];
+    },
+
+    expressionPreview() {
+      if (this.mode === 'simple') {
+        const { left, op, right } = this.condition;
+        if (!left) return '(请选择变量)';
+        if (['isEmpty', 'isNotEmpty'].includes(op)) {
+          return `${left} ${op === 'isEmpty' ? '== null' : '!= null'}`;
+        }
+        return `${left} ${op} ${right || '?'}`;
+      } else {
+        const conditions = this.conditionGroup.conditions
+          .filter(c => c.left)
+          .map(c => `(${c.left} ${c.op} ${c.right || '?'})`);
+
+        if (conditions.length === 0) return '(请添加条件)';
+        return conditions.join(` ${this.conditionGroup.logic} `);
+      }
+    }
+  },
+
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.parseValue(val);
+      }
+    },
+    condition: {
+      deep: true,
+      handler() {
+        this.emitChange();
+      }
+    },
+    conditionGroup: {
+      deep: true,
+      handler() {
+        this.emitChange();
+      }
+    },
+    attrKey(val) {
+      if (val) {
+        this.condition.left = val;
+      }
+    }
+  },
+
+  created() {
+    if (this.showAttrSelect) {
+      this.loadDeviceAttrOptions();
+    }
+  },
+
+  methods: {
+    parseValue(val) {
+      if (!val) {
+        this.mode = 'simple';
+        this.condition = { left: this.attrKey || '', op: '==', right: '' };
+        return;
+      }
+
+      try {
+        const parsed = typeof val === 'string' ? JSON.parse(val) : val;
+
+        if (parsed.logic) {
+          // 高级模式
+          this.mode = 'advanced';
+          this.conditionGroup = parsed;
+        } else if (parsed.left) {
+          // 简单模式
+          this.mode = 'simple';
+          this.condition = parsed;
+        }
+      } catch (e) {
+        // 纯字符串表达式
+        this.mode = 'simple';
+        this.condition = { left: '', op: '==', right: val };
+      }
+    },
+
+    emitChange() {
+      let result;
+
+      if (this.mode === 'simple') {
+        result = { ...this.condition };
+      } else {
+        result = { ...this.conditionGroup };
+      }
+
+      this.$emit('input', JSON.stringify(result));
+      this.$emit('change', result);
+    },
+
+    switchMode(mode) {
+      this.mode = mode;
+
+      if (mode === 'advanced') {
+        // 将简单条件转为高级模式
+        this.conditionGroup.conditions = [{ ...this.condition }];
+      } else {
+        // 取第一个条件
+        if (this.conditionGroup.conditions.length > 0) {
+          this.condition = { ...this.conditionGroup.conditions[0] };
+        }
+      }
+    },
+
+    addCondition() {
+      this.conditionGroup.conditions.push({
+        left: '',
+        op: '==',
+        right: ''
+      });
+    },
+
+    removeCondition(index) {
+      this.conditionGroup.conditions.splice(index, 1);
+    },
+
+    async loadDeviceAttrOptions() {
+      // 构建设备-属性级联选项
+      const options = [];
+
+      for (const device of this.deviceList.slice(0, 50)) { // 限制数量避免性能问题
+        try {
+          const response = await getObjAttr(2, device.deviceCode);
+          const attrs = response.data || [];
+
+          if (attrs.length > 0) {
+            options.push({
+              code: device.deviceCode,
+              name: device.deviceName,
+              children: attrs.map(attr => ({
+                code: attr.attrKey,
+                name: attr.attrName
+              }))
+            });
+          }
+        } catch (e) {
+          // 忽略加载失败的设备
+        }
+      }
+
+      this.deviceAttrOptions = options;
+    },
+
+    handleAttrSelect(value) {
+      if (value && value.length === 2) {
+        const deviceCode = value[0];
+        const attrKey = value[1];
+
+        // 设置到条件左侧
+        if (this.mode === 'simple') {
+          this.condition.left = `${deviceCode}.${attrKey}`;
+        } else {
+          // 添加到最后一个条件
+          const lastIndex = this.conditionGroup.conditions.length - 1;
+          if (lastIndex >= 0) {
+            this.conditionGroup.conditions[lastIndex].left = `${deviceCode}.${attrKey}`;
+          }
+        }
+
+        this.deviceAttrs.push({
+          key: `${deviceCode}.${attrKey}`,
+          name: `${deviceCode}.${attrKey}`
+        });
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.condition-builder {
+  .simple-mode {
+    .el-row {
+      align-items: center;
+    }
+  }
+
+  .advanced-mode {
+    .mode-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 12px;
+    }
+
+    .condition-list {
+      .condition-row {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 8px;
+        padding: 8px;
+        background: #f5f7fa;
+        border-radius: 6px;
+      }
+    }
+
+    .expression-preview {
+      background: #f5f7fa;
+      padding: 10px;
+      border-radius: 6px;
+
+      .preview-label {
+        font-size: 12px;
+        color: #909399;
+        display: block;
+        margin-bottom: 4px;
+      }
+
+      .preview-code {
+        font-family: 'Consolas', 'Monaco', monospace;
+        font-size: 13px;
+        color: #409eff;
+        word-break: break-all;
+      }
+    }
+  }
+
+  .attr-quick-select {
+    margin-top: 12px;
+  }
+}
+</style>

+ 528 - 0
ems-ui-cloud/src/views/mgr/strategy/components/StepConfig.vue

@@ -0,0 +1,528 @@
+<template>
+  <div class="step-config">
+    <el-form :model="form" label-width="90px" size="small">
+      <el-form-item label="步骤名称">
+        <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-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-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">
+            <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"
+          >
+            <el-option
+              v-for="device in deviceList"
+              :key="device.deviceCode"
+              :label="device.deviceName"
+              :value="device.deviceCode"
+            >
+              <span>{{ device.deviceName }}</span>
+              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ device.deviceCode }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="系统代码" v-else>
+          <el-input v-model="form.targetObjCode" placeholder="请输入系统代码" />
+        </el-form-item>
+
+        <el-form-item label="选择能力">
+          <el-select
+            v-model="form.abilityKey"
+            filterable
+            placeholder="请选择要调用的能力"
+            style="width: 100%"
+            @change="handleAbilityChange"
+          >
+            <el-option
+              v-for="ability in abilityList"
+              :key="ability.abilityKey"
+              :label="ability.abilityName"
+              :value="ability.abilityKey"
+            >
+              <span>{{ ability.abilityName }}</span>
+              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ ability.abilityKey }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-divider content-position="left">参数配置</el-divider>
+
+        <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-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>
+
+          <el-form-item label="JSON预览">
+            <el-input
+              type="textarea"
+              :value="JSON.stringify(staticParams, null, 2)"
+              :rows="4"
+              readonly
+            />
+          </el-form-item>
+        </template>
+
+        <!-- 上下文变量映射 -->
+        <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
+                v-for="variable in contextVariables"
+                :key="variable.varKey"
+                :label="variable.varName"
+                :value="'context.' + variable.varKey"
+              />
+            </el-select>
+          </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%"
+            />
+          </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>
+
+        <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>
+        </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"
+          />
+        </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-select
+            v-model="form.parallelSteps"
+            multiple
+            placeholder="选择要并行执行的步骤"
+            style="width: 100%"
+          >
+            <!-- 从父组件获取所有步骤 -->
+          </el-select>
+        </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>
+        </el-form-item>
+      </template>
+
+      <!-- 循环执行配置 -->
+      <template v-if="form.stepType === 'LOOP'">
+        <el-divider content-position="left">循环配置</el-divider>
+
+        <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="循环次数" v-if="form.loopType === 'COUNT'">
+          <el-input-number v-model="form.loopCount" :min="1" :max="100" />
+        </el-form-item>
+
+        <el-form-item label="循环条件" v-if="form.loopType === 'CONDITION'">
+          <condition-builder v-model="form.loopCondition" :variables="allVariables" />
+        </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>
+      </template>
+
+      <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>
+
+      <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="重试间隔" 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>
+      </el-form-item>
+
+      <el-form-item label="是否启用">
+        <el-switch v-model="form.enable" :active-value="1" :inactive-value="0" />
+      </el-form-item>
+
+      <el-form-item label="备注">
+        <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选" />
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { listAbility } from '@/api/basecfg/objAbility';
+import { getObjAttr } from '@/api/basecfg/objAttribute';
+import ConditionBuilder from './ConditionBuilder.vue';
+
+export default {
+  name: 'StepConfig',
+  components: { ConditionBuilder },
+
+  props: {
+    step: {
+      type: Object,
+      default: () => ({})
+    },
+    deviceList: {
+      type: Array,
+      default: () => []
+    },
+    contextVariables: {
+      type: Array,
+      default: () => []
+    }
+  },
+
+  data() {
+    return {
+      form: {},
+      abilityList: [],
+      currentAbility: null,
+      currentAbilityParams: [],
+      staticParams: {},
+      paramMapping: {},
+      deviceAttrOptions: []
+    };
+  },
+
+  computed: {
+    allVariables() {
+      // 合并上下文变量和设备属性
+      return [
+        ...this.contextVariables.map(v => ({
+          key: 'context.' + v.varKey,
+          name: v.varName,
+          type: 'context'
+        }))
+      ];
+    }
+  },
+
+  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) {
+          try {
+            this.paramMapping = JSON.parse(val.paramMapping);
+          } catch (e) {
+            this.paramMapping = {};
+          }
+        }
+
+        // 加载设备能力
+        if (val.targetObjCode) {
+          this.loadDeviceAbilities(val.targetObjCode);
+        }
+      }
+    },
+    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();
+  },
+
+  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 loadDeviceAbilities(deviceCode) {
+      const device = this.deviceList.find(d => d.deviceCode === deviceCode);
+      if (!device) return;
+
+      try {
+        const response = await listAbility({ modelCode: device.modelCode });
+        this.abilityList = response.rows || [];
+
+        // 如果已选择能力,加载能力参数
+        if (this.form.abilityKey) {
+          this.handleAbilityChange(this.form.abilityKey);
+        }
+      } catch (error) {
+        console.error('加载能力列表失败', error);
+      }
+    },
+
+    handleAbilityChange(abilityKey) {
+      const ability = this.abilityList.find(a => a.abilityKey === abilityKey);
+      this.currentAbility = ability;
+
+      // 解析能力参数定义
+      if (ability && ability.inputParam) {
+        try {
+          this.currentAbilityParams = JSON.parse(ability.inputParam);
+        } catch (e) {
+          this.currentAbilityParams = [];
+        }
+      } else {
+        // 提供默认参数示例
+        this.currentAbilityParams = [
+          { paramKey: 'value', paramName: '参数值', paramType: 'STRING', placeholder: '请输入参数值' }
+        ];
+      }
+
+      // 初始化参数值
+      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);
+        }
+      });
+    },
+
+    handleParamSourceChange() {
+      // 切换参数来源时清空映射
+      this.paramMapping = {};
+    },
+
+    buildDeviceAttrOptions() {
+      // 构建设备-属性级联选项
+      this.deviceAttrOptions = this.deviceList.map(device => ({
+        code: device.deviceCode,
+        name: device.deviceName,
+        children: [] // 延迟加载
+      }));
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.step-config {
+  .delay-shortcuts {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .delay-tag {
+      cursor: pointer;
+
+      &:hover {
+        background: #ecf5ff;
+        color: #409eff;
+      }
+    }
+  }
+
+  .param-desc {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 4px;
+  }
+
+  .form-tip {
+    font-size: 12px;
+    color: #909399;
+    margin-left: 12px;
+  }
+}
+</style>

+ 330 - 0
ems-ui-cloud/src/views/mgr/strategy/components/TriggerConfig.vue

@@ -0,0 +1,330 @@
+<template>
+  <div class="trigger-config">
+    <el-form :model="form" label-width="90px" size="small">
+      <el-form-item label="触发器名称">
+        <el-input v-model="form.triggerName" placeholder="请输入触发器名称" />
+      </el-form-item>
+
+      <el-form-item label="触发类型">
+        <el-select v-model="form.triggerType" style="width: 100%" @change="handleTypeChange">
+          <el-option label="事件触发" value="EVENT">
+            <i class="el-icon-lightning" style="color: #f56c6c; margin-right: 8px"></i> 事件触发
+          </el-option>
+          <el-option label="属性变化" value="ATTR">
+            <i class="el-icon-data-line" style="color: #e6a23c; margin-right: 8px"></i> 属性变化
+          </el-option>
+          <el-option label="定时触发" value="TIME">
+            <i class="el-icon-time" style="color: #409eff; margin-right: 8px"></i> 定时触发
+          </el-option>
+          <el-option label="条件触发" value="CONDITION">
+            <i class="el-icon-set-up" style="color: #67c23a; margin-right: 8px"></i> 条件触发
+          </el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- 事件触发配置 -->
+      <template v-if="form.triggerType === 'EVENT'">
+        <el-divider content-position="left">事件源配置</el-divider>
+
+        <el-form-item label="对象类型">
+          <el-radio-group v-model="form.sourceObjType">
+            <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"
+          >
+            <el-option
+              v-for="device in deviceList"
+              :key="device.deviceCode"
+              :label="device.deviceName"
+              :value="device.deviceCode"
+            >
+              <span>{{ device.deviceName }}</span>
+              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ device.deviceCode }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="系统代码" v-else>
+          <el-input v-model="form.sourceObjCode" placeholder="请输入系统代码" />
+        </el-form-item>
+
+        <el-form-item label="模型代码">
+          <el-input v-model="form.sourceModelCode" placeholder="自动关联或手动输入" />
+        </el-form-item>
+
+        <el-form-item label="事件标识">
+          <el-select
+            v-model="form.eventKey"
+            filterable
+            allow-create
+            placeholder="选择或输入事件标识"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="event in eventList"
+              :key="event.eventKey"
+              :label="event.eventName"
+              :value="event.eventKey"
+            >
+              <span>{{ event.eventName }}</span>
+              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ event.eventKey }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </template>
+
+      <!-- 属性变化配置 -->
+      <template v-if="form.triggerType === 'ATTR'">
+        <el-divider content-position="left">属性监控配置</el-divider>
+
+        <el-form-item label="对象类型">
+          <el-radio-group v-model="form.sourceObjType">
+            <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"
+          >
+            <el-option
+              v-for="device in deviceList"
+              :key="device.deviceCode"
+              :label="device.deviceName"
+              :value="device.deviceCode"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="监控属性">
+          <el-select
+            v-model="form.attrKey"
+            filterable
+            allow-create
+            placeholder="选择或输入属性标识"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="attr in attrList"
+              :key="attr.attrKey"
+              :label="attr.attrName"
+              :value="attr.attrKey"
+            >
+              <span>{{ attr.attrName }}</span>
+              <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ attr.attrKey }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="触发条件">
+          <condition-builder v-model="form.conditionExpr" :attr-key="form.attrKey" />
+        </el-form-item>
+      </template>
+
+      <!-- 定时触发配置 -->
+      <template v-if="form.triggerType === 'TIME'">
+        <el-divider content-position="left">定时配置</el-divider>
+
+        <el-form-item label="CRON表达式">
+          <el-input v-model="cronExpression" placeholder="0 0 8 * * ?">
+            <el-button slot="append" @click="showCronHelper">
+              <i class="el-icon-question"></i>
+            </el-button>
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="快捷选择">
+          <div class="cron-shortcuts">
+            <el-tag
+              v-for="shortcut in cronShortcuts"
+              :key="shortcut.value"
+              @click="setCron(shortcut.value)"
+              class="cron-tag"
+            >
+              {{ shortcut.label }}
+            </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>
+
+      <!-- 条件触发配置 -->
+      <template v-if="form.triggerType === 'CONDITION'">
+        <el-divider content-position="left">条件配置</el-divider>
+
+        <el-form-item label="条件表达式">
+          <condition-builder v-model="form.conditionExpr" :variables="availableVariables" />
+        </el-form-item>
+      </template>
+
+      <el-divider content-position="left">高级配置</el-divider>
+
+      <el-form-item label="优先级">
+        <el-slider v-model="form.priority" :min="0" :max="100" show-input />
+      </el-form-item>
+
+      <el-form-item label="是否启用">
+        <el-switch v-model="form.enable" :active-value="1" :inactive-value="0" />
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { getObjAttr } from '@/api/basecfg/objAttribute';
+import ConditionBuilder from './ConditionBuilder.vue';
+
+export default {
+  name: 'TriggerConfig',
+  components: { ConditionBuilder },
+
+  props: {
+    trigger: {
+      type: Object,
+      default: () => ({})
+    },
+    deviceList: {
+      type: Array,
+      default: () => []
+    }
+  },
+
+  data() {
+    return {
+      form: {},
+      eventList: [],
+      attrList: [],
+      cronExpression: '',
+
+      cronShortcuts: [
+        { label: '每天8点', value: '0 0 8 * * ?' },
+        { label: '每天0点', value: '0 0 0 * * ?' },
+        { label: '每小时', value: '0 0 * * * ?' },
+        { label: '每30分钟', value: '0 */30 * * * ?' },
+        { label: '工作日8点', value: '0 0 8 ? * MON-FRI' },
+        { label: '每月1号', value: '0 0 0 1 * ?' }
+      ],
+
+      availableVariables: []
+    };
+  },
+
+  computed: {
+    cronPreview() {
+      // TODO: 解析CRON表达式,返回接下来几次执行时间
+      return ['计算中...'];
+    }
+  },
+
+  watch: {
+    trigger: {
+      immediate: true,
+      handler(val) {
+        this.form = { ...val };
+        if (val.sourceObjCode) {
+          this.loadDeviceAttrs(val.sourceObjCode);
+        }
+      }
+    },
+    form: {
+      deep: true,
+      handler(val) {
+        this.$emit('change', val);
+      }
+    }
+  },
+
+  methods: {
+    handleTypeChange() {
+      // 切换类型时清空相关字段
+      this.form.eventKey = null;
+      this.form.attrKey = null;
+      this.form.conditionExpr = null;
+    },
+
+    async handleDeviceChange(deviceCode) {
+      const device = this.deviceList.find(d => d.deviceCode === deviceCode);
+      if (device) {
+        this.form.sourceModelCode = device.modelCode;
+        await this.loadDeviceAttrs(deviceCode);
+      }
+    },
+
+    async loadDeviceAttrs(deviceCode) {
+      try {
+        const response = await getObjAttr(2, deviceCode);
+        this.attrList = response.data || [];
+      } catch (error) {
+        console.error('加载属性失败', error);
+      }
+    },
+
+    setCron(value) {
+      this.cronExpression = value;
+      // 将CRON表达式存入条件表达式字段
+      this.form.conditionExpr = JSON.stringify({ cron: value });
+    },
+
+    showCronHelper() {
+      // TODO: 显示CRON表达式帮助
+      this.$message.info('CRON帮助文档开发中');
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.trigger-config {
+  .cron-shortcuts {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .cron-tag {
+      cursor: pointer;
+
+      &:hover {
+        background: #ecf5ff;
+        color: #409eff;
+      }
+    }
+  }
+
+  .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;
+      }
+    }
+  }
+}
+</style>

+ 1146 - 0
ems-ui-cloud/src/views/mgr/strategy/editor.vue

@@ -0,0 +1,1146 @@
+<template>
+  <div class="strategy-editor">
+    <!-- 顶部工具栏 -->
+    <div class="editor-header">
+      <div class="header-left">
+        <el-button icon="el-icon-arrow-left" @click="goBack">返回</el-button>
+        <el-divider direction="vertical"></el-divider>
+        <div class="strategy-info">
+          <h2 class="strategy-name">
+            {{ strategyData.strategyName || '新策略' }}
+            <el-tag :type="getSceneTagType(strategyData.sceneType)" size="small">
+              {{ getSceneLabel(strategyData.sceneType) }}
+            </el-tag>
+          </h2>
+          <span class="strategy-code">{{ strategyData.strategyCode }}</span>
+        </div>
+      </div>
+      <div class="header-center">
+        <el-radio-group v-model="editorMode" size="small">
+          <el-radio-button label="design">
+            <i class="el-icon-edit"></i> 设计
+          </el-radio-button>
+          <el-radio-button label="preview">
+            <i class="el-icon-view"></i> 预览
+          </el-radio-button>
+          <el-radio-button label="debug">
+            <i class="el-icon-magic-stick"></i> 调试
+          </el-radio-button>
+        </el-radio-group>
+      </div>
+      <div class="header-right">
+        <el-button size="small" @click="handleUndo" :disabled="!canUndo">
+          <i class="el-icon-refresh-left"></i>
+        </el-button>
+        <el-button size="small" @click="handleRedo" :disabled="!canRedo">
+          <i class="el-icon-refresh-right"></i>
+        </el-button>
+        <el-divider direction="vertical"></el-divider>
+        <el-button type="success" size="small" icon="el-icon-video-play" @click="handleTestRun">
+          测试执行
+        </el-button>
+        <el-button type="primary" size="small" icon="el-icon-check" @click="handleSave">
+          保存
+        </el-button>
+      </div>
+    </div>
+
+    <div class="editor-main">
+      <!-- 左侧组件面板 -->
+      <div class="component-panel">
+        <el-collapse v-model="activePanels">
+          <!-- 触发器组件 -->
+          <el-collapse-item title="触发器" name="triggers">
+            <div class="component-list">
+              <div
+                v-for="trigger in triggerComponents"
+                :key="trigger.type"
+                class="component-item"
+                draggable="true"
+                @dragstart="(e) => handleDragStart(e, 'trigger', trigger)"
+              >
+                <div class="component-icon" :style="{ background: trigger.color }">
+                  <i :class="trigger.icon"></i>
+                </div>
+                <span class="component-name">{{ trigger.name }}</span>
+              </div>
+            </div>
+          </el-collapse-item>
+
+          <!-- 设备能力 -->
+          <el-collapse-item title="设备能力" name="devices">
+            <div class="device-search">
+              <el-input
+                v-model="deviceKeyword"
+                placeholder="搜索设备"
+                size="mini"
+                prefix-icon="el-icon-search"
+                clearable
+              />
+            </div>
+            <div class="device-tree">
+              <el-tree
+                :data="deviceTreeData"
+                :props="{ children: 'children', label: 'label' }"
+                node-key="id"
+                :filter-node-method="filterDeviceNode"
+                :expand-on-click-node="false"
+                @node-click="handleDeviceNodeClick"
+                ref="deviceTree"
+              >
+                <span class="device-tree-node" slot-scope="{ node, data }">
+                  <i :class="getDeviceTreeIcon(data)"></i>
+                  <span>{{ node.label }}</span>
+                  <el-tag v-if="data.type === 'ability'" size="mini" type="success">能力</el-tag>
+                </span>
+              </el-tree>
+            </div>
+          </el-collapse-item>
+
+          <!-- 流程控制 -->
+          <el-collapse-item title="流程控制" name="flow">
+            <div class="component-list">
+              <div
+                v-for="flow in flowComponents"
+                :key="flow.type"
+                class="component-item"
+                draggable="true"
+                @dragstart="(e) => handleDragStart(e, 'flow', flow)"
+              >
+                <div class="component-icon" :style="{ background: flow.color }">
+                  <i :class="flow.icon"></i>
+                </div>
+                <span class="component-name">{{ flow.name }}</span>
+              </div>
+            </div>
+          </el-collapse-item>
+
+          <!-- 上下文变量 -->
+          <el-collapse-item title="上下文变量" name="context">
+            <div class="context-vars">
+              <div
+                v-for="variable in contextVariables"
+                :key="variable.varKey"
+                class="context-var-item"
+                draggable="true"
+                @dragstart="(e) => handleDragStart(e, 'variable', variable)"
+              >
+                <span class="var-name">{{ variable.varName }}</span>
+                <span class="var-type">{{ variable.dataType }}</span>
+              </div>
+              <el-button type="text" icon="el-icon-plus" size="mini" @click="showAddVariable">
+                添加变量
+              </el-button>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+      </div>
+
+      <!-- 中间画布区域 -->
+      <div class="canvas-panel" @dragover.prevent @drop="handleCanvasDrop">
+        <div class="canvas-toolbar">
+          <el-button-group>
+            <el-button size="mini" icon="el-icon-zoom-in" @click="zoomIn"></el-button>
+            <el-button size="mini">{{ Math.round(zoom * 100) }}%</el-button>
+            <el-button size="mini" icon="el-icon-zoom-out" @click="zoomOut"></el-button>
+          </el-button-group>
+          <el-button size="mini" icon="el-icon-rank" @click="autoLayout">自动布局</el-button>
+        </div>
+
+        <div class="canvas-container" ref="canvasContainer">
+          <div class="canvas-content" :style="{ transform: `scale(${zoom})` }" ref="canvas">
+            <!-- 开始节点 -->
+            <div class="flow-node start-node">
+              <div class="node-icon"><i class="el-icon-caret-right"></i></div>
+              <span>开始</span>
+            </div>
+
+            <!-- 触发器节点 -->
+            <div
+              v-for="(trigger, index) in triggers"
+              :key="'trigger-' + index"
+              :class="['flow-node', 'trigger-node', { selected: selectedNode === trigger }]"
+              @click="selectNode(trigger)"
+            >
+              <div class="node-header" :style="{ background: getTriggerColor(trigger.triggerType) }">
+                <i :class="getTriggerIcon(trigger.triggerType)"></i>
+                <span>{{ trigger.triggerName || '触发器' }}</span>
+              </div>
+              <div class="node-body">
+                <p>{{ getTriggerDesc(trigger) }}</p>
+              </div>
+              <div class="node-actions">
+                <i class="el-icon-edit" @click.stop="editNode(trigger, 'trigger')"></i>
+                <i class="el-icon-delete" @click.stop="deleteNode(trigger, 'trigger')"></i>
+              </div>
+            </div>
+
+            <!-- 连接线 -->
+            <div class="flow-connector" v-if="triggers.length > 0"></div>
+
+            <!-- 步骤节点列表 -->
+            <div class="steps-container">
+              <template v-for="(step, index) in steps">
+                <div
+                  :key="'step-' + step.id"
+                  :class="['flow-node', 'step-node', getStepNodeClass(step), { selected: selectedNode === step }]"
+                  @click="selectNode(step)"
+                  draggable="true"
+                  @dragstart="(e) => handleStepDragStart(e, step, index)"
+                  @dragover.prevent
+                  @drop="(e) => handleStepDrop(e, index)"
+                >
+                  <div class="node-header" :style="{ background: getStepColor(step.stepType) }">
+                    <i :class="getStepIcon(step.stepType)"></i>
+                    <span>{{ step.stepName || getStepTypeName(step.stepType) }}</span>
+                    <el-tag v-if="!step.enable" size="mini" type="info">禁用</el-tag>
+                  </div>
+                  <div class="node-body">
+                    <template v-if="step.stepType === 'ABILITY'">
+                      <p class="node-target"><i class="el-icon-aim"></i>{{ step.targetObjCode || '未选择设备' }}</p>
+                      <p class="node-ability"><i class="el-icon-s-operation"></i>{{ step.abilityKey || '未选择能力' }}</p>
+                    </template>
+                    <template v-else-if="step.stepType === 'DELAY'">
+                      <p class="node-delay"><i class="el-icon-time"></i>延时 {{ step.delaySeconds || 0 }} 秒</p>
+                    </template>
+                    <template v-else-if="step.stepType === 'CONDITION'">
+                      <p class="node-condition"><i class="el-icon-question"></i>{{ getConditionPreview(step.conditionExpr) }}</p>
+                    </template>
+                  </div>
+                  <div class="node-actions">
+                    <i class="el-icon-edit" @click.stop="editNode(step, 'step')"></i>
+                    <i class="el-icon-document-copy" @click.stop="copyStep(step)"></i>
+                    <i class="el-icon-delete" @click.stop="deleteNode(step, 'step')"></i>
+                  </div>
+                  <div class="node-index">{{ index + 1 }}</div>
+                </div>
+
+                <!-- 连接线 -->
+                <div v-if="index < steps.length - 1" :key="'conn-' + index" class="flow-connector">
+                  <div class="connector-line"></div>
+                  <div class="connector-add" @click="addStepAfter(index)"><i class="el-icon-plus"></i></div>
+                </div>
+              </template>
+            </div>
+
+            <!-- 添加步骤按钮 -->
+            <div class="add-step-btn" @click="addStepAtEnd">
+              <i class="el-icon-plus"></i>
+              <span>添加步骤</span>
+            </div>
+
+            <!-- 结束节点 -->
+            <div class="flow-node end-node">
+              <div class="node-icon"><i class="el-icon-circle-check"></i></div>
+              <span>结束</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧属性面板 -->
+      <div class="property-panel" v-show="selectedNode || showStrategyConfig">
+        <div class="panel-header">
+          <span>{{ propertyPanelTitle }}</span>
+          <i class="el-icon-close" @click="closePropertyPanel"></i>
+        </div>
+
+        <div class="panel-body">
+          <!-- 策略基础配置 -->
+          <template v-if="showStrategyConfig">
+            <el-form :model="strategyData" label-width="90px" size="small">
+              <el-form-item label="策略名称">
+                <el-input v-model="strategyData.strategyName" />
+              </el-form-item>
+              <el-form-item label="场景类型">
+                <el-select v-model="strategyData.sceneType" style="width: 100%">
+                  <el-option v-for="scene in sceneTypes" :key="scene.value" :label="scene.label" :value="scene.value" />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="优先级">
+                <el-slider v-model="strategyData.priority" :min="0" :max="100" show-input />
+              </el-form-item>
+              <el-form-item label="超时时间">
+                <el-input-number v-model="strategyData.timeout" :min="0" style="width: 100%" />
+              </el-form-item>
+              <el-form-item label="失败重试">
+                <el-input-number v-model="strategyData.retryTimes" :min="0" :max="5" style="width: 100%" />
+              </el-form-item>
+              <el-form-item label="策略描述">
+                <el-input type="textarea" v-model="strategyData.strategyDesc" :rows="3" />
+              </el-form-item>
+            </el-form>
+          </template>
+
+          <!-- 触发器配置 -->
+          <template v-else-if="selectedNodeType === 'trigger'">
+            <trigger-config :trigger="selectedNode" :device-list="deviceList" @change="handleTriggerChange" />
+          </template>
+
+          <!-- 步骤配置 -->
+          <template v-else-if="selectedNodeType === 'step'">
+            <step-config :step="selectedNode" :device-list="deviceList" :context-variables="contextVariables" @change="handleStepChange" />
+          </template>
+        </div>
+      </div>
+    </div>
+
+    <!-- 快速添加步骤弹窗 -->
+    <el-dialog title="添加步骤" :visible.sync="addStepDialogVisible" width="500px">
+      <div class="step-type-grid">
+        <div v-for="stepType in stepTypes" :key="stepType.type" class="step-type-card" @click="confirmAddStep(stepType.type)">
+          <div class="step-type-icon" :style="{ background: stepType.color }">
+            <i :class="stepType.icon"></i>
+          </div>
+          <div class="step-type-info">
+            <span class="step-type-name">{{ stepType.name }}</span>
+            <span class="step-type-desc">{{ stepType.desc }}</span>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 添加变量弹窗 -->
+    <el-dialog title="添加上下文变量" :visible.sync="addVarDialogVisible" width="450px">
+      <el-form :model="newVariable" label-width="80px" size="small">
+        <el-form-item label="变量键">
+          <el-input v-model="newVariable.varKey" placeholder="如: pvPower" />
+        </el-form-item>
+        <el-form-item label="变量名称">
+          <el-input v-model="newVariable.varName" placeholder="如: 光伏功率" />
+        </el-form-item>
+        <el-form-item label="变量类型">
+          <el-select v-model="newVariable.varType" style="width: 100%">
+            <el-option label="输入变量" value="INPUT" />
+            <el-option label="输出变量" value="OUTPUT" />
+            <el-option label="临时变量" value="TEMP" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="数据类型">
+          <el-select v-model="newVariable.dataType" style="width: 100%">
+            <el-option label="字符串" value="STRING" />
+            <el-option label="数字" value="NUMBER" />
+            <el-option label="布尔值" value="BOOLEAN" />
+            <el-option label="对象" value="OBJECT" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="默认值">
+          <el-input v-model="newVariable.defaultValue" placeholder="可选" />
+        </el-form-item>
+        <el-form-item label="值来源">
+          <el-input v-model="newVariable.valueSource" placeholder="如: ESS001.soc" />
+          <div class="form-tip">格式: 设备代码.属性键</div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="addVarDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmAddVariable">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  getEnergyStrategy,
+  getEnergyStrategyByCode,
+  updateEnergyStrategy,
+  getStrategySteps,
+  addStrategyStep,
+  updateStrategyStep,
+  deleteStrategyStep,
+  saveStrategyStepBatch,
+  getTriggers,
+  saveTrigger,
+  deleteTrigger,
+  executeStrategy,
+  getStrategyContext,
+  saveStrategyContext
+} from '@/api/mgr/energyStrategy';
+import { listDevRecursionByArea } from '@/api/device/device';
+import { listAbility } from '@/api/basecfg/objAbility';
+import TriggerConfig from './components/TriggerConfig.vue';
+import StepConfig from './components/StepConfig.vue';
+
+export default {
+  name: 'StrategyEditor',
+  components: { TriggerConfig, StepConfig },
+
+  data() {
+    return {
+      strategyCode: '',
+      strategyData: {},
+      triggers: [],
+      steps: [],
+      contextVariables: [],
+
+      editorMode: 'design',
+      zoom: 1,
+      selectedNode: null,
+      selectedNodeType: null,
+      showStrategyConfig: false,
+
+      history: [],
+      historyIndex: -1,
+
+      activePanels: ['triggers', 'devices', 'flow'],
+      deviceKeyword: '',
+
+      deviceList: [],
+      deviceTreeData: [],
+
+      addStepDialogVisible: false,
+      addStepPosition: -1,
+      addVarDialogVisible: false,
+      newVariable: {},
+
+      triggerComponents: [
+        { type: 'EVENT', name: '事件触发', icon: 'el-icon-lightning', color: '#f56c6c' },
+        { type: 'ATTR', name: '属性变化', icon: 'el-icon-data-line', color: '#e6a23c' },
+        { type: 'TIME', name: '定时触发', icon: 'el-icon-time', color: '#409eff' },
+        { type: 'CONDITION', name: '条件触发', icon: 'el-icon-set-up', color: '#67c23a' }
+      ],
+
+      flowComponents: [
+        { type: 'CONDITION', name: '条件判断', icon: 'el-icon-question', color: '#e6a23c' },
+        { type: 'PARALLEL', name: '并行执行', icon: 'el-icon-sort', color: '#909399' },
+        { type: 'LOOP', name: '循环执行', icon: 'el-icon-refresh', color: '#67c23a' },
+        { type: 'DELAY', name: '延时等待', icon: 'el-icon-time', color: '#409eff' }
+      ],
+
+      stepTypes: [
+        { type: 'ABILITY', name: '能力调用', icon: 'el-icon-s-operation', color: '#409eff', desc: '调用设备能力执行操作' },
+        { type: 'DELAY', name: '延时等待', icon: 'el-icon-time', color: '#67c23a', desc: '等待指定时间后继续' },
+        { type: 'CONDITION', name: '条件判断', icon: 'el-icon-question', color: '#e6a23c', desc: '根据条件决定执行分支' },
+        { type: 'PARALLEL', name: '并行执行', icon: 'el-icon-sort', color: '#909399', desc: '同时执行多个步骤' },
+        { type: 'LOOP', name: '循环执行', icon: 'el-icon-refresh', color: '#f56c6c', desc: '重复执行指定步骤' }
+      ],
+
+      sceneTypes: [
+        { value: 'PV_ESS', label: '光储协同' },
+        { value: 'DEMAND_RESP', label: '需求响应' },
+        { value: 'PEAK_VALLEY', label: '削峰填谷' },
+        { value: 'EMERGENCY', label: '应急保供' },
+        { value: 'ENERGY_SAVE', label: '节能优化' }
+      ]
+    };
+  },
+
+  computed: {
+    canUndo() { return this.historyIndex > 0; },
+    canRedo() { return this.historyIndex < this.history.length - 1; },
+    propertyPanelTitle() {
+      if (this.showStrategyConfig) return '策略配置';
+      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';
+    }
+  },
+
+  watch: {
+    deviceKeyword(val) {
+      this.$refs.deviceTree && this.$refs.deviceTree.filter(val);
+    }
+  },
+
+  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.loadTriggers();
+      this.loadSteps();
+    } else {
+      this.$message.warning('缺少策略代码参数');
+      setTimeout(() => {
+        this.goBack();
+      }, 1500);
+      return;
+    }
+    this.loadDevices();
+  },
+
+  methods: {
+    // 加载策略数据
+    async loadStrategy() {
+      try {
+        // 优先通过code查询
+        let response;
+        try {
+          response = await getEnergyStrategyByCode(this.strategyCode);
+        } catch {
+          // 如果接口不存在,尝试用列表接口查询
+          const listRes = await listEnergyStrategy({ strategyCode: this.strategyCode, pageSize: 1 });
+          if (listRes.rows && listRes.rows.length > 0) {
+            response = { data: listRes.rows[0] };
+          }
+        }
+        this.strategyData = response?.data || {};
+      } catch (error) {
+        console.error('加载策略失败', error);
+        this.$message.error('加载策略失败');
+      }
+    },
+
+    async loadTriggers() {
+      try {
+        const response = await getTriggers(this.strategyCode);
+        this.triggers = response.data || response.rows || [];
+      } catch (error) {
+        console.error('加载触发器失败', error);
+        this.triggers = [];
+      }
+    },
+
+    async loadSteps() {
+      try {
+        const response = await getStrategySteps(this.strategyCode);
+        this.steps = (response.data || response.rows || []).sort((a, b) => a.stepIndex - b.stepIndex);
+      } catch (error) {
+        console.error('加载步骤失败', error);
+        this.steps = [];
+      }
+    },
+
+    async loadContextVariables() {
+      try {
+        const response = await getStrategyContext(this.strategyCode);
+        this.contextVariables = response.data || response.rows || [];
+      } catch (error) {
+        console.error('加载上下文变量失败', error);
+        this.contextVariables = [];
+      }
+    },
+
+    async loadDevices() {
+      try {
+        const response = await listDevRecursionByArea({ pageNum: 1, pageSize: 1000 });
+        this.deviceList = response.rows || [];
+        this.buildDeviceTree();
+      } catch (error) {
+        console.error('加载设备失败', error);
+      }
+    },
+
+    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: [] }
+      };
+
+      this.deviceList.forEach(device => {
+        const cat = device.deviceCategory || 'Z';
+        const node = {
+          id: device.deviceCode,
+          label: device.deviceName,
+          type: 'device',
+          data: device,
+          children: []
+        };
+        if (categoryMap[cat]) {
+          categoryMap[cat].children.push(node);
+        } else {
+          categoryMap['Z'].children.push(node);
+        }
+      });
+
+      this.deviceTreeData = Object.entries(categoryMap)
+        .filter(([_, v]) => v.children.length > 0)
+        .map(([code, cat]) => ({
+          id: code,
+          label: cat.label,
+          icon: cat.icon,
+          type: 'category',
+          children: cat.children
+        }));
+    },
+
+    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) {
+        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 }
+          }));
+          this.$set(node, 'expanded', true);
+        } catch (error) {
+          console.error('加载能力失败', error);
+        }
+      } else if (data.type === 'ability') {
+        this.addAbilityStep(data.data);
+      }
+    },
+
+    addAbilityStep(abilityData) {
+      const newStep = {
+        strategyCode: this.strategyCode,
+        stepCode: 'STEP_' + Date.now(),
+        stepName: abilityData.abilityName,
+        stepType: 'ABILITY',
+        stepIndex: this.steps.length + 1,
+        targetObjType: 2,
+        targetObjCode: abilityData.deviceCode,
+        targetModelCode: abilityData.modelCode,
+        abilityKey: abilityData.abilityKey,
+        paramSource: 'STATIC',
+        enable: 1
+      };
+      this.steps.push(newStep);
+      this.selectNode(newStep);
+      this.selectedNodeType = 'step';
+      this.saveHistory();
+    },
+
+    handleDragStart(e, category, component) {
+      e.dataTransfer.setData('category', category);
+      e.dataTransfer.setData('component', JSON.stringify(component));
+    },
+
+    handleCanvasDrop(e) {
+      const category = e.dataTransfer.getData('category');
+      const component = JSON.parse(e.dataTransfer.getData('component') || '{}');
+      if (category === 'trigger') {
+        this.addTrigger(component);
+      } else if (category === 'flow') {
+        this.addFlowStep(component);
+      }
+    },
+
+    handleStepDragStart(e, step, index) {
+      e.dataTransfer.setData('stepIndex', index.toString());
+    },
+
+    handleStepDrop(e, targetIndex) {
+      const sourceIndex = parseInt(e.dataTransfer.getData('stepIndex'));
+      if (sourceIndex === targetIndex) return;
+      const [movedStep] = this.steps.splice(sourceIndex, 1);
+      this.steps.splice(targetIndex, 0, movedStep);
+      this.steps.forEach((step, idx) => { step.stepIndex = idx + 1; });
+      this.saveHistory();
+    },
+
+    addTrigger(component) {
+      const newTrigger = {
+        strategyCode: this.strategyCode,
+        triggerName: component.name,
+        triggerType: component.type,
+        sourceObjType: 2,
+        priority: 50,
+        enable: 1
+      };
+      this.triggers.push(newTrigger);
+      this.selectNode(newTrigger);
+      this.selectedNodeType = 'trigger';
+      this.saveHistory();
+    },
+
+    addFlowStep(component) {
+      const newStep = {
+        strategyCode: this.strategyCode,
+        stepCode: 'STEP_' + Date.now(),
+        stepName: component.name,
+        stepType: component.type,
+        stepIndex: this.steps.length + 1,
+        enable: 1
+      };
+      if (component.type === 'DELAY') {
+        newStep.delaySeconds = 10;
+      }
+      this.steps.push(newStep);
+      this.selectNode(newStep);
+      this.selectedNodeType = 'step';
+      this.saveHistory();
+    },
+
+    addStepAfter(index) {
+      this.addStepPosition = index + 1;
+      this.addStepDialogVisible = true;
+    },
+
+    addStepAtEnd() {
+      this.addStepPosition = this.steps.length;
+      this.addStepDialogVisible = true;
+    },
+
+    confirmAddStep(stepType) {
+      const newStep = {
+        strategyCode: this.strategyCode,
+        stepCode: 'STEP_' + Date.now(),
+        stepName: this.getStepTypeName(stepType),
+        stepType: stepType,
+        stepIndex: this.addStepPosition + 1,
+        enable: 1
+      };
+      if (stepType === 'DELAY') {
+        newStep.delaySeconds = 10;
+      } else if (stepType === 'ABILITY') {
+        newStep.targetObjType = 2;
+        newStep.paramSource = 'STATIC';
+      }
+      this.steps.splice(this.addStepPosition, 0, newStep);
+      for (let i = this.addStepPosition + 1; i < this.steps.length; i++) {
+        this.steps[i].stepIndex = i + 1;
+      }
+      this.addStepDialogVisible = false;
+      this.selectNode(newStep);
+      this.selectedNodeType = 'step';
+      this.saveHistory();
+    },
+
+    copyStep(step) {
+      const newStep = {
+        ...step,
+        id: null,
+        stepCode: 'STEP_' + Date.now(),
+        stepName: step.stepName + '_副本',
+        stepIndex: step.stepIndex + 1
+      };
+      const index = this.steps.findIndex(s => s === step);
+      this.steps.splice(index + 1, 0, newStep);
+      for (let i = index + 2; i < this.steps.length; i++) {
+        this.steps[i].stepIndex = i + 1;
+      }
+      this.saveHistory();
+    },
+
+    selectNode(node) {
+      this.selectedNode = node;
+      this.showStrategyConfig = false;
+    },
+
+    editNode(node, type) {
+      this.selectedNode = node;
+      this.selectedNodeType = type;
+      this.showStrategyConfig = false;
+    },
+
+    deleteNode(node, type) {
+      this.$confirm('确认删除该节点吗?', '提示', { type: 'warning' }).then(() => {
+        if (type === 'trigger') {
+          const index = this.triggers.indexOf(node);
+          if (index > -1) {
+            if (node.id) deleteTrigger(node.id).catch(() => {});
+            this.triggers.splice(index, 1);
+          }
+        } else if (type === 'step') {
+          const index = this.steps.indexOf(node);
+          if (index > -1) {
+            if (node.id) deleteStrategyStep(node.id).catch(() => {});
+            this.steps.splice(index, 1);
+            for (let i = index; i < this.steps.length; i++) {
+              this.steps[i].stepIndex = i + 1;
+            }
+          }
+        }
+        this.selectedNode = null;
+        this.saveHistory();
+      }).catch(() => {});
+    },
+
+    closePropertyPanel() {
+      this.selectedNode = null;
+      this.showStrategyConfig = false;
+    },
+
+    handleTriggerChange(trigger) {
+      const index = this.triggers.findIndex(t => t === this.selectedNode);
+      if (index > -1) {
+        this.$set(this.triggers, index, trigger);
+      }
+    },
+
+    handleStepChange(step) {
+      const index = this.steps.findIndex(s => s === this.selectedNode);
+      if (index > -1) {
+        this.$set(this.steps, index, step);
+      }
+    },
+
+    showAddVariable() {
+      this.newVariable = {
+        strategyCode: this.strategyCode,
+        varKey: '',
+        varName: '',
+        varType: 'TEMP',
+        dataType: 'STRING',
+        defaultValue: '',
+        valueSource: ''
+      };
+      this.addVarDialogVisible = true;
+    },
+
+    confirmAddVariable() {
+      if (!this.newVariable.varKey || !this.newVariable.varName) {
+        this.$message.warning('请填写变量键和名称');
+        return;
+      }
+      this.contextVariables.push({ ...this.newVariable });
+      this.addVarDialogVisible = false;
+    },
+
+    zoomIn() { this.zoom = Math.min(2, this.zoom + 0.1); },
+    zoomOut() { this.zoom = Math.max(0.5, this.zoom - 0.1); },
+    autoLayout() { this.$message.info('自动布局功能开发中'); },
+
+    saveHistory() {
+      const snapshot = {
+        triggers: JSON.parse(JSON.stringify(this.triggers)),
+        steps: JSON.parse(JSON.stringify(this.steps))
+      };
+      this.history = this.history.slice(0, this.historyIndex + 1);
+      this.history.push(snapshot);
+      this.historyIndex = this.history.length - 1;
+    },
+
+    handleUndo() {
+      if (!this.canUndo) return;
+      this.historyIndex--;
+      this.restoreFromHistory();
+    },
+
+    handleRedo() {
+      if (!this.canRedo) return;
+      this.historyIndex++;
+      this.restoreFromHistory();
+    },
+
+    restoreFromHistory() {
+      const snapshot = this.history[this.historyIndex];
+      this.triggers = JSON.parse(JSON.stringify(snapshot.triggers));
+      this.steps = JSON.parse(JSON.stringify(snapshot.steps));
+    },
+
+    async handleSave() {
+      try {
+        // 保存策略基础信息
+        await updateEnergyStrategy(this.strategyData);
+        // 保存触发器
+        for (const trigger of this.triggers) {
+          await saveTrigger(trigger);
+        }
+        // 批量保存步骤
+        if (this.steps.length > 0) {
+          await saveStrategyStepBatch(this.steps);
+        }
+        this.$message.success('保存成功');
+      } catch (error) {
+        this.$message.error('保存失败: ' + (error.message || '未知错误'));
+      }
+    },
+
+    handleTestRun() {
+      this.$confirm('确认执行测试吗?', '提示').then(() => {
+        executeStrategy(this.strategyCode, { testMode: true }).then(response => {
+          const result = response.data || response;
+          this.$message.success('测试执行已启动,执行ID: ' + (result.execId || ''));
+          this.editorMode = 'debug';
+        }).catch(error => {
+          this.$message.error('执行失败: ' + (error.msg || error.message || ''));
+        });
+      }).catch(() => {});
+    },
+
+    // 返回上级页面
+    goBack() {
+      this.$router.push('/strategy-mgr/strategy-index');
+    },
+
+    // 辅助方法
+    getSceneTagType(scene) {
+      const map = { 'PV_ESS': 'warning', 'DEMAND_RESP': '', 'PEAK_VALLEY': 'success', 'EMERGENCY': 'danger', 'ENERGY_SAVE': 'info' };
+      return map[scene] || '';
+    },
+    getSceneLabel(scene) {
+      const found = this.sceneTypes.find(s => s.value === scene);
+      return found ? found.label : scene || '未分类';
+    },
+    getTriggerColor(type) {
+      const map = { 'EVENT': '#f56c6c', 'ATTR': '#e6a23c', 'TIME': '#409eff', 'CONDITION': '#67c23a' };
+      return map[type] || '#909399';
+    },
+    getTriggerIcon(type) {
+      const map = { 'EVENT': 'el-icon-lightning', 'ATTR': 'el-icon-data-line', 'TIME': 'el-icon-time', 'CONDITION': 'el-icon-set-up' };
+      return map[type] || 'el-icon-question';
+    },
+    getTriggerDesc(trigger) {
+      if (trigger.triggerType === 'EVENT') return `监听 ${trigger.sourceObjCode || '?'} 的 ${trigger.eventKey || '?'} 事件`;
+      if (trigger.triggerType === 'ATTR') return `监控 ${trigger.sourceObjCode || '?'} 的 ${trigger.attrKey || '?'} 属性变化`;
+      if (trigger.triggerType === 'TIME') return `定时执行`;
+      if (trigger.triggerType === 'CONDITION') return `条件满足时触发`;
+      return '-';
+    },
+    getStepColor(type) {
+      const map = { 'ABILITY': '#409eff', 'DELAY': '#67c23a', 'CONDITION': '#e6a23c', 'PARALLEL': '#909399', 'LOOP': '#f56c6c' };
+      return map[type] || '#909399';
+    },
+    getStepIcon(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';
+    },
+    getStepTypeName(type) {
+      const map = { 'ABILITY': '能力调用', 'DELAY': '延时等待', 'CONDITION': '条件判断', 'PARALLEL': '并行执行', 'LOOP': '循环执行' };
+      return map[type] || type;
+    },
+    getStepNodeClass(step) { return 'step-' + (step.stepType || 'default').toLowerCase(); },
+    getConditionPreview(expr) {
+      if (!expr) return '未设置条件';
+      try {
+        const obj = JSON.parse(expr);
+        return `${obj.left} ${obj.op} ${obj.right}`;
+      } catch { return expr; }
+    },
+    getDeviceTreeIcon(data) {
+      if (data.type === 'category') return data.icon || 'el-icon-folder';
+      if (data.type === 'device') return 'el-icon-cpu';
+      if (data.type === 'ability') return 'el-icon-s-operation';
+      return 'el-icon-document';
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+/* 样式与原文件保持一致,此处省略大部分样式 */
+.strategy-editor {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: #f0f2f5;
+}
+
+.editor-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 20px;
+  background: #fff;
+  border-bottom: 1px solid #e4e7ed;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    .strategy-info {
+      margin-left: 16px;
+      .strategy-name {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 600;
+        display: flex;
+        align-items: center;
+        gap: 8px;
+      }
+      .strategy-code { font-size: 12px; color: #909399; }
+    }
+  }
+  .header-right { display: flex; align-items: center; gap: 8px; }
+}
+
+.editor-main {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+}
+
+.component-panel {
+  width: 260px;
+  background: #fff;
+  border-right: 1px solid #e4e7ed;
+  overflow-y: auto;
+
+  .component-list {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 8px;
+    padding: 8px;
+
+    .component-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      padding: 12px 8px;
+      background: #f5f7fa;
+      border-radius: 8px;
+      cursor: grab;
+      transition: all 0.2s;
+      &:hover { background: #ecf5ff; transform: translateY(-2px); }
+      .component-icon {
+        width: 36px; height: 36px; border-radius: 8px;
+        display: flex; align-items: center; justify-content: center; margin-bottom: 6px;
+        i { color: #fff; font-size: 18px; }
+      }
+      .component-name { font-size: 12px; color: #606266; }
+    }
+  }
+
+  .device-search { padding: 8px; }
+  .device-tree {
+    max-height: 300px;
+    overflow-y: auto;
+    .device-tree-node { display: flex; align-items: center; gap: 6px; i { color: #909399; } }
+  }
+
+  .context-vars {
+    padding: 8px;
+    .context-var-item {
+      display: flex; justify-content: space-between;
+      padding: 8px 12px; background: #f5f7fa; border-radius: 6px; margin-bottom: 6px; cursor: grab;
+      .var-name { font-size: 13px; color: #303133; }
+      .var-type { font-size: 12px; color: #909399; }
+    }
+  }
+}
+
+.canvas-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fafafa;
+  position: relative;
+
+  .canvas-toolbar {
+    position: absolute; top: 16px; left: 16px; z-index: 10;
+    display: flex; gap: 12px; padding: 8px;
+    background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+
+  .canvas-container { flex: 1; overflow: auto; padding: 80px 40px 40px; }
+
+  .canvas-content {
+    display: flex; flex-direction: column; align-items: center; gap: 16px;
+    transform-origin: top center; min-width: 400px;
+  }
+}
+
+.flow-node {
+  background: #fff; border-radius: 10px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  transition: all 0.2s; position: relative;
+  &:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); }
+  &.selected { box-shadow: 0 0 0 2px #409eff, 0 4px 16px rgba(64, 158, 255, 0.3); }
+}
+
+.start-node, .end-node {
+  display: flex; align-items: center; gap: 8px; padding: 12px 24px;
+  .node-icon {
+    width: 32px; height: 32px; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+  }
+  span { font-size: 14px; font-weight: 500; color: #606266; }
+}
+.start-node .node-icon { background: #67c23a; i { color: #fff; } }
+.end-node .node-icon { background: #909399; i { color: #fff; } }
+
+.trigger-node, .step-node {
+  width: 280px;
+  .node-header {
+    padding: 10px 14px; border-radius: 10px 10px 0 0;
+    display: flex; align-items: center; gap: 8px;
+    i, span { color: #fff; } span { font-weight: 500; }
+  }
+  .node-body { padding: 12px 14px; p { margin: 0 0 6px; font-size: 13px; color: #606266; display: flex; align-items: center; gap: 6px; i { color: #909399; } &:last-child { margin-bottom: 0; } } }
+  .node-actions {
+    position: absolute; top: 8px; right: 8px; display: none; gap: 8px;
+    i { color: rgba(255, 255, 255, 0.8); cursor: pointer; &:hover { color: #fff; } }
+  }
+  &:hover .node-actions { display: flex; }
+}
+
+.step-node {
+  cursor: grab;
+  .node-index {
+    position: absolute; top: -8px; left: -8px;
+    width: 24px; height: 24px; background: #409eff; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    color: #fff; font-size: 12px; font-weight: 600;
+  }
+}
+
+.flow-connector {
+  width: 2px; height: 24px; background: #dcdfe6; position: relative;
+  .connector-line { width: 100%; height: 100%; background: #dcdfe6; }
+  .connector-add {
+    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
+    width: 20px; height: 20px; background: #fff; border: 2px solid #dcdfe6; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    cursor: pointer; opacity: 0; transition: all 0.2s;
+    i { font-size: 12px; color: #909399; }
+    &:hover { border-color: #409eff; i { color: #409eff; } }
+  }
+  &:hover .connector-add { opacity: 1; }
+}
+
+.add-step-btn {
+  display: flex; align-items: center; gap: 8px;
+  padding: 12px 24px; background: #fff; border: 2px dashed #dcdfe6; border-radius: 10px;
+  cursor: pointer; color: #909399; transition: all 0.2s;
+  &:hover { border-color: #409eff; color: #409eff; }
+}
+
+.property-panel {
+  width: 320px; background: #fff; border-left: 1px solid #e4e7ed;
+  display: flex; flex-direction: column;
+  .panel-header {
+    display: flex; justify-content: space-between; align-items: center;
+    padding: 14px 16px; border-bottom: 1px solid #e4e7ed; font-weight: 600;
+    i { cursor: pointer; color: #909399; &:hover { color: #606266; } }
+  }
+  .panel-body { flex: 1; overflow-y: auto; padding: 16px; }
+}
+
+.step-type-grid {
+  display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
+  .step-type-card {
+    display: flex; align-items: center; gap: 12px;
+    padding: 16px; background: #f5f7fa; border-radius: 10px;
+    cursor: pointer; transition: all 0.2s;
+    &:hover { background: #ecf5ff; transform: translateY(-2px); }
+    .step-type-icon {
+      width: 44px; height: 44px; border-radius: 10px;
+      display: flex; align-items: center; justify-content: center;
+      i { color: #fff; font-size: 20px; }
+    }
+    .step-type-info {
+      flex: 1;
+      .step-type-name { display: block; font-weight: 600; color: #303133; margin-bottom: 4px; }
+      .step-type-desc { display: block; font-size: 12px; color: #909399; }
+    }
+  }
+}
+
+.form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
+</style>

+ 1381 - 0
ems-ui-cloud/src/views/mgr/strategy/index.vue

@@ -0,0 +1,1381 @@
+<template>
+  <div class="strategy-container">
+    <!-- 顶部标题区 -->
+    <div class="page-header">
+      <div class="header-left">
+        <h1 class="page-title">
+          <i class="el-icon-connection"></i>
+          策略调度中心
+        </h1>
+        <span class="page-subtitle">设备协同 · 智能调度 · 能源优化</span>
+      </div>
+      <div class="header-right">
+        <el-button type="primary" icon="el-icon-plus" @click="handleCreate">
+          新建策略
+        </el-button>
+        <el-dropdown @command="handleTemplateSelect" trigger="click">
+          <el-button icon="el-icon-document-copy">
+            从模板创建 <i class="el-icon-arrow-down el-icon--right"></i>
+          </el-button>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item
+              v-for="tpl in templateList"
+              :key="tpl.templateCode"
+              :command="tpl"
+            >
+              <i :class="tpl.icon || 'el-icon-document'"></i>
+              {{ tpl.templateName }}
+            </el-dropdown-item>
+            <el-dropdown-item divided disabled v-if="templateList.length === 0">
+              暂无可用模板
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+      </div>
+    </div>
+
+    <div class="main-content">
+      <!-- 左侧导航面板 -->
+      <div class="side-panel">
+        <!-- 场景分类 -->
+        <div class="nav-section">
+          <div class="nav-title">场景分类</div>
+          <div class="nav-list">
+            <div
+              v-for="scene in sceneTypes"
+              :key="scene.value"
+              :class="['nav-item', { active: currentScene === scene.value }]"
+              @click="handleSceneChange(scene.value)"
+            >
+              <div class="nav-item-icon" :style="{ background: scene.gradient }">
+                <i :class="scene.icon"></i>
+              </div>
+              <div class="nav-item-content">
+                <span class="nav-item-label">{{ scene.label }}</span>
+                <span class="nav-item-count">{{ getSceneCount(scene.value) }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 区块筛选 -->
+        <div class="nav-section">
+          <div class="nav-title">
+            区块筛选
+            <el-input
+              v-model="areaKeyword"
+              placeholder="搜索区块"
+              size="mini"
+              prefix-icon="el-icon-search"
+              clearable
+              class="area-search"
+            />
+          </div>
+          <div class="area-tree-wrapper">
+            <el-tree
+              ref="areaTree"
+              :data="areaOptions"
+              :props="{ children: 'children', label: 'label' }"
+              :filter-node-method="filterAreaNode"
+              node-key="id"
+              highlight-current
+              :expand-on-click-node="false"
+              @node-click="handleAreaClick"
+            >
+              <span class="area-tree-node" slot-scope="{ node, data }">
+                <i :class="data.id === '-1' ? 'el-icon-s-home' : 'el-icon-location-outline'"></i>
+                <span>{{ node.label }}</span>
+              </span>
+            </el-tree>
+          </div>
+        </div>
+
+        <!-- 快捷筛选 -->
+        <div class="nav-section">
+          <div class="nav-title">快捷筛选</div>
+          <div class="quick-filters">
+            <el-tag
+              :effect="quickFilter === 'running' ? 'dark' : 'plain'"
+              @click="setQuickFilter('running')"
+            >
+              <i class="el-icon-video-play"></i> 运行中
+            </el-tag>
+            <el-tag
+              :effect="quickFilter === 'stopped' ? 'dark' : 'plain'"
+              @click="setQuickFilter('stopped')"
+            >
+              <i class="el-icon-video-pause"></i> 已停用
+            </el-tag>
+            <el-tag
+              :effect="quickFilter === 'debug' ? 'dark' : 'plain'"
+              type="warning"
+              @click="setQuickFilter('debug')"
+            >
+              <i class="el-icon-magic-stick"></i> 调试中
+            </el-tag>
+            <el-tag
+              :effect="quickFilter === 'failed' ? 'dark' : 'plain'"
+              type="danger"
+              @click="setQuickFilter('failed')"
+            >
+              <i class="el-icon-warning"></i> 执行失败
+            </el-tag>
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧内容区 -->
+      <div class="content-panel">
+        <!-- 搜索和视图切换 -->
+        <div class="content-toolbar">
+          <div class="toolbar-left">
+            <el-input
+              v-model="searchKeyword"
+              placeholder="搜索策略名称、代码..."
+              prefix-icon="el-icon-search"
+              clearable
+              style="width: 280px"
+              @input="handleSearch"
+            />
+            <el-select
+              v-model="triggerTypeFilter"
+              placeholder="触发类型"
+              clearable
+              style="width: 140px; margin-left: 12px"
+              @change="handleFilterChange"
+            >
+              <el-option label="事件触发" :value="1">
+                <i class="el-icon-lightning"></i> 事件触发
+              </el-option>
+              <el-option label="定时触发" :value="2">
+                <i class="el-icon-time"></i> 定时触发
+              </el-option>
+              <el-option label="手动触发" :value="3">
+                <i class="el-icon-s-operation"></i> 手动触发
+              </el-option>
+              <el-option label="条件触发" :value="4">
+                <i class="el-icon-set-up"></i> 条件触发
+              </el-option>
+            </el-select>
+          </div>
+          <div class="toolbar-right">
+            <el-radio-group v-model="viewMode" size="small">
+              <el-radio-button label="card">
+                <i class="el-icon-menu"></i>
+              </el-radio-button>
+              <el-radio-button label="table">
+                <i class="el-icon-s-unfold"></i>
+              </el-radio-button>
+            </el-radio-group>
+            <el-button
+              icon="el-icon-refresh"
+              circle
+              size="small"
+              @click="refreshList"
+              style="margin-left: 12px"
+            ></el-button>
+          </div>
+        </div>
+
+        <!-- 当前筛选状态 -->
+        <div class="filter-tags" v-if="hasActiveFilters">
+          <span class="filter-label">当前筛选:</span>
+          <el-tag
+            v-if="currentScene !== 'ALL'"
+            closable
+            size="small"
+            @close="currentScene = 'ALL'; getList()"
+          >
+            {{ getSceneLabel(currentScene) }}
+          </el-tag>
+          <el-tag
+            v-if="currentAreaCode"
+            closable
+            size="small"
+            @close="currentAreaCode = null; getList()"
+          >
+            {{ currentAreaName }}
+          </el-tag>
+          <el-tag
+            v-if="quickFilter"
+            closable
+            size="small"
+            @close="quickFilter = null; getList()"
+          >
+            {{ getQuickFilterLabel(quickFilter) }}
+          </el-tag>
+          <el-button type="text" size="mini" @click="clearAllFilters">
+            清除全部
+          </el-button>
+        </div>
+
+        <!-- 卡片视图 -->
+        <div v-if="viewMode === 'card'" class="strategy-cards" v-loading="loading">
+          <div
+            v-for="strategy in strategyList"
+            :key="strategy.id"
+            class="strategy-card"
+            @click="handleCardClick(strategy)"
+          >
+            <!-- 卡片头部 -->
+            <div class="card-header">
+              <div class="card-title-row">
+                <span class="card-title">{{ strategy.strategyName }}</span>
+                <el-dropdown trigger="click" @command="(cmd) => handleCardCommand(cmd, strategy)">
+                  <i class="el-icon-more card-more" @click.stop></i>
+                  <el-dropdown-menu slot="dropdown">
+                    <el-dropdown-item command="edit">
+                      <i class="el-icon-edit"></i> 编辑
+                    </el-dropdown-item>
+                    <el-dropdown-item command="copy">
+                      <i class="el-icon-document-copy"></i> 复制
+                    </el-dropdown-item>
+                    <el-dropdown-item command="export">
+                      <i class="el-icon-download"></i> 导出
+                    </el-dropdown-item>
+                    <el-dropdown-item command="delete" divided>
+                      <span style="color: #f56c6c"><i class="el-icon-delete"></i> 删除</span>
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </el-dropdown>
+              </div>
+              <div class="card-meta">
+                <span class="card-code">{{ strategy.strategyCode }}</span>
+                <el-tag
+                  :type="getSceneTagType(strategy.sceneType)"
+                  size="mini"
+                  effect="plain"
+                >
+                  {{ getSceneLabel(strategy.sceneType) }}
+                </el-tag>
+              </div>
+            </div>
+
+            <!-- 卡片内容 -->
+            <div class="card-body">
+              <p class="card-desc">{{ strategy.strategyDesc || '暂无描述' }}</p>
+
+              <!-- 触发器信息 -->
+              <div class="card-trigger">
+                <i :class="getTriggerIcon(strategy.triggerType)"></i>
+                <span>{{ getTriggerTypeName(strategy.triggerType) }}</span>
+                <span v-if="strategy.triggerType === 2" class="trigger-cron">
+                  {{ strategy.execRule }}
+                </span>
+              </div>
+            </div>
+
+            <!-- 卡片底部 -->
+            <div class="card-footer">
+              <div class="card-stats">
+                <div class="stat-item">
+                  <span class="stat-value">{{ strategy.execCount || 0 }}</span>
+                  <span class="stat-label">执行</span>
+                </div>
+                <div class="stat-item success">
+                  <span class="stat-value">{{ strategy.successCount || 0 }}</span>
+                  <span class="stat-label">成功</span>
+                </div>
+                <div class="stat-item fail">
+                  <span class="stat-value">{{ strategy.failCount || 0 }}</span>
+                  <span class="stat-label">失败</span>
+                </div>
+              </div>
+              <div class="card-actions">
+                <el-button
+                  type="primary"
+                  size="mini"
+                  plain
+                  icon="el-icon-video-play"
+                  @click.stop="handleExecute(strategy)"
+                  :disabled="strategy.strategyState === 0"
+                >
+                  执行
+                </el-button>
+                <el-switch
+                  v-model="strategy.strategyState"
+                  :active-value="1"
+                  :inactive-value="0"
+                  @change="handleStateChange(strategy)"
+                  @click.native.stop
+                ></el-switch>
+              </div>
+            </div>
+
+            <!-- 状态指示器 -->
+            <div :class="['card-status-indicator', getStatusClass(strategy)]"></div>
+          </div>
+
+          <!-- 空状态 -->
+          <div v-if="!loading && strategyList.length === 0" class="empty-state">
+            <i class="el-icon-folder-opened empty-icon"></i>
+            <p class="empty-text">暂无策略数据</p>
+            <el-button type="primary" icon="el-icon-plus" @click="handleCreate">
+              创建第一个策略
+            </el-button>
+          </div>
+        </div>
+
+        <!-- 表格视图 -->
+        <div v-else class="strategy-table" v-loading="loading">
+          <el-table :data="strategyList" stripe>
+            <el-table-column label="策略名称" min-width="180">
+              <template slot-scope="{ row }">
+                <div class="table-strategy-name">
+                  <span class="name-text" @click="handleCardClick(row)">
+                    {{ row.strategyName }}
+                  </span>
+                  <span class="code-text">{{ row.strategyCode }}</span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="场景类型" width="120" align="center">
+              <template slot-scope="{ row }">
+                <el-tag :type="getSceneTagType(row.sceneType)" size="small">
+                  {{ getSceneLabel(row.sceneType) }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="触发类型" width="120" align="center">
+              <template slot-scope="{ row }">
+                <span>
+                  <i :class="getTriggerIcon(row.triggerType)"></i>
+                  {{ getTriggerTypeName(row.triggerType) }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column label="优先级" width="80" align="center">
+              <template slot-scope="{ row }">
+                <el-tag :type="getPriorityType(row.priority)" size="mini">
+                  {{ row.priority }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="状态" width="80" align="center">
+              <template slot-scope="{ row }">
+                <el-switch
+                  v-model="row.strategyState"
+                  :active-value="1"
+                  :inactive-value="0"
+                  @change="handleStateChange(row)"
+                ></el-switch>
+              </template>
+            </el-table-column>
+            <el-table-column label="执行统计" width="150" align="center">
+              <template slot-scope="{ row }">
+                <div class="table-stats">
+                  <span>{{ row.execCount || 0 }}</span> /
+                  <span class="success">{{ row.successCount || 0 }}</span> /
+                  <span class="fail">{{ row.failCount || 0 }}</span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="最后执行" width="160" align="center">
+              <template slot-scope="{ row }">
+                <div v-if="row.lastExecTime">
+                  <div>{{ parseTime(row.lastExecTime) }}</div>
+                  <el-tag :type="row.lastExecResult === 0 ? 'success' : 'danger'" size="mini">
+                    {{ row.lastExecResult === 0 ? '成功' : '失败' }}
+                  </el-tag>
+                </div>
+                <span v-else class="text-muted">-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="200" align="center" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button size="mini" type="text" @click="handleExecute(row)">
+                  <i class="el-icon-video-play"></i> 执行
+                </el-button>
+                <el-button size="mini" type="text" @click="handleCardClick(row)">
+                  <i class="el-icon-edit"></i> 编排
+                </el-button>
+                <el-button size="mini" type="text" @click="handleViewLog(row)">
+                  <i class="el-icon-document"></i> 日志
+                </el-button>
+                <el-button size="mini" type="text" class="text-danger" @click="handleDelete(row)">
+                  <i class="el-icon-delete"></i>
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <!-- 分页 -->
+        <div class="pagination-wrapper" v-if="total > 0">
+          <el-pagination
+            background
+            layout="total, sizes, prev, pager, next, jumper"
+            :total="total"
+            :page-size.sync="queryParams.pageSize"
+            :current-page.sync="queryParams.pageNum"
+            :page-sizes="[12, 24, 48, 96]"
+            @size-change="getList"
+            @current-change="getList"
+          />
+        </div>
+      </div>
+    </div>
+
+    <!-- 新建/编辑策略基础信息弹窗 -->
+    <el-dialog
+      :title="dialogTitle"
+      :visible.sync="dialogVisible"
+      width="650px"
+      :close-on-click-modal="false"
+    >
+      <el-form ref="strategyForm" :model="strategyForm" :rules="strategyRules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="策略代码" prop="strategyCode">
+              <el-input
+                v-model="strategyForm.strategyCode"
+                placeholder="如: PV_ESS_001"
+                :disabled="strategyForm.id != null"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="策略名称" prop="strategyName">
+              <el-input v-model="strategyForm.strategyName" placeholder="请输入策略名称" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="场景类型" prop="sceneType">
+              <el-select v-model="strategyForm.sceneType" placeholder="请选择场景类型" style="width: 100%">
+                <el-option
+                  v-for="scene in sceneTypes.filter(s => s.value !== 'ALL')"
+                  :key="scene.value"
+                  :label="scene.label"
+                  :value="scene.value"
+                >
+                  <i :class="scene.icon" style="margin-right: 8px"></i>
+                  {{ scene.label }}
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="所属区块" prop="areaCode">
+              <treeselect
+                v-model="strategyForm.areaCode"
+                :options="areaTreeOptions"
+                placeholder="请选择区块"
+                :normalizer="normalizeAreaNode"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="触发类型" prop="triggerType">
+              <el-select v-model="strategyForm.triggerType" placeholder="请选择触发类型" style="width: 100%">
+                <el-option label="事件触发" :value="1">
+                  <i class="el-icon-lightning"></i> 事件触发
+                </el-option>
+                <el-option label="定时触发" :value="2">
+                  <i class="el-icon-time"></i> 定时触发
+                </el-option>
+                <el-option label="手动触发" :value="3">
+                  <i class="el-icon-s-operation"></i> 手动触发
+                </el-option>
+                <el-option label="条件触发" :value="4">
+                  <i class="el-icon-set-up"></i> 条件触发
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="优先级" prop="priority">
+              <el-slider v-model="strategyForm.priority" :min="0" :max="100" show-input />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="CRON表达式" prop="execRule" v-if="strategyForm.triggerType === 2">
+          <el-input v-model="strategyForm.execRule" placeholder="如: 0 0 8 * * ?">
+            <template slot="append">
+              <el-button @click="showCronHelper">
+                <i class="el-icon-question"></i>
+              </el-button>
+            </template>
+          </el-input>
+          <div class="form-tip">提示:0 0 8 * * ? 表示每天8点执行</div>
+        </el-form-item>
+        <el-form-item label="策略描述" prop="strategyDesc">
+          <el-input
+            type="textarea"
+            v-model="strategyForm.strategyDesc"
+            :rows="3"
+            placeholder="请输入策略描述"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="handleSaveOnly" :loading="saveLoading">仅保存</el-button>
+        <el-button type="success" @click="handleSaveAndEdit" :loading="saveLoading">保存并编排</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 执行日志弹窗 -->
+    <el-dialog title="执行日志" :visible.sync="logDialogVisible" width="90%">
+      <el-table :data="execLogList" border v-loading="logLoading">
+        <el-table-column label="执行ID" prop="execId" width="280" show-overflow-tooltip />
+        <el-table-column label="触发类型" prop="triggerType" width="100" />
+        <el-table-column label="触发源" prop="triggerSource" width="120" />
+        <el-table-column label="执行状态" prop="execStatus" width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-tag :type="getExecStatusType(row.execStatus)" size="small">
+              {{ getExecStatusText(row.execStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="开始时间" prop="startTime" width="160">
+          <template slot-scope="{ row }">
+            {{ parseTime(row.startTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="耗时(ms)" prop="duration" width="100" align="center" />
+        <el-table-column label="错误信息" prop="errorMessage" show-overflow-tooltip />
+        <el-table-column label="操作" width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="text" @click="viewLogDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listEnergyStrategy,
+  getEnergyStrategy,
+  addEnergyStrategy,
+  updateEnergyStrategy,
+  delEnergyStrategy,
+  changeStrategyState,
+  executeStrategy,
+  getExecLogList,
+  listStrategyTemplate
+} from '@/api/mgr/energyStrategy';
+import { areaTreeSelect } from '@/api/basecfg/area';
+import Treeselect from '@riophae/vue-treeselect';
+import '@riophae/vue-treeselect/dist/vue-treeselect.css';
+
+export default {
+  name: 'StrategyCenter',
+  components: { Treeselect },
+
+  data() {
+    return {
+      loading: false,
+      logLoading: false,
+      saveLoading: false,
+      viewMode: 'card',
+
+      // 场景类型定义
+      sceneTypes: [
+        { value: 'ALL', label: '全部策略', icon: 'el-icon-menu', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
+        { value: 'PV_ESS', label: '光储协同', icon: 'el-icon-sunny', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
+        { value: 'DEMAND_RESP', label: '需求响应', icon: 'el-icon-s-marketing', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
+        { value: 'PEAK_VALLEY', label: '削峰填谷', icon: 'el-icon-data-analysis', gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
+        { value: 'EMERGENCY', label: '应急保供', icon: 'el-icon-warning-outline', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
+        { value: 'ENERGY_SAVE', label: '节能优化', icon: 'el-icon-odometer', gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }
+      ],
+
+      currentScene: 'ALL',
+      currentAreaCode: null,
+      currentAreaName: '',
+      quickFilter: null,
+      searchKeyword: '',
+      triggerTypeFilter: null,
+      areaKeyword: '',
+
+      // 数据
+      strategyList: [],
+      total: 0,
+      areaOptions: [],
+      areaTreeOptions: [],
+      templateList: [],
+      sceneCountMap: {},
+
+      queryParams: {
+        pageNum: 1,
+        pageSize: 12,
+        strategyName: null,
+        sceneType: null,
+        areaCode: null,
+        triggerType: null,
+        strategyState: null
+      },
+
+      // 弹窗
+      dialogVisible: false,
+      dialogTitle: '',
+      strategyForm: {},
+      strategyRules: {
+        strategyCode: [{ required: true, message: '请输入策略代码', trigger: 'blur' }],
+        strategyName: [{ required: true, message: '请输入策略名称', trigger: 'blur' }],
+        sceneType: [{ required: true, message: '请选择场景类型', trigger: 'change' }],
+        areaCode: [{ required: true, message: '请选择所属区块', trigger: 'change' }],
+        triggerType: [{ required: true, message: '请选择触发类型', trigger: 'change' }]
+      },
+
+      // 日志
+      logDialogVisible: false,
+      execLogList: [],
+      currentLogStrategy: null
+    };
+  },
+
+  computed: {
+    hasActiveFilters() {
+      return this.currentScene !== 'ALL' || this.currentAreaCode || this.quickFilter;
+    },
+    // 获取当前路由的基础路径
+    basePath() {
+      // 从当前路由获取父级路径
+      const path = this.$route.path;
+      // 如果当前路径包含 /index,则去掉
+      if (path.endsWith('/index')) {
+        return path.replace('/index', '');
+      }
+      return path;
+    }
+  },
+
+  watch: {
+    areaKeyword(val) {
+      this.$refs.areaTree && this.$refs.areaTree.filter(val);
+    }
+  },
+
+  created() {
+    this.loadAreaTree();
+    this.loadTemplates();
+    this.getList();
+  },
+
+  methods: {
+    // 加载区块树
+    loadAreaTree() {
+      areaTreeSelect('0', 1).then(response => {
+        const treeData = response.data || [];
+        this.areaOptions = [{
+          id: '-1',
+          label: '全部区块',
+          children: treeData
+        }];
+        this.areaTreeOptions = treeData;
+      }).catch(() => {
+        this.areaOptions = [];
+        this.areaTreeOptions = [];
+      });
+    },
+
+    // 加载模板列表
+    loadTemplates() {
+      listStrategyTemplate({ status: 1 }).then(response => {
+        this.templateList = response.rows || response.data || [];
+      }).catch(() => {
+        this.templateList = [];
+      });
+    },
+
+    // 获取策略列表
+    getList() {
+      this.loading = true;
+
+      const params = { ...this.queryParams };
+      if (this.currentScene !== 'ALL') {
+        params.sceneType = this.currentScene;
+      }
+      if (this.currentAreaCode && this.currentAreaCode !== '-1') {
+        params.areaCode = this.currentAreaCode;
+      }
+      if (this.searchKeyword) {
+        params.strategyName = this.searchKeyword;
+      }
+      if (this.triggerTypeFilter) {
+        params.triggerType = this.triggerTypeFilter;
+      }
+      if (this.quickFilter) {
+        switch (this.quickFilter) {
+          case 'running': params.strategyState = 1; break;
+          case 'stopped': params.strategyState = 0; break;
+          case 'debug': params.strategyState = 2; break;
+          case 'failed': params.lastExecResult = 1; break;
+        }
+      }
+
+      listEnergyStrategy(params).then(response => {
+        this.strategyList = response.rows || [];
+        this.total = response.total || 0;
+        this.loading = false;
+        this.updateSceneCounts();
+      }).catch(() => {
+        this.strategyList = [];
+        this.total = 0;
+        this.loading = false;
+      });
+    },
+
+    updateSceneCounts() {
+      this.sceneCountMap['ALL'] = this.total;
+    },
+
+    refreshList() {
+      this.getList();
+      this.$message.success('刷新成功');
+    },
+
+    handleSceneChange(scene) {
+      this.currentScene = scene;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    handleAreaClick(data) {
+      this.currentAreaCode = data.id === '-1' ? null : data.id;
+      this.currentAreaName = data.label;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    filterAreaNode(value, data) {
+      if (!value) return true;
+      return data.label.indexOf(value) !== -1;
+    },
+
+    setQuickFilter(filter) {
+      this.quickFilter = this.quickFilter === filter ? null : filter;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    handleSearch() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    handleFilterChange() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    clearAllFilters() {
+      this.currentScene = 'ALL';
+      this.currentAreaCode = null;
+      this.quickFilter = null;
+      this.searchKeyword = '';
+      this.triggerTypeFilter = null;
+      this.getList();
+    },
+
+    // 新建策略
+    handleCreate() {
+      this.strategyForm = {
+        strategyCode: '',
+        strategyName: '',
+        sceneType: this.currentScene !== 'ALL' ? this.currentScene : null,
+        areaCode: this.currentAreaCode,
+        triggerType: 3,
+        priority: 50,
+        strategyDesc: '',
+        strategyState: 0
+      };
+      this.dialogTitle = '新建策略';
+      this.dialogVisible = true;
+    },
+
+    // 从模板创建
+    handleTemplateSelect(template) {
+      this.strategyForm = {
+        strategyCode: '',
+        strategyName: template.templateName + '_副本',
+        sceneType: template.sceneType || null,
+        areaCode: this.currentAreaCode,
+        triggerType: 3,
+        priority: 50,
+        strategyDesc: '基于模板【' + template.templateName + '】创建',
+        strategyState: 0,
+        templateCode: template.templateCode
+      };
+      this.dialogTitle = '从模板创建策略';
+      this.dialogVisible = true;
+    },
+
+    // 仅保存(不跳转)
+    handleSaveOnly() {
+      this.$refs.strategyForm.validate(valid => {
+        if (!valid) return;
+
+        this.saveLoading = true;
+        const isEdit = !!this.strategyForm.id;
+        const saveMethod = isEdit ? updateEnergyStrategy : addEnergyStrategy;
+
+        saveMethod(this.strategyForm).then(() => {
+          this.$message.success('保存成功');
+          this.dialogVisible = false;
+          this.saveLoading = false;
+          // 刷新列表
+          this.getList();
+        }).catch(error => {
+          this.saveLoading = false;
+          this.$message.error('保存失败: ' + (error.msg || error.message || '未知错误'));
+        });
+      });
+    },
+
+    // 保存并进入编排
+    handleSaveAndEdit() {
+      this.$refs.strategyForm.validate(valid => {
+        if (!valid) return;
+
+        this.saveLoading = true;
+        const isEdit = !!this.strategyForm.id;
+        const saveMethod = isEdit ? updateEnergyStrategy : addEnergyStrategy;
+
+        saveMethod(this.strategyForm).then(() => {
+          this.$message.success('保存成功');
+          this.dialogVisible = false;
+          this.saveLoading = false;
+
+          const strategyCode = this.strategyForm.strategyCode;
+          // 跳转到编排页面 - 使用相对路径
+          this.$router.push({
+            path: '/strategy-mgr/strategy-editor',
+            query: { strategyCode: strategyCode }
+          });
+        }).catch(error => {
+          this.saveLoading = false;
+          this.$message.error('保存失败: ' + (error.msg || error.message || '未知错误'));
+        });
+      });
+    },
+
+    // 点击卡片进入编排
+    handleCardClick(strategy) {
+      this.$router.push({
+        path: '/strategy-mgr/strategy-editor',
+        query: { strategyCode: strategy.strategyCode }
+      });
+    },
+
+    // 卡片操作菜单
+    handleCardCommand(command, strategy) {
+      switch (command) {
+        case 'edit':
+          this.handleEdit(strategy);
+          break;
+        case 'copy':
+          this.handleCopy(strategy);
+          break;
+        case 'export':
+          this.handleExport(strategy);
+          break;
+        case 'delete':
+          this.handleDelete(strategy);
+          break;
+      }
+    },
+
+    handleEdit(strategy) {
+      getEnergyStrategy(strategy.id).then(response => {
+        this.strategyForm = response.data || response;
+        this.dialogTitle = '编辑策略';
+        this.dialogVisible = true;
+      });
+    },
+
+    handleCopy(strategy) {
+      this.strategyForm = {
+        ...strategy,
+        id: null,
+        strategyCode: strategy.strategyCode + '_copy',
+        strategyName: strategy.strategyName + '_副本'
+      };
+      this.dialogTitle = '复制策略';
+      this.dialogVisible = true;
+    },
+
+    handleExport(strategy) {
+      const exportData = {
+        strategy: strategy,
+        exportTime: new Date().toISOString()
+      };
+      const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `strategy_${strategy.strategyCode}.json`;
+      a.click();
+      URL.revokeObjectURL(url);
+      this.$message.success('导出成功');
+    },
+
+    handleDelete(strategy) {
+      this.$confirm('确认删除策略【' + strategy.strategyName + '】吗?', '警告', {
+        type: 'warning'
+      }).then(() => {
+        delEnergyStrategy(strategy.id).then(() => {
+          this.$message.success('删除成功');
+          this.getList();
+        });
+      }).catch(() => {});
+    },
+
+    // 执行策略
+    handleExecute(strategy) {
+      this.$confirm('确认执行策略【' + strategy.strategyName + '】吗?', '提示', {
+        type: 'info'
+      }).then(() => {
+        executeStrategy(strategy.strategyCode, {}).then(response => {
+          const result = response.data || response;
+          this.$message.success('策略已开始执行,执行ID: ' + (result.execId || ''));
+          setTimeout(() => this.getList(), 1000);
+        }).catch(error => {
+          this.$message.error('执行失败: ' + (error.msg || error.message || '未知错误'));
+        });
+      }).catch(() => {});
+    },
+
+    // 状态切换
+    handleStateChange(strategy) {
+      const originalState = strategy.strategyState === 1 ? 0 : 1;
+      changeStrategyState(strategy.strategyCode, strategy.strategyState).then(() => {
+        this.$message.success(strategy.strategyState === 1 ? '启用成功' : '停用成功');
+      }).catch(() => {
+        strategy.strategyState = originalState;
+        this.$message.error('操作失败');
+      });
+    },
+
+    // 查看日志
+    handleViewLog(strategy) {
+      this.currentLogStrategy = strategy;
+      this.logDialogVisible = true;
+      this.logLoading = true;
+
+      getExecLogList(strategy.strategyCode, { pageNum: 1, pageSize: 50 }).then(response => {
+        this.execLogList = response.rows || response.data || [];
+        this.logLoading = false;
+      }).catch(() => {
+        this.execLogList = [];
+        this.logLoading = false;
+      });
+    },
+
+    viewLogDetail(log) {
+      // 跳转到日志详情页
+      this.$router.push({
+        path: '/strategy-mgr/strategy-log',
+        query: { execId: log.execId }
+      });
+    },
+
+    showCronHelper() {
+      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
+      });
+    },
+
+    normalizeAreaNode(node) {
+      return {
+        id: node.id,
+        label: node.label,
+        children: node.children
+      };
+    },
+
+    // 辅助方法
+    getSceneCount(scene) {
+      return this.sceneCountMap[scene] || 0;
+    },
+
+    getSceneLabel(scene) {
+      const found = this.sceneTypes.find(s => s.value === scene);
+      return found ? found.label : scene || '未分类';
+    },
+
+    getSceneTagType(scene) {
+      const typeMap = {
+        'PV_ESS': 'warning',
+        'DEMAND_RESP': '',
+        'PEAK_VALLEY': 'success',
+        'EMERGENCY': 'danger',
+        'ENERGY_SAVE': 'info'
+      };
+      return typeMap[scene] || '';
+    },
+
+    getQuickFilterLabel(filter) {
+      const map = { running: '运行中', stopped: '已停用', debug: '调试中', failed: '执行失败' };
+      return map[filter] || filter;
+    },
+
+    getTriggerTypeName(type) {
+      const map = { 1: '事件触发', 2: '定时触发', 3: '手动触发', 4: '条件触发' };
+      return map[type] || '-';
+    },
+
+    getTriggerIcon(type) {
+      const map = { 1: 'el-icon-lightning', 2: 'el-icon-time', 3: 'el-icon-s-operation', 4: 'el-icon-set-up' };
+      return map[type] || 'el-icon-question';
+    },
+
+    getPriorityType(priority) {
+      if (priority >= 80) return 'danger';
+      if (priority >= 50) return 'warning';
+      return 'info';
+    },
+
+    getStatusClass(strategy) {
+      if (strategy.strategyState === 0) return 'status-stopped';
+      if (strategy.strategyState === 2) return 'status-debug';
+      if (strategy.lastExecResult === 1) return 'status-error';
+      return 'status-running';
+    },
+
+    getExecStatusType(status) {
+      const map = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' };
+      return map[status] || '';
+    },
+
+    getExecStatusText(status) {
+      const map = { 0: '执行中', 1: '成功', 2: '失败', 3: '超时' };
+      return map[status] || '-';
+    },
+
+    parseTime(time) {
+      if (!time) return '-';
+      return new Date(time).toLocaleString('zh-CN', { hour12: false });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+/* 样式保持不变,此处省略 */
+.strategy-container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #f0f2f5 0%, #e8ecf1 100%);
+  padding: 20px;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 20px 24px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  .header-left {
+    .page-title {
+      margin: 0;
+      font-size: 22px;
+      font-weight: 600;
+      color: #1a1a2e;
+      i { margin-right: 10px; color: #409eff; }
+    }
+    .page-subtitle {
+      display: block;
+      margin-top: 4px;
+      font-size: 13px;
+      color: #909399;
+    }
+  }
+  .header-right {
+    display: flex;
+    gap: 12px;
+  }
+}
+
+.main-content {
+  display: flex;
+  gap: 20px;
+}
+
+.side-panel {
+  width: 260px;
+  flex-shrink: 0;
+
+  .nav-section {
+    background: #fff;
+    border-radius: 12px;
+    padding: 16px;
+    margin-bottom: 16px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    .nav-title {
+      font-size: 13px;
+      font-weight: 600;
+      color: #606266;
+      margin-bottom: 12px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .area-search { width: 120px; }
+    }
+  }
+
+  .nav-list {
+    .nav-item {
+      display: flex;
+      align-items: center;
+      padding: 10px 12px;
+      border-radius: 8px;
+      cursor: pointer;
+      transition: all 0.2s;
+      margin-bottom: 4px;
+
+      &:hover { background: #f5f7fa; }
+      &.active {
+        background: linear-gradient(135deg, #ecf5ff 0%, #f0f7ff 100%);
+        .nav-item-label { color: #409eff; font-weight: 600; }
+      }
+
+      .nav-item-icon {
+        width: 32px;
+        height: 32px;
+        border-radius: 8px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-right: 12px;
+        i { color: #fff; font-size: 16px; }
+      }
+
+      .nav-item-content {
+        flex: 1;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .nav-item-label { font-size: 14px; color: #303133; }
+        .nav-item-count {
+          font-size: 12px;
+          color: #909399;
+          background: #f0f2f5;
+          padding: 2px 8px;
+          border-radius: 10px;
+        }
+      }
+    }
+  }
+
+  .area-tree-wrapper {
+    max-height: 200px;
+    overflow-y: auto;
+    .area-tree-node {
+      display: flex;
+      align-items: center;
+      i { margin-right: 6px; color: #909399; }
+    }
+  }
+
+  .quick-filters {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    .el-tag {
+      cursor: pointer;
+      transition: all 0.2s;
+      &:hover { transform: scale(1.05); }
+    }
+  }
+}
+
+.content-panel {
+  flex: 1;
+  min-width: 0;
+
+  .content-toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px 20px;
+    background: #fff;
+    border-radius: 12px;
+    margin-bottom: 16px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+    .toolbar-left, .toolbar-right { display: flex; align-items: center; }
+  }
+
+  .filter-tags {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 16px;
+    padding: 0 4px;
+    .filter-label { font-size: 13px; color: #909399; }
+  }
+}
+
+.strategy-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+  gap: 16px;
+
+  .strategy-card {
+    background: #fff;
+    border-radius: 12px;
+    padding: 20px;
+    cursor: pointer;
+    transition: all 0.3s;
+    position: relative;
+    overflow: hidden;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+    &:hover {
+      transform: translateY(-4px);
+      box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
+    }
+
+    .card-header {
+      margin-bottom: 12px;
+      .card-title-row {
+        display: flex;
+        justify-content: space-between;
+        align-items: flex-start;
+        .card-title { font-size: 16px; font-weight: 600; color: #303133; }
+        .card-more {
+          color: #909399;
+          cursor: pointer;
+          padding: 4px;
+          &:hover { color: #409eff; }
+        }
+      }
+      .card-meta {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-top: 6px;
+        .card-code { font-size: 12px; color: #909399; }
+      }
+    }
+
+    .card-body {
+      .card-desc {
+        font-size: 13px;
+        color: #606266;
+        line-height: 1.5;
+        margin-bottom: 12px;
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+        overflow: hidden;
+      }
+      .card-trigger {
+        display: flex;
+        align-items: center;
+        font-size: 13px;
+        color: #909399;
+        i { margin-right: 6px; }
+        .trigger-cron {
+          margin-left: 8px;
+          padding: 2px 8px;
+          background: #f0f2f5;
+          border-radius: 4px;
+          font-family: monospace;
+          font-size: 12px;
+        }
+      }
+    }
+
+    .card-footer {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 16px;
+      padding-top: 16px;
+      border-top: 1px solid #f0f2f5;
+
+      .card-stats {
+        display: flex;
+        gap: 16px;
+        .stat-item {
+          text-align: center;
+          .stat-value { display: block; font-size: 18px; font-weight: 600; color: #303133; }
+          .stat-label { font-size: 12px; color: #909399; }
+          &.success .stat-value { color: #67c23a; }
+          &.fail .stat-value { color: #f56c6c; }
+        }
+      }
+      .card-actions { display: flex; align-items: center; gap: 12px; }
+    }
+
+    .card-status-indicator {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 4px;
+      height: 100%;
+      &.status-running { background: linear-gradient(180deg, #67c23a, #85ce61); }
+      &.status-stopped { background: linear-gradient(180deg, #909399, #c0c4cc); }
+      &.status-debug { background: linear-gradient(180deg, #e6a23c, #f5b86c); }
+      &.status-error { background: linear-gradient(180deg, #f56c6c, #f89898); }
+    }
+  }
+}
+
+.empty-state {
+  grid-column: 1 / -1;
+  text-align: center;
+  padding: 60px 20px;
+  .empty-icon { font-size: 64px; color: #dcdfe6; }
+  .empty-text { font-size: 14px; color: #909399; margin: 20px 0; }
+}
+
+.strategy-table {
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  .table-strategy-name {
+    .name-text {
+      display: block;
+      color: #409eff;
+      cursor: pointer;
+      &:hover { text-decoration: underline; }
+    }
+    .code-text { font-size: 12px; color: #909399; }
+  }
+  .table-stats {
+    .success { color: #67c23a; }
+    .fail { color: #f56c6c; }
+  }
+  .text-muted { color: #c0c4cc; }
+  .text-danger { color: #f56c6c; }
+}
+
+.pagination-wrapper {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 20px;
+  padding: 16px 20px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+</style>

+ 678 - 0
ems-ui-cloud/src/views/mgr/strategy/log.vue

@@ -0,0 +1,678 @@
+<template>
+  <div class="strategy-log-container">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <h1 class="page-title">
+        <i class="el-icon-document"></i>
+        策略执行日志
+      </h1>
+      <div class="header-actions">
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="yyyy-MM-dd"
+          @change="handleDateChange"
+          style="margin-right: 12px"
+        />
+        <el-button icon="el-icon-refresh" @click="refreshList">刷新</el-button>
+      </div>
+    </div>
+
+    <div class="main-content">
+      <!-- 左侧筛选面板 -->
+      <div class="filter-panel">
+        <div class="filter-section">
+          <div class="filter-title">策略筛选</div>
+          <el-select
+            v-model="queryParams.strategyCode"
+            placeholder="选择策略"
+            filterable
+            clearable
+            style="width: 100%"
+            @change="getList"
+          >
+            <el-option
+              v-for="strategy in strategyList"
+              :key="strategy.strategyCode"
+              :label="strategy.strategyName"
+              :value="strategy.strategyCode"
+            />
+          </el-select>
+        </div>
+
+        <div class="filter-section">
+          <div class="filter-title">执行状态</div>
+          <div class="status-tags">
+            <el-tag
+              v-for="status in execStatusOptions"
+              :key="status.value"
+              :effect="queryParams.execStatus === status.value ? 'dark' : 'plain'"
+              :type="status.type"
+              @click="handleStatusFilter(status.value)"
+              class="status-tag"
+            >
+              {{ status.label }}
+            </el-tag>
+          </div>
+        </div>
+
+        <div class="filter-section">
+          <div class="filter-title">触发类型</div>
+          <el-radio-group v-model="queryParams.triggerType" @change="getList">
+            <el-radio label="">全部</el-radio>
+            <el-radio label="EVENT">事件</el-radio>
+            <el-radio label="TIME">定时</el-radio>
+            <el-radio label="MANUAL">手动</el-radio>
+            <el-radio label="CONDITION">条件</el-radio>
+          </el-radio-group>
+        </div>
+
+        <div class="filter-section">
+          <div class="filter-title">执行统计</div>
+          <div class="stats-summary">
+            <div class="stat-item">
+              <span class="stat-value">{{ stats.total || 0 }}</span>
+              <span class="stat-label">总执行</span>
+            </div>
+            <div class="stat-item success">
+              <span class="stat-value">{{ stats.success || 0 }}</span>
+              <span class="stat-label">成功</span>
+            </div>
+            <div class="stat-item fail">
+              <span class="stat-value">{{ stats.fail || 0 }}</span>
+              <span class="stat-label">失败</span>
+            </div>
+            <div class="stat-item timeout">
+              <span class="stat-value">{{ stats.timeout || 0 }}</span>
+              <span class="stat-label">超时</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧日志列表 -->
+      <div class="log-panel">
+        <el-table :data="logList" v-loading="loading" border stripe>
+          <el-table-column label="执行ID" prop="execId" width="200" show-overflow-tooltip>
+            <template slot-scope="{ row }">
+              <el-button type="text" @click="viewLogDetail(row)">
+                {{ row.execId.slice(0, 8) }}...
+              </el-button>
+            </template>
+          </el-table-column>
+          <el-table-column label="策略" min-width="150">
+            <template slot-scope="{ row }">
+              <div class="strategy-info">
+                <span class="strategy-name">{{ row.strategyName || row.strategyCode }}</span>
+                <span class="strategy-code">{{ row.strategyCode }}</span>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="触发类型" width="100" align="center">
+            <template slot-scope="{ row }">
+              <el-tag size="small" :type="getTriggerTagType(row.triggerType)">
+                {{ getTriggerTypeName(row.triggerType) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="触发源" prop="triggerSource" width="120" show-overflow-tooltip />
+          <el-table-column label="执行状态" width="100" align="center">
+            <template slot-scope="{ row }">
+              <el-tag :type="getExecStatusType(row.execStatus)" size="small">
+                <i :class="getExecStatusIcon(row.execStatus)"></i>
+                {{ getExecStatusText(row.execStatus) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="开始时间" width="160">
+            <template slot-scope="{ row }">
+              {{ parseTime(row.startTime) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="耗时" width="100" align="center">
+            <template slot-scope="{ row }">
+              <span :class="getDurationClass(row.duration)">
+                {{ formatDuration(row.duration) }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="执行人" prop="execBy" width="100" align="center">
+            <template slot-scope="{ row }">
+              {{ row.execBy || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="120" align="center" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="text" @click="viewLogDetail(row)">
+                <i class="el-icon-view"></i> 详情
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <div class="pagination-wrapper">
+          <el-pagination
+            background
+            layout="total, sizes, prev, pager, next, jumper"
+            :total="total"
+            :page-size.sync="queryParams.pageSize"
+            :current-page.sync="queryParams.pageNum"
+            :page-sizes="[20, 50, 100]"
+            @size-change="getList"
+            @current-change="getList"
+          />
+        </div>
+      </div>
+    </div>
+
+    <!-- 日志详情抽屉 -->
+    <el-drawer
+      title="执行详情"
+      :visible.sync="detailDrawerVisible"
+      size="60%"
+      direction="rtl"
+    >
+      <div class="log-detail" v-if="currentLog">
+        <!-- 基本信息 -->
+        <div class="detail-section">
+          <h3 class="section-title">基本信息</h3>
+          <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="触发类型">{{ getTriggerTypeName(currentLog.triggerType) }}</el-descriptions-item>
+            <el-descriptions-item label="触发源">{{ currentLog.triggerSource || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="执行状态">
+              <el-tag :type="getExecStatusType(currentLog.execStatus)" size="small">
+                {{ getExecStatusText(currentLog.execStatus) }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="执行人">{{ currentLog.execBy || '系统' }}</el-descriptions-item>
+            <el-descriptions-item label="开始时间">{{ parseTime(currentLog.startTime) }}</el-descriptions-item>
+            <el-descriptions-item label="结束时间">{{ parseTime(currentLog.endTime) || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="执行耗时">{{ formatDuration(currentLog.duration) }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <!-- 错误信息 -->
+        <div class="detail-section" v-if="currentLog.errorMessage">
+          <h3 class="section-title error-title">
+            <i class="el-icon-warning"></i> 错误信息
+          </h3>
+          <div class="error-content">
+            {{ currentLog.errorMessage }}
+          </div>
+        </div>
+
+        <!-- 步骤执行时间线 -->
+        <div class="detail-section">
+          <h3 class="section-title">执行步骤</h3>
+          <el-timeline v-if="stepLogs.length > 0">
+            <el-timeline-item
+              v-for="step in stepLogs"
+              :key="step.id"
+              :type="getStepTimelineType(step.execStatus)"
+              :icon="getStepTimelineIcon(step.execStatus)"
+            >
+              <div class="step-log-item">
+                <div class="step-header">
+                  <span class="step-name">{{ step.stepName }}</span>
+                  <span class="step-index">#{{ step.stepIndex }}</span>
+                  <el-tag :type="getExecStatusType(step.execStatus)" size="mini">
+                    {{ getExecStatusText(step.execStatus) }}
+                  </el-tag>
+                  <span class="step-duration">{{ formatDuration(step.duration) }}</span>
+                </div>
+                <div class="step-time">
+                  {{ parseTime(step.startTime) }} - {{ parseTime(step.endTime) || '执行中' }}
+                </div>
+                <div class="step-params" v-if="step.inputParam">
+                  <el-collapse>
+                    <el-collapse-item title="输入参数">
+                      <pre>{{ formatJson(step.inputParam) }}</pre>
+                    </el-collapse-item>
+                  </el-collapse>
+                </div>
+                <div class="step-result" v-if="step.outputResult">
+                  <el-collapse>
+                    <el-collapse-item title="输出结果">
+                      <pre>{{ formatJson(step.outputResult) }}</pre>
+                    </el-collapse-item>
+                  </el-collapse>
+                </div>
+                <div class="step-error" v-if="step.errorMessage">
+                  <span class="error-label">错误:</span>
+                  {{ step.errorMessage }}
+                </div>
+              </div>
+            </el-timeline-item>
+          </el-timeline>
+          <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>
+</template>
+
+<script>
+import { listEnergyStrategy, getExecLogList, getExecLog, getStepExecLog } from '@/api/mgr/energyStrategy';
+
+export default {
+  name: 'StrategyLog',
+
+  data() {
+    return {
+      loading: false,
+      logList: [],
+      total: 0,
+      strategyList: [],
+      dateRange: [],
+
+      queryParams: {
+        pageNum: 1,
+        pageSize: 20,
+        strategyCode: '',
+        execStatus: null,
+        triggerType: '',
+        startTime: '',
+        endTime: ''
+      },
+
+      stats: {
+        total: 0,
+        success: 0,
+        fail: 0,
+        timeout: 0
+      },
+
+      execStatusOptions: [
+        { value: null, label: '全部', type: '' },
+        { value: 0, label: '执行中', type: 'info' },
+        { value: 1, label: '成功', type: 'success' },
+        { value: 2, label: '失败', type: 'danger' },
+        { value: 3, label: '超时', type: 'warning' }
+      ],
+
+      detailDrawerVisible: false,
+      currentLog: null,
+      stepLogs: []
+    };
+  },
+
+  created() {
+
+    this.loadStrategies();
+    this.getList();
+  },
+
+  methods: {
+    // 加载策略列表(用于筛选)
+    async loadStrategies() {
+      try {
+        const response = await listEnergyStrategy({ pageSize: 1000 });
+        this.strategyList = response.rows || [];
+      } catch (error) {
+        console.error('加载策略列表失败', error);
+      }
+    },
+
+    // 获取日志列表
+    async getList() {
+      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';
+        }
+
+        const response = await getExecLogList(params.strategyCode, params);
+        this.logList = response.rows || response.data || [];
+        this.total = response.total || 0;
+
+        // 计算统计数据
+        this.calculateStats();
+      } catch (error) {
+        console.error('获取日志列表失败', error);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    calculateStats() {
+      this.stats = {
+        total: this.total,
+        success: this.logList.filter(l => l.execStatus === 1).length,
+        fail: this.logList.filter(l => l.execStatus === 2).length,
+        timeout: this.logList.filter(l => l.execStatus === 3).length
+      };
+    },
+
+    refreshList() {
+      this.getList();
+      this.$message.success('刷新成功');
+    },
+
+    handleDateChange() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    handleStatusFilter(status) {
+      this.queryParams.execStatus = this.queryParams.execStatus === status ? null : status;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    // 查看日志详情
+    async viewLogDetail(log) {
+      this.currentLog = log;
+      this.detailDrawerVisible = true;
+
+      // 加载步骤执行日志
+      try {
+        const response = await getStepExecLog(log.execId);
+        this.stepLogs = (response.data || []).sort((a, b) => a.stepIndex - b.stepIndex);
+      } catch (error) {
+        console.error('加载步骤日志失败', error);
+        this.stepLogs = [];
+      }
+    },
+
+    // 辅助方法
+    getTriggerTypeName(type) {
+      const map = { 'EVENT': '事件', 'TIME': '定时', 'MANUAL': '手动', 'CONDITION': '条件', 'ATTR': '属性' };
+      return map[type] || type || '-';
+    },
+
+    getTriggerTagType(type) {
+      const map = { 'EVENT': 'danger', 'TIME': '', 'MANUAL': 'info', 'CONDITION': 'success', 'ATTR': 'warning' };
+      return map[type] || '';
+    },
+
+    getExecStatusType(status) {
+      const map = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' };
+      return map[status] || '';
+    },
+
+    getExecStatusText(status) {
+      const map = { 0: '执行中', 1: '成功', 2: '失败', 3: '超时' };
+      return map[status] || '-';
+    },
+
+    getExecStatusIcon(status) {
+      const map = { 0: 'el-icon-loading', 1: 'el-icon-success', 2: 'el-icon-error', 3: 'el-icon-time' };
+      return map[status] || '';
+    },
+
+    getStepTimelineType(status) {
+      const map = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' };
+      return map[status] || 'info';
+    },
+
+    getStepTimelineIcon(status) {
+      const map = { 0: 'el-icon-loading', 1: 'el-icon-check', 2: 'el-icon-close', 3: 'el-icon-time' };
+      return map[status] || 'el-icon-more';
+    },
+
+    getDurationClass(duration) {
+      if (!duration) return '';
+      if (duration > 10000) return 'duration-slow';
+      if (duration > 5000) return 'duration-normal';
+      return 'duration-fast';
+    },
+
+    formatDuration(ms) {
+      if (!ms && ms !== 0) return '-';
+      if (ms < 1000) return ms + 'ms';
+      if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
+      return (ms / 60000).toFixed(1) + 'min';
+    },
+
+    parseTime(time) {
+      if (!time) return '';
+      return new Date(time).toLocaleString('zh-CN', { hour12: false });
+    },
+
+    formatJson(str) {
+      if (!str) return '';
+      try {
+        const obj = typeof str === 'string' ? JSON.parse(str) : str;
+        return JSON.stringify(obj, null, 2);
+      } catch (e) {
+        return str;
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.strategy-log-container {
+  min-height: 100vh;
+  background: #f0f2f5;
+  padding: 20px;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 20px 24px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  .page-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+
+    i {
+      margin-right: 10px;
+      color: #409eff;
+    }
+  }
+}
+
+.main-content {
+  display: flex;
+  gap: 20px;
+}
+
+.filter-panel {
+  width: 260px;
+  flex-shrink: 0;
+  background: #fff;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  .filter-section {
+    margin-bottom: 24px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .filter-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #303133;
+      margin-bottom: 12px;
+    }
+  }
+
+  .status-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .status-tag {
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &:hover {
+        transform: scale(1.05);
+      }
+    }
+  }
+
+  .stats-summary {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+
+    .stat-item {
+      text-align: center;
+      padding: 12px;
+      background: #f5f7fa;
+      border-radius: 8px;
+
+      .stat-value {
+        display: block;
+        font-size: 24px;
+        font-weight: 600;
+        color: #303133;
+      }
+
+      .stat-label {
+        font-size: 12px;
+        color: #909399;
+      }
+
+      &.success .stat-value { color: #67c23a; }
+      &.fail .stat-value { color: #f56c6c; }
+      &.timeout .stat-value { color: #e6a23c; }
+    }
+  }
+}
+
+.log-panel {
+  flex: 1;
+  background: #fff;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  .strategy-info {
+    .strategy-name {
+      display: block;
+      font-weight: 500;
+    }
+
+    .strategy-code {
+      font-size: 12px;
+      color: #909399;
+    }
+  }
+
+  .duration-fast { color: #67c23a; }
+  .duration-normal { color: #e6a23c; }
+  .duration-slow { color: #f56c6c; }
+}
+
+.pagination-wrapper {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 20px;
+}
+
+// 日志详情
+.log-detail {
+  padding: 0 20px 20px;
+
+  .detail-section {
+    margin-bottom: 24px;
+
+    .section-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+      margin-bottom: 16px;
+      padding-bottom: 8px;
+      border-bottom: 1px solid #ebeef5;
+
+      &.error-title {
+        color: #f56c6c;
+      }
+    }
+  }
+
+  .error-content {
+    padding: 12px 16px;
+    background: #fef0f0;
+    border-radius: 6px;
+    color: #f56c6c;
+    font-family: monospace;
+    white-space: pre-wrap;
+    word-break: break-all;
+  }
+
+  .step-log-item {
+    .step-header {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin-bottom: 8px;
+
+      .step-name {
+        font-weight: 600;
+        color: #303133;
+      }
+
+      .step-index {
+        font-size: 12px;
+        color: #909399;
+      }
+
+      .step-duration {
+        margin-left: auto;
+        font-size: 12px;
+        color: #909399;
+      }
+    }
+
+    .step-time {
+      font-size: 12px;
+      color: #909399;
+      margin-bottom: 8px;
+    }
+
+    .step-error {
+      padding: 8px 12px;
+      background: #fef0f0;
+      border-radius: 4px;
+      font-size: 12px;
+      color: #f56c6c;
+      margin-top: 8px;
+
+      .error-label {
+        font-weight: 600;
+      }
+    }
+
+    pre {
+      background: #f5f7fa;
+      padding: 12px;
+      border-radius: 6px;
+      font-size: 12px;
+      overflow-x: auto;
+    }
+  }
+}
+</style>

+ 651 - 0
ems-ui-cloud/src/views/mgr/strategy/template.vue

@@ -0,0 +1,651 @@
+<template>
+  <div class="template-container">
+    <!-- 头部 -->
+    <div class="page-header">
+      <div class="header-left">
+        <h1 class="page-title">
+          <i class="el-icon-document-copy"></i>
+          策略模板库
+        </h1>
+        <span class="page-subtitle">快速创建 · 最佳实践 · 经验复用</span>
+      </div>
+      <div class="header-right">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索模板..."
+          prefix-icon="el-icon-search"
+          clearable
+          style="width: 240px"
+          @input="handleSearch"
+        />
+        <el-button type="primary" icon="el-icon-plus" @click="handleCreate">
+          创建模板
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 场景分类标签 -->
+    <div class="scene-tabs">
+      <el-radio-group v-model="currentScene" @change="handleSceneChange">
+        <el-radio-button label="ALL">
+          <i class="el-icon-menu"></i> 全部
+        </el-radio-button>
+        <el-radio-button v-for="scene in sceneTypes" :key="scene.value" :label="scene.value">
+          <i :class="scene.icon"></i> {{ scene.label }}
+        </el-radio-button>
+      </el-radio-group>
+    </div>
+
+    <!-- 模板卡片列表 -->
+    <div class="template-grid" v-loading="loading">
+      <div
+        v-for="template in templateList"
+        :key="template.templateCode"
+        class="template-card"
+        @click="handlePreview(template)"
+      >
+        <!-- 封面 -->
+        <div class="card-cover" :style="{ background: getSceneGradient(template.sceneType) }">
+          <div class="cover-icon">
+            <i :class="template.icon || getSceneIcon(template.sceneType)"></i>
+          </div>
+          <div class="card-badges">
+            <el-tag v-if="template.isSystem === 1" size="mini" effect="dark" type="warning">
+              官方
+            </el-tag>
+            <el-tag size="mini" effect="plain">
+              {{ getSceneLabel(template.sceneType) }}
+            </el-tag>
+          </div>
+        </div>
+
+        <!-- 内容 -->
+        <div class="card-content">
+          <h3 class="card-title">{{ template.templateName }}</h3>
+          <p class="card-desc">{{ template.description || '暂无描述' }}</p>
+
+          <!-- 标签 -->
+          <div class="card-tags" v-if="template.tags">
+            <el-tag
+              v-for="tag in template.tags.split(',')"
+              :key="tag"
+              size="mini"
+              type="info"
+              effect="plain"
+            >
+              {{ tag }}
+            </el-tag>
+          </div>
+
+          <!-- 适用设备 -->
+          <div class="card-devices" v-if="template.applicableDevices">
+            <span class="devices-label">适用设备:</span>
+            <div class="devices-icons">
+              <el-tooltip
+                v-for="deviceType in template.applicableDevices.split(',')"
+                :key="deviceType"
+                :content="getDeviceTypeName(deviceType)"
+              >
+                <i :class="getDeviceIcon(deviceType)"></i>
+              </el-tooltip>
+            </div>
+          </div>
+        </div>
+
+        <!-- 底部 -->
+        <div class="card-footer">
+          <div class="card-stats">
+            <span class="stat-item">
+              <i class="el-icon-document-checked"></i>
+              {{ template.useCount || 0 }}次使用
+            </span>
+          </div>
+          <div class="card-actions">
+            <el-button type="primary" size="mini" @click.stop="handleUseTemplate(template)">
+              使用模板
+            </el-button>
+            <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, template)">
+              <el-button size="mini" icon="el-icon-more" @click.stop></el-button>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item command="preview">
+                  <i class="el-icon-view"></i> 预览
+                </el-dropdown-item>
+                <el-dropdown-item command="edit" v-if="template.isSystem !== 1">
+                  <i class="el-icon-edit"></i> 编辑
+                </el-dropdown-item>
+                <el-dropdown-item command="copy">
+                  <i class="el-icon-document-copy"></i> 复制
+                </el-dropdown-item>
+                <el-dropdown-item command="export">
+                  <i class="el-icon-download"></i> 导出
+                </el-dropdown-item>
+                <el-dropdown-item command="delete" divided v-if="template.isSystem !== 1">
+                  <span style="color: #f56c6c"><i class="el-icon-delete"></i> 删除</span>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </div>
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-if="!loading && templateList.length === 0" class="empty-state">
+        <i class="el-icon-folder-opened"></i>
+        <p>暂无模板</p>
+      </div>
+    </div>
+
+    <!-- 模板预览弹窗 -->
+    <el-dialog
+      :title="previewTemplate.templateName"
+      :visible.sync="previewVisible"
+      width="800px"
+      class="preview-dialog"
+    >
+      <div class="preview-content">
+        <el-descriptions :column="2" border size="small">
+          <el-descriptions-item label="模板代码">
+            {{ previewTemplate.templateCode }}
+          </el-descriptions-item>
+          <el-descriptions-item label="场景类型">
+            <el-tag :type="getSceneTagType(previewTemplate.sceneType)" size="small">
+              {{ getSceneLabel(previewTemplate.sceneType) }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="模板描述" :span="2">
+            {{ previewTemplate.description }}
+          </el-descriptions-item>
+          <el-descriptions-item label="适用设备">
+            {{ previewTemplate.applicableDevices }}
+          </el-descriptions-item>
+          <el-descriptions-item label="使用次数">
+            {{ previewTemplate.useCount || 0 }}
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <el-divider content-position="left">触发器配置</el-divider>
+        <el-table :data="templateData.triggers || []" size="small" border>
+          <el-table-column label="触发器名称" prop="triggerName" />
+          <el-table-column label="触发类型" prop="triggerType" width="100" />
+          <el-table-column label="条件表达式" prop="conditionExpr" show-overflow-tooltip />
+        </el-table>
+
+        <el-divider content-position="left">执行步骤</el-divider>
+        <el-table :data="templateData.steps || []" size="small" border>
+          <el-table-column label="序号" prop="stepIndex" width="60" />
+          <el-table-column label="步骤名称" prop="stepName" />
+          <el-table-column label="步骤类型" prop="stepType" width="100" />
+          <el-table-column label="能力/动作" prop="abilityKey" />
+        </el-table>
+
+        <el-divider content-position="left">上下文变量</el-divider>
+        <el-table :data="templateData.context || []" size="small" border>
+          <el-table-column label="变量名" prop="varName" />
+          <el-table-column label="变量键" prop="varKey" />
+          <el-table-column label="类型" prop="varType" width="80" />
+          <el-table-column label="数据类型" prop="dataType" width="100" />
+          <el-table-column label="默认值" prop="defaultValue" />
+        </el-table>
+      </div>
+      <div slot="footer">
+        <el-button @click="previewVisible = false">关闭</el-button>
+        <el-button type="primary" @click="handleUseTemplate(previewTemplate)">
+          使用此模板
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 从模板创建策略弹窗 -->
+    <el-dialog title="从模板创建策略" :visible.sync="createDialogVisible" width="550px">
+      <el-form :model="createForm" :rules="createRules" ref="createForm" label-width="100px">
+        <el-form-item label="模板">
+          <el-tag type="info">{{ selectedTemplate.templateName }}</el-tag>
+        </el-form-item>
+        <el-form-item label="策略代码" prop="strategyCode">
+          <el-input v-model="createForm.strategyCode" placeholder="请输入策略代码" />
+        </el-form-item>
+        <el-form-item label="策略名称" prop="strategyName">
+          <el-input v-model="createForm.strategyName" placeholder="请输入策略名称" />
+        </el-form-item>
+        <el-form-item label="所属区块" prop="areaCode">
+          <treeselect
+            v-model="createForm.areaCode"
+            :options="areaOptions"
+            placeholder="请选择区块"
+            :normalizer="normalizeAreaNode"
+          />
+        </el-form-item>
+        <el-form-item label="策略描述">
+          <el-input
+            type="textarea"
+            v-model="createForm.strategyDesc"
+            :rows="3"
+            placeholder="可选"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="createDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmCreate">创建策略</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listStrategyTemplate,
+  getStrategyTemplate,
+  createFromTemplate,
+  deleteStrategyTemplate
+} from '@/api/mgr/energyStrategy';
+import { areaTreeSelect } from '@/api/basecfg/area';
+import Treeselect from '@riophae/vue-treeselect';
+import '@riophae/vue-treeselect/dist/vue-treeselect.css';
+
+export default {
+  name: 'StrategyTemplate',
+  components: { Treeselect },
+
+  data() {
+    return {
+      loading: false,
+      templateList: [],
+      searchKeyword: '',
+      currentScene: 'ALL',
+
+      sceneTypes: [
+        { value: 'PV_ESS', label: '光储协同', icon: 'el-icon-sunny' },
+        { value: 'DEMAND_RESP', label: '需求响应', icon: 'el-icon-s-marketing' },
+        { value: 'PEAK_VALLEY', label: '削峰填谷', icon: 'el-icon-data-analysis' },
+        { value: 'EMERGENCY', label: '应急保供', icon: 'el-icon-warning-outline' },
+        { value: 'ENERGY_SAVE', label: '节能优化', icon: 'el-icon-odometer' }
+      ],
+
+      // 预览
+      previewVisible: false,
+      previewTemplate: {},
+      templateData: {},
+
+      // 创建
+      createDialogVisible: false,
+      selectedTemplate: {},
+      createForm: {},
+      createRules: {
+        strategyCode: [{ required: true, message: '请输入策略代码', trigger: 'blur' }],
+        strategyName: [{ required: true, message: '请输入策略名称', trigger: 'blur' }],
+        areaCode: [{ required: true, message: '请选择所属区块', trigger: 'change' }]
+      },
+      areaOptions: []
+    };
+  },
+
+  created() {
+    this.loadTemplates();
+    this.loadAreaTree();
+  },
+
+  methods: {
+    async loadTemplates() {
+      this.loading = true;
+      try {
+        const params = {
+          templateName: this.searchKeyword,
+          sceneType: this.currentScene !== 'ALL' ? this.currentScene : null,
+          status: 1
+        };
+        const response = await listStrategyTemplate(params);
+        this.templateList = response.rows || response.data || [];
+      } catch (error) {
+        console.error('加载模板失败', error);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    loadAreaTree() {
+      areaTreeSelect('0', 1).then(response => {
+        this.areaOptions = response.data || [];
+      });
+    },
+
+    handleSearch() {
+      this.loadTemplates();
+    },
+
+    handleSceneChange() {
+      this.loadTemplates();
+    },
+
+    handleCreate() {
+      this.$message.info('请先从已有策略保存为模板');
+    },
+
+    async handlePreview(template) {
+      this.previewTemplate = template;
+
+      // 解析模板数据
+      try {
+        this.templateData = typeof template.templateData === 'string'
+          ? JSON.parse(template.templateData)
+          : template.templateData;
+      } catch (e) {
+        this.templateData = { triggers: [], steps: [], context: [] };
+      }
+
+      this.previewVisible = true;
+    },
+
+    handleUseTemplate(template) {
+      this.selectedTemplate = template;
+      this.createForm = {
+        strategyCode: '',
+        strategyName: template.templateName + '_' + Date.now().toString().slice(-6),
+        areaCode: null,
+        strategyDesc: '基于模板【' + template.templateName + '】创建'
+      };
+      this.createDialogVisible = true;
+      this.previewVisible = false;
+    },
+
+    confirmCreate() {
+      this.$refs.createForm.validate(async valid => {
+        if (!valid) return;
+
+        try {
+          await createFromTemplate(this.selectedTemplate.templateCode, this.createForm);
+          this.$message.success('策略创建成功');
+          this.createDialogVisible = false;
+
+          // 跳转到编排页面
+          this.$router.push({
+            path: '/strategy-mgr/strategy-editor',
+            query: { strategyCode: this.createForm.strategyCode }
+          });
+        } catch (error) {
+          this.$message.error('创建失败: ' + (error.message || '未知错误'));
+        }
+      });
+    },
+
+    handleCommand(command, template) {
+      switch (command) {
+        case 'preview':
+          this.handlePreview(template);
+          break;
+        case 'edit':
+          // TODO: 编辑模板
+          break;
+        case 'copy':
+          // TODO: 复制模板
+          break;
+        case 'export':
+          this.exportTemplate(template);
+          break;
+        case 'delete':
+          this.deleteTemplate(template);
+          break;
+      }
+    },
+
+    exportTemplate(template) {
+      const data = JSON.stringify(template, null, 2);
+      const blob = new Blob([data], { type: 'application/json' });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `template_${template.templateCode}.json`;
+      a.click();
+      URL.revokeObjectURL(url);
+    },
+
+    deleteTemplate(template) {
+      this.$confirm('确认删除模板【' + template.templateName + '】吗?', '警告', {
+        type: 'warning'
+      }).then(async () => {
+        await deleteStrategyTemplate(template.templateCode);
+        this.$message.success('删除成功');
+        this.loadTemplates();
+      }).catch(() => {});
+    },
+
+    normalizeAreaNode(node) {
+      return { id: node.id, label: node.label, children: node.children };
+    },
+
+    // 辅助方法
+    getSceneLabel(scene) {
+      const found = this.sceneTypes.find(s => s.value === scene);
+      return found ? found.label : scene || '其他';
+    },
+
+    getSceneIcon(scene) {
+      const found = this.sceneTypes.find(s => s.value === scene);
+      return found ? found.icon : 'el-icon-document';
+    },
+
+    getSceneGradient(scene) {
+      const gradients = {
+        'PV_ESS': 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
+        'DEMAND_RESP': 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
+        'PEAK_VALLEY': 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
+        'EMERGENCY': 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
+        'ENERGY_SAVE': 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)'
+      };
+      return gradients[scene] || 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
+    },
+
+    getSceneTagType(scene) {
+      const map = { 'PV_ESS': 'warning', 'DEMAND_RESP': '', 'PEAK_VALLEY': 'success', 'EMERGENCY': 'danger', 'ENERGY_SAVE': 'info' };
+      return map[scene] || '';
+    },
+
+    getDeviceIcon(type) {
+      const map = { 'PV': 'el-icon-sunny', 'ESS': 'el-icon-coin', 'HVAC': 'el-icon-cloudy', 'LIGHT': 'el-icon-light-rain', 'GRID': 'el-icon-connection', 'SENSOR': 'el-icon-view' };
+      return map[type] || 'el-icon-cpu';
+    },
+
+    getDeviceTypeName(type) {
+      const map = { 'PV': '光伏', 'ESS': '储能', 'HVAC': '空调', 'LIGHT': '照明', 'GRID': '电网', 'SENSOR': '传感器' };
+      return map[type] || type;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.template-container {
+  padding: 20px;
+  background: #f0f2f5;
+  min-height: 100vh;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 20px 24px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  .header-left {
+    .page-title {
+      margin: 0;
+      font-size: 22px;
+      font-weight: 600;
+      color: #1a1a2e;
+
+      i { margin-right: 10px; color: #409eff; }
+    }
+
+    .page-subtitle {
+      display: block;
+      margin-top: 4px;
+      font-size: 13px;
+      color: #909399;
+    }
+  }
+
+  .header-right {
+    display: flex;
+    gap: 12px;
+  }
+}
+
+.scene-tabs {
+  margin-bottom: 20px;
+  padding: 16px 20px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+}
+
+.template-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  gap: 20px;
+}
+
+.template-card {
+  background: #fff;
+  border-radius: 12px;
+  overflow: hidden;
+  transition: all 0.3s;
+  cursor: pointer;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+
+  &:hover {
+    transform: translateY(-4px);
+    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
+  }
+
+  .card-cover {
+    height: 120px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+
+    .cover-icon {
+      width: 56px;
+      height: 56px;
+      background: rgba(255, 255, 255, 0.2);
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      i {
+        font-size: 28px;
+        color: #fff;
+      }
+    }
+
+    .card-badges {
+      position: absolute;
+      top: 12px;
+      right: 12px;
+      display: flex;
+      gap: 6px;
+    }
+  }
+
+  .card-content {
+    padding: 16px;
+
+    .card-title {
+      margin: 0 0 8px;
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+    }
+
+    .card-desc {
+      margin: 0 0 12px;
+      font-size: 13px;
+      color: #909399;
+      line-height: 1.5;
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      -webkit-box-orient: vertical;
+      overflow: hidden;
+    }
+
+    .card-tags {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 6px;
+      margin-bottom: 12px;
+    }
+
+    .card-devices {
+      display: flex;
+      align-items: center;
+
+      .devices-label {
+        font-size: 12px;
+        color: #909399;
+        margin-right: 8px;
+      }
+
+      .devices-icons {
+        display: flex;
+        gap: 8px;
+
+        i {
+          font-size: 18px;
+          color: #606266;
+        }
+      }
+    }
+  }
+
+  .card-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 12px 16px;
+    border-top: 1px solid #f0f2f5;
+
+    .card-stats {
+      .stat-item {
+        font-size: 12px;
+        color: #909399;
+
+        i { margin-right: 4px; }
+      }
+    }
+
+    .card-actions {
+      display: flex;
+      gap: 8px;
+    }
+  }
+}
+
+.empty-state {
+  grid-column: 1 / -1;
+  text-align: center;
+  padding: 80px 20px;
+
+  i {
+    font-size: 64px;
+    color: #dcdfe6;
+  }
+
+  p {
+    margin-top: 16px;
+    font-size: 14px;
+    color: #909399;
+  }
+}
+
+.preview-dialog {
+  .preview-content {
+    max-height: 60vh;
+    overflow-y: auto;
+  }
+}
+</style>