Bläddra i källkod

替换DashBoard

344080Wuyunfeng 2 år sedan
förälder
incheckning
e55945f0b4

+ 1 - 0
package.json

@@ -60,6 +60,7 @@
     "uid": "2.0.2",
     "url-join": "5.0.0",
     "vue": "3.3.4",
+    "vue-echarts": "^6.6.8",
     "vue-hooks-plus": "1.8.6",
     "vue-konva": "3.0.2",
     "vue-router": "4.1.2",

+ 27 - 0
pnpm-lock.yaml

@@ -107,6 +107,9 @@ dependencies:
   vue:
     specifier: 3.3.4
     version: 3.3.4
+  vue-echarts:
+    specifier: ^6.6.8
+    version: 6.6.9(echarts@5.3.3)(vue@3.3.4)
   vue-hooks-plus:
     specifier: 1.8.6
     version: 1.8.6(vue@3.3.4)
@@ -6863,6 +6866,10 @@ packages:
     dev: false
     optional: true
 
+  /resize-detector@0.3.0:
+    resolution: {integrity: sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==}
+    dev: false
+
   /resolve-dir@1.0.1:
     resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==}
     engines: {node: '>=0.10.0'}
@@ -8231,6 +8238,26 @@ packages:
       vue: 3.3.4
     dev: false
 
+  /vue-echarts@6.6.9(echarts@5.3.3)(vue@3.3.4):
+    resolution: {integrity: sha512-mojIq3ZvsjabeVmDthhAUDV8Kgf2Rr/X4lV4da7gEFd1fP05gcSJ0j7wa7HQkW5LlFmF2gdCJ8p4Chas6NNIQQ==}
+    requiresBuild: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.5
+      '@vue/runtime-core': ^3.0.0
+      echarts: ^5.4.1
+      vue: ^2.6.12 || ^3.1.1
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+      '@vue/runtime-core':
+        optional: true
+    dependencies:
+      echarts: 5.3.3
+      resize-detector: 0.3.0
+      vue: 3.3.4
+      vue-demi: 0.13.11(vue@3.3.4)
+    dev: false
+
   /vue-eslint-parser@8.3.0(eslint@8.20.0):
     resolution: {integrity: sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}

BIN
src/assets/images/date-to.png


+ 31 - 0
src/views/dashboard/home/Home.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="home-page">
+    <!-- <HomeHeader style="z-index: 2" /> -->
+    <div class="flex">
+      <CameraInfo class="flex-1" />
+      <AlgoData />
+    </div>
+    <!-- <MoreMask class="mask-pos" /> -->
+  </div>
+</template>
+
+<script setup lang="ts">
+  // import { ref } from "vue";
+  // import HomeHeader from './components/header/PageHeader.vue';
+  import CameraInfo from './components/CameraInfo.vue';
+  import AlgoData from './components/AlgoDataPanel.vue';
+  // import MoreMask from './components/moreFeatureIMark/index.vue';
+</script>
+
+<style scoped>
+  .home-page {
+    width: 100%;
+    height: 100%;
+    padding-bottom: 24px;
+    background: #ffffff;
+  }
+
+  .mask-pos {
+    margin-top: 16px;
+  }
+</style>

+ 93 - 0
src/views/dashboard/home/components/AlgoCensusTabs.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="flex justify-between" style="width: 100%">
+    <div class="text-tabs">
+      <div v-for="item in timeTypeList" class="tab-item" @click="onClickTab(item)">
+        <span> {{ item.label }} </span>
+        <div v-if="activeTab == item.value" class="tab-underline"></div>
+      </div>
+    </div>
+    <el-date-picker
+      v-model="timeSlot"
+      type="daterange"
+      start-placeholder="开始日期"
+      end-placeholder="结束日期"
+      @change="onDateChnage"
+    >
+      <template #range-separator>
+        <img src="@/assets/images/date-to.png" />
+      </template>
+    </el-date-picker>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { TimeTabEnum, timeTypeList } from '../types';
+
+  const activeTab = ref<TimeTabEnum>(TimeTabEnum.DAY);
+
+  const emits = defineEmits(['checkTab', 'changeDateRange']);
+
+  const timeSlot = ref('');
+
+  const resetTime = () => {
+    timeSlot.value = '';
+  };
+
+  const onClickTab = (tabItem: any) => {
+    activeTab.value = tabItem.value;
+    emits('checkTab', tabItem.value);
+    if (tabItem.value !== 'range') {
+      resetTime();
+    }
+  };
+
+  const onDateChnage = (date) => {
+    onClickTab(TimeTabEnum.RANGE);
+    console.log(date);
+  };
+</script>
+
+<style scoped>
+  .text-tabs {
+    width: 132px;
+    height: 26px;
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .tab-item {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 14px;
+    font-weight: 400;
+    color: #2e2e2e;
+    line-height: 20px;
+    cursor: pointer;
+  }
+
+  .tab-underline {
+    width: 100%;
+    height: 2px;
+    background: #1677ff;
+  }
+
+  :deep(.el-date-editor .el-range__icon) {
+    display: none;
+  }
+  :deep(.el-date-editor .el-range__close-icon--hidden) {
+    display: none;
+  }
+
+  :deep(.el-range-editor.el-input__wrapper) {
+    width: 236px;
+    flex: unset;
+    height: 32px;
+  }
+
+  :deep(.el-input__wrapper) {
+    padding: 0;
+  }
+</style>

+ 165 - 0
src/views/dashboard/home/components/AlgoDataPanel.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="algo-data">
+    <span class="algo-tit">算法数据分析</span>
+    <CensusTabs @check-tab="onCheckTab" />
+    <v-chart class="chart" :option="option" />
+    <div class="stat-show">
+      <ViolationStatItem :data="violationHandleCounts[0]" />
+      <div class="stat-divider"></div>
+      <ViolationStatItem :data="violationHandleCounts[1]" />
+      <div class="stat-divider"></div>
+      <ViolationStatItem :data="violationHandleCounts[2]" />
+      <div class="stat-divider"></div>
+      <ViolationStatItem :data="violationHandleCounts[3]" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import CensusTabs from './AlgoCensusTabs.vue';
+  import ViolationStatItem from './ViolationStatItem.vue';
+  import { TimeTabEnum, violationHandleCounts } from '../types';
+  import { use } from 'echarts/core';
+  import { CanvasRenderer } from 'echarts/renderers';
+  import { PieChart } from 'echarts/charts';
+  import { TooltipComponent, LegendComponent } from 'echarts/components';
+  import VChart from 'vue-echarts';
+
+  use([CanvasRenderer, PieChart, TooltipComponent, LegendComponent]);
+
+  const data = [
+    { value: 335, name: '人员闯入' },
+    { value: 310, name: '未穿反光背心' },
+    { value: 2, name: '明火烟雾' },
+    { value: 135, name: '机翼保护垫' },
+    { value: 148, name: '工装未归位' },
+    { value: 335, name: '人员闯入1' },
+    { value: 310, name: '未穿反光背心1' },
+    { value: 2, name: '明火烟雾1' },
+    { value: 135, name: '机翼保护垫1' },
+    { value: 148, name: '工装未归位1' },
+  ];
+
+  const option = ref({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{a} <br/>{b} : {c} ({d}%)',
+    },
+    legend: {
+      orient: 'horizontial',
+      x: 'center',
+      y: 'bottom',
+      icon: 'circle',
+      width: '80%',
+      height: '28%',
+      type: 'scroll',
+      data: data.map((item) => item.name),
+      formatter: function (name) {
+        let total = 0;
+        let target;
+        for (let i = 0; i < data.length; i++) {
+          total += data[i].value;
+          if (data[i].name === name) {
+            target = data[i].value;
+          }
+        }
+        var arr = [
+          '{a|' + name + '}',
+          '{b|' + ' | ' + ((target / total) * 100).toFixed(0) + '%}\n',
+        ];
+        return arr.join('  ');
+      },
+      textStyle: {
+        padding: [8, 0, 0, 0],
+        fontSize: 14,
+        rich: {
+          a: {
+            fontSize: 15,
+          },
+          b: {
+            fontSize: 15,
+            color: '#c1c1c1',
+          },
+        },
+      },
+    },
+    series: [
+      {
+        name: '违规统计',
+        type: 'pie',
+        radius: ['40%', '65%'],
+        center: ['50%', '40%'],
+        labelLine: {
+          show: false,
+        },
+
+        label: {
+          show: false,
+          position: 'center',
+        },
+        data,
+        itemStyle: {
+          borderColor: '#fff',
+          borderWidth: 5,
+        },
+        emphasis: {
+          label: {
+            show: true,
+            fontSize: 20,
+            fontWeight: 'bold',
+          },
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  });
+
+  const timeTab = ref<TimeTabEnum>(TimeTabEnum.DAY);
+
+  const onCheckTab = (tab) => {
+    timeTab.value = tab;
+  };
+</script>
+
+<style scoped>
+  .algo-data {
+    width: 484px;
+    padding: 12px 27px;
+    border-left: 2px solid #e8e8e8;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .chart {
+    width: 100%;
+    height: 450px;
+  }
+
+  .algo-tit {
+    font-size: 16px;
+    font-weight: 500;
+    margin-bottom: 10px;
+    line-height: 44px;
+    color: #2e2e2e;
+  }
+
+  .stat-show {
+    width: 100%;
+    margin-top: 32px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .stat-divider {
+    width: 1px;
+    height: 40px;
+    background: #e9e9e9;
+  }
+</style>

+ 71 - 0
src/views/dashboard/home/components/CameraInfo.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="camera-info">
+    <span class="info-tit">相机视频流</span>
+    <div>
+      <el-select v-model="selectedCamera" />
+    </div>
+    <div class="video-block">
+      <LiveVideo :url="`http://172.16.23.243/tianyan11/live/C12-200-11.flv`" />
+    </div>
+    <div class="flex">
+      <span class="algo-text">相关算法:</span>
+      <div class="tag-list">
+        <el-tag v-for="item in 20" class="algo-name"> Tag {{ item }} </el-tag>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import LiveVideo from './LiveVideo.vue';
+
+  const selectedCamera = ref('');
+</script>
+
+<style scoped>
+  .camera-info {
+    padding: 12px 30px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .info-tit {
+    font-size: 16px;
+    font-weight: 500;
+    color: #2e2e2e;
+    line-height: 44px;
+    margin-bottom: 10px;
+  }
+
+  .video-block {
+    /* width: 889px;
+  height: 500px; */
+    min-width: 444.5;
+    width: 100%;
+    margin-top: 16px;
+    margin-bottom: 28px;
+    aspect-ratio: 1920/1080;
+  }
+
+  .algo-text {
+    font-size: 14px;
+    font-weight: 400;
+    color: #2e2e2e;
+    line-height: 20px;
+  }
+
+  .tag-list {
+    width: 60%;
+    display: flex;
+    margin-left: 16px;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+  }
+
+  .algo-name {
+    margin-right: 12px;
+    margin-bottom: 12px;
+  }
+</style>

+ 52 - 0
src/views/dashboard/home/components/LegendItem.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="legend-col">
+    <div class="legend-mark" :style="{ background: props.color }"></div>
+    <span class="legend-name">{{ props.name }}</span>
+    <div class="legend-divider"></div>
+    <span class="legend-count">{{ props.count }}</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+  const props = defineProps<{
+    color: string;
+    name: string;
+    count: string;
+  }>();
+</script>
+
+<style scoped>
+  .legend-col {
+    width: 120px;
+    height: 22px;
+    margin-bottom: 16px;
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+  }
+
+  .legend-mark {
+    width: 8px;
+    height: 8px;
+    margin-right: 8px;
+    border-radius: 50%;
+  }
+
+  .legend-name {
+    font-size: 14px;
+    font-weight: 400;
+    color: #6d6d6d;
+  }
+
+  .legend-divider {
+    width: 1px;
+    height: 12px;
+    margin: 0 8px;
+    background: #d9d9d9;
+  }
+
+  .legend-count {
+    font-size: 14px;
+    color: #bdbdbd;
+  }
+</style>

+ 73 - 0
src/views/dashboard/home/components/LiveVideo.vue

@@ -0,0 +1,73 @@
+<template>
+  <video id="video" autoplay muted loop class="video-js video-content">
+    <source :src="props.url" />
+  </video>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, onBeforeUnmount, watch } from 'vue';
+  import flvjs from 'flv.js';
+
+  const props = defineProps<{
+    url: string;
+  }>();
+
+  let player: flvjs.Player | null;
+
+  const initPlay = () => {
+    const videoElement = document.getElementById('video') as HTMLMediaElement;
+    player = flvjs.createPlayer({
+      type: 'flv',
+      isLive: true,
+      hasAudio: false,
+      url: props.url,
+    });
+    player.attachMediaElement(videoElement);
+    player.load();
+    player.on(flvjs.Events.METADATA_ARRIVED, () => {});
+    // player.play();
+    setTimeout(() => {
+      player?.play();
+    }, 50);
+  };
+
+  const destroyPlayer = () => {
+    if (player) {
+      player.pause();
+      player.unload();
+      player.detachMediaElement();
+      player.destroy();
+      player = null;
+    }
+  };
+
+  onMounted(() => {
+    initPlay();
+  });
+
+  //切换播放url
+  watch(
+    () => props.url,
+    () => {
+      destroyPlayer();
+      if (props.url) {
+        initPlay();
+      }
+    },
+    {
+      deep: true,
+    },
+  );
+
+  onBeforeUnmount(() => {
+    destroyPlayer();
+  });
+</script>
+
+<style scoped>
+  .video-content {
+    width: 100%;
+    height: 100%;
+    background-color: transparent !important;
+  }
+</style>

+ 35 - 0
src/views/dashboard/home/components/ViolationStatItem.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="stat-item">
+    <span class="stat-type" :style="{ color: props.data.color }">
+      {{ props.data.label }}
+    </span>
+    <span class="stat-count">25</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+  const props = defineProps<{
+    data: { label: string; value: string; color: string };
+  }>();
+</script>
+
+<style scoped>
+  .stat-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .stat-type {
+    font-size: 14px;
+    font-weight: 400;
+    line-height: 22px;
+    margin-bottom: 4px;
+  }
+
+  .stat-count {
+    font-size: 24px;
+    color: #2e2e2e;
+    line-height: 32px;
+  }
+</style>

+ 43 - 0
src/views/dashboard/home/types/index.ts

@@ -0,0 +1,43 @@
+export enum TimeTabEnum {
+  DAY = "day",
+  WEEK = "week",
+  MONTH = "month",
+  RANGE = "range",
+}
+
+export const timeTypeList = [
+  {
+    label: "今日",
+    value: TimeTabEnum.DAY,
+  },
+  {
+    label: "本周",
+    value: TimeTabEnum.WEEK,
+  },
+  {
+    label: "本月",
+    value: TimeTabEnum.MONTH,
+  },
+];
+
+export enum ViolationHandleStat {
+  UNTREAT = "untreat",
+  TREATED = "treated",
+  OVERTIME = "overtime",
+  LONGTIME = "longtime",
+}
+
+export const violationHandleCounts = [
+  { label: "未处理", value: ViolationHandleStat.UNTREAT, color: "#FAAD14" },
+  { label: "已处理", value: ViolationHandleStat.TREATED, color: "#52C41A" },
+  {
+    label: "超期未处理",
+    value: ViolationHandleStat.OVERTIME,
+    color: "#FF4D4F",
+  },
+  {
+    label: "长期未处理",
+    value: ViolationHandleStat.LONGTIME,
+    color: "#FF4D4F",
+  },
+];