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>