Selaa lähdekoodia

修改智慧运维界面、添加设备界面

linqilong 6 kuukautta sitten
vanhempi
commit
891631f5a9

+ 2 - 1
package.json

@@ -25,6 +25,7 @@
     "@yzfe/svgicon": "^1.2.2",
     "@yzfe/vue-svgicon": "^5.0.3",
     "echarts": "^5.5.1",
+    "echarts-gl": "^2.0.9",
     "element-plus": "^2.8.6",
     "mitt": "^3.0.1",
     "pinia": "^2.2.4",
@@ -50,7 +51,7 @@
     "jsdom": "^25.0.1",
     "npm-run-all2": "^6.2.3",
     "prettier": "^3.3.3",
-    "sass": "^1.80.3",
+    "sass": "^1.79.3",
     "sass-loader": "^16.0.2",
     "start-server-and-test": "^2.0.8",
     "typescript": "~5.5.4",

+ 32 - 7
src/api/device.ts

@@ -40,25 +40,25 @@ export function getDeviceInfo() {
       "deviceName": "视频监控摄像机",
       "state": "异常",
       "online": "故障",
-    },{
+    }, {
       "deviceType": "水质测验设备",
       "deviceId": "0012356T06",
       "deviceName": "总磷总氮",
       "state": "正常",
       "online": "运行中",
-    },{
+    }, {
       "deviceType": "水质测验设备",
       "deviceId": "0012356T07",
       "deviceName": "多参数采样器",
       "state": "正常",
       "online": "运行中",
-    },{
+    }, {
       "deviceType": "水质测验设备",
       "deviceId": "0012356T08",
       "deviceName": "COD分析仪",
       "state": "正常",
       "online": "运行中",
-    },{
+    }, {
       "deviceType": "水质测验设备",
       "deviceId": "0012356T09",
       "deviceName": "氨氮",
@@ -81,15 +81,15 @@ export function getDetailInfo() {
       "partsName": "1-16泵管",
       "useDate": "160",
       "offDate": "2025-03-16",
-    },{
+    }, {
       "partsName": "1-16泵管",
       "useDate": "160",
       "offDate": "2025-03-16",
-    },{
+    }, {
       "partsName": "1-16泵管",
       "useDate": "160",
       "offDate": "2025-03-16",
-    },{
+    }, {
       "partsName": "1-16泵管",
       "useDate": "160",
       "offDate": "2025-03-16",
@@ -97,3 +97,28 @@ export function getDetailInfo() {
   ])
 }
 
+
+/**
+ * 运维人员统计
+ */
+export function getOperationsPersonnelData() {
+  return Promise.resolve([
+    {
+      "name": "1-16泵管",
+      "processed": 2,
+      "untreated": 0,
+    }, {
+      "name": "1-16泵管",
+      "processed": 2,
+      "untreated": 0,
+    }, {
+      "name": "1-16泵管",
+      "processed": 2,
+      "untreated": 0,
+    }, {
+      "name": "1-16泵管",
+      "processed": 2,
+      "untreated": 1,
+    }
+  ])
+}

+ 6 - 0
src/assets/styles/introduce.scss

@@ -17,3 +17,9 @@
   height: 200px;
   border-radius: 6px;
 }
+
+.introduce-float-img {
+  float: right;
+  height: 200px;
+  border-radius: 6px;
+}

+ 155 - 3
src/components/Chart.vue

@@ -1,21 +1,29 @@
 <script lang="ts" setup>
-defineExpose({loadChart, carousel})
-
+import {getParametricEquation} from "@/utils/chart";
 import * as echarts from "echarts";
+import 'echarts-gl' // 3d图表库
 import {waterQualitys} from "@/utils/unit.js";
 import {nextTick, onMounted, onUnmounted, ref} from "vue";
 
+defineExpose({loadChart, carousel, bindListen})
+
 const chartRef = ref(null)
 let chart = null
 let timer = undefined
+let selectedIndex = ''
+let hoveredIndex = ''
 
-async function loadChart(option) {
+async function loadChart(option, type = null) {
   if (!chart) {
     await nextTick(); // 确保DOM已经渲染完成
     chart = echarts.init(chartRef.value);
   }
   chart.setOption(option, true);
   setTimeout(() => chart.resize(), 1000)
+
+  if (type && type === 'bindListen') {
+    bindListen();
+  }
 }
 
 function reloadChart() {
@@ -69,6 +77,150 @@ function carousel(timeout = 5000, yAxisChange = false) {
   checkAndCarousel();
 }
 
+// 监听鼠标事件,实现饼图选中效果(单选),近似实现高亮(放大)效果。
+function bindListen() {
+  // 监听点击事件,实现选中效果(单选)
+  chart.on('click', (params) => {
+    const option = chart.getOption()
+    // 从 option.series 中读取重新渲染扇形所需的参数,将是否选中取反。
+    const isSelected = !option.series[params.seriesIndex].pieStatus.selected
+    const isHovered = option.series[params.seriesIndex].pieStatus.hovered
+    const k = option.series[params.seriesIndex].pieStatus.k
+    const startRatio = option.series[params.seriesIndex].pieData.startRatio
+    const endRatio = option.series[params.seriesIndex].pieData.endRatio
+    // 如果之前选中过其他扇形,将其取消选中(对 option 更新)
+    if (selectedIndex !== '' && selectedIndex !== params.seriesIndex) {
+      option.series[selectedIndex].parametricEquation = getParametricEquation(
+        option.series[selectedIndex].pieData.startRatio,
+        option.series[selectedIndex].pieData.endRatio,
+        false,
+        false,
+        k,
+        option.series[selectedIndex].pieData.value
+      )
+      option.series[selectedIndex].pieStatus.selected = false
+    }
+    // 对当前点击的扇形,执行选中/取消选中操作(对 option 更新)
+    option.series[params.seriesIndex].parametricEquation = getParametricEquation(
+      startRatio,
+      endRatio,
+      isSelected,
+      isHovered,
+      k,
+      option.series[params.seriesIndex].pieData.value
+    )
+    option.series[params.seriesIndex].pieStatus.selected = isSelected
+    // 如果本次是选中操作,记录上次选中的扇形对应的系列号 seriesIndex
+    selectedIndex = isSelected ? params.seriesIndex : null
+    // 使用更新后的 option,渲染图表
+    chart.setOption(option)
+  })
+  // 监听 mouseover,近似实现高亮(放大)效果
+  chart.on('mouseover', (params) => {
+    const option = chart.getOption()
+    // 准备重新渲染扇形所需的参数
+    let isSelected
+    let isHovered
+    let startRatio
+    let endRatio
+    let k
+    // 如果触发 mouseover 的扇形当前已高亮,则不做操作
+    if (hoveredIndex === params.seriesIndex) {
+      // 否则进行高亮及必要的取消高亮操作
+    } else {
+      // 如果当前有高亮的扇形,取消其高亮状态(对 option 更新)
+      if (hoveredIndex !== '') {
+        // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。
+        isSelected = option.series[hoveredIndex].pieStatus.selected
+        isHovered = false
+        startRatio = option.series[hoveredIndex].pieData.startRatio
+        endRatio = option.series[hoveredIndex].pieData.endRatio
+        k = option.series[hoveredIndex].pieStatus.k
+        // 对当前点击的扇形,执行取消高亮操作(对 option 更新)
+        option.series[
+          hoveredIndex
+          ].parametricEquation = getParametricEquation(
+          startRatio,
+          endRatio,
+          isSelected,
+          isHovered,
+          k,
+          option.series[hoveredIndex].pieData.value
+        )
+        option.series[hoveredIndex].pieStatus.hovered = isHovered
+        // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
+        hoveredIndex = ''
+      }
+      // 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新)
+      if (
+        params.seriesName !== 'mouseoutSeries' &&
+        params.seriesName !== 'pie2d'
+      ) {
+        // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
+        isSelected =
+          option.series[params.seriesIndex].pieStatus.selected
+        isHovered = true
+        startRatio =
+          option.series[params.seriesIndex].pieData.startRatio
+        endRatio = option.series[params.seriesIndex].pieData.endRatio
+        k = option.series[params.seriesIndex].pieStatus.k
+        // 对当前点击的扇形,执行高亮操作(对 option 更新)
+        option.series[
+          params.seriesIndex
+          ].parametricEquation = getParametricEquation(
+          startRatio,
+          endRatio,
+          isSelected,
+          isHovered,
+          k,
+          option.series[params.seriesIndex].pieData.value * 1.3
+        )
+        option.series[
+          params.seriesIndex
+          ].pieStatus.hovered = isHovered
+        // 记录上次高亮的扇形对应的系列号 seriesIndex
+        hoveredIndex = params.seriesIndex
+      }
+      // 使用更新后的 option,渲染图表
+      chart.setOption(option)
+    }
+  })
+  // 修正取消高亮失败的 bug
+  chart.on('globalout', () => {
+    const option = chart.getOption()
+    // 准备重新渲染扇形所需的参数
+    let isSelected
+    let isHovered
+    let startRatio
+    let endRatio
+    let k
+    if (hoveredIndex !== '') {
+      // 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
+      isSelected = option.series[hoveredIndex].pieStatus.selected
+      isHovered = false
+      k = option.series[hoveredIndex].pieStatus.k
+      startRatio = option.series[hoveredIndex].pieData.startRatio
+      endRatio = option.series[hoveredIndex].pieData.endRatio
+      // 对当前点击的扇形,执行取消高亮操作(对 option 更新)
+      option.series[
+        hoveredIndex
+        ].parametricEquation = getParametricEquation(
+        startRatio,
+        endRatio,
+        isSelected,
+        isHovered,
+        k,
+        option.series[hoveredIndex].pieData.value
+      )
+      option.series[hoveredIndex].pieStatus.hovered = isHovered
+      // 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
+      hoveredIndex = ''
+    }
+    // 使用更新后的 option,渲染图表
+    chart.setOption(option)
+  })
+}
+
 onMounted(() => chartRef.value?.addEventListener("resize", reloadChart))
 onUnmounted(() => {
   if (timer) {

+ 1 - 1
src/components/tag/ColorTag.vue

@@ -4,7 +4,7 @@ import {HexToRgb} from "@/utils/color";
 
 defineProps({
   label: {type: String, default: null},
-  value: {type: String, default: null},
+  value: {default: null},
   unit: {type: String, default: null},
   backgroundColor: {type: String, default: '#1088d7'},
 })

+ 11 - 3
src/router/index.ts

@@ -18,19 +18,27 @@ const router = createRouter({
           path: 'station/:stcd',
           name: 'station',
           component: () => import('@/views/Station.vue'),
-        },{
+        },
+        {
           path: 'situational/:stcd',
           name: 'situational',
           component: () => import('@/views/Situational.vue'),
-        },{
+        },
+        {
           path: 'intelloper/:stcd',
           name: 'intelloper',
           component: () => import('@/views/IntellOper.vue'),
-        },{
+        },
+        {
           path: 'detail/:stcd',
           name: 'detail',
           component: () => import('@/views/Detail.vue'),
         },
+        {
+          path: 'device/:stcd/:deviceid',
+          name: 'device',
+          component: () => import('@/views/Device.vue'),
+        },
       ],
     },
   ],

+ 291 - 0
src/utils/chart.ts

@@ -0,0 +1,291 @@
+/**
+ * 绘制3d图
+ * @param pieData 总数据
+ * @param internalDiameterRatio:透明的空心占比
+ */
+const getPie3DSimple = (pieData: any[], internalDiameterRatio: number) => {
+  return getPie3D(pieData, internalDiameterRatio, 200, 50, 13)
+}
+
+/**
+ * 绘制3d图
+ * @param pieData 总数据
+ * @param internalDiameterRatio:透明的空心占比
+ * @param distance 视角到主体的距离
+ * @param alpha 旋转角度
+ * @param pieHeight 立体的高度
+ * @param opacity 饼或者环的透明度
+ */
+const getPie3D = (pieData: any[], internalDiameterRatio: number, distance: number, alpha: number, pieHeight: number, opacity = 1) => {
+  const series = []
+  let sumValue = 0
+  let startValue = 0
+  let endValue = 0
+  let legendData = []
+  let legendBfb = []
+  const k = 1 - internalDiameterRatio
+  pieData.sort((a: any, b: any) => {
+    return b.value - a.value
+  })
+  // 为每一个饼图数据,生成一个 series-surface 配置
+  for (let i = 0; i < pieData.length; i++) {
+    sumValue += pieData[i].value
+    const seriesItem = {
+      name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
+      type: 'surface',
+      parametric: true,
+      wireframe: {show: false},
+      pieData: pieData[i],
+      pieStatus: {
+        selected: false,
+        hovered: false,
+        k: k
+      },
+      center: ['10%', '50%'],
+      itemStyle: undefined
+    }
+    if (typeof pieData[i].itemStyle !== 'undefined') {
+      const itemStyle: any = {}
+      itemStyle.color =
+        typeof pieData[i].itemStyle.color !== 'undefined'
+          ? pieData[i].itemStyle.color
+          : opacity
+      itemStyle.opacity =
+        typeof pieData[i].itemStyle.opacity !== 'undefined'
+          ? pieData[i].itemStyle.opacity
+          : opacity
+      seriesItem.itemStyle = itemStyle
+    }
+    series.push(seriesItem)
+  }
+
+  // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
+  // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
+  legendData = []
+  legendBfb = []
+  for (let i = 0; i < series.length; i++) {
+    endValue = startValue + series[i].pieData.value
+    series[i].pieData.startRatio = startValue / sumValue
+    series[i].pieData.endRatio = endValue / sumValue
+    // @ts-ignore
+    series[i].parametricEquation = getParametricEquation(
+      series[i].pieData.startRatio,
+      series[i].pieData.endRatio,
+      false,
+      false,
+      k,
+      series[i].pieData.value
+    )
+    startValue = endValue
+    const bfb = fomatFloat(series[i].pieData.value / sumValue, 4)
+    legendData.push({
+      name: series[i].name,
+      value: bfb
+    })
+    legendBfb.push({
+      name: series[i].name,
+      value: bfb
+    })
+  }
+  const boxHeight = getHeight3D(series, pieHeight) // 通过pieHeight设定3d饼/环的高度,单位是px
+  // 准备待返回的配置项,把准备好的 legendData、series 传入。
+  const option = {
+    legend: {
+      show: true,
+      data: legendData,
+      orient: 'horizontal',
+      left: 0,
+      top: 0,
+      itemGap: 10,
+      textStyle: {
+        color: '#A1E2FF'
+      },
+      icon: 'circle',
+      formatter: function (param: any) {
+        const item: any = legendBfb.filter(item => item.name === param)[0]
+        const bfs = fomatFloat(item.value * 100, 2) + '%'
+        return `${item.name}  ${bfs}`
+      }
+    },
+    labelLine: {
+      show: true,
+      lineStyle: {
+        color: '#7BC0CB'
+      }
+    },
+    label: {
+      show: true,
+      position: 'outside',
+      rich: {
+        b: {
+          color: '#7BC0CB',
+          fontSize: 12,
+          lineHeight: 20
+        },
+        c: {
+          fontSize: 16,
+        },
+      },
+      // formatter: '{b|{b} \n}{c|{c}}{b|  亩}',
+      formatter: '{b|{b} }{c|{c}}{b| 次}',
+    },
+    tooltip: {
+      backgroundColor: '#033b77',
+      borderColor: '#21f2c4',
+      textStyle: {
+        color: '#fff',
+        fontSize: 13
+      },
+      formatter: (params: any) => {
+        if (
+          params.seriesName !== 'mouseoutSeries' &&
+          params.seriesName !== 'pie2d'
+        ) {
+          const bfb = (
+            (option.series[params.seriesIndex].pieData.endRatio -
+              option.series[params.seriesIndex].pieData.startRatio) *
+            100
+          ).toFixed(2)
+          return (
+            `${params.seriesName}<br/>` +
+            `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>` +
+            `${bfb}%`
+          )
+        }
+      }
+    },
+    xAxis3D: {
+      min: -1,
+      max: 1
+    },
+    yAxis3D: {
+      min: -1,
+      max: 1
+    },
+    zAxis3D: {
+      min: -1,
+      max: 1
+    },
+    grid3D: {
+      show: false,
+      boxHeight: boxHeight, // 圆环的高度
+      viewControl: {
+        // 3d效果可以放大、旋转等,请自己去查看官方配置
+        alpha, // 角度
+        distance, // 调整视角到主体的距离,类似调整zoom
+        rotateSensitivity: 0, // 设置为0无法旋转
+        zoomSensitivity: 0, // 设置为0无法缩放
+        panSensitivity: 0, // 设置为0无法平移
+        autoRotate: false // 自动旋转
+      }
+    },
+    series: series
+  }
+  return option
+}
+
+/**
+ * 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
+ */
+const getParametricEquation = (startRatio: number, endRatio: number, isSelected: boolean, isHovered: boolean, k: number, h: number) => {
+  // 计算
+  const midRatio = (startRatio + endRatio) / 2
+  const startRadian = startRatio * Math.PI * 2
+  const endRadian = endRatio * Math.PI * 2
+  const midRadian = midRatio * Math.PI * 2
+  // 如果只有一个扇形,则不实现选中效果。
+  if (startRatio === 0 && endRatio === 1) {
+    isSelected = false
+  }
+  // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
+  k = typeof k !== 'undefined' ? k : 1 / 3
+  // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
+  const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0
+  const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0
+  // 计算高亮效果的放大比例(未高亮,则比例为 1)
+  const hoverRate = isHovered ? 1.1 : 1
+  // 返回曲面参数方程
+  return {
+    u: {
+      min: -Math.PI,
+      max: Math.PI * 3,
+      step: Math.PI / 32
+    },
+    v: {
+      min: 0,
+      max: Math.PI * 2,
+      step: Math.PI / 20
+    },
+    x: function (u: number, v: number) {
+      if (u < startRadian) {
+        return (
+          offsetX +
+          Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate
+        )
+      }
+      if (u > endRadian) {
+        return (
+          offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate
+        )
+      }
+      return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate
+    },
+    y: function (u: number, v: number) {
+      if (u < startRadian) {
+        return (
+          offsetY +
+          Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate
+        )
+      }
+      if (u > endRadian) {
+        return (
+          offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate
+        )
+      }
+      return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate
+    },
+    z: function (u: number, v: number) {
+      if (u < -Math.PI * 0.5) {
+        return Math.sin(u)
+      }
+      if (u > Math.PI * 2.5) {
+        return Math.sin(u) * h * 0.1
+      }
+      return Math.sin(v) > 0 ? 1 * h * 0.1 : -1
+    }
+  }
+}
+
+/**
+ * 获取3d丙图的最高扇区的高度
+ */
+const getHeight3D = (series: any, height: number) => {
+  series.sort((a: any, b: any) => {
+    return b.pieData.value - a.pieData.value
+  })
+  return (height * 25) / series[0].pieData.value
+}
+
+/**
+ * 格式化浮点数
+ */
+const fomatFloat = (num: number, n: number) => {
+  let f = parseFloat(num.toString())
+  if (isNaN(f)) {
+    return false
+  }
+  f = Math.round(num * Math.pow(10, n)) / Math.pow(10, n) // n 幂
+  let s = f.toString()
+  let rs = s.indexOf('.')
+  // 判定如果是整数,增加小数点再补0
+  if (rs < 0) {
+    rs = s.length
+    s += '.'
+  }
+  while (s.length <= rs + n) {
+    s += '0'
+  }
+  return s
+}
+
+export {getPie3DSimple, getPie3D, getParametricEquation}

+ 73 - 0
src/utils/device.ts

@@ -0,0 +1,73 @@
+const deviceDetailList = [
+  {id: 1, name: '控制单元', inputName: '控制单元', gName: '控制单元', idx: [], detail: ""},
+  {
+    id: 11,
+    name: '高锰酸盐指数分析仪(NSY)',
+    gName: 'CODmn',
+    idx: ['CODMN'],
+    inputName: '高锰酸盐指数分析仪',
+    detail: "仪表选型来自水利部南京水利水文自动化研究所自研的NSY-CODmn型智能高锰酸盐指数在线分析仪。\n检测方法:高锰酸盐氧化法,光度滴定测量法。"
+  },
+  {
+    id: 6,
+    name: '总氮、氨氮分析仪(BBE)',
+    gName: '氨氮总氮',
+    idx: ['TN', 'NH3N'],
+    inputName: '总氮、氨氮分析仪',
+    detail: "仪表选型来自国产仪表厂商宝仪环境科技(上海)有限公司。\n总氮测量原理:碱性过硫酸盐消解紫外分光光度法;\n氨氮测量原理:水杨酸分光光度法。"
+  },
+  {
+    id: 4,
+    name: '总磷分析仪(PowerMon)',
+    gName: '总磷',
+    idx: ['TP'],
+    inputName: '总磷分析仪',
+    detail: "仪表选型来自国产仪表厂商宝怡环境科技(上海)有限公司。\n总磷测量原理:钼酸铵分光光度法。"
+  },
+  {
+    id: 10,
+    name: '总有机碳分析仪(GE)',
+    gName: 'TOC',
+    idx: [],
+    inputName: '总有机碳分析仪',
+    detail: "仪表选型来自著名仪表厂商岛津。型号为TOC-4200。测量原理是680℃燃烧催化剂氧化-NDIR检测法。"
+  },
+  {
+    id: 1,
+    name: '常规六参数测定仪',
+    gName: '多参数',
+    idx: ['WT', 'PH', 'COND', 'REDOX', 'TURB'],
+    inputName: '常规六参数测定仪',
+    detail: "监测指标:\n水温(工作原理:热电阻温度传感器是利用导体或半导体的电阻值随温度变化而变化的原理进行测温的);\n" +
+      "pH/ORP(工作原理:利用玻璃电极对水溶液中氢离子浓度变化的选择性响应,与Ag/AgCl参比电极一起在溶液中组合形成“化学电池”。通过对其电动势变化的测量,确定溶液的pH值。)\n" +
+      "电导率(四极式电导率电极。相比2极式电导率,多了另外两个极板,这两个极板之间没有电流流过,只负责提供稳定不变的参考电位。当系统有变化时,如电极受到 污染了,电极根据感测到的信号自动调节加在电流极板上的电压大小,从而自动实现背景补偿。)\n" +
+      "溶解氧(荧光法溶解氧探头在顶端的薄膜上覆盖了一层荧光染料。 当一束短波长的光照射到荧光染料时,荧光染料从应激态回到基态时会发射长波长的红光,红光就是测试信号。溶解氧的浓度越高,红光持续的时间越短,通过测试红光的持续时间就可以知道溶解氧的浓度。)\n" +
+      "浊度(浊度是由水中的悬浮颗粒引起的,悬浮颗粒会漫 反射入射光,通常采用 90 度那个方向的散射光做为 测试信号,这样测试出来的单位称为 NTU。)"
+  },
+  {
+    name: '预处理单元',
+    gName: '预处理单元',
+    inputName: '预处理单元',
+    idx: [],
+    detail: ""
+  },
+  {
+    name: '水情单元',
+    gName: '水情单元',
+    idx: [],
+    inputName: '水情单元',
+    detail: ""
+  },
+  // {
+  //   id: 2,
+  //   name: '总氮分析仪(PowerMon)',
+  //   inputName: '总氮分析仪',
+  //   detail: ""
+  // },
+];
+
+function getDeviceByName(name: string) {
+  return deviceDetailList.find(item => item.inputName === name);
+}
+
+export {deviceDetailList, getDeviceByName};

+ 196 - 0
src/views/Device.vue

@@ -0,0 +1,196 @@
+<script lang="ts" setup>
+import {onMounted, ref} from "vue";
+import {useRoute} from 'vue-router';
+import RightFrame from "@/components/RightFrame.vue";
+import Card01 from "@/components/card/Card01.vue";
+import StripeTable from "@/components/StripeTable.vue";
+import {getDeviceByName} from "@/utils/device";
+import Chart from "@/components/Chart.vue";
+
+const route = useRoute()
+const right3Ref = ref(null)
+const device = ref(getDeviceByName("高锰酸盐指数分析仪"))
+
+
+const deviceStatusColumns = [
+  {label: '设备名称', prop: 'name'},
+  {label: '设备位置', prop: 'loc', width: '110'},
+  {label: '设备状态', prop: 'status', width: '110'},
+]
+const deviceStatusData = [
+  {name: '总磷分析仪', loc: '100米', status: '正常'},
+  {name: '总氮分析仪', loc: '100米', status: '正常'},
+  {name: '水位计', loc: '100米', status: '正常'},
+  {name: '氢氮分析仪', loc: '100米', status: '正常'},
+]
+
+const accessoriesColumns = [
+  {label: '设备名称', prop: 'name'},
+  {label: '设备位置', prop: 'loc', width: '110'},
+  {label: '设备状态', prop: 'status', width: '110'},
+]
+const accessoriesData = [
+  {name: '总磷分析仪', loc: '100米', status: '正常'},
+  {name: '总氮分析仪', loc: '100米', status: '正常'},
+  {name: '水位计', loc: '100米', status: '正常'},
+  {name: '氢氮分析仪', loc: '100米', status: '正常'},
+]
+
+const reagentColumns = [
+  {label: '设备名称', prop: 'name'},
+  {label: '设备位置', prop: 'loc', width: '110'},
+  {label: '设备状态', prop: 'status', width: '110'},
+]
+const reagentData = [
+  {name: '总磷分析仪', loc: '100米', status: '正常'},
+  {name: '总氮分析仪', loc: '100米', status: '正常'},
+  {name: '水位计', loc: '100米', status: '正常'},
+  {name: '氢氮分析仪', loc: '100米', status: '正常'},
+]
+
+function reloadRight3(list) {
+  if (!list || list.length === 0) {
+    return
+  }
+
+  const typeSet = new Set()
+  const times = []
+  list.forEach((item, index) => {
+    Object.keys(item).forEach(key => typeSet.add(key))
+    times.push(item['maintainDate'] || index + 1)
+  })
+  typeSet.delete('maintainDate');
+  const types = Array.from(typeSet)
+  const rawData = [];
+  types.forEach(t => {
+    let array = []
+    list.forEach(d => array.push(d[t] || 0))
+    rawData.push(array)
+  })
+  const totalData = [];
+  for (let i = 0; i < rawData[0].length; ++i) {
+    let sum = 0;
+    for (let j = 0; j < rawData.length; ++j) {
+      sum += Number(rawData[j][i]);
+    }
+    totalData.push(sum);
+  }
+  const grid = {
+    left: 10,
+    right: 10,
+    top: 50,
+    bottom: 10,
+    containLabel: true
+  };
+  const series = types.map((name, sid) => {
+    return {
+      name,
+      type: 'bar',
+      stack: 'total',
+      barWidth: '60%',
+      label: {show: false,},
+      data: rawData[sid]
+    };
+  });
+  series.push({
+    name: '总计',
+    type: 'bar',
+    stack: 'total',
+    label: {
+      show: true,
+      position: 'top',
+      color: '#fff',
+      formatter: params => totalData[params.dataIndex] + '个'
+    },
+    data: totalData.map(t => 0)
+  })
+  let maxValue = totalData.reduce((a, b) => Math.max(a, b));
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      formatter(params) {
+        let circle = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;left:5px;background-color:`;
+        let info = params[0].axisValueLabel
+        for (let i = 0; i < params.length; i++) {
+          let param = params[i];
+          if (i === params.length - 1) {
+            info += `<br/>${circle}${param.color}"></span> ${param.seriesName}: ${totalData[param.dataIndex]}个`;
+          } else {
+            if (param.value > 0) {
+              info += `<br/>${circle}${param.color}"></span> ${param.seriesName}: ${param.value}个`;
+            }
+          }
+        }
+        return info;
+      }
+    },
+    legend: {
+      type: 'scroll',
+      icon: 'circle',
+      data: types,
+      selectedMode: false,
+      textStyle: {
+        color: '#fff'
+      },
+    },
+    grid,
+    yAxis: {
+      type: 'value',
+      name: '个',
+      nameTextStyle: {
+        color: '#fff',
+      },
+      minInterval: 1,
+      interval: Math.ceil(maxValue / 5),
+      axisLabel: {
+        color: '#fff',
+      },
+    },
+    xAxis: {
+      type: 'category',
+      axisLabel: {
+        color: '#fff',
+      },
+      data: times
+    },
+    series
+  };
+  right3Ref.value.loadChart(option)
+}
+
+onMounted(() => {
+  reloadRight3([])
+
+})
+</script>
+
+<template>
+  <right-frame>
+    <template #leftModule>
+      <card01 :title="device.name" style="height: 60%">
+        <p v-for="text in device.detail.split('\n')" class="introduce-text" v-html="text"></p>
+        <img v-if="device.img" :src="device.img" alt="" class="introduce-float-img"/>
+      </card01>
+      <card01 style="height: 40%" title="设备状态">
+        <stripe-table :columns="deviceStatusColumns" :data="deviceStatusData"></stripe-table>
+      </card01>
+    </template>
+    <template #rightModule>
+      <card01 style="height: 33%" title="配件运维信息">
+        <stripe-table :columns="accessoriesColumns" :data="accessoriesData"></stripe-table>
+      </card01>
+      <card01 style="height: 33%" title="试剂运维信息">
+        <stripe-table :columns="reagentColumns" :data="reagentData"></stripe-table>
+      </card01>
+      <card01 style="height: 33%" title="耗材余量监控">
+        <chart ref="right3Ref"></chart>
+      </card01>
+    </template>
+  </right-frame>
+</template>
+<style lang="scss" scoped>
+@use "@/assets/styles/introduce.scss";
+</style>

+ 112 - 216
src/views/IntellOper.vue

@@ -1,214 +1,118 @@
 <script lang="ts" setup>
-import { onMounted, reactive, ref } from 'vue'
-import { useRoute } from 'vue-router'
+import {onMounted, reactive, ref} from 'vue'
+import {useRoute} from 'vue-router'
 import RightFrame from '@/components/RightFrame.vue'
 import Card01 from '@/components/card/Card01.vue'
-import {stations} from '@/utils/station'
 import StripeTable from '@/components/StripeTable.vue'
 import ColorTag from '@/components/tag/ColorTag.vue'
-import ImageTag from '@/components/tag/ImageTag.vue'
-import { getDetailInfo, getDeviceInfo } from '@/api/device'
+import {getDeviceInfo, getOperationsPersonnelData} from '@/api/device'
 import Chart from '@/components/Chart.vue'
+import {getPie3DSimple} from "@/utils/chart";
 
 const route = useRoute()
-let dibaImage = ref(new URL('@/assets/images/dike.png', import.meta.url).href)
-const jiankong = new URL('@/assets/images/tmp/jiankong.png', import.meta.url).href
 
-
-function getStation(stcd) {
-  return stations.find(item => item.stcd === stcd)
-}
-
-const station = ref(getStation(route.params.stcd))
-const introduces = ref(station.value.detail?.split('\n'))
-const introduceImg = new URL(station.value.img, import.meta.url).href
-
-const deviceInfoColumns = [
-  { label: '设备名称', prop: 'name' },
-  { label: '设备位置', prop: 'loc', width: '110' },
-  { label: '设备状态', prop: 'status', width: '110' }
+const color = ['#005aff', '#f8b551']
+const equipmentStatusData = reactive({
+  total: 10,
+  onlineTotal: 8,
+  "缺数": 0,
+  "上下限异常": 1,
+  "数据中断": 0,
+  "其他": 1,
+})
+const operationsPersonnelColumns = [
+  {label: '姓名', prop: 'name'},
+  {label: '已处理', prop: 'processed', width: '100'},
+  {label: '未处理', prop: 'untreated', width: '100'},
 ]
-
-const deviceInfoData = [
-  { name: '总磷分析仪', loc: '100米', status: '正常' },
-  { name: '总氮分析仪', loc: '100米', status: '正常' },
-  { name: '水位计', loc: '100米', status: '正常' },
-  { name: '氢氮分析仪', loc: '100米', status: '正常' }
+const operationsPersonnelData = reactive([])
+const deviceColumns = [
+  {label: '设备类型', prop: 'deviceType', width: '110'},
+  {label: '设备名称', prop: 'deviceName'},
+  {label: '运行状态', prop: 'state', width: '90'}
 ]
-
-const wqData = ref([])
-const wlData = reactive({ ss: '3.80', zg: '4.65', zd: '4.65', bz: '4.65', jj: '4.65' })
-
-const moduleTableColumns = [
-  {
-    label: '耗材名称', prop: 'partsName', width: '100', convertFn: (data) => {
-      return data ? data.trim() : ''
-    }
-  },
-  {
-    label: '剩余有效期', prop: 'useDate', width: '100', convertFn: (data) => {
-      return Number(data).toFixed(0)
-    }
-  },
-  {
-    label: '时间', prop: 'offDate', width: '190', convertFn: (data) => {
-      return data
-    }
-  }
-]
-const reagentTableColumns = [
-  {
-    label: '试剂名称', prop: 'stnm', width: '110', convertFn: (data) => {
-      return data ? data.trim() : ''
-    }
-  },
-  {
-    label: '添加体积', prop: 'z', width: '80', convertFn: (data) => {
-      return Number(data).toFixed(2)
-    }
-  },
-  {
-    label: '时间', prop: 'tm', width: '200', convertFn: (data) => {
-      return data ? data.substring(5, 16) : ''
-    }
-  }
-]
-const deviceTableColumns = [
-  {
-    label: '设备类型', prop: 'deviceType', width: '110', convertFn: (data) => {
-      return data ? data.trim() : ''
-    }
-  },
-  {
-    label: '设备名称', prop: 'deviceName', width: '110', convertFn: (data) => {
-      return data ? data.trim() : ''
-    }
-  },{
-    label: '运行状态', prop: 'state', width: '90', convertFn: (data) => {
-      return data ? data.trim() : ''
-    }
-  },
-  {
-    label: '运行情况', prop: 'online', width: '100', convertFn: (data) => {
-      return data ? data.trim() : ''
-    }
-  }
-]
-const operationTableData = reactive([])
-const deviceTableData = reactive([])
+const deviceData = reactive([])
 
 const right3Ref = ref(null)
 
 function reloadRight3() {
-  const option = {
-    // backgroundColor: "#0B2D55",
-    tooltip: {
-      axisPointer: {
-        type: 'cross'
-      }
+  const optionData = [
+    {
+      name: '故障',
+      value: 1,
+      itemStyle: {
+        color: '#f3914b',
+      },
     },
-    grid: {
-      top: '14%',
-      left: '2%',
-      right: '4%',
-      bottom: '5%',
-      containLabel: true
+    {
+      name: '提醒',
+      value: 4,
+      itemStyle: {
+        color: '#f5bf53'
+      }
     },
-    xAxis: [{
-      type: 'category',
-      axisLine: { // 坐标轴轴线相关设置。数学上的x轴
-        show: true,
-        lineStyle: {
-          color: '#233e64'
-        }
-      },
-      axisLabel: { // 坐标轴刻度标签的相关设置
-        color: '#02cacf'
-      },
-      axisTick: { show: false },
-      data: ['1-16氯管', 'p3-16氯管', '3-16氯管']
-    }],
-    yAxis: [{
-      name: '个',
-      nameTextStyle: {
-        color: '#02cacf'
-      },
-      min: value => (value.min - 1).toFixed(0),
-      max: value => (value.max + 1).toFixed(0),
-      axisLabel: {
-        margin: 20,
-        color: '#02cacf'
-      },
-      splitLine: {
-        show: true,
-        lineStyle: {
-          color: '#233e64'
-        }
-      },
-      axisLine: {
-        show: true
+    {
+      name: '巡检',
+      value: 6,
+      itemStyle: {
+        color: '#92d1f4'
       }
-    }],
-    series: [{
-      name: '水位',
-      type: 'bar',
+    },
+    {
+      name: '定时巡检',
+      value: 3,
       itemStyle: {
-        color: function (params) {
-          // 根据params的
-          const colorsMap = [
-            '#4768ec',
-            '#92da76',
-            '#dc8a5e',
-          ]
-          //返回对应的颜色
-          return colorsMap[params.dataIndex]
-        }
-      },
-      barWidth: 50,
-      label: {
-        show: true,
-        position: 'top'
-      },
-      data: [3, 5, 6]
+        color: '#0e2d5b'
+      }
     }
-    ]
-  }
-  right3Ref.value.loadChart(option)
+  ]
+  const option = getPie3DSimple(optionData, 0.8);
+  //是否需要label指引线,如果要就添加一个透明的2d饼状图并调整角度使得labelLine和3d的饼状图对齐,并再次setOption
+  option.series.push({
+    name: 'pie2d',
+    type: 'pie',
+    label: {
+      opacity: 1,
+      position: 'outside',
+      fontSize: 12,
+      lineHeight: 20,
+      textStyle: {
+        fontSize: 12,
+        color: '#fff'
+      }
+    },
+    labelLine: {
+      length: 30,
+      length2: 30
+    },
+    minAngle: 10,
+    startAngle: -50, // 起始角度,支持范围[0, 360]。
+    clockwise: false, // 饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式
+    radius: ['40%', '60%'],
+    center: ['50%', '50%'],
+    data: optionData.map(item => {
+      item.itemStyle.opacity = 0
+      return item
+    })
+  });
+  right3Ref.value.loadChart(option, 'bindListen')
 }
 
 function getDeviceInfoList() {
   getDeviceInfo().then(res => {
-    deviceTableData.push(...res)
+    deviceData.push(...res)
   })
 }
+
 function getDeviceDetailInfoList() {
-  getDetailInfo().then(res => {
-    operationTableData.push(...res)
+  getOperationsPersonnelData().then(res => {
+    operationsPersonnelData.push(...res)
   })
 }
 
 const imgSrc = ref(new URL('@/assets/images/szpj.png', import.meta.url).href)
 
 onMounted(() => {
-  wqData.value = [
-    { label: '水温', value: '22.3', unit: '℃', imgSrc: new URL('@/assets/images/sz-wd.png', import.meta.url).href },
-    { label: '浊度', value: '235', unit: 'NTU', imgSrc: new URL('@/assets/images/sz-zd.png', import.meta.url).href },
-    {
-      label: '溶解氧',
-      value: '8.22',
-      unit: 'mg/L',
-      imgSrc: new URL('@/assets/images/sz-rjy.png', import.meta.url).href
-    },
-    {
-      label: '电导率',
-      value: '8.22',
-      unit: 'US/cm',
-      imgSrc: new URL('@/assets/images/sz-ddl.png', import.meta.url).href
-    },
-    { label: '总磷', value: '22.3', unit: 'mg/L', imgSrc: new URL('@/assets/images/sz-zl.png', import.meta.url).href },
-    { label: '总氮', value: '22.3', unit: 'mg/L', imgSrc: new URL('@/assets/images/sz-tn.png', import.meta.url).href },
-    { label: '氨氮', value: '22.3', unit: 'mg/L', imgSrc: new URL('@/assets/images/sz-ad.png', import.meta.url).href }
-  ]
   getDeviceInfoList()
   getDeviceDetailInfoList()
   reloadRight3()
@@ -218,69 +122,61 @@ onMounted(() => {
 <template>
   <right-frame>
     <template #leftModule>
-      <card01 style="height: 70%" title="运行实态">
-        <el-row style="height: 120px;">
+      <card01 style="height: 33%" title="运行实态">
+        <el-row style="height: 66%;">
           <el-col :span="7">
             <div class="device_num">
               <span class="background-image">设备管理</span>
-              <span style="font-size: 26px;font-weight: bold;">31</span>
+              <span style="font-size: 26px;font-weight: bold;">{{ equipmentStatusData.total }}</span>
             </div>
           </el-col>
           <el-col :span="10">
             <div style="display: flex;justify-content: center;">
               <div style="position: absolute;bottom:40%;font-size:16px;">设备清单</div>
-              <img :src="imgSrc" style="height: 120px;" />
+              <img :src="imgSrc" style="height: 120px;"/>
             </div>
           </el-col>
           <el-col :span="7">
             <div class="device_num">
               <span class="background-image">在线设备</span>
-              <span style="font-size: 26px;font-weight: bold;">30</span>
+              <span style="font-size: 26px;font-weight: bold;">{{ equipmentStatusData.onlineTotal }}</span>
             </div>
           </el-col>
         </el-row>
-        <el-row style="height: calc(100% - 120px)">
-          <stripe-table :columns="deviceTableColumns" :data="deviceTableData"></stripe-table>
+        <el-row :gutter="10" style="height: 33%;">
+          <el-col :span="6" style="height: 100%;">
+            <!--       #adcbe0     -->
+            <color-tag :value="equipmentStatusData['缺数']" backgroundColor="#bb232f" label="缺数"></color-tag>
+          </el-col>
+          <el-col :span="6" style="height: 100%;">
+            <!--       #adcbe0     -->
+            <color-tag :value="equipmentStatusData['上下限异常']" backgroundColor="#b38b30"
+                       label="上下限异常"></color-tag>
+          </el-col>
+          <el-col :span="6" style="height: 100%;">
+            <!--       #adcbe0     -->
+            <color-tag :value="equipmentStatusData['数据中断']" background-color="#97759c" label="数据中断"></color-tag>
+          </el-col>
+          <el-col :span="6" style="height: 100%;">
+            <color-tag :value="equipmentStatusData['其他']" backgroundColor="#adcbe0" label="其他"></color-tag>
+          </el-col>
         </el-row>
-
+      </card01>
+      <card01 style="height: 33%" title="维护情况">
+        <chart ref="right3Ref"></chart>
+      </card01>
+      <card01 style="height: 33%" title="运维人员统计">
+        <stripe-table :columns="operationsPersonnelColumns" :data="operationsPersonnelData"></stripe-table>
       </card01>
     </template>
     <template #rightModule>
-      <card01 style="height: 33%" title="配件运维信息">
-        <stripe-table :columns="moduleTableColumns" :data="operationTableData"></stripe-table>
-      </card01>
-      <card01 style="height: 33%" title="耗材余量监控">
-        <chart ref="right3Ref"></chart>
+      <card01 title="设备清单">
+        <stripe-table :columns="deviceColumns" :data="deviceData"></stripe-table>
       </card01>
     </template>
   </right-frame>
 </template>
 <style lang="scss" scoped>
-@use "@/assets/styles/introduce.scss";
-
-.difang-water-level {
-  position: absolute;
-  bottom: 10px;
-  width: 80%;
-  height: 60%;
-  background-color: rgba(0, 204, 255, 0.6);
-  border-top: 3px solid rgba(0, 204, 255);
-
-  .difang-water-level-value {
-    position: absolute;
-    top: -2rem;
-    font-size: 1.2rem;
-    color: #fff;
-  }
-
-}
-
-.difang-image {
-  position: relative;
-  width: 100%;
-  height: 100%;
-}
-
 .background-image {
   background-image: url(/src/assets/images/layout/menu-right.png);
   background-repeat: no-repeat;