github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/static_source/admin/src/views/Entities/edit.vue (about) 1 <script setup lang="ts"> 2 import {computed, onMounted, onUnmounted, reactive, ref, unref} from 'vue' 3 import {useI18n} from '@/hooks/web/useI18n' 4 import {ElButton, ElMessage, ElPopconfirm, ElTabPane, ElTabs} from 'element-plus' 5 import {useRoute, useRouter} from 'vue-router' 6 import api from "@/api/api"; 7 import Form from './components/Form.vue' 8 import {ContentWrap} from "@/components/ContentWrap"; 9 import {Entity, EntityAction, EntityState, Plugin} from "@/views/Entities/components/types"; 10 import Actions from "@/views/Entities/components/Actions.vue"; 11 import {useEmitt} from "@/hooks/web/useEmitt"; 12 import { 13 ApiArea, 14 ApiAttribute, 15 ApiEntityAction, 16 ApiEntityState, 17 ApiPlugin, 18 ApiScript, 19 ApiUpdateEntityRequestAction, 20 ApiUpdateEntityRequestState 21 } from "@/api/stub"; 22 import States from "@/views/Entities/components/States.vue"; 23 import {AttributesEditor} from "@/components/Attributes"; 24 import {Dialog} from '@/components/Dialog' 25 import {JsonViewer} from "@/components/JsonViewer"; 26 import {copyToClipboard} from "@/utils/clipboard"; 27 import {EventStateChange} from "@/api/types"; 28 import {UUID} from "uuid-generator-ts"; 29 import stream from "@/api/stream"; 30 import Storage from "@/views/Entities/components/Storage.vue"; 31 import Metrics from "@/views/Entities/components/Metrics.vue"; 32 33 const {push} = useRouter() 34 const route = useRoute(); 35 const {t} = useI18n() 36 37 const writeRef = ref<ComponentRef<typeof Form>>() 38 const loading = ref(false) 39 const entityId = computed(() => route.params.id as string); 40 const currentEntity = ref<Nullable<Entity>>(null) 41 const activeTab = ref('attributes') 42 const dialogSource = ref({}) 43 const dialogVisible = ref(false) 44 const lastEvent = ref<Nullable<EventStateChange>>(null) 45 46 interface Internal { 47 attributes: ApiAttribute[]; 48 settings: ApiAttribute[]; 49 } 50 51 const internal = reactive<Internal>( 52 { 53 attributes: [], 54 settings: [], 55 }, 56 ); 57 58 const fetch = async () => { 59 loading.value = true 60 const res = await api.v1.entityServiceGetEntity(entityId.value) 61 .catch(() => { 62 }) 63 .finally(() => { 64 loading.value = false 65 }) 66 if (res) { 67 const entity = res.data as Entity 68 69 // attributes 70 internal.attributes = []; 71 if (entity.attributes) { 72 for (const key in entity.attributes) { 73 internal.attributes.push(entity.attributes[key]); 74 } 75 } 76 77 // settings 78 internal.settings = []; 79 if (entity.settings) { 80 for (const key in entity.settings) { 81 internal.settings.push(entity.settings[key]); 82 } 83 } 84 85 currentEntity.value = entity 86 87 await fetchPlugin() 88 89 loading.value = false 90 } else { 91 currentEntity.value = null 92 } 93 } 94 95 const fetchPlugin = async () => { 96 loading.value = true 97 const res = await api.v1.pluginServiceGetPlugin(currentEntity.value.pluginName) 98 if (res) { 99 const plugin = res.data as ApiPlugin; 100 101 // attributes 102 let actorAttrs: ApiAttribute[] = []; 103 if (plugin.options?.actorAttrs) { 104 for (const key in plugin.options.actorAttrs) { 105 actorAttrs.push(plugin.options.actorAttrs[key]); 106 } 107 } 108 109 // actorSetts 110 let actorSetts: ApiAttribute[] = []; 111 if (plugin.options?.actorSetts) { 112 for (const key in plugin.options.actorSetts) { 113 actorSetts.push(plugin.options.actorSetts[key]); 114 } 115 } 116 117 // setts 118 let setts: ApiAttribute[] = []; 119 if (plugin.options?.setts) { 120 for (const key in plugin.options?.setts) { 121 setts.push(plugin.options?.setts[key]); 122 } 123 } 124 125 // actorActions 126 let actorActions: EntityAction[] = []; 127 if (plugin.options?.actorActions) { 128 for (const key in plugin.options?.actorActions) { 129 actorActions.push(plugin.options?.actorActions[key]); 130 } 131 } 132 133 // actorStates 134 let actorStates: EntityState[] = []; 135 if (plugin.options?.actorStates) { 136 for (const key in plugin.options?.actorStates) { 137 actorStates.push(plugin.options?.actorStates[key]); 138 } 139 } 140 141 currentEntity.value.plugin = { 142 name: plugin.name, 143 version: plugin.version, 144 enabled: plugin.enabled, 145 system: plugin.system, 146 actor: plugin.actor, 147 actorCustomAttrs: plugin.options?.actorCustomAttrs, 148 actorCustomActions: plugin.options?.actorCustomActions, 149 actorCustomStates: plugin.options?.actorCustomStates, 150 actorCustomSetts: plugin.options?.actorCustomSetts, 151 triggers: plugin.options?.triggers, 152 actorAttrs: actorAttrs, 153 actorSetts: actorSetts, 154 setts: setts, 155 actorActions: actorActions, 156 actorStates: actorStates, 157 } as Plugin; 158 // console.log(plugin) 159 } 160 } 161 162 const prepareForSave = async () => { 163 const write = unref(writeRef) 164 const validate = await write?.elFormRef?.validate()?.catch(() => { 165 }) 166 if (validate) { 167 const data = (await write?.getFormData()) as Entity 168 let actions: ApiUpdateEntityRequestAction[] = [] 169 let states: ApiUpdateEntityRequestState[] = [] 170 for (const a of data?.actions) { 171 actions.push({ 172 name: a.name, 173 description: a.description, 174 icon: a.icon, 175 imageId: a.image?.id || a.imageId, 176 scriptId: a.script?.id || a.scriptId, 177 type: a.type, 178 }) 179 } 180 for (const a of data?.states) { 181 states.push({ 182 name: a.name, 183 description: a.description, 184 imageId: a.image?.id || a.imageId, 185 }) 186 } 187 let attributes: { [key: string]: ApiAttribute } = {}; 188 for (const index in internal.attributes) { 189 attributes[internal.attributes[index].name] = internal.attributes[index]; 190 } 191 let settings: { [key: string]: ApiAttribute } = {}; 192 for (const index in internal.settings) { 193 settings[internal.settings[index].name] = internal.settings[index]; 194 } 195 const body = { 196 id: data.id, 197 pluginName: data.plugin?.name || data.pluginName, 198 description: data.description, 199 areaId: data.area?.id, 200 icon: data.icon, 201 imageId: data.image?.id, 202 autoLoad: data.autoLoad, 203 restoreState: data.restoreState, 204 parentId: data.parent?.id, 205 actions: actions, 206 states: states, 207 attributes: attributes, 208 settings: settings, 209 scriptIds: data.scriptIds, 210 metrics: data.metrics, 211 tags: data.tags, 212 } 213 return body 214 } 215 return null 216 } 217 218 let _scriptsPromises = [] 219 let _scripts: Map<string, ApiScript> = [] 220 const fetchScript = async (id: number) => { 221 const res = await api.v1.scriptServiceGetScriptById(id) 222 if (res) { 223 _scripts[id] = res.data 224 } 225 } 226 227 const prepareForExport = async () => { 228 const write = unref(writeRef) 229 const validate = await write?.elFormRef?.validate()?.catch(() => { 230 }) 231 if (validate) { 232 const data = (await write?.getFormData()) as Entity 233 let actions: ApiEntityAction[] = [] 234 let states: ApiEntityState[] = [] 235 236 data.actions.forEach(action => { 237 if (action.scriptId) { 238 _scripts[action.scriptId] = null 239 } 240 if (action.script?.id) { 241 _scripts[action.script.id] = null 242 } 243 }) 244 data.scripts.forEach(script => { 245 if (script.id) { 246 _scripts[script.id] = null 247 } 248 }) 249 250 _scripts.forEach((value: ApiScript, key: string) => { 251 _scriptsPromises.push(fetchScript(key)) 252 }) 253 254 await Promise.all(_scriptsPromises) 255 256 for (const a of data?.actions) { 257 let script: ApiScript = null; 258 if (a.script) { 259 script = { 260 id: a.script.id, 261 lang: _scripts[a.script.id]?.lang, 262 name: a.script.name, 263 source: _scripts[a.script.id]?.source || '', 264 description: a.script.description, 265 } as ApiScript 266 } 267 actions.push({ 268 name: a.name, 269 description: a.description, 270 icon: a.icon, 271 image: a.image, 272 script: script, 273 scriptId: script?.id || 0, 274 type: a.type, 275 } as ApiEntityAction) 276 } 277 for (const a of data?.states) { 278 states.push({ 279 name: a.name, 280 description: a.description, 281 image: a.image, 282 icon: a.icon, 283 } as ApiEntityState) 284 } 285 let attributes: { [key: string]: ApiAttribute } = {}; 286 for (const index in internal.attributes) { 287 attributes[internal.attributes[index].name] = internal.attributes[index]; 288 } 289 let settings: { [key: string]: ApiAttribute } = {}; 290 for (const index in internal.settings) { 291 settings[internal.settings[index].name] = internal.settings[index]; 292 } 293 let area: ApiArea = null 294 if (data.area) { 295 area = { 296 id: data.area.id, 297 name: data.area.name, 298 description: data.area.description, 299 polygon: data.area.polygon, 300 } as ApiArea 301 } 302 let scripts: ApiScript[] = []; 303 if (data.scripts) { 304 data.scripts.forEach((value: ApiScript) => { 305 scripts.push({ 306 id: value.id, 307 lang: _scripts[value.id]?.lang, 308 name: value.name, 309 source: _scripts[value.id]?.source || '', 310 description: value.description, 311 } as ApiScript) 312 }) 313 } 314 const body = { 315 id: data.id, 316 pluginName: data.plugin?.name || data.pluginName, 317 description: data.description, 318 area: area, 319 icon: data.icon, 320 image: data.image, 321 autoLoad: data.autoLoad, 322 restoreState: data.restoreState, 323 parent: data.parent, 324 actions: actions, 325 states: states, 326 attributes: attributes, 327 settings: data.settings, 328 scripts: scripts, 329 metrics: data.metrics, 330 tags: data.tags, 331 } 332 return body 333 } 334 return null 335 } 336 337 const save = async () => { 338 const body = await prepareForSave() 339 if (!body) { 340 return 341 } 342 const res = await api.v1.entityServiceUpdateEntity(entityId.value, body) 343 .catch(() => { 344 }) 345 .finally(() => { 346 347 }) 348 if (res) { 349 fetch() 350 ElMessage({ 351 title: t('Success'), 352 message: t('message.uploadSuccessfully'), 353 type: 'success', 354 duration: 2000 355 }) 356 } 357 } 358 359 const callAction = async (name: string) => { 360 await api.v1.interactServiceEntityCallAction({id: entityId.value, name: name}); 361 ElMessage({ 362 title: t('Success'), 363 message: t('message.callSuccessful'), 364 type: 'success', 365 duration: 2000 366 }); 367 } 368 369 const setState = async (name: string) => { 370 await api.v1.developerToolsServiceEntitySetState({id: entityId.value, name: name}); 371 ElMessage({ 372 title: t('Success'), 373 message: t('message.callSuccessful'), 374 type: 'success', 375 duration: 2000 376 }); 377 } 378 379 useEmitt({ 380 name: 'updateActions', 381 callback: (val: EntityAction[]) => { 382 const second = JSON.parse(JSON.stringify(val)) 383 currentEntity.value.actions = JSON.parse(JSON.stringify(second)) 384 } 385 }) 386 387 useEmitt({ 388 name: 'updateStates', 389 callback: (val: EntityState[]) => { 390 const second = JSON.parse(JSON.stringify(val)) 391 currentEntity.value.states = JSON.parse(JSON.stringify(second)) 392 } 393 }) 394 395 useEmitt({ 396 name: 'callAction', 397 callback: (val: string) => { 398 callAction(val) 399 } 400 }) 401 402 useEmitt({ 403 name: 'setState', 404 callback: (val: string) => { 405 setState(val) 406 } 407 }) 408 409 const onAttrsUpdated = (attrs: ApiAttribute[]) => { 410 const second = JSON.parse(JSON.stringify(attrs)) 411 internal.attributes = JSON.parse(JSON.stringify(second)) 412 } 413 414 const onSettingsUpdated = (attrs: ApiAttribute[]) => { 415 const second = JSON.parse(JSON.stringify(attrs)) 416 internal.settings = JSON.parse(JSON.stringify(second)) 417 } 418 419 const cancel = () => { 420 push('/entities') 421 } 422 423 const copy = async () => { 424 const body = await prepareForExport() 425 copyToClipboard(JSON.stringify(body, null, 2)) 426 } 427 428 const exportEntity = async () => { 429 const body = await prepareForExport() 430 dialogSource.value = body 431 dialogVisible.value = true 432 } 433 434 const restart = async () => { 435 await api.v1.developerToolsServiceReloadEntity({id: entityId.value}); 436 ElMessage({ 437 title: t('Success'), 438 message: t('message.reloadSuccessful'), 439 type: 'success', 440 duration: 2000 441 }); 442 } 443 444 const remove = async () => { 445 loading.value = true 446 const res = await api.v1.entityServiceDeleteEntity(entityId.value) 447 .catch(() => { 448 }) 449 .finally(() => { 450 loading.value = false 451 }) 452 if (res) { 453 cancel() 454 } 455 } 456 457 const currentID = ref('') 458 459 const onStateChanged = (event: EventStateChange) => { 460 if (event.entity_id != entityId.value) { 461 return; 462 } 463 464 lastEvent.value = event; 465 } 466 467 const requestCurrentState = () => { 468 stream.send({ 469 id: UUID.createUUID(), 470 query: 'event_get_last_state', 471 body: btoa(JSON.stringify({'entity_id': entityId.value})) 472 }); 473 } 474 475 const handleTabChange = (tab: any, event: any) => { 476 const {paneName} = tab; 477 if (paneName == 'currentState') { 478 requestCurrentState(); 479 } 480 } 481 482 onMounted(() => { 483 const uuid = new UUID() 484 currentID.value = uuid.getDashFreeUUID() 485 486 setTimeout(() => { 487 stream.subscribe('state_changed', currentID.value, onStateChanged); 488 }, 1000) 489 }) 490 491 onUnmounted(() => { 492 stream.unsubscribe('state_changed', currentID.value); 493 }) 494 495 fetch() 496 497 </script> 498 499 <template> 500 <ContentWrap> 501 <el-tabs class="demo-tabs" v-model="activeTab" @tab-click="handleTabChange"> 502 <!-- main --> 503 <el-tab-pane :label="$t('entities.main')" name="main"> 504 <Form ref="writeRef" :current-row="currentEntity"/> 505 </el-tab-pane> 506 <!-- /main --> 507 508 <!-- actions --> 509 <el-tab-pane :label="$t('entities.actions')" name="actions"> 510 <Actions 511 :actions="currentEntity?.actions" 512 :custom-actions="currentEntity?.plugin?.actorCustomActions" 513 :plugin-actions="currentEntity?.plugin?.actorActions" 514 /> 515 </el-tab-pane> 516 <!-- /actions --> 517 518 <!-- states --> 519 <el-tab-pane :label="$t('entities.states')" name="states"> 520 <States 521 :states="currentEntity?.states" 522 :custom-states="currentEntity?.plugin?.actorCustomStates" 523 :plugin-states="currentEntity?.plugin?.actorStates" 524 /> 525 </el-tab-pane> 526 <!-- /states --> 527 528 <!-- attributes --> 529 <el-tab-pane :label="$t('entities.attributes')" name="attributes"> 530 <AttributesEditor 531 v-model="internal.attributes" 532 :custom-attrs="currentEntity?.plugin?.actorCustomAttrs" 533 :plugin-attrs="currentEntity?.plugin?.actorAttrs" 534 @change="onAttrsUpdated" 535 /> 536 </el-tab-pane> 537 <!-- /attributes --> 538 539 <!-- settings --> 540 <el-tab-pane :label="$t('entities.settings')" name="settings"> 541 <AttributesEditor 542 v-model="internal.settings" 543 :custom-attrs="currentEntity?.plugin?.actorCustomSetts" 544 :plugin-attrs="currentEntity?.plugin?.actorSetts" 545 @change="onSettingsUpdated" 546 /> 547 </el-tab-pane> 548 <!-- /settings --> 549 550 <!-- metrics --> 551 <el-tab-pane :label="$t('entities.metrics')" name="metrics"> 552 <Metrics :metrics="currentEntity?.metrics"/> 553 </el-tab-pane> 554 <!-- /metrics --> 555 556 <!-- storage --> 557 <el-tab-pane :label="$t('entities.storage')" name="storage"> 558 <Storage v-model="currentEntity"/> 559 </el-tab-pane> 560 <!-- /storage --> 561 562 <!-- current state --> 563 <el-tab-pane :label="$t('entities.currentState')" name="currentState"> 564 <ElButton type="default" @click.prevent.stop="requestCurrentState()" class="mb-20px"> 565 <Icon icon="ep:refresh" class="mr-5px"/> 566 {{ $t('main.currentState') }} 567 </ElButton> 568 569 <JsonViewer v-model="lastEvent"/> 570 </el-tab-pane> 571 <!-- /current state --> 572 573 </el-tabs> 574 575 576 <div style="text-align: right" class="mt-20px"> 577 578 <ElButton type="primary" @click="save()"> 579 {{ t('main.save') }} 580 </ElButton> 581 582 <ElButton type="primary" @click="restart()"> 583 <Icon icon="ep:refresh" class="mr-5px"/> 584 {{ t('main.restart') }} 585 </ElButton> 586 587 <ElButton type="primary" @click="exportEntity()"> 588 <Icon icon="uil:file-export" class="mr-5px"/> 589 {{ t('main.export') }} 590 </ElButton> 591 592 <ElButton type="default" @click="fetch()"> 593 {{ t('main.loadFromServer') }} 594 </ElButton> 595 596 <ElButton type="default" @click="cancel()"> 597 {{ t('main.return') }} 598 </ElButton> 599 600 <ElPopconfirm 601 :confirm-button-text="$t('main.ok')" 602 :cancel-button-text="$t('main.no')" 603 width="250" 604 style="margin-left: 10px;" 605 :title="$t('main.are_you_sure_to_do_want_this?')" 606 @confirm="remove" 607 > 608 <template #reference> 609 <ElButton class="mr-10px" type="danger" plain> 610 <Icon icon="ep:delete" class="mr-5px"/> 611 {{ t('main.remove') }} 612 </ElButton> 613 </template> 614 </ElPopconfirm> 615 616 </div> 617 </ContentWrap> 618 619 <!-- export dialog --> 620 <Dialog v-model="dialogVisible" :title="t('entities.dialogExportTitle')" :maxHeight="400" width="80%"> 621 <JsonViewer v-model="dialogSource"/> 622 <!-- <template #footer>--> 623 <!-- <ElButton @click="copy()">{{ t('setting.copy') }}</ElButton>--> 624 <!-- <ElButton @click="dialogVisible = false">{{ t('main.closeDialog') }}</ElButton>--> 625 <!-- </template>--> 626 </Dialog> 627 <!-- /export dialog --> 628 629 </template> 630 631 <style lang="less" scoped> 632 633 </style>