Parcourir la source

feat: 院区安全态势左右两侧数据看板完成

bxy il y a 6 mois
Parent
commit
1811268e9b

+ 1 - 1
package.json

@@ -42,7 +42,7 @@
     "axios": "0.27.2",
     "cropperjs": "1.5.12",
     "dayjs": "1.11.4",
-    "echarts": "5.4.2",
+    "echarts": "5.5.0",
     "element-plus": "2.9.7",
     "element-resize-detector": "1.2.4",
     "flv.js": "^1.6.2",

+ 14 - 0
src/api/disaster-overview/index.ts

@@ -353,3 +353,17 @@ export const getTodayDisasterWarnInfoList = () => {
     method: 'get',
   });
 };
+
+/**
+ * @description: 查询安全态势应急管理统计
+ */
+export interface DisasterPreventionInfoRes {
+  monthDisasterCount: number; // 本月气象灾害预警数
+  preventiveInspectRatio: string; // 预防检查覆盖
+}
+export const getDisasterPreventionInfo = () => {
+  return http.request<DisasterPreventionInfoRes>({
+    url: '/overview/queryDisasterPreventionStatistics',
+    method: 'get',
+  });
+};

+ 28 - 0
src/api/emergency-overview/index.ts

@@ -128,3 +128,31 @@ export const getOverviewEmergencyProcedure = () => {
     method: 'get',
   });
 };
+
+/**
+ * @description: 查询本年度已完成应急演练数量
+ */
+export const getCompletedEmergencyDrillCountThisYear = () => {
+  return http.request({
+    url: '/emergencyDrill/queryCompletedEmergencyDrillCountThisYear',
+    method: 'get',
+  });
+};
+
+/**
+ * @description: 查询安全态势应急管理统计
+ */
+export interface EmergencyManageInfoRes {
+  emergencySuppliesCount: number; // 应急物资总数
+  validityPeriodRatio: string; // 有效期内物资比例
+  variousTypeSuppliesRatio: {
+    suppliesTypeName: string; // 物资类型名称
+    typeRatio: string; // 类型占比
+  }[];
+}
+export const getEmergencyManageInfo = () => {
+  return http.request<EmergencyManageInfoRes>({
+    url: '/overview/queryEmergencyManagementStatistics',
+    method: 'get',
+  });
+};

+ 20 - 0
src/assets/svg/arrow-down.svg

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 62</title>
+    <g id="移动端-lynn" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="1-灾害防范-有管理权限-有待执行检查任务" transform="translate(-666, -312)">
+            <g id="今日天气" transform="translate(20, 284)">
+                <g id="编组-8" transform="translate(434, 16)">
+                    <g id="编组-10" transform="translate(24, 8)">
+                        <g id="编组-62" transform="translate(188, 4)">
+                            <g id="编组" transform="translate(3, 3)" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.4">
+                                <line x1="9.006225" y1="17.924625" x2="9.006225" y2="0" id="路径"></line>
+                                <polyline id="路径" points="18 9 9 18 0 9"></polyline>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 1 - 1
src/views/disaster/overview/components/WeatherCard.vue

@@ -49,7 +49,7 @@
   let timer: NodeJS.Timeout;
   const weatherDisasterDic = ref<SysDictDataDetail[]>([]); // 气象灾害预警字典
   const disasterMeasureDic = ref<SysDictDataDetail[]>([]); // 灾害应急措施字典
-  const todayWarningInfo = ref<DisasterWarningListType[]>([]); // 今日灾害预警信息TODO:
+  const todayWarningInfo = ref<DisasterWarningListType[]>([]); // 今日灾害预警信息
   const measureTitle = ref<string | undefined>('');
   const measureInfo = ref<string | undefined>('');
 

+ 0 - 2
src/views/institute-safety/PageInstituteSafety.vue

@@ -44,8 +44,6 @@
   .card-weather {
     width: 330px;
     height: 196px;
-    background: linear-gradient(90deg, #b4ccff 0%, #e4fbf9 100%);
-    border-radius: 8px;
   }
 
   .card-production-safety {

+ 62 - 3
src/views/institute-safety/components/CardDisasterPrevention.vue

@@ -1,7 +1,66 @@
 <template>
-  <div> </div>
+  <div>
+    <TopTitle title="灾害防范" />
+    <div class="content">
+      <div class="content-item">
+        <span class="title">本月气象灾害预警数</span>
+        <span class="value">{{ weatherWarningCount }}</span>
+      </div>
+      <div class="content-item">
+        <span class="title">预防检查覆盖</span>
+        <span class="value">{{ preventionCheckCoverage }}</span>
+      </div>
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import TopTitle from './TopTitle.vue';
+  import { getDisasterPreventionInfo } from '@/api/disaster-overview';
 
-<style scoped lang="scss"></style>
+  const weatherWarningCount = ref<number>(0);
+  const preventionCheckCoverage = ref<string>('');
+
+  onMounted(async () => {
+    const disasterPreventionRes = await getDisasterPreventionInfo();
+    weatherWarningCount.value = disasterPreventionRes.monthDisasterCount;
+    preventionCheckCoverage.value = disasterPreventionRes.preventiveInspectRatio || '0%';
+  });
+</script>
+
+<style scoped lang="scss">
+  .content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 10px;
+    margin: 12px 10px 24px 10px;
+  }
+
+  .content-item {
+    width: 150px;
+    height: 76px;
+    background: linear-gradient(180deg, #eaefff 0%, #ffffff 100%);
+    border-radius: 4px;
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+    padding: 12px 12px 16px 12px;
+  }
+
+  .title {
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    line-height: 20px;
+  }
+
+  .value {
+    font-family: DINAlternate;
+    font-weight: bold;
+    font-size: 22px;
+    color: #333333;
+    line-height: 26px;
+  }
+</style>

+ 261 - 3
src/views/institute-safety/components/CardEmergencyManage.vue

@@ -1,7 +1,265 @@
 <template>
-  <div> </div>
+  <div>
+    <TopTitle title="应急管理" />
+    <div class="counts">
+      <div class="count-item top-item">
+        <div class="count-value emergency-plan-count">{{ emergencyPlanCount }}</div>
+        <div class="count-label">应急预案数</div>
+      </div>
+      <div class="count-item top-item">
+        <div class="count-value emergency-exercise-count">{{ emergencyExerciseCount }}</div>
+        <div class="count-label">本年应急演练数</div>
+      </div>
+      <div class="count-item bottom-item">
+        <div class="count-value emergency-supplies-count">{{ emergencySuppliesCount }}</div>
+        <div class="count-label">应急物资总数</div>
+      </div>
+      <div class="count-item bottom-item">
+        <div class="count-value validity-period-ratio">{{ validityPeriodRatio }}</div>
+        <div class="count-label">有效期内物资比例</div>
+      </div>
+      <div class="divider-horizontal"></div>
+      <div class="divider-vertical"></div>
+    </div>
+    <div ref="chartRef" class="chart"></div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, onUnmounted, ref, watch } from 'vue';
+  import TopTitle from './TopTitle.vue';
+  import {
+    getOverviewEmergencyPlan,
+    getCompletedEmergencyDrillCountThisYear,
+    getEmergencyManageInfo,
+  } from '@/api/emergency-overview';
+  import echarts from '@/utils/lib/echarts';
+  import type { EChartsOption } from 'echarts';
 
-<style scoped lang="scss"></style>
+  const emergencyPlanCount = ref<number>(0); // 生效应急预案数量
+  const emergencyExerciseCount = ref<number>(0); // 本自然年已完成应急演练数量
+  const emergencySuppliesCount = ref<number>(0); // 应急物资总数
+  const validityPeriodRatio = ref<string>(''); // 有效期内物资比例
+  const variousTypeSuppliesRatio = ref<{ suppliesTypeName: string; typeRatio: string }[]>([]); // 各类型物资占比
+
+  const chartRef = ref<HTMLElement | null>(null);
+  let chartInstance: echarts.ECharts | null = null;
+
+  // 颜色配置,可以根据需要自定义
+  const colorPalette = ['#117af1', '#ff9237', '#ff6257', '#17cba5', '#5ab1ef', '#67e0e3', '#ffd53e', '#f0853a'];
+
+  // 初始化图表
+  const initChart = () => {
+    if (!chartRef.value || variousTypeSuppliesRatio.value.length === 0) return;
+
+    // 销毁旧实例
+    if (chartInstance) {
+      chartInstance.dispose();
+    }
+
+    // 初始化 ECharts 实例
+    chartInstance = echarts.init(chartRef.value);
+
+    // 转换数据格式
+    const chartData = variousTypeSuppliesRatio.value.map((item, index) => ({
+      value: parseFloat(item.typeRatio) || 0,
+      name: item.suppliesTypeName,
+      itemStyle: {
+        color: colorPalette[index % colorPalette.length],
+      },
+    }));
+
+    // 动态生成 rich 样式,为每个数据项的 ratio 设置对应的颜色
+    const richStyles: Record<string, any> = {
+      name: {
+        fontSize: 12,
+        color: '#999',
+        width: 90,
+        align: 'left',
+        lineHeight: 17,
+        padding: [0, 0, 0, 4],
+      },
+    };
+
+    // 为每个数据项创建对应的 ratio 样式
+    variousTypeSuppliesRatio.value.forEach((_item, index) => {
+      const color = colorPalette[index % colorPalette.length];
+      richStyles[`ratio${index}`] = {
+        fontSize: 12,
+        fontFamily: 'DINAlternate',
+        fontWeight: 'bold',
+        color: color,
+        width: 35,
+        align: 'right',
+        lineHeight: 14,
+      };
+    });
+
+    const option: EChartsOption = {
+      legend: {
+        orient: 'vertical',
+        left: 'right',
+        top: 'center',
+        align: 'left',
+        itemWidth: 6,
+        itemHeight: 6,
+        itemGap: 10,
+        icon: 'circle',
+        formatter: (name: string) => {
+          const itemIndex = variousTypeSuppliesRatio.value.findIndex((item) => item.suppliesTypeName === name);
+          if (itemIndex === -1) return name;
+          const item = variousTypeSuppliesRatio.value[itemIndex];
+          const nameText = item.suppliesTypeName;
+          const ratioText = item.typeRatio;
+          return `{name|${nameText}} {ratio${itemIndex}|${ratioText}}`;
+        },
+        textStyle: {
+          rich: richStyles,
+        },
+      },
+      series: [
+        {
+          type: 'pie',
+          radius: ['60%', '90%'],
+          center: ['20%', '50%'],
+          avoidLabelOverlap: false,
+          itemStyle: {
+            borderRadius: 0,
+          },
+          label: {
+            show: false,
+          },
+          emphasis: {
+            scale: false,
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)',
+            },
+          },
+          data: chartData,
+        },
+      ],
+    };
+
+    chartInstance.setOption(option);
+
+    // 监听窗口大小变化
+    window.addEventListener('resize', handleResize);
+  };
+
+  const handleResize = () => {
+    if (chartInstance) {
+      chartInstance.resize();
+    }
+  };
+
+  onMounted(async () => {
+    const emergencyPlanRes = await getOverviewEmergencyPlan();
+    emergencyPlanCount.value = emergencyPlanRes.planInfoList.reduce((acc, curr) => acc + curr.count, 0);
+    emergencyExerciseCount.value = await getCompletedEmergencyDrillCountThisYear();
+    const emergencyManageRes = await getEmergencyManageInfo();
+    emergencySuppliesCount.value = emergencyManageRes.emergencySuppliesCount;
+    validityPeriodRatio.value = emergencyManageRes.validityPeriodRatio;
+    variousTypeSuppliesRatio.value = emergencyManageRes.variousTypeSuppliesRatio;
+
+    // 数据加载完成后初始化图表
+    initChart();
+  });
+
+  // 监听数据变化
+  watch(
+    () => variousTypeSuppliesRatio.value,
+    () => {
+      initChart();
+    },
+    { deep: true },
+  );
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize);
+    if (chartInstance) {
+      chartInstance.dispose();
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  .counts {
+    width: 314px;
+    height: 131px;
+    margin: 20px 8px;
+    position: relative;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: 1fr 1fr;
+    gap: 0;
+
+    .count-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 4px;
+      position: relative;
+    }
+    .top-item {
+      justify-content: flex-start;
+    }
+    .bottom-item {
+      justify-content: flex-end;
+    }
+
+    .count-label {
+      font-weight: 400;
+      font-size: 12px;
+      color: #999999;
+      line-height: 17px;
+    }
+
+    .count-value {
+      font-family: DINAlternate;
+      font-weight: bold;
+      font-size: 24px;
+      line-height: 28px;
+    }
+    .emergency-plan-count {
+      color: #117af1;
+    }
+    .emergency-exercise-count {
+      color: #ff9237;
+    }
+    .emergency-supplies-count {
+      color: #ff6257;
+    }
+    .validity-period-ratio {
+      color: #17cba5;
+    }
+
+    .divider-horizontal {
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 50%;
+      height: 1px;
+      background-color: #e8e8e8;
+      transform: translateY(-50%);
+      z-index: 1;
+    }
+
+    .divider-vertical {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 50%;
+      width: 1px;
+      background-color: #e8e8e8;
+      transform: translateX(-50%);
+      z-index: 1;
+    }
+  }
+
+  .chart {
+    height: 100px;
+    margin: 34px 24px 24px 20px;
+  }
+</style>

+ 24 - 3
src/views/institute-safety/components/CardProductionSafety.vue

@@ -1,7 +1,28 @@
 <template>
-  <div> </div>
+  <div>
+    <TopTitle title="生产安全" />
+    <ResponsibilityImplementation class="responsibility-implementation" />
+    <EducationTraining class="education-training" />
+    <RiskIdentification class="risk-identification" />
+    <DangerInvestigation class="danger-investigation" />
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import TopTitle from './TopTitle.vue';
+  import ResponsibilityImplementation from './production-safety/ResponsibilityImplementation.vue';
+  import EducationTraining from './production-safety/EducationTraining.vue';
+  import RiskIdentification from './production-safety/RiskIdentification.vue';
+  import DangerInvestigation from './production-safety/DangerInvestigation.vue';
+</script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+  .responsibility-implementation {
+    margin: 12px 0 24px 0;
+  }
+  .education-training,
+  .risk-identification,
+  .danger-investigation {
+    margin-bottom: 24px;
+  }
+</style>

+ 162 - 3
src/views/institute-safety/components/CardSecurityConfidentiality.vue

@@ -1,7 +1,166 @@
 <template>
-  <div> </div>
+  <div>
+    <TopTitle title="保卫保密" />
+    <div class="counts">
+      <div class="count-item top-item">
+        <div class="count-value position-count">{{ positionCount }}</div>
+        <div class="count-label">要害及重点监控区域数</div>
+      </div>
+      <div class="count-item top-item">
+        <div class="count-value intrusion-capture-count">{{ intrusionCaptureCount }}</div>
+        <div class="count-label">本月要害部位闯入抓拍数</div>
+      </div>
+      <div class="count-item bottom-item">
+        <div class="count-value personnel-intrusion-count">{{ aroundCameraAlarmsCount }}</div>
+        <div class="count-label">本月周界系统报警数</div>
+      </div>
+      <div class="count-item bottom-item">
+        <div class="count-value visitor-count">{{ visitorCount }} / {{ foreignVisitorCount }}</div>
+        <div class="count-label">今日访客/外籍访客数</div>
+      </div>
+      <div class="divider-horizontal"></div>
+      <div class="divider-vertical"></div>
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import dayjs from 'dayjs';
+  import TopTitle from './TopTitle.vue';
+  import {
+    getConfidentialityPositionList,
+    getSecurityPositionList,
+    getInvasionSnapshotList,
+  } from '@/api/security-confidentiality-position';
+  import { getPersonOverview } from '@/api/security-confidentiality-overview';
 
-<style scoped lang="scss"></style>
+  const confidentialityPositionCameraCount = ref<number>(0);
+  const securityPositionCameraCount = ref<number>(0);
+  const positionCount = ref<number>(0); // 要害及重点监控区域数
+  const intrusionCaptureCount = ref<number>(0); // 本月要害部位闯入抓拍数
+  const aroundCameraAlarmsCount = ref<number>(0); // 本月周界系统报警数
+  const visitorCount = ref<number>(0); // 今日访客
+  const foreignVisitorCount = ref<number>(0); // 外籍访客数
+
+  onMounted(async () => {
+    // 要害及重点监控区域数
+    await getConfidentialityPositionList({
+      groupName: '',
+      cameraName: '',
+    }).then((res) => {
+      const confidentialityPositionCameraInfo = res.flatMap((item) => {
+        return item.children.map((child) => ({
+          ...child,
+          positionName: item.groupName,
+        }));
+      });
+      confidentialityPositionCameraCount.value = confidentialityPositionCameraInfo.length;
+    });
+    await getSecurityPositionList({
+      groupName: '',
+      cameraName: '',
+    }).then((res) => {
+      const securityPositionCameraInfo = res.flatMap((item) => {
+        return item.children.map((child) => ({
+          ...child,
+          positionName: item.groupName,
+        }));
+      });
+      securityPositionCameraCount.value = securityPositionCameraInfo.length;
+    });
+    positionCount.value = confidentialityPositionCameraCount.value + securityPositionCameraCount.value;
+    // 本月要害部位闯入抓拍数
+    await getInvasionSnapshotList({
+      pageNumber: 1,
+      pageSize: 10,
+      queryParam: {
+        startTime: dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss'),
+        endTime: dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss'),
+      },
+    }).then((res) => {
+      intrusionCaptureCount.value = res.totalRow || 0;
+    });
+    // 本月周界系统报警数 + 今日访客/外籍访客数
+    await getPersonOverview().then((res) => {
+      visitorCount.value = res.foreignVisitorCount + res.nonForeignVisitorCount;
+      foreignVisitorCount.value = res.foreignVisitorCount;
+      aroundCameraAlarmsCount.value = res.aroundCameraAlarmsCount;
+    });
+  });
+</script>
+
+<style scoped lang="scss">
+  .counts {
+    width: 314px;
+    height: 131px;
+    margin: 20px 8px;
+    position: relative;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: 1fr 1fr;
+    gap: 0;
+
+    .count-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 4px;
+      position: relative;
+    }
+    .top-item {
+      justify-content: flex-start;
+    }
+    .bottom-item {
+      justify-content: flex-end;
+    }
+
+    .count-label {
+      font-weight: 400;
+      font-size: 12px;
+      color: #999999;
+      line-height: 17px;
+    }
+
+    .count-value {
+      font-family: DINAlternate;
+      font-weight: bold;
+      font-size: 24px;
+      line-height: 28px;
+    }
+    .position-count {
+      color: #117af1;
+    }
+    .intrusion-capture-count {
+      color: #ff9237;
+    }
+    .personnel-intrusion-count {
+      color: #ff6257;
+    }
+    .visitor-count {
+      color: #17cba5;
+    }
+
+    .divider-horizontal {
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 50%;
+      height: 1px;
+      background-color: #e8e8e8;
+      transform: translateY(-50%);
+      z-index: 1;
+    }
+
+    .divider-vertical {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 50%;
+      width: 1px;
+      background-color: #e8e8e8;
+      transform: translateX(-50%);
+      z-index: 1;
+    }
+  }
+</style>

+ 126 - 3
src/views/institute-safety/components/CardTrafficSafety.vue

@@ -1,7 +1,130 @@
 <template>
-  <div> </div>
+  <div>
+    <TopTitle title="交通安全" />
+    <div class="counts">
+      <div class="count-item top-item">
+        <div class="count-value vehicle-count">{{ vehicleCount }}</div>
+        <div class="count-label">本月车流数</div>
+      </div>
+      <div class="count-item top-item">
+        <div class="count-value regulation-count">{{ regulationCount }}</div>
+        <div class="count-label">本月违章数</div>
+      </div>
+      <div class="count-item bottom-item">
+        <div class="count-value notice-count">{{ noticeCount }}</div>
+        <div class="count-label">本月通报数</div>
+      </div>
+      <div class="count-item bottom-item">
+        <div class="count-value accident-count">{{ accidentCount }}</div>
+        <div class="count-label">本月事故数</div>
+      </div>
+      <div class="divider-horizontal"></div>
+      <div class="divider-vertical"></div>
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import dayjs from 'dayjs';
+  import TopTitle from './TopTitle.vue';
+  import { getViolationStatisticsOverview } from '@/api/traffic-overview';
+  import { getVehicleRecordList } from '@/api/security-confidentiality-vehicle';
 
-<style scoped lang="scss"></style>
+  const vehicleCount = ref<number>(0); // 本月车流数
+  const regulationCount = ref<number>(0); // 本月违章数
+  const noticeCount = ref<number>(0); // 本月通报数
+  const accidentCount = ref<number>(0); // 本月事故数
+
+  onMounted(async () => {
+    const res = await getViolationStatisticsOverview();
+    regulationCount.value = res.vehicleViolationCount;
+    noticeCount.value = res.violationNoticeCount;
+    accidentCount.value = res.trafficAccidentCount;
+
+    const vehicleRecordList = await getVehicleRecordList({
+      pageNumber: 1,
+      pageSize: 10,
+      queryParam: {
+        startTime: dayjs().startOf('month').format('YYYY-MM-DD HH:mm:ss'),
+        endTime: dayjs().endOf('month').format('YYYY-MM-DD HH:mm:ss'),
+      },
+    });
+    vehicleCount.value = vehicleRecordList.totalRow;
+  });
+</script>
+
+<style scoped lang="scss">
+  .counts {
+    width: 314px;
+    height: 131px;
+    margin: 20px 8px;
+    position: relative;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: 1fr 1fr;
+    gap: 0;
+
+    .count-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 4px;
+      position: relative;
+    }
+    .top-item {
+      justify-content: flex-start;
+    }
+    .bottom-item {
+      justify-content: flex-end;
+    }
+
+    .count-label {
+      font-weight: 400;
+      font-size: 12px;
+      color: #999999;
+      line-height: 17px;
+    }
+
+    .count-value {
+      font-family: DINAlternate;
+      font-weight: bold;
+      font-size: 24px;
+      line-height: 28px;
+    }
+    .vehicle-count {
+      color: #117af1;
+    }
+    .regulation-count {
+      color: #ff9237;
+    }
+    .notice-count {
+      color: #ff6257;
+    }
+    .accident-count {
+      color: #17cba5;
+    }
+
+    .divider-horizontal {
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 50%;
+      height: 1px;
+      background-color: #e8e8e8;
+      transform: translateY(-50%);
+      z-index: 1;
+    }
+
+    .divider-vertical {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 50%;
+      width: 1px;
+      background-color: #e8e8e8;
+      transform: translateX(-50%);
+      z-index: 1;
+    }
+  }
+</style>

+ 356 - 3
src/views/institute-safety/components/CardWeather.vue

@@ -1,7 +1,360 @@
 <template>
-  <div> </div>
+  <div class="weather-card">
+    <div class="cloud-bg">
+      <div class="date-time">
+        <span>{{ currentDate }}</span>
+        <span>{{ currentWeek }}</span>
+        <span class="time">{{ currentTime }}</span>
+      </div>
+      <div class="temperature-wind">
+        <div class="temperature">{{ curTemperature || '--' }}℃</div>
+        <div class="wind">
+          <div>
+            风速:
+            <span class="wind-value">{{ curWindVelocity || '--' }} km/h</span>
+          </div>
+          <div>
+            风力:
+            <span class="wind-value">{{ windSpeedLevel || '--' }}级</span>
+          </div>
+        </div>
+      </div>
+      <div class="weather-warning" :class="`weather-warning--${warningLevel}`" @click="handleClick">
+        <SvgIcon iconName="weather-warning" :color="levelStyles[warningLevel].iconColor" width="20px" height="20px" />
+        <Transition name="warning-text" mode="out-in">
+          <span v-if="currentWarning" :key="currentIndex" class="weather-warning__text">
+            {{ currentWarning.disasterName }}
+          </span>
+          <span v-else key="no-warning" class="weather-warning__text">今日无气象灾害预警</span>
+        </Transition>
+        <Transition name="arrow-icon" mode="out-in">
+          <SvgIcon
+            v-if="todayWarningInfo.length > 1"
+            :key="currentIndex"
+            class="arrow-icon"
+            iconName="arrow-down"
+            :color="levelStyles[warningLevel].arrowColor"
+            width="16px"
+            height="16px"
+          />
+        </Transition>
+      </div>
+    </div>
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
+  import dayjs from 'dayjs';
+  import SvgIcon from '@/components/SvgIcon/SvgIcon.vue';
+  import { SysDictDataDetail, queryDictTypeDetail } from '@/api/dict';
+  import { getTodayDisasterWarnInfoList, getRealTimeWeatherData } from '@/api/disaster-overview';
 
-<style scoped lang="scss"></style>
+  export interface DisasterWarningListType {
+    disasterType: string;
+    disasterName: string;
+  }
+
+  const currentDate = ref('');
+  const currentWeek = ref('');
+  const currentTime = ref('');
+  let timer: NodeJS.Timeout;
+  const weatherDisasterDic = ref<SysDictDataDetail[]>([]); // 气象灾害预警字典
+  const todayWarningInfo = ref<DisasterWarningListType[]>([]); // 今日灾害预警信息
+
+  const curTemperature = ref('0');
+  const curWindVelocity = ref(0);
+  const windSpeedLevel = ref(0);
+  // 风速km/h等级换算表
+  const windLevels = [
+    { max: 1, level: 0 },
+    { max: 5, level: 1 },
+    { max: 11, level: 2 },
+    { max: 19, level: 3 },
+    { max: 28, level: 4 },
+    { max: 38, level: 5 },
+    { max: 49, level: 6 },
+    { max: 61, level: 7 },
+    { max: 74, level: 8 },
+    { max: 88, level: 9 },
+    { max: 102, level: 10 },
+    { max: 117, level: 11 },
+    { max: 133, level: 12 },
+    { max: 149, level: 13 },
+    { max: 166, level: 14 },
+    { max: 183, level: 15 },
+    { max: 201, level: 16 },
+    { max: 220, level: 17 },
+    { max: Infinity, level: 18 },
+  ];
+
+  const getWindLevel = (windSpeedKmh: number): number => {
+    const matched = windLevels.find(({ max }) => windSpeedKmh <= max);
+    return matched ? matched.level : 18;
+  };
+
+  watch(
+    () => curWindVelocity.value,
+    (newVal) => {
+      windSpeedLevel.value = getWindLevel(newVal);
+    },
+  );
+
+  // 更新时间函数
+  const updateDateTime = () => {
+    const now = dayjs();
+    currentDate.value = now.format('MM月DD日');
+    currentWeek.value = `星期${now.format('dd').slice(-1)}`;
+    currentTime.value = now.format('HH:mm:ss');
+  };
+
+  // 获取实时天气数据
+  const getRealTimeWeatherDataInfo = async () => {
+    const res = await getRealTimeWeatherData();
+    curTemperature.value = res?.temperature || '0';
+    curWindVelocity.value = (res?.windVelocity || 0) * 3.6;
+  };
+
+  // 获取今日灾害预警信息
+  const getTodayWarningInfo = async () => {
+    todayWarningInfo.value = (await getTodayDisasterWarnInfoList())?.map((item) => ({
+      disasterType: item.disasterType,
+      disasterName:
+        weatherDisasterDic.value.find((dic) => dic.itemCode === item.disasterType)?.itemValue || '未知预警信息',
+    }));
+  };
+
+  // 当前显示的索引
+  const currentIndex = ref(0);
+
+  // 当前显示的预警项
+  const currentWarning = computed(() => {
+    return todayWarningInfo.value.length > 0 ? todayWarningInfo.value[currentIndex.value] : null;
+  });
+
+  // 预警等级样式配置
+  const levelStyles = {
+    blue: {
+      iconColor: 'rgba(23, 119, 255, 1)',
+      arrowColor: 'rgba(23, 119, 255, 0.8)',
+    },
+    yellow: {
+      iconColor: 'rgba(250, 173, 20, 1)',
+      arrowColor: 'rgba(250, 173, 20, 0.8)',
+    },
+    orange: {
+      iconColor: 'rgba(255, 124, 77, 1)',
+      arrowColor: 'rgba(255, 124, 77, 0.8)',
+    },
+    red: {
+      iconColor: 'rgba(255, 77, 79, 1)',
+      arrowColor: 'rgba(255, 77, 79, 0.8)',
+    },
+    common: {
+      iconColor: 'rgba(0, 0, 0, 0.5)',
+      arrowColor: 'rgba(0, 0, 0, 0.5)',
+    },
+  };
+
+  // 根据类型判断预警等级
+  const warningLevel = computed(() => {
+    if (!currentWarning.value) return 'common';
+
+    const type = currentWarning.value.disasterType.toLowerCase();
+    if (type.includes('red')) return 'red';
+    if (type.includes('orange')) return 'orange';
+    if (type.includes('yellow')) return 'yellow';
+    if (type.includes('blue')) return 'blue';
+
+    return 'common'; // 默认通用
+  });
+
+  // 点击切换到下一个
+  const handleClick = () => {
+    if (todayWarningInfo.value.length <= 1) return;
+
+    currentIndex.value = (currentIndex.value + 1) % todayWarningInfo.value.length;
+  };
+
+  onMounted(async () => {
+    await queryDictTypeDetail('weather_warning').then((res) => {
+      weatherDisasterDic.value = res.sysDictDataList;
+    });
+    updateDateTime();
+    timer = setInterval(updateDateTime, 1000);
+    getRealTimeWeatherDataInfo();
+    getTodayWarningInfo();
+  });
+
+  onUnmounted(() => {
+    clearInterval(timer);
+  });
+</script>
+
+<style scoped lang="scss">
+  .weather-card {
+    background: linear-gradient(90deg, #b4ccff 0%, #e4fbf9 100%);
+    border-radius: 8px;
+
+    .cloud-bg {
+      width: 100%;
+      height: 100%;
+      background-image: url('@/assets/images/disaster-overview/cloud-bg.png');
+      background-repeat: no-repeat;
+      background-position: left top;
+      background-size: auto;
+      border-radius: 8px;
+    }
+  }
+
+  .date-time {
+    padding: 16px 20px;
+    font-weight: 400;
+    font-size: 14px;
+    color: #000000;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+
+    .time {
+      margin-left: auto;
+    }
+  }
+
+  .temperature-wind {
+    display: flex;
+    align-items: center;
+    gap: 32px;
+    padding: 0 20px;
+
+    .temperature {
+      line-height: 67px;
+      font-weight: bold;
+      font-size: 56px;
+      color: #0f3d7d;
+    }
+
+    .wind {
+      display: flex;
+      flex-direction: column;
+      gap: 7px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #0f3d7d;
+    }
+
+    .wind-value {
+      font-weight: 500;
+    }
+  }
+
+  .weather-warning {
+    width: 306px;
+    height: 48px;
+    margin: 17px 12px 12px 12px;
+    border-radius: 8px;
+    backdrop-filter: blur(10px);
+    background: rgba(#8799b3, 0.15);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    position: relative;
+    cursor: pointer;
+
+    &__text {
+      line-height: 1;
+      font-weight: 600;
+    }
+
+    .arrow-icon {
+      position: absolute;
+      right: 16px;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+
+    // 蓝色预警
+    &--blue {
+      background-color: rgb(23 119 255 / 15%);
+
+      .weather-warning__text {
+        color: #1777ff;
+      }
+    }
+
+    // 黄色预警
+    &--yellow {
+      background-color: rgb(250 173 20 / 15%);
+
+      .weather-warning__text {
+        color: #faad14;
+      }
+    }
+
+    // 橙色预警
+    &--orange {
+      background-color: rgb(255 124 77 / 15%);
+
+      .weather-warning__text {
+        color: #ff7c4d;
+      }
+    }
+
+    // 红色预警
+    &--red {
+      background-color: rgb(255 77 77 / 15%);
+
+      .weather-warning__text {
+        color: #ff4d4f;
+      }
+    }
+
+    // 通用
+    &--common {
+      background-color: rgba(#8799b3, 0.15);
+
+      .weather-warning__text {
+        color: rgba(0, 0, 0);
+        font-weight: 400;
+      }
+    }
+
+    // 悬停效果(仅当有多个预警时)
+    &:hover {
+      opacity: 0.8;
+    }
+  }
+
+  // 预警文本切换动画
+  .warning-text-enter-active,
+  .warning-text-leave-active,
+  .arrow-icon-enter-active,
+  .arrow-icon-leave-active {
+    transition: all 0.3s ease;
+  }
+
+  .warning-text-enter-from,
+  .arrow-icon-enter-from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+
+  .warning-text-enter-to,
+  .arrow-icon-enter-to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+
+  .warning-text-leave-from,
+  .arrow-icon-leave-from {
+    opacity: 1;
+    transform: translateY(0);
+  }
+
+  .warning-text-leave-to,
+  .arrow-icon-leave-to {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+</style>

+ 24 - 0
src/views/institute-safety/components/TopTitle.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="top-title"> {{ props.title }} </div>
+</template>
+
+<script setup lang="ts">
+  const props = defineProps<{
+    title: string;
+  }>();
+</script>
+
+<style scoped lang="scss">
+  .top-title {
+    width: 100%;
+    height: 48px;
+    background: linear-gradient(180deg, rgba(#adcfff, 0.15) 0%, #ffffff 100%);
+    border-radius: 8px 8px 0px 0px;
+    font-weight: 600;
+    font-size: 16px;
+    color: #000000;
+    display: flex;
+    align-items: center;
+    padding: 0 14px;
+  }
+</style>

+ 115 - 0
src/views/institute-safety/components/production-safety/DangerInvestigation.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="danger-investigation">
+    <div class="title">
+      <span class="line"></span>
+      <span class="text">隐患的排查与治理</span>
+    </div>
+    <div class="danger-investigation__content">
+      <div class="content-item" :class="item.type" v-for="item in itemList" :key="item.name">
+        <span class="name">{{ item.name }}</span>
+        <span class="value">{{ item.value }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, onMounted, ref } from 'vue';
+
+  const hiddenDangerCount = ref<number>(0);
+  const rectificationCount = ref<number>(0);
+  const closedCount = ref<number>(0);
+
+  const itemList = computed(() => [
+    { name: '隐患数', value: hiddenDangerCount.value, type: 'error' },
+    { name: '整改数', value: rectificationCount.value, type: 'warning' },
+    { name: '闭环', value: closedCount.value, type: 'success' },
+  ]);
+
+  onMounted(() => {
+    hiddenDangerCount.value = 124;
+    rectificationCount.value = 0;
+    closedCount.value = 124;
+  });
+</script>
+
+<style scoped lang="scss">
+  .title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 16px;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+    }
+
+    .text {
+      font-weight: 500;
+      font-size: 16px;
+      color: #000000;
+      line-height: 22px;
+    }
+  }
+
+  .danger-investigation__content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 9px;
+    padding: 0 12px;
+
+    .content-item {
+      width: 96px;
+      height: 92px;
+      border-radius: 0px 4px 4px 0px;
+      padding: 19px 22px;
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+
+      .name {
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+        line-height: 20px;
+      }
+
+      .value {
+        font-family: DINAlternate;
+        font-weight: 600;
+        font-size: 22px;
+        line-height: 26px;
+      }
+    }
+
+    .error {
+      border-left: 3px solid #ff0000;
+      background: linear-gradient(180deg, rgba(255, 77, 79, 0.15) 0%, rgba(255, 77, 79, 0.3) 100%);
+
+      .value {
+        color: #ff0000;
+      }
+    }
+
+    .warning {
+      border-left: 3px solid #ffd300;
+      background: linear-gradient(180deg, rgba(250, 173, 20, 0.15) 0%, rgba(250, 173, 20, 0.3) 100%);
+
+      .value {
+        color: #cdaa00;
+      }
+    }
+
+    .success {
+      border-left: 3px solid #1777ff;
+      background: linear-gradient(180deg, rgba(227, 239, 255, 0.4) 0%, rgba(185, 209, 255, 0.38) 100%);
+
+      .value {
+        color: #1777ff;
+      }
+    }
+  }
+</style>

+ 87 - 0
src/views/institute-safety/components/production-safety/EducationTraining.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="education-training">
+    <div class="title">
+      <span class="line"></span>
+      <span class="text">安全教育培训</span>
+    </div>
+    <div class="education-training__content">
+      <div class="content-item" v-for="item in educationTrainingContent" :key="item.name">
+        <span class="name">{{ item.name }}</span>
+        <span class="value">{{ item.value }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+
+  interface EducationTrainingContent {
+    name: string;
+    value: number;
+  }
+
+  const educationTrainingContent = ref<EducationTrainingContent[]>([]);
+
+  onMounted(() => {
+    educationTrainingContent.value = [
+      { name: '安全知识培训(次)', value: 20 },
+      { name: '参加人总计(人)', value: 14197 },
+    ];
+  });
+</script>
+
+<style scoped lang="scss">
+  .title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 16px;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+    }
+
+    .text {
+      font-weight: 500;
+      font-size: 16px;
+      color: #000000;
+      line-height: 22px;
+    }
+  }
+
+  .education-training__content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 10px;
+
+    .content-item {
+      width: 150px;
+      height: 76px;
+      background: linear-gradient(180deg, #eaefff 0%, #ffffff 100%);
+      border-radius: 4px;
+      padding: 14px 12px;
+      display: flex;
+      flex-direction: column;
+      gap: 2px;
+
+      .name {
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+        line-height: 20px;
+      }
+
+      .value {
+        font-family: DINAlternate;
+        font-weight: 600;
+        font-size: 22px;
+        color: #333333;
+        line-height: 26px;
+      }
+    }
+  }
+</style>

+ 144 - 0
src/views/institute-safety/components/production-safety/ResponsibilityImplementation.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="responsibility-implementation">
+    <div class="title">
+      <span class="line"></span>
+      <span class="text">安全责任落实</span>
+    </div>
+    <div class="responsibility-implementation__content">
+      <div class="content-title">责任书签订情况</div>
+      <div class="signing-rate">
+        <span>签订率</span>
+        <span class="signing-rate-value">
+          <span class="signing-rate-value-bar" :style="{ width: `${signingRate}%` }"></span>
+        </span>
+        <span class="signing-rate-unit">{{ signingRate }}%</span>
+      </div>
+      <div class="signing-nums">
+        <div class="signing-nums__item" v-for="item in signingList" :key="item.name">
+          <span class="name">{{ item.name }}</span>
+          <span class="num">{{ item.num }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+
+  interface SigningList {
+    name: string;
+    num: number;
+  }
+
+  const signingRate = ref<number>(0);
+  const signingList = ref<SigningList[]>([]);
+
+  onMounted(() => {
+    signingRate.value = 10;
+    signingList.value = [
+      { name: '所/中心', num: 28 },
+      { name: '科室', num: 347 },
+      { name: '员工', num: 3368 },
+      { name: '常驻供应商', num: 19 },
+    ];
+  });
+</script>
+
+<style scoped lang="scss">
+  .title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 16px;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+    }
+
+    .text {
+      font-weight: 500;
+      font-size: 16px;
+      color: #000000;
+      line-height: 22px;
+    }
+  }
+
+  .responsibility-implementation__content {
+    width: 310px;
+    height: 160px;
+    margin: 0 10px;
+    background: linear-gradient(90deg, #ebf3ff 0%, #fafcff 100%);
+    border-radius: 4px;
+    padding-top: 14px;
+
+    .content-title {
+      margin: 0 12px;
+      font-weight: 500;
+      font-size: 16px;
+      color: #333333;
+      line-height: 22px;
+    }
+
+    .signing-rate {
+      margin: 20px 16px;
+      font-weight: 400;
+      font-size: 14px;
+      color: #333333;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .signing-rate-value {
+        width: 192px;
+        height: 6px;
+        background: #d8d8d8;
+        border-radius: 4px;
+        position: relative;
+
+        .signing-rate-value-bar {
+          width: 0;
+          height: 100%;
+          background: #1777ff;
+          border-radius: 4px;
+          position: absolute;
+          left: 0;
+          top: 0;
+        }
+      }
+
+      .signing-rate-unit {
+        font-weight: 600;
+      }
+    }
+
+    .signing-nums {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 28px;
+
+      .signing-nums__item {
+        display: flex;
+        flex-direction: column;
+
+        .name {
+          font-weight: 400;
+          font-size: 14px;
+          color: #666666;
+          line-height: 20px;
+        }
+
+        .num {
+          font-family: DINAlternate;
+          font-weight: 600;
+          font-size: 22px;
+          color: #333333;
+          line-height: 26px;
+        }
+      }
+    }
+  }
+</style>

+ 119 - 0
src/views/institute-safety/components/production-safety/RiskIdentification.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="risk-identification">
+    <div class="title">
+      <span class="line"></span>
+      <span class="text">风险的识别与管控</span>
+    </div>
+    <div class="risk-identification__content">
+      <div class="check-count">
+        <span class="name">安全生产检查</span>
+        <span class="value">{{ checkCount }}</span>
+      </div>
+      <div class="check-result">
+        <div class="check-result__item">
+          <span class="item-name">今日危险作业数</span>
+          <span class="item-value">{{ hazardousWorkCount }}</span>
+        </div>
+        <div class="check-result__item">
+          <span class="item-name">今日施工作业数</span>
+          <span class="item-value">{{ constructionWorkCount }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+
+  const checkCount = ref<number>(0);
+  const hazardousWorkCount = ref<number>(0);
+  const constructionWorkCount = ref<number>(0);
+
+  onMounted(() => {
+    checkCount.value = 270;
+    hazardousWorkCount.value = 0;
+    constructionWorkCount.value = 0;
+  });
+</script>
+
+<style scoped lang="scss">
+  .title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 16px;
+
+    .line {
+      width: 3px;
+      height: 16px;
+      background: #1777ff;
+    }
+
+    .text {
+      font-weight: 500;
+      font-size: 16px;
+      color: #000000;
+      line-height: 22px;
+    }
+  }
+
+  .risk-identification__content {
+    width: 310px;
+    height: 140px;
+    background: linear-gradient(90deg, #ebf3ff 0%, #fafcff 100%);
+    border-radius: 4px;
+    margin: 0 10px;
+
+    .check-count {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 14px 26px 14px 12px;
+      border-bottom: 1px solid rgba(#979797, 0.11);
+
+      .name {
+        font-weight: 500;
+        font-size: 16px;
+        color: #333333;
+        line-height: 22px;
+      }
+
+      .value {
+        font-family: DINAlternate;
+        font-weight: 600;
+        font-size: 16px;
+        color: #333333;
+        line-height: 19px;
+      }
+    }
+
+    .check-result {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 20px 16px 19px 16px;
+
+      .check-result__item {
+        display: flex;
+        flex-direction: column;
+        gap: 4px;
+
+        .item-name {
+          font-weight: 400;
+          font-size: 14px;
+          color: #666666;
+          line-height: 20px;
+        }
+
+        .item-value {
+          font-family: DINAlternate;
+          font-weight: 600;
+          font-size: 22px;
+          color: #333333;
+          line-height: 26px;
+        }
+      }
+    }
+  }
+</style>

+ 1 - 1
src/views/security-confidentiality/overview/charts/OuterPersonChart.vue

@@ -124,7 +124,7 @@
           center: ['30%', '50%'],
           clockwise: true,
           avoidLabelOverlap: false,
-          padAngle: 5,
+          // padAngle: 5, // 升级到5.5.0后,padAngle参数生效,但是存在数据为0时间距仍然存在的问题,先注释
           label: {
             show: false,
           },

+ 1 - 0
src/views/security-confidentiality/overview/types.ts

@@ -13,6 +13,7 @@ export interface BarChartData {
 export interface PersonOverview {
   nonForeignVisitorCount: number;
   foreignVisitorCount: number;
+  aroundCameraAlarmsCount: number; // 本月周界相机报警数
 }
 
 export interface VehicleOverview {