|
|
@@ -0,0 +1,275 @@
|
|
|
+<script setup lang="ts">
|
|
|
+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 { IotStatApi } from '@/api/pms/stat'
|
|
|
+
|
|
|
+import { use } from 'echarts/core'
|
|
|
+import { LineChart } from 'echarts/charts'
|
|
|
+import {
|
|
|
+ TitleComponent,
|
|
|
+ TooltipComponent,
|
|
|
+ LegendComponent,
|
|
|
+ GridComponent
|
|
|
+} from 'echarts/components'
|
|
|
+import { CanvasRenderer } from 'echarts/renderers'
|
|
|
+
|
|
|
+use([TitleComponent, TooltipComponent, LegendComponent, GridComponent, LineChart, CanvasRenderer])
|
|
|
+
|
|
|
+defineOptions({ name: 'TdDeviceDetail' })
|
|
|
+
|
|
|
+const { query } = useRoute()
|
|
|
+
|
|
|
+const data = ref({
|
|
|
+ deviceCode: query.code || '',
|
|
|
+ deviceName: query.name || '',
|
|
|
+ lastInlineTime: query.time || '',
|
|
|
+ ifInline: query.ifInline || '',
|
|
|
+ dept: query.dept || '',
|
|
|
+ vehicle: query.vehicle || '',
|
|
|
+ carOnline: query.carOnline || ''
|
|
|
+})
|
|
|
+
|
|
|
+// 首先加载 维度 分为两种一种 网关维度 一种中航北斗维度
|
|
|
+interface Dimension {
|
|
|
+ name: string
|
|
|
+ identifier: string
|
|
|
+ value: string
|
|
|
+}
|
|
|
+
|
|
|
+const dimensions = ref<Dimension[]>([])
|
|
|
+const gatewayDimensions = ref<Dimension[]>([])
|
|
|
+const carDimensions = ref<Dimension[]>([])
|
|
|
+
|
|
|
+// 一部分值不能展示在图表上
|
|
|
+const disabledDimensions = ['车牌号码', '是否在线']
|
|
|
+
|
|
|
+// 选中的维度
|
|
|
+const selectedDimensions = ref<{ [key: Dimension['name']]: boolean }>({})
|
|
|
+
|
|
|
+const dimensionLoading = ref(false)
|
|
|
+
|
|
|
+const loadDimensions = async () => {
|
|
|
+ if (!query.id) return
|
|
|
+ dimensionLoading.value = true
|
|
|
+
|
|
|
+ // 网关维度
|
|
|
+ const gateway = (((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as any[]) ?? [])
|
|
|
+ .sort((a, b) => b.modelOrder - a.modelOrder)
|
|
|
+ .map((v) => ({ name: v.modelName, identifier: v.identifier, value: v.value }))
|
|
|
+
|
|
|
+ // 中航北斗维度
|
|
|
+ const car = (((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as any[]) ?? [])
|
|
|
+ .sort((a, b) => b.modelOrder - a.modelOrder)
|
|
|
+ .map((v) => ({ name: v.modelName, identifier: v.identifier, value: v.value }))
|
|
|
+
|
|
|
+ // 合并维度 过滤不可展示维度
|
|
|
+ dimensions.value = [...gateway, ...car].filter((v) => !disabledDimensions.includes(v.name))
|
|
|
+ gatewayDimensions.value = gateway
|
|
|
+ carDimensions.value = car
|
|
|
+
|
|
|
+ // 生成 chart legend selected
|
|
|
+ selectedDimensions.value = Object.fromEntries(dimensions.value.map((v) => [v.name, false]))
|
|
|
+
|
|
|
+ // 默认选中第一个
|
|
|
+ selectedDimensions.value[dimensions.value[0].name] = true
|
|
|
+
|
|
|
+ dimensionLoading.value = false
|
|
|
+
|
|
|
+ chartData.value = Object.fromEntries(dimensions.value.map((v) => [v.name, []]))
|
|
|
+}
|
|
|
+
|
|
|
+// 时间范围
|
|
|
+const dateRange = ref<string[]>([
|
|
|
+ ...rangeShortcuts[3].value().map((v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
+])
|
|
|
+
|
|
|
+// 获取图表数据
|
|
|
+const chartData = ref<{ [key: Dimension['name']]: number[][] }>({})
|
|
|
+
|
|
|
+const loadChartData = async () => {
|
|
|
+ if (!dimensions.value.length) return
|
|
|
+
|
|
|
+ chartData.value = Object.fromEntries(dimensions.value.map((v) => [v.name, []]))
|
|
|
+
|
|
|
+ for (const { name, identifier } of dimensions.value) {
|
|
|
+ const res = await IotStatApi.getDeviceInfoChart(
|
|
|
+ data.value.deviceCode,
|
|
|
+ identifier,
|
|
|
+ dateRange.value[0],
|
|
|
+ dateRange.value[1]
|
|
|
+ )
|
|
|
+
|
|
|
+ const sorted = res.sort((a, b) => a.ts - b.ts)
|
|
|
+ const values = sorted.map((v) => v.value)
|
|
|
+
|
|
|
+ // --- 归一化参数 ---
|
|
|
+ const min = Math.min(...values)
|
|
|
+ const max = Math.max(...values)
|
|
|
+ const range = max - min || 1 // 防止除 0
|
|
|
+
|
|
|
+ const seriesData = sorted.map((v) => {
|
|
|
+ const norm = (v.value - min) / range
|
|
|
+ return [v.ts, norm, v.value] // ts, 归一化值, 原始值
|
|
|
+ })
|
|
|
+
|
|
|
+ chartData.value[name] = seriesData
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('chartData :>> ', Object.values(chartData.value))
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadDimensions().then(() => {
|
|
|
+ loadChartData()
|
|
|
+ })
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<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
|
|
|
+ v-if="data.ifInline === '3'"
|
|
|
+ type="success"
|
|
|
+ size="default"
|
|
|
+ class="flex items-center"
|
|
|
+ >
|
|
|
+ <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
|
|
|
+ 在线
|
|
|
+ </el-tag>
|
|
|
+
|
|
|
+ <el-tag v-if="data.ifInline === '4'" type="danger" size="default" class="flex items-center">
|
|
|
+ <el-icon class="text-rose"><CircleCloseFilled /></el-icon>
|
|
|
+ 离线
|
|
|
+ </el-tag>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="data.carOnline" label="中航北斗" class="online" type="plain">
|
|
|
+ <el-tag
|
|
|
+ v-if="data.carOnline === 'true'"
|
|
|
+ type="success"
|
|
|
+ size="default"
|
|
|
+ class="flex items-center"
|
|
|
+ >
|
|
|
+ <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
|
|
|
+ 在线
|
|
|
+ </el-tag>
|
|
|
+
|
|
|
+ <el-tag
|
|
|
+ v-if="data.carOnline === 'false'"
|
|
|
+ type="danger"
|
|
|
+ size="default"
|
|
|
+ class="flex items-center"
|
|
|
+ >
|
|
|
+ <el-icon class="text-rose"><CircleCloseFilled /></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 min-h-50">
|
|
|
+ <header class="font-medium text-center w-full">网关数采</header>
|
|
|
+ <div
|
|
|
+ v-loading="dimensionLoading"
|
|
|
+ element-loading-background="transparent"
|
|
|
+ class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
|
|
|
+ id="dimension"
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ v-for="item in gatewayDimensions"
|
|
|
+ :key="item.identifier"
|
|
|
+ class="outline-none 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': selectedDimensions[item.name] }"
|
|
|
+ :disabled="disabledDimensions.includes(item.name)"
|
|
|
+ >
|
|
|
+ <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.name }}</span>
|
|
|
+ <span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <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>
|
|
|
+ <div
|
|
|
+ v-loading="dimensionLoading"
|
|
|
+ element-loading-background="transparent"
|
|
|
+ class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
|
|
|
+ id="dimension"
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ v-for="item in carDimensions"
|
|
|
+ :key="item.identifier"
|
|
|
+ class="outline-none 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': selectedDimensions[item.name] }"
|
|
|
+ :disabled="disabledDimensions.includes(item.name)"
|
|
|
+ >
|
|
|
+ <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.name }}</span>
|
|
|
+ <span class="text-lg font-medium ms-a">{{ item.value }}</span>
|
|
|
+ </button>
|
|
|
+ </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>
|
|
|
+
|
|
|
+ <div class="flex gap-4">
|
|
|
+ <!-- <el-button type="primary" size="default" @click="exportChart">导出为图片</el-button>
|
|
|
+ <el-button size="default" @click="reset">重置</el-button> -->
|
|
|
+ <el-date-picker
|
|
|
+ v-model="dateRange"
|
|
|
+ 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"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+:deep(.el-form-item) {
|
|
|
+ margin-bottom: 0;
|
|
|
+
|
|
|
+ .el-form-item__label {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-form-item__content {
|
|
|
+ font-size: 1rem;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.online {
|
|
|
+ .el-form-item__content {
|
|
|
+ height: 2.5rem;
|
|
|
+
|
|
|
+ .el-tag__content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|