SplitScreenMode.vue 10.8 KB
<script setup lang="ts">
  import { PageWrapper } from '/@/components/Page';
  import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
  import { computed, onMounted, reactive, ref, unref, watch } from 'vue';
  import { Tabs, Row, Col, Spin, Button, Pagination, Empty } from 'ant-design-vue';
  import { cameraPage } from '/@/api/camera/cameraManager';
  import { CameraRecord } from '/@/api/camera/model/cameraModel';
  import { videoPlay as VideoPlay } from 'vue3-video-play';
  import 'vue3-video-play/dist/style.css';
  import { useFullscreen } from '@vueuse/core';
  import CameraDrawer from './CameraDrawer.vue';
  import { useDrawer } from '/@/components/Drawer';
  import { AccessMode, MediaType, PageMode } from './config.data';
  import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
  import { isDef } from '/@/utils/is';
  import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';

  type CameraRecordItem = CameraRecord & {
    canPlay?: boolean;
    type?: string;
    isTransform?: boolean;
  };

  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 options = reactive({
    width: '200px',
    height: '200px',
    color: '#409eff',
    muted: false, //静音
    webFullScreen: false,
    autoPlay: true, //自动播放
    currentTime: 0,
    loop: false, //循环播放
    mirror: false, //镜像画面
    ligthOff: false, //关灯模式
    volume: 0.3, //默认音量大小
    control: true, //是否显示控制器
    type: 'm3u8',
    controlBtns: [
      'audioTrack',
      'quality',
      'speedRate',
      'volume',
      'setting',
      'pip',
      'pageFullScreen',
      'fullScreen',
    ],
  });

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

  const getColLayout = computed(() => {
    const totalSpan = 24;
    return totalSpan / pagination.colNumber;
  });

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

      for (const item of items) {
        // await beforeVideoPlay(item);
        (item as CameraRecordItem).isTransform = false;
        beforeVideoPlay(item);
      }
      cameraList.value = items;
    } catch (error) {
    } finally {
      loading.value = false;
    }
  };
  const getMediaType = (suffix: string) => {
    return suffix === MediaType.M3U8 ? suffix : `video/${suffix}`;
  };

  const beforeVideoPlay = async (record: CameraRecordItem) => {
    let reg = /(?:.*)(?<=\.)/;
    if (record.accessMode === AccessMode.ManuallyEnter) {
      if (record.videoUrl) {
        const type = record.videoUrl.replace(reg, '');
        record.type = getMediaType(type);
        record.isTransform = true;
      }
    }
    if (record.accessMode === AccessMode.Streaming) {
      try {
        const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
        const type = url.replace(reg, '');
        const index = unref(cameraList).findIndex((item) => item.id === record.id);
        if (~index) {
          const oldRecord = unref(cameraList).at(index)!;
          unref(cameraList)[index] = {
            ...oldRecord,
            videoUrl: url,
            type: getMediaType(type),
            isTransform: true,
          };
        }
      } catch (error) {
      } finally {
        const index = unref(cameraList).findIndex((item) => item.id === record.id);
        if (~index) unref(cameraList)[index].isTransform = true;
      }
    }
  };

  const handleSwitchLayoutWay = (pageSize: number, layout: number) => {
    pagination.colNumber = layout;
    pagination.pageSize = pageSize;
    pagination.page = 1;
    getCameraList();
  };

  const { enter, isFullscreen } = useFullscreen(videoContainer);

  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 handleLoadStart = (record: CameraRecordItem) => {
    const index = unref(cameraList).findIndex((item) => item.id === record.id);
    setTimeout(() => {
      ~index &&
        !unref(cameraList).at(index)!.canPlay &&
        (unref(cameraList).at(index)!.canPlay = false);
    }, 30000);
  };

  const handleLoadData = (record: CameraRecordItem) => {
    const index = unref(cameraList).findIndex((item) => item.id === record.id);
    ~index && (unref(cameraList).at(index)!.canPlay = true);
  };

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

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

  onMounted(() => {
    getCameraList();
  });
</script>

<template>
  <div>
    <PageWrapper dense contentFullHeight contentClass="flex">
      <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
      <section class="p-4 pl-9 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">
          <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="(total) => `共 ${total} 条`"
              />
            </div>
          </div>
          <div class="flex items-center gap-4">
            <div class="flex">
              <Button type="primary" @click="handleAddCamera">新增视频</Button>
            </div>
            <Tabs type="card" v-model:activeKey="activeKey" @change="handleChangeMode">
              <Tabs.TabPane :key="PageMode.SPLIT_SCREEN_MODE" tab="分屏模式" />
              <Tabs.TabPane :key="PageMode.LIST_MODE" tab="列表模式" />
              <Tabs.TabPane :key="PageMode.FULL_SCREEN_MODE" tab="全屏" />
            </Tabs>
          </div>
        </div>
        <section ref="videoContainer" class="bg-light-50 flex-auto">
          <Spin :spinning="loading" class="h-full">
            <Empty
              class="h-full flex flex-col justify-center items-center"
              v-if="!cameraList.length"
            />
            <Row :gutter="16" class="h-full mx-0">
              <Col
                v-for="item in cameraList"
                :key="item.id"
                :style="{ height: `${100 / pagination.colNumber}%` }"
                class="h-1/2 !px-0 !flex justify-center items-center gap-2"
                :span="getColLayout"
              >
                <div class="box-border w-full h-full p-3">
                  <div class="bg-black w-full h-full overflow-hidden relative video-container">
                    <Spin v-show="!item.isTransform" :spinning="!item.isTransform">
                      <div class="bg-black text-light-50"> </div>
                    </Spin>
                    <VideoPlay
                      v-show="item.isTransform"
                      @loadstart="handleLoadStart(item)"
                      @loadeddata="handleLoadData(item)"
                      v-bind="options"
                      :src="item.videoUrl"
                      :title="item.name"
                      :type="item.type"
                    />
                    <div
                      v-if="item.isTransform && isDef(item.canPlay) && !item.canPlay"
                      class="video-container-error-msk absolute top-0 left-0 text-lg w-full h-full text-light-50 flex justify-center items-center z-50 bg-black"
                    >
                      视频加载出错了!
                    </div>
                    <div
                      class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center"
                      style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
                    >
                      <span>{{ item.name }}</span>
                    </div>
                  </div>
                </div>
              </Col>
            </Row>
          </Spin>
        </section>
      </section>
    </PageWrapper>
    <CameraDrawer @register="registerDrawer" @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%;
  }

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

  .split-screen-mode:deep(.d-player-wrap) {
    width: 100%;
    height: 100%;
  }

  .split-screen-mode:deep(.ant-tabs-tab-active) {
    border-bottom: 1px solid #eee;
  }

  .split-screen-mode:deep(video) {
    position: absolute;
    height: calc(100%) !important;
  }

  .split-screen-mode:deep(.d-player-control) {
    z-index: 99;
  }

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

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

      .video-container-error-msk {
        // opacity: 0;
        // visibility: hidden;
        color: #000;
      }
    }
  }
</style>