SplitScreenMode.vue 12.1 KB
<script setup lang="ts">
  import { PageWrapper } from '/@/components/Page';
  import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
  import { onMounted, reactive, Ref, ref, unref, watch } from 'vue';
  import { Spin, Button, Pagination, Space, List } from 'ant-design-vue';
  import { cameraPage, closeFlvPlay } from '/@/api/camera/cameraManager';
  import { CameraRecord } from '/@/api/camera/model/cameraModel';
  import { useFullscreen } from '@vueuse/core';
  import CameraDrawer from './CameraDrawer.vue';
  import GBTDrawer from './components/GBTDrawer.vue';
  import { useDrawer } from '/@/components/Drawer';
  import { CameraPermission, getPlayUrl, PageMode } from './config.data';
  import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
  import { buildUUID } from '/@/utils/uuid';
  import { getBoundingClientRect } from '/@/utils/domUtils';
  import { Authority } from '/@/components/Authority';
  import { isRtspProtocol } from '/@/components/Video/src/utils';
  import { useFingerprint } from '/@/utils/useFingerprint';
  import { GetResult } from '@fingerprintjs/fingerprintjs';
  import XGPlayer from '/@/components/Video/src/XGPlayer.vue';
  import { isNullOrUnDef } from '/@/utils/is';
  import { useI18n } from '/@/hooks/web/useI18n';
  const { t } = useI18n();

  const props = defineProps({
    mode: {
      type: String,
      default: PageMode.SPLIT_SCREEN_MODE,
    },
  });
  type CameraRecordItem = CameraRecord & {
    placeholder?: boolean;
    canPlay?: boolean;
    isTransform?: boolean;
    withToken?: boolean;
    playSourceUrl?: string;
    streamType?: string;
    failed?: boolean;
    failedMessage?: string;
  };

  const emit = defineEmits(['switchMode']);
  const organizationIdTreeRef = ref(null);
  const videoContainer = ref<Nullable<HTMLDivElement>>(null);
  const activeKey = ref(PageMode.SPLIT_SCREEN_MODE);
  const cameraList = ref<CameraRecordItem[]>([]);
  const organizationId = ref<Nullable<string>>(null);
  const loading = ref(false);
  const pagination = reactive({
    page: 1,
    pageSize: 4,
    colNumber: 2,
    total: 0,
  });

  const fingerprintResult = ref<Nullable<GetResult>>(null);

  // 树形选择器
  const handleSelect = (orgId: string) => {
    organizationId.value = orgId;
    getCameraList();
  };

  const getCameraList = async () => {
    try {
      cameraList.value = [];
      loading.value = true;
      let { items, total } = await cameraPage({
        page: pagination.page,
        pageSize: pagination.pageSize,
        organizationId: unref(organizationId)!,
      });
      pagination.total = total;

      const result = await getResult();
      fingerprintResult.value = result;
      if (items.length < pagination.pageSize) {
        const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({
          id: buildUUID(),
          placeholder: true,
        }));
        items = [...items, ...fillArr];
      }
      cameraList.value = items;

      for (const item of unref(cameraList)) {
        (item as CameraRecordItem).isTransform = false;
        beforeVideoPlay(item as CameraRecordItem);
      }
    } catch (error) {
    } finally {
      loading.value = false;
    }
  };

  const { getResult } = useFingerprint();
  const beforeVideoPlay = async (record: CameraRecordItem) => {
    if (isNullOrUnDef(record.accessMode)) return;
    const { url, type, withToken, failed, failedMessage } = (await getPlayUrl(record)) || {};
    record.playSourceUrl = url;
    record.streamType = type;
    record.isTransform = true;
    record.withToken = withToken;
    record.failedMessage = failedMessage;
    record.failed = failed;
  };

  const gridLayout = ref({ gutter: 1, column: 2 });

  const handleSwitchLayoutWay = (pageSize: number, layout: number) => {
    gridLayout.value = { gutter: 1, column: Math.sqrt(pageSize) };
    pagination.colNumber = layout;
    pagination.pageSize = pageSize;
    pagination.page = 1;
    getCameraList();
  };

  const { enter, isFullscreen } = useFullscreen(videoContainer as Ref<HTMLDivElement>);

  const handleFullScreen = () => {
    enter();
  };

  watch(isFullscreen, () => {
    activeKey.value = unref(isFullscreen) ? PageMode.FULL_SCREEN_MODE : PageMode.SPLIT_SCREEN_MODE;
  });

  const handleChangeMode = (activeKey: string) => {
    if (activeKey === PageMode.FULL_SCREEN_MODE) {
      handleFullScreen();
    } else if (activeKey === PageMode.LIST_MODE) {
      emit('switchMode', PageMode.LIST_MODE);
    }
  };

  const [registerDrawer, { openDrawer }] = useDrawer();

  //GBT28181 Drawer
  const [registerGBTDrawer, { openDrawer: openGBTDrawer }] = useDrawer();

  const handleAddCamera = () => {
    openDrawer(true, {
      isUpdate: false,
    });
  };

  // 批量新增GBT28181国标
  const handleOpenGBTDrawer = () => {
    openGBTDrawer(true);
  };

  const handleCloseFlvPlayUrl = async (record: CameraRecordItem) => {
    if (isRtspProtocol(record.videoUrl)) {
      closeFlvPlay(record.videoUrl, unref(fingerprintResult)!.visitorId!);
    }
  };

  onMounted(() => {
    getCameraList();
  });

  const listEl = ref();
  onMounted(() => {
    const clientHeight = document.documentElement.clientHeight;
    const rect = getBoundingClientRect(unref(listEl)!.$el!) as DOMRect;
    // list pading top 8 maring-top 8 extra slot 56
    const listContainerMarginBottom = 16;
    const listContainerHeight = clientHeight - rect.top - listContainerMarginBottom;
    const listContainerEl = (unref(listEl)!.$el as HTMLElement).querySelector(
      '.ant-spin-container'
    ) as HTMLElement;
    listContainerEl && (listContainerEl.style.height = listContainerHeight + 'px');
  });

  const getTotal = (total: number) => t('component.table.total', { total });
</script>

<template>
  <div>
    <PageWrapper dense contentFullHeight contentClass="flex">
      <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
      <section class="p-4 split-screen-mode flex flex-col flex-auto w-3/4 xl:w-4/5">
        <div class="p-3 bg-light-50 flex justify-between mb-4 dark:bg-dark-900">
          <div class="flex gap-4 cursor-pointer items-center">
            <div
              class="w-8 h-8 flex justify-center items-center"
              :style="{ color: pagination.colNumber === 1 ? '#1890ff' : '' }"
              @click="handleSwitchLayoutWay(1, 1)"
            >
              <SvgIcon class="text-2xl" prefix="iconfont" name="grid-one" />
            </div>
            <div
              class="w-8 h-8 flex justify-center items-center"
              :style="{ color: pagination.colNumber === 2 ? '#1890ff' : '' }"
              @click="handleSwitchLayoutWay(4, 2)"
            >
              <SvgIcon class="text-2xl" prefix="iconfont" name="grid-four" />
            </div>
            <div
              class="w-8 h-8 flex justify-center items-center"
              :style="{ color: pagination.colNumber === 3 ? '#1890ff' : '' }"
              @click="handleSwitchLayoutWay(9, 3)"
            >
              <SvgIcon class="text-2xl" prefix="iconfont" name="grid-nine" />
            </div>
            <div>
              <Pagination
                v-model:current="pagination.page"
                :total="pagination.total"
                :page-size="pagination.pageSize"
                :show-size-changer="false"
                @change="getCameraList"
                :simple="true"
                :show-quick-jumper="true"
                :show-total="getTotal"
              />
            </div>
          </div>
          <div class="flex items-center gap-4">
            <div class="flex">
              <Authority :value="CameraPermission.CREATE">
                <Button type="primary" @click="handleOpenGBTDrawer">
                  {{ t('video.configuration.batchCreateGBTText') }}
                </Button>
              </Authority>
              <div style="width: 17px"></div>
              <Authority :value="CameraPermission.CREATE">
                <Button type="primary" @click="handleAddCamera">
                  {{ t('video.configuration.createText') }}
                </Button>
              </Authority>
            </div>
            <Space>
              <Button
                v-if="props.mode !== PageMode.SPLIT_SCREEN_MODE"
                type="primary"
                @click="handleChangeMode(PageMode.SPLIT_SCREEN_MODE)"
              >
                {{ t('video.configuration.createText') }}
              </Button>
              <Button type="primary" @click="handleChangeMode(PageMode.LIST_MODE)">
                {{ t('video.configuration.listModeText') }}
              </Button>
              <Button type="primary" @click="handleChangeMode(PageMode.FULL_SCREEN_MODE)">
                {{ t('video.configuration.fullscreenText') }}
              </Button>
            </Space>
          </div>
        </div>
        <section ref="videoContainer" class="flex-auto">
          <List
            ref="listEl"
            :loading="loading"
            :data-source="cameraList"
            class="bg-light-50 w-full !h-full dark:bg-dark-900 split-mode-list full"
            :grid="(gridLayout as any)"
            :style="{ '--height': `${100 / pagination.colNumber}%` }"
          >
            <template #renderItem="{ item }: { item: CameraRecordItem }">
              <List.Item>
                <div class="box-border w-full !h-full p-1px">
                  <div
                    v-if="item.placeholder"
                    class="bg-black w-full !h-full overflow-hidden relative"
                  >
                  </div>
                  <div
                    v-if="!item.placeholder"
                    class="bg-black w-full !h-full overflow-hidden relative video-container"
                  >
                    <Spin
                      class="!absolute top-1/2 left-1/2 transform -translate-1/2"
                      v-show="!item.isTransform"
                      :spinning="!item.isTransform"
                    />
                    <XGPlayer
                      v-if="item.isTransform && !item.failed"
                      :url="item.playSourceUrl"
                      :stream-type="item.streamType"
                      :with-token="item.withToken"
                      :config="{ width: '100%', height: '100%' }"
                      @on-unmounted="handleCloseFlvPlayUrl(item)"
                    />
                    <div
                      class="absolute pointer-events-none top-0 left-0 w-full h-full z-99 text-light-50 flex justify-center items-center text-lg"
                    >
                      <span>{{ item.failedMessage }}</span>
                    </div>
                    <div
                      class="video-container-mask pointer-events-none absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center"
                      style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
                    >
                      <span>{{ item.name }}</span>
                    </div>
                  </div>
                </div>
              </List.Item>
            </template>
          </List>
        </section>
      </section>
    </PageWrapper>
    <CameraDrawer @register="registerDrawer" @success="getCameraList" />
    <GBTDrawer @register="registerGBTDrawer" @success="getCameraList" />
  </div>
</template>

<style scoped lang="less">
  .split-screen-mode:deep(.ant-tabs-bar) {
    margin-bottom: 0;
  }

  .split-screen-mode:deep(.ant-spin-nested-loading) {
    height: 100%;
  }

  .full {
    :deep(.ant-spin-container) {
      height: 100% !important;
    }
  }

  .video-container {
    .video-container-mask {
      opacity: 0;
      transition: opacity 0.5;
      pointer-events: none;
    }

    &:hover {
      .video-container-mask {
        opacity: 1;
      }

      .video-container-error-msk {
        color: #000;
      }
    }
  }

  .split-mode-list:deep(.ant-row) {
    width: 100%;
    height: 100%;

    > div {
      height: var(--height);
    }
  }

  .split-mode-list:deep(.ant-list-item) {
    width: 100%;
    height: 100%;
  }

  .split-mode-list:deep(.ant-col) {
    width: 100%;
    height: 100%;
  }

  .split-mode-lis:deep(.ant-spin-container:fullscreen) {
    height: 100% !important;
  }
</style>