RuleSceneForm.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <template>
  2. <el-drawer
  3. v-model="drawerVisible"
  4. :title="drawerTitle"
  5. size="80%"
  6. direction="rtl"
  7. :close-on-click-modal="false"
  8. :close-on-press-escape="false"
  9. @close="handleClose"
  10. >
  11. <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
  12. <!-- 基础信息配置 -->
  13. <BasicInfoSection v-model="formData" :rules="formRules" />
  14. <!-- 触发器配置 -->
  15. <TriggerSection v-model:triggers="formData.triggers" />
  16. <!-- 执行器配置 -->
  17. <ActionSection v-model:actions="formData.actions" />
  18. </el-form>
  19. <template #footer>
  20. <div class="drawer-footer">
  21. <el-button :disabled="submitLoading" type="primary" @click="handleSubmit">
  22. <Icon icon="ep:check" />
  23. 确 定
  24. </el-button>
  25. <el-button @click="handleClose">
  26. <Icon icon="ep:close" />
  27. 取 消
  28. </el-button>
  29. </div>
  30. </template>
  31. </el-drawer>
  32. </template>
  33. <script setup lang="ts">
  34. import { useVModel } from '@vueuse/core'
  35. import BasicInfoSection from './sections/BasicInfoSection.vue'
  36. import TriggerSection from './sections/TriggerSection.vue'
  37. import ActionSection from './sections/ActionSection.vue'
  38. import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
  39. import { RuleSceneApi } from '@/api/iot/rule/scene'
  40. import {
  41. IotRuleSceneTriggerTypeEnum,
  42. IotRuleSceneActionTypeEnum,
  43. isDeviceTrigger
  44. } from '@/views/iot/utils/constants'
  45. import { ElMessage } from 'element-plus'
  46. import { CommonStatusEnum } from '@/utils/constants'
  47. /** IoT 场景联动规则表单 - 主表单组件 */
  48. defineOptions({ name: 'RuleSceneForm' })
  49. /** 组件属性定义 */
  50. const props = defineProps<{
  51. /** 抽屉显示状态 */
  52. modelValue: boolean
  53. /** 编辑的场景联动规则数据 */
  54. ruleScene?: IotRuleSceneDO
  55. }>()
  56. /** 组件事件定义 */
  57. const emit = defineEmits<{
  58. (e: 'update:modelValue', value: boolean): void
  59. (e: 'success'): void
  60. }>()
  61. const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
  62. /** 创建默认的表单数据 */
  63. const createDefaultFormData = (): RuleSceneFormData => {
  64. return {
  65. name: '',
  66. description: '',
  67. status: CommonStatusEnum.ENABLE, // 默认启用状态
  68. triggers: [
  69. {
  70. type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
  71. productId: undefined,
  72. deviceId: undefined,
  73. identifier: undefined,
  74. operator: undefined,
  75. value: undefined,
  76. cronExpression: undefined,
  77. conditionGroups: [] // 空的条件组数组
  78. }
  79. ],
  80. actions: []
  81. }
  82. }
  83. // 表单数据和状态
  84. const formRef = ref()
  85. const formData = ref<RuleSceneFormData>(createDefaultFormData())
  86. // 自定义校验器
  87. const validateTriggers = (_rule: any, value: any, callback: any) => {
  88. if (!value || !Array.isArray(value) || value.length === 0) {
  89. callback(new Error('至少需要一个触发器'))
  90. return
  91. }
  92. for (let i = 0; i < value.length; i++) {
  93. const trigger = value[i]
  94. // 校验触发器类型
  95. if (!trigger.type) {
  96. callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
  97. return
  98. }
  99. // 校验设备触发器
  100. if (isDeviceTrigger(trigger.type)) {
  101. if (!trigger.productId) {
  102. callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
  103. return
  104. }
  105. if (!trigger.deviceId) {
  106. callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
  107. return
  108. }
  109. if (!trigger.identifier) {
  110. callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
  111. return
  112. }
  113. if (!trigger.operator) {
  114. callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
  115. return
  116. }
  117. if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
  118. callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
  119. return
  120. }
  121. }
  122. // 校验定时触发器
  123. if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
  124. if (!trigger.cronExpression) {
  125. callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
  126. return
  127. }
  128. }
  129. }
  130. callback()
  131. }
  132. const validateActions = (_rule: any, value: any, callback: any) => {
  133. if (!value || !Array.isArray(value) || value.length === 0) {
  134. callback(new Error('至少需要一个执行器'))
  135. return
  136. }
  137. for (let i = 0; i < value.length; i++) {
  138. const action = value[i]
  139. // 校验执行器类型
  140. if (!action.type) {
  141. callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
  142. return
  143. }
  144. // 校验设备控制执行器
  145. if (
  146. action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
  147. action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
  148. ) {
  149. if (!action.productId) {
  150. callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
  151. return
  152. }
  153. if (!action.deviceId) {
  154. callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
  155. return
  156. }
  157. // 服务调用需要验证服务标识符
  158. if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
  159. if (!action.identifier) {
  160. callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
  161. return
  162. }
  163. }
  164. if (!action.params || Object.keys(action.params).length === 0) {
  165. callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
  166. return
  167. }
  168. }
  169. // 校验告警执行器
  170. if (
  171. action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
  172. action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
  173. ) {
  174. if (!action.alertConfigId) {
  175. callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
  176. return
  177. }
  178. }
  179. }
  180. callback()
  181. }
  182. const formRules = reactive({
  183. name: [
  184. { required: true, message: '场景名称不能为空', trigger: 'blur' },
  185. { type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
  186. ],
  187. status: [
  188. { required: true, message: '场景状态不能为空', trigger: 'change' },
  189. {
  190. type: 'enum',
  191. enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
  192. message: '状态值必须为启用或禁用',
  193. trigger: 'change'
  194. }
  195. ],
  196. description: [
  197. { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
  198. ],
  199. triggers: [{ required: true, validator: validateTriggers, trigger: 'change' }],
  200. actions: [{ required: true, validator: validateActions, trigger: 'change' }]
  201. })
  202. const submitLoading = ref(false)
  203. // 计算属性
  204. const isEdit = ref(false)
  205. const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
  206. /** 提交表单 */
  207. const handleSubmit = async () => {
  208. // 校验表单
  209. if (!formRef.value) return
  210. const valid = await formRef.value.validate()
  211. if (!valid) return
  212. // 提交请求
  213. submitLoading.value = true
  214. try {
  215. // 数据结构已对齐,直接使用表单数据
  216. console.log('提交数据:', formData.value)
  217. // 调用API保存数据
  218. if (isEdit.value) {
  219. // 更新场景联动规则
  220. await RuleSceneApi.updateRuleScene(formData.value)
  221. ElMessage.success('更新成功')
  222. } else {
  223. // 创建场景联动规则
  224. await RuleSceneApi.createRuleScene(formData.value)
  225. ElMessage.success('创建成功')
  226. }
  227. // 关闭抽屉并触发成功事件
  228. drawerVisible.value = false
  229. emit('success')
  230. } catch (error) {
  231. console.error('保存失败:', error)
  232. ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
  233. } finally {
  234. submitLoading.value = false
  235. }
  236. }
  237. const handleClose = () => {
  238. drawerVisible.value = false
  239. }
  240. /** 初始化表单数据 */
  241. const initFormData = () => {
  242. if (props.ruleScene) {
  243. // 编辑模式:数据结构已对齐,直接使用后端数据
  244. isEdit.value = true
  245. formData.value = {
  246. ...props.ruleScene,
  247. // 确保触发器数组不为空
  248. triggers: props.ruleScene.triggers?.length
  249. ? props.ruleScene.triggers
  250. : [
  251. {
  252. type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
  253. productId: undefined,
  254. deviceId: undefined,
  255. identifier: undefined,
  256. operator: undefined,
  257. value: undefined,
  258. cronExpression: undefined,
  259. conditionGroups: []
  260. }
  261. ],
  262. // 确保执行器数组不为空
  263. actions: props.ruleScene.actions || []
  264. }
  265. } else {
  266. // 新增模式:使用默认数据
  267. isEdit.value = false
  268. formData.value = createDefaultFormData()
  269. }
  270. }
  271. // 监听抽屉显示
  272. watch(drawerVisible, (visible) => {
  273. if (visible) {
  274. initFormData()
  275. // 重置表单验证状态
  276. nextTick(() => {
  277. formRef.value?.clearValidate()
  278. })
  279. }
  280. })
  281. // 监听编辑数据变化
  282. watch(
  283. () => props.ruleScene,
  284. () => {
  285. if (drawerVisible.value) {
  286. initFormData()
  287. }
  288. },
  289. { deep: true }
  290. )
  291. </script>