github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/static_source/admin/src/views/Entities/index.vue (about) 1 <script setup lang="tsx"> 2 import {useI18n} from '@/hooks/web/useI18n' 3 import {Table} from '@/components/Table' 4 import {h, onMounted, onUnmounted, reactive, ref, watch} from 'vue' 5 import {Pagination, TableColumn} from '@/types/table' 6 import api from "@/api/api"; 7 import {ElButton, ElCollapse, ElCollapseItem, ElMessage, ElTag} from 'element-plus' 8 import {ApiArea, ApiEntityShort, ApiPlugin, ApiStatistics, ApiTag} from "@/api/stub"; 9 import {useForm} from "@/hooks/web/useForm"; 10 import {useRouter} from "vue-router"; 11 import {parseTime} from "@/utils"; 12 import {ContentWrap} from "@/components/ContentWrap"; 13 import {Dialog} from '@/components/Dialog' 14 import {UUID} from "uuid-generator-ts"; 15 import stream from "@/api/stream"; 16 import {EventStateChange} from "@/api/types"; 17 import {FormSchema} from "@/types/form"; 18 import {Form} from '@/components/Form' 19 import {useCache} from "@/hooks/web/useCache"; 20 import {JsonEditor} from "@/components/JsonEditor"; 21 import Statistics from "@/components/Statistics/Statistics.vue"; 22 23 const {push} = useRouter() 24 const {register, methods} = useForm() 25 const {t} = useI18n() 26 const {wsCache} = useCache() 27 28 interface TableObject { 29 tableList: ApiEntityShort[] 30 params?: any 31 loading: boolean 32 sort?: string 33 query?: string 34 plugin?: string 35 area?: ApiArea 36 tags?: string[] 37 } 38 39 interface Params { 40 page?: number; 41 limit?: number; 42 sort?: string; 43 } 44 45 const cachePref = 'entities' 46 const tableObject = reactive<TableObject>( 47 { 48 tableList: [], 49 loading: false, 50 sort: wsCache.get(cachePref + 'Sort') || '-createdAt', 51 query: wsCache.get(cachePref + 'Query'), 52 plugin: wsCache.get(cachePref + 'Plugin')?.name, 53 area: wsCache.get(cachePref + 'Area'), 54 tags: wsCache.get(cachePref + 'Tags') 55 }, 56 ); 57 58 const columns: TableColumn[] = [ 59 { 60 field: 'id', 61 label: t('entities.name'), 62 width: "190px", 63 sortable: true 64 }, 65 { 66 field: 'pluginName', 67 label: t('entities.pluginName'), 68 width: "140px", 69 sortable: true 70 }, 71 // { 72 // field: 'tags', 73 // label: t('main.tags'), 74 // width: "150px", 75 // sortable: false, 76 // type: 'expand' 77 // }, 78 { 79 field: 'areaId', 80 label: t('entities.area'), 81 width: "100px", 82 sortable: true, 83 formatter: (row: ApiEntityShort) => { 84 return h( 85 'span', 86 row.area?.name 87 ) 88 } 89 }, 90 { 91 field: 'description', 92 sortable: true, 93 label: t('entities.description') 94 }, 95 { 96 field: 'status', 97 label: t('entities.status'), 98 width: "70px", 99 }, 100 { 101 field: 'createdAt', 102 label: t('main.createdAt'), 103 type: 'time', 104 sortable: true, 105 width: "170px", 106 formatter: (row: ApiEntityShort) => { 107 return h( 108 'span', 109 parseTime(row.createdAt) 110 ) 111 } 112 }, 113 { 114 field: 'updatedAt', 115 label: t('main.updatedAt'), 116 type: 'time', 117 sortable: true, 118 width: "170px", 119 formatter: (row: ApiEntityShort) => { 120 return h( 121 'span', 122 parseTime(row.updatedAt) 123 ) 124 } 125 } 126 ] 127 128 const paginationObj = ref<Pagination>({ 129 currentPage: wsCache.get(cachePref + 'CurrentPage') || 1, 130 pageSize: wsCache.get(cachePref + 'PageSize') || 50, 131 total: 0, 132 }) 133 134 const statistic = ref<Nullable<ApiStatistics>>(null) 135 const getStatistic = async () => { 136 137 const res = await api.v1.entityServiceGetStatistic() 138 .catch(() => { 139 }) 140 .finally(() => { 141 142 }) 143 if (res) { 144 statistic.value = res.data 145 } 146 } 147 148 const getList = async () => { 149 getStatistic() 150 151 tableObject.loading = true 152 153 wsCache.set(cachePref + 'CurrentPage', paginationObj.value.currentPage) 154 wsCache.set(cachePref + 'PageSize', paginationObj.value.pageSize) 155 wsCache.set(cachePref + 'Sort', tableObject.sort) 156 wsCache.set(cachePref + 'Query', tableObject.query) 157 wsCache.set(cachePref + 'Tags', tableObject.tags) 158 159 let params: Params = { 160 page: paginationObj.value.currentPage, 161 limit: paginationObj.value.pageSize, 162 sort: tableObject.sort, 163 query: tableObject.query || undefined, 164 plugin: tableObject.plugin || undefined, 165 area: tableObject?.area?.id || undefined, 166 tags: tableObject?.tags || undefined, 167 } 168 169 const res = await api.v1.entityServiceGetEntityList(params) 170 .catch(() => { 171 }) 172 .finally(() => { 173 tableObject.loading = false 174 }) 175 if (res) { 176 const {items, meta} = res.data; 177 tableObject.tableList = items; 178 paginationObj.value.currentPage = meta.pagination.page; 179 paginationObj.value.total = meta.pagination.total; 180 } else { 181 tableObject.tableList = []; 182 } 183 } 184 185 const currentID = ref('') 186 187 const onStateChanged = (event: EventStateChange) => { 188 getList() 189 } 190 191 onMounted(() => { 192 const uuid = new UUID() 193 currentID.value = uuid.getDashFreeUUID() 194 195 setTimeout(() => { 196 stream.subscribe('event_entity_loaded', currentID.value, onStateChanged); 197 stream.subscribe('event_entity_unloaded', currentID.value, onStateChanged); 198 }, 200) 199 }) 200 201 onUnmounted(() => { 202 stream.unsubscribe('event_entity_loaded', currentID.value); 203 stream.unsubscribe('event_entity_unloaded', currentID.value); 204 }) 205 206 watch( 207 () => paginationObj.value.currentPage, 208 () => { 209 getList() 210 } 211 ) 212 213 watch( 214 () => paginationObj.value.pageSize, 215 () => { 216 getList() 217 } 218 ) 219 220 const sortChange = (data) => { 221 const {column, prop, order} = data; 222 const pref: string = order === 'ascending' ? '+' : '-' 223 tableObject.sort = pref + prop 224 getList() 225 } 226 227 getList() 228 229 const addNew = () => { 230 push('/entities/new') 231 } 232 233 const selectRow = (row) => { 234 if (!row) { 235 return 236 } 237 const {id} = row 238 push(`/entities/edit/${id}`) 239 } 240 241 const restart = async (entity: ApiEntityShort) => { 242 await api.v1.developerToolsServiceReloadEntity({id: entity.id}); 243 ElMessage({ 244 title: t('Success'), 245 message: t('message.requestSentSuccessfully'), 246 type: 'success', 247 duration: 2000 248 }); 249 } 250 251 const enable = async (entity: ApiEntityShort) => { 252 if (!entity?.id) return; 253 await api.v1.entityServiceEnabledEntity(entity.id); 254 ElMessage({ 255 title: t('Success'), 256 message: t('message.requestSentSuccessfully'), 257 type: 'success', 258 duration: 2000 259 }); 260 } 261 262 const disable = async (entity: ApiEntityShort) => { 263 if (!entity?.id) return; 264 await api.v1.entityServiceDisabledEntity(entity.id); 265 ElMessage({ 266 title: t('Success'), 267 message: t('message.requestSentSuccessfully'), 268 type: 'success', 269 duration: 2000 270 }); 271 } 272 273 const dialogVisible = ref(false) 274 const importedEntity = ref(null) 275 const showImportDialog = () => { 276 dialogVisible.value = true 277 } 278 279 const importHandler = (val: any) => { 280 if (importedEntity.value == val) { 281 return 282 } 283 importedEntity.value = val 284 } 285 286 const importEntity = async () => { 287 let val: ApiEntityShort; 288 try { 289 if (importedEntity.value?.json) { 290 val = importedEntity.value.json as ApiEntityShort; 291 } else if (importedEntity.value.text) { 292 val = JSON.parse(importedEntity.value.text) as ApiEntityShort; 293 } 294 } catch { 295 ElMessage({ 296 title: t('Error'), 297 message: t('message.corruptedJsonFormat'), 298 type: 'error', 299 duration: 2000 300 }); 301 return 302 } 303 const entity: ApiEntityShort = { 304 id: val.id, 305 pluginName: val.pluginName, 306 description: val.description, 307 area: val.area, 308 icon: val.icon, 309 image: val.image, 310 autoLoad: val.autoLoad, 311 parent: val.parent || undefined, 312 actions: val.actions, 313 states: val.states, 314 attributes: val.attributes, 315 settings: val.settings, 316 scripts: val.scripts, 317 tags: val.tags 318 } 319 const res = await api.v1.entityServiceImportEntity(entity) 320 if (res) { 321 ElMessage({ 322 title: t('Success'), 323 message: t('message.importedSuccessful'), 324 type: 'success', 325 duration: 2000 326 }) 327 getList() 328 } 329 } 330 331 // search form 332 const schema = reactive<FormSchema[]>([ 333 { 334 field: 'name', 335 label: t('entities.name'), 336 component: 'Input', 337 colProps: { 338 span: 12 339 }, 340 componentProps: { 341 placeholder: t('entities.name'), 342 onChange: (val: string) => { 343 tableObject.query = val || undefined 344 getList() 345 } 346 } 347 }, 348 { 349 field: 'plugin', 350 label: t('entities.pluginName'), 351 component: 'Plugin', 352 value: null, 353 colProps: { 354 span: 12 355 }, 356 hidden: false, 357 componentProps: { 358 placeholder: t('entities.pluginName'), 359 onChange: (val: ApiPlugin) => { 360 tableObject.plugin = val?.name || undefined 361 wsCache.set(cachePref + 'Plugin', val) 362 getList() 363 } 364 }, 365 }, 366 { 367 field: 'area', 368 label: t('entities.area'), 369 value: null, 370 component: 'Area', 371 colProps: { 372 span: 12 373 }, 374 componentProps: { 375 placeholder: t('entities.area'), 376 onChange: (val: ApiArea) => { 377 wsCache.set(cachePref + 'Area', val) 378 tableObject.area = val || undefined 379 getList() 380 } 381 }, 382 }, 383 { 384 field: 'tags', 385 label: t('main.tags'), 386 component: 'Tags', 387 colProps: { 388 span: 12 389 }, 390 value: [], 391 hidden: false, 392 componentProps: { 393 placeholder: t('main.tags'), 394 onChange: (val: ApiTag) => { 395 wsCache.set(cachePref + 'Tags', val) 396 tableObject.tags = val || undefined 397 getList() 398 } 399 } 400 }, 401 ]) 402 403 const filterList = () => { 404 let list = '' 405 if (tableObject?.query) { 406 list += 'name(' + tableObject.query + ') ' 407 } 408 if (tableObject?.plugin) { 409 list += 'plugin(' + tableObject.plugin + ') ' 410 } 411 if (tableObject?.area) { 412 list += 'area(' + tableObject.area.name + ') ' 413 } 414 if (tableObject?.tags && tableObject?.tags.length) { 415 list += 'tags(' + tableObject.tags + ') ' 416 } 417 if (list != '') { 418 list = ': ' + list 419 } 420 return list 421 } 422 423 const {setValues, setSchema} = methods 424 if (wsCache.get(cachePref + 'Query')) { 425 setValues({ 426 name: wsCache.get(cachePref + 'Query') 427 }) 428 } 429 if (wsCache.get(cachePref + 'Plugin')) { 430 setValues({ 431 plugin: wsCache.get(cachePref + 'Plugin') 432 }) 433 } 434 if (wsCache.get(cachePref + 'Area')) { 435 setValues({ 436 area: wsCache.get(cachePref + 'Area') 437 }) 438 } 439 if (wsCache.get(cachePref + 'Tags')) { 440 setValues({ 441 tags: wsCache.get(cachePref + 'Tags') 442 }) 443 } 444 445 </script> 446 447 <template> 448 <Statistics v-model="statistic" :cols="6" /> 449 450 <ContentWrap> 451 <ElButton class="flex mb-20px items-left" type="primary" @click="addNew()" plain> 452 <Icon icon="ep:plus" class="mr-5px"/> 453 {{ t('entities.addNew') }} 454 </ElButton> 455 <ElButton class="flex mb-20px items-left" type="primary" @click="showImportDialog()" plain> 456 {{ t('main.import') }} 457 </ElButton> 458 <ElCollapse class="mb-20px"> 459 <ElCollapseItem :title="$t('main.filter') + filterList()"> 460 <Form 461 class="filter-form" 462 :schema="schema" 463 label-position="top" 464 label-width="auto" 465 hide-required-asterisk 466 @register="register" 467 /> 468 </ElCollapseItem> 469 </ElCollapse> 470 <Table 471 class="entity-table" 472 :expand="true" 473 :selection="false" 474 v-model:pageSize="paginationObj.pageSize" 475 v-model:currentPage="paginationObj.currentPage" 476 :showUpPagination="20" 477 :columns="columns" 478 :data="tableObject.tableList" 479 :loading="tableObject.loading" 480 :pagination="paginationObj" 481 @sort-change="sortChange" 482 style="width: 100%" 483 @current-change="selectRow" 484 > 485 486 <template #status="{ row }"> 487 <div class="w-[100%] text-center"> 488 <ElButton :link="true" @click.prevent.stop="enable(row)" v-if="!row?.isLoaded"> 489 <Icon icon="noto:red-circle" class="mr-5px"/> 490 </ElButton> 491 <ElButton :link="true" @click.prevent.stop="disable(row)" v-if="row?.isLoaded"> 492 <Icon icon="noto:green-circle" class="mr-5px"/> 493 </ElButton> 494 </div> 495 </template> 496 497 <template #id="{row}"> 498 {{ row.id.split('.')[1] }} 499 </template> 500 501 <template #pluginName="{row}"> 502 <ElTag> 503 {{ row.pluginName }} 504 </ElTag> 505 </template> 506 507 <template #expand="{row}"> 508 <div class="tag-list" v-if="row.tags"> 509 <ElTag v-for="tag in row.tags" type="info" :key="tag" round effect="light" size="small"> 510 {{ tag }} 511 </ElTag> 512 </div> 513 </template> 514 515 </Table> 516 </ContentWrap> 517 518 <!-- import dialog --> 519 <Dialog v-model="dialogVisible" :title="t('entities.dialogImportTitle')" :maxHeight="400" width="80%" custom-class> 520 <JsonEditor @change="importHandler"/> 521 <template #footer> 522 <ElButton type="primary" @click="importEntity()" plain>{{ t('main.import') }}</ElButton> 523 <ElButton @click="dialogVisible = false">{{ t('main.closeDialog') }}</ElButton> 524 </template> 525 </Dialog> 526 <!-- /import dialog --> 527 528 </template> 529 530 <style lang="less"> 531 532 //:deep(.filter-form .el-col) { 533 // padding-left: 0!important; 534 // padding-right: 0!important; 535 //} 536 537 .entity-table { 538 .tag-list { 539 .el-tag { 540 margin: 0 5px; 541 } 542 } 543 544 :deep(.el-table__row) { 545 cursor: pointer; 546 } 547 548 tr.el-table__row [class*="el-table__cell"] { 549 //background-color: green; border-top: var(--el-table-border); border-bottom: none !important; 550 border-top: var(--el-table-border); 551 } 552 553 .el-table__expanded-cell { 554 &.el-table__cell [class*="tag-list"] { 555 //background-color: red!important; border-bottom: none !important; 556 } 557 558 &.el-table__cell:not(:has(.tag-list)) { 559 display: none !important; 560 //background-color: blue!important; 561 } 562 } 563 564 .el-table td.el-table__cell, 565 .el-table th.el-table__cell.is-leaf { 566 border-bottom: none !important; 567 } 568 569 .el-table--enable-row-hover .el-table__body tr.el-table__row:hover, 570 .el-table--enable-row-hover .el-table__body tr.el-table__row:hover + tr { 571 & > td.el-table__cell { 572 background-color: var(--el-table-row-hover-bg-color); 573 } 574 } 575 } 576 577 578 </style>