|
@@ -1,16 +1,12 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import * as echarts from 'echarts'
|
|
|
|
|
import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
|
-import { rangeShortcuts } from '@/utils/formatTime'
|
|
|
|
|
-
|
|
|
|
|
|
|
+import { IotDeviceApi } from '@/api/pms/device'
|
|
|
import dayjs from 'dayjs'
|
|
import dayjs from 'dayjs'
|
|
|
-import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
|
|
|
|
-import { IotDeviceApi, cancelAllRequests } from '@/api/pms/device'
|
|
|
|
|
-import { IotStatApi } from '@/api/pms/stat'
|
|
|
|
|
-
|
|
|
|
|
-defineOptions({ name: 'TdDeviceDetail' })
|
|
|
|
|
|
|
+import { rangeShortcuts } from '@/utils/formatTime'
|
|
|
|
|
+import { IotStatApi, cancelAllRequests } from '@/api/pms/stat'
|
|
|
|
|
|
|
|
-dayjs.extend(quarterOfYear)
|
|
|
|
|
|
|
+import * as echarts from 'echarts'
|
|
|
|
|
+import { colors } from './color'
|
|
|
|
|
|
|
|
const { query } = useRoute()
|
|
const { query } = useRoute()
|
|
|
|
|
|
|
@@ -24,236 +20,100 @@ const data = ref({
|
|
|
carOnline: query.carOnline || ''
|
|
carOnline: query.carOnline || ''
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const disabledIdentifier = ref<string[]>(['online', 'vehicle_name', 'touchScreenDataAccumulate'])
|
|
|
|
|
-
|
|
|
|
|
-const specs = ref<any[]>([])
|
|
|
|
|
-const gatewayspecs = ref<any[]>([])
|
|
|
|
|
-const zhbdspecs = ref<any[]>([])
|
|
|
|
|
-const selectSpec = ref<Record<string, boolean>>({})
|
|
|
|
|
-const chartMap = ref<Record<string, { name: string; value: any[] }>>({})
|
|
|
|
|
-
|
|
|
|
|
-const specsLoading = ref(false)
|
|
|
|
|
-
|
|
|
|
|
-const lastTsMap = ref<Record<string, number>>({})
|
|
|
|
|
-
|
|
|
|
|
-// 每 10s 刷新定时器
|
|
|
|
|
-const timer = ref<NodeJS.Timeout | null>(null)
|
|
|
|
|
-
|
|
|
|
|
-const defaultDate = rangeShortcuts[0].value()
|
|
|
|
|
-const date = ref([
|
|
|
|
|
- dayjs(defaultDate[0]).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
- dayjs(defaultDate[1]).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
-])
|
|
|
|
|
-
|
|
|
|
|
-const reset = () => {
|
|
|
|
|
- cancelAllRequests().then(() => {
|
|
|
|
|
- const def = rangeShortcuts[0].value()
|
|
|
|
|
-
|
|
|
|
|
- date.value = [
|
|
|
|
|
- dayjs(def[0]).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
- dayjs(def[1]).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
- ]
|
|
|
|
|
- stopAutoFetch()
|
|
|
|
|
- if (chart) chart.clear()
|
|
|
|
|
- render()
|
|
|
|
|
- initLoad()
|
|
|
|
|
- })
|
|
|
|
|
|
|
+interface Dimensions {
|
|
|
|
|
+ identifier: string
|
|
|
|
|
+ name: string
|
|
|
|
|
+ value: string
|
|
|
|
|
+ color?: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const handleDateChange = () => {
|
|
|
|
|
- cancelAllRequests().then(() => {
|
|
|
|
|
- stopAutoFetch()
|
|
|
|
|
- if (chart) chart.clear()
|
|
|
|
|
- render()
|
|
|
|
|
- initLoad(false)
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const handleClickSpec = (modelName: string) => {
|
|
|
|
|
- selectSpec.value[modelName] = !selectSpec.value[modelName]
|
|
|
|
|
- chart?.setOption({
|
|
|
|
|
- legend: {
|
|
|
|
|
- selected: selectSpec.value
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- // getIntervalArr()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const chartRef = ref<HTMLDivElement | null>(null)
|
|
|
|
|
-let chart: echarts.ECharts | null = null
|
|
|
|
|
-
|
|
|
|
|
-const exportChart = () => {
|
|
|
|
|
- if (!chart) return
|
|
|
|
|
- let img = new Image()
|
|
|
|
|
- img.src = chart.getDataURL({
|
|
|
|
|
- type: 'png',
|
|
|
|
|
- pixelRatio: 1,
|
|
|
|
|
- backgroundColor: '#fff'
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- img.onload = function () {
|
|
|
|
|
- let canvas = document.createElement('canvas')
|
|
|
|
|
- canvas.width = img.width
|
|
|
|
|
- canvas.height = img.height
|
|
|
|
|
- let ctx = canvas.getContext('2d')
|
|
|
|
|
- ctx?.drawImage(img, 0, 0)
|
|
|
|
|
- let dataURL = canvas.toDataURL('image/png')
|
|
|
|
|
-
|
|
|
|
|
- let a = document.createElement('a')
|
|
|
|
|
|
|
+const dimensions = ref<Dimensions[]>([])
|
|
|
|
|
+const gatewayDimensions = ref<Dimensions[]>([])
|
|
|
|
|
+const carDimensions = ref<Dimensions[]>([])
|
|
|
|
|
|
|
|
- let event = new MouseEvent('click')
|
|
|
|
|
|
|
+const disabledDimensions = ref<string[]>(['online', 'vehicle_name'])
|
|
|
|
|
|
|
|
- a.href = dataURL
|
|
|
|
|
- a.download = `${data.value.deviceName}-设备监控-${dayjs().format('YYYY-MM-DD HH:mm:ss')}.png`
|
|
|
|
|
- a.dispatchEvent(event)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+interface SelectedDimension {
|
|
|
|
|
+ [key: Dimensions['name']]: boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const chartInit = () => {
|
|
|
|
|
- if (!chart) return
|
|
|
|
|
|
|
+const selectedDimension = ref<SelectedDimension>({})
|
|
|
|
|
|
|
|
- chart.on('legendselectchanged', (params: any) => {
|
|
|
|
|
- selectSpec.value = params.selected
|
|
|
|
|
- })
|
|
|
|
|
|
|
+const dimensionLoading = ref(false)
|
|
|
|
|
|
|
|
- window.addEventListener('resize', () => {
|
|
|
|
|
- if (chart) chart.resize()
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 映射区间相关
|
|
|
|
|
-let intervalArr = ref<number[]>([])
|
|
|
|
|
-let maxInterval = ref(0)
|
|
|
|
|
-let minInterval = ref(0)
|
|
|
|
|
-
|
|
|
|
|
-// 1. 加载 specs
|
|
|
|
|
-const loadSpecs = async () => {
|
|
|
|
|
|
|
+async function loadDimensions() {
|
|
|
if (!query.id) return
|
|
if (!query.id) return
|
|
|
- specsLoading.value = true
|
|
|
|
|
- const res = await IotDeviceApi.getIotDeviceTds(Number(query.id))
|
|
|
|
|
- const zhbdres = await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))
|
|
|
|
|
-
|
|
|
|
|
- zhbdspecs.value = zhbdres.sort((a, b) => b.modelOrder - a.modelOrder)
|
|
|
|
|
- gatewayspecs.value = res.sort((a, b) => b.modelOrder - a.modelOrder)
|
|
|
|
|
-
|
|
|
|
|
- specs.value = [
|
|
|
|
|
- ...JSON.parse(JSON.stringify(gatewayspecs.value)),
|
|
|
|
|
- ...JSON.parse(JSON.stringify(zhbdspecs.value))
|
|
|
|
|
- ]
|
|
|
|
|
-
|
|
|
|
|
- selectSpec.value = specs.value.reduce(
|
|
|
|
|
- (acc, spec, index: number) => {
|
|
|
|
|
- acc[spec.modelName] = index === 0
|
|
|
|
|
- return acc
|
|
|
|
|
- },
|
|
|
|
|
- {} as Record<string, boolean>
|
|
|
|
|
- )
|
|
|
|
|
|
|
|
|
|
- specsLoading.value = false
|
|
|
|
|
|
|
+ dimensionLoading.value = true
|
|
|
|
|
|
|
|
- chartMap.value = specs.value
|
|
|
|
|
- .filter((spec) => !disabledIdentifier.value.includes(spec.identifier))
|
|
|
|
|
- .reduce(
|
|
|
|
|
- (acc, spec) => {
|
|
|
|
|
- acc[spec.identifier] = { name: spec.modelName, value: [] }
|
|
|
|
|
- return acc
|
|
|
|
|
- },
|
|
|
|
|
- {} as Record<string, { name: string; value: any[] }>
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const chartLoading = ref(false)
|
|
|
|
|
-
|
|
|
|
|
-const initLoad = async (real_time: boolean = true) => {
|
|
|
|
|
- if (!specs.value.length) return
|
|
|
|
|
-
|
|
|
|
|
- Object.keys(chartMap.value).forEach((identifier) => {
|
|
|
|
|
- chartMap.value[identifier].value = []
|
|
|
|
|
- lastTsMap.value[identifier] = 0
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- chartLoading.value = true
|
|
|
|
|
-
|
|
|
|
|
- for (const identifier of Object.keys(chartMap.value)) {
|
|
|
|
|
- const res = await IotStatApi.getDeviceInfoChart(
|
|
|
|
|
- data.value.deviceCode,
|
|
|
|
|
- identifier,
|
|
|
|
|
- date.value[0],
|
|
|
|
|
- date.value[1]
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- const sorted = res
|
|
|
|
|
- .sort((a, b) => a.ts - b.ts)
|
|
|
|
|
- .map((item) => ({
|
|
|
|
|
- ts: item.ts,
|
|
|
|
|
- value: item.value
|
|
|
|
|
- }))
|
|
|
|
|
- chartMap.value[identifier].value = sorted
|
|
|
|
|
- lastTsMap.value[identifier] = sorted.at(-1)?.ts ?? 0
|
|
|
|
|
|
|
+ const gateway = (((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as any[]) ?? [])
|
|
|
|
|
+ .sort((a, b) => b.modelOrder - a.modelOrder)
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ identifier: item.identifier,
|
|
|
|
|
+ name: item.modelName,
|
|
|
|
|
+ value: item.value
|
|
|
|
|
+ }))
|
|
|
|
|
+ const car = (((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as any[]) ?? [])
|
|
|
|
|
+ .sort((a, b) => b.modelOrder - a.modelOrder)
|
|
|
|
|
+ .map((item) => ({
|
|
|
|
|
+ identifier: item.identifier,
|
|
|
|
|
+ name: item.modelName,
|
|
|
|
|
+ value: item.value
|
|
|
|
|
+ }))
|
|
|
|
|
|
|
|
- updateSingleSeries(identifier)
|
|
|
|
|
|
|
+ dimensions.value = [...gateway, ...car]
|
|
|
|
|
+ .filter((item) => !disabledDimensions.value.includes(item.identifier))
|
|
|
|
|
+ .map((item, index) => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ color: colors[index]
|
|
|
|
|
+ }))
|
|
|
|
|
|
|
|
- // if (selectSpec.value[chartMap.value[identifier].name]) {
|
|
|
|
|
- // getIntervalArr()
|
|
|
|
|
- // }
|
|
|
|
|
|
|
+ gatewayDimensions.value = gateway
|
|
|
|
|
+ carDimensions.value = car
|
|
|
|
|
|
|
|
- chartLoading.value = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ selectedDimension.value = Object.fromEntries(dimensions.value.map((item) => [item.name, false]))
|
|
|
|
|
|
|
|
- console.log('chartMap.value :>> ', JSON.stringify(Object.values(chartMap.value), null, 2))
|
|
|
|
|
|
|
+ selectedDimension.value[dimensions.value[0].name] = true
|
|
|
|
|
|
|
|
- if (real_time) startAutoFetch()
|
|
|
|
|
|
|
+ dimensionLoading.value = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const startAutoFetch = () => {
|
|
|
|
|
- timer.value = setInterval(fetchIncrementData, 10000)
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const selectedDate = ref<string[]>([
|
|
|
|
|
+ ...rangeShortcuts[3].value().map((v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
|
|
+])
|
|
|
|
|
|
|
|
-const stopAutoFetch = () => {
|
|
|
|
|
- if (timer.value) clearInterval(timer.value)
|
|
|
|
|
- timer.value = null
|
|
|
|
|
|
|
+interface ChartData {
|
|
|
|
|
+ [key: Dimensions['name']]: { ts: number; value: number }[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const fetchIncrementData = () => {
|
|
|
|
|
- for (const identifier of Object.keys(chartMap.value)) {
|
|
|
|
|
- const lastTs = lastTsMap.value[identifier]
|
|
|
|
|
- if (!lastTs) continue
|
|
|
|
|
-
|
|
|
|
|
- IotStatApi.getDeviceInfoChart(
|
|
|
|
|
- data.value.deviceCode,
|
|
|
|
|
- identifier,
|
|
|
|
|
- dayjs(lastTs).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
- dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
- ).then((res) => {
|
|
|
|
|
- if (!res.length) return
|
|
|
|
|
|
|
+const chartData = ref<ChartData>({})
|
|
|
|
|
|
|
|
- const sorted = res.sort((a, b) => a.ts - b.ts)
|
|
|
|
|
|
|
+let intervalArr = ref<number[]>([])
|
|
|
|
|
+let maxInterval = ref(0)
|
|
|
|
|
+let minInterval = ref(0)
|
|
|
|
|
|
|
|
- // push 到本地
|
|
|
|
|
- chartMap.value[identifier].value.push(...sorted)
|
|
|
|
|
- // 更新 lastTs
|
|
|
|
|
- lastTsMap.value[identifier] = sorted.at(-1).ts
|
|
|
|
|
|
|
+const chartRef = ref<HTMLDivElement | null>(null)
|
|
|
|
|
+let chart: echarts.ECharts | null = null
|
|
|
|
|
|
|
|
- appendToSeries(identifier, chartMap.value[identifier].value)
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// const genderIntervalArrDebounce = useDebounceFn(
|
|
|
|
|
+// (init: boolean = false) => genderIntervalArr(init),
|
|
|
|
|
+// 300
|
|
|
|
|
+// )
|
|
|
|
|
|
|
|
-const getIntervalArr = (init: boolean = false) => {
|
|
|
|
|
|
|
+function genderIntervalArr(init: boolean = false) {
|
|
|
const values: number[] = []
|
|
const values: number[] = []
|
|
|
|
|
|
|
|
- for (const [key, value] of Object.entries(selectSpec.value)) {
|
|
|
|
|
|
|
+ for (const [key, value] of Object.entries(selectedDimension.value)) {
|
|
|
if (value) {
|
|
if (value) {
|
|
|
- const identifier = specs.value.find((spec) => spec.modelName === key)?.identifier
|
|
|
|
|
- values.push(...(chartMap.value[identifier]?.value?.map((item) => item.value) ?? []))
|
|
|
|
|
|
|
+ values.push(...(chartData.value[key]?.map((item) => item.value) ?? []))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const maxVal = values.length === 0 ? 10000 : Math.max(...values)
|
|
const maxVal = values.length === 0 ? 10000 : Math.max(...values)
|
|
|
- const minVal = Math.min(...values, -100)
|
|
|
|
|
|
|
+ const minVal = values.length === 0 ? 0 : Math.min(...values)
|
|
|
|
|
|
|
|
const maxDigits = (Math.floor(maxVal) + '').length
|
|
const maxDigits = (Math.floor(maxVal) + '').length
|
|
|
- const minDigits = (Math.floor(Math.abs(minVal)) + '').length - 2
|
|
|
|
|
|
|
+ const minDigits = (Math.floor(Math.abs(minVal)) + '').length
|
|
|
const interval = Math.max(maxDigits, minDigits)
|
|
const interval = Math.max(maxDigits, minDigits)
|
|
|
|
|
|
|
|
maxInterval.value = interval
|
|
maxInterval.value = interval
|
|
@@ -274,18 +134,39 @@ const getIntervalArr = (init: boolean = false) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const render = () => {
|
|
|
|
|
|
|
+function chartInit() {
|
|
|
|
|
+ if (!chart) return
|
|
|
|
|
+
|
|
|
|
|
+ chart.on('legendselectchanged', (params: any) => {
|
|
|
|
|
+ selectedDimension.value = params.selected
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
|
|
+ if (chart) chart.resize()
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function render() {
|
|
|
if (!chartRef.value) return
|
|
if (!chartRef.value) return
|
|
|
|
|
|
|
|
- if (!chart) chart = echarts.init(chartRef.value)
|
|
|
|
|
|
|
+ if (!chart) chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
|
|
|
|
|
|
|
|
chartInit()
|
|
chartInit()
|
|
|
|
|
|
|
|
- getIntervalArr(true)
|
|
|
|
|
|
|
+ genderIntervalArr(true)
|
|
|
|
|
|
|
|
chart.setOption({
|
|
chart.setOption({
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '8%',
|
|
|
|
|
+ top: '0%',
|
|
|
|
|
+ right: '8%',
|
|
|
|
|
+ bottom: '12%'
|
|
|
|
|
+ },
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
trigger: 'axis',
|
|
trigger: 'axis',
|
|
|
|
|
+ axisPointer: {
|
|
|
|
|
+ type: 'line'
|
|
|
|
|
+ },
|
|
|
formatter: (params) => {
|
|
formatter: (params) => {
|
|
|
let d = `${params[0].axisValueLabel}<br>`
|
|
let d = `${params[0].axisValueLabel}<br>`
|
|
|
const exist: string[] = []
|
|
const exist: string[] = []
|
|
@@ -295,25 +176,30 @@ const render = () => {
|
|
|
return true
|
|
return true
|
|
|
})
|
|
})
|
|
|
let item = params.map(
|
|
let item = params.map(
|
|
|
- (el) => `${el.marker} ${el.seriesName}: ${el.value[2].toFixed(2)}<br>`
|
|
|
|
|
|
|
+ (el) => `<div class="flex items-center justify-between mt-1">
|
|
|
|
|
+ <span>${el.marker} ${el.seriesName}</span>
|
|
|
|
|
+ <span>${el.value[2].toFixed(2)}</span>
|
|
|
|
|
+ </div>`
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
return d + item.join('')
|
|
return d + item.join('')
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- dataZoom: [
|
|
|
|
|
- { type: 'inside', xAxisIndex: 0 },
|
|
|
|
|
- { type: 'slider', xAxisIndex: 0 }
|
|
|
|
|
- ],
|
|
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'time',
|
|
type: 'time',
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
- formatter: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
- rotate: 20
|
|
|
|
|
|
|
+ formatter: (v) => dayjs(v).format('YYYY-MM-DD\nHH:mm:ss'),
|
|
|
|
|
+ rotate: 0,
|
|
|
|
|
+ align: 'left'
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
+ dataZoom: [
|
|
|
|
|
+ { type: 'inside', xAxisIndex: 0 },
|
|
|
|
|
+ { type: 'slider', xAxisIndex: 0 }
|
|
|
|
|
+ ],
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
- min: -minInterval.value,
|
|
|
|
|
|
|
+ min: minInterval.value,
|
|
|
max: maxInterval.value,
|
|
max: maxInterval.value,
|
|
|
interval: 1,
|
|
interval: 1,
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
@@ -322,80 +208,209 @@ const render = () => {
|
|
|
|
|
|
|
|
return num.toLocaleString()
|
|
return num.toLocaleString()
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ show: false
|
|
|
},
|
|
},
|
|
|
legend: {
|
|
legend: {
|
|
|
- data: Object.values(chartMap.value).map((i) => i.name),
|
|
|
|
|
- selected: selectSpec.value,
|
|
|
|
|
|
|
+ data: dimensions.value.map((item) => item.name),
|
|
|
|
|
+ selected: selectedDimension.value,
|
|
|
show: false
|
|
show: false
|
|
|
},
|
|
},
|
|
|
- series: Object.keys(chartMap.value).map((identifier) => ({
|
|
|
|
|
- name: chartMap.value[identifier].name,
|
|
|
|
|
|
|
+ series: dimensions.value.map((item) => ({
|
|
|
|
|
+ name: item.name,
|
|
|
type: 'line',
|
|
type: 'line',
|
|
|
smooth: true,
|
|
smooth: true,
|
|
|
showSymbol: false,
|
|
showSymbol: false,
|
|
|
|
|
+ color: item.color,
|
|
|
data: [] // 占位数组
|
|
data: [] // 占位数组
|
|
|
}))
|
|
}))
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const updateSingleSeries = (identifier: string) => {
|
|
|
|
|
|
|
+function mapData({ value, ts }) {
|
|
|
|
|
+ if (value === 0) return [ts, 0, 0]
|
|
|
|
|
+
|
|
|
|
|
+ const isPositive = value > 0
|
|
|
|
|
+ const absItem = Math.abs(value)
|
|
|
|
|
+
|
|
|
|
|
+ const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
|
|
|
|
|
+ const min_index = intervalArr.value.findIndex((v) => v === min_value)
|
|
|
|
|
+
|
|
|
|
|
+ const new_value =
|
|
|
|
|
+ (absItem - min_value) / (intervalArr.value[min_index + 1] - intervalArr.value[min_index]) +
|
|
|
|
|
+ min_index
|
|
|
|
|
+
|
|
|
|
|
+ return [ts, isPositive ? new_value : -new_value, value]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateSingleSeries(name: string) {
|
|
|
if (!chart) render()
|
|
if (!chart) render()
|
|
|
if (!chart) return
|
|
if (!chart) return
|
|
|
|
|
|
|
|
- const idx = Object.keys(chartMap.value).indexOf(identifier)
|
|
|
|
|
|
|
+ const idx = dimensions.value.findIndex((item) => item.name === name)
|
|
|
if (idx === -1) return
|
|
if (idx === -1) return
|
|
|
|
|
|
|
|
- const data = chartMap.value[identifier].value.map((v) => mapData(v))
|
|
|
|
|
|
|
+ const data = chartData.value[name].map((v) => mapData(v))
|
|
|
|
|
|
|
|
chart.setOption({
|
|
chart.setOption({
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- name: chartMap.value[identifier].name,
|
|
|
|
|
- data
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ series: [{ name, data }]
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const appendToSeries = (identifier, list) => {
|
|
|
|
|
- if (!chart) return
|
|
|
|
|
|
|
+const lastTsMap = ref<Record<Dimensions['name'], number>>({})
|
|
|
|
|
|
|
|
- const idx = Object.keys(chartMap.value).indexOf(identifier)
|
|
|
|
|
- if (idx === -1) return
|
|
|
|
|
|
|
+async function fetchIncrementData() {
|
|
|
|
|
+ for (const { identifier, name } of dimensions.value) {
|
|
|
|
|
+ const lastTs = lastTsMap.value[name]
|
|
|
|
|
+ if (!lastTs) continue
|
|
|
|
|
|
|
|
- const data = list.map(mapData)
|
|
|
|
|
|
|
+ IotStatApi.getDeviceInfoChart(
|
|
|
|
|
+ data.value.deviceCode,
|
|
|
|
|
+ identifier,
|
|
|
|
|
+ dayjs(lastTs).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
+ dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
+ ).then((res) => {
|
|
|
|
|
+ if (!res.length) return
|
|
|
|
|
|
|
|
- chart.setOption({
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- name: chartMap.value[identifier].name,
|
|
|
|
|
- data
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ const sorted = res.sort((a, b) => a.ts - b.ts)
|
|
|
|
|
+
|
|
|
|
|
+ // push 到本地
|
|
|
|
|
+ chartData.value[name].push(...sorted)
|
|
|
|
|
+ // 更新 lastTs
|
|
|
|
|
+ lastTsMap.value[identifier] = sorted.at(-1).ts
|
|
|
|
|
+
|
|
|
|
|
+ // 更新图表
|
|
|
|
|
+ updateSingleSeries(name)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const mapData = ({ value, ts }) => {
|
|
|
|
|
- if (value === 0) return [ts, 0, 0]
|
|
|
|
|
|
|
+const timer = ref<NodeJS.Timeout | null>(null)
|
|
|
|
|
|
|
|
- const isPositive = value > 0
|
|
|
|
|
- const absItem = Math.abs(value)
|
|
|
|
|
|
|
+function startAutoFetch() {
|
|
|
|
|
+ timer.value = setInterval(fetchIncrementData, 10000)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
|
|
|
|
|
- const min_index = intervalArr.value.findIndex((v) => v === min_value)
|
|
|
|
|
|
|
+function stopAutoFetch() {
|
|
|
|
|
+ if (timer.value) clearInterval(timer.value)
|
|
|
|
|
+ timer.value = null
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const new_value =
|
|
|
|
|
- (absItem - min_value) / (intervalArr.value[min_index + 1] - intervalArr.value[min_index]) +
|
|
|
|
|
- min_index
|
|
|
|
|
|
|
+const chartLoading = ref(false)
|
|
|
|
|
|
|
|
- return [ts, isPositive ? new_value : -new_value, value]
|
|
|
|
|
|
|
+async function initLoadChartData(real_time: boolean = true) {
|
|
|
|
|
+ if (!dimensions.value.length) return
|
|
|
|
|
+
|
|
|
|
|
+ chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
|
|
|
|
|
+
|
|
|
|
|
+ chartLoading.value = true
|
|
|
|
|
+
|
|
|
|
|
+ for (const { identifier, name } of dimensions.value) {
|
|
|
|
|
+ const res = await IotStatApi.getDeviceInfoChart(
|
|
|
|
|
+ data.value.deviceCode,
|
|
|
|
|
+ identifier,
|
|
|
|
|
+ selectedDate.value[0],
|
|
|
|
|
+ selectedDate.value[1]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const sorted = res
|
|
|
|
|
+ .sort((a, b) => a.ts - b.ts)
|
|
|
|
|
+ .map((item) => ({ ts: item.ts, value: item.value }))
|
|
|
|
|
+
|
|
|
|
|
+ chartData.value[name] = sorted
|
|
|
|
|
+
|
|
|
|
|
+ lastTsMap.value[name] = sorted.at(-1)?.ts ?? 0
|
|
|
|
|
+
|
|
|
|
|
+ updateSingleSeries(name)
|
|
|
|
|
+
|
|
|
|
|
+ chartLoading.value = false
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedDimension.value[name]) {
|
|
|
|
|
+ genderIntervalArr()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (real_time) startAutoFetch()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-onMounted(async () => {
|
|
|
|
|
- await loadSpecs()
|
|
|
|
|
|
|
+async function initfn(load: boolean = true, real_time: boolean = true) {
|
|
|
|
|
+ if (load) await loadDimensions()
|
|
|
render()
|
|
render()
|
|
|
- initLoad()
|
|
|
|
|
|
|
+ initLoadChartData(real_time)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ initfn()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function reset() {
|
|
|
|
|
+ cancelAllRequests().then(() => {
|
|
|
|
|
+ selectedDate.value = rangeShortcuts[0]
|
|
|
|
|
+ .value()
|
|
|
|
|
+ .map((v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
|
|
+
|
|
|
|
|
+ stopAutoFetch()
|
|
|
|
|
+ if (chart) chart.clear()
|
|
|
|
|
+ initfn(false)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleDateChange() {
|
|
|
|
|
+ cancelAllRequests().then(() => {
|
|
|
|
|
+ stopAutoFetch()
|
|
|
|
|
+ if (chart) chart.clear()
|
|
|
|
|
+ initfn(false, false)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleClickSpec(modelName: string) {
|
|
|
|
|
+ selectedDimension.value[modelName] = !selectedDimension.value[modelName]
|
|
|
|
|
+ chart?.setOption({
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ selected: selectedDimension.value
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ chart?.resize()
|
|
|
|
|
+ // genderIntervalArrDebounce()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const exportChart = () => {
|
|
|
|
|
+ if (!chart) return
|
|
|
|
|
+ let img = new Image()
|
|
|
|
|
+ img.src = chart.getDataURL({
|
|
|
|
|
+ type: 'png',
|
|
|
|
|
+ pixelRatio: 1,
|
|
|
|
|
+ backgroundColor: '#fff'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ img.onload = function () {
|
|
|
|
|
+ let canvas = document.createElement('canvas')
|
|
|
|
|
+ canvas.width = img.width
|
|
|
|
|
+ canvas.height = img.height
|
|
|
|
|
+ let ctx = canvas.getContext('2d')
|
|
|
|
|
+ ctx?.drawImage(img, 0, 0)
|
|
|
|
|
+ let dataURL = canvas.toDataURL('image/png')
|
|
|
|
|
+
|
|
|
|
|
+ let a = document.createElement('a')
|
|
|
|
|
+
|
|
|
|
|
+ let event = new MouseEvent('click')
|
|
|
|
|
+
|
|
|
|
|
+ a.href = dataURL
|
|
|
|
|
+ a.download = `${data.value.deviceName}-设备监控-${dayjs().format('YYYY-MM-DD HH:mm:ss')}.png`
|
|
|
|
|
+ a.dispatchEvent(event)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const maxmin = computed(() => {
|
|
|
|
|
+ if (!dimensions.value.length) return []
|
|
|
|
|
+ return dimensions.value
|
|
|
|
|
+ .filter((v) => selectedDimension.value[v.name])
|
|
|
|
|
+ .map((v) => ({
|
|
|
|
|
+ name: v.name,
|
|
|
|
|
+ color: v.color,
|
|
|
|
|
+ max: Math.max(...(chartData.value[v.name]?.map((v) => v.value) ?? [])).toFixed(2),
|
|
|
|
|
+ min: Math.min(...(chartData.value[v.name]?.map((v) => v.value) ?? [])).toFixed(2)
|
|
|
|
|
+ }))
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
@@ -463,43 +478,41 @@ onUnmounted(() => {
|
|
|
<div class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow min-h-50">
|
|
<div class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow min-h-50">
|
|
|
<header class="font-medium text-center w-full">网关数采</header>
|
|
<header class="font-medium text-center w-full">网关数采</header>
|
|
|
<div
|
|
<div
|
|
|
- v-loading="specsLoading"
|
|
|
|
|
|
|
+ v-loading="dimensionLoading"
|
|
|
element-loading-background="transparent"
|
|
element-loading-background="transparent"
|
|
|
class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
|
|
class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
|
|
|
id="dimension"
|
|
id="dimension"
|
|
|
>
|
|
>
|
|
|
- <div
|
|
|
|
|
- v-for="item in gatewayspecs"
|
|
|
|
|
- :key="item.productId"
|
|
|
|
|
- class="h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
|
|
|
|
|
- :class="{
|
|
|
|
|
- 'bg-blue-200': selectSpec[item.modelName]
|
|
|
|
|
- }"
|
|
|
|
|
- @click="!disabledIdentifier.includes(item.identifier) && handleClickSpec(item.modelName)"
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="item in gatewayDimensions"
|
|
|
|
|
+ :key="item.identifier"
|
|
|
|
|
+ class="border-none h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
|
|
|
|
|
+ :class="{ 'bg-blue-200': selectedDimension[item.name] }"
|
|
|
|
|
+ :disabled="disabledDimensions.includes(item.identifier)"
|
|
|
|
|
+ @click="handleClickSpec(item.name)"
|
|
|
>
|
|
>
|
|
|
- <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
|
|
|
|
|
|
|
+ <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.name }}</span>
|
|
|
<span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
<span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div
|
|
<div
|
|
|
- v-if="zhbdspecs.length"
|
|
|
|
|
|
|
+ v-if="carDimensions.length"
|
|
|
class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow"
|
|
class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow"
|
|
|
>
|
|
>
|
|
|
<header class="font-medium text-center w-full">中航北斗</header>
|
|
<header class="font-medium text-center w-full">中航北斗</header>
|
|
|
<div class="w-full mt-4 grid grid-cols-4 gap-4" id="dimension">
|
|
<div class="w-full mt-4 grid grid-cols-4 gap-4" id="dimension">
|
|
|
- <div
|
|
|
|
|
- v-for="item in zhbdspecs"
|
|
|
|
|
- :key="item.productId"
|
|
|
|
|
- class="h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
|
|
|
|
|
- :class="{
|
|
|
|
|
- 'bg-blue-200': selectSpec[item.modelName]
|
|
|
|
|
- }"
|
|
|
|
|
- @click="!disabledIdentifier.includes(item.identifier) && handleClickSpec(item.modelName)"
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="item in carDimensions"
|
|
|
|
|
+ :key="item.identifier"
|
|
|
|
|
+ class="border-none h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
|
|
|
|
|
+ :class="{ 'bg-blue-200': selectedDimension[item.name] }"
|
|
|
|
|
+ :disabled="disabledDimensions.includes(item.identifier)"
|
|
|
|
|
+ @click="handleClickSpec(item.name)"
|
|
|
>
|
|
>
|
|
|
- <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
|
|
|
|
|
|
|
+ <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.name }}</span>
|
|
|
<span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
<span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="w-full mt-4 bg-gradient-to-r from-blue-100 to-white min-h-175 rounded-lg p-6 shadow">
|
|
<div class="w-full mt-4 bg-gradient-to-r from-blue-100 to-white min-h-175 rounded-lg p-6 shadow">
|
|
@@ -508,12 +521,11 @@ onUnmounted(() => {
|
|
|
<div class="i-material-symbols:area-chart-outline-rounded text-sky size-6" text-sky></div>
|
|
<div class="i-material-symbols:area-chart-outline-rounded text-sky size-6" text-sky></div>
|
|
|
数据趋势
|
|
数据趋势
|
|
|
</h3>
|
|
</h3>
|
|
|
-
|
|
|
|
|
<div class="flex gap-4">
|
|
<div class="flex gap-4">
|
|
|
<el-button type="primary" size="default" @click="exportChart">导出为图片</el-button>
|
|
<el-button type="primary" size="default" @click="exportChart">导出为图片</el-button>
|
|
|
<el-button size="default" @click="reset">重置</el-button>
|
|
<el-button size="default" @click="reset">重置</el-button>
|
|
|
<el-date-picker
|
|
<el-date-picker
|
|
|
- v-model="date"
|
|
|
|
|
|
|
+ v-model="selectedDate"
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
type="datetimerange"
|
|
type="datetimerange"
|
|
|
unlink-panels
|
|
unlink-panels
|
|
@@ -527,12 +539,31 @@ onUnmounted(() => {
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
</header>
|
|
</header>
|
|
|
- <div
|
|
|
|
|
- v-loading="specsLoading"
|
|
|
|
|
- element-loading-background="transparent"
|
|
|
|
|
- ref="chartRef"
|
|
|
|
|
- class="w-full h-158 mt-4 mb-4"
|
|
|
|
|
- ></div>
|
|
|
|
|
|
|
+ <div class="flex h-160 mt-4">
|
|
|
|
|
+ <div class="flex gap-1">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="item of maxmin"
|
|
|
|
|
+ :key="item.name"
|
|
|
|
|
+ class="w-8 h-full flex flex-col items-center justify-between py-2 gap-1 rounded bg-transparent border-none"
|
|
|
|
|
+ @click="handleClickSpec(item.name)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="[writing-mode:sideways-lr]">{{ item.max }}</span>
|
|
|
|
|
+ <div class="flex-1 w-1" :style="{ backgroundColor: item.color }"></div>
|
|
|
|
|
+ <span class="[writing-mode:sideways-lr]">{{ item.name }}</span>
|
|
|
|
|
+ <div class="flex-1 w-1" :style="{ backgroundColor: item.color }"></div>
|
|
|
|
|
+ <span class="[writing-mode:sideways-lr]">{{ item.min }}</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-1">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-loading="chartLoading"
|
|
|
|
|
+ element-loading-background="transparent"
|
|
|
|
|
+ ref="chartRef"
|
|
|
|
|
+ class="flex-1 h-full"
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|