TriggerConfig.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. <template>
  2. <div class="trigger-config">
  3. <el-form :model="form" label-width="90px" size="small">
  4. <el-form-item label="触发器名称">
  5. <el-input v-model="form.triggerName" placeholder="请输入触发器名称" @input="emitChange" />
  6. </el-form-item>
  7. <el-form-item label="触发类型">
  8. <el-select v-model="form.triggerType" style="width: 100%" @change="handleTypeChange">
  9. <el-option label="事件触发" value="EVENT">
  10. <i class="el-icon-lightning" style="color: #f56c6c; margin-right: 8px"></i> 事件触发
  11. </el-option>
  12. <el-option label="状态切换" value="STATE_CHANGE">
  13. <i class="el-icon-switch-button" style="color: #67c23a; margin-right: 8px"></i> 状态切换
  14. </el-option>
  15. <el-option label="定时触发" value="TIME">
  16. <i class="el-icon-time" style="color: #409eff; margin-right: 8px"></i> 定时触发
  17. </el-option>
  18. </el-select>
  19. </el-form-item>
  20. <!-- ==================== 事件触发配置 ==================== -->
  21. <template v-if="form.triggerType === 'EVENT'">
  22. <el-divider content-position="left">
  23. <i class="el-icon-lightning"></i> 事件源配置
  24. </el-divider>
  25. <el-form-item label="选择设备">
  26. <el-select
  27. v-model="form.sourceObjCode"
  28. filterable
  29. placeholder="搜索选择设备"
  30. style="width: 100%"
  31. @change="handleDeviceSelect"
  32. >
  33. <el-option
  34. v-for="device in deviceList"
  35. :key="device.deviceCode"
  36. :label="device.deviceName"
  37. :value="device.deviceCode"
  38. >
  39. <span>{{ device.deviceName }}</span>
  40. <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ device.deviceCode }}</span>
  41. </el-option>
  42. </el-select>
  43. </el-form-item>
  44. <el-form-item v-if="loadingModel">
  45. <div style="color: #909399; font-size: 12px;">
  46. <i class="el-icon-loading"></i> 正在加载物模型...
  47. </div>
  48. </el-form-item>
  49. <el-form-item label="监听事件" v-if="form.sourceObjCode && !loadingModel">
  50. <el-select
  51. v-model="form.eventKey"
  52. filterable
  53. placeholder="请选择事件"
  54. style="width: 100%"
  55. @change="emitChange"
  56. >
  57. <el-option
  58. v-for="event in eventList"
  59. :key="event.eventKey"
  60. :label="event.eventName"
  61. :value="event.eventKey"
  62. >
  63. <div style="display: flex; justify-content: space-between; align-items: center;">
  64. <span>
  65. <i class="el-icon-bell" style="color: #f56c6c; margin-right: 4px"></i>
  66. {{ event.eventName }}
  67. </span>
  68. <el-tag size="mini" type="info">{{ event.eventKey }}</el-tag>
  69. </div>
  70. </el-option>
  71. </el-select>
  72. <div class="form-tip" v-if="eventList.length === 0 && form.sourceObjCode">
  73. ⚠️ 该设备暂无事件定义
  74. </div>
  75. </el-form-item>
  76. <el-form-item v-if="form.sourceObjCode && form.eventKey">
  77. <div class="condition-preview success">
  78. <i class="el-icon-info"></i>
  79. 当设备 <strong>{{ getDeviceName(form.sourceObjCode) }}</strong>
  80. 触发 <strong>{{ getEventName(form.eventKey) }}</strong> 事件时执行策略
  81. </div>
  82. </el-form-item>
  83. </template>
  84. <!-- ==================== 状态切换触发配置 ==================== -->
  85. <template v-if="form.triggerType === 'STATE_CHANGE'">
  86. <el-divider content-position="left">
  87. <i class="el-icon-switch-button"></i> 监控模式
  88. </el-divider>
  89. <el-form-item label="监控模式">
  90. <el-radio-group v-model="stateChangeMode" @change="handleModeChange">
  91. <el-radio-button label="PASSIVE">
  92. <i class="el-icon-upload2"></i> 主动上报
  93. </el-radio-button>
  94. <el-radio-button label="POLLING">
  95. <i class="el-icon-refresh"></i> 轮询监控
  96. </el-radio-button>
  97. </el-radio-group>
  98. </el-form-item>
  99. <el-alert
  100. v-if="stateChangeMode === 'PASSIVE'"
  101. type="info"
  102. :closable="false"
  103. show-icon
  104. style="margin-bottom: 16px;"
  105. >
  106. <template slot="title">
  107. <strong>主动上报模式</strong>:适用于设备会主动推送状态变化的场景(如4G/WiFi直连设备)
  108. </template>
  109. </el-alert>
  110. <el-alert
  111. v-if="stateChangeMode === 'POLLING'"
  112. type="warning"
  113. :closable="false"
  114. show-icon
  115. style="margin-bottom: 16px;"
  116. >
  117. <template slot="title">
  118. <strong>轮询监控模式</strong>:适用于需要主动查询的设备(如485设备)
  119. </template>
  120. </el-alert>
  121. <el-divider content-position="left">
  122. <i class="el-icon-aim"></i> 设备与属性
  123. </el-divider>
  124. <el-form-item label="监控设备">
  125. <el-select
  126. v-model="form.sourceObjCode"
  127. filterable
  128. placeholder="请选择监控设备"
  129. style="width: 100%"
  130. @change="handleDeviceSelect"
  131. >
  132. <el-option
  133. v-for="device in deviceList"
  134. :key="device.deviceCode"
  135. :label="device.deviceName"
  136. :value="device.deviceCode"
  137. >
  138. <span>{{ device.deviceName }}</span>
  139. <span style="color: #909399; font-size: 12px; margin-left: 8px">{{ device.deviceCode }}</span>
  140. </el-option>
  141. </el-select>
  142. </el-form-item>
  143. <el-form-item v-if="loadingModel">
  144. <div style="color: #909399; font-size: 12px;">
  145. <i class="el-icon-loading"></i> 正在加载物模型...
  146. </div>
  147. </el-form-item>
  148. <el-form-item label="监控属性" v-if="form.sourceObjCode && !loadingModel">
  149. <el-select
  150. v-model="form.attrKey"
  151. filterable
  152. placeholder="请选择监控属性"
  153. style="width: 100%"
  154. @change="handleAttrSelect"
  155. >
  156. <el-option-group
  157. v-for="group in attrGroups"
  158. :key="group.name"
  159. :label="group.label"
  160. >
  161. <el-option
  162. v-for="attr in group.attrs"
  163. :key="attr.attrKey"
  164. :label="attr.attrName"
  165. :value="attr.attrKey"
  166. >
  167. <div style="display: flex; justify-content: space-between; align-items: center;">
  168. <span>
  169. <i class="el-icon-document" style="color: #409eff; margin-right: 4px"></i>
  170. {{ attr.attrName }}
  171. </span>
  172. <div>
  173. <el-tag size="mini" type="info" style="margin-right: 4px">{{ attr.attrKey }}</el-tag>
  174. <el-tag size="mini" v-if="attr.attrUnit">{{ attr.attrUnit }}</el-tag>
  175. </div>
  176. </div>
  177. </el-option>
  178. </el-option-group>
  179. </el-select>
  180. <div class="form-tip" v-if="attrList.length === 0 && form.sourceObjCode">
  181. ⚠️ 该设备暂无属性定义
  182. </div>
  183. </el-form-item>
  184. <el-form-item v-if="form.attrKey && selectedAttr">
  185. <div class="attr-type-info">
  186. <el-tag :type="getAttrTypeTag(selectedAttr.attrValueType)" size="small">
  187. {{ getAttrTypeName(selectedAttr.attrValueType) }}
  188. </el-tag>
  189. <span v-if="selectedAttr.attrUnit" style="margin-left: 8px; color: #606266;">
  190. 单位: {{ selectedAttr.attrUnit }}
  191. </span>
  192. </div>
  193. </el-form-item>
  194. <!-- 轮询监控专属配置 -->
  195. <template v-if="stateChangeMode === 'POLLING'">
  196. <el-divider content-position="left">
  197. <i class="el-icon-setting"></i> 轮询参数
  198. </el-divider>
  199. <el-form-item label="轮询间隔">
  200. <el-input-number
  201. v-model="pollingConfig.interval"
  202. :min="1000"
  203. :max="60000"
  204. :step="500"
  205. style="width: 150px"
  206. @change="updateCondition"
  207. />
  208. <span class="unit-label">毫秒(建议≥2000ms)</span>
  209. </el-form-item>
  210. <el-form-item label="初始延迟">
  211. <el-input-number
  212. v-model="pollingConfig.initialDelay"
  213. :min="0"
  214. :max="10000"
  215. :step="500"
  216. style="width: 150px"
  217. @change="updateCondition"
  218. />
  219. <span class="unit-label">毫秒</span>
  220. </el-form-item>
  221. <el-divider content-position="left">
  222. <i class="el-icon-s-promotion"></i> 主动查询配置
  223. </el-divider>
  224. <el-form-item label="主动查询">
  225. <el-switch
  226. v-model="pollingConfig.activeQuery"
  227. active-text="开启"
  228. inactive-text="关闭"
  229. @change="updateCondition"
  230. />
  231. <el-tooltip content="485设备需要主动下发查询指令才能获取最新状态" placement="right">
  232. <i class="el-icon-question" style="margin-left: 8px; color: #909399;"></i>
  233. </el-tooltip>
  234. </el-form-item>
  235. <template v-if="pollingConfig.activeQuery">
  236. <el-form-item label="查询能力">
  237. <el-select
  238. v-model="pollingConfig.queryAbilityKey"
  239. placeholder="请选择用于查询状态的能力"
  240. style="width: 100%"
  241. @change="updateCondition"
  242. >
  243. <el-option
  244. v-for="ability in abilityList"
  245. :key="ability.abilityKey"
  246. :label="ability.abilityName"
  247. :value="ability.abilityKey"
  248. >
  249. <span>{{ ability.abilityName }}</span>
  250. <span style="float: right; color: #909399; font-size: 12px">{{ ability.abilityKey }}</span>
  251. </el-option>
  252. </el-select>
  253. <div class="form-tip">
  254. 选择物模型中定义的能力,用于主动查询设备状态(如 syncState)
  255. </div>
  256. </el-form-item>
  257. <el-form-item label="等待时间">
  258. <el-input-number
  259. v-model="pollingConfig.queryWaitTime"
  260. :min="100"
  261. :max="5000"
  262. :step="100"
  263. style="width: 150px"
  264. @change="updateCondition"
  265. />
  266. <span class="unit-label">毫秒(设备响应等待时间)</span>
  267. </el-form-item>
  268. </template>
  269. </template>
  270. <!-- 触发条件配置(两种模式共用) -->
  271. <el-divider content-position="left">
  272. <i class="el-icon-s-check"></i> 触发条件
  273. </el-divider>
  274. <el-form-item label="触发条件">
  275. <el-row :gutter="8">
  276. <el-col :span="8">
  277. <el-select v-model="conditionOperator" style="width: 100%" @change="updateCondition">
  278. <el-option label="等于 (==)" value="==" />
  279. <el-option label="不等于 (!=)" value="!=" />
  280. <el-option label="大于 (>)" value=">" />
  281. <el-option label="大于等于 (>=)" value=">=" />
  282. <el-option label="小于 (<)" value="<" />
  283. <el-option label="小于等于 (<=)" value="<=" />
  284. </el-select>
  285. </el-col>
  286. <el-col :span="16">
  287. <el-select
  288. v-if="selectedAttr && selectedAttr.attrValueType === 'Enum' && attrEnums.length > 0"
  289. v-model="conditionValue"
  290. placeholder="请选择值"
  291. style="width: 100%"
  292. @change="updateCondition"
  293. >
  294. <el-option
  295. v-for="enumItem in attrEnums"
  296. :key="enumItem.attrValue"
  297. :label="enumItem.attrValueName"
  298. :value="enumItem.attrValue"
  299. >
  300. <span>{{ enumItem.attrValueName }}</span>
  301. <span style="float: right; color: #909399; font-size: 12px">{{ enumItem.attrValue }}</span>
  302. </el-option>
  303. </el-select>
  304. <el-input
  305. v-else
  306. v-model="conditionValue"
  307. placeholder="请输入比较值"
  308. @input="updateCondition"
  309. >
  310. <template slot="prepend">{{ form.attrKey || '属性' }}</template>
  311. </el-input>
  312. </el-col>
  313. </el-row>
  314. </el-form-item>
  315. <!-- 轮询模式下的触发方式 -->
  316. <el-form-item label="触发方式" v-if="stateChangeMode === 'POLLING'">
  317. <el-radio-group v-model="pollingConfig.triggerMode" @change="updateCondition">
  318. <el-radio label="ON_FIRST_MATCH">
  319. <span>首次满足</span>
  320. <el-tooltip content="条件首次满足时触发(边沿触发),避免重复执行" placement="top">
  321. <i class="el-icon-question" style="margin-left: 4px; color: #909399;"></i>
  322. </el-tooltip>
  323. </el-radio>
  324. <el-radio label="ON_CHANGE">
  325. <span>值变化</span>
  326. <el-tooltip content="值变化且条件满足时触发" placement="top">
  327. <i class="el-icon-question" style="margin-left: 4px; color: #909399;"></i>
  328. </el-tooltip>
  329. </el-radio>
  330. <el-radio label="ALWAYS">
  331. <span>总是触发</span>
  332. <el-tooltip content="只要条件满足就触发(谨慎使用)" placement="top">
  333. <i class="el-icon-warning" style="margin-left: 4px; color: #E6A23C;"></i>
  334. </el-tooltip>
  335. </el-radio>
  336. </el-radio-group>
  337. </el-form-item>
  338. <!-- 条件预览 -->
  339. <el-form-item label="配置预览">
  340. <div class="condition-preview" :class="conditionPreviewClass">
  341. <i class="el-icon-info"></i>
  342. {{ conditionPreviewText }}
  343. </div>
  344. </el-form-item>
  345. </template>
  346. <!-- ==================== 定时触发配置 ==================== -->
  347. <template v-if="form.triggerType === 'TIME'">
  348. <el-divider content-position="left">
  349. <i class="el-icon-time"></i> 定时配置
  350. </el-divider>
  351. <el-form-item label="CRON表达式">
  352. <el-input v-model="cronExpression" placeholder="0 0 8 * * ?" @input="updateCronCondition">
  353. <el-button slot="append" @click="showCronHelper">
  354. <i class="el-icon-question"></i>
  355. </el-button>
  356. </el-input>
  357. </el-form-item>
  358. <el-form-item label="快捷选择">
  359. <div class="cron-shortcuts">
  360. <el-tag
  361. v-for="shortcut in cronShortcuts"
  362. :key="shortcut.value"
  363. @click="setCron(shortcut.value)"
  364. class="cron-tag"
  365. :effect="cronExpression === shortcut.value ? 'dark' : 'plain'"
  366. >
  367. {{ shortcut.label }}
  368. </el-tag>
  369. </div>
  370. </el-form-item>
  371. <el-form-item label="表达式说明" v-if="cronExpression">
  372. <div class="cron-desc">{{ cronDescription }}</div>
  373. </el-form-item>
  374. </template>
  375. <!-- ==================== 高级配置 ==================== -->
  376. <el-divider content-position="left">
  377. <i class="el-icon-setting"></i> 高级配置
  378. </el-divider>
  379. <el-form-item label="优先级">
  380. <el-slider v-model="form.priority" :min="0" :max="100" show-input @change="emitChange" />
  381. </el-form-item>
  382. <el-form-item label="是否启用">
  383. <el-switch v-model="form.enable" :active-value="1" :inactive-value="0" @change="emitChange" />
  384. </el-form-item>
  385. </el-form>
  386. </div>
  387. </template>
  388. <script>
  389. import { getModelByCode } from '@/api/basecfg/objModel'
  390. export default {
  391. name: 'TriggerConfig',
  392. props: {
  393. trigger: { type: Object, default: () => ({}) },
  394. deviceList: { type: Array, default: () => [] },
  395. // 新增:策略代码
  396. strategyCode: { type: String, default: '' }
  397. },
  398. data() {
  399. return {
  400. form: {
  401. id: null, // 数据库ID,用于更新
  402. strategyCode: '', // 策略代码,必需字段
  403. triggerName: '',
  404. triggerType: 'STATE_CHANGE',
  405. sourceObjType: 2,
  406. sourceObjCode: '',
  407. sourceModelCode: '',
  408. eventKey: '',
  409. attrKey: '',
  410. conditionExpr: '',
  411. priority: 50,
  412. enable: 1
  413. },
  414. stateChangeMode: 'POLLING',
  415. eventList: [],
  416. attrList: [],
  417. abilityList: [],
  418. attrEnums: [],
  419. loadingModel: false,
  420. conditionOperator: '==',
  421. conditionValue: '',
  422. cronExpression: '',
  423. cronShortcuts: [
  424. { label: '每天8点', value: '0 0 8 * * ?' },
  425. { label: '每天0点', value: '0 0 0 * * ?' },
  426. { label: '每小时', value: '0 0 * * * ?' },
  427. { label: '每30分钟', value: '0 */30 * * * ?' },
  428. { label: '每5分钟', value: '0 */5 * * * ?' },
  429. { label: '工作日8点', value: '0 0 8 ? * MON-FRI' }
  430. ],
  431. pollingConfig: {
  432. interval: 2000,
  433. initialDelay: 1000,
  434. activeQuery: true,
  435. queryAbilityKey: 'syncState',
  436. queryAbilityParam: '',
  437. queryWaitTime: 500,
  438. triggerMode: 'ON_FIRST_MATCH'
  439. },
  440. lastLoadedModelCode: ''
  441. }
  442. },
  443. computed: {
  444. selectedAttr() {
  445. return this.attrList.find(a => a.attrKey === this.form.attrKey)
  446. },
  447. attrGroups() {
  448. const groups = {}
  449. this.attrList.forEach(attr => {
  450. const groupName = attr.attrGroup || 'Default'
  451. if (!groups[groupName]) groups[groupName] = []
  452. groups[groupName].push(attr)
  453. })
  454. return Object.entries(groups).map(([name, attrs]) => ({
  455. name,
  456. label: this.getGroupLabel(name),
  457. attrs
  458. }))
  459. },
  460. cronDescription() {
  461. const expr = this.cronExpression
  462. if (!expr) return ''
  463. if (expr === '0 0 8 * * ?') return '每天早上8点执行'
  464. if (expr === '0 0 0 * * ?') return '每天凌晨0点执行'
  465. if (expr === '0 0 * * * ?') return '每小时整点执行'
  466. if (expr === '0 */30 * * * ?') return '每30分钟执行一次'
  467. if (expr === '0 */5 * * * ?') return '每5分钟执行一次'
  468. if (expr === '0 0 8 ? * MON-FRI') return '工作日早上8点执行'
  469. const parts = expr.split(' ')
  470. if (parts.length >= 6) {
  471. return `秒:${parts[0]} 分:${parts[1]} 时:${parts[2]} 日:${parts[3]} 月:${parts[4]} 周:${parts[5]}`
  472. }
  473. return '表达式格式不正确'
  474. },
  475. conditionPreviewText() {
  476. const deviceName = this.getDeviceName(this.form.sourceObjCode) || '[未选择设备]'
  477. const attrName = this.selectedAttr ? this.selectedAttr.attrName : (this.form.attrKey || '[未选择属性]')
  478. const op = this.conditionOperator || '=='
  479. const value = this.getConditionValueDisplay()
  480. if (this.stateChangeMode === 'PASSIVE') {
  481. return `当 ${deviceName} 的 ${attrName} ${this.getOperatorText(op)} ${value} 时执行策略(等待设备主动上报)`
  482. } else {
  483. const interval = this.pollingConfig.interval / 1000
  484. const queryDesc = this.pollingConfig.activeQuery
  485. ? `,调用 ${this.pollingConfig.queryAbilityKey} 查询`
  486. : ''
  487. const modeDesc = this.getTriggerModeText(this.pollingConfig.triggerMode)
  488. return `每${interval}秒检查 ${deviceName} 的 ${attrName}${queryDesc},当 ${this.getOperatorText(op)} ${value} 时${modeDesc}`
  489. }
  490. },
  491. conditionPreviewClass() {
  492. if (!this.form.sourceObjCode || !this.form.attrKey) return 'warning'
  493. if (!this.conditionValue) return 'warning'
  494. return 'success'
  495. }
  496. },
  497. watch: {
  498. trigger: {
  499. immediate: true,
  500. handler(val) {
  501. if (val && Object.keys(val).length > 0) {
  502. this.initFormFromTrigger(val)
  503. }
  504. }
  505. },
  506. // 监听 strategyCode 变化
  507. strategyCode: {
  508. immediate: true,
  509. handler(val) {
  510. if (val && !this.form.strategyCode) {
  511. this.form.strategyCode = val
  512. }
  513. }
  514. }
  515. },
  516. methods: {
  517. initFormFromTrigger(trigger) {
  518. // 确保 strategyCode 存在
  519. const strategyCode = trigger.strategyCode || this.strategyCode
  520. this.form = {
  521. id: trigger.id || null, // 保留数据库ID
  522. strategyCode: strategyCode, // 确保 strategyCode 存在
  523. triggerName: trigger.triggerName || '',
  524. triggerType: this.mapTriggerType(trigger.triggerType),
  525. sourceObjType: trigger.sourceObjType || 2,
  526. sourceObjCode: trigger.sourceObjCode || '',
  527. sourceModelCode: trigger.sourceModelCode || '',
  528. eventKey: trigger.eventKey || '',
  529. attrKey: trigger.attrKey || '',
  530. conditionExpr: trigger.conditionExpr || '',
  531. priority: trigger.priority || 50,
  532. enable: trigger.enable !== undefined ? trigger.enable : 1
  533. }
  534. if (trigger.conditionExpr) {
  535. this.parseConditionExpr(trigger.conditionExpr)
  536. }
  537. if (trigger.sourceModelCode && trigger.sourceModelCode !== this.lastLoadedModelCode) {
  538. this.loadObjectModel(trigger.sourceModelCode)
  539. }
  540. },
  541. mapTriggerType(oldType) {
  542. if (oldType === 'ATTR' || oldType === 'POLLING') {
  543. return 'STATE_CHANGE'
  544. }
  545. return oldType || 'STATE_CHANGE'
  546. },
  547. parseConditionExpr(expr) {
  548. try {
  549. const condition = JSON.parse(expr)
  550. if (condition.cron) {
  551. this.cronExpression = condition.cron
  552. } else {
  553. this.conditionOperator = condition.op || '=='
  554. this.conditionValue = condition.right || ''
  555. }
  556. if (condition.polling && condition.polling.enabled) {
  557. this.stateChangeMode = 'POLLING'
  558. this.pollingConfig = {
  559. interval: condition.polling.interval || 2000,
  560. initialDelay: condition.polling.initialDelay || 1000,
  561. activeQuery: condition.polling.activeQuery !== false,
  562. queryAbilityKey: condition.polling.queryAbilityKey ||
  563. (condition.polling.queryAbility && condition.polling.queryAbility.abilityKey) ||
  564. 'syncState',
  565. queryAbilityParam: condition.polling.queryAbilityParam ||
  566. (condition.polling.queryAbility && condition.polling.queryAbility.abilityParam) ||
  567. '',
  568. queryWaitTime: condition.polling.queryWaitTime || 500,
  569. triggerMode: condition.polling.triggerMode || 'ON_FIRST_MATCH'
  570. }
  571. } else {
  572. this.stateChangeMode = 'PASSIVE'
  573. }
  574. } catch (e) {
  575. console.warn('解析条件表达式失败', e)
  576. }
  577. },
  578. handleTypeChange() {
  579. this.form.eventKey = ''
  580. this.form.attrKey = ''
  581. this.form.conditionExpr = ''
  582. this.conditionOperator = '=='
  583. this.conditionValue = ''
  584. this.cronExpression = ''
  585. this.eventList = []
  586. this.attrList = []
  587. this.abilityList = []
  588. this.attrEnums = []
  589. this.emitChange()
  590. },
  591. handleModeChange(mode) {
  592. this.stateChangeMode = mode
  593. this.updateCondition()
  594. },
  595. handleDeviceSelect(deviceCode) {
  596. const device = this.deviceList.find(d => d.deviceCode === deviceCode)
  597. if (!device) return
  598. const modelCode = device.deviceModel || device.modelCode
  599. this.form.sourceModelCode = modelCode || ''
  600. this.form.sourceObjType = 2
  601. this.form.eventKey = ''
  602. this.form.attrKey = ''
  603. this.eventList = []
  604. this.attrList = []
  605. this.abilityList = []
  606. this.attrEnums = []
  607. if (modelCode && modelCode !== this.lastLoadedModelCode) {
  608. this.loadObjectModel(modelCode)
  609. } else if (!modelCode) {
  610. this.$message.warning('该设备未配置物模型')
  611. }
  612. this.emitChange()
  613. },
  614. async loadObjectModel(modelCode) {
  615. if (!modelCode) return
  616. if (modelCode === this.lastLoadedModelCode) return
  617. if (this.loadingModel) return
  618. this.loadingModel = true
  619. this.lastLoadedModelCode = modelCode
  620. try {
  621. const response = await getModelByCode(modelCode)
  622. const modelData = response.data
  623. if (!modelData) {
  624. this.$message.warning('物模型数据为空')
  625. return
  626. }
  627. this.attrList = modelData.attrList || []
  628. this.eventList = modelData.eventList || []
  629. this.abilityList = modelData.abilityList || []
  630. if (this.abilityList.length > 0) {
  631. const syncAbility = this.abilityList.find(a => a.abilityKey === 'syncState')
  632. if (syncAbility && !this.pollingConfig.queryAbilityKey) {
  633. this.pollingConfig.queryAbilityKey = 'syncState'
  634. }
  635. }
  636. } catch (error) {
  637. console.error('加载物模型失败', error)
  638. this.$message.error('加载物模型失败')
  639. this.attrList = []
  640. this.eventList = []
  641. this.abilityList = []
  642. this.lastLoadedModelCode = ''
  643. } finally {
  644. this.loadingModel = false
  645. }
  646. },
  647. handleAttrSelect(attrKey) {
  648. const attr = this.attrList.find(a => a.attrKey === attrKey)
  649. this.conditionValue = ''
  650. this.conditionOperator = '=='
  651. if (attr && attr.attrValueType === 'Enum' && attr.valueEnums) {
  652. this.attrEnums = attr.valueEnums
  653. } else {
  654. this.attrEnums = []
  655. }
  656. this.updateCondition()
  657. },
  658. updateCondition() {
  659. if (this.form.triggerType === 'STATE_CHANGE') {
  660. const condition = {
  661. left: this.form.attrKey,
  662. op: this.conditionOperator,
  663. right: this.conditionValue
  664. }
  665. if (this.stateChangeMode === 'POLLING') {
  666. condition.polling = {
  667. enabled: true,
  668. interval: this.pollingConfig.interval,
  669. initialDelay: this.pollingConfig.initialDelay,
  670. activeQuery: this.pollingConfig.activeQuery,
  671. queryWaitTime: this.pollingConfig.queryWaitTime,
  672. triggerMode: this.pollingConfig.triggerMode
  673. }
  674. if (this.pollingConfig.activeQuery) {
  675. condition.polling.queryAbility = {
  676. abilityKey: this.pollingConfig.queryAbilityKey,
  677. abilityParam: this.pollingConfig.queryAbilityParam
  678. }
  679. }
  680. }
  681. this.form.conditionExpr = JSON.stringify(condition)
  682. }
  683. this.emitChange()
  684. },
  685. updateCronCondition() {
  686. this.form.conditionExpr = JSON.stringify({ cron: this.cronExpression })
  687. this.emitChange()
  688. },
  689. setCron(value) {
  690. this.cronExpression = value
  691. this.updateCronCondition()
  692. },
  693. showCronHelper() {
  694. this.$alert(`
  695. <div style="line-height: 1.8">
  696. <p><b>CRON表达式格式:</b>秒 分 时 日 月 周</p>
  697. <p><b>常用示例:</b></p>
  698. <p>• 0 0 8 * * ? - 每天8点执行</p>
  699. <p>• 0 0/30 * * * ? - 每30分钟执行</p>
  700. <p>• 0 0 9-18 * * ? - 每天9-18点整点执行</p>
  701. <p>• 0 0 8 ? * MON-FRI - 工作日8点执行</p>
  702. </div>
  703. `, 'CRON表达式帮助', { dangerouslyUseHTMLString: true })
  704. },
  705. emitChange() {
  706. // 构建完整的表单数据,确保包含 strategyCode
  707. const formData = {
  708. ...this.form,
  709. strategyCode: this.form.strategyCode || this.strategyCode // 双重保障
  710. }
  711. // 转换回后端需要的 triggerType
  712. if (formData.triggerType === 'STATE_CHANGE') {
  713. formData.triggerType = this.stateChangeMode === 'POLLING' ? 'POLLING' : 'ATTR'
  714. }
  715. this.$emit('change', formData)
  716. },
  717. getDeviceName(deviceCode) {
  718. const device = this.deviceList.find(d => d.deviceCode === deviceCode)
  719. return device ? device.deviceName : deviceCode
  720. },
  721. getEventName(eventKey) {
  722. const event = this.eventList.find(e => e.eventKey === eventKey)
  723. return event ? event.eventName : eventKey
  724. },
  725. getConditionValueDisplay() {
  726. if (!this.conditionValue) return '[未设置]'
  727. if (this.attrEnums.length > 0) {
  728. const enumItem = this.attrEnums.find(e => e.attrValue === this.conditionValue)
  729. if (enumItem) return enumItem.attrValueName
  730. }
  731. return this.conditionValue
  732. },
  733. getOperatorText(op) {
  734. const map = { '==': '等于', '!=': '不等于', '>': '大于', '>=': '大于等于', '<': '小于', '<=': '小于等于' }
  735. return map[op] || op
  736. },
  737. getTriggerModeText(mode) {
  738. const map = { 'ON_FIRST_MATCH': '触发(首次满足)', 'ON_CHANGE': '触发(值变化时)', 'ALWAYS': '触发(每次检查)' }
  739. return map[mode] || '触发'
  740. },
  741. getGroupLabel(groupName) {
  742. const labelMap = { 'Base': '基础信息', 'State': '状态信息', 'Protocol': '协议信息', 'Measure': '测量数据', 'Control': '控制参数', 'Default': '其他' }
  743. return labelMap[groupName] || groupName
  744. },
  745. getAttrTypeName(type) {
  746. const typeMap = { 'Value': '数值', 'String': '字符串', 'Enum': '枚举', 'Boolean': '布尔' }
  747. return typeMap[type] || type
  748. },
  749. getAttrTypeTag(type) {
  750. const tagMap = { 'Value': '', 'String': 'info', 'Enum': 'warning', 'Boolean': 'success' }
  751. return tagMap[type] || ''
  752. }
  753. }
  754. }
  755. </script>
  756. <style lang="scss" scoped>
  757. .trigger-config {
  758. .el-divider { margin: 16px 0 12px; }
  759. .form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
  760. .unit-label { margin-left: 8px; color: #909399; font-size: 12px; }
  761. .attr-type-info {
  762. padding: 8px 12px;
  763. background: #f5f7fa;
  764. border-radius: 6px;
  765. display: flex;
  766. align-items: center;
  767. }
  768. .cron-shortcuts {
  769. display: flex;
  770. flex-wrap: wrap;
  771. gap: 8px;
  772. .cron-tag { cursor: pointer; transition: all 0.2s; &:hover { transform: scale(1.05); } }
  773. }
  774. .cron-desc {
  775. font-size: 12px;
  776. color: #67c23a;
  777. padding: 8px 12px;
  778. background: #f0f9eb;
  779. border-radius: 4px;
  780. }
  781. .condition-preview {
  782. padding: 12px 14px;
  783. border-radius: 6px;
  784. font-size: 13px;
  785. line-height: 1.6;
  786. i { margin-right: 6px; }
  787. strong { color: #409eff; }
  788. &.success {
  789. background: linear-gradient(135deg, #f0f9eb 0%, #e8f8e0 100%);
  790. border-left: 3px solid #67c23a;
  791. color: #606266;
  792. }
  793. &.warning {
  794. background: linear-gradient(135deg, #fef0e5 0%, #fdf6ec 100%);
  795. border-left: 3px solid #e6a23c;
  796. color: #909399;
  797. }
  798. }
  799. }
  800. </style>