github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/static_source/admin/src/views/Dashboard/editor/editor.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, ElEmpty, ElMessage, ElTabPane, ElTabs} from 'element-plus' 5 import {useRoute} from 'vue-router' 6 import api from "@/api/api"; 7 import {UUID} from "uuid-generator-ts"; 8 import stream from "@/api/stream"; 9 import {Card, Core, eventBus, EventContextMenu, stateService, Tab} from "@/views/Dashboard/core"; 10 import ViewTab from "@/views/Dashboard/editor/ViewTab.vue"; 11 import {DraggableContainer} from "@/components/DraggableContainer"; 12 import TabSettings from "@/views/Dashboard/editor/TabSettings.vue"; 13 import TabEditor from "@/views/Dashboard/editor/TabEditor.vue"; 14 import TabCardItem from "@/views/Dashboard/editor/TabCardItem.vue"; 15 import TabCard from "@/views/Dashboard/editor/TabCard.vue"; 16 import {EventStateChange} from "@/api/types"; 17 import {useAppStore} from "@/store/modules/app"; 18 import {GetFullImageUrl} from "@/utils/serverId"; 19 import {SecondMenu} from "@/views/Dashboard/core/src/secondMenu"; 20 import {JsonEditor} from "@/components/JsonEditor"; 21 import {Dialog} from "@/components/Dialog"; 22 import {ApiDashboardTab} from "@/api/stub"; 23 import CardListWindow from "@/views/Dashboard/editor/CardListWindow.vue"; 24 25 const route = useRoute(); 26 const {t} = useI18n() 27 const appStore = useAppStore() 28 29 // --------------------------------- 30 // common 31 // --------------------------------- 32 33 const loading = ref(true) 34 const dashboardId = computed(() => parseInt(route.params.id as string) as number); 35 const core = reactive<Core>(new Core()); 36 const currentID = ref('') 37 38 // context menu 39 const contextMenu = reactive<SecondMenu>(new SecondMenu(unref(core))); 40 41 const eventHandler = (event: string, args: any[]) => { 42 switch (event) { 43 case 'showTabImportDialog': 44 importDialogVisible.value = true 45 break; 46 case 'fetchDashboard': 47 fetchDashboard() 48 break; 49 } 50 } 51 52 const eventStateChanged = (eventName: string, event: EventStateChange) => { 53 core.onStateChanged(event) 54 } 55 56 const eventBusHandler = (eventName: string, event: EventStateChange) => { 57 core.eventBusHandler(eventName, event) 58 } 59 60 onMounted(() => { 61 const uuid = new UUID() 62 currentID.value = uuid.getDashFreeUUID() 63 64 fetchDashboard() 65 66 stream.subscribe('state_changed', currentID.value, stateService.onStateChanged); 67 eventBus.subscribe('stateChanged', eventStateChanged) 68 69 eventBus.subscribe(['showTabImportDialog', 'fetchDashboard'], eventHandler) 70 eventBus.subscribe('eventContextMenu', contextMenu.eventHandler) 71 eventBus.subscribe(undefined, eventBusHandler) 72 }) 73 74 onUnmounted(() => { 75 76 stream.unsubscribe('state_changed', currentID.value); 77 eventBus.unsubscribe('stateChanged', eventStateChanged) 78 79 eventBus.unsubscribe(['showTabImportDialog', 'fetchDashboard'], eventHandler) 80 eventBus.unsubscribe('eventContextMenu', contextMenu.eventHandler) 81 eventBus.unsubscribe(undefined, eventBusHandler) 82 }) 83 84 // --------------------------------- 85 // dashboard 86 // --------------------------------- 87 88 const fetchDashboard = async () => { 89 loading.value = true; 90 const res = await api.v1.dashboardServiceGetDashboardById(dashboardId.value) 91 .catch(() => { 92 }) 93 .finally(() => { 94 loading.value = false; 95 }) 96 core.currentBoard(res.data); 97 } 98 99 // --------------------------------- 100 // tabs 101 // --------------------------------- 102 103 const activeTabIdx = computed({ 104 get(): string { 105 return core.activeTabIdx + '' 106 }, 107 set(value: string) { 108 core.activeTabIdx = parseInt(value) 109 eventBus.emit('updateGrid', core.getActiveTab?.id) 110 } 111 }) 112 113 const activeTab = computed<Tab>(() => core.getActiveTab as Tab) 114 const activeCard = computed<Card>(() => core.getActiveTab.cards[core.activeCard] as Card) 115 116 const getTabStyle = () => { 117 const style = { 118 margin: 0 119 } 120 if (core.getActiveTab?.background) { 121 style['background-color'] = core.getActiveTab?.background 122 } else { 123 if (core.getActiveTab?.backgroundAdaptive) { 124 style['background-color'] = appStore.isDark ? '#333335' : '#FFF' 125 } 126 } 127 128 if (activeTab.value?.backgroundImage) { 129 style['background-image'] = `url(${GetFullImageUrl(activeTab.value.backgroundImage)})` 130 style['background-repeat'] = 'repeat'; 131 style['background-position'] = 'center'; 132 // style['background-size'] = 'cover'; 133 } 134 return style 135 } 136 137 const tagsView = computed(() => tagsView.value ? 37 : 0) 138 139 const createTab = async () => { 140 await core.createTab(); 141 142 ElMessage({ 143 title: t('Success'), 144 message: t('message.createdSuccessfully'), 145 type: 'success', 146 duration: 2000 147 }); 148 } 149 150 const addCard = () => { 151 core.createCard(); 152 } 153 154 const toggleMenu = (menu: string): void => { 155 switch (menu) { 156 case 'tabs': 157 eventBus.emit('toggleTabsMenu'); 158 break 159 case 'cards': 160 eventBus.emit('toggleCardsMenu'); 161 break 162 case 'cardItems': 163 eventBus.emit('toggleCardItemsMenu'); 164 break 165 } 166 } 167 168 const onContextMenu = (e: MouseEvent, owner: 'editor' | 'tab', tabId?: number) => { 169 e.preventDefault(); 170 e.stopPropagation(); 171 eventBus.emit('eventContextMenu', { 172 event: e, 173 owner: owner, 174 tabId: tabId, 175 } as EventContextMenu) 176 } 177 178 // --------------------------------- 179 // import/export 180 // --------------------------------- 181 182 const importedTab = ref(null) 183 const importDialogVisible = ref(false) 184 185 const importHandler = (val: any) => { 186 if (importedTab.value == val) { 187 return 188 } 189 importedTab.value = val 190 } 191 192 const importTab = async () => { 193 let card: ApiDashboardTab 194 try { 195 if (importedTab.value?.json) { 196 card = importedTab.value.json as ApiDashboardTab; 197 } else if (importedTab.value.text) { 198 card = JSON.parse(importedTab.value.text) as ApiDashboardTab; 199 } 200 } catch { 201 ElMessage({ 202 title: t('Error'), 203 message: t('message.corruptedJsonFormat'), 204 type: 'error', 205 duration: 2000 206 }); 207 return 208 } 209 const res = await core.importTab(card); 210 if (res) { 211 ElMessage({ 212 title: t('Success'), 213 message: t('message.importedSuccessful'), 214 type: 'success', 215 duration: 2000 216 }) 217 } 218 importDialogVisible.value = false 219 } 220 221 defineOptions({ 222 inheritAttrs: false 223 }) 224 </script> 225 226 <template> 227 228 <div class="dashboard-container" 229 v-if="!loading" 230 :style="getTabStyle()" 231 @contextmenu="onContextMenu($event, 'editor', undefined)"> 232 233 <ElTabs 234 v-model="activeTabIdx" 235 class="ml-20px" 236 :lazy="true"> 237 <ElTabPane 238 v-for="(tab, index) in core.tabs" 239 :label="tab.name" 240 :key="index" 241 :disabled="!tab.enabled" 242 :class="[{'gap': tab.gap}]" 243 :lazy="true" 244 @contextmenu="onContextMenu($event, 'tab', tab.id)" 245 > 246 <ViewTab :tab="tab" :key="index" :core="core"/> 247 </ElTabPane> 248 </ElTabs> 249 250 <!-- main menu --> 251 <DraggableContainer :name="'editor-main'"> 252 <template #header> 253 <div class="w-[100%]"> 254 <div style="float: left">Main menu</div> 255 <div style="float: right; text-align: right"> 256 <a href="#" @click.prevent.stop='toggleMenu("tabs")'> 257 <Icon icon="vaadin:tabs" class="mr-5px" @click.prevent.stop='toggleMenu("tabs")'/> 258 </a> 259 <a href="#" class="mr-5px" @click.prevent.stop='toggleMenu("cards")'> 260 <Icon icon="material-symbols:cards-outline"/> 261 </a> 262 <a href="#" @click.prevent.stop='toggleMenu("cardItems")'> 263 <Icon icon="icon-park-solid:add-item"/> 264 </a> 265 </div> 266 </div> 267 </template> 268 <template #default> 269 <ElTabs v-model="core.mainTab"> 270 <!-- main --> 271 <ElTabPane :label="$t('dashboard.mainTab')" name="main"> 272 <template #label> 273 <Icon icon="wpf:maintenance"/> 274 </template> 275 <TabSettings v-if="core.current" :core="core"/> 276 </ElTabPane> 277 <!-- /main --> 278 279 <!-- tabs --> 280 <ElTabPane :label="$t('dashboard.tabsTab')" name="tabs"> 281 <template #label> 282 <Icon icon="vaadin:tabs"/> 283 </template> 284 <TabEditor v-if="core.current && activeTab" :tab="activeTab" :core="core"/> 285 <ElEmpty v-if="!core.tabs.length" :rows="5"> 286 <ElButton type="primary" @click="createTab()"> 287 {{ t('dashboard.addNewTab') }} 288 </ElButton> 289 </ElEmpty> 290 </ElTabPane> 291 <!-- /tabs --> 292 293 <!-- cards --> 294 <ElTabPane :label="$t('dashboard.cardsTab')" name="cards"> 295 <template #label> 296 <Icon icon="material-symbols:cards-outline"/> 297 </template> 298 <TabCard v-if="core.current && activeTab" :tab="activeTab" :core="core"/> 299 <ElEmpty v-if="!core.tabs.length" :rows="5"> 300 <ElButton type="primary" @click="createTab()"> 301 {{ t('dashboard.addNewTab') }} 302 </ElButton> 303 </ElEmpty> 304 </ElTabPane> 305 <!-- /cards --> 306 307 <!-- cardItems --> 308 <ElTabPane :label="$t('dashboard.cardItemsTab')" name="cardItems"> 309 <template #label> 310 <Icon icon="icon-park-solid:add-item"/> 311 </template> 312 <TabCardItem v-if="core.current && activeTab && activeCard" :card="activeCard" :core="core"/> 313 <ElEmpty v-if="!core.tabs.length" :rows="5"> 314 <ElButton type="primary" @click="createTab()"> 315 {{ t('dashboard.addNewTab') }} 316 </ElButton> 317 </ElEmpty> 318 <ElEmpty v-if="core.tabs.length && !(core.activeCard >= 0)" :rows="5"> 319 <ElButton type="primary" @click="addCard()"> 320 {{ t('dashboard.addNewCard') }} 321 </ElButton> 322 </ElEmpty> 323 </ElTabPane> 324 <!-- /cardItems --> 325 326 </ElTabs> 327 </template> 328 </DraggableContainer> 329 <!-- /main menu --> 330 331 <!-- card list window --> 332 <CardListWindow v-if="core.current && activeTab" :core="core"/> 333 <!-- /card list window --> 334 335 <!-- import dialog --> 336 <Dialog v-model="importDialogVisible" :title="t('main.dialogImportTitle')" :maxHeight="400" width="80%" 337 custom-class> 338 <JsonEditor @change="importHandler"/> 339 <template #footer> 340 <ElButton type="primary" @click="importTab()" plain>{{ t('main.import') }}</ElButton> 341 <ElButton @click="importDialogVisible = false">{{ t('main.closeDialog') }}</ElButton> 342 </template> 343 </Dialog> 344 <!-- /import dialog --> 345 346 </div> 347 348 </template> 349 350 <style lang="less"> 351 352 353 /* Track */ 354 ::-webkit-scrollbar-track { 355 background: #f1f1f1; 356 } 357 358 .dashboard-container { 359 position: relative; 360 min-height: calc(100vh - 87px); 361 } 362 363 p { 364 display: block; 365 margin-block-start: 1em; 366 margin-block-end: 1em; 367 margin-inline-start: 0; 368 margin-inline-end: 0; 369 } 370 371 h1 { 372 display: block; 373 font-size: 2em; 374 margin-block-start: 0.67em; 375 margin-block-end: 0.67em; 376 margin-inline-start: 0; 377 margin-inline-end: 0; 378 font-weight: 700; 379 } 380 381 h2 { 382 display: block; 383 font-size: 1.5em; 384 margin-block-start: 0.67em; 385 margin-block-end: 0.67em; 386 margin-inline-start: 0; 387 margin-inline-end: 0; 388 font-weight: 700; 389 } 390 391 html { 392 line-height: 1.15; 393 } 394 395 .el-tabs { 396 height: inherit; 397 height: -webkit-fill-available; 398 } 399 400 html.dark { 401 .draggable-container { 402 &.container-editor-main { 403 404 405 .el-card { 406 .el-divider__text { 407 background-color: var(--el-bg-color-overlay); 408 } 409 } 410 } 411 412 &.container-editor-cards, 413 &.container-editor-tabs, 414 &.container-editor-card-items, 415 &.container-editor-main, 416 &.container-frame-editor { 417 .draggable-container-content, 418 .el-divider__text { 419 background-color: hsl(230, 7%, 17%); 420 } 421 } 422 } 423 424 } 425 426 // custom style 427 .draggable-container.container-editor-main { 428 .el-main { 429 padding: 2px !important; 430 } 431 432 .el-card__header { 433 padding: 18px 20px !important; 434 } 435 436 .el-card { 437 --el-card-padding: 2px 5px; 438 } 439 440 .el-form-item--small { 441 margin-bottom: 5px; 442 } 443 444 .el-divider--horizontal { 445 margin: 11px 0; 446 } 447 448 .el-col.el-col-12 { 449 padding-right: 6px; 450 padding-left: 6px; 451 } 452 453 .el-menu-item { 454 padding: 0 2px; 455 line-height: 14px !important; 456 height: 14px !important; 457 } 458 459 .el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item, .el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item-group__title, .el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-sub-menu__title { 460 padding-left: 2px; 461 } 462 463 .el-col.el-col-24.is-guttered { 464 padding: 0 !important; 465 } 466 467 .el-button { 468 margin-bottom: 10px !important; 469 } 470 471 .el-collapse-item__content { 472 padding-bottom: 10px !important; 473 } 474 } 475 476 .container-editor-main { 477 .draggable-container-content { 478 padding: 0 10px; 479 } 480 } 481 482 .draggable-container { 483 &.container-editor-cards, 484 &.container-editor-tabs, 485 &.container-editor-card-items, 486 &.container-editor-main, 487 &.container-frame-editor { 488 .draggable-container-header { 489 font-size: 12px; 490 } 491 } 492 } 493 494 .draggable-container { 495 &.container-editor-cards, 496 &.container-editor-tabs, 497 &.container-editor-card-items, 498 &.container-frame-editor { 499 .el-menu-item { 500 padding-left: 5px !important; 501 padding-right: 5px !important; 502 line-height: 30px; 503 height: 30px; 504 font-size: 12px; 505 } 506 507 .el-menu-item * { 508 vertical-align: baseline; 509 } 510 } 511 } 512 513 // menu 514 .menu-item { 515 display: flex; 516 justify-content: space-between; 517 align-items: center; 518 } 519 520 .buttons { 521 display: none; 522 position: absolute; 523 right: 0; 524 background: var(--el-bg-color); 525 } 526 527 .el-menu-item:hover .buttons { 528 display: block; 529 color: red; 530 } 531 </style>