|
@@ -1,332 +1,412 @@
|
|
|
-<template>
|
|
|
|
|
- <ContentWrap v-loading="formLoading">
|
|
|
|
|
- <ContentWrap>
|
|
|
|
|
- <el-form style="height:89px;margin-left: 20px;">
|
|
|
|
|
- <el-row style="display: flex;flex-direction: row; ">
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-form-item prop="deviceCode">
|
|
|
|
|
- <template #label>
|
|
|
|
|
- <span class="custom-label">资产编码:</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <span class="custom-label">{{ formData.deviceCode }}</span>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-form-item prop="deviceName">
|
|
|
|
|
- <template #label>
|
|
|
|
|
- <span class="custom-label">设备类别:</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <span class="custom-label">{{ formData.deviceName }}</span>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-form-item prop="dept">
|
|
|
|
|
- <template #label>
|
|
|
|
|
- <span class="custom-label">所在部门:</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <span class="custom-label">{{ formData.dept }}</span>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-form-item prop="ifInline">
|
|
|
|
|
- <template #label>
|
|
|
|
|
- <span class="custom-label">是否在线:</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <template #default>
|
|
|
|
|
- <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="formData.ifInline" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-form-item prop="lastInlineTime">
|
|
|
|
|
- <template #label>
|
|
|
|
|
- <span class="custom-label">最后数据时间:</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <span class="custom-label">{{ formData.lastInlineTime }}</span>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="8">
|
|
|
|
|
- <el-form-item v-if="formData.vehicle" prop="vehicle">
|
|
|
|
|
- <template #label>
|
|
|
|
|
- <span class="custom-label">车牌号码:</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- <span class="custom-label">{{ formData.vehicle }}</span>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
- </el-form>
|
|
|
|
|
- </ContentWrap>
|
|
|
|
|
- <ContentWrap>
|
|
|
|
|
- <el-row>
|
|
|
|
|
- <el-col :span="24">
|
|
|
|
|
- <TdDeviceLabel :tags="specs" @select="labelSelect" tag-width="24%" />
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
- </ContentWrap>
|
|
|
|
|
- <ContentWrap>
|
|
|
|
|
- <div class="chart-container">
|
|
|
|
|
- <!-- 图表容器 -->
|
|
|
|
|
- <el-date-picker
|
|
|
|
|
- v-model="dateRange"
|
|
|
|
|
- type="datetimerange"
|
|
|
|
|
- :default-time="[
|
|
|
|
|
- new Date(2000, 1, 1, 0, 0, 0),
|
|
|
|
|
- new Date(2000, 1, 1, 23, 59, 59)
|
|
|
|
|
- ]"
|
|
|
|
|
- start-placeholder="起始日期时间"
|
|
|
|
|
- end-placeholder="结束日期时间"
|
|
|
|
|
- format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
- value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
- @change="handleDateChange"
|
|
|
|
|
- />
|
|
|
|
|
- <div v-loading="loading" style="height: 100%" ref="chartContainer"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </ContentWrap>
|
|
|
|
|
- </ContentWrap>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
|
|
|
|
-import TdDeviceLabel from '@/views/pms/device/monitor/TdDeviceLabel.vue'
|
|
|
|
|
-import {IotDeviceApi} from "@/api/pms/device";
|
|
|
|
|
import * as echarts from 'echarts'
|
|
import * as echarts from 'echarts'
|
|
|
|
|
+import { Odometer, CircleCheckFilled } from '@element-plus/icons-vue'
|
|
|
|
|
+import { rangeShortcuts } from '@/utils/formatTime'
|
|
|
|
|
+
|
|
|
import dayjs from 'dayjs'
|
|
import dayjs from 'dayjs'
|
|
|
-import {IotStatApi} from "@/api/pms/stat";
|
|
|
|
|
-import {IotAlarmSettingApi} from "@/api/pms/alarm";
|
|
|
|
|
|
|
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
|
|
|
|
+import { IotDeviceApi, cancelAllRequests } from '@/api/pms/device'
|
|
|
|
|
+import { IotStatApi } from '@/api/pms/stat'
|
|
|
|
|
|
|
|
-const { params, name } = useRoute() // 查询参数
|
|
|
|
|
-const info = ref({})
|
|
|
|
|
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
|
|
-const id = params.id
|
|
|
|
|
defineOptions({ name: 'TdDeviceDetail' })
|
|
defineOptions({ name: 'TdDeviceDetail' })
|
|
|
-const formData = ref({
|
|
|
|
|
- deviceCode: '',
|
|
|
|
|
- deviceName: '',
|
|
|
|
|
- ifInline: undefined,
|
|
|
|
|
- lastInlineTime: '',
|
|
|
|
|
- dept:'',
|
|
|
|
|
- vehicle:''
|
|
|
|
|
|
|
+
|
|
|
|
|
+dayjs.extend(quarterOfYear)
|
|
|
|
|
+
|
|
|
|
|
+const { params } = useRoute()
|
|
|
|
|
+
|
|
|
|
|
+const data = ref({
|
|
|
|
|
+ deviceCode: params.code || '',
|
|
|
|
|
+ deviceName: params.name || '',
|
|
|
|
|
+ lastInlineTime: params.time || '',
|
|
|
|
|
+ ifInline: params.ifInline || '',
|
|
|
|
|
+ dept: params.dept || '',
|
|
|
|
|
+ vehicle: params.vehicle || ''
|
|
|
})
|
|
})
|
|
|
-const specs = ref([])
|
|
|
|
|
-
|
|
|
|
|
-// 响应式数据
|
|
|
|
|
-const startTime = ref('')
|
|
|
|
|
-const endTime = ref('')
|
|
|
|
|
-const topicName = ref([])
|
|
|
|
|
-const loading = ref(false)
|
|
|
|
|
-const topic = ref('')
|
|
|
|
|
-// 设置固定阈值
|
|
|
|
|
-
|
|
|
|
|
-const handleDateChange = async (val) => {
|
|
|
|
|
- if (val && val.length === 2) {
|
|
|
|
|
- await getChart(val)
|
|
|
|
|
- await renderChart()
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
|
|
+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 lastTsMap = ref<Record<string, number>>({})
|
|
|
|
|
|
|
|
-const defaultEnd = dayjs()
|
|
|
|
|
-const defaultStart = defaultEnd.subtract(1, 'day')
|
|
|
|
|
-const dateRange = ref([
|
|
|
|
|
- defaultStart.format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
- defaultEnd.format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
|
|
+// 每 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 labelSelect =async (row) =>{
|
|
|
|
|
- topic.value = row.identifier
|
|
|
|
|
- topicName.value = row.modelName
|
|
|
|
|
- await getChart(dateRange.value)
|
|
|
|
|
- await renderChart()
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-const chartContainer = ref(null)
|
|
|
|
|
-let chartInstance = null
|
|
|
|
|
|
|
+const reset = () => {
|
|
|
|
|
+ cancelAllRequests()
|
|
|
|
|
+ const def = rangeShortcuts[0].value()
|
|
|
|
|
|
|
|
-// 时间格式化(HH:mm)
|
|
|
|
|
-const formatTime = timestamp => {
|
|
|
|
|
- return new Date(timestamp)
|
|
|
|
|
- .toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit',second:'2-digit' })
|
|
|
|
|
- .slice(0, 5)
|
|
|
|
|
|
|
+ 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()
|
|
|
}
|
|
}
|
|
|
-const result = ref([])
|
|
|
|
|
-const getChart = async (range) =>{
|
|
|
|
|
- loading.value = true
|
|
|
|
|
- await IotStatApi.getDeviceInfoChart(params.code, topic.value, range[0], range[1]).then(res=>{
|
|
|
|
|
- result.value = res
|
|
|
|
|
- loading.value = false
|
|
|
|
|
- })
|
|
|
|
|
|
|
+
|
|
|
|
|
+const handleDateChange = () => {
|
|
|
|
|
+ cancelAllRequests()
|
|
|
|
|
+ stopAutoFetch()
|
|
|
|
|
+ if (chart) chart.clear()
|
|
|
|
|
+ render()
|
|
|
|
|
+ initLoad(false)
|
|
|
}
|
|
}
|
|
|
-// 初始化图表
|
|
|
|
|
-const renderChart = async () => {
|
|
|
|
|
- if (!chartContainer.value) return
|
|
|
|
|
- let upperLimit;
|
|
|
|
|
- let lowerLimit;
|
|
|
|
|
- await IotAlarmSettingApi.getDeviceRange(params.code, topic.value).then(res=>{
|
|
|
|
|
- if (res){
|
|
|
|
|
- if (res.maxValue){
|
|
|
|
|
- upperLimit = res.maxValue
|
|
|
|
|
- }
|
|
|
|
|
- if (res.minValue){
|
|
|
|
|
- lowerLimit = res.minValue
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+const handleClickSpec = (modelName: string) => {
|
|
|
|
|
+ selectSpec.value[modelName] = !selectSpec.value[modelName]
|
|
|
|
|
+ chart?.setOption({
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ selected: selectSpec.value
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- // 销毁旧实例
|
|
|
|
|
- if (chartInstance) chartInstance.dispose()
|
|
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const chartRef = ref<HTMLDivElement | null>(null)
|
|
|
|
|
+let chart: echarts.ECharts | null = null
|
|
|
|
|
+
|
|
|
|
|
+// 映射区间相关
|
|
|
|
|
+let intervalArr: number[] = []
|
|
|
|
|
+let maxInterval = 0
|
|
|
|
|
+let minInterval = 0
|
|
|
|
|
+
|
|
|
|
|
+// 1. 加载 specs
|
|
|
|
|
+const loadSpecs = async () => {
|
|
|
|
|
+ if (!params.id) return
|
|
|
|
|
+ const res = await IotDeviceApi.getIotDeviceTds(Number(params.id))
|
|
|
|
|
+ const zhbdres = await IotDeviceApi.getIotDeviceZHBDTds(Number(params.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) => {
|
|
|
|
|
+ acc[spec.modelName] = gatewayspecs.value.some((item) => item.modelName === spec.modelName)
|
|
|
|
|
+ return acc
|
|
|
|
|
+ },
|
|
|
|
|
+ {} as Record<string, boolean>
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ chartMap.value = specs.value
|
|
|
|
|
+ .filter((spec) => !['online', 'vehicle_name'].includes(spec.identifier))
|
|
|
|
|
+ .reduce(
|
|
|
|
|
+ (acc, spec) => {
|
|
|
|
|
+ acc[spec.identifier] = { name: spec.modelName, value: [] }
|
|
|
|
|
+ return acc
|
|
|
|
|
+ },
|
|
|
|
|
+ {} as Record<string, { name: string; value: any[] }>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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)
|
|
|
|
|
+ chartMap.value[identifier].value = sorted
|
|
|
|
|
+ lastTsMap.value[identifier] = sorted.at(-1)?.ts ?? 0
|
|
|
|
|
+
|
|
|
|
|
+ updateSingleSeries(identifier)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (real_time) startAutoFetch()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startAutoFetch = () => {
|
|
|
|
|
+ timer.value = setInterval(fetchIncrementData, 10000)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const stopAutoFetch = () => {
|
|
|
|
|
+ if (timer.value) clearInterval(timer.value)
|
|
|
|
|
+ timer.value = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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 sorted = res.sort((a, b) => a.ts - b.ts)
|
|
|
|
|
+
|
|
|
|
|
+ // push 到本地
|
|
|
|
|
+ chartMap.value[identifier].value.push(...sorted)
|
|
|
|
|
+ // 更新 lastTs
|
|
|
|
|
+ lastTsMap.value[identifier] = sorted.at(-1).ts
|
|
|
|
|
+
|
|
|
|
|
+ appendToSeries(identifier, chartMap.value[identifier].value)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const render = () => {
|
|
|
|
|
+ if (!chartRef.value) return
|
|
|
|
|
+
|
|
|
|
|
+ if (!chart) chart = echarts.init(chartRef.value)
|
|
|
|
|
+
|
|
|
|
|
+ const values = Object.values(chartMap.value).flatMap((item) => item.value.map((d) => d.value))
|
|
|
|
|
+
|
|
|
|
|
+ const maxVal = Math.max(...values)
|
|
|
|
|
+ const minVal = Math.min(...values, -100)
|
|
|
|
|
+
|
|
|
|
|
+ const maxDigits = (Math.floor(maxVal) + '').length
|
|
|
|
|
+ const minDigits = (Math.floor(Math.abs(minVal)) + '').length
|
|
|
|
|
+ const interval = Math.max(maxDigits, minDigits)
|
|
|
|
|
+
|
|
|
|
|
+ maxInterval = interval
|
|
|
|
|
+ minInterval = minDigits
|
|
|
|
|
|
|
|
- chartInstance = markRaw(echarts.init(chartContainer.value))
|
|
|
|
|
|
|
+ intervalArr = [0]
|
|
|
|
|
+ for (let i = 1; i <= interval; i++) {
|
|
|
|
|
+ intervalArr.push(Math.pow(10, i))
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const option = {
|
|
|
|
|
- title: {
|
|
|
|
|
- text: topicName.value + '数据趋势',
|
|
|
|
|
- left: 'center',
|
|
|
|
|
|
|
+ chart.setOption({
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ formatter: (params) => {
|
|
|
|
|
+ let d = `${params[0].axisValueLabel}<br>`
|
|
|
|
|
+ let item = params.map((el) => `${el.marker} ${el.seriesName}: ${el.value[2]}<br>`)
|
|
|
|
|
+ return d + item.join('')
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
- tooltip: { trigger: 'axis' },
|
|
|
|
|
|
|
+ dataZoom: [
|
|
|
|
|
+ { type: 'inside', xAxisIndex: 0 },
|
|
|
|
|
+ { type: 'slider', xAxisIndex: 0 }
|
|
|
|
|
+ ],
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
- type: 'category',
|
|
|
|
|
- data: result.value.map(d => dayjs(d.timestamp).format('YYYY-MM-DD HH:mm:ss')),
|
|
|
|
|
- axisLabel: { rotate: 45 },
|
|
|
|
|
- inverse: true,
|
|
|
|
|
|
|
+ type: 'time',
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ formatter: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
+ rotate: 20
|
|
|
|
|
+ },
|
|
|
|
|
+ inverse: true
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
- // 根据固定阈值和实际数据调整Y轴范围,使阈值线更清晰
|
|
|
|
|
- // min: Math.min(lowerLimit * 0.9, ...result.value.map(d => d.value || 0)),
|
|
|
|
|
- // max: Math.max(upperLimit * 1.03, ...result.value.map(d => d.value || 0))
|
|
|
|
|
|
|
+ min: -minInterval,
|
|
|
|
|
+ max: maxInterval,
|
|
|
|
|
+ interval: 1,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ formatter: (v) => (v === 0 ? '0' : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v))
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ data: Object.values(chartMap.value).map((i) => i.name),
|
|
|
|
|
+ selected: selectSpec.value
|
|
|
},
|
|
},
|
|
|
- dataZoom: [{
|
|
|
|
|
- type: 'slider',
|
|
|
|
|
- xAxisIndex: 0,
|
|
|
|
|
- start: 0,
|
|
|
|
|
- end: 100
|
|
|
|
|
- }],
|
|
|
|
|
|
|
+ series: Object.keys(chartMap.value).map((identifier) => ({
|
|
|
|
|
+ name: chartMap.value[identifier].name,
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ smooth: true,
|
|
|
|
|
+ showSymbol: false,
|
|
|
|
|
+ data: [] // 占位数组
|
|
|
|
|
+ }))
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const updateSingleSeries = (identifier: string) => {
|
|
|
|
|
+ if (!chart) render()
|
|
|
|
|
+ if (!chart) return
|
|
|
|
|
+
|
|
|
|
|
+ const idx = Object.keys(chartMap.value).indexOf(identifier)
|
|
|
|
|
+ if (idx === -1) return
|
|
|
|
|
+
|
|
|
|
|
+ const data = chartMap.value[identifier].value.map((v) => mapData(v))
|
|
|
|
|
+
|
|
|
|
|
+ chart.setOption({
|
|
|
series: [
|
|
series: [
|
|
|
- // 原始数据曲线
|
|
|
|
|
- {
|
|
|
|
|
- data: result.value.map(d => d.value),
|
|
|
|
|
- type: 'line',
|
|
|
|
|
- smooth: true,
|
|
|
|
|
- name: '实时数据',
|
|
|
|
|
- lineStyle: { color: '#409eff' }
|
|
|
|
|
- },
|
|
|
|
|
- // 上限阈值线(固定100)
|
|
|
|
|
{
|
|
{
|
|
|
- data: result.value.map(() => upperLimit),
|
|
|
|
|
- type: 'line',
|
|
|
|
|
- name: '上限阈值',
|
|
|
|
|
- lineStyle: {
|
|
|
|
|
- color: '#f56c6c', // 红色虚线
|
|
|
|
|
- type: 'dashed'
|
|
|
|
|
- },
|
|
|
|
|
- symbol: 'none', // 不显示数据点
|
|
|
|
|
- emphasis: { disabled: true } // 禁用悬停高亮
|
|
|
|
|
- },
|
|
|
|
|
- // 下限阈值线(固定95)
|
|
|
|
|
|
|
+ name: chartMap.value[identifier].name,
|
|
|
|
|
+ data
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const appendToSeries = (identifier, list) => {
|
|
|
|
|
+ if (!chart) return
|
|
|
|
|
+
|
|
|
|
|
+ const idx = Object.keys(chartMap.value).indexOf(identifier)
|
|
|
|
|
+ if (idx === -1) return
|
|
|
|
|
+
|
|
|
|
|
+ const data = list.map(mapData)
|
|
|
|
|
+
|
|
|
|
|
+ chart.setOption({
|
|
|
|
|
+ series: [
|
|
|
{
|
|
{
|
|
|
- data: result.value.map(() => lowerLimit),
|
|
|
|
|
- type: 'line',
|
|
|
|
|
- name: '下限阈值',
|
|
|
|
|
- lineStyle: {
|
|
|
|
|
- color: '#e6a23c', // 橙色虚线
|
|
|
|
|
- type: 'dashed'
|
|
|
|
|
- },
|
|
|
|
|
- symbol: 'none',
|
|
|
|
|
- emphasis: { disabled: true }
|
|
|
|
|
|
|
+ name: chartMap.value[identifier].name,
|
|
|
|
|
+ data
|
|
|
}
|
|
}
|
|
|
- ],
|
|
|
|
|
- // 添加图例显示各线条含义
|
|
|
|
|
- legend: {
|
|
|
|
|
- data: ['实时数据', '上限阈值', '下限阈值'],
|
|
|
|
|
- top: 30
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ]
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const mapData = ({ value, ts }) => {
|
|
|
|
|
+ if (value === 0) return [ts, 0, 0]
|
|
|
|
|
|
|
|
- chartInstance.setOption(option)
|
|
|
|
|
|
|
+ const isPositive = value > 0
|
|
|
|
|
+ const absItem = Math.abs(value)
|
|
|
|
|
|
|
|
- // 窗口自适应
|
|
|
|
|
- window.addEventListener('resize', () => chartInstance.resize())
|
|
|
|
|
|
|
+ const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
|
|
|
|
|
+ const min_index = intervalArr.findIndex((v) => v === min_value)
|
|
|
|
|
+
|
|
|
|
|
+ const new_value =
|
|
|
|
|
+ (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
|
|
|
|
|
+
|
|
|
|
|
+ return [ts, isPositive ? new_value : -new_value, value]
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
- formLoading.value = true
|
|
|
|
|
- formData.value.deviceCode = params.code
|
|
|
|
|
- formData.value.deviceName = params.name
|
|
|
|
|
- formData.value.lastInlineTime = params.time
|
|
|
|
|
- formData.value.ifInline = params.ifInline
|
|
|
|
|
- formData.value.dept = params.dept
|
|
|
|
|
- formData.value.vehicle = params.vehicle
|
|
|
|
|
- await IotDeviceApi.getIotDeviceTds(id).then(res => {
|
|
|
|
|
- specs.value = res
|
|
|
|
|
- specs.value = specs.value.sort((a, b) => {
|
|
|
|
|
- return b.modelOrder - a.modelOrder
|
|
|
|
|
- })
|
|
|
|
|
- formLoading.value = false
|
|
|
|
|
- topic.value = specs.value[0].identifier
|
|
|
|
|
- topicName.value = specs.value[0].modelName
|
|
|
|
|
- })
|
|
|
|
|
- await getChart(dateRange.value)
|
|
|
|
|
- await renderChart()
|
|
|
|
|
|
|
+ await loadSpecs()
|
|
|
|
|
+ render()
|
|
|
|
|
+ initLoad()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ stopAutoFetch()
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
-<style scoped lang="scss">
|
|
|
|
|
-.container {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- margin: 20px auto;
|
|
|
|
|
- padding: 24px;
|
|
|
|
|
- //background: #f8f9fa;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
-}
|
|
|
|
|
-.chart-container {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 600px;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-.date-controls {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 15px;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="w-full bg-gradient-to-r from-blue-100 to-white rounded-lg p-6 shadow"
|
|
|
|
|
+ id="td-device-info"
|
|
|
|
|
+ >
|
|
|
|
|
+ <h2 class="flex items-center gap-2">
|
|
|
|
|
+ <el-icon class="text-sky!"><Odometer /></el-icon> 设备基础信息
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <el-form size="large" label-position="top" class="mt-4 grid grid-cols-3 gap-4">
|
|
|
|
|
+ <el-form-item label="资产编码"> {{ data.deviceCode }} </el-form-item>
|
|
|
|
|
+ <el-form-item label="设备类别"> {{ data.deviceName }} </el-form-item>
|
|
|
|
|
+ <el-form-item label="所在部门"> {{ data.dept }} </el-form-item>
|
|
|
|
|
+ <el-form-item label="是否在线" class="online" type="plain">
|
|
|
|
|
+ <el-tag type="success" size="default" class="flex items-center">
|
|
|
|
|
+ <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
|
|
|
|
|
+ 在线
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="最后数据时间"> {{ data.lastInlineTime }} </el-form-item>
|
|
|
|
|
+ <el-form-item v-if="data.vehicle" label="车牌号码"> {{ data.vehicle }} </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div 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>
|
|
|
|
|
+ <div class="w-full mt-4 grid grid-cols-4 gap-4" 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="handleClickSpec(item.modelName)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
|
|
|
|
|
+ <span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div 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>
|
|
|
|
|
+ <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="handleClickSpec(item.modelName)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
|
|
|
|
|
+ <span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
|
|
|
+ </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">
|
|
|
|
|
+ <header class="flex items-center justify-between">
|
|
|
|
|
+ <h3 class="flex items-center gap-2">
|
|
|
|
|
+ <div class="i-material-symbols:area-chart-outline-rounded text-sky size-6" text-sky></div>
|
|
|
|
|
+ 数据趋势
|
|
|
|
|
+ </h3>
|
|
|
|
|
|
|
|
-input[type="datetime-local"] {
|
|
|
|
|
- padding: 8px 12px;
|
|
|
|
|
- border: 1px solid #dcdfe6;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- transition: border-color 0.2s;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ <div class="flex gap-4">
|
|
|
|
|
+ <el-button type="primary" size="default" @click="reset">重置</el-button>
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="date"
|
|
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
+ type="datetimerange"
|
|
|
|
|
+ unlink-panels
|
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
|
+ :shortcuts="rangeShortcuts"
|
|
|
|
|
+ size="default"
|
|
|
|
|
+ class="w-100!"
|
|
|
|
|
+ placement="bottom-end"
|
|
|
|
|
+ @change="handleDateChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+ <div ref="chartRef" class="w-full h-158 mt-4 mb-4"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
|
|
|
-input[type="datetime-local"]:focus {
|
|
|
|
|
- border-color: #409eff;
|
|
|
|
|
- outline: none;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+:deep(.el-form-item) {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
|
|
|
-.separator {
|
|
|
|
|
- color: #606266;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ .el-form-item__label {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-.query-btn {
|
|
|
|
|
- padding: 8px 20px;
|
|
|
|
|
- background: #409eff;
|
|
|
|
|
- color: white;
|
|
|
|
|
- border: none;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition: opacity 0.2s;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ .el-form-item__content {
|
|
|
|
|
+ font-size: 1rem;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-.query-btn:hover {
|
|
|
|
|
- opacity: 0.8;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ &.online {
|
|
|
|
|
+ .el-form-item__content {
|
|
|
|
|
+ height: 2.5rem;
|
|
|
|
|
|
|
|
-//.chart {
|
|
|
|
|
-// width: 100%;
|
|
|
|
|
-// height: 500px;
|
|
|
|
|
-// margin-top: 20px;
|
|
|
|
|
-//}
|
|
|
|
|
-.custom-label{
|
|
|
|
|
- font-size: 17px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
|
|
+ .el-tag__content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|