浏览代码

首页布局

zhudie 2 年之前
父节点
当前提交
ede5ecb2c3

+ 1 - 1
package.json

@@ -46,7 +46,7 @@
     "cropperjs": "1.5.12",
     "dayjs": "1.11.4",
     "echarts": "5.3.3",
-    "element-plus": "2.3.6",
+    "element-plus": "2.7.1",
     "element-resize-detector": "1.2.4",
     "form-data": "^4.0.0",
     "html2canvas": "1.0.0",

+ 13 - 5
pnpm-lock.yaml

@@ -57,8 +57,8 @@ dependencies:
     specifier: 5.3.3
     version: 5.3.3
   element-plus:
-    specifier: 2.3.6
-    version: 2.3.6(vue@3.3.4)
+    specifier: 2.7.1
+    version: 2.7.1(vue@3.3.4)
   element-resize-detector:
     specifier: 1.2.4
     version: 1.2.4
@@ -823,6 +823,14 @@ packages:
       vue: 3.3.4
     dev: false
 
+  /@element-plus/icons-vue@2.3.1(vue@3.3.4):
+    resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
+    peerDependencies:
+      vue: ^3.2.0
+    dependencies:
+      vue: 3.3.4
+    dev: false
+
   /@esbuild-kit/cjs-loader@2.3.1:
     resolution: {integrity: sha512-ov6ALYD9xZSPoo5mmGOQtEC/b0xXeUlPy65p8aHMHLF4DfBEe8Y+iquH2lTDsy6Iskc1uMTadF+SVADTSTNJMA==}
     dependencies:
@@ -3444,13 +3452,13 @@ packages:
     resolution: {integrity: sha512-7EZCIDDraA2NUaHewLaAh6T63cZzgBmgDx/iiaeZ/pjSs36bOFEJ3hLIrn1TKCFhV0PEZZKu6qFPrxa/LGAzLg==}
     dev: true
 
-  /element-plus@2.3.6(vue@3.3.4):
-    resolution: {integrity: sha512-GLz0pXUYI2zRfIgyI6W7SWmHk6dSEikP9yR++hsQUyy63+WjutoiGpA3SZD4cGPSXUzRFeKfVr8CnYhK5LqXZw==}
+  /element-plus@2.7.1(vue@3.3.4):
+    resolution: {integrity: sha512-yk/vXFwJp0flMrd2kfcR0XlumhwtPjB19HJvwcf0n3DvRE7UK8LeSK14LVghSzk0TzPsFFElweMnZEEv7+MYuQ==}
     peerDependencies:
       vue: ^3.2.0
     dependencies:
       '@ctrl/tinycolor': 3.4.1
-      '@element-plus/icons-vue': 2.0.9(vue@3.3.4)
+      '@element-plus/icons-vue': 2.3.1(vue@3.3.4)
       '@floating-ui/dom': 1.0.1
       '@popperjs/core': /@sxzz/popperjs-es@2.11.7
       '@types/lodash': 4.14.182

+ 40 - 0
src/api/home/home-score.ts

@@ -0,0 +1,40 @@
+import { http } from '@/utils/http/axios';
+
+/** 得分信息 */
+export type ScoreType = {
+  /** 日期 */
+  date: string;
+  /** 得分 */
+  score: number;
+  /** 创建时间 */
+  workshopList: Array<string>;
+};
+
+/** 根据用户权限查询场景树 */
+export const getDailyScore = () => {
+  return http.request<ScoreType[]>({
+    url: '/dataPreview/getDailyScore',
+    method: 'get',
+  });
+};
+
+export const getWeeklyScore = () => {
+  return http.request<ScoreType[]>({
+    url: '/dataPreview/getWeeklyScore',
+    method: 'get',
+  });
+};
+
+export const getMonthlyScore = () => {
+  return http.request<ScoreType[]>({
+    url: '/dataPreview/getMonthlyScore',
+    method: 'get',
+  });
+};
+
+export const getQuarterlyScore = () => {
+  return http.request<ScoreType[]>({
+    url: '/dataPreview/getQuarterlyScore',
+    method: 'get',
+  });
+};

二进制
src/assets/camera/algorithm-manage.png


二进制
src/assets/camera/camera-config.png


二进制
src/assets/camera/camera-playback.png


二进制
src/assets/camera/camera-preview.png


二进制
src/assets/camera/organization-manage.png


二进制
src/assets/camera/user-manage.png


二进制
src/assets/icons/user-logo.png


+ 76 - 9
src/views/dashboard/home/Home.vue

@@ -1,31 +1,98 @@
 <template>
   <div class="home-page">
-    <div class="flex">
-      <CameraInfo class="flex-1" :data="sceneData" :get-algoes="getAlgoList" />
-      <AlgoData :data="violationData" :get-violations="getViolationCount" />
+    <div class="header">
+      <Header />
+    </div>
+    <div class="content">
+      <div class="content-left">
+        <div class="content-algorithm">
+          <AlgoData :data="violationData" :get-violations="getViolationCount" />
+        </div>
+        <div class="content-scores">
+          <Score />
+        </div>
+      </div>
+      <div class="content-right">
+        <div class="content-click">
+          <ClickShow />
+        </div>
+        <div class="content-operate">
+          <QuickAction />
+        </div>
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  // import { ref } from "vue";
-  import CameraInfo from './components/CameraInfo.vue';
   import AlgoData from './components/AlgoDataPanel.vue';
+  import ClickShow from './components/ClickShow.vue';
+  import Score from './components/Score.vue';
+  import Header from './components/Header.vue';
   import useHomeInfo from './hooks/useHomeInfo';
+  import QuickAction from './components/QuickAction.vue';
 
   const homeInfos = useHomeInfo();
-  const { sceneData, violationData, getAlgoList, getViolationCount } = homeInfos;
+  const { violationData, getViolationCount } = homeInfos;
 </script>
 
 <style scoped>
   .home-page {
     width: 100%;
-    height: 100%;
+    height: 91vh;
     padding-bottom: 24px;
+    display: flex;
+    flex-direction: column;
+    /* background: #ffffff; */
+  }
+
+  .header {
+    height: 128px;
+    width: 100%;
+    background: #ffffff;
+    margin-bottom: 10px;
+  }
+  .content {
+    flex-grow: 1;
+    display: flex;
+    width: 100%;
+    /* background: #ffffff; */
+  }
+
+  .content-left {
+    flex-grow: 1;
+    margin-right: 10px;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .content-algorithm {
+    height: 392px;
+    margin-bottom: 10px;
+    background: #ffffff;
+  }
+
+  .content-scores {
+    flex-grow: 1;
+    background: #ffffff;
+  }
+
+  .content-right {
+    /* flex-grow: 0; */
+    width: 280px;
+    display: flex;
+    flex-direction: column;
+    flex-shrink: 0;
+  }
+
+  .content-click {
+    height: 203px;
+    margin-bottom: 10px;
     background: #ffffff;
   }
 
-  .mask-pos {
-    margin-top: 16px;
+  .content-operate {
+    flex-grow: 1;
+    background: #ffffff;
   }
 </style>

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

@@ -0,0 +1,109 @@
+<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)"
+        :key="item.label"
+      >
+        <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';
+  import dayjs from 'dayjs';
+
+  const activeTab = ref<TimeTabEnum>(TimeTabEnum.DAY);
+
+  const emits = defineEmits(['checkTab', 'changeDateRange']);
+
+  const today = dayjs().format('YYYY-MM-DD');
+  const weekDay = dayjs().startOf('week').add(1, 'day').format('YYYY-MM-DD');
+  const monthDay = dayjs().startOf('month').format('YYYY-MM-DD');
+
+  const timeSlot = ref([today, today]);
+
+  const onClickTab = (tabItem: any) => {
+    activeTab.value = tabItem.value;
+    switch (tabItem.value) {
+      case TimeTabEnum.DAY:
+        timeSlot.value = [today, today];
+        break;
+
+      case TimeTabEnum.WEEK:
+        timeSlot.value = [weekDay, today];
+        break;
+
+      case TimeTabEnum.MONTH:
+        timeSlot.value = [monthDay, today];
+        break;
+    }
+    emits('checkTab', { tab: tabItem.value, data: timeSlot.value });
+  };
+
+  const onDateChnage = (date) => {
+    timeSlot.value = date.map((item) => dayjs(item).format('YYYY-MM-DD'));
+    onClickTab(TimeTabEnum.RANGE);
+  };
+</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>

+ 21 - 10
src/views/dashboard/home/components/AlgoCensusTabs.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="flex justify-between" style="width: 100%">
+  <div style="display: flex">
     <div class="text-tabs">
-      <div
+      <!-- <div
         v-for="item in timeTypeList"
         class="tab-item"
         @click="onClickTab(item)"
@@ -9,7 +9,15 @@
       >
         <span> {{ item.label }} </span>
         <div v-if="activeTab == item.value" class="tab-underline"></div>
-      </div>
+      </div> -->
+      <el-radio-group v-model="activeTab" class="score-select" @change="onClickTab">
+        <el-radio-button
+          v-for="item in timeTypeList"
+          :value="item.value"
+          :key="item.value"
+          :label="item.label"
+        ></el-radio-button>
+      </el-radio-group>
     </div>
     <el-date-picker
       v-model="timeSlot"
@@ -40,9 +48,8 @@
 
   const timeSlot = ref([today, today]);
 
-  const onClickTab = (tabItem: any) => {
-    activeTab.value = tabItem.value;
-    switch (tabItem.value) {
+  const onClickTab = () => {
+    switch (activeTab.value) {
       case TimeTabEnum.DAY:
         timeSlot.value = [today, today];
         break;
@@ -55,21 +62,23 @@
         timeSlot.value = [monthDay, today];
         break;
     }
-    emits('checkTab', { tab: tabItem.value, data: timeSlot.value });
+    emits('checkTab', { tab: activeTab.value, data: timeSlot.value });
   };
 
   const onDateChnage = (date) => {
     timeSlot.value = date.map((item) => dayjs(item).format('YYYY-MM-DD'));
-    onClickTab(TimeTabEnum.RANGE);
+    onClickTab();
   };
 </script>
 
 <style scoped>
   .text-tabs {
-    width: 132px;
-    height: 26px;
+    /* width: 132px; */
+    height: 23px;
     display: flex;
     justify-content: space-between;
+    margin-top: 15px;
+    margin-right: 24px;
   }
 
   .tab-item {
@@ -101,6 +110,8 @@
     width: 236px;
     flex: unset;
     height: 32px;
+    margin-top: 13px;
+    margin-right: 10px;
   }
 
   :deep(.el-input__wrapper) {

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

@@ -0,0 +1,204 @@
+<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="getVioStatData(0)" />
+      <div class="stat-divider"></div>
+      <ViolationStatItem :data="getVioStatData(1)" />
+      <div class="stat-divider"></div>
+      <ViolationStatItem :data="getVioStatData(2)" />
+      <div class="stat-divider"></div>
+      <ViolationStatItem :data="getVioStatData(3)" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, 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 { ViolationCount } from '@/api/home/home.ts';
+  import VChart from 'vue-echarts';
+
+  const props = defineProps<{
+    data: ViolationCount;
+    getViolations: (range: string[]) => void;
+  }>();
+
+  use([CanvasRenderer, PieChart, TooltipComponent, LegendComponent]);
+
+  const algoData = computed(() => {
+    let newData: any[] = [];
+    const vioList = props.data.violationAlgoList;
+    if (vioList && vioList.length) {
+      newData = vioList.map((item) => {
+        return {
+          value: item.proportion,
+          name: item.name,
+        };
+      });
+    }
+    console.log(newData);
+
+    return newData;
+  });
+  //  [
+  //   { 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 statData = computed(() => props.data.statusCountList);
+
+  const getVioStatData = (index) => {
+    let count = 0;
+    if (statData.value && statData.value.length) {
+      const matchItem = statData.value.find(
+        (item) => item.name === violationHandleCounts[index].value,
+      );
+      if (matchItem) {
+        count = matchItem.value;
+      }
+    }
+    return { ...violationHandleCounts[index], count };
+  };
+
+  const option = computed(() => {
+    return {
+      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: algoData.value.map((item) => item.name),
+        formatter: function (name) {
+          let total = 0;
+          let target;
+          for (let i = 0; i < algoData.value.length; i++) {
+            total += algoData.value[i].value;
+            if (algoData.value[i].name === name) {
+              target = algoData.value[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: algoData.value,
+          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 = (info: { tab: TimeTabEnum; data: string[] }) => {
+    timeTab.value = info.tab;
+    props.getViolations(info.data);
+  };
+</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>

+ 200 - 71
src/views/dashboard/home/components/AlgoDataPanel.vue

@@ -1,17 +1,21 @@
 <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="getVioStatData(0)" />
-      <div class="stat-divider"></div>
-      <ViolationStatItem :data="getVioStatData(1)" />
-      <div class="stat-divider"></div>
-      <ViolationStatItem :data="getVioStatData(2)" />
-      <div class="stat-divider"></div>
-      <ViolationStatItem :data="getVioStatData(3)" />
+  <div>
+    <div class="algo-header">
+      <span class="algo-tit">算法数据分析</span>
+      <CensusTabs @check-tab="onCheckTab" />
     </div>
+    <el-divider />
+    <div style="flex-grow: 1; display: flex">
+      <v-chart class="chart" :option="option" />
+      <el-divider direction="vertical" border-style="solid" />
+      <div class="stat-show">
+        <ViolationStatItem :data="getVioStatData(0)" class="stat-data" />
+        <div class="stat-divider"></div>
+        <ViolationStatItem :data="getVioStatData(1)" class="stat-data" />
+        <ViolationStatItem :data="getVioStatData(2)" class="stat-data" />
+        <div class="stat-divider"></div>
+        <ViolationStatItem :data="getVioStatData(3)" class="stat-data" /> </div
+    ></div>
   </div>
 </template>
 
@@ -23,7 +27,7 @@
   import { use } from 'echarts/core';
   import { CanvasRenderer } from 'echarts/renderers';
   import { PieChart } from 'echarts/charts';
-  import { TooltipComponent, LegendComponent } from 'echarts/components';
+  import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
   import { ViolationCount } from '@/api/home/home.ts';
   import VChart from 'vue-echarts';
 
@@ -32,7 +36,7 @@
     getViolations: (range: string[]) => void;
   }>();
 
-  use([CanvasRenderer, PieChart, TooltipComponent, LegendComponent]);
+  use([CanvasRenderer, PieChart, TooltipComponent, TitleComponent, LegendComponent]);
 
   const algoData = computed(() => {
     let newData: any[] = [];
@@ -49,6 +53,10 @@
 
     return newData;
   });
+
+  const algoLegendData = computed(() => {
+    return algoData.value.map((item) => item.name);
+  });
   //  [
   //   { value: 335, name: "人员闯入" },
   //   { value: 310, name: "未穿反光背心" },
@@ -77,74 +85,151 @@
     return { ...violationHandleCounts[index], count };
   };
 
+  // const option = computed(() => {
+  //   return {
+  //     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: algoData.value.map((item) => item.name),
+  //       formatter: function (name) {
+  //         let total = 0;
+  //         let target;
+  //         for (let i = 0; i < algoData.value.length; i++) {
+  //           total += algoData.value[i].value;
+  //           if (algoData.value[i].name === name) {
+  //             target = algoData.value[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: algoData.value,
+  //         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 option = computed(() => {
     return {
+      title: {
+        text: '算法占比', // 设置标题文本
+        left: '25%', // 标题居中对齐
+        top: 'center',
+        textStyle: {
+          fontSize: 20,
+        },
+      },
       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: algoData.value.map((item) => item.name),
-        formatter: function (name) {
-          let total = 0;
-          let target;
-          for (let i = 0; i < algoData.value.length; i++) {
-            total += algoData.value[i].value;
-            if (algoData.value[i].name === name) {
-              target = algoData.value[i].value;
+      grid: {
+        left: '10px',
+        right: '4%',
+        bottom: '3%',
+      },
+      legend: [
+        {
+          icon: 'circle',
+          orient: 'vertical',
+          type: 'scroll',
+          left: '65%',
+          top: 'center',
+          itemWidth: 10,
+          itemHeight: 10,
+          align: 'left',
+          textStyle: {
+            fontSize: 14,
+            color: '#6e69b2',
+          },
+          data: algoLegendData.value,
+          formatter: function (name) {
+            if (algoData.value && algoData.value.length) {
+              for (var i = 0; i < algoData.value.length; i++) {
+                if (name === algoData.value[i].name) {
+                  return (
+                    '' + name + '   |   ' + ' ' + (algoData.value[i].value * 100).toFixed(2) + '%'
+                  );
+                }
+              }
             }
-          }
-          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: '违规统计',
+          // name: 'Access From',
+          center: ['30%', '50%'],
           type: 'pie',
-          radius: ['40%', '65%'],
-          center: ['50%', '40%'],
-          labelLine: {
-            show: false,
-          },
-
+          radius: ['50%', '70%'],
+          avoidLabelOverlap: false,
           label: {
             show: false,
-            position: 'center',
-          },
-          data: algoData.value,
-          itemStyle: {
-            borderColor: '#fff',
-            borderWidth: 5,
+            // position: 'center',
           },
+
           emphasis: {
             label: {
               show: true,
               fontSize: 20,
               fontWeight: 'bold',
+              formatter: '{b}:   {c}',
             },
             itemStyle: {
               shadowBlur: 10,
@@ -152,6 +237,10 @@
               shadowColor: 'rgba(0, 0, 0, 0.5)',
             },
           },
+          labelLine: {
+            show: false,
+          },
+          data: algoData.value,
         },
       ],
     };
@@ -166,39 +255,79 @@
 </script>
 
 <style scoped>
-  .algo-data {
+  /* .algo-data {
     width: 484px;
     padding: 12px 27px;
     border-left: 2px solid #e8e8e8;
     display: flex;
     flex-direction: column;
     align-items: flex-start;
+  } */
+
+  .algo-header {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
   }
 
   .chart {
     width: 100%;
-    height: 450px;
+    flex-grow: 1;
+    height: 340px;
+  }
+  .el-divider--horizontal {
+    margin: 0px;
   }
 
   .algo-tit {
-    font-size: 16px;
+    /* font-size: 16px;
     font-weight: 500;
     margin-bottom: 10px;
     line-height: 44px;
-    color: #2e2e2e;
+    color: #2e2e2e; */
+    font-size: 16px;
+    margin-top: 17px;
+    margin-left: 18px;
   }
 
   .stat-show {
-    width: 100%;
-    margin-top: 32px;
+    flex-grow: 0;
+    width: 266px;
+    flex-shrink: 0;
+    font-size: 14px;
     display: flex;
-    justify-content: space-between;
-    align-items: center;
+    flex-wrap: wrap;
+    justify-content: center;
+    padding: 0;
+    margin-top: 72px;
+    margin-bottom: 82px;
+  }
+
+  .stat-data {
+    flex-basis: calc(50% - 1px);
+    padding-bottom: 35px;
   }
 
   .stat-divider {
     width: 1px;
     height: 40px;
     background: #e9e9e9;
+    /* margin-left: 29px;
+    margin-right: 27px; */
+  }
+
+  .el-divider--vertical {
+    margin: 0px;
+    height: 339px;
+  }
+
+  /* 控制图例的样式 */
+  ::v-deep .echarts-legend {
+    display: flex;
+    flex-wrap: wrap; /* 图例换行 */
+  }
+
+  ::v-deep .echarts-legend-item {
+    flex: 0 0 50%; /* 每行显示两个图例 */
   }
 </style>

+ 87 - 0
src/views/dashboard/home/components/ClickShow.vue

@@ -0,0 +1,87 @@
+<template>
+  <div>
+    <div class="click-header">
+      <div class="click-title">点击量</div>
+      <el-tag type="success" class="click-time">日</el-tag>
+    </div>
+    <el-divider />
+    <div>
+      <div class="click-number">126560</div>
+      <div class="click-compare">
+        <div class="click-detail">日同比</div>
+        <div>12%</div>
+        <el-icon v-if="ratio > 0" class="ratio-ico-up"><Top /></el-icon>
+        <el-icon v-else class="ratio-ico-down"><Bottom /></el-icon>
+      </div>
+    </div>
+    <el-divider />
+    <div class="click-total">
+      <div class="click-detail">总点击量</div>
+      <div style="margin-left: 22px">12423</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { Top, Bottom } from '@element-plus/icons-vue';
+  import { ref } from 'vue';
+  const ratio = ref(1);
+</script>
+
+<style scoped>
+  .click-header {
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .click-title {
+    font-size: 16px;
+    margin-left: 24px;
+    margin-top: 17px;
+    margin-bottom: 10px;
+  }
+  .click-time {
+    margin-top: 18px;
+    margin-right: 23px;
+    font-size: 12px;
+  }
+
+  .el-divider--horizontal {
+    margin: 0px;
+  }
+
+  .click-number {
+    margin-top: 8px;
+    margin-left: 24px;
+    margin-bottom: 20px;
+    font-size: 30px;
+  }
+
+  .click-compare {
+    margin-left: 24px;
+    margin-bottom: 20px;
+    display: flex;
+  }
+
+  .click-detail {
+    font-size: 14px;
+    margin-right: 8px;
+    color: rgba(0, 0, 0, 0.65);
+  }
+
+  .ratio-ico-up {
+    color: red;
+    margin-top: 4px;
+  }
+
+  .ratio-ico-down {
+    color: green;
+    margin-top: 3px;
+  }
+
+  .click-total {
+    margin-left: 24px;
+    margin-top: 8px;
+    display: flex;
+  }
+</style>

+ 124 - 0
src/views/dashboard/home/components/Header.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="header">
+    <div class="header-data">
+      <div>
+        <div class="data-title">算法总量</div>
+        <div class="algorithm-total">56</div>
+      </div>
+      <el-divider direction="vertical" border-style="solid" />
+      <div>
+        <div class="data-title">在线相机</div>
+        <div class="camera-data">
+          <div class="camera-online">56</div>
+          <div class="camera-total">/56</div>
+        </div>
+      </div>
+      <el-divider direction="vertical" border-style="solid" />
+      <div>
+        <div style="margin-left: 20px" class="data-title">用户总量</div>
+        <div class="user-total">2223</div>
+      </div>
+    </div>
+    <div class="header-user">
+      <div class="user-greet">
+        <img src="~@/assets/icons/user-logo.png" class="img-user" alt="" />
+        <div class="greet-content">早安,{{ getUsername }},祝你开心每一天</div>
+      </div>
+      <div class="user-guide">天眼管理中台 | 配置安全管控平台</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { useUserStore } from '@/store/modules/user';
+  import { computed } from 'vue';
+  const userStore = useUserStore();
+
+  const getUsername = computed(() => {
+    // return userStore.getUserInfo.username;
+    return userStore.getUserInfo.nickname;
+  });
+</script>
+
+<style scoped>
+  .header {
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .header-data {
+    margin-left: 28px;
+    margin-top: 32px;
+    display: flex;
+  }
+  .header-user {
+    margin-top: 28px;
+    margin-right: 32px;
+    /* display: flex; */
+  }
+
+  .algorithm-total {
+    font-size: 30px;
+    color: #1890ff;
+    margin-left: 22px;
+    margin-top: 4px;
+  }
+
+  .el-divider--vertical {
+    height: 40px;
+    margin-left: 32px;
+    margin-top: 11px;
+    margin-right: 32px;
+  }
+
+  .camera-data {
+    display: flex;
+    margin-top: 4px;
+  }
+
+  .camera-online {
+    font-size: 30px;
+    color: #1890ff;
+  }
+
+  .camera-total {
+    color: rgb(0, 0, 0, 0.45);
+    font-size: 20px;
+    margin-left: 4px;
+    margin-top: 9px;
+  }
+
+  .user-total {
+    font-size: 30px;
+    color: #1890ff;
+    margin-top: 4px;
+  }
+
+  .data-title {
+    color: rgb(0, 0, 0, 0.65);
+    font-size: 14px;
+  }
+
+  .user-greet {
+    display: flex;
+  }
+
+  .greet-content {
+    margin-top: 5px;
+    margin-left: 16px;
+    margin-bottom: 9px;
+    color: rgb(0, 0, 0, 0.85);
+    font-size: 20px;
+  }
+
+  .user-guide {
+    color: rgb(0, 0, 0, 0.45);
+    font-size: 14px;
+    margin-left: 52px;
+  }
+
+  .img-user {
+    width: 36px;
+    height: 36px;
+  }
+</style>

+ 88 - 0
src/views/dashboard/home/components/QuickAction.vue

@@ -0,0 +1,88 @@
+<template>
+  <div>
+    <div class="quick-title">快捷操作</div>
+    <!-- <el-divider /> -->
+    <div class="quick-content">
+      <div v-for="item in quickList" class="quick-go" @click="goPage(item.address)">
+        <img :src="item.img" alt="" />
+        <div class="quick-name">{{ item.name }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import CameraConfig from '@/assets/camera/camera-config.png';
+  import CameraPlayback from '@/assets/camera/camera-playback.png';
+  import CameraPreview from '@/assets/camera/camera-preview.png';
+  import AlgoManage from '@/assets/camera/algorithm-manage.png';
+  import UserManage from '@/assets/camera/user-manage.png';
+  import OrgaManage from '@/assets/camera/organization-manage.png';
+  import { useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+
+  const router = useRouter();
+
+  const quickList = ref([
+    { name: '相机配置', img: CameraConfig, address: '/cameras/overview' },
+    { name: '相机回放', img: CameraPlayback, address: '' },
+    { name: '相机预览', img: CameraPreview, address: '/cameras/preview' },
+    { name: '算法管理', img: AlgoManage, address: '/cameras/algo-manager' },
+    { name: '用户管理', img: UserManage, address: '/auth/user' },
+    { name: '组织管理', img: OrgaManage, address: '' },
+  ]);
+
+  const goPage = (address) => {
+    if (!address) {
+      ElMessage({
+        message: '该通道暂未开放',
+        type: 'warning',
+      });
+      return;
+    }
+    router.push(address);
+  };
+</script>
+
+<style scoped>
+  .quick-title {
+    font-size: 16px;
+    margin-top: 19px;
+    margin-left: 24px;
+    margin-bottom: 10px;
+  }
+
+  /* .el-divider--horizontal {
+    margin: 0px;
+  } */
+
+  .quick-go {
+    width: 139px;
+    height: 149px;
+    margin-top: 2px;
+    box-sizing: border-box;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    background-color: white;
+  }
+
+  .quick-go:hover {
+    box-shadow: 2px 1px 10px rgba(0, 0, 0, 0.5);
+    cursor: pointer;
+  }
+
+  .quick-content {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    background-color: #f0f2f5;
+  }
+
+  .quick-name {
+    margin-top: 19px;
+    font-size: 16px;
+  }
+</style>

+ 263 - 0
src/views/dashboard/home/components/Score.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="score-layout">
+    <div class="score-left">
+      <div class="score-header">
+        <span class="score-title">综合评分</span>
+        <el-radio-group v-model="timeSelect" class="score-select" @change="changeTime">
+          <el-radio-button v-for="item in ScoreTimeList" :label="item.label" :value="item.value" />
+        </el-radio-group>
+      </div>
+      <el-divider />
+      <div class="score-show">
+        <VChart class="pic-show" :option="options" />
+      </div>
+    </div>
+    <div class="score-divider"></div>
+    <div class="score-right">
+      <div class="concern-title">重点关注车间</div>
+      <ul class="concern-workspace">
+        <li v-for="workspace in workspaceList">{{ workspace }}</li>
+      </ul>
+      <div class="score-tip">
+        <el-icon><Warning /></el-icon>
+        <div class="tip-content">动态显示当前安全评分最低的三个车间</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { ScoreTimeList, TimeEnum } from '../types';
+  import { computed } from 'vue';
+  import VChart from 'vue-echarts';
+  import { use } from 'echarts/core';
+  import { CanvasRenderer } from 'echarts/renderers';
+  import { BarChart } from 'echarts/charts';
+  import { Warning } from '@element-plus/icons-vue';
+  import {
+    TooltipComponent,
+    LegendComponent,
+    GridComponent,
+    DataZoomComponent,
+  } from 'echarts/components';
+  import useScore from '../hooks/useScoreInfo';
+
+  const useHomeScore = useScore();
+  const {
+    scoreInfoList,
+    getDailyScoreList,
+    getWeeklyScoreList,
+    getMonthlyScoreList,
+    getQuarterlyScoreList,
+  } = useHomeScore;
+
+  use([
+    CanvasRenderer,
+    BarChart,
+    TooltipComponent,
+    LegendComponent,
+    GridComponent,
+    DataZoomComponent,
+  ]);
+  const timeSelect = ref<TimeEnum>(TimeEnum.DAY);
+  const workspaceList = ['车间1', '车间2', '车间3'];
+
+  const dataZoomConfig = computed(() => [
+    {
+      type: 'slider',
+      show: true,
+      showDetail: false,
+      showDataShadow: false,
+      xAxisIndex: [0],
+      start: 0,
+      end: Number(10 / scoreInfoList.value.length) * 100,
+      handleSize: 1,
+      height: 7,
+      bottom: 2,
+      brushSelect: false,
+      handleStyle: {
+        opacity: 0,
+      },
+    },
+    {
+      // 没有下面这块的话,只能拖动滚动条,
+      // 鼠标滚轮在区域内不能控制外部滚动条
+      type: 'inside',
+      // 控制哪个轴,如果是number表示控制一个轴,
+      // 如果是Array表示控制多个轴。此处控制第二根轴
+      xAxisIndex: [0, 1],
+      // 滚轮是否触发缩放
+      zoomOnMouseWheel: false,
+      // 鼠标移动能否触发平移
+      moveOnMouseMove: true,
+      // 鼠标滚轮能否触发平移
+      moveOnMouseWheel: true,
+    },
+  ]);
+
+  const options = computed(() => {
+    return {
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'none',
+        },
+        formatter: (v) => {
+          let [a] = v;
+          const workshopList = scoreInfoList.value.find(
+            (item) => item.date === a.name,
+          )?.workshopList;
+          return `
+            <div class='u-p-2'>
+                <div>评分最低的三个车间</div>
+                ${workshopList?.map((t) => `<span style="color: blue;">•</span> ${t}`).join('<br>')}
+            </div>
+        `;
+        },
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '4%',
+        top: '10%',
+        containLabel: true,
+      },
+      xAxis: {
+        type: 'category',
+        data: scoreInfoList.value.map((item) => item.date),
+        axisLabel: {
+          interval: 0, // 强制显示所有标签
+          //rotate: dataChart.value.map((item) => item.name).length > 10 ? 45 : 0, // 旋转角度
+          margin: 20, // 调整标签与轴线的距离,避免标签被遮挡
+        },
+      },
+      yAxis: {
+        type: 'value',
+      },
+      series: [
+        {
+          data: scoreInfoList.value.map((item) => item.score),
+          type: 'bar',
+        },
+      ],
+      dataZoom: scoreInfoList.value.length > 10 ? dataZoomConfig.value : null,
+      // 启用数据区域缩放
+    };
+  });
+
+  const changeTime = () => {
+    switch (timeSelect.value) {
+      case TimeEnum.DAY:
+        getDailyScoreList();
+        break;
+
+      case TimeEnum.WEEK:
+        getWeeklyScoreList();
+        break;
+
+      case TimeEnum.MONTH:
+        getMonthlyScoreList();
+        break;
+      case TimeEnum.QUARTER:
+        getQuarterlyScoreList();
+        break;
+    }
+  };
+</script>
+
+<style scoped>
+  .score-layout {
+    display: flex;
+  }
+
+  .score-left {
+    display: flex;
+    /* width: 965px; */
+    flex-grow: 1;
+    flex-direction: column;
+  }
+
+  .score-right {
+    width: 158px;
+    flex-shrink: 0;
+  }
+
+  .score-header {
+    display: flex;
+    justify-content: space-between;
+    height: 50px;
+  }
+
+  .score-show {
+    flex-grow: 1;
+    width: calc(100%-660px);
+    overflow-x: auto;
+  }
+
+  .score-title {
+    font-size: 16px;
+    margin-top: 17px;
+    margin-left: 18px;
+  }
+  .score-select {
+    /* flex-grow: 1; */
+    margin-right: 10px;
+  }
+
+  .score-divider {
+    width: 1px;
+    height: 320px;
+    background: #e9e9e9;
+    /* margin-left: 29px;
+    margin-right: 27px; */
+  }
+
+  .el-divider--horizontal {
+    margin: 0px;
+  }
+
+  .pic-show {
+    flex-grow: 1;
+    height: 270px;
+  }
+
+  .concern-title {
+    margin-left: 20px;
+    margin-top: 51px;
+    font-size: 14px;
+  }
+  .concern-workspace {
+    margin-left: 23px;
+    margin-top: 24px;
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.65);
+  }
+
+  li {
+    margin-bottom: 16px;
+  }
+  li:before {
+    content: '•';
+    color: red;
+    display: inline-block;
+    margin-right: 20px;
+    width: 8px;
+    height: 8px;
+    font-size: 16px;
+    line-height: 1;
+  }
+
+  .score-tip {
+    display: flex;
+    margin-top: 20px;
+    margin-left: 14px;
+  }
+
+  .tip-content {
+    margin-left: 16px;
+    margin-right: 14px;
+    color: rgba(0, 0, 0, 0.45);
+    font-size: 10px;
+  }
+</style>

+ 55 - 0
src/views/dashboard/home/hooks/useScoreInfo.ts

@@ -0,0 +1,55 @@
+import { ref, onMounted } from 'vue';
+import {
+  ScoreType,
+  getDailyScore,
+  getWeeklyScore,
+  getMonthlyScore,
+  getQuarterlyScore,
+} from '@/api/home/home-score.ts';
+
+export function useScoreInfo() {
+  const scoreInfoList = ref<ScoreType[]>([
+    {
+      date: '2024-05-06',
+      score: 99,
+      workshopList: ['ARJ21总装车间', 'C919部装车间', '维修交付中心'],
+    },
+  ]);
+
+  const getDailyScoreList = () => {
+    getDailyScore().then((res) => {
+      scoreInfoList.value = res;
+    });
+  };
+
+  const getWeeklyScoreList = () => {
+    getWeeklyScore().then((res) => {
+      scoreInfoList.value = res;
+    });
+  };
+
+  const getMonthlyScoreList = () => {
+    getMonthlyScore().then((res) => {
+      scoreInfoList.value = res;
+    });
+  };
+
+  const getQuarterlyScoreList = () => {
+    getQuarterlyScore().then((res) => {
+      scoreInfoList.value = res;
+    });
+  };
+
+  onMounted(() => {
+    getDailyScoreList();
+  });
+  return {
+    scoreInfoList,
+    getDailyScoreList,
+    getWeeklyScoreList,
+    getMonthlyScoreList,
+    getQuarterlyScoreList,
+  };
+}
+
+export default useScoreInfo;

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

@@ -5,6 +5,13 @@ export enum TimeTabEnum {
   RANGE = 'range',
 }
 
+export enum TimeEnum {
+  DAY = 'day',
+  WEEK = 'week',
+  MONTH = 'month',
+  QUARTER = 'quarter',
+}
+
 export const timeTypeList = [
   {
     label: '今日',
@@ -20,6 +27,25 @@ export const timeTypeList = [
   },
 ];
 
+export const ScoreTimeList = [
+  {
+    label: '日',
+    value: TimeEnum.DAY,
+  },
+  {
+    label: '周',
+    value: TimeEnum.WEEK,
+  },
+  {
+    label: '月',
+    value: TimeEnum.MONTH,
+  },
+  {
+    label: '季度',
+    value: TimeEnum.QUARTER,
+  },
+];
+
 export enum ViolationHandleStat {
   // UNTREAT = 'untreat',
   // TREATED = 'treated',

+ 1 - 1
src/views/system/user/CreateDrawer.vue

@@ -84,7 +84,7 @@
   import { useDictionary } from '@/hooks/web/useDictionary';
   import { userInfo } from '@/api/system/user';
   import { postList } from '@/api/common/index';
-  import { cloneDeep, reject } from 'lodash-es';
+  import { cloneDeep } from 'lodash-es';
   import { UserType, addSingleUser, updateUser } from '@/api/system/user-operate';
   import useSelectContent from './hooks/use-user-para';