yuanchao 1 ماه پیش
والد
کامیت
f671e69f48
2فایلهای تغییر یافته به همراه572 افزوده شده و 0 حذف شده
  1. 3 0
      src/api/pms/stat/index.ts
  2. 569 0
      src/views/pms/iotopeationfill/statistics .vue

+ 3 - 0
src/api/pms/stat/index.ts

@@ -86,4 +86,7 @@ export const IotStatApi = {
   getSafeCount: async (params: any) => {
     return await request.get({ url: `/rq/stat/home/safe` })
   },
+  getDeptStatistics: async (params: any) => {
+      return await request.get({ url: `/rq/iot-opeation-fill/getCount`, params })
+  },
 }

+ 569 - 0
src/views/pms/iotopeationfill/statistics .vue

@@ -0,0 +1,569 @@
+<template>
+  <!-- 第一行:统计卡片行 -->
+  <el-row :gutter="16" class="mb-4">
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="所属部门" prop="project_name">
+            <el-tree-select
+              v-model="queryParams.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              filterable
+              placeholder="请选择所在部门"
+              clearable
+              style="width: 180px"
+            />
+          </el-form-item>
+          <el-form-item label="创建时间" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-220px"
+            />
+          </el-form-item>
+<!--          <el-form-item label="填写状态" prop="project_name">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="填写状态"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>-->
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </el-col>
+  </el-row>
+  <!-- 第二行:图表行 -->
+  <el-row :gutter="16" class="mb-4">
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <template #header>
+          <div class="flex items-center">
+            <span class="text-base font-medium text-gray-600">运行记录填写统计</span>
+          </div>
+        </template>
+        <el-row class="h-[220px]">
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="reportingChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">总数</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="dealFinishedChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">已填写</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="transOrderChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">未填写</span>
+            </div>
+          </el-col>
+          <el-col :span="6" class="flex flex-col items-center">
+            <div ref="writeChartRef" class="h-[160px] w-full"></div>
+            <div class="text-center mt-2">
+              <span class="text-sm text-gray-600">填写中</span>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+    </el-col>
+  </el-row>
+
+  <!-- 第三行:消息统计行 -->
+  <el-row>
+    <el-col :span="24">
+      <el-card class="chart-card" shadow="never">
+        <template #header>
+          <div class="flex items-center justify-between">
+            <span class="text-base font-medium text-gray-600">部门数量统计</span>
+          </div>
+        </template>
+        <div ref="chartContainer" class="h-[300px]"></div>
+      </el-card>
+    </el-col>
+  </el-row>
+
+</template>
+
+<script setup lang="ts" name="Index">
+import * as echarts from 'echarts/core'
+import { BarChart } from 'echarts/charts'; // 显式导入柱状图模块
+
+import {
+  GridComponent,
+  LegendComponent,
+  TitleComponent,
+  ToolboxComponent,
+  TooltipComponent
+} from 'echarts/components'
+import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
+import { LabelLayout, UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import {
+  IotStatisticsDeviceMessageSummaryRespVO,
+  IotStatisticsSummaryRespVO
+} from '@/api/iot/statistics'
+import { formatDate } from '@/utils/formatTime'
+import { IotStatApi } from '@/api/pms/stat'
+import { ref, reactive, onMounted, onUnmounted } from "vue";
+import { defaultProps, handleTree } from "@/utils/tree";
+import * as DeptApi from "@/api/system/dept";
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import {useUserStore} from "@/store/modules/user";
+// 导入部门数据类型
+import { DeptTreeItem } from '@/api/system/dept'
+
+// 初始化echarts
+echarts.use([
+  TooltipComponent,
+  LegendComponent,
+  PieChart,
+  CanvasRenderer,
+  LabelLayout,
+  TitleComponent,
+  ToolboxComponent,
+  GridComponent,
+  LineChart,
+  UniversalTransition,
+  GaugeChart,
+  BarChart
+])
+
+const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
+const deptDataList = ref<DeptDataItem[]>([]) // 部门数据列表
+
+// 定义部门数据类型
+interface DeptDataItem {
+  deptId: number;
+  name: string;
+  totalCount: number;
+  filledCount: number;
+  unfilledCount: number;
+  fillingCount: number;
+}
+
+/** IoT 首页 */
+defineOptions({ name: 'iotOpeationSta' })
+
+const timeRange = ref('7d') // 修改默认选择为近一周
+const dateRange = ref<[Date, Date] | null>(null)
+
+const queryParams = reactive({
+  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
+  endTime: Date.now(), // 设置默认结束时间为当前时间
+  deptId: null, // 选中的部门ID
+  status: null // 填写状态
+})
+
+const handleQuery = () => {
+  // 重新获取数据
+  getStats()
+  initChart()
+}
+
+const resetQuery = () => {
+  // 重置查询参数
+  queryParams.startTime = Date.now() - 7 * 24 * 60 * 60 * 1000
+  queryParams.endTime = Date.now()
+  queryParams.deptId = null
+  queryParams.status = null
+
+  // 重新获取数据
+  getStats()
+  initChart()
+}
+
+const reportingChartRef = ref() // 在线设备统计的图表
+const dealFinishedChartRef = ref() // 离线设备统计的图表
+const transOrderChartRef = ref() // 待激活设备统计的图表
+const orderFinishChartRef = ref()
+const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
+const writeChartRef = ref() // 待填写图表
+const finishedChartRef = ref() // 已完成图表
+
+// 基础统计数据
+const statsData = ref<IotStatisticsSummaryRespVO>({
+  productCategoryCount: 0,
+  productCount: 0,
+  deviceCount: 0,
+  deviceMessageCount: 0,
+  productCategoryTodayCount: 0,
+  productTodayCount: 0,
+  deviceTodayCount: 0,
+  deviceMessageTodayCount: 0,
+  deviceOnlineCount: 0,
+  deviceOfflineCount: 0,
+  deviceInactiveCount: 0,
+  productCategoryDeviceCounts: {}
+})
+
+const day = ref({
+  failureDay: undefined,
+  maintainDay: undefined
+})
+const week = ref({
+  failureWeek: undefined,
+  maintainWeek: undefined
+})
+const month = ref({
+  failureMonth: undefined,
+  maintainMonth: undefined
+})
+const total = ref({
+  failureTotal: undefined,
+  maintainTotal: undefined
+})
+
+const status = ref<IotStatusItem[]>([])
+
+// 定义状态项接口
+interface IotStatusItem {
+  totalCount: number
+  filledCount: number
+  unfilledCount: number
+  fillingCount: number
+}
+
+// 消息统计数据
+const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
+  upstreamCounts: {},
+  downstreamCounts: {}
+})
+
+/** 处理快捷时间范围选择 */
+const handleTimeRangeChange = (timeRange: string) => {
+  const now = Date.now()
+  let startTime: number
+
+  switch (timeRange) {
+    case '1h':
+      startTime = now - 60 * 60 * 1000
+      break
+    case '24h':
+      startTime = now - 24 * 60 * 60 * 1000
+      break
+    case '7d':
+      startTime = now - 7 * 24 * 60 * 60 * 1000
+      break
+    default:
+      return
+  }
+
+  // 清空日期选择器
+  dateRange.value = null
+
+  // 更新查询参数
+  queryParams.startTime = startTime
+  queryParams.endTime = now
+
+  // 重新获取数据
+  getStats()
+  initChart()
+}
+
+/** 处理自定义日期范围选择 */
+const handleDateRangeChange = (value: [Date, Date] | null) => {
+  if (value) {
+    // 清空快捷选项
+    timeRange.value = ''
+
+    // 更新查询参数
+    queryParams.startTime = value[0].getTime()
+    queryParams.endTime = value[1].getTime()
+
+    // 重新获取数据
+    getStats()
+    initChart()
+  }
+}
+
+/** 获取统计数据 */
+const getStats = async () => {
+  // 获取基础统计数据
+  // 获取部门统计数据
+  IotStatApi.getDeptStatistics(queryParams).then((res) => {
+    deptDataList.value = res.deptCountList || [];
+    status.value = res.totalList;
+    initChart()
+    initCharts()
+  })
+}
+
+/** 初始化图表 */
+const initCharts = () => {
+  // 使用数组的第一个元素(如果存在)
+  const firstStatus = status.value[0] || {}
+  // 上报中
+  initGaugeChart(
+    reportingChartRef.value,
+    firstStatus.totalCount === undefined ? 0 : firstStatus.totalCount,
+    '#0d9'
+  )
+  // 处理完成
+  initGaugeChart(
+    dealFinishedChartRef.value,
+    firstStatus.filledCount === undefined ? 0 : firstStatus.filledCount,
+    '#f50'
+  )
+  // 转工单
+  initGaugeChart(
+    transOrderChartRef.value,
+    firstStatus.unfilledCount === undefined ? 0 : firstStatus.unfilledCount,
+    '#05b'
+  )
+  // 待填写
+  initGaugeChart(
+    writeChartRef.value,
+    firstStatus.fillingCount === undefined ? 0 : firstStatus.fillingCount,
+    '#05b'
+  )
+}
+
+/** 初始化仪表盘图表 */
+const initGaugeChart = (el: any, value: number, color: string) => {
+  echarts.init(el).setOption({
+    series: [
+      {
+        type: 'gauge',
+        startAngle: 360,
+        endAngle: 0,
+        min: 0,
+        max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
+        progress: {
+          show: true,
+          width: 12,
+          itemStyle: {
+            color: color
+          }
+        },
+        axisLine: {
+          lineStyle: {
+            width: 12,
+            color: [[1, '#E5E7EB']]
+          }
+        },
+        axisTick: { show: false },
+        splitLine: { show: false },
+        axisLabel: { show: false },
+        pointer: { show: false },
+        anchor: { show: false },
+        title: { show: false },
+        detail: {
+          valueAnimation: true,
+          fontSize: 24,
+          fontWeight: 'bold',
+          fontFamily: 'Inter, sans-serif',
+          color: color,
+          offsetCenter: [0, '0'],
+          formatter: (value: number) => {
+            return `${value} `
+          }
+        },
+        data: [{ value: value }]
+      }
+    ]
+  })
+}
+
+const chartContainer = ref(null)
+let chartInstance = null
+
+// 初始化部门统计图表
+const initChart = () => {
+  if (!chartContainer.value) return
+
+  // 准备数据
+  const deptNames = deptDataList.value.map(item => item.name)
+  const totalCounts = deptDataList.value.map(item => item.totalCount)
+  const filledCounts = deptDataList.value.map(item => item.filledCount)
+  const unfilledCounts = deptDataList.value.map(item => item.unfilledCount)
+  const fillingCounts = deptDataList.value.map(item => item.fillingCount)
+
+  // ECharts配置
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      formatter: (params) => {
+        let result = `<div class="font-bold">${params[0].axisValue}</div>`
+        params.forEach(param => {
+          result += `<div>${param.marker} ${param.seriesName}: ${param.value}</div>`
+        })
+        return result
+      }
+    },
+    legend: {
+      data: ['总数', '已填写', '未填写', '填写中'],
+      top: 25
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: deptNames,
+      axisLabel: {
+        rotate: 45,
+        margin: 15,
+        fontSize: 10
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        formatter: (value) => Math.floor(value).toString()
+      }
+    },
+    series: [
+      {
+        name: '总数',
+        type: 'bar',
+        barGap: 0,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#188df0' },
+            { offset: 1, color: '#188df0' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: totalCounts
+      },
+      {
+        name: '已填写',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#d3a137' },
+            { offset: 1, color: '#d3a137' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: filledCounts
+      },
+      {
+        name: '未填写',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'green' },
+            { offset: 1, color: 'green' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: unfilledCounts
+      },
+      {
+        name: '填写中',
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'red' },
+            { offset: 1, color: 'red' }
+          ])
+        },
+        emphasis: {
+          focus: 'series'
+        },
+        data: fillingCounts
+      }
+    ]
+  }
+
+  // 初始化图表
+  chartInstance = echarts.init(chartContainer.value)
+  chartInstance.setOption(option)
+
+  // 窗口缩放监听
+  window.addEventListener('resize', handleResize)
+  handleResize()
+}
+
+// 自适应调整
+const handleResize = () => {
+  chartInstance?.resize()
+}
+
+/** 初始化 */
+onMounted(async () => {
+  queryParams.deptId = useUserStore().getUser.deptId;
+  getStats()
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+
+onUnmounted(() => {
+  chartInstance?.dispose()
+  window.removeEventListener('resize', handleResize)
+})
+</script>
+
+<style lang="scss" scoped>
+.chart-card {
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
+  padding: 16px;
+}
+
+// 新增样式,隐藏滚动条但保留功能
+::-webkit-scrollbar {
+  display: none;
+}
+
+:host {
+  overflow: hidden;
+}
+
+// 确保页面铺满屏幕并不出现滚动条
+html, body {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+// 适配图表容器高度
+.el-row {
+  max-height: calc(100vh - 32px); // 减去页面padding
+  overflow: auto;
+  -ms-overflow-style: none; // 隐藏IE滚动条
+  scrollbar-width: none; // 隐藏Firefox滚动条
+}
+</style>