Ver Fonte

feat: 完成指标趋势页面开发

yanglzh há 1 ano atrás
pai
commit
98d32a7869

+ 1 - 0
package.json

@@ -37,6 +37,7 @@
     "event-source-polyfill": "^1.0.31",
     "jsplumb": "^2.15.6",
     "jsrsasign": "^10.8.6",
+    "loadsh": "^0.0.4",
     "mitt": "^3.0.0",
     "nprogress": "^0.2.0",
     "pako": "^1.0.11",

+ 6 - 0
src/App.vue

@@ -18,6 +18,7 @@ import LockScreen from '/@/layout/lockScreen/index.vue';
 import Setings from '/@/layout/navBars/breadcrumb/setings.vue';
 import CloseFull from '/@/layout/navBars/breadcrumb/closeFull.vue';
 import api from '/@/api/system';
+import _ from 'loadsh'
 // 进入系统的时间
 sessionStorage.setItem('comeTime', Date.now().toString())
 
@@ -38,6 +39,11 @@ export default defineComponent({
 		});
 	},
 	setup() {
+		// 监听屏幕尺寸变化
+		window.onresize = _.debounce(() => {
+			store.commit('global/setResize')
+		}, 200)
+		
 		const { proxy } = <any>getCurrentInstance();
 		const setingsRef = ref();
 		const route = useRoute();

+ 4 - 0
src/api/device/index.ts

@@ -32,6 +32,7 @@ export default {
     getTypesAll: (data: object) => get('/system/plugins/getTypesAll', data),
     // 脚本更新
     script: (data: object) => put('/product/script/update', data),
+    getpropertyList: (params: object) => get('/product/tsl/property/all', params),
   },
   category: {
     getList: (params: object) => get('/product/category/list', params),
@@ -94,6 +95,9 @@ export default {
     record: (params: object) => get('/envirotronics/device_tree/record', params),
     param: (params: object) => get('/envirotronics/device_tree/param', params),
   },
+  analysis: {
+    deviceIndicatorTrend: (params: object) => get('/analysis/deviceIndicatorTrend', params),
+  },
   device: {
     getList: (params: object) => get('/product/device/bind_list', params),
     allList: (params: object) => get('/product/device/list', params),

+ 127 - 0
src/components/chart/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="echart" :class="{ bg, noPadding }" :style="{ height }" ref="chart"></div>
+</template>
+
+<script setup lang="ts">
+import * as echarts from 'echarts'
+import { onMounted, ref, watch } from 'vue'
+// import * as chartOptions from './options.js'
+import { useStore } from '/@/store/index'
+const store = useStore()
+
+const loadingOption = {
+	// maskColor: 'rgba(255, 255, 255, 0.1)'
+	maskColor: 'rgba(255, 255, 255, 0)',
+	text: '',
+	color: '#409eff',
+	// textColor: '#000',
+	// zlevel: 0,
+	// fontSize: 12,
+	// showSpinner: true,
+	spinnerRadius: 18,
+	lineWidth: 2,
+	// fontWeight: 'normal',
+	// fontStyle: 'normal',
+	// fontFamily: 'sans-serif'
+}
+
+const props = defineProps({
+	bg: Boolean,
+	noPadding: Boolean,
+	auto: {
+		type: Boolean,
+		default: false,
+	},
+	height: {
+		type: String,
+		default: '100%',
+	},
+	type: {
+		type: String,
+		default: 'Bar',
+	},
+	option: {
+		type: Object,
+		default: () => {
+			return {}
+		},
+	},
+})
+
+let myChart: any = null
+let optionCache: any = null
+const chart = ref()
+
+// 绘制图形
+const draw = (option?: object) => {
+	myChart && myChart.dispose()
+	myChart = echarts.init(chart.value)
+	myChart.hideLoading()
+	if (option) {
+		optionCache = option
+		myChart.setOption(option)
+	} else {
+		// myChart.setOption(chartOptions[`get${props.type}Option`]({ ...props.option }))
+	}
+}
+
+const loading = () => {
+	myChart?.setOption({}, { notMerge: true })
+	myChart?.showLoading(loadingOption)
+}
+
+const getChart = () => {
+	return myChart
+}
+
+const download = (name: string = 'chart picture') => {
+	const picInfo = myChart.getDataURL({
+		type: 'png',
+		pixelRatio: 2,
+		backgroundColor: '#fff',
+	})
+	let elink = document.createElement('a')
+	elink.download = name
+	elink.style.display = 'none'
+	elink.href = picInfo
+	document.body.appendChild(elink)
+	elink.click()
+	URL.revokeObjectURL(elink.href)
+	document.body.removeChild(elink)
+}
+
+// 配置变化 重绘
+watch(
+	() => props.option,
+	() => {
+		draw()
+	}
+)
+watch(
+	() => store.state.global.resize,
+	() => {
+		draw(optionCache)
+	}
+)
+
+onMounted(() => {
+	myChart = echarts.init(chart.value)
+	loading()
+	props.auto && draw()
+})
+
+defineExpose({ draw, loading, download, getChart })
+</script>
+
+<style scoped lang="scss">
+.echart {
+	height: 100%;
+	width: 100%;
+	// margin: 1vh 0;
+	padding: 2px;
+}
+
+.bg {
+	background: #444;
+}
+</style>

+ 482 - 0
src/components/chart/options.ts

@@ -0,0 +1,482 @@
+import { title } from "process";
+
+const fontFamily = 'CenturyGothic';
+
+function getPx(px: number) {
+  return Math.round((window.innerWidth * px) / 1920)
+  // return px
+}
+
+const grid = {
+  top: 25,
+  left: 15,
+  right: 25,
+  bottom: 0,
+  containLabel: true
+};
+
+export const echarts_loading_config = {
+  // text: "loading",
+  text: '',
+  color: '#409EFF',
+  textColor: '#999999',
+  maskColor: 'rgba(255, 255, 255, 0)',
+  fontSize: 18,
+  spinnerRadius: 17,
+  lineWidth: 2,
+  fontFamily
+};
+
+export default {
+  tooltip: {},
+  grid: grid,
+  xAxis: {
+    data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
+  },
+  yAxis: {},
+  series: [
+    {
+      name: '销量',
+      type: 'bar',
+      data: [5, 20, 36, 10, 10, 20]
+    }
+  ]
+};
+
+export function getPieOption({ data = [] as any[],
+  radius = ['30%', '50%'],
+  center = ['50%', '55%'],
+  legend = { bottom: '5%', left: 'center', top: '0' } as any,
+  rate = ''
+}) {
+  return {
+    // tooltip: {
+    //   trigger: 'item'
+    // },
+    legend: {
+      textStyle: {
+        fontSize: getPx(14),
+      },
+      itemWidth: getPx(20), itemHeight: getPx(14),
+      ...legend
+    },
+    title: {
+      text: rate,
+      top: 'center',
+      left: 'center',
+      // bottom: 0,
+      textStyle: {
+        fontSize: getPx(24),
+        // fontWeight: 'normal'
+      },
+    },
+    color: ['#2578f2', '#a1c7f8'],
+    series: [
+      {
+        name: '',
+        type: 'pie',
+        radius,
+        center,
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 5,
+          borderColor: '#fff',
+          borderWidth: 1
+        },
+        label: {
+          position: 'outer',
+          alignTo: 'labelLine',
+          bleedMargin: 5,
+          formatter: '{b}\n{d}%',
+          minMargin: 5,
+          edgeDistance: 10,
+          lineHeight: getPx(14),
+          fontSize: getPx(14),
+        },
+        labelLine: {
+          length: 15,
+          length2: 0,
+          maxSurfaceAngle: 80
+        },
+        data
+      }
+    ]
+  };
+}
+
+export function getPie3Option({ data = [] as any[],
+  radius = ['40%', '57%'],
+  center = ['50%', '45%'],
+  title = 'Server Group Name_1'
+}) {
+  return {
+    tooltip: {
+      trigger: 'item'
+    },
+    title: {
+      text: title,
+      bottom: '0',
+      left: 'center',
+      textStyle: {
+        fontSize: getPx(14),
+        fontWeight: 'normal'
+      },
+    },
+    color: ['#2578f2', '#a1c7f8'],
+    series: [
+      {
+        name: '',
+        type: 'pie',
+        radius,
+        center,
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 5,
+          borderColor: '#fff',
+          borderWidth: 1
+        },
+        label: {
+          position: 'outer',
+          alignTo: 'labelLine',
+          bleedMargin: 5,
+          formatter: '{b}\n{d}%',
+          minMargin: 5,
+          edgeDistance: 10,
+          lineHeight: getPx(12),
+          fontSize: getPx(12),
+        },
+        labelLine: {
+          length: 6,
+          length2: 6,
+          maxSurfaceAngle: 80
+        },
+        data
+      }
+    ]
+  };
+}
+
+export function getPie4Option({ data = [] as any[],
+  radius = ['40%', '57%'],
+  center = ['50%', '45%'],
+  title = 'Server Group Name_1'
+}) {
+  return {
+    tooltip: {
+      trigger: 'item',
+      formatter: function (params: any) {
+        // $.get('detail?name=' + params.name, function (content) {
+        //   callback(ticket, toHTML(content));
+        // });
+        return params.name + ': ' + params.data.count;
+      }
+    },
+    title: {
+      text: title,
+      bottom: '0',
+      left: 'center',
+      textStyle: {
+        fontSize: getPx(14),
+        fontWeight: 'normal'
+      },
+    },
+    color: ['#2578f2', '#a1c7f8'],
+    series: [
+      {
+        name: '',
+        type: 'pie',
+        radius,
+        center,
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 5,
+          borderColor: '#fff',
+          borderWidth: 1
+        },
+        label: {
+          position: 'outer',
+          alignTo: 'labelLine',
+          bleedMargin: 5,
+          formatter: '{b}\n{d}%',
+          minMargin: 5,
+          edgeDistance: 10,
+          lineHeight: getPx(12),
+          fontSize: getPx(12),
+        },
+        labelLine: {
+          length: 6,
+          length2: 6,
+          maxSurfaceAngle: 80
+        },
+        data
+      }
+    ]
+  };
+}
+
+export function getPie2Option({ data = [] as any[],
+  radius = ['65%', '85%'],
+  center = ['50%', '50%'],
+}) {
+  return {
+    title: {
+      text: data[0],
+      top: 'center',
+      left: 'center',
+      // bottom: 0,
+      textStyle: {
+        fontSize: getPx(40),
+        // fontWeight: 'normal'
+      },
+    },
+    color: ['#1891FF', '#EFEFEF'],
+    series: [
+      {
+        type: 'pie',
+        radius,
+        center,
+        labelLine: {
+          show: false
+        },
+        data
+      }
+    ]
+  };
+}
+
+export function getBarOption({
+  data = [12, 14, 0, 0] as any[],
+  data2 = [12, 14, 0, 0] as any[],
+  xAxis = ['Q1', 'Q2', 'Q3', 'Q4'] as any[],
+}) {
+  return {
+    tooltip: {
+      trigger: 'item'
+    },
+    grid: { top: 10, bottom: 0, left: 0, right: 0, containLabel: true },
+    color: ['#2578f2', '#a1c7f8'],
+    xAxis: {
+      type: 'category',
+      data: xAxis
+    },
+    yAxis: [{
+      type: 'value'
+    }, {
+      type: 'value',
+      splitLine: {
+        show: false
+      },
+      axisLabel: {
+        formatter: '{value}%'
+      }
+    }],
+    series: [
+      {
+        data,
+        type: 'bar',
+        barMaxWidth: '60%',
+      },
+      {
+        data: data2,
+        yAxisIndex: 1,
+        type: 'line',
+        smooth: true
+      },
+    ]
+  };
+}
+
+export function getRadarOption({
+  data1 = [12, 14, 0] as any[],
+  data2 = [12, 14, 0] as any[],
+  legend = [] as string[],
+}) {
+  const max = Math.max(...data1, ...data2);
+
+  return {
+    tooltip: {
+    },
+    legend: {
+      left: 'center',
+      textStyle: {
+        fontSize: getPx(16),
+      },
+      itemWidth: getPx(20), itemHeight: getPx(12), top: 0, data: legend
+    },
+    color: ['rgb(6, 176, 60)', 'rgb(253, 109, 90)'],
+    radar: {
+      indicator: [
+        { name: 'Non-Critical', max },
+        { name: 'Security\nAdvisory', max },
+        { name: 'Product Enhancement Advisory', max },
+        { name: 'Bug Fix\nAdvisory', max },
+      ],
+      center: ['50%', '55%'],
+      radius: 65,
+      nameGap: 4,
+      // startAngle: 45,
+      axisName: {
+        color: '#222',
+        fontSize: getPx(14),
+        padding: [0, 0]
+      },
+    },
+    series: [
+      {
+        type: 'radar',
+        data: [
+          {
+            value: data1,
+            name: legend[0]
+          },
+          {
+            value: data2,
+            name: legend[1]
+          }
+        ]
+      }
+    ]
+  };
+}
+
+export function getLineOption({
+  datas = [[12, 14, 0]] as any[],
+  xAxis = ['4', '5', '6'] as any[],
+  legend = [] as string[]
+}) {
+
+  const series = datas.map((data, i) => {
+    return {
+      data,
+      type: 'line',
+      name: legend[i],
+      smooth: true
+    }
+  })
+
+  return {
+    tooltip: {
+      trigger: 'axis',
+    },
+    legend: { left: 'center', top: 0, data: legend },
+    grid: { top: 30, bottom: 0, left: 0, right: 0, containLabel: true },
+    color: ['rgb(6, 176, 60)', 'rgb(253, 109, 90)'],
+    xAxis: {
+      type: 'category',
+      data: xAxis
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series
+  };
+}
+
+export function getLineAreaOption({
+  data1 = [12, 14, 0] as any[],
+  data2 = [12, 14, 0] as any[],
+  xAxis = ['4', '5', '6'] as any[],
+  legend = [] as string[]
+}) {
+  const color = ['rgb(36, 120, 242)', '#84b7f9']
+  return {
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: { left: 'center', top: 0, data: legend },
+    grid: { top: 40, bottom: 0, left: 0, right: 0, containLabel: true },
+    color,
+    xAxis: {
+      type: 'category',
+      // boundaryGap: false,
+      data: xAxis
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        data: data1,
+        type: 'line',
+        name: legend[0],
+        areaStyle: {
+          opacity: 0.2
+        },
+        label: {
+          show: true,
+          color: color[0],
+          position: 'top'
+        },
+        smooth: true
+      },
+      {
+        data: data2,
+        type: 'line',
+        name: legend[1],
+        areaStyle: {
+          opacity: 0.2
+        },
+        label: {
+          show: true,
+          color: color[1],
+          position: 'top'
+        },
+        smooth: true
+      },
+    ]
+  };
+}
+
+export function getBarRowOption({
+  data = [2, 3, 4, 5, 7] as any[],
+  xAxis = ['Group_1', 'Group_2', 'Group_3', 'Group_4', 'Group_5'] as any[],
+  legend = [] as string[],
+  max = undefined as any
+}) {
+  return {
+    tooltip: {
+      trigger: 'item',
+      formatter: "{b}: <b>{c}%</b>"
+    },
+    legend: { left: 'center', bottom: 0, data: legend },
+    grid: { top: 10, bottom: 28, left: 5, right: 15, containLabel: true },
+    color: ['#2578f2', '#a1c7f8'],
+    xAxis: {
+      type: 'value',
+      // splitNumber: 4,
+      max: max,
+      axisLabel: {
+        formatter: "{value}%"
+      }
+    },
+    yAxis: {
+      type: 'category',
+      axisTick: { show: false },
+      //开启鼠标事件!!这一句很重要
+      triggerEvent: true,
+      // nameLocation: 'end',
+      axisLine: {
+        show: false
+      },
+      axisLabel: {
+        // color: '#fff', // 文字颜色
+        // 文字省略
+        formatter: function (value: string) {
+          if (value.length > 9) {
+            return `${value.slice(0, 9)}...`
+          }
+          return value
+        }
+      },
+      data: xAxis
+    },
+    series: [
+      {
+        data,
+        type: 'bar',
+        name: legend[0],
+        barMaxWidth: 30
+      },
+    ]
+  };
+}

+ 8 - 0
src/store/interface/index.ts

@@ -96,3 +96,11 @@ export interface RootStateTypes {
 	userInfos: UserInfosState;
 	requestOldRoutes: RequestOldRoutesState;
 }
+
+// global
+export interface GlobalState {
+	resize: {
+		innerHeight: number;
+		innerWidth: number;
+	};
+}

+ 22 - 0
src/store/modules/global.ts

@@ -0,0 +1,22 @@
+import { Module } from 'vuex';
+import { GlobalState, RootStateTypes } from '/@/store/interface/index';
+
+const globalModule: Module<GlobalState, RootStateTypes> = {
+  namespaced: true,
+  state: {
+    resize: {
+      innerHeight: window.innerHeight,
+      innerWidth: window.innerWidth
+    }
+  },
+  mutations: {
+    setResize(state: any) {
+      state.resize = {
+        innerHeight: window.innerHeight,
+        innerWidth: window.innerWidth
+      }
+    },
+  },
+};
+
+export default globalModule;

+ 56 - 7
src/views/iot/dataAnalysis/exponentialTrend/index.vue

@@ -20,14 +20,18 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="选择属性" prop="deviceKey">
-          <el-select v-model="params.deviceKey" filterable placeholder="请选择属性" @change="deviceChange">
-            <el-option v-for="item in deviceList" :key="item.key" :label="item.name" :value="item.key">
+        <el-form-item label="选择属性" prop="properties">
+          <el-select v-model="params.properties" filterable placeholder="请选择属性" @change="propertyChange">
+            <el-option v-for="item in propertyList" :key="item.key" :label="item.name" :value="item.key">
               <span style="float: left">{{ item.name }}</span>
               <span style="float: right; font-size: 13px">{{ item.key }}</span>
             </el-option>
           </el-select>
         </el-form-item>
+        <el-form-item label="选择时间" prop="dateRange">
+          <el-date-picker v-model="params.dateRange" style="width: 360px" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" type="datetimerange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
+            :clearable="false"></el-date-picker>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" class="ml10" @click="getData">
             <el-icon>
@@ -37,6 +41,13 @@
           </el-button>
         </el-form-item>
       </el-form>
+      <div class="title">
+        <el-icon style="margin-right: 5px;">
+          <ele-Histogram />
+        </el-icon>
+        指标趋势统计图
+      </div>
+      <Chart class="flex1" height="12vw" ref="chart" style="margin-top: 20px;" v-loading="loading"></Chart>
     </el-card>
   </div>
 </template>
@@ -45,34 +56,72 @@
 import { ref, toRefs, reactive, onMounted, defineComponent } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import api from '/@/api/device';
+import dayjs from 'dayjs';
+import Chart from '/@/components/chart/index.vue'
+import { getLineOption } from '/@/components/chart/options'
 
 const productList = ref<any[]>([])
 const deviceList = ref<any[]>([])
+const propertyList = ref<any[]>([])
+const chart = ref()
+const loading = ref(false)
+const propertyName = ref('')
 
 const params = reactive({
   productKey: '',
   deviceKey: '',
+  properties: '',
+  dateRange: [dayjs().format('YYYY-MM-DD HH:00:00'), dayjs().format('YYYY-MM-DD HH:mm:ss')]
 })
 
-
 api.product.getLists({ status: 1 }).then((res: any) => {
   productList.value = res.product || [];
 });
 
 function getData() {
+  if (!params.productKey) return ElMessage('请选选择产品')
+  if (!params.deviceKey) return ElMessage('请选选择设备')
+  if (!params.properties) return ElMessage('请选选择属性')
 
+  loading.value = true
+  api.analysis.deviceIndicatorTrend(params).then((res: any[]) => {
+    console.log(res)
+    console.log(propertyName.value)
+    chart.value.draw(
+      getLineOption({
+        datas: [res.map(item => item.dataValue)],
+        xAxis: res.map(item => item.date),
+        legend: [propertyName.value]
+      })
+    )
+  }).finally(() => loading.value = false)
 }
 
 function productChange(productKey: string) {
   params.deviceKey = ''
+  params.properties = ''
   deviceList.value = []
+  propertyList.value = []
   api.device.allList({ productKey }).then((res: any) => {
     deviceList.value = res.device;
   });
+  api.product.getpropertyList({ productKey }).then((res: any) => {
+    propertyList.value = res;
+  });
 }
 
-function deviceChange() {
-
+function propertyChange(property: string) {
+  propertyName.value = propertyList.value.find(item => item.key === property)?.name
 }
 
-</script>
+</script>
+<style scoped>
+.title {
+  font-size: 14px;
+  font-weight: bold;
+  border-bottom: 1px solid #eee;
+  display: flex;
+  align-items: center;
+  padding-bottom: 6px;
+}
+</style>

+ 5 - 0
yarn.lock

@@ -2157,6 +2157,11 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+loadsh@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.npmmirror.com/loadsh/-/loadsh-0.0.4.tgz#5314babd12bb13315dde024a4ca70758c5489d2d"
+  integrity sha512-U+wLL8InpfRalWrr+0SuhWgGt10M4OyAk6G8xCYo2rwpiHtxZkWiFpjei0vO463ghW8LPCdhqQxXlMy2qicAEw==
+
 locate-path@^6.0.0:
   version "6.0.0"
   resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz"